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

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

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

Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

Tkinter

Tkinterは、GUIツールキットである“Tk”をPythonから利用できるようにした標準ライブラリである。

Q&A

解決済

1回答

3687閲覧

Python3 Tkinter ダイアログを表示したい2

person

総合スコア223

Python 3.x

Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

Tkinter

Tkinterは、GUIツールキットである“Tk”をPythonから利用できるようにした標準ライブラリである。

0グッド

0クリップ

投稿2020/07/06 10:42

編集2020/07/28 05:02

少し前に、
Python3 Tkinter ダイアログ表示したい
にてダイアログを表示し、そこで選んだボタンの文字列(OK, キャンセルなど)やTrue, Falseを戻り値として取得したいという質問をしました。

そこで
teamikl様にsimpledialog.SimpleDialog をカスタマイズして使う提案をしていただいたのですが、以下のようにソースコードを書き換えたところ、RuntimeErrorが発生してしまいました。

スレッドを使っていない時は問題なく動作したので、スレッドの使い方やスレッドとの相性だと思うのですが、具体的な理由がわかりません。

対処方法を教えていただきたいです。

アプリケーションの仕様としては、ボタンを押した時にエントリが空だったらダイアログを表示する。ダイアログの続行ボタンを押した場合はエントリがから出なかった時と同じ処理(App.func())を実行、キャンセルを押した時は何もしない(returnする)ような感じです。

  • main.py

Python

1import tkinter as tk 2import tkinter.ttk as ttk 3import threading 4import time 5from datetime import datetime 6 7import mydialog 8 9class App: 10 def __init__(self, win): 11 self.flag = True 12 self.win = win 13 self.create() 14 15 def create(self): 16 self.clock = tk.Label(self.win) 17 self.clock.grid() 18 self.entry = ttk.Entry(win) 19 self.entry.grid() 20 self.button = ttk.Button(win, text="click") 21 self.button.grid() 22 self.thread = threading.Thread(target=self.update) 23 self.thread.start() 24 self.bind_event() 25 26 def update(self): 27 while self.flag: 28 self.clock["text"] = datetime.now().strftime("%Y/%m/%d %H:%M:%S.%f") 29 time.sleep(0.001) 30 31 def bind_event(self): 32 self.button.bind("<ButtonRelease>", self.pushed) 33 34 def pushed(self, ev): 35 self.str = self.entry.get() 36 print("str:" + self.str) 37 if self.str == "": 38 ret = mydialog.askyesno(message="no str...", buttons=["続行", "キャンセル"], parent=self.win) 39 print(ret) 40 if ret == "続行": 41 pass 42 else: 43 return 44 self.func() 45 46 def func(self): 47 # 続行処理 48 pass 49 50 51def close(app): 52 app.flag = False 53 app.win.destroy() 54 55if __name__ == "__main__": 56 win = tk.Tk() 57 app = App(win) 58 win.protocol("WM_DELETE_WINDOW", lambda:close(app)) 59 win.mainloop()
  • mydialog.py

Python

1import tkinter as tk 2from tkinter import ttk, simpledialog 3from functools import partial 4 5# https://teratail.com/questions/274362 6# 押されたボタンの文字列を戻り値とする 7 8def askyesno(title="", message="", buttons=["OK", "Cancel"], parent=None): 9 # messageのフォント変更 10 parent.option_add("*Message.font", "MSPゴシック 50") 11 # ボタンのフォント変更 12 font = ("", 20) 13 style = ttk.Style() 14 style.configure("MyDialog.TLabel", font=font) 15 style.configure("MyDialog.TButton", font=font) 16 simpledialog.Frame = partial(ttk.Frame, style="MyDialog.TFrame") 17 simpledialog.Label = partial(ttk.Label, style="MyDialog.TLabel") 18 simpledialog.Button = partial(ttk.Button, style="MyDialog.TButton") 19 dialog = simpledialog.SimpleDialog( 20 master=parent, 21 title=title, 22 text=message, 23 buttons=buttons 24 ) 25 return buttons[dialog.go()]
  • 実行結果(続行ボタンを押した時のものだが、キャンセルボタンも同様のエラー).
str: 続行 Exception in thread Thread-1: Traceback (most recent call last): File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/threading.py", line 932, in _bootstrap_inner self.run() File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/threading.py", line 870, in run self._target(*self._args, **self._kwargs) File "/Users/USER/Desktop/main.py", line 28, in update self.clock["text"] = datetime.now().strftime("%Y/%m/%d %H:%M:%S.%f") File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/tkinter/__init__.py", line 1648, in __setitem__ self.configure({key: value}) File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/tkinter/__init__.py", line 1637, in configure return self._configure('configure', cnf, kw) File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/tkinter/__init__.py", line 1627, in _configure self.tk.call(_flatten((self._w, cmd)) + self._options(cnf)) RuntimeError: main thread is not in main loop

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

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

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

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

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

guest

回答1

0

ベストアンサー

スレッドの使い方やスレッドとの相性だと思うのですが、具体的な理由がわかりません。

GUIでマルチスレッドにする場合、
GUI関係の操作は必ず 描画のイベントループを担当しているスレッド側
(tkinter の場合は mainloop() で、大抵の場合メインスレッド)で行う必要があります。

他のスレッドから同時にGUIへアクセスすると不都合が生じます。
一見うまく動いているように見えても、タイミング次第でエラーになる事もあるので、
どの操作がスレッドセーフなのか、そうでないのかは見極めが難しい箇所です。

以下は、3つのトピックがあるので注意してください。

  • GUIでのマルチスレッドで、スレッド側からGUIを更新する方法
  • SimpleDialogの実装の問題点
  • アプリ終了時に、スレッドが終了済みのGUIへアクセスする問題

問題点: 別スレッドからのGUI操作

self.clock["text"] = datetime.now().strftime("%Y/%m/%d %H:%M:%S.%f")

問題点2:

simpledialog.SimpleDialog がイベントループを終了してるようです。
simpledialog.Dialog や messagebox.askyesno では発生しません。
(アプリ終了時にエラーは出ますが、別件です。今回はダイアログを閉じた時のエラーのみを焦点)

問題点3:

スレッドとTkinter の生存期間について、
スレッド内からTkinter へ直接アクセスが発生しているが、
Tkinter のイベントループ終了後にもGUIへのアクセスが発生する。
対策A => WM_DELETE_WINDOW のハンドラで、tkのイベントループ終了前にスレッドを終了する。
対策B => 独自のqueue 経由して GUI 更新を処理する。


解決策1-1: メインスレッド側でGUIの更新をする

完全なの方法ではありませんが、一般的なケースで
tkinterの場合は、after_idle に関数・引数を渡す事で
mainloop() 内から呼び出してくれます。

  • event_generate
  • after_idle
  • after

辺りのメソッドを使い、mainloop側でGUIの処理は行います。

ラベルの表示変更だけならスレッド側で行っても大丈夫なことはあります、
…が実際に安全かどうかは解りません(詳しく調べてない)

但し今回(SimpleDialog利用)の場合、これでは不足で
独自にqueue等を用いて確実にメインスレッド側の win.mainloop() で処理する必要があるかもしれません。(ここは推測)
SimpleDialog内部でも独自のイベントループを持ち、
ダイアログ表示中はそちらで肩代わりしてますが、ダイアログが閉じされたときに終了してしまうので、
上記のエラーの原因となっているようでした。

対応するコードは割愛。少し大掛かりな変更が必要です。
要: queue を使ったスレッド間通信。

解決策2: simpledialog.Dialog の実装を使う。

解決策3:
因みに、時刻の更新表示のようなものならスレッドを使わなくても
after()でのタイマーでも可能で、after_cancel()で停止もできます。

スレッドを他の用途に使う必要がなければ、タイマーが一番無難な解決策です。

追記: タイマーでの実装。(※スレッドで扱う場合の問題解決にはなりません)

python

1#!/usr/bin/env python3.8 2 3import datetime 4import tkinter as tk 5from tkinter import ttk 6import mydialog # https://teratail.com/questions/275549 質問文から借用 7 8 9def getTimestamp(_time_format="%Y/%m/%d %H:%M:%S.%f"): 10 return datetime.datetime.now().strftime(_time_format) 11 12 13class App: 14 def __init__(self, win): 15 self.win = win 16 self._running = False 17 self._timer = None 18 self._init_widgets() 19 self._init_events() 20 21 def _init_widgets(self): 22 time_text = self._time_text = tk.StringVar(self.win) 23 input_text = self._input_text = tk.StringVar(self.win) 24 frame = self._frame = ttk.Frame(self.win) 25 label = self._label = ttk.Label(frame, textvar=time_text) 26 entry = self._entry = ttk.Entry(frame, textvar=input_text) 27 stop_button = self._stop_button = ttk.Button(frame, text="Stop") 28 start_button = self._start_button = ttk.Button(frame, text="Start") 29 30 frame.pack(fill=tk.BOTH, expand=True) 31 label.pack() 32 entry.pack() 33 stop_button.pack() 34 start_button.pack() 35 36 def start_timer(self): 37 assert not self._running 38 self._running = True 39 self._timer = self.win.after_idle(self._on_timer) 40 41 def _init_events(self): 42 self._stop_button.bind("<Button-1>", self._on_stop_timer) 43 self._start_button.bind("<Button-1>", self._on_start_timer) 44 45 def _on_timer(self): 46 if not self._running and self._timer: 47 self.win.after_cancel(self._timer) 48 self._timer = None 49 return 50 51 self._time_text.set(getTimestamp()) 52 53 # NOTE: 60FPS: 16.666 (1000/60) 54 # あまり小さくし過ぎても無効な値になります。 55 self._timer = self.win.after(15, self._on_timer) 56 57 def _on_start_timer(self, event): 58 if not self._running: 59 self.start_timer() 60 61 def _on_stop_timer(self, event): 62 import mydialog 63 if not self._input_text.get(): 64 ret = mydialog.askyesno(message="中断します", parent=self.win) 65 if ret == "OK": 66 self._running = False 67 68 69def main(): 70 win = tk.Tk() 71 app = App(win) 72 app.start_timer() 73 win.mainloop() 74 75 76if __name__ == '__main__': 77 main()

「スレッド側でダイアログを開いて更にモーダルにしたい」
みたいな時は、更にスレッドに配慮した工夫が必要になります。

参考:
PythonでTkinterを使ってDiscordのRichPresenceを使えるようにしたい
質問自体は全然異なりますが、スレッド内ループの停止・再開や
スレッド側でダイアログ表示&queue,Event を用いたスレッド間通信のサンプル
[source on repl.it]

投稿2020/07/06 12:33

編集2020/07/07 04:04
teamikl

総合スコア8664

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

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

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.48%

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

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

質問する

関連した質問