2022/08/06(土) [n年前の日記]
#2 [pygame] tkinterの中にpygameを埋め込む
Windows10 x64 21H2 + Python 2.7.18 + pygame 1.9.6 を使って、tkinter のウインドウの中に pygame のウインドウを埋め込めそうか実験してみた。
実行結果は以下。tkinter のウインドウの中に、pygame のウインドウ(青い部分)が表示されて、赤いボールが跳ね回ってる。ボタンをクリックするとボールの座標が初期化される。
- tkinter : Python から Tk GUI ツールキットを制御してGUIアプリを作れるライブラリ。
- pygame : Python を使って2Dゲームを制作できるライブラリ。SDL というマルチメディア用ライブラリを利用している。
実行結果は以下。tkinter のウインドウの中に、pygame のウインドウ(青い部分)が表示されて、赤いボールが跳ね回ってる。ボタンをクリックするとボールの座標が初期化される。
◎ 必要なモジュールのインストール。 :
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 をインストールする例。
pygame のバージョン確認は以下。
_コマンドラインからpygameバージョンを調べる - Qiita
それとは別に。これは Windows限定の話だけど、今回、win32gui というモジュールを利用してウインドウのサイズ(横幅と縦幅)を取得してみた。win32gui は pywin32 というモジュールの中に入っているらしいので、pywin32 をインストールしないといけない。
pywin32 228 がインストールされた。
しかし、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
python 01_embed_pygamewindow.py で実行できる。
終了は、ウインドウの右上の閉じるボタンをクリック。
_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() を使うらしい。
また、import pygame をする前に、環境変数 SDL_WINDOWID を設定しておかないといけない、という話も見かけたので、一応そのような流れにしておいた。
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() を使えば、横幅、縦幅を取得できる模様。
ただし、事前に tkinter のウインドウが表示されて、レイアウト等が決まった状態になってないとサイズの取得はできないそうで。そのため、.winfo_width() 等を呼ぶ前に .update_idletasks() か .update() を一度呼んでおく必要がある。
ところで、.winfo_width() 等は tkinter の利用時しか使えない。tkinter を使っていない場合はどうやってウインドウサイズを取得すればいいのだろう…? 別の方法も調べてみた。
これは Windows 限定の話だけれど、pywin32 の中に入っている win32gui を使ってウインドウサイズを取得する方法もあると知った。win32gui を使えば、Windows上のどのウインドウでもサイズを取得できるはずなので、こちらのやり方のほうが色んな場面で利用できそうな気もする。もっとも、pywin32 をインストールする必要が出てくるけれど…。
win32gui.GetWindowRect(ウインドウID) を呼ぶと、左上 x 座標、左上 y 座標、右下 x 座標、右下 y 座標を返してくる。それらの値を使えば、横幅と縦幅は求められる。
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() を使うといいらしい。
.after(ミリ秒, 関数名) で、指定したミリ秒が経過したら、指定した関数を呼んでくれる模様。つまり、その関数の最後で、また .after() を呼んでやれば、おおよそ一定周期で処理が行われる状態になるはず。まあ、処理内容によっては、そこで処理時間も変動してしまうだろうから、正確に一定周期で呼ばれるわけでもないだろうけど…。
何にせよ、この書き方をしたら、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 ウインドウの閉じるボタンをクリックしてもエラーが出ずに終了してくれる状態になった。
◎ 参考ページ。 :
_python - tkinter and pygame do not want to work in one window - Stack Overflow
_python - Embedding a Pygame window into a Tkinter or WxPython frame - Stack Overflow
_python - Draw a circle in Pygame using Tkinter - Stack Overflow
_python - interrupting embedded pygame in tkinter skips KEYUP events and thinks the key is still pressed - Stack Overflow
_python - I'm embedding a pygame window into Tkinter, how do I manipulate the pygame window? - Stack Overflow
_user-interface - Tkinterでのpygame機能の使用 | TagsQA
_Python 3 - Tkinter メインウィンドウを表示する
_【Python tkinter】after()メソッド:関数を指定時間経過後(定期的)に実行する | OFFICE54
_python - tkinterを用いて動的にカウントを更新したい - スタック・オーバーフロー
_Get window position & size with python - Stack Overflow
_Python の win32gui を使ってアクティブウインドウの記録を取るスクリプトを作ってみた - Qiita
_僕のwin32gui(Python) - Qiita
_python - Embedding a Pygame window into a Tkinter or WxPython frame - Stack Overflow
_python - Draw a circle in Pygame using Tkinter - Stack Overflow
_python - interrupting embedded pygame in tkinter skips KEYUP events and thinks the key is still pressed - Stack Overflow
_python - I'm embedding a pygame window into Tkinter, how do I manipulate the pygame window? - Stack Overflow
_user-interface - Tkinterでのpygame機能の使用 | TagsQA
_Python 3 - Tkinter メインウィンドウを表示する
_【Python tkinter】after()メソッド:関数を指定時間経過後(定期的)に実行する | OFFICE54
_python - tkinterを用いて動的にカウントを更新したい - スタック・オーバーフロー
_Get window position & size with python - Stack Overflow
_Python の win32gui を使ってアクティブウインドウの記録を取るスクリプトを作ってみた - Qiita
_僕のwin32gui(Python) - Qiita
*1: ちなみに、Python 3.9 上で pygame をインストールしようとすると、pygame 2.1.2 がインストールされる。
[ ツッコむ ]
以上です。