mieki256's diary



2022/08/05(金) [n年前の日記]

#1 [pygame][xscreensaver] pygameを使ってxscreensaver用スクリーンセーバを作成

Python + pygame を使って xscreensaver用のスクリーンセーバを作れないか試していた。


環境は、Ubuntu Linux 20.04 LTS + Python 2.7.18 + pygame 1.9.61 + xscreensaver 6.04 + xwininfo 1.1.5。

ちなみに、Ubuntu Linux は Windows10 x64 21H2 + VMware Player上で動かしてる。

実行結果は以下。

制約。 :

pygame を使って xscreensaver用スクリーンセーバを作成する場合、現時点では制約があるっぽい。pygame は 1.x.x を使わないといけない。pygame 2.x.x では動作しない可能性が高い。

何故かと言うと…。
  • pygame 1.x.x は SDL 1.x を利用して描画その他を行う。
  • pygame 2.x.x は SDL 2.x を利用して描画その他を行う。
  • SDL 1.x なら、環境変数 SDL_VIDEO_DRIVER と SDL_WINDOWID を設定することで、SDL の動作を制御することができる。
  • SDL 2.x は、環境変数 SDL_VIDEO_DRIVER と SDL_WINDOWID を設定しても反映されない。らしい。確認してないけどそういう話を見かけた。

_pygameでxscreensaver用スクリーンセーバを作れないものか - mieki256's diary
_pygameでスクリーンセーバを作りたいのだけど - mieki256's diary

  • Ubuntu Linux 20.04 LTS は、python*-pygame で pygame 1.9.6 がインストールされるので大丈夫。
  • Ubuntu Linux 22.04 LTS は、python3-pygame で pygame 2.1.2 がインストールされるので、今回のスクリプトはおそらく動かない。

もう一つ。今回のスクリプトを動かすには xwininfo というツールが必要になる。Ubuntu Linux の場合、x11-utils というパッケージに入ってる。

※ 2022/08/06追記。Ubuntu Linux 22.04 LTS を VMware Player上でインストールして動作確認してみたところ、やはり Ubuntu 22.04 + pygame 2.1.2 では正常動作しなかった。

必要なパッケージのインストール。 :

Ubuntu Linux 20.04 LTS の場合、以下で pygame 1.9.6 をインストールできる。
sudo apt install python-pygame
sudo apt install python3-pygame
  • python-pygame は、Python 2.7 用のパッケージ。
  • python3-pygame は、Python 3.x 用のパッケージ。

xwininfo は、以下でインストールできる。
sudo apt install x11-utils

ソース。 :

ソースは以下。処理内容は、ウインドウ内でいくつかのボールが跳ね回るだけのもの。

_xscrsavpygame.py
import os
import sys
import math
import platform
import random
import subprocess
import re

IMG_FILE = "ball_64x64.png"
FPS = 60


class Ball:

    def __init__(self, img, scrw, scrh, t):
        self.img = img
        self.img_w = img.get_width()
        self.img_h = img.get_height()
        self.scrw = scrw
        self.scrh = scrh

        self.x = random.randint(16, scrw - self.img_w - 16)
        self.y = random.randint(16, scrh - self.img_h - 16)

        ang = random.randint(0, 360)
        spd = float(scrw) / t
        self.dx = spd * math.cos(math.radians(ang))
        self.dy = spd * math.sin(math.radians(ang))

    def update(self):
        self.x += self.dx
        self.y += self.dy
        if self.x <= 0 or self.x >= self.scrw - self.img_w:
            self.dx *= -1
        if self.y <= 0 or self.y >= self.scrh - self.img_h:
            self.dy *= -1

    def draw(self, screen):
        screen.blit(self.img, (self.x, self.y))


def main():
    if platform.system() == "Windows":
        os.environ["SDL_VIDEODRIVER"] = "windib"
        print("$SDL_VIDEODRIVER = %s" % os.environ["SDL_VIDEODRIVER"])
    else:
        # Linux or Darwin
        # os.environ["SDL_VIDEODRIVER"] = "x11"
        pass

    if "XSCREENSAVER_WINDOW" in os.environ:
        # xscreensaver
        hwnd = os.environ["XSCREENSAVER_WINDOW"]
        os.environ["SDL_WINDOWID"] = str(int(hwnd, 16))
        # print("$XSCREENSAVER_WINDOW = %s" % os.environ["XSCREENSAVER_WINDOW"])
        # print("$SDL_WINDOWID = %s" % os.environ["SDL_WINDOWID"])

        # get window size using xwininfo
        cmd = ["xwininfo", "-id", os.environ["XSCREENSAVER_WINDOW"]]
        p = subprocess.Popen(
            cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        ret = str(p.communicate())
        size_info = re.search('Width:\s+(\d+)[^H]+Height:\s+(\d+)', ret)
        w, h = size_info.groups()
        scrw = int(w)
        scrh = int(h)
    else:
        # not xscreensaver
        hwnd = None
        scrw, scrh = 512, 256

    import pygame

    # pygame.init()
    pygame.display.init()

    # print("driver = %s" % pygame.display.get_driver())
    # print("pygame.display.get_wm_info()")
    # print(pygame.display.get_wm_info())
    # print("pygame.display.Info()")
    # print(pygame.display.Info())

    screen = pygame.display.set_mode((scrw, scrh))

    # pygame.display.set_caption('xscreensaver by pygame')

    # load image
    base_dir = os.path.realpath(os.path.dirname(__file__))
    file_path = os.path.join(base_dir, IMG_FILE)

    img = pygame.image.load(file_path).convert()
    img.set_colorkey(img.get_at((0, 0)), pygame.RLEACCEL)

    # create balls
    balls = []
    for i in range(16):
        balls.append(Ball(img, scrw, scrh, 60.0 * 3.0))

    clock = pygame.time.Clock()
    looping = True

    while looping:
        # main loop

        # event check
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                looping = False
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_ESCAPE or event.key == pygame.K_q:
                    looping = False

        if not looping:
            break

        # move balls
        for obj in balls:
            obj.update()

        # fill bg
        screen.fill((0, 0, 0))

        # draw balls
        for obj in balls:
            obj.draw(screen)

        pygame.display.flip()
        clock.tick(FPS)

    pygame.quit()
    sys.exit()


if __name__ == '__main__':
    main()

使用画像は以下。

_ball_64x64.png
ball_64x64.png


まずは xscreensaver を経由させずに動作確認してみる。
  • xscrsavpygame.py と ball_64x64.png を任意の場所に置く。
  • chmod +x xscrsavpygame.py で実行権限をつける。
  • ./xscrsavpygame.py と打てば、pygameのウインドウが開いてボールが跳ね回るはず。

xscreensaver を経由させて実行してみる。xscreensaver の設定ファイル、~/.xscreensaver を編集。

programs:                                                                     \
                 "PYGAMESAVER"                                                \
                                  /home/USERNAME/hoge/fuga/xscrsavpygame.py \n\
                                maze -root                                  \n\
  • TAB幅は8文字、TAB文字有効で編集を行う。
  • 「programs:」以下に、追加するスクリーンセーバの名前と、Pythonスクリプトのパスを記述。
  • 行の最後が「\」なら継続行、「\n\」ならスクリーンセーバ1つ分の記述が終わったことを示す、のだと思う。

xscreensaverの設定画面を表示して、上記で追加した「PYGAMESAVER」を選ぶと、プレビュー画面の中でもボールが跳ね回ってくれた。「プレビュー」ボタンをクリックしたら、フルスクリーンでテスト表示された。

これで、C/C++ を使わずに Python + pygame 1.x.x を使っても、xscreensaver用のスクリーンセーバを書けそうだと分かった。

環境変数 XSCREENSAVER_WINDOW について。 :

以下、スクリプトソースについて少々解説。

環境変数 XSCREENSAVER_WINDOW 関係は、ruby-gtk2 で作ったスクリーンセーバの解説が参考になるかと。

_ruby-gtk2を使ってxscreensaver用スクリーンセーバを作る

環境変数 XSCREENSAVER_WINDOW が存在するかどうかは、以下の記述で調べられる。

if "XSCREENSAVER_WINDOW" in os.environ:
    # xscreensaver
    hwnd = os.environ["XSCREENSAVER_WINDOW"]
else:
    # not xscreensaver
    pass

環境変数 SDL_WINDOWID について。 :

pygame 1.x.x は、環境変数 SDL_WINDOWID にウインドウハンドルを設定してある状態で pygame.display.init() もしくは pygame.init() を呼ぶと、SDL_WINDOWID で指定されたウインドウで pygame のウインドウ(SDL 1.x のウインドウ)を作成してくれる。

また、環境変数 SDL_VIDEODRIVER に x11 や windib 等を設定すれば、どのビデオドライバーを使って描画するかを変更できる。まあ、pygame 1.x.x の場合、Linux は x11、Windows は windib がデフォルトで利用されるようではあるけれど…。

ただし、注意点がある。環境変数 SDL_WINDOWID や SDL_VIDEODRIVER の設定は、import pygame を呼ぶ前にしておかないといけない。import pygame を呼んでから、それらの環境変数を変更しても反映されない、らしい。試してないけど。

ウインドウサイズの取得について。 :

指定されたウインドウハンドル(ウインドウID)のウインドウから、サイズ(横幅と縦幅)を取得したかったのだけど、Python で取得する方法が分からなくて、かなりハマった…。先日書いた条件に、もう一つ条件を追加しないといかんなと…。

  • 「環境変数 XSCREENSAVER_WINDOW があるかどうかを調べることができて」
  • 「XSCREENSAVER_WINDOW で指定されたウインドウハンドルを使って描画ができて」
  • 「XSCREENSAVER_WINDOW で指定されたウインドウのサイズを取得できる」 ← New!
そんな言語/フレームワーク/プログラムなら、xscreensaver用スクリーンセーバが作れますよ、みたいな。

ググってみた感じでは、pygame で環境変数 SDL_WINDOWID を使う事例として、「tkinter の Frame の中に pygame のウインドウを埋め込むサンプル」がほとんどだったのだけど。どのサンプルもウインドウサイズを事前に固定してるものばかり。どうやら Python のみを使ってウインドウサイズを取得するのは難しい模様。

Python でダメなら、他のツールではどうだろう。X11関連ツールの中にウインドウ情報を取得するツールがあったりしないか ―― と探してみたら、xwininfo というツールがあると知った。

_Ubuntu xwininfoコマンド その1 - ウィンドウの情報を調べる - kledgeb

このツールを Python から呼び出せば目的を果たせるのではないか。ググってみたら、そのものズバリ、Python から xwininfo を利用する事例が以下で紹介されていた。ありがたや。

_pythonで端末画面を保存する。 - Kinaconの技術ブログ
_pythonでウィンドウ座標を取得する - キラキラするのが筋トレです

そんなわけで、環境変数 XSCREENSAVER_WINDOW で指定されたウインドウのサイズを取得する記述は以下になる。

import subprocess
import re

# get window size using xwininfo
cmd = ["xwininfo", "-id", os.environ["XSCREENSAVER_WINDOW"]]
p = subprocess.Popen(
    cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
ret = str(p.communicate())
size_info = re.search('Width:\s+(\d+)[^H]+Height:\s+(\d+)', ret)
w, h = size_info.groups()
scrw = int(w)
scrh = int(h)

画像ファイル読み込みについて。 :

pygameスクリプトから画像を読み込むあたりでもハマった。スクリプトが置いてある場所をカレントディレクトリにした状態で動作確認している分には動くのだけど、別のカレントディレクトリから実行した際に「画像ファイルが読み込めない」と言われてしまった。

どうやら、スクリプトの置いてあるディレクトリを取得して、それを利用して画像のファイルパスを求めないといかんようだなと…。記述は以下。

import os
import pygame

# load image
base_dir = os.path.realpath(os.path.dirname(__file__))
file_path = os.path.join(base_dir, "hoge.png")

img = pygame.image.load(file_path).convert()
img.set_colorkey(img.get_at((0, 0)), pygame.RLEACCEL)
  • __file__ にスクリプトのファイルパスが入っているから、それを利用してディレクトリを求める。
  • os.path.join() で、ディレクトリと画像ファイル名を結合する。

_Pythonで実行中のファイルの場所(パス)を取得する__file__ | note.nkmk.me
_Python スクリプトが格納されているディレクトリのパスを取得する - まくまくPythonノート

余談。 :

pygame 2.x.x (Ubuntu Linux 22.04 LTS) では本当に動かないのかどうかがちょっと気になる。Ubuntu 22.04 LTS をどこかにインストールして実験してみないとダメかな…。

以上、1 日分です。

過去ログ表示

Prev - 2022/08 - Next
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31

カテゴリで表示

検索機能は Namazu for hns で提供されています。(詳細指定/ヘルプ


注意: 現在使用の日記自動生成システムは Version 2.19.6 です。
公開されている日記自動生成システムは Version 2.19.5 です。

Powered by hns-2.19.6, HyperNikkiSystem Project