質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.35%
Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

Q&A

1回答

510閲覧

win32apiを利用したウィンドウ描画にて、特定の条件下でウィンドウの表示が更新できない

hiro12345

総合スコア0

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

0グッド

1クリップ

投稿2024/12/15 05:44

編集2024/12/22 11:13

実現したいこと

https://94.gigafile.nu/0325-c7f527087ad73dda5963f92a783e34fbb
上記ファイルのrun.batを押して動くプログラムをPythonで再現したいと思っています
バルーンウィンドウを表示し、マウスの現在座標を表示するというプログラムです
しかしPythonではバルーンを表示する機能がなく、一から作成する必要があります
Tkinterで似たようなものは作れますが、見た目をなるべく同じにしたいため、上記プログラムと同じようにwin32apiを利用したウィンドウ描写を試みています
そこで上記プログラム(Rust製)のballoon関数のソースコードをChatGPTにトランスパイルしてもらい修正を繰り返したのですがある問題が解決できずにいます
こちらのバルーンですが
指定した文字列をバルーン上に表示、バルーンの大きさは文字列の文字数と行数に応じて変化する
というプログラムになっています、別途でバルーン自体の色や透明度も変更可能です
内容に変更があれば、シームレスに変更が可能で、すぐに文字列が変えられます
ひとまずテスト用プログラムとして最初に"Hello World!"とバルーンに表示
3秒後に"Updated Content!"とバルーンに表示し、また3秒後に"Final Update!"と表示するプログラムを作成しましたが、問題が解決できずにいるので助言いただけると幸いです

発生している問題・分からないこと

こちらのコードですが
実行直後に別ウィンドウをクリックし、別のウィンドウがアクティブ状態になると
"Updated Content!"
までは正常にウィンドウが表示されるのですが
balloon.update(message="Final Update!")
を処理するときにウィンドウの表示が乱れてメッセージが正常に表示されません
別ウィンドウをアクティブにしないままなら正常に動きます
3回内容を更新させて実験してみたところ、バルーンが非アクティブかつupdateメソッドが2回目以降の場合に表示が乱れるようです
プログラムの用途上、ウィンドウのアクティブ状態にかかわらず内容が表示される必要があります
なぜ、1回目のupdateではウィンドウが非アクティブでも正常に更新されるのに2回目以降は表示が乱れるのですか?
どうやったら問題を解決できますか?

イメージ説明

該当のソースコード ※12/21追記

Python

1import win32gui 2import win32api 3import win32con 4import win32ui 5from ctypes import windll 6 7class Balloon: 8 class_name = "BalloonWindow" 9 class_registered = False 10 11 def __init__(self, message, x=0, y=0, font_name="Arial", font_size=20, fore_color=0x000000, back_color=0xFFFFFF, transparency=255): 12 self.message = message 13 self.x = x 14 self.y = y 15 self.font_name = font_name 16 self.font_size = font_size 17 self.fore_color = fore_color 18 self.back_color = back_color 19 self.transparency = transparency 20 21 # フォント作成 22 self.font = win32ui.CreateFont({ 23 "name": self.font_name, 24 "height": self.font_size, 25 }) 26 27 # ウィンドウ作成 28 self.hwnd = self.create_window() 29 30 # 描画 31 self.draw() 32 33 @classmethod 34 def register_class(cls): 35 if not cls.class_registered: 36 wnd_class = win32gui.WNDCLASS() 37 wnd_class.hInstance = win32api.GetModuleHandle(None) 38 wnd_class.lpszClassName = cls.class_name 39 wnd_class.lpfnWndProc = cls.wnd_proc 40 win32gui.RegisterClass(wnd_class) 41 cls.class_registered = True 42 43 def create_window(self): 44 self.__class__.register_class() 45 46 hwnd = win32gui.CreateWindowEx( 47 win32con.WS_EX_LAYERED | win32con.WS_EX_TOPMOST | win32con.WS_EX_TOOLWINDOW, 48 self.__class__.class_name, 49 "Balloon", 50 win32con.WS_POPUP, 51 self.x, 52 self.y, 53 300, 150, # 仮の初期サイズ 54 0, 55 0, 56 win32api.GetModuleHandle(None), 57 None 58 ) 59 60 win32gui.SetLayeredWindowAttributes( 61 hwnd, 0, self.transparency, win32con.LWA_ALPHA 62 ) 63 win32gui.ShowWindow(hwnd, win32con.SW_SHOW) 64 return hwnd 65 66 def draw(self): 67 hdc = win32gui.GetDC(self.hwnd) 68 hdc_mem = win32gui.CreateCompatibleDC(hdc) 69 70 text_width, text_height = self.calculate_text_size(hdc_mem) 71 72 new_width = text_width + 20 73 new_height = text_height + 20 74 win32gui.MoveWindow(self.hwnd, self.x, self.y, new_width, new_height, True) 75 76 brush = win32gui.CreateSolidBrush(self.back_color) 77 bitmap = win32gui.CreateCompatibleBitmap(hdc, new_width, new_height) 78 win32gui.SelectObject(hdc_mem, bitmap) 79 win32gui.FillRect(hdc_mem, (0, 0, new_width, new_height), brush) 80 81 win32gui.SelectObject(hdc_mem, self.font.GetSafeHandle()) 82 win32gui.SetTextColor(hdc_mem, self.fore_color) 83 win32gui.SetBkMode(hdc_mem, win32con.TRANSPARENT) 84 win32gui.DrawText( 85 hdc_mem, 86 self.message, 87 -1, 88 (10, 10, new_width - 10, new_height - 10), 89 win32con.DT_LEFT | win32con.DT_WORDBREAK 90 ) 91 92 win32gui.BitBlt(hdc, 0, 0, new_width, new_height, hdc_mem, 0, 0, win32con.SRCCOPY) 93 94 win32gui.DeleteObject(bitmap) 95 win32gui.DeleteDC(hdc_mem) 96 97 def calculate_text_size(self, hdc): 98 win32gui.SelectObject(hdc, self.font.GetSafeHandle()) 99 100 lines = self.message.split("\n") 101 max_width = 0 102 line_height = abs(self.font_size) 103 total_height = len(lines) * line_height 104 105 for line in lines: 106 size = win32gui.GetTextExtentPoint32(hdc, line) 107 max_width = max(max_width, size[0]) 108 109 return max_width, total_height 110 111 def update(self, message=None, x=None, y=None, font_name=None, font_size=None, fore_color=None, back_color=None, transparency=None): 112 """ バルーンの内容や位置を更新 """ 113 if message is not None: 114 self.message = message 115 if x is not None: 116 self.x = x 117 if y is not None: 118 self.y = y 119 if font_name is not None: 120 self.font_name = font_name 121 if font_size is not None: 122 self.font_size = font_size 123 if fore_color is not None: 124 self.fore_color = fore_color 125 if back_color is not None: 126 self.back_color = back_color 127 if transparency is not None: 128 self.transparency = transparency 129 self.draw() 130 131 @staticmethod 132 def wnd_proc(hwnd, msg, wparam, lparam): 133 if msg == win32con.WM_DESTROY: 134 win32gui.PostQuitMessage(0) 135 return win32gui.DefWindowProc(hwnd, msg, wparam, lparam) 136 137 def destroy(self): 138 win32gui.DestroyWindow(self.hwnd) 139 140import time 141win32gui.PumpWaitingMessages() 142 143# 初期バルーン 144balloon = Balloon( 145 message="Hello World!", 146 x=500, 147 y=300, 148 font_name="Arial", 149 font_size=20, 150 fore_color=0x000000, 151 back_color=0xFFFF00, 152 transparency=200 153) 154win32gui.PumpWaitingMessages() 155time.sleep(3) 156 157# 内容と位置を更新 158balloon.update(message="Updated Content!", x=500, y=300) 159win32gui.PumpWaitingMessages() 160time.sleep(3) 161 162# さらに更新 163balloon.update(message="Final Update!") 164win32gui.PumpWaitingMessages() 165time.sleep(3) 166 167# 終了 168balloon.destroy() 169 170

試したこと・調べたこと

  • teratailやGoogle等で検索した
  • ソースコードを自分なりに変更した
  • 知人に聞いた
  • その他
上記の詳細・結果

ChatGPTに症状を伝え、修正を依頼することを何度も繰り返しましたが、修正してもらったコードでも正常な結果が得られません
win32apiの情報もネット上に少なく、検索してもいまいちです

補足

Python 3.10.4で実行しています
ライブラリはpywin32です

追記

rust製のインタプリタ言語「UWSCR」での挙動
イメージ説明
UWSCRソースコード

UWSCR

1FUNCTION GETSTATE() 2 x=G_MOUSE_X 3 y=G_MOUSE_Y 4 s="マウス座標:" + x + "," + y 5 result=s 6FEND 7 8WHILE True 9 s = GETSTATE() 10 balloon(s, 10, 10) 11 Sleep(0.01) 12WEND

上記についてTkinterで再現できました、今のところ問題ありません
イメージ説明
※balloon.pyのソースコードは省略

Python

1from balloon import * 2import time 3import pyautogui 4 5 6def getstate(): 7 x,y =pyautogui.position() 8 s=f"マウス座標:{x},{y}" 9 return s 10 11while True: 12 s=getstate() 13 balloon(s,10,10) 14 time.sleep(0.01)

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

teamikl

2024/12/15 20:24 編集

Python 3.10.9 / win11 です。 他のウィンドウをアクティブにしても問題の現象は確認できませんでした。 環境依存な問題だとすると、他に情報が必要になってくるかな? ちなみに、tkinter でも(自分が確認した限りの) 同様の見た目は実現できますが、 どのあたりで判断されたのでしょう?情報は少ないかもしれないですが、 枠無しウィンドウ、背景透過、最上位に表示、ならオプション指定で可能です。 tkinter での HINT: overrideredirect, attributes alpha, topmost もし、より詳細な win32api の機能が使いたい場合でも、 windowsのtkinterでは winfo_idメソッどで各ウィンドウの hWnd が得ることが出来るので、 必要最小限のみ ctypes で win32api 呼び出しというアプローチもとることができます。 ---- GUI プログラムとしてなら、質問に掲載のコードにはパッと見た感じで2点ほど気になる箇所があります ただし、自分の環境ではそれでも一応動いたことから、問題との関連は解りません。 追記 - time.sleep GUI プログラムはイベントループ(win32用語だとメッセージループ)を稼働し続けなければ  現状のコードではマウスカーソルが回転状態・ウィンドウがフリーズした状態になっていて、  対象のウィンドウは入力を一妻受け付けません。  表示だけなので実行には支障ありませんが、sleep 時間が長いと強制終了されます。  ⇒ GUIプログラムで time.sleep したい場合は、イベントループの提供するタイマー機能を用います。  (もしくは別スレッドで time.sleep して、スレッド間通信でGUIが動いているスレッドへ通知) - メッセージループがない。イベントループを処理する為のメッセージループのコードが見当たりません  表示の更新だけなら問題ないかもしれませんが、理由は同上。
teamikl

2024/12/15 20:45

追加で報告、何度か実行してると一度だけ 「ウィンドウの大きさが変わらず文字が見切れてる状態」 「(再確認できず) 多分、3回目なのに2回目の文字が表示」 を確認できました。その後再現は出来ず。 > バルーンが「「非アクティブ」」かつupdateメソッドが2回目以降の場合に表示が乱れるようです タイミング問題だとすれば、上述した件(time.sleep)が問題になる可能性はあります。 質問の状況とは異なりますが、このコードでは、balloonのウィンドウが「「アクティブ」」になった時、 応答なし状態になるので。
hiro12345

2024/12/17 22:15 編集

回答ありがとうございます、GUIなのでsleepは使うと問題でしたね... クリックイベントについては、自力で修正しようとしましたが、ダメでChatGPTもお手上げのようでした
hiro12345

2024/12/17 21:27

再現画像を載せました 別ウィンドウをクリックすると画像のような乱れ方をします
hiro12345

2024/12/17 22:14 編集

さっきの透明度の話は、元のGUIプログラムでも同じ仕様だったので忘れてください ひとまず、Tkinterで再現を再度してみることにします
teamikl

2024/12/17 22:42

通知がたくさん来てた。変更点・コメントは全部追いきれてないけど、とりあえず気付いたよ報告です。 こちらの環境では別ウィンドウをクリックしただけでは問題を再現できませんでしたが、 何かいろいろ操作をしてると(再現性の規則は把握できず) 問題の現象、3回目なのに2回目の文字Updated で文字が見切れる同じ現象は何度か確認しました。 win32apiで、確認して貰いたい事が一点、自分の回答の追記した分 「追記: win32gui でタイマー&メッセージループを使う実装」 は試していただけましたか?time.sleepが問題だった場合はこれで解消が見込めます。
teamikl

2024/12/17 23:05

> さっきの透明度の話は、 ここはもしかして削除済なのかな? 言いそびれましたが、元のGUIプログラムは見られませんでした。 リンク先を見てもダウンロード方法が解らず。パスワード?っぽいものも求められてて、 あとファイルサイズから実行ファイルなのかなという感じもして、敬遠。 ※ 実行ファイルの場合はこちらでは試せないのでご了承下さい。
hiro12345

2024/12/18 12:57

透明度の件は解決したので手間を取らせないために削除させていただきました リンク先ですが、パスワードは空欄のままダウンロード開始を押せばダウンロードできますが、実行ファイルになります ご指摘の通り、win32gui.PumpWaitingMessages()をtime.sleepの前に書いたら解決しました ありがとうございます マウスカーソルを重ねたさい、グルグルになる問題もある程度解決したようですが、一定の条件でやはりグルグルのままになります(詳しい条件不明) これはウィンドウにクリックイベントを設定したら解決するのですか?
teamikl

2024/12/18 19:17 編集

グルグルになる問題は、メッセージループが機能してないことが原因です。 PumpWaitingMessages は、メッセージループを一時的に進めます。 time.sleep で起こる問題かどうかを調べるための措置なので、 根本的な解決策は、 自分の回答の「追記: win32gui でタイマー&メッセージループを使う実装」 を試してみてください。(ここの確認が取れてません) 他にも問題はあるかもしれませんが、 time.sleep を全て排除は必須なはずです。 全てのGUIプログラムで共通なのですが、応答なしになる原因は、 処理が何処かで止まり(今回の場合は time.sleep)、 イベントループ(メッセージループ)に処理が戻ってない点にあります。 質問に掲載のコードにはメッセージループ自体がありません。
hiro12345

2024/12/18 23:00

ありがとうございます 以下のコードで実行しましたところ、更新されない問題は解決しています ただしwin32gui.PumpWaitingMessages()をtime.sleepの前に置いた場合の違い、こちらは100%マウスカーソルがグルグルになるようです import win32gui import win32api import win32con import win32ui from ctypes import windll import timer class Balloon: class_name = "BalloonWindow" class_registered = False def __init__(self, message, x=0, y=0, font_name="Arial", font_size=20, fore_color=0x000000, back_color=0xFFFFFF, transparency=255): self.message = message self.x = x self.y = y self.font_name = font_name self.font_size = font_size self.fore_color = fore_color self.back_color = back_color self.transparency = transparency # フォント作成 self.font = win32ui.CreateFont({ "name": self.font_name, "height": self.font_size, }) # ウィンドウ作成 self.hwnd = self.create_window() # 描画 self.draw() @classmethod def register_class(cls): if not cls.class_registered: wnd_class = win32gui.WNDCLASS() wnd_class.hInstance = win32api.GetModuleHandle(None) wnd_class.lpszClassName = cls.class_name wnd_class.lpfnWndProc = cls.wnd_proc win32gui.RegisterClass(wnd_class) cls.class_registered = True def create_window(self): self.__class__.register_class() hwnd = win32gui.CreateWindowEx( win32con.WS_EX_LAYERED | win32con.WS_EX_TOPMOST | win32con.WS_EX_TOOLWINDOW, self.__class__.class_name, "Balloon", win32con.WS_POPUP, self.x, self.y, 300, 150, # 仮の初期サイズ 0, 0, win32api.GetModuleHandle(None), None ) win32gui.SetLayeredWindowAttributes( hwnd, 0, self.transparency, win32con.LWA_ALPHA ) win32gui.ShowWindow(hwnd, win32con.SW_SHOW) return hwnd def draw(self): hdc = win32gui.GetDC(self.hwnd) hdc_mem = win32gui.CreateCompatibleDC(hdc) text_width, text_height = self.calculate_text_size(hdc_mem) new_width = text_width + 20 new_height = text_height + 20 win32gui.MoveWindow(self.hwnd, self.x, self.y, new_width, new_height, True) brush = win32gui.CreateSolidBrush(self.back_color) bitmap = win32gui.CreateCompatibleBitmap(hdc, new_width, new_height) win32gui.SelectObject(hdc_mem, bitmap) win32gui.FillRect(hdc_mem, (0, 0, new_width, new_height), brush) win32gui.SelectObject(hdc_mem, self.font.GetSafeHandle()) win32gui.SetTextColor(hdc_mem, self.fore_color) win32gui.SetBkMode(hdc_mem, win32con.TRANSPARENT) win32gui.DrawText( hdc_mem, self.message, -1, (10, 10, new_width - 10, new_height - 10), win32con.DT_LEFT | win32con.DT_WORDBREAK ) win32gui.BitBlt(hdc, 0, 0, new_width, new_height, hdc_mem, 0, 0, win32con.SRCCOPY) win32gui.DeleteObject(bitmap) win32gui.DeleteDC(hdc_mem) def calculate_text_size(self, hdc): win32gui.SelectObject(hdc, self.font.GetSafeHandle()) lines = self.message.split("\n") max_width = 0 line_height = abs(self.font_size) total_height = len(lines) * line_height for line in lines: size = win32gui.GetTextExtentPoint32(hdc, line) max_width = max(max_width, size[0]) return max_width, total_height def update(self, message=None, x=None, y=None, font_name=None, font_size=None, fore_color=None, back_color=None, transparency=None): """ バルーンの内容や位置を更新 """ if message is not None: self.message = message if x is not None: self.x = x if y is not None: self.y = y if font_name is not None: self.font_name = font_name if font_size is not None: self.font_size = font_size if fore_color is not None: self.fore_color = fore_color if back_color is not None: self.back_color = back_color if transparency is not None: self.transparency = transparency self.draw() @staticmethod def wnd_proc(hwnd, msg, wparam, lparam): if msg == win32con.WM_DESTROY: win32gui.PostQuitMessage(0) return win32gui.DefWindowProc(hwnd, msg, wparam, lparam) def destroy(self): win32gui.DestroyWindow(self.hwnd) win32gui.PumpWaitingMessages() # 初期バルーン balloon = Balloon( message="Hello World!", x=500, y=300, font_name="Arial", font_size=20, fore_color=0x000000, back_color=0xFFFF00, transparency=200 ) if 0: import time time.sleep(3) # 内容と位置を更新 balloon.update(message="Updated Content!", x=500, y=300) time.sleep(3) # さらに更新 balloon.update(message="Final Update!") time.sleep(3) # 終了 balloon.destroy() else: def gen_timer(gen): # ジェネレーターをタイマーで処理する(簡易版) def _next(tid, _): timer.kill_timer(tid) if interval := next(gen, None): timer.set_timer(interval*1000, _next) timer.set_timer(0, _next) def timeline(balloon): # time.sleep の替わりに、ジェネレーターで実装 yield 3 balloon.update(message="Updated Content!", x=500, y=300) yield 3 balloon.update(message="Final Update!") yield 3 balloon.destroy() gen_timer(timeline(balloon)) # タイマー win32gui.PumpMessages() # メッセージループ
teamikl

2024/12/19 00:57 編集

コメント欄のコードはインデントが崩れて試せないので、質問のコードを編集してください。 ぐるぐるの問題の原因自体は、イベントループ周りですが、他の箇所が問題のようです。 質問の現象はとりあえず解消かな? 正しく動くプログラムにするには、 win32 での POPUP ウィンドウの正しい実装方法辺りを調べて・・となってきそう。 検証: 試しに win32con.WS_POPUP の所を数値の 0 にすると、応答なしの現象は解消できるはずです。 表示自体が変わってしまうので、目的の挙動ではないかもしれませんが。 ---- それから tkinter 版のコードは試されましたか? win32 のデバッグをするよりも tkinter で作った方が早いかもしれない。
teamikl

2024/12/19 04:21 編集

少し前の返答になるけど、ここからの(表示が崩れる問題とは別) 解決策としてはこちらが近いかも。 > これはウィンドウにクリックイベントを設定したら解決するのですか? POPUPウィンドウでは、標準のウィンドウと違い、 捕捉されてないメッセージがあります。 wnd_proc 内で適切にメッセージを処理することで解消できるはず(試せてない) 大幅な変更が必要になる可能性もあります。 win32 のプログラムとしてみた場合、他にも懸念点は多々あります。 WM_PAINT 外での描画 ⇒ メッセージキューがブロックされる ウィンドウ作成後の処理をメインに書いている ⇒ WM_CREATE メッセージ WM_DESTROY 以外のメッセージも DefWindowProc で処理してる。
hiro12345

2024/12/21 08:37

返信が遅くなってしまってすみません コードについては、timerをインポートしteamikiさんの投稿したものの差分を適用したのみです また、time.sleepを使えない問題をマルチスレッドにして動かして解決しようとしましたが、別スレッドだとウィンドウが表示されない問題が発生しているのでおっしゃる通りtkinterで再現したほうがよさそうで、ただいま作成中です time.sleepを使わなくても別の時間のかかる処理をするとフリーズしてしまうからです tkinterなら別のスレッドで動かせることを確認済みなので、問題なさそうです 見た目も今完全再現できることを確認したので機能面の作成を行っています
teamikl

2024/12/21 14:53

お気になさらず。何かと忙しい時期ですし、 自分も毎日チェックできるかわかりませんが。 マルチスレッドにする場合は、同期処理の問題等々もあって 単純にtime.sleep を別スレッドに追い出すだけとはいかないです。 queue 等を用いたスレッド間通信か、win32api でスレッドセーフな関数があればそれを使う。 PostMessage 辺り。別スレッドからの GUI関連の直接操作は NG です。どのGUIライブラリでも共通 ちなみに、イベントループが稼働しているスレッドで時間のかかる処理をすると フリーズするのも全GUIライブラリ共通です。 GUIのプログラムでは、イベントループ(メッセージループ)がずっと稼働していて、 そこからユーザー定義の関数(今回の例では wndproc) が呼び出されるのですが、関数は即座に終了し処理をイベントループ側に戻さなければなりません。 戻らなかった場合が、応答なしのフリーズ状態。 その点にさえ注意すれば、どのGUI環境を選択しても同じです。⇒時間のかかる処理に関して 仮に今回のwin32 のプログラムをスレッドを使って実装するなら スレッド側では、time.sleep を用いて各パラメーターの変更のみを行います。 メイン側では timer を用いて定期的にパラメーターに更新があれば GUI へ反映とします。 但し、マウスを乗せたときに応答なしとなる問題については、更に色々あって 秒が関連の処理は WM_PAINT メッセージ内で行うように変更する等、 設計から見直しが必要になりそうなので、そういった点で 別ライブラリをお勧めしました。 > tkinterなら別のスレッドで動かせることを確認済み 割と強く GUI はメインスレッドで動かすことを推奨です 簡単なプログラムで上手くいったのかもしれませんが、 別スレッドで動かした場合の問題がたくさんあります。(通常利用より注意点が増える感じです) 説明は ChatGPT に丸投げ「tkinter を別スレッドで動かした場合の問題は?」と聞いてみよう。 後々、何か問題があった時に、別スレッドで動かしてるのが問題ではと疑うことになります。 何か別の処理が必要な場合は、そちらを別スレッドにしましょう。 どうしても tkinter 側を別スレッドにする必要がある場合は、 (別処理で利用してるライブラリがメインスレッド依存の場合) tkinterを「別プロセス」で動かす事を検討します。メインスレッドで動かす事が重要。 プロセス間の値のやり取りなどは少々面倒になります ---- 長くなったので要点まとめ: - GUIプログラムでは、関数はすぐに終わり処理をイベントループに戻す  - 時間のかかる処理は別スレッドに投げる - スレッドとのやり取りはスレッド間通信が必要  - 同期queueを使う方法が一般的。サンプルコードも多い。 - GUIのイベントループは必ずメインスレッドで動かす  - メインスレッド想定で作られているライブラリが多い。   別スレッドでイベントループを動かす場合は、取り扱いに注意事項が増える。 - 別スレッドから GUIの操作は行わない (全GUI共通の注意点)  GUIの操作はイベントループが同期を取って管理してます。  別スレッドから割り込むことは、原則禁止。想定外の挙動の原因になります。 ※例外で、スレッドセーフなGUIライブラリもありますが、自分の知る限り保守されてません。
hiro12345

2024/12/22 02:01 編集

ありがとうございます、別スレッドで動かすことを想定されてないのですね アップしてあるrustのプログラムではGUIは完全に別スレッド扱いで動いているようなのでできる限り動作を再現させる方針でいます、座標表示プログラムの他にもballoonを使用したものでPythonに移植したいものがありコードの書き方も可能な限り同一性を保ちたいので いろいろ試してみて、何か問題が起きたらマルチスレッドによる問題を疑いmultiprocessingによる動作に変えてみます
teamikl

2024/12/22 06:04

具体的なコード抜きでの話になってるので、 少し文書の説明での齟齬が発生してるかも。>「別スレッド扱いで動いてるようなので~」 と、少し前の「別スレッドだとウィンドウが表示されない問題」 自分が先述したGUIプログラムの注意点は、言語やGUIライブラリ関係なく、 主にGUIプログラムに多いイベント駆動型プログラムでの構造的な話です。 別スレッドだとウィンドウが表示されないのは、 ライブラリ等の問題ではなく、試されたコードの設計的な問題です。 rust では別スレッドでも出来たみたいな話ではなく、 rustのプログラムでは正しい作法で、別スレッドからGUIのスレッドへ通知を出し GUIのスレッド側からウインドウを作成という事をしてるはずです。少なくとも内部の挙動は。 ここは、具体的なコードをベースに話をした方が良くて、 ちなみにアップしてあるのは exe ですよね。rust のソースコードはありませんか? もしくは以下の2つの試したコードを(該当部分に絞って10-20行程度で)提示して 貰えれば、問題の修正方法(の方針)や、確認した方法の欠点・正しい確認かどうかを指摘できます。 > 別スレッドだとウィンドウが表示されない問題 > tkinterなら別のスレッドで動かせることを確認済み ---- >何か問題が起きたらマルチスレッドによる問題を疑いmultiprocessingによる動作に変えてみます これは悪手だと思う。起こってからでは修正が困難で 私の意見としては、言葉足らずだった箇所を補うと 「設計の段階で」そうならない為の最初からマルチスレッドでという妥協案の提案です。 可能なら、GUIをメインスレッドで、他の処理を別スレッドにした方が楽です。
hiro12345

2024/12/22 12:18 編集

rustのソースコードは難解で文字数もかなり多いのでここに載せるのは難しいです なお、exeファイルはrustで作成されたUWSCRというインタプリタ言語の実行ファイルとなっており、UWSCRのソースコードで質問内容のPythonのコードを再現した動作を書くと以下のようになります 書き方はこのソースコードの書き方と同じにする方針です あくまでもballoonを書いたpyファイルを読み込んでライブラリ的な使い方をする想定のため、メインスレッドで実行させるとmainのpyファイルの書き方を大幅に変えなくてはいけません balloon(メッセージ[, X=0, Y=0, 変形=FUKI_DEFAULT, フォントサイズ=EMPTY, 文字色=$000000, 背景色=$00FFFF, 透過=0]) ---------------------------------------- balloon("Hello World!",300,500) sleep(3) balloon("Updated Content!",300,500) sleep(3) balloon("Final Update!",300,500) sleep(3) ---------------------------------------- tkinterで再現したソースコードも載せます、文字数制限に引っかかるため外部リンクです このソースコードと同じようにballoonがない場合は出現させる、出現している場合で実行すると内容を更新させます pyファイルをインポートしてライブラリ的な使い方をできるのも確認済みです time.sleepを使っても応答なしにならなそうです https://downloadx.getuploader.com/g/6767e37c-b184-4a89-8dc1-3136a010e467/1%7Csample/18775/sample_18775.txt
hiro12345

2024/12/22 11:16

再現したかったプログラムについても完全再現できました 今使ってる分についてはこちらのコードで問題なさそうです、参考までにUWSCRとPythonで再現した挙動のgifを載せました おそらく解決です
teamikl

2024/12/23 00:43 編集

ソースコード拝見しました。 - tk.Tk() を何度も呼び出すのは、想定外の挙動の原因になったりします。  通常は、初期化は一度のみ・mainloop も一回。ウィンドウが必要なら別途 tk.Toplevel を作成します。  工夫して使うことで何とかならなくもないが、というのが global_balloon での工夫だと思うけど  calculate_label_size でアウトです。マルチスレッド間で安全でない操作。  2つ目の tk.Tk() 呼び出しで、update_balloon はメインスレッドでの実行です。  イベントループが別スレッドで稼働中の最初の mainloop を使うことになっているので  現状何とかなってるうちは良いですが、変な挙動になる可能性があります。対策は root = tk._get_default_root() # ほんとはこれもあまり好ましくない。スレッドを跨ぐ変数の参照 root.after_idle(lambda: tk.global_balloon.update_balloon(message, x, y, ...略)) でイベントループ側のスレッドから update_balloonを呼びだすことができます。 (destroy でやってる after 0 と同じアプローチ。厳密には after もスレッドセーフではないが  after では描画の同期には影響ないので、tkitnerでは  GUIスレッドへの通知として、after を使ってイベントループ側から関数を呼び出す方法が使えます) calculate_label_size 内の tk.Tk は tk.Toplevelで済みます。⇒ tk.Tk が複数回呼ばれることを回避 (1度目の実行時 create_balloon では calc~の後にtk.Tk() なので、 Toplevel を生成する前に Tk での初期化が先に行われるように順序入れ替え) tkinter.Tk は、ライブラリの初期化 + メインウィンドウ(Toplevel) を持ちます。 ウィンドウが欲しいだけ場合は、再度初期化する必要はなく、Toplevel を使いましょう。 また、破棄して再生成というアプローチを取ってますが、 ウィンドウを破棄せずにを更新する方法もあります。HINT: configメソッド
teamikl

2024/12/22 16:01

コメントが入れ違いになってたのに気が付きませんでした。 上のソースコードを見たのは、sample~txt ファイルについてのコメントでした。 スレッドセーフではないコードは、 タイミングで問題が発生したりしなかったりなので、 デバッグ時は大丈夫でも本運用で長時間使うと不安定みたいなことが 起こりやすい傾向があります。そして問題の再現が困難だったり。 気になった点では他に 秒間100回近くサイズ計算の為にウィンドウを生成破棄を繰り返してる辺りは改善できるはず ⇒ packで配置すると自動でフィットしてくれるはずなので、計算不要 大丈夫かどうかは用途次第ですが、動けばとりあえずそれでいいならこれで解決でも良いですし 問題点を理解して根本から解決したい場合は、2つ前の自分のコメントがヒントです。 (同期処理やGUIのイベントループへの理解が解決のカギ) マウス座標のリアルタイム追跡だったら、タイマー(tkinterではafter)で更新するのが正着だったかな。
hiro12345

2024/12/23 10:54 編集

ありがとうございます、calculate_label_sizeでウィンドウを破棄→再生成しないように修正しました 結果ウィンドウの切り替わりのスピードが目に見えて改善しました あと、afterメソッドを使うのが比較的安全とのことなので update_balloonのupdateをself.root.after(0, self.root.update)に書き換え calculate_label_size内のラベル更新についても self.dummy_label.after(0, self.dummy_label.update_idletasks)のような形に変えました 改善したソースコードはこちらです(さっきまで別のURLsample_18776.txtを載せてたので入れ違いになっていたら申し訳ないです) https://u1.getuploader.com/sample/download/18777 >>でイベントループ側のスレッドから update_balloonを呼びだすことができます。 このコードの使い方をChatGPTに聞いてもよくわからなかったのですが、書き換える部分は以下の部分なのですか? どちらにしても、update_balloonのupdateを上記で修正したようにafterメソッドで行えば同じ効果が得られそうなのですが なお、書き換えて動かしたら正常に動作しなくなりました def balloon(message=" ", x=0,略): try: root = tk._get_default_root() root.after_idle(lambda: tk.global_balloon.update_balloon(message, x, y,略)) except Exception: tk.global_balloon=balloon_class(message, x, y, 略)
guest

回答1

0

問題点

  • GUI プログラムでの time.sleep は、 GUIが応答なし状態になります

 ⇒ GUI環境が提供するタイマーを使う方法が順当な解決策

  • イベントループがない (※ win32api用語 では メッセージ・ループ)

 ⇒ イベントループがないとタイマーも動かないので必須

ChatGPT に間違っている点の教えてあげると修正してくれますが、
そもそも正解が解らない状態での AI 頼りは、生成して試してみるしかできないので生産性が良くなさそう。
最低限、C言語で win32 SDK を用いて同様のプログラムが作れるようになってからでないと
独力でのデバッグができません。

AI活用が役に立つ局面もあります。ctypes を用いての win32 api 個々の呼び出し等は
殆どが機械的に出来る単純作業なので、割と正確な回答が返ってきます。⇒時間節約に繋がるケース

但し、その使われ方に関しては、利用者側が正解を知ってるケースでないと活用しにくいことが多いです。

例えばですが、QAサイトで質問に上がるような不完全なコードを AI が学習して
それを元に AI が誤った回答を試みるといったことがあるかもしれません。
実際どうなのかはわかりませんが、未実装の機能を元にした回答は確認したことがあります。

GUIアプリケーションで time.sleep したい時の方法

win32gui の解決策ではないけど、
Tkinter での Balloon ? の実装のサンプルコードを添付します。
自作ライブラリの紹介も兼ねてデモンストレーション

イメージ説明

※ 自分の Balloonの認識は、枠無しダイアログくらいの認識です。
別の GUIライブラリ wxPython では balloon はツールチップみたいな形で提供されてます。
通知用途だったら、wxPython の ToasterBox が近いかもしれません。

依存ライブラリ

  • pip install gentimer でインストール可能です。

ジェネレーターで記述したコードを、GUI側の提供するタイマー上で実行するライブラリです。
標準ライブラリでは asyncio でも同種のアプローチは取れますが、手軽&軽量にしたくらいの立ち位置。

大雑把に説明すると、GUI プログラムでは time.sleep すると応答なしになってしまう問題があります。
解決策としてGUIのイベントループが提供するタイマー機能を使う必要があるが、
使い方が関数を登録するコールバック形式になるため、処理の流れが追いにくくなる傾向があります。
そこで、関数を中断・再開可能な形式 つまりジェネレーターとして実装して、
タイマーにより一定時間ごとにジェネレーターの実行を再開するといった内容です。

source code

python

1import tkinter as tk 2from tkinter.font import Font 3from typing import Union, Tuple 4from dataclasses import dataclass 5import itertools 6from gentimer.timer_tk import gen_timer # pip install gentimer 7 8@dataclass 9class BalloonOption: 10 message: str = "" 11 x: int = -1 12 y: int = -1 13 width: int = -1 14 height: int = -1 15 fg: str = "#ffffff" 16 bg: str = "#000000" 17 font: Union[Font, Tuple[str, int], None] = ("", 40), 18 alpha: float = -1 19 timeout: int = -1 20 21class BalloonAnimation: 22 def __init__(self, balloon): 23 self.window, self.label = balloon.window, balloon.label 24 25 def _calc_range_step(self, a, b, steps): 26 return (max(a,b) - min(a,b))/steps * (-1 if a > b else 1) 27 28 def _frange(self, start, stop, step, scale=1000): 29 # float<=>int の誤差調整。NOTE: 終端要素を含む 30 for value in range(int(start*scale), int(stop*scale), int(step*scale)): 31 yield int(value / 1000) 32 else: 33 yield stop 34 35 def move(self, x=0, y=0, duration=5, fps=30): 36 """バルーンの移動アニメーション""" 37 window = self.window 38 cx = int(window.winfo_x()) 39 cy = int(window.winfo_y()) 40 steps = duration * fps 41 tick = duration/steps 42 sx = self._calc_range_step(cx, x, steps) 43 sy = self._calc_range_step(cy, y, steps) 44 for mx, my in zip(self._frange(cx, x, sx), 45 self._frange(cy, y, sy)): 46 window.geometry(f"+{mx}+{my}") 47 yield tick 48 else: 49 window.geometry(f"+{x}+{y}") 50 51 def resize(self, width, height, duration=5, fps=30): 52 """ウィンドウの伸縮アニメーション""" 53 window = self.window 54 cw = int(window.winfo_reqwidth()) 55 ch = int(window.winfo_reqheight()) 56 steps = duration * fps 57 tick = duration / steps 58 sw = self._calc_range_step(cw, width, steps) 59 sh = self._calc_range_step(ch, height, steps) 60 iw = [width]*steps if sw == 0 else self._frange(cw, width, sw) 61 ih = [height]*steps if sh == 0 else self._frange(ch, height, sh) 62 for mw, mh in zip(iw, ih): 63 window.geometry(f"{mw}x{mh}") 64 yield tick 65 66 def fade(self, alpha=0.5, duration=5, fps=30): 67 """透過度のアニメーション FadeIn,FadeOut""" 68 window = self.window 69 ca = int(float(window.attributes("-alpha")) * 1000) 70 ta = int(alpha * 1000) 71 steps = duration * fps 72 tick = duration / steps 73 step = self._calc_range_step(ca, ta, steps) 74 try: 75 for val in self._frange(ca, ta, step): 76 window.attributes("-alpha", f"{val/1000:0.3}") 77 yield tick 78 except ValueError as e: 79 pass # XXX: range(_, _, 0) の時に例外 80 81 def blink(self, interval=1, duration=5, fps=30): 82 """文字色を背景色と同じにして点滅アニメーション""" 83 label = self.label 84 fg = label["fg"] 85 bg = label["bg"] 86 colors = itertools.cycle([bg, fg]) 87 88 for _ in range(0, duration*1000, int(interval*1000)): 89 label["fg"] = next(colors) 90 yield interval 91 else: 92 label["fg"] = fg 93 94class BalloonApp: 95 def __init__(self, *args, **kwargs): 96 window = tk.Toplevel(*args, **kwargs) 97 window.overrideredirect(True) # ウィンドウの枠けし 98 window.attributes("-alpha", 1.0) # 透過度 99 window.attributes("-topmost", True) # 最上位に表示 100 101 label = tk.Label(window, text="") 102 label.pack(fill=tk.BOTH, expand=True) 103 104 self.window = window 105 self.label = label 106 self.animation = BalloonAnimation(self) 107 108 def __call__(self, message, **kw) -> None: 109 """Update balloon info""" 110 opt = BalloonOption(message=message, **kw) 111 window = self.window 112 113 self.label.config( 114 text=opt.message, 115 fg=opt.fg, 116 bg=opt.bg, 117 ) 118 if opt.font: 119 self.label.config(font=opt.font) 120 window.configure(bg=opt.bg) 121 122 # 透過 0.0 ~ 1.0 123 if opt.alpha >= 0: 124 window.attributes("-alpha", opt.alpha) 125 126 # 位置 127 if opt.x < 0: 128 opt.x = window.winfo_x() 129 if opt.y < 0: 130 opt.y = window.winfo_y() 131 window.geometry(f"+{opt.x}+{opt.y}") 132 133 # サイズ 134 if opt.width > 0 and opt.height > 0: 135 window.geometry(f"{opt.width}x{opt.height}") 136 137 if opt.timeout > 0: # 一定時間後に非表示 138 window.after(opt.timeout*1000, window.withdraw) 139 140 window.deiconify() # 再表示 141 return self 142 143def timeline(balloon): 144 """time.sleep を使ったコードをジェネレーターとして実装する 145 146 例: 147 time.sleep(3) => yield 3 148 """ 149 ## 1) 150 balloon("Hello, world!", x=50, y=50, 151 font=("", 40), 152 alpha=0.0, 153 fg="#00ffff", 154 timeout=8, # 8秒後に非表示 155 ) 156 # Demo: 3秒かけてフェードイン 157 yield from balloon.animation.fade(alpha=0.5, duration=3) 158 159 # Demo: 点滅表示 160 yield from balloon.animation.blink(duration=3) 161 162 yield 3 # wait 3 sec 163 164 ## 2) 165 balloon("Updated Content!", 166 font=("", 25), 167 ) 168 yield 3 # wait 3 sec 169 170 # Demo: 移動アニメーション 171 yield from balloon.animation.move(x=200, y=200, duration=3) 172 173 ## 3) 174 balloon("Final update!") 175 176 yield from balloon.animation.resize(width=400, height=400) 177 178 yield 3 # wait 3 sec 179 180 # Demo: フェードアウト 181 yield from balloon.animation.fade(alpha=0, duration=3) 182 183def main(): 184 root = tk.Tk() 185 if not __debug__: 186 # メインウィンドウを非表示にするとデバッグ中終了方法が解らず困る事があります 187 # 事例 ⇒ PID が解らず 大量の python.exe がタスクマネージャーに 188 # __debug__ の True/False は Python の起動オプションを参照 189 root.withdraw() # メインウィンドウを非表示 190 191 # NOTE: gen_timer ジェネレーターをtkのタイマーで実行する 192 # done=root.quit ジェネレーター完了後に終了 193 balloon = BalloonApp(root) 194 gen_timer(root, timeline(balloon), done=root.quit) 195 196 # イベントループはGUIプログラムでは必須 197 root.mainloop() 198 199if __name__ == '__main__': 200 main()

追記: win32gui でタイマー&メッセージループを使う実装

自分の環境では問題の再現が難しいので、(もしくは再現条件を把握できていない)
解決に繋がるかはわかりませんが、問題の懸念要素は取り除けると思います。

上述の「ジェネレーターをタイマーで処理する」アプローチの win32gui 版です。
pywin32 では API の SetTimer/KillTimer を直接扱わず、timer モジュールが提供されてるようです。

コードは差分のみ。balloon生成後

python

1if 0: 2 import time 3 time.sleep(3) 4 5 # 内容と位置を更新 6 balloon.update(message="Updated Content!", x=500, y=300) 7 8 time.sleep(3) 9 10 # さらに更新 11 balloon.update(message="Final Update!") 12 time.sleep(3) 13 14 # 終了 15 balloon.destroy() 16else: 17 def gen_timer(gen): # ジェネレーターをタイマーで処理する(簡易版) 18 def _next(tid, _): 19 timer.kill_timer(tid) 20 if interval := next(gen, None): 21 timer.set_timer(interval*1000, _next) 22 timer.set_timer(0, _next) 23 24 def timeline(balloon): # time.sleep の替わりに、ジェネレーターで実装 25 yield 3 26 balloon.update(message="Updated Content!", x=500, y=300) 27 yield 3 28 balloon.update(message="Final Update!") 29 yield 3 30 balloon.destroy() 31 32 gen_timer(timeline(balloon)) # タイマー 33 win32gui.PumpMessages() # メッセージループ 34

投稿2024/12/16 01:33

編集2024/12/16 07:58
teamikl

総合スコア8760

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

hiro12345

2024/12/17 20:46

ありがとうございます! win32apiの知識がないのにAI頼りにしたのが仇となったようです 別のライブラリを使って再現してみます
teamikl

2024/12/17 23:30

## time.sleep が問題の場合&time.sleepを使う場合の回避策 問題の症状からの仮説・推測になりますが、思いついた別解。 time.sleepの前・後どちらかもしくは両方で win32gui.PumpWaitingMessages() を呼び出してみてください。 問題の現象が、 応答なし状態により未処理のイベントが3回目のupdate時にずれ込んでるのだとすると、 update 前に未処理のイベントを進める事でも解消できそうです。ただし確証はありません。 自分のところでは、time.sleep 前に pump~呼び出しで、問題を確認できず。 お勧めの解決方法ではありませんが、問題点を絞り込む情報にはなると思います。 ⇒ time.sleep が問題かどうか
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

まだベストアンサーが選ばれていません

会員登録して回答してみよう

アカウントをお持ちの方は

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.35%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問