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

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

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

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

Q&A

解決済

2回答

1249閲覧

Tkinterによる可変サイズダイアログの作成

hiro12345

総合スコア1

Python

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

1グッド

1クリップ

投稿2024/12/15 05:44

編集2025/01/10 23:56

実現したいこと

※(1/11追記)実用できる範囲まで改修できたので解決とさせていただきます、ありがとうございました!

https://94.gigafile.nu/0325-c7f527087ad73dda5963f92a783e34fbb
上記ファイルのrun.batを押して動くプログラムをPythonで再現したいと思っています
一例として、マウス座標をリアルタイムで表示するプログラムですが、他にも移植したいものがあり、だいたいのコードは再現できるもののある機能がPythonに用意しておらず、1から作成する必要があります
上記プログラムはexeファイルで、rustで作られたインタプリタ言語、「UWSCR」の実行ファイルとUWSCRの実行用ソースコードです
UWSCRの組み込み関数「balloon」をPythonで再現することが第一の目的となります
基本的にプログラムの書き方はこのUWSCRと同じ書き方で再現させる方針です、
※UWSCR公式github
https://github.com/stuncloud/UWSCR

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

マルチスレッドを使用するため、なるべくスレッドセーフなコードにする必要がありましたが、UWSCRと同じ書き方という点は妥協できないのでその上でのコードの改修

該当のソースコード ※2025/1/8更新

Python

1import tkinter as tk 2import threading 3import queue 4 5global_balloon = None 6 7class balloon_class: 8 9 thread_creation_count = 0 10 root_creation_count = 0 11 12 def __init__(self, queue, message, x, y, font_name, font_size, fore_color, back_color, transparency, show_border): 13 # スレッドを2重生成しないためのフラグ 14 type(self).thread_creation_count += 1 15 if 1 >= type(self).thread_creation_count: 16 self.thread = threading.Thread(target=self.create_balloon, daemon=True, args=(queue,message,x,y,font_name,font_size,fore_color,back_color,transparency, show_border)) 17 self.thread.start() 18 else: 19 print("Error: スレッド生成メソッドが2回以上参照されています") 20 21 def create_balloon(self, queue, message, x, y, font_name, font_size, fore_color, back_color, transparency, show_border): 22 23 # rootを2重生成しないためのフラグ 24 type(self).root_creation_count += 1 25 if 1 >= type(self).root_creation_count: 26 self.root = tk.Tk() 27 self.root.withdraw() 28 self.root.wm_overrideredirect(True) 29 self.sub_window = tk.Toplevel(self.root) 30 self.sub_window.deiconify() 31 32 self.previous_should_hide_balloon = False 33 34 if show_border: 35 border = 'solid' 36 else: 37 border = 'flat' 38 39 #サイズ計算用のラベル 40 self.dummy_label = tk.Label(self.sub_window, text=message, font=(font_name, font_size)) 41 # ラベルサイズを計算 42 width = self.dummy_label.winfo_reqwidth() 43 height = self.dummy_label.winfo_reqheight() 44 45 self.sub_window.attributes("-topmost", True) 46 # 全体を半透明にする(ウィンドウ全体が透明度を持つ) 47 self.sub_window.wm_attributes("-alpha", transparency) 48 self.sub_window.geometry(f"{width+20}x{height+20}+{x}+{y}") 49 self.sub_window.configure(bg=back_color) 50 self.frame = tk.Frame(self.sub_window, bg=back_color, bd=1, relief=border) 51 self.frame.place(x=0, y=0, width=width+20, height=height+20) 52 self.label = tk.Label(self.frame, text=message, fg=fore_color, bg=back_color, font=(font_name, font_size), justify="left") 53 self.label.place(x=8, y=10, width=width, height=height) 54 self.sub_window.wm_overrideredirect(True) 55 self.update_balloon(queue, message, x, y, font_name, font_size, fore_color, back_color, transparency, show_border) 56 self.root.mainloop() 57 else: 58 print("Error: 初期ウィンドウ生成メソッドが2回以上参照されています") 59 60 61 62 def update_balloon(self, queue, message, x, y, font_name, font_size, fore_color, back_color, transparency, show_border): 63 64 # キューから変数を取得 65 try: 66 balloon_args, should_hide_balloon = queue.get_nowait() 67 except Exception: 68 balloon_args = None 69 should_hide_balloon = False 70 71 # 比較用に使う前回使用の引数とqueueで読み取った表示更新用の引数のリスト 72 previous_args = [message, x, y, font_name, font_size, fore_color, back_color, transparency, show_border] 73 new_args = balloon_args 74 75 previous_should_hide_balloon = self.previous_should_hide_balloon 76 new_should_hide_balloon = should_hide_balloon 77 78 # ウィンドウを消しても引数が前と同じ値だとGUI表示を更新しない仕組みなので、ウィンドウ再生成用のフラグを作成、要再生成なら再生成と更新処理を実行 79 try: 80 should_redefine_sub_window = not self.sub_window.winfo_exists() #sub_windowが存在しない場合True 81 except Exception: 82 should_redefine_sub_window = False 83 84 if new_args is not None and (previous_args != new_args or previous_should_hide_balloon != new_should_hide_balloon or should_redefine_sub_window == True): #値が更新された場合かウィンドウを消した場合に更新処理を実行 85 message, x, y, font_name, font_size, fore_color, back_color, transparency, show_border = new_args 86 self.previous_should_hide_balloon = new_should_hide_balloon #次回表示更新時の値比較用に代入 87 try: 88 if new_should_hide_balloon: 89 self.sub_window.withdraw() 90 else: 91 self.sub_window.deiconify() 92 except Exception: 93 # ウィンドウをAlt+F4で消した場合も再生成 94 self.sub_window = tk.Toplevel(self.root) 95 self.sub_window.wm_overrideredirect(True) 96 self.sub_window.deiconify() 97 self.dummy_label = tk.Label(self.sub_window) 98 self.frame = tk.Frame(self.sub_window) 99 self.label = tk.Label(self.frame) 100 101 102 if show_border: 103 border = 'solid' 104 else: 105 border = 'flat' 106 107 self.dummy_label.config(text=message, font=(font_name, font_size)) 108 width = self.dummy_label.winfo_reqwidth() 109 height = self.dummy_label.winfo_reqheight() 110 111 self.sub_window.wm_attributes("-alpha", transparency) 112 self.sub_window.geometry(f"{width+20}x{height+20}+{x}+{y}") 113 self.sub_window.configure(bg=back_color) 114 self.frame.config(bg=back_color, bd=1, relief=border) 115 self.frame.place(x=0, y=0, width=width+20, height=height+20) 116 self.label.config(text=message, fg=fore_color, bg=back_color, font=(font_name, font_size), justify="left") 117 self.label.place(x=8, y=10, width=width, height=height) 118 119 self.root.after(30, self.update_balloon, queue, message, x, y, font_name, font_size, fore_color, back_color, transparency, show_border) 120 121 122 123def balloon(message=" ", x=0, y=0, font_name="Arial", font_size=14, fore_color="#000000", back_color="#FFFF00", transparency=1, show_border=True): 124 global global_balloon, balloon_queue 125 126 if not isinstance(global_balloon, balloon_class): 127 balloon_queue = queue.Queue() 128 global_balloon=balloon_class(balloon_queue, message, x, y, font_name, font_size, fore_color, back_color, transparency, show_border) 129 elif isinstance(global_balloon, balloon_class): 130 try: 131 if balloon_queue.empty(): 132 balloon_args = [message, x, y, font_name, font_size, fore_color, back_color, transparency, show_border] 133 should_hide_balloon = False 134 balloon_queue.put((balloon_args, should_hide_balloon)) 135 except Exception: 136 pass 137 138 139def hide_balloon(): 140 try: 141 if balloon_queue.empty(): 142 dummy_args = [" ", 0, 0, "Arial", 14, "#000000", "#FFFF00", 1, True] 143 should_hide_balloon = True 144 balloon_queue.put((dummy_args, should_hide_balloon)) 145 except Exception: 146 pass 147 148 149 150if __name__ == "__main__": 151 import time 152 balloon(message="Hello World!\nThis is a test message.\nこんにちは", x=300, y=500, font_name="Arial", font_size=14, fore_color="#000000", back_color="#00FFFF", transparency=0.7, show_border=False) 153 154 time.sleep(3) 155 hide_balloon() 156 time.sleep(1) 157 hide_balloon() 158 time.sleep(1) 159 balloon(message="Updated Content!", x=300, y=500, font_size=14, back_color="#00FFFF", transparency=0.7, show_border=False) 160 time.sleep(3) 161 balloon(message="Final Update!", x=300, y=500, font_size=14, back_color="#00FFFF", transparency=0.7, show_border=True) 162 time.sleep(3)

試したこと・調べたこと

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

当初はwin32apiで実現しようと思いましたが難しいことがわかりTkinterでの実装中です

補足

WIn11 Python 3.10.4で実行しています

追記

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で再現することに成功しました
イメージ説明

※マルチプロセス版も作成しました(下記リンク)、実行側のソースコードはマルチスレッド版用そのままで使えますが、マルチプロセスを使う仕様上処理をif name == "main":またはif name != "mp_main":のブロック中に記載しないとエラーが出る点に注意してください
https://incandescent-belekoy-906db2.netlify.app/balloon_multiprocessing_v20250108.txt

・実行側pyファイルソースコード
※balloon.pyのソースコードは省略

Python

1from balloon import * 2import time 3import pyautogui 4 5def getstate(): 6 x,y =pyautogui.position() 7 s=f"マウス座標:{x},{y}" 8 return s 9 10def func(): 11 while True: 12 s=getstate() 13 balloon(s,10,10) 14 time.sleep(0.01) 15 16if __name__ != "__mp_main__": 17 func()
teamikl👍を押しています

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

クリップした質問は、後からいつでも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, 略)
teamikl

2024/12/23 20:11

> 書き換えて動かしたら正常に動作しなくなりました エラーが出ずに期待通り動かないという事ですか? 念のため確認だけど、略のとこはキチンとしたコードが埋まってる前提で 確認事項は tk.Tk() が複数あったのを 1回に収めるように変更したかどうか。 ライブラリの初期化とウィンドウ生成(Toplevel) を別けます。 追記: 若干元質問(win32api) から離れ気味なので、 質問の軌道修正をお願いします。(後から参考にする人の為) ---- 説明書いてたら長文になったので一旦削除。コメントで読みにくい規模になったので回答欄に投稿します。 書いてから思ったけど、文章で説明が難しくて、コードで示した方が良いかな。 改善されたコードはこれから読んでみます。
teamikl

2024/12/23 20:57 編集

> > 書き換えて動かしたら正常に動作しなくなりました 原因: try/except のロジック変更が必要だったかも。 初回の tk.global_balloon = が実行されません。 try 内の tk.global_balloon は lambda に包まれてて、 イベントループ側で評価・実行されるので、ここでの except では捕捉できません。 回避策: root = の前に、tk.global_balloon とだけ書いた行を置く。 今回のケースではエラーは出ませんが、Exception ⇒ AttributeError で必要な例外のみに絞る。
hiro12345

2024/12/23 21:25

質問内容を編集しました >>念のため確認だけど、略のとこはキチンとしたコードが埋まってる前提で もちろんです、コメント欄ではインデントが崩れていますがコード上では問題ありません、ウィンドウサイズ計算についてはToplevelを使用し、再生成ではなく更新制にしました
teamikl

2024/12/23 22:45

コメントでの修正方法は入違いになったかな? 一つ上のコメントで修正方法、齟齬がでないようにコードで示すと try: tk.global_balloon # <-- これを適切なインデントで追加 except Exception: # <-- ここは AttributeError がいい。エラーメッセージを全て無視してしまう為 tk.global_balloon=balloon_class(message, x, y, 略) else: # try/except/else のelseに移動可能。他の例外も try/except で捕捉されてしまうため root = tk._get_default_root() root.after_idle(lambda: tk.global_balloon.update_balloon(message, x, y,略))
hiro12345

2024/12/23 23:18

丁寧にサンプルコードまでありがとうございます、まだ勉強不足なので今のバージョンをできるだけ改善し、一旦解決とさせていただいた後、ゆっくり読ませていただきます、特にqueue周りの知識はほとんどないので 先ほどのコメントと入れ違いになってしまいましたが >>回避策: root = の前に、tk.global_balloon とだけ書いた行を置く。 こちらを修正し、質問内容のコードに反映させました。 >>今回のケースではエラーは出ませんが、Exception ⇒ AttributeError で必要な例外のみに絞る。 destroy_balloonの修正をしてから、試してみます 上記の修正をしてみたところdestroy_balloonを実行後balloonを再実行時にballoonが再出現しなくなってしまったのですが原因わかりますか?その前の修正(18777.txt)では再出現することを確認しています 消した後の再出現はできることを正常な仕様としています 例外処理を2箇所ともExceptionにしてもダメだったのでおそらくroot = tk._get_default_root()と無名関数実行のところを変えたのが原因みたいです destroy_balloonを追加した処理を質問内容のコードに反映させました 念のために確認したいのですが root = tk._get_default_root()はマルチスレッドでの動作を安定させるために使うんですよね、update_balloon内でself.root.after(0, self.root.update)を使うよりも安定するのですか? あまり変わらないのであれば、その前のバージョンを暫定的に使用してみたいと思います balloon(message="Hello World!\nThis is a test message.\nこんにちは", x=300, y=500, font_name="Arial", font_size=14, fore_color="#000000", back_color="#00FFFF", transparency=0.7, show_border=False) time.sleep(3) destroy_balloon() time.sleep(2) balloon(message="Updated Content!", x=300, y=500, font_size=14, back_color="#00FFFF", transparency=0.7, show_border=False) time.sleep(3) balloon(message="Final Update!", x=300, y=500, font_size=14, back_color="#00FFFF", transparency=0.7, show_border=True) time.sleep(3)
hiro12345

2024/12/23 23:26

try、exceptのエラーをAttributeErrorにするほか、 下記の修正を入れるのですよね? (スマホからなので文法変です) これでdestroy_balloonの挙動含め試してみます try tk.global_balloon except AttributeError tk.global_balloon=balloon_class(message, x, y, 略) else root = tk._get_default_root() root.after_idle(lambda: tk.global_balloon.update_balloon(message, x, y,略))
teamikl

2024/12/24 00:03

文章で説明が難しくなってきて、コード読んだ方が齟齬が出なくてよいかなと思ったので ご自分のペースで消化してください。 >destroy_balloonを実行後balloonを再実行時にballoonが再出現しなくなってしまった 今試せませんが、 global_balloon=None ⇒ delatter(tk, "global_balloon") かな? 属性自体は存在して値がNoneなだけでは例外は投げられません。 解りやすくするなら、例外方式ではなく素直にif文で値チェックだけど、 但しここも破棄しなくても再利用でいいかもしれない、withdraw/deiconify で非表示・表示 ---- 08:26 のコメントに関して、 こちらでは  sample 18777 をベースに試してみて、 問題の現象を確認後、上述の修正通りで表示を確認できました。 原因は、after で登録した lambda で包んだ内部のコードは、イベントループ側で実行される為で、 その場の try/excepet では例外は得られず、初回の balloon_create が呼ばれていませんでした。
hiro12345

2024/12/24 03:36

確認したいのですが root = tk._get_default_root()とlambdaでの実行はマルチスレッドでの動作を安定させるために使うんですよね、 self.root.after(0, self.root.destroy) ↑12/23 9:43のコメントですと上記の動作はこれと同じアプローチとのことですが update_balloon内でself.root.after(0, self.root.update)とだけ記述するのとは違うアプローチになるのですか?
teamikl

2024/12/24 06:45 編集

> root = tk._get_default_root()とlambdaでの実行は~ 重要なところが抜けてますが after を用いた実行が、です。意図としてはその通りです。 問題点: 別スレッドから直接GUI操作をすると、 複数のスレッドから同時アクセスで挙動がおかしくなることがあります。 回避策: after 経由での実行は、関数を tkinter 側のタイマーに登録します イベント queue に入れられ、イベントループ側で順番に実行されるので、 イベントループ内では(実際の順序は定かではありませんが、イメージで) 描画イベント ⇒ ユーザ入力イベント ⇒ タイマーイベント ⇒ 他イベント、繰り返し ユーザー定義の関数(今回の lambda の部分)は、イベントループが稼働しているスレッドから、 こういったイベント処理の流れの中で「順番に」呼び出されます。 実行中は他のイベントは待機中になるので、同時アクセスが起こらない、といった感じで機能します。 同期queue に入れることで順番が整列するイメージ。← マルチスレッドでの同時アクセスを回避 同時に、このイベントループの中身の理解が最初の問題のキーポイント time.sleep や時間のかかる処理があると、イベントループに処理が戻らずに 描画イベントやユーザ入力イベントの処理がされずに、応答なし状態に陥ります。 ==== root.after(0, self.root.destroy) は、直接 self.root.destroy() を呼び出すのとは異なり 一旦イベントループに処理を戻してから、destroy を読んでもらうので、 待機中の他のイベントが実行される可能性があります。 提示のコード内では別スレッドからの利用には使われてませんが、 仮に mainスレッドから読んだ場合に、同様の働きをします。 > update_balloon内でself.root.after(0, self.root.update)とだけ記述するのとは違うアプローチになるのですか? 恐らくこの after~update は省略可能です。 root.update() は、一時的にイベントループの実行を進めるみたいな処理です。 今回のコードでは、イベントループは別スレッドの mainloop で稼働中。 ついでに見つけた同じようなコードの self.dummy_label.after(0, self.dummy_label.update_idletasks) # レイアウト情報を更新 は、マルチスレッドで別スレッドでイベントループを処理してるので 実際は更新されてるのかもしれないけど、意図された挙動とは異なります。 '(仮に)シングルスレッドだった場合、after(0, の update_idletasks が呼び出されるのは calculate_label_size関数を抜けてイベントループへ処理が戻った後です。 winfo_width() で実際の幅を所得したい場合に、update() を直前に読んだりしますが winfo_reqwidth() で要求した幅(大抵は同じだけど、実際の幅とはレイアウト次第で異なる場合もあり) を得られてるので、試しにその行をコメントアウトして挙動を確かめてみてください。不要なはず。 ⇒ 可変サイズは、レイアウトマネージャーに任せた方が楽です。
hiro12345

2024/12/24 17:38 編集

ありがとうございます、助言を元に質問内容のコードを修正しました 修正内容は以下の通りです ・calculate_label_sizeを廃止しサイズ計算の方法を変更 ・update_balloonのself.root.after(0, self.root.update)を削除 ・balloon()のtry~except周りの処理を変更 ・destroy_balloon()の処理を変更 ・例外処理をAttributeErrorに変更 destroy_balloonの後も無事再出現できるようにしました destroy_balloonは隠すのではなく、従来通り破棄の仕様にしたいと思っています Tkinterを別スレッドで使う仕様を変えない場合、今のコードなら改善点はもうなさそうでしょうか?
teamikl

2024/12/25 02:40 編集

要点: (コメント内に埋もれたので頭に移動) tkinter を別スレッドで使う場合、プログラムの起動・終了を通じて GUI用のスレッドは1つのみ。 tk.Tk() と mainloop() もプログラムを通じて一つのみと設計してください。 ⇒ tk.Tk()の再生成しない、構造的に tk.Tk() が多重に呼ばれる事がない設計にする。 破棄する仕様にするとしても、Tk() を何度も呼び出す方法ではなく、 ライブラリの初期化と、ウィンドウ(Toplevel) の生成・破棄と区別した方が良いです。 ライブラリの初期化 tk.Tk() は一度にする。メインウィンドウは不要なら非表示に。 一応、気を付けて使うと大丈夫ですが、何かあった時に真っ先に疑われるポイントになるので。 現状問題ないとしても、後の拡張次第で問題を起こしやすい使い方です。 生成破棄する場合、気を付けないといけないことは、 必ず前の root が破棄されてから新たに tk.Tk() を呼ばないと挙動がおかしくなったりします。 例えば、以下のコードは、一見大丈夫そうに見えて 破棄される前に2重に tk.Tk() が呼ばれる「余地」があります。 baloon_destory(...) balloon(...) 事前に baloon_destory を呼んでいるが、但し 実際の root.destroy は after で呼ばれます。 GUIスレッド内に時間のかかる処理があると、期待通り処理されないケースが存在します root.destory 呼ばれる前だが、global はなくなってるので、create_balloon が呼ばれる⇒ tk.Tk が多重に after を使わずに、とか update_idletasks で destory を呼ぶ迄イベントループを進めるとか ここで何か対策を考えたいとこですが、状況はこれがマルチスレッドで別スレッドからの操作になる点 ⇒ スレッドセーフでない操作になってしまいます この方法で拡張を続けるのは、マルチスレッド・プログラミングの理解が無いと厳しく、 気を付けないといけない点が多すぎるので、今となっては極力避けたい設計方針です。 マルチスレッドのデバッグはタイミングに起因するものの場合、 長時間稼働だったり、他での重たい処理・場合によってはネット回線の混雑等が影響する等、問題再現が難しい。 (滅多に再現しないバグ修正よりも、機能拡張等に時間を使いたいはずです) 逆に言うと、タイミングに起因する問題⇒外部環境の影響をうけやすい設計・構造になってしまってます。 まとめると、現状の質問のコードに対する改善案は - root の生成・破棄ではなく、Toplevel の生成・破棄にする。 - tk.Tk() と mainloop はプログラムを通じて一つにする。 根本的な解決策は、完全にtkinterはGUIスレッド内でのみで使用する設計にする。同期queueでスレッド間通信 他の選択肢は、最初提案したように別プロセスにする、 もしくは、他で行ってるメインの処理を別スレッドにする。仕様側をなんとかするしかない。 妥協点を探すなら、他の拡張しない、一度切りのプログラムで短時間ならいいかもしれない。 もし他者が使う想定だったり長時間稼働するものなら、ここは改善した方が良いです。
hiro12345

2025/01/02 14:44 編集

ありがとうございます、返信が遅くなりました >>tkinter を別スレッドで使う場合、プログラムの起動・終了を通じて GUI用のスレッドは1つのみ。 >> tk.Tk() と mainloop はプログラムを通じて一つにする。 別スレッドでTkinterを動かしている場合、メインスレッドでTkinterを動かすことを推奨しないということですか? 今回の場合balloonを使い、以下のような使い方をすると不具合の原因になるということでしょうか 別スレッド(1):balloon 回数カウント用 メインスレッド:tkinter ユーザーフォーム用 別スレッド(2):ユーザーフォームを操作したときの処理 質問内容に掲載のコードについて以下を修正しました ・コードを見直し、tk.Tk()を生成後すぐ非表示に、tk.Toplevelを表示用として生成する仕様に変えました ・またdestroy_balloonをhide_balloonに変えて破棄の仕様から非表示にするようにしました ・calculate_label_sizeの再計算のための生成→破棄については前回の修正分で仕様を変えたので関数自体を既に廃止済です ・(1/2 22;15追記)上記の再計算用関数を廃止したことによるバグを発見したので修正しました ・あと、いろいろ検証してみてモジュール間の共有変数は不要と判断したのでモジュール変数tk.global_balloonも廃止し、普通のグローバル変数にしました ・それに伴いballoonの作成/更新を例外処理からifでの判定に変更しました 上記のコードとは別にmultiprocessingとqueueを使ったものも作ってみました、文字数制限にひっかかってしまうので別リンクになります https://sprightly-fairy-022a61.netlify.app/balloon_multiprocess_14.txt 別プロセスを使う仕様の場合、if __name__ == "__main__":の中にメイン処理を書かないとエラーで動かないようですがそれ以外では解決できない感じでしょうか? if __name__ == "__main__":を使う仕様だと処理全体のインデントをずらさないといけなくなる&UWSCのcallを使って別のファイルを呼び出す処理の再現時、 importで代用すると呼び出し先のpyファイルのif __name__ == "__main__":以下が動かなくなってしまうので ※呼び出し先にif __name__ == "__main__":を書かなければ使えるが、そうすると単独で動かなくなる ↑↑これをずっと考えていましたが一応、逆のアプローチとしてif __name__ != "__mp_main__":を代わりに書けば単独使用、呼び出し時共に問題なく動くので解決できることを確認しました インデントについては妥協するしかなさそうですが... なんとかできないかと、balloon関数の部分だけifブロックで囲んでみたりしましたが、正常に動作しないようです ただし、これにも問題があり、呼び出す側のpyファイルでif __name__ == "__main__":またはif __name__ != "__mp_main__":のブロック中でimportを実行しなければ 呼び出される側でif __name__ != "__mp_main__":の中に処理が書かれていてもエラーが発生するようです
teamikl

2025/01/03 12:00

>今回の場合balloonを使い、以下のような使い方をすると不具合の原因になるということでしょうか ... > 別スレッド(2) ユーザーフォームを操作したときの処理 別スレッド2 が何か次第ですが、 - tk.Tk() のインスタンスは「プロセス内で」ひとつのみ (複数創る場合に別プロセスに別ける)  multiprocess のコードは、状況によっては tk.Tk() が複数回呼ばれる構造ですが  別プロセスなのでその点は大丈夫。別スレッドでは問題になる可能性が残りました。  ただし、ここは別プロセス・別スレッドよりも、  tk.Tk() を複数呼び出す構造・設計自体を見直す事でも改善可能です。  ⇒ root の生成・破棄の代わりに Toplevel の表示・非表示 ここが文章ではうまく伝わってないなと思うポイントですが、、 非表示・表示はひとまずokで、tk.Tk() の再生成について - create_baloon ないで tk.Tk()/ mainloop <-これを1度のみにしたい  - create_balloon は balloon_classクラスのコンストラクタから   - balloon_class は balloon 関数から    - そして balloon 関数は複数回呼ばれます。 通常は最初の一度のみを想定してますが、プロセス・スレッドが何らかの問題を持つとき 初期化処理が再度呼ばれてしまう可能性が残ってます。 ⇒ tk.Tk() はプロセス内で一度のみが保証できる場所で呼び出した方良い あとは、root.after_idle をメインスレッドから使ってるとこですが (ここは妥協案通り) スレッド版でも multiprocessing と同様の queue を用いるアプローチにすると 別スレッドからの tkiner への直接アクセスを避けることができます。 今回の使い方では起こりませんが、tkinter が破棄されてるのにメインスレッドからtkinterへアクセス といった状況を、構造的に排除することができます。⇒ より安全なコードに。 ---- コードは実行出来てませんが、見た感じ。細かい点をいくつか挙げると - while not queue.empty(): は、キューが溜まっていると GUI をフリーズさせる可能性があります。 解決策: 一度の呼び出しで全てを処理しようとせずに、イベントループに処理を戻す。  基本的に GUI 操作スレッドでのループ処理は一度に行わず、タイマーで分割します。 - after(1, ... ) 計測してみると解りますが、1ms は、実際には他のイベント処理もあるので  大抵の環境ではタイマーの精度はそんなによくなく 15ms前後(60FPS程度)周期 です。  実際に秒間 1000回もGUIの生成コードが呼び出されると物凄く重たくなります。  用途次第ですが、 30-50ms 程度でいいと思う。逆にそれ以上の精度を求める場合は tkitner では不足  ※ hide() の after(0, ...) とは状況が異なり、タイマーで頻繁に呼び出される処理での after(1ms 数値は少ない方が遅延が少なく滑らかと思われるかもしれませんが、(FPSの理想値自体はその通り) 時間のかかる処理(GUI生成等)をあまりにも頻繁に呼び出す場合、 実行環境に依存していて、処理が追いつかない場合は遅延・カクツキの原因になることがあります。 ⇒ 適切な値にすることで呼び出し回数削減⇒負担軽減で解消。目安 30-60FPS(32~16ms) ---- マルチプロセスの制約に関しては、windows では他に選択肢はなかったはず。 一応、再度念を押しておくと、tk.Tk() が再生成される可能性のある設計を避けられれば、 マルチプロセスにする必要性はなくなります。(現状の、質問の情報で把握してる限りでは)
hiro12345

2025/01/03 16:55

>>通常は最初の一度のみを想定してますが、プロセス・スレッドが何らかの問題を持つとき、初期化処理が再度呼ばれてしまう可能性が残ってます。 コンストラクタのスレッド生成において try: dummy = threading.Thread.is_alive(self.thread) except Exception: self.thread = ~略 self.thread.start() create_balloonにおいても try: self.root.withdraw() except Exception: self.root = tk.Tk()以下の処理 とすれば初回は参照エラーで例外処理が実行されますし、仮に初期化処理が再度呼ばれてしまった場合はtryブロック内の参照処理が実行され、再生成されることはなくなりそうなのですがどうでしょうか? >>今回の使い方では起こりませんが、tkinter が破棄されてるのに~~ update_balloonについてはqueueを使わなくても処理全体をtry-exceptで囲むことでrootが強制終了してしまった場合でもアクセスしてエラーが出ることを防げそうですがどうでしょうか? >>while not queue.empty(): は、キューが溜まっていると GUI をフリーズさせる可能性があります。 >>解決策: 一度の呼び出しで全てを処理しようとせずに、イベントループに処理を戻す。 >>基本的に GUI 操作スレッドでのループ処理は一度に行わず、タイマーで分割します。 よくわからなかったのでChatGPTに聞いてみたところ、whileループだとキューの中身がなくなるまで処理を続けフリーズする可能性を指摘されました そこでwhile not queue.empty()をif not queue.empty():に変え、ついでにself.root.after(1~の1を30に変えてマウス座標の追跡プログラムを動かしてみたところ 数秒のタイムラグが生じるようになってしまいリアルタイムにGUI表示が更新されなくなってしまいました while not queue.empty():のままならroot.afterを30にしてもリアルタイム更新されますし、if not queue.empty()でもroot.afterが1ならリアルタイム更新されるようです
teamikl

2025/01/03 21:44

インデントの妥協のとこは、ソースコードのトップレベルにコードを記述したいという事かな。 multiprocessing/windows では、起動方式の都合、コードを2重に実行してしまいます。 これは 仕様通りの挙動で、多分 windows では他に選択肢がありません。 WSL上の別OSで実行とかそういった大掛かりな話になってくる。 一応、仕組み的にはプロセス間での排他制御(2回目実行されたときに大丈夫なように) 出来ればいいのですが、これも割と大掛かりな仕組みの導入が必要になってくるので、 簡易的な対策方法が if __name__ ~による判別です。 一旦、要望と問題解決は切り分けて対処しましょう。 トップレベルに記述したいという要求を通したい場合は、 windows の場合はマルチプロセスにしない方が無難な選択枝です。 (出来ないわけではないが作業量が増えます。そして限られた文字数で説明できる規模ではありません) そうすると、tkinterでの問題が再発する可能性が出てきますが、 上のコメントに書いたように、tk.Tk() の再生成さえ抑制できれば解消できます。 マルチプロセスの提案は、「再生成されたときに」変な挙動にならないようにだったので。 ⇒ 根本的な原因を取り除くことで解決。 但し、マルチプロセス対応のコードも無駄ではなく、queue を用いたメッセージのやり取りは そのまま (multiprocessing.Queue ではなく通常の同期queue で) マルチスレッドでも流用できます。 可能なら root.after で別スレッドから制御する よりも queue を通じたやり取りの方が、より安全です。 マルチプロセスでは別プロセスからの直接アクセスは、プロセスが別れている為無理ですが、 マルチスレッドでは同一プロセス内なので、「別スレッドのリソースに同時アクセスしない」 という運用ルールを守るのはプログラマの責任です。 queue 方式にしたから安全、という訳ではなく、 「スレッドを跨ぐtkitnerへのアクセスを全て排除すること」で初めて tkinter のスレッドセーフな運用が可能になります。 queue 自体は tkinter とやりとりするスレッド間通信の手段という立ち位置で。 queue を使っていても他所で別スレッドからの tkinter へのアクセスがあれば崩れてしまいます。 (そして、それを検出するのは、どの関数がどのスレッドで実行されているかの把握が必須。  logging モジュールで実行してる threadName を表示させたりして確認します) ※ queue はスレッドセーフな設計なので、複数のスレッドから扱うことが可能です。 他に、print() 等は、基本はスレッドセーフですが、IDEでのデバッグ実行等では保証されてないこともあります ⇒ マルチスレッドでのデバッグは print 代替に logging モジュールを使ったりします。 よく使う snippet 残しておくと import logging logger = logging.getLogger(__name__) def worker(): logger.debug("test") # <- logger は別スレッドから跨いで使っても大丈夫。print代替 if __name__ == "__main__": # ここのインデントは読み替えて logging.basicConfig( level=logging.DEBUG, format="[%(threadName)-10s][%(levelname)-8s] %(message)s") ...
teamikl

2025/01/03 22:30

リロードしてなかったのでコメント返信が入れ違いに。 > 再生成されることはなくなりそうなのですがどうでしょうか? 何度も呼ばれる可能性がある場所で初期化されてることが問題で、 そういった条件・状況の確認作業・検証が必要になってきます。 tk.Tk() はそういった分岐内ではない方が良いのだけど、 おそらくここも UWSC みたいなコードでというところ と スレッド化による制約で 手段が限られてるのだろうなと思います。 マルチプロセスでは、再生成されても問題ない構造ですが、 マルチスレッドの場合は、スレッド自体が再生成されることになるので、 tk.Tk() だけ再生成を抑制しても、もっと複雑な状況になりそうです。 > if not queue.empty():に変え while ループも問題だけど empty 自体も empty~get の間に queue の中身が変わってる事もあり得ます(可能性として) ここは事前に empty チェックをしなくても、 get_nowait を try/except で捕まえ queue.Empty の例外だけ読み捨てます。 遅延の原因は、利用側のコードの ループ内 time.sleep(0.01) の遅延の値、 こちらも合わせて調整しないと、処理できる量以上のメッセージが溜まって遅延に繋がるみたいです。 ここも 0.03 位でも見た目的に支障はないはずですが、0.01 でも動かせるようにしたいとなると queue のメッセージ読み込みのタイマーでは情報を更新だけして GUI 操作はしない。 別のタイマー 15-30ms周期で GUI の更新とすると、見た目は変わることなく処理負担も軽減できます。 別海は、tkinter の <Motion> イベントでマウスの追跡を行った方が根本的な解決策。 (その場合、UWSCみたいな sleepでの記述とはいきませんが) > >>今回の使い方では起こりませんが、tkinter が破棄されてるのに~~ > update_balloonについてはqueueを使わなくても処理全体をtry-exceptで囲むことで > rootが強制終了してしまった場合でもアクセスしてエラーが出ることを防げそうですがどうでしょうか? これでいいなら、妥協ポイントとしてはいいと思いますが、 except Except: にすると他の例外メッセージも虚空に消えてしまうことになり、 このコードが増えるとデバッグするときに大変になっていきます。 ここの try/except で例外が消えてしまってないかとか、コードを修正して調べる羽目に。
hiro12345

2025/01/06 10:58

ありがとうございます、要するにtry-exceptを多用するとデバッグが大変になるということですよね しかしコードの書き方を優先するとなると妥協も止む無しです 質問内容のコードを修正したものに編集しました threadingを使用したコードについてですが、その後rootを表示状態にして処理中に意図的に終了させてデバッグをし、 マウスの座標の表示プログラムでループ中にprintを使い、rootが強制終了した場合にどんな影響があるのか調べました 従来のコードでは、どうやってもrootを強制終了させた時点でメインスレッド側の処理が止まってしまい、printが実行されなくなってしまいました ifやtry-exceptを使ってなんとかできないか試みましたが、やはりダメでした メイン処理に影響が出てしまっては元も子もないのでソースコード全体の構造を変えました、かつmain側の関数の書き方自体はそのままを維持しました やはり、別スレッドでrootを生成した後にメインスレッド側でrootを更新させる仕様が仇となっていたようでした 修正後はrootを強制終了させてもメインスレッド側の処理の継続には影響が出ないことを確認しました queueでなくクラス変数を使用しているため、root.afterを30にしていますが遅延はほぼありません 修正点は以下の通りです ・マルチプロセス用のコードをベースにし、GUIの更新は別スレッド側でループで変数を読みに行く仕様に、メインスレッドからGUIの操作はせず変数を更新するだけです マルチプロセスとは違い、スレッド間ならグローバル変数またはクラス変数は共通ですし、queueを使うと複雑なのでクラス変数を使ったシンプルな仕様にしました また、前回引数との比較をループ内で行い引数が変わった場合のみGUIの更新処理を行うようにし、なるべく重くしない工夫をしました ・balloonの作成/更新の分岐にif not isinstanceを使うようにし、分岐処理をより確実なものとしました ・念押しでスレッド生成時と、rootの生成時にtry-exceptを使うようにし上記の分岐がうまくいかなかった際も2重生成がされないようにしました 完全なスレッドセーフではないかもしれませんが、落としどころとしては今回の修正はいい線をいっていると思います、いかがでしょうか?
teamikl

2025/01/06 16:57

現状動いてるプログラムで要件を満たせているなら、落としどころとしてはそれでよいと思います。 他の指摘等は、かなりオフトピになってる気がするので、 この質問内で消化しなくても、プログラムを運用・改良を重ねていくうえで、 実際に問題が起こった時にでも思い出してくれればいいかな、くらいのつもりで書き綴ってます。 ----- > 別スレッドでrootを生成した後にメインスレッド側でrootを更新させる仕様 スレッドを跨ぐ 直接のGUI の操作は NG です。tkinter に限らず。 winfo_exists も tkinter のリソースへのアクセスがメインスレッド上で発生してます。 前のコードでは after を使い GUIスレッド側で更新させてたはずですが、、違ったかな。 >スレッド生成時と ... 二重生成がされないようにしました 生成時のチェックが良く分からなかったです。 self.root や self.thread はインスタンス変数、 未定義なので常に AttributeError となるはずです。 root を破棄すると別のエラーがでたので、試せませんでしたが print("Error: スレッド生成メソッドが2回以上参照されています") の出力を確認されましたか? クラス変数であればチェックの意図は解りますが、他の文章で 「クラス変数を使ったシンプルな仕様にしました」 でコードで実際に使われてるのは「インスタンス変数」です。 用語もしくはpythonでのクラス変数の記述方法の認識で齟齬が発生しているかも。 ---- > スレッド間ならグローバル変数またはクラス変数は共通ですし、 定数なら良いのですが、値が変動する変数を複数スレッドで共有するのは 想定外の挙動の原因になる可能性があるので危険、問題のあるコードです。 queue を使った方が、結果的にマルチスレッドではシンプルな構造に出来ます。 (A) read_~や show/hide を after(...) で GUIスレッドから呼び出すようにする  ⇒ イベントは順番に実行されるため、update~ と同時に実行されることがなくなる (B) threading.Lock で、共有変数にアクセスする前に排他制御 ⇒ 同時アクセスを発生させない (C) queue を使う スレッドセーフでない変数の共有が、どの様な問題かが把握しにくいと思いますが、 if flag: print(flag) これで False と表示されることが、マルチスレッドだと起こりえます。  ⇒ if のチェックを通過した後に別スレッドから値が書き換わる つまり、どういうことかというと、 例えば、引数が変わった場合のみ~のチェックは、シングルスレッドでは正しいロジックなのかもしれません ですが、マルチスレッドで変数の同時アクセスが発生する状況では、そのロジックが破綻する可能性が生まれてきます。 実際には現状のコードは大丈夫なのかもしれませんが、コードを変更するたびに精査が必要で こういったことを意識しないといけなくなるという状況自体が大きなデメリットです。 queueやafter を用いた方法では、構造的にそのようなことを気にする必要をなくせます。 より良い設計の為には、 - スレッドを跨いで使ってよいのは、スレッドセーフに設計されたもののみ queue, logging 他、同期プリミティブ (threading の Event/Lock/Semaphore 等) - 別スレッドへの通信には queue を用いる。
hiro12345

2025/01/07 03:59

ありがとうございます、とりあえずこれで使ってみて、変なところがあれば直す方針でいきたいと思います。 ご指摘に対する回答ですが >> 前のコードでは after を使い GUIスレッド側で更新させてたはずですが GUI自体が内部的にどう動いてるかを抜きにして説明すると以下の違いになります 従来の仕様:tk.tk()は別スレッドで定義、mainloopも別スレッドで実行 更新時のafter、update関数内のplaceはメインスレッドで実行(メインスレッド側にコードを記述) 新仕様:定義、更新処理全て別スレッド内で完結させる、メインスレッドからは引数とするインスタンス変数の値の変更をするだけ GUIスレッド側はウインドウ生成後にループ処理を行い、インスタンス変数の変更を常に監視し、変更があれば更新処理を行う >> winfo_exists も tkinter のリソースへのアクセスがメインスレッド上で発生してます。 ここは盲点でした、しかし、変更しているわけではなく変数を返してるだけからか特に問題はないようです、例外処理で参照エラーの対策もしていますし デバッグした限りだとメインスレッド上で別スレッドで定義したGUIの変更処理をしてしまうことがまずかったようです >>生成時のチェックが良く分からなかったです。 万が一ifをすり抜けてしまった場合、初回の場合はエラーが発生し例外処理の生成処理が実行、2回目以降にthread、rootにアクセスされた場合は正常に参照できてしまい、例外処理が実行されなくなります これを利用した2回目以降にコンストラクタが参照されてしまった場合の対策です しかし通常はballoonのifで2回目以降は弾くので"Error: スレッド生成メソッドが2回以上参照されています"についてはコードの仕様上通常表示されることはないかと思います、実際確認していません >>用語もしくはpythonでのクラス変数の記述方法の認識で齟齬が発生しているかも 私の勉強不足で正しくはインスタンス変数でした >>if flag: print(flag) これで False と表示されることが、マルチスレッドだと起こりえます。 これ自体は作成時に危惧したため対策としてself.should_redefine_sub_window以外一旦ローカル変数に代入し、そのローカル変数を使った処理をするようにしています 比較前にインスタンス変数をローカル変数new_argsにリストとして代入、比較後はインスタンス変数を直接参照せずリストを元に再代入のところですね self.should_redefine_sub_windowは1回しか参照しないので直接参照で問題ないと判断しました
teamikl

2025/01/07 06:20 編集

> 更新時のafter、update関数内のplaceはメインスレッドで実行(メインスレッド側にコードを記述) (念のための解説ですが) after の呼び出しはメインスレッドからでしたが、 after に登録した関数は、GUIのスレッド側から呼び出されます。 その為、queueを用いたときと同様(tkinter内部のイベントqueueを利用する形で) スレッドセーフにはなってました。 (妥協点: root.after での root の参照を除く、別スレッドからのtkinterへのアクセス) 「メインスレッドで実行」はメインスレッド側にafter を記述したという意図ですよね。 以前のコードでも update の実行自体は after によりGUIスレッド側で行われていたはずです。 > しかし、変更しているわけではなく変数を返してるだけからか特に問題はないようです、 ここの問題点は、エラーやクラッシュがなかったから動作に支障がない・問題ないではなく、 exists チェックだけど、タイミングによっては存在していなくても True もしくはその逆の状況が マルチスレッドでは起こりえる点にあります。要はコードのロジックが保証されなくなる点です。 その結果、通常利用の挙動には影響がないこともありますし、 それによりおかしな挙動を引き起こす可能性もあります。(現状大丈夫でもここは一旦論点ではない) ⇒ 可能性があり、個別に検討が必要になる事自体がデメリットという問題点です。 但し、早急に修正が必要かというと、現状問題ないなら大丈夫ですが、 マルチスレッドのプログラムでは、 意図してない箇所のコードが原因で意味不明な挙動をすることがあります。 if や while の条件が全部でたらめになるような状況を想像してみてください スレッドセーフでないマルチスレッドのコードは、 そういった危ない状況で、一見問題なく動いてるだけに過ぎません。 スレッドセーフでないコードが増えていくと、その全てが修正対象になる為、 デバッグや修正のコストが増していくことに注意してください。 実際に体感しないと解らないかもしれませんが。 queue を用いなくてもスレッドなら簡単に実装できる、、と思われたなら それはマルチスレッドでの落とし穴です。結果的には queue を用いた方が楽できます。 もしくは、queue の代替手段・妥協案が after を使う方法でした。 ---- > コードの仕様上通常表示されることはないかと思います おそらくクラス変数・インスタンス変数の挙動の認識が違ってるため、 try/except で「インスタンス変数」へのアクセスを試してみるロジック自体が 多重起動のチェックとして機能してません。 2回目以降に拘わらず、常に except が実行されるようになってます。 >通常はballoonのifで2回目以降は弾くので こちらのチェックが通常は機能してるので、影響はないかもしれませんが。 self.root や self.thread はインスタンス変数へのアクセスになってるので、 2回目以降でも未定義です。⇒ 「クラス変数」になってれば、多重起動チェックですが Python ではクラス変数へのアクセスは self.__class__ や クラス名を通じて行います。 (self.~でもアクセスできますが、インスタンス変数と混同して問題になりやすい為避ける傾向があり) ==== 多分、今回の変更も自分のコメントの返信を受けての対応なのでしょうね、 色々と文章の説明では補えなかった箇所があったりします、排他制御周り。 こういった事があるから文章での説明ではなく具体的なコードでの提示という形にしたかったのですが。 queue => スレッドを跨ぐ変数の共有 になってしまった個所は 「排他制御」としか言及してなかった所で、 queue はマルチプロセスのプログラムで使えていたので それをスレッド版でも同様に使えれば排他制御に関しては大丈夫かなと思ってました。
hiro12345

2025/01/07 16:45

やはり共通変数へのアクセスは控えたほうがいいのですね queueを使うと蓄積により遅延が発生してしまうと思っていたので使用を控えていましたが、そうならないようにする方法を思いついたので実装してみました 質問内容のコードの所に反映させました 主な修正として マルチプロセス用のコードをベースにqueueを使った仕様に変更 balloonまたはhide_balloon関数内でputをする前にif balloon_queue.empty():で判定することで、queueが必要以上に蓄積するのを防ぎました update_balloon内ではtry-exceptで直接キャッチする仕様です →以前のqueueを使用した仕様だとroot.afterを30にしてしまうとかなりの遅延が発生していましたが、今回は30でも遅延ほぼなし、実行側コードにtime.sleep(0.01)があっても影響なし ついでの修正として winfo_exists()はupdate_balloon内で取得しても問題なかったためスレッド内で完結するように仕様変更しました スレッド生成とroot生成時にクラス変数を使った分岐処理を行い、2重参照をしないようにしました if queue.empty():をqueue.putの前に置いただけで使用感が劇的に変わりました、これでしたらスレッドセーフかつシンプルな仕様にできそうです
teamikl

2025/01/08 00:59 編集

> queueを使うと蓄積により遅延が発生してしまうと思っていたので使用を控えていました これの改善案が、 タイマーをUI更新用と値更新用に別けるというアプローチでした。 勿論、処理速度に限度はありますが。 恐らくwindwsでのタイマーの最短サイクルが 15.6ms 仮に after 1ms 指定してもそれくらいの頻度でしか処理ができません。 ⇒ queue.put の頻度/sleep の間隔が queue の消化速度を超えると遅延に。 > if queue.empty():をqueue.putの前に置いただけで使用感が劇的に変わりました、これでしたらスレッドセーフかつシンプルな仕様にできそうです queue.put の頻度を落とすのは、解決策として正しい方向です。 queue.get 側の処理頻度を高めるのは、GUIの応答速度低下につながる為。 実際に改善出来てるので、実装・対策はこれでよいのですが、 コメント内でのスレッドセーフの認識に対して queue はスレッドセーフに設計されたクラスなのですが empty の使い方がスレッドセーフではありません。 queue.empty がスレッドセーフなのは、empty 内だけで、 empty 呼び出し後に queue の状態が変わる可能性あります。 >、これでしたらスレッドセーフかつシンプルな仕様にできそうです 「スレッドセーフ」かというと、そうではなく潜在的な問題になり得ます。⇒ if queue.empty(): マルチスレッドの話では、文脈的に、現時点ではスレッドセーフかどうかではなく、 コードの構造自体がスレッドセーフかどうかという事が多いです。 なぜなら問題が起きたときに、スレッドセーフな構造でない全てのコードがデバッグの調査対象になるからです。 ==== 恐らく sleep する時間の適切な調整だけでも解消は出来るはずです。 empty チェックにより読み飛ばされてる間のループは、無駄になってるので。 ここも「queue の消化処理速度の問題」と 「sleep の値が短くてもスムーズに動くようにしたい」という要望とで区別して取り組みましょう。 問題の原因は、queue の消化速度の限界 15.6ms (実際には 16ms 約 60FPS) sleep 10ms でも動くようにしたい場合、 (その前に、本当に必要な要件かどうかを考えよう。  sleepの値調性で済むのに実装コストに見合う恩恵があるか) empty チェックで改善できたように、 過剰なデータ挿入を適度に間引くことで実現できます。 他の実装案は幾つか提案出来ますが、 解決策というよりは、一時凌ぎみたいな方法ばかり。⇒他の用途で問題になりそう (A) queue生成時に maxsize=1 指定すると、  put でロックが掛かります。これは put 内部で完結するのでスレッドセーフです。  ⇒ 過剰な無駄なループを抑制できる ⇒ 副作用としてループ頻度は減ります  ⇒ リスク: デッドロックで止まる可能性。GUIが落ちたとき queue 消化が滞り、put でブロック状態に   ⇒ 適切に復帰処理をすることで再開可能。    逆に言うと GUI が落ちたときにメイン処理を put で一時停止しておけます。   副作用: sleep 10ms としても実際のループ周期は queue の消化頻度に依存 (B) rxpy というライブラリを使うと  大量のデータを一定間隔で間引く処理が簡単にかけたりします。  GUI アプリでこういった処理に実際に使われてたりした。  ⇒ 学習・導入コスト高め (追記) UWSC みたいな記述というレギュレーションからも外れてしまう (C) empty チェックではなく、5回に一回 put する等で  queue への put を間引く。  ⇒ ループ側のコード弄ってもいいなら、sleepの値調整が一番楽な妥協案 個人的には、業務系アプリ想定なら 30~60fps 適切なsleepでいいのではと思ってます。モニターのリフレッシュレート的にも。 それ以上の頻度(FPS)での要求となると tkinter のタイマーでは厳しくて 10ms => 100fps になってくると ゲーム系のライブラリで実装した方がいいかもしれません。
hiro12345

2025/01/11 00:09

ありがとうございます、このコードで実装してみることにするのでベストアンサーに選ばせていただきました、細かい仕様について様々なご指摘をいただき、大変勉強になりました。
guest

回答2

0

イメージ説明

Pythonに移植したいものがありコードの書き方も可能な限り同一性を保ちたいので

という意図を組んで、以下のような形でどうかな。
実行可能なコードは後述。追加で必要な依存ライブラリはありません。

UWSCからの移植に相当する部分は

python

1@uwsc.task 2def mouse_tracker(uwsc): 3 balloon = uwsc.balloon(x=100, y=100, bg="#ffff00") 4 5 while True: 6 x, y = uwsc.mouse_position 7 balloon(message=f"マウス座標: ({x:4}, {y:4})") 8 yield uwsc.sleep(0.1)

python

1@uwsc.task 2def headline(uwsc): 3 balloon = uwsc.balloon(x=20, y=20, alpha=0.7, font=("", 40)) 4 5 balloon(message="First") 6 yield uwsc.sleep(3) 7 8 balloon(message="Second") 9 yield uwsc.sleep(3) 10 11 balloon(message="Final") 12 yield uwsc.sleep(3) 13 14 uwsc.quit() # 終了

更に balloon を事前準備不要・ビルドイン関数にしたいみたいな事も工夫すれば可能だけど、
簡単に(200行程度で)実装出来そうな範囲ではこのあたりで妥協。

コメントの内容や提示されたコードを見てると、
問題の本質「マルチスレッドでの同期処理」や「GUIでのイベントループ」の問題と、
要望である 「移植元のコードと書き方を可能な限り同じにしたい」を同時に満たそうとされてるので、
ひとつひとつ徐々に解決・改善してみてはどうでしょう。

マウス座標表示をタイマーで実装から、

python

1import tkinter as tk 2 3def update_mouse_position(): 4 x, y = root.winfo_pointerxy() 5 label.config(text=f"Mouse position: ({x}, {y})") 6 root.after(100, update_mouse_position) 7 8root = tk.Tk() 9root.geometry("+10+10") 10root.configure(bg="#00ffff") 11root.overrideredirect(True) 12root.attributes("-topmost", True) 13root.attributes("-alpha", 0.7) 14label = tk.Label(root, text="", bg="#00ffff", font=("Arial", 14)) 15label.pack(fill=tk.BOTH, expand=True) 16update_mouse_position() 17root.mainloop()

このコードをそのまま別のコードと一緒に再利用したいとなると、tk.Tk() 部分が複数回呼ばれることになるので
マルチプロセスにした方が良いのですが、コードを改変可能なら tk.Toplevel を用いることで
ライブラリの初期化とウィンドウ生成を分離できます。⇒ tk.Tk() と mainloop は一度のみにする。

また、コードの書き方を移植元と同じというのも、time.sleep相当の部分をジェネレーターにする
先の回答で紹介した gen_timer のアプローチで対応できます。⇒ yield の箇所

python

1from gentimer.timer_tk import gen_timer # pip install gentimer 2 3def update_mouse_position(): 4 while True: 5 x, y = root.winfo_pointerxy() 6 label.config(text=f"Mouse position: ({x}, {y})") 7 yield 0.1 8 9... # 略 10 11gen_timer(root, update_mouse_position()) 12root.mainloop() 13

以下で提示するアプローチは、uwsc の簡易版みたいなのを python/tkinter で実装する方法。


サンプルでは、3つのタスクを同時に実行してます。

2つは上述のタイマーで稼働するジェネレーターとして実装、

もうひとつは後述のソース内 @background_task、「別の時間のかかる処理」想定で
別スレッドで動作&別スレッドからGUIスレッドへ安全にGUIへ反映させる方法。

※ 別スレッドからGUIスレッドへの通知は after メソッドを通じて
tkinter のイベント・キューをスレッド間通信に代替利用します。
tkinter のマルチスレッドのサンプルでは割とみられる方法ですが、
after の呼び出し自体はスレッドセーフではありません。
幾つか注意事項が発生して、ウィンドウを閉じた後や終了時の処理で気を付けないとエラーになります。
詳細な説明は後述

完全なスレッド安全な設計にする為には、別途、同期キューを用いたりします。ここでは未実装

python

1import tkinter as tk 2from dataclasses import dataclass 3from operator import attrgetter 4from threading import Thread, Event, RLock 5_get_geometry = attrgetter(*"x y width height".split()) 6 7def gen_timer(root, gen, timer=None): # 簡易実装版、キャンセル可能なタイマー 8 def _step(): 9 nonlocal timer 10 interval = next(gen, None) 11 if interval: 12 timer = root.after(int(interval*1000), _step) 13 else: 14 timer = None 15 def _cancel(): 16 nonlocal timer 17 if timer: 18 root.after_cancel(timer) 19 timer = None 20 root.after_idle(_step) 21 return _cancel 22 23@dataclass 24class BalloonOptions: # オプション、引数に書くのが大変なので纏める 25 message:str = "" 26 fg:str = "#000000" 27 bg:str = "#ffffff" 28 x:int = -1 29 y:int = -1 30 width:int = -1 31 height:int = -1 32 alpha:float = 1.0 33 font = ("", 24) 34 timeout: int = -1 35 36class Balloon: 37 def __init__(self, parent=None, name="", **kwargs) -> None: 38 try: # name指定で以前生成したウィジェットを再利用 39 self._window = parent.nametowidget(f"{name}") 40 self._label = parent.nametowidget(f"{name}.!label") 41 except KeyError as _: 42 window = self._window = tk.Toplevel(parent, name=name) 43 window.overrideredirect(True) 44 label = self._label = tk.Label(window) 45 label.pack(fill=tk.BOTH, expand=True) 46 self._options = BalloonOptions(**kwargs) 47 48 def update(self, **kwargs): 49 vars(self._options).update(kwargs) 50 51 def redraw(self): 52 window, label, options = self._window, self._label, self._options 53 label.config( 54 text=options.message, 55 font=options.font, 56 fg=options.fg, 57 bg=options.bg, 58 ) 59 x, y, width, height = _get_geometry(options) 60 if x >= 0 and y >= 0: 61 window.geometry(f"+{x}+{y}") 62 if width >= 0 and height >= 0: 63 window.geometry(f"{width}x{height}") 64 window.configure(bg=options.bg) 65 window.attributes("-alpha", options.alpha) 66 if options.timeout >= 0: 67 window.after(int(options.timeout*1000), self.hide) 68 self.show() 69 70 @classmethod 71 def create(cls, parent=None, name=None, **kwargs): 72 balloon = cls(parent, name=name) 73 balloon.update(**kwargs) 74 balloon.redraw() 75 return balloon 76 77 def hide(self): 78 self._window.withdraw() 79 80 def show(self): 81 self._window.deiconify() 82 83 def __call__(self, message, **kwargs): 84 self.update(message=message, **kwargs) 85 self.redraw() 86 87class UWSC: # とりあえずの命名。バルーン、タイマー管理、スレッド管理。 88 def __init__(self): 89 root = tk.Tk() 90 # root.withdraw() 91 self._root = root 92 self._tasks = [] 93 self._threads = [] 94 self._event = Event() 95 self._mutex = RLock() 96 97 def sleep(self, interval): 98 return interval 99 100 def thread_sleep(self, interval): # NOTE: thread-safe 101 if __debug__: 102 from threading import current_thread 103 assert current_thread().name != "MainThread", "don't call me in MainThread" 104 self._event.wait(interval) 105 106 @property 107 def tk_root(self): 108 return self._root 109 110 @property 111 def mouse_position(self): 112 return self._root.winfo_pointerxy() 113 114 def gentimer_task(self, func): # Decorator 115 self._tasks.append(gen_timer(self.tk_root, func(self))) 116 task = gentimer_task # alias 117 118 def background_task(self, func): # Decorator 119 thread = Thread(target=func, args=(self,), daemon=True) 120 self._threads.append(thread) 121 self.tk_root.after_idle(thread.start) 122 123 def emit(self, func, *args, **kw): # NOTE: thread-safe 124 with self._mutex: # <-- ここの同期が正常に保てればスレッド安全 125 if self.is_running: 126 self.tk_root.after_idle(lambda: func(*args, **kw)) 127 128 def run(self): 129 root = self.tk_root 130 131 def _on_closing(): # 閉じるボタン時のアクション。※quitでは呼ばれない 132 self._stop() 133 for cancel in self._tasks: 134 cancel() 135 root.update_idletasks() 136 root.destroy() 137 138 root.protocol("WM_DELETE_WINDOW", _on_closing) 139 140 root.mainloop() 141 142 self._stop() 143 for thread in self._threads: # mainloop終了後なので、ブロッキング操作が使える 144 thread.join() # スレッドの完了を待つ。 145 146 def balloon(self, **kwargs): 147 return Balloon.create(self.tk_root, **kwargs) 148 149 def quit(self): 150 self._stop() 151 self._root.quit() 152 153 def _stop(self): # NOTE: thread-safe 154 with self._mutex: 155 self._event.set() 156 157 @property 158 def is_running(self): # NOTE: thread-safe 159 with self._mutex: 160 return not self._event.is_set() 161 162uwsc = UWSC() 163 164@uwsc.background_task 165def time_sleep(uwsc): 166 # ここは別スレッドで実行。マルチスレッドGUIプログラミングの作法で、 167 # イベントループの外・別スレッドから GUI操作を直接実行してはいけません。 168 169 def report_step(num): # nameを付けて再利用 170 uwsc.balloon(name="time_sleep", message=f"作業進捗: {num=}", x=400, y=200) 171 172 def done(): # NOTE: ここは、イベントループ側から実行される ⇒ GUI操作可能 173 uwsc.balloon(name="time_sleep", message="終了", width=200, height=200, timeout=3) 174 175 # 何か重たいタスク (仮で1秒スリープx6回) 176 for num in range(6): 177 if not uwsc.is_running: # スレッドを中断可能にする 178 return 179 180 # ここでブロックするが、メインスレッド側から Event.set で中断可能 181 # threading.Event.wait での time.sleep 代替実装 182 uwsc.thread_sleep(1) 183 184 uwsc.emit(report_step, num) # 進捗報告 185 186 uwsc.emit(done) # 完了通知 (イベントループ側に実行してもらう) 187 188@uwsc.task # イベントループからタイマーで実行されるので、直接GUI操作が可能 189def mouse_tracker(uwsc): # 関数内部はジェネレーターとして実装。gentimerでは、yield でイベントループに処理を戻せます 190 balloon = uwsc.balloon(x=100, y=100, bg="#ffff00") 191 192 while True: 193 x, y = uwsc.mouse_position 194 balloon(message=f"マウス座標: ({x:4}, {y:4})") 195 yield uwsc.sleep(0.1) 196 197@uwsc.task 198def headline(uwsc): # NONE: 3x3秒後にプログラム終了 199 balloon = uwsc.balloon(x=20, y=20, alpha=0.7, font=("", 40)) 200 201 balloon(message="First") 202 yield uwsc.sleep(3) 203 balloon(message="Second") 204 yield uwsc.sleep(3) 205 balloon(message="Final") 206 yield uwsc.sleep(3) 207 208 uwsc.quit() # プログラムを終了 209 210if __name__ == '__main__': 211 uwsc.run()

投稿2024/12/23 22:21

編集2024/12/24 03:33
teamikl

総合スコア8794

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

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

teamikl

2024/12/23 22:22

文字数制限に引っ掛かったので、続き ## after メソッドを利用したスレッド間通信について。詳細と注意点 TL;DR: queue利用の簡易版・妥協設計です、注意して使えば安全で問題ありません。     コードが規模が大きくなると、注意点が増えるのが面倒なので別の方法を推奨。 (オフトピ気味&この内容をコードへ反映するのは作業量が多いので一旦スルーでも大丈夫、将来の課題として) マルチスレッドのデバッグで面倒なのは、 問題が起こったソースコードの箇所ピンポイントに修正すれば治るといった性質ではなく、 別スレッドの動作がタイミングとかの噛み合いで影響したりしなかったりするので、 スレッドセーフ出ない操作を全部追いだすくらいしないと根本解決になりません。⇒設計の問題。 短時間の実行では一見正しく動いてるように見えるのが面倒なポイント この関数はスレッドセーフだとか、ひとつひとつ気にしながらコードを書くのは生産性が落ちるので そうならない為に queue 等を用いてスレッドセーフなスレッド間通信の仕組みを作ると楽出来ます。 root.after は、tkinter 側のイベントループのイベント queue を代用する感じで、 別スレッドからタイマーに登録し、イベントループ側から呼び出してもらうアプローチ。 root.after により登録した関数の実行が、イベントループ側から順番に実行されるので スレッドセーフになります。(別スレッドで直接実行は同時アクセスによる衝突が問題) after 自体はタイマーに登録するだけなので、描画処理の同期に影響しない事を利用して、 別スレッドから呼び出して使ってます。(きちんとするなら別スレッドでの root へのアクセス自体が修正対象) ** 但し、root.after 自体はスレッドセーフではないことには、注意してください。** 基本的に、定数以外のスレッドを跨ぐ変数のアクセスは避けた方が良いです。 (スレッドセーフな実装の関数等、例外はあり) 別スレッドでの root.after がどのようなときに問題になるかは、 GUIが終了したが別スレッドが稼働してるときに、破棄された root を使う事になるのでエラーになります。 (これは root.after を経由していなくても update_balloonも同じ 、root破棄後のGUI操作は全てエラー) どの様に対策するかというと、 A: asyncio 導入 (コードを全て asyncio フレームワークの作法に合わせる必要あり) B: 独自に同期queue を使い、別スレッド側に tkinter を持ち込まない。⇒別GUI移植でもコードを再利用できる C: tkinter 終了時にフラグ。別スレッドからGUI操作要求前に確認。   但し同時アクセスが発生しないように同期処理が必要です。threading.Lockや Mutex 等 D: 終了時に気にせず強制終了 (daemonスレッドを使う) A は別解で導入コスト重め(説明すると長くなるので選択肢の紹介のみ) 理想は B だけど、Cは妥協案、Dは雑な方法。 B の方法では tkinter側のタイマーで queue.get_nowait (非ブロッキングの読み出し) GUIスレッドへの通知にafterを使う方法自体は、 queue 読み出しを tkinter の mainloop に任せることができる ⇒ 実装するコード量の削減 という利点があります。 一種の手抜き TIPS で、tkinter/thread のサンプルでそれなりに使われてます。小規模コード向け
teamikl

2024/12/23 22:53

after() の同じ説明を、コメントとで3回くらい繰り返してしまった気がする。 投稿の文字数制限に引っ掛かって、書き直ししてるうちに複製されてしまった。 冗長になってしまったけど、そのままにしておきます。
teamikl

2024/12/24 03:37

質問の表題の「可変サイズ」に関しては、 マウス座標 {x:4} {y:4} -> {x} {y}のように文字列幅固定の書式を変更すると確認できます。 ラベルをpackで配置することで、tk側のレイアウトマネージャにより自動計算。
guest

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

総合スコア8794

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

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

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.34%

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

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

質問する

関連した質問