mieki256's diary



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

#2 [pygame] tkinterの中にpygameを埋め込む

Windows10 x64 21H2 + Python 2.7.18 + pygame 1.9.6 を使って、tkinter のウインドウの中に pygame のウインドウを埋め込めそうか実験してみた。


実行結果は以下。tkinter のウインドウの中に、pygame のウインドウ(青い部分)が表示されて、赤いボールが跳ね回ってる。ボタンをクリックするとボールの座標が初期化される。

ss_01_embed_pygamewindow.gif

必要なモジュールのインストール。 :

Windows10 x64 21H2 + Python 2.7.18 上で、pip install pygame と打って pygame をインストールしようとすると、現時点では pygame 2.0.3 がインストールされてしまう。 *1

しかし、pygame 2.x.x は、環境変数 SDL_WINDOWID を使った制御ができないので、今回はそのあたりの制御ができる pygame 1.9.6 をインストールしないといけない。

以下は、pygame をアンインストールしてから、pygame 1.9.6 をインストールする例。
pip uninstall pygame
pip install pygame==1.9.6

pygame のバージョン確認は以下。
> python -c "import pygame; print(pygame.version.vernum);"
pygame 1.9.6
Hello from the pygame community. https://www.pygame.org/contribute.html
1.9.6

_コマンドラインからpygameバージョンを調べる - Qiita

それとは別に。これは Windows限定の話だけど、今回、win32gui というモジュールを利用してウインドウのサイズ(横幅と縦幅)を取得してみた。win32gui は pywin32 というモジュールの中に入っているらしいので、pywin32 をインストールしないといけない。

pip install pywin32
> pip install pywin32
...
Collecting pywin32
  Using cached pywin32-228-cp27-cp27m-win32.whl (6.9 MB)
Installing collected packages: pywin32
Successfully installed pywin32-228

pywin32 228 がインストールされた。

ソース。 :

ソースは以下。処理内容としては、tkinter のウインドウの中に pygame のウインドウが埋め込まれて、赤いボールが跳ね回るだけ。

_01_embed_pygamewindow.py
"""
Windows10 x64 21H2 + Python 2.7.18 32bit + pygame 1.9.6 + tkinter
"""

import Tkinter
import os
import sys
import platform


w, h = 640, 360
x, y = (w / 2), (h / 2)
dx = float(w) / (60 * 1.0)
dy = float(h) / (60 * 1.5)


def update_pygame_window():
    global x, y, dx, dy, screen, root

    w = screen.get_width()
    h = screen.get_height()
    x += dx
    y += dy
    r = 24
    if x <= r or x >= w - r:
        dx *= -1
    if y <= r or y >= h - r:
        dy *= -1
    
    # draw pygame window
    screen.fill(pygame.Color(20, 80, 160))
    pygame.draw.circle(screen, pygame.Color(255, 0, 0), (int(x), int(y)), r)

    # Update the pygame display
    pygame.display.flip()

    root.after(16, update_pygame_window)


def reset_pos():
    global x, y, w, h
    x, y = (w / 2), (h / 2)


# init tkinter widget
root = Tkinter.Tk()
root.title("Embed pygame window in tkinter")

embed = Tkinter.Frame(root, width=w, height=h)
embed.pack()

btn = Tkinter.Button(root, text="Reset Position", command=reset_pos)
btn.pack()

root.update()
root.update_idletasks()

# set SDL environ
hwnd = embed.winfo_id()
os.environ['SDL_WINDOWID'] = str(hwnd)

if platform.system() == "Windows":
    # Windows
    os.environ['SDL_VIDEODRIVER'] = 'windib'

    # get window size (Windows only)
    import win32gui
    rect = win32gui.GetWindowRect(hwnd)
    x0, y0, x1, y1 = rect[0], rect[1], rect[2], rect[3]
    frm_size = ((x1 - x0), (y1 - y0))
    print("size : %d x %d" % frm_size)
else:
    # Linux or Darwin
    os.environ['SDL_VIDEODRIVER'] = 'x11'

# init pygame
import pygame

pygame.display.init()

# get tkinter.widget size
frm_w = embed.winfo_width()
frm_h = embed.winfo_height()
# print("frame size : %d x %d" % (frm_w, frm_h))

screen = pygame.display.set_mode((frm_w, frm_h))
# screen = pygame.display.set_mode((0, 0))

update_pygame_window()

root.mainloop()  # tkinter main loop

pygame.quit()
sys.exit()

python 01_embed_pygamewindow.py で実行できる。

終了は、ウインドウの右上の閉じるボタンをクリック。

tkinter widget のウインドウハンドルを取得する。 :

pygame 1.x.x は、環境変数 SDL_WINDOWID にウインドウハンドル(ウインドウID)を指定してから、pygame.display.init() (または pygame.init()) を呼ぶことで、SDL_WINDOWID で指定されたウインドウを pygame のウインドウとして使ってくれる。ソレを利用して、tkinter のウインドウ内に pygame ウインドウを埋め込んでいる。

tkinter でウインドウハンドル(ウインドウID) を取得するには、.winfo_id() を使うらしい。

embed = Tkinter.Frame(root, width=w, height=h)
embed.pack()

# ...

hwnd = embed.winfo_id()
os.environ['SDL_WINDOWID'] = str(hwnd)

# ...

import pygame

pygame.display.init()

また、import pygame をする前に、環境変数 SDL_WINDOWID を設定しておかないといけない、という話も見かけたので、一応そのような流れにしておいた。

ウインドウサイズを取得する。 :

pygame ウインドウの描画用サーフェイスを取得するには、pygame.display.set_mode((横幅, 縦幅)) を呼ばないといけない。そのためには、埋め込み先の tkinter.Frame のサイズ(横幅と縦幅)が分かってないといけない。

tkinter を使ってる場合は、.winfo_width()、.winfo_height() を使えば、横幅、縦幅を取得できる模様。

root.update()
root.update_idletasks()

# ...

frm_w = embed.winfo_width()
frm_h = embed.winfo_height()
print("frame size : %d x %d" % (frm_w, frm_h))

screen = pygame.display.set_mode((frm_w, frm_h))

ただし、事前に tkinter のウインドウが表示されて、レイアウト等が決まった状態になってないとサイズの取得はできないそうで。そのため、.winfo_width() 等を呼ぶ前に .update_idletasks() か .update() を一度呼んでおく必要がある。

ところで、.winfo_width() 等は tkinter の利用時しか使えない。tkinter を使っていない場合はどうやってウインドウサイズを取得すればいいのだろう…? 別の方法も調べてみた。

これは Windows 限定の話だけれど、pywin32 の中に入っている win32gui を使ってウインドウサイズを取得する方法もあると知った。win32gui を使えば、Windows上のどのウインドウでもサイズを取得できるはずなので、こちらのやり方のほうが色んな場面で利用できそうな気もする。もっとも、pywin32 をインストールする必要が出てくるけれど…。

import platform

if platform.system() == "Windows":
    # Windows
    os.environ['SDL_VIDEODRIVER'] = 'windib'

    # get window size (Windows only)
    import win32gui
    rect = win32gui.GetWindowRect(hwnd)
    x0, y0, x1, y1 = rect[0], rect[1], rect[2], rect[3]
    frm_size = ((x1 - x0), (y1 - y0))
    print("size : %d x %d" % frm_size)
else:
    # Linux or Darwin
    os.environ['SDL_VIDEODRIVER'] = 'x11'

win32gui.GetWindowRect(ウインドウID) を呼ぶと、左上 x 座標、左上 y 座標、右下 x 座標、右下 y 座標を返してくる。それらの値を使えば、横幅と縦幅は求められる。

tkinter の mainloop を呼びたい。 :

これは tkinter を使う時だけ意識しないといけない話だけれど。tkinter を使うなら、最後に .mainloop() を呼ぶことで、そこでずっとメインループを回してやらないといけない。らしい。もし、.mainloop() を呼ばずに、独自に while 等でメインループを回してしまうと、例えばウインドウの閉じるボタンをクリックした時にエラーが表示されてしまったりする。.mainloop() の中で tkinter の動作に必要なアレコレを行っているのだろうなと…。

しかし、tkinter 側の .mainloop() を回してしまうと、今度は pygame を利用した処理部分をメインループとして回せなくなる。

そんな時は、tkinter の .after() を使うといいらしい。

def update_pygame_window():
    global x, y, dx, dy, screen, root

    # ...
    
    # draw pygame window
    screen.fill(pygame.Color(20, 80, 160))
    pygame.draw.circle(screen, pygame.Color(255, 0, 0), (int(x), int(y)), r)
    pygame.display.flip()

    root.after(16, update_pygame_window)

# ...

update_pygame_window()
root.mainloop()  # tkinter main loop

pygame.quit()
sys.exit()

.after(ミリ秒, 関数名) で、指定したミリ秒が経過したら、指定した関数を呼んでくれる模様。つまり、その関数の最後で、また .after() を呼んでやれば、おおよそ一定周期で処理が行われる状態になるはず。まあ、処理内容によっては、そこで処理時間も変動してしまうだろうから、正確に一定周期で呼ばれるわけでもないだろうけど…。

何にせよ、この書き方をしたら、tkinter ウインドウの閉じるボタンをクリックしてもエラーが出ずに終了してくれる状態になった。

参考ページ。 :


*1: ちなみに、Python 3.9 上で pygame をインストールしようとすると、pygame 2.1.2 がインストールされる。

以上です。

過去ログ表示

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