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

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

新規登録して質問してみよう
ただいま回答率
85.50%
ファイル

ファイルとは、文字列に基づいた名前又はパスからアクセスすることができる、任意の情報のブロック又は情報を格納するためのリソースです。

Tkinter

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

Python

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

Q&A

解決済

3回答

5007閲覧

Tkinterでファイルダイアログを再度開くとエラーになる

sakuramochi_py

総合スコア32

ファイル

ファイルとは、文字列に基づいた名前又はパスからアクセスすることができる、任意の情報のブロック又は情報を格納するためのリソースです。

Tkinter

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

Python

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

0グッド

0クリップ

投稿2020/11/17 10:16

##解決したいこと
ほかのプログラムを動かしているときに、Tkinterのファイルダイアログ(filedialog)を実行し、ファイルを選択するプログラム。一度目は成功するが、二度目は下記のようなエラーが発生する。それを防ぎたい。

##コード

Python

1import pyxel as px 2import tkinter as tk 3import tkinter.filedialog 4import os 5import threading 6 7class App: 8 def __init__(self): 9 self.sp = None #選択されたファイルのファイルパス 10 px.init(250,125,caption="") 11 px.run(self.update, self.draw) 12 13 def update(self): 14 if px.btnp(px.KEY_Q): 15 px.quit() 16 17 if px.btnp(px.KEY_F): 18 threading.Thread(target = self.open_file).start()#pyxelとtkinterを同時実行 19 20 def draw(self): 21 px.cls(0) 22 px.text(0,0,str(self.sp),7) #指定されたファイルパスを表示 23 24 def open_file(self): 25 fTyp = [("","")] 26 iDir = os.path.abspath(os.path.dirname(__file__)) 27 selected_file_path = tkinter.filedialog.askopenfilename(filetypes = fTyp,initialdir=iDir) #ファイルパスの入力 28 self.sp = selected_file_path 29 30App()

##エラー

Error

1Exception in thread Thread-2: 2Traceback (most recent call last): 3 File "C:\Users\Sakuramochi\AppData\Local\Programs\Python\Python39\lib\threading.py", line 950, in _bootstrap_inner 4 self.run() 5 File "C:\Users\Sakuramochi\AppData\Local\Programs\Python\Python39\lib\threading.py", line 888, in run 6 self._target(*self._args, **self._kwargs) 7 File "c:\Users\Sakuramochi\kpp\Pyxel\pyxel coder\pyxel_coder.py", line 27, in open_file 8 selected_file_path = tkinter.filedialog.askopenfilename(filetypes = fTyp,initialdir=iDir) 9 File "C:\Users\Sakuramochi\AppData\Local\Programs\Python\Python39\lib\tkinter\filedialog.py", line 382, in askopenfilename 10 return Open(**options).show() 11 File "C:\Users\Sakuramochi\AppData\Local\Programs\Python\Python39\lib\tkinter\commondialog.py", line 42, in show 12 w = Frame(self.master) 13 File "C:\Users\Sakuramochi\AppData\Local\Programs\Python\Python39\lib\tkinter\__init__.py", line 3121, in __init__ 14 Widget.__init__(self, master, 'frame', cnf, {}, extra) 15 File "C:\Users\Sakuramochi\AppData\Local\Programs\Python\Python39\lib\tkinter\__init__.py", line 2569, in __init__ 16 self.tk.call( 17RuntimeError: main thread is not in main loop

##バージョン
Python-最新
tkinter-8.6.9
pyxel-1.4.3

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

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

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

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

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

guest

回答3

0

ベストアンサー

エラーについては解決済みなので、掘り下げる必要はないかもしれませんが、
動作確認のために書いたコード張っておきます。


#!/usr/bin/env python3.8 import tkinter as tk from tkinter.filedialog import askopenfilename from threading import Thread def open_dialog(): filename = askopenfilename() print("Filename: {}".format(filename)) def main(): root = tk.Tk() thread1 = Thread(target=open_dialog, daemon=True) thread2 = Thread(target=open_dialog, daemon=True) # イベントループ開始後にスレッド開始 root.after_idle(thread1.start) root.after_idle(thread2.start) root.mainloop() # 課題: ここが px.run とは共存できない if __name__ == '__main__': main()

普通の tkinter の作法で mainloop を使う解決策。この場合のtkのイベントループは
root オブジェクトが作られたスレッドと同じスレッドで稼働してる為、
スレッド側で新たに作らなくとも、デフォルトのrootオブジェクトの利用で問題ありません。

別の課題があって、pyxel のイベントループどのように一緒に使うかが難しい点。
→ 解決案は後述


tkinter のみでのエラーの再現。

スレッド内で複数回 root オブジェクトを作るのは、
同時起動の際に問題になる事があります。

以下のコードは2つ目のダイアログを「同時に」開こうとする際にエラー。
この場合、parent=root とすることで回避可能。

#!/usr/bin/env python3.8 from threading import Thread import tkinter as tk from tkinter.filedialog import askopenfilename def open_dialog(): root = tk.Tk() root.withdraw() filename = askopenfilename() # XXX: (parent=root) print("Filename: {}".format(filename)) root.destroy() def main(): thread1 = Thread(target=open_dialog) thread1.start() # NOTE: 複数の root を作っている為、2つのダイアログを同時に開ける thread2 = Thread(target=open_dialog) thread2.start() # ダイアログが閉じられるのを待つ thread1.join() thread2.join() if __name__ == '__main__': main()

原因は、イベントループとtk.Tk()の作成されたスレッドが異なる事なので、
別スレッドで非表示の tkinter mainloop を起動しておく解決案。

python

1#!/usr/bin/env python3.8 2 3from threading import Thread, Event 4import tkinter as tk 5from tkinter.filedialog import askopenfilename 6 7def tk_dummy_loop(event): 8 root = tk.Tk() 9 root.withdraw() 10 root.after_idle(event.set) # mainloop内から呼び出される。イベント待機へ通知 11 root.mainloop() 12 13 14def open_dialog(): 15 filename = askopenfilename() 16 print("Filename: {}".format(filename)) 17 18 19def main(): 20 event = Event() 21 thread1 = Thread(target=tk_dummy_loop, args=(event,), daemon=True) 22 thread1.start() 23 event.wait() # mainloop起動を待つ 24 25 thread1 = Thread(target=open_dialog) 26 thread1.start() 27 28 # NOTE: tkinter のイベントループはひとつなので、スレッドは起動中だが 29 # 2つ目のダイアログは1つ目のダイアログが閉じられる迄開かない 30 thread2 = Thread(target=open_dialog) 31 thread2.start() 32 33 # この方法の場合は、メインスレッドで px.run が使える 34 35 # スレッドの終了待ち 36 thread1.join() 37 thread2.join() 38 39 40if __name__ == '__main__': 41 main()

tkinter mainloop のスレッドには daemon=True を指定し、強制終了。

投稿2020/11/23 08:48

編集2020/11/23 08:50
teamikl

総合スコア8664

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

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

sakuramochi_py

2020/11/23 13:21

いろいろとありがとうございます。 参考にさせて戴きます
guest

0

ベストアンサーさんの回答を参考にしました。

Python

1def open_file(self): 2 root = tk.Tk() 3 root.withdraw() 4 fTyp = [("","")] 5 iDir = os.path.abspath(os.path.dirname(__file__)) 6 selected_file_path = tkinter.filedialog.askopenfilename(filetypes = fTyp,initialdir=iDir) 7 self.sp = selected_file_path 8 root.deiconify() 9 root.destroy()

これで不必要なTkinterウィンドウも非表示でエラー無く実行できます。

投稿2020/11/17 13:24

sakuramochi_py

総合スコア32

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

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

nto

2020/11/18 03:16

deiconifyを使用しなくても、withdrawのままrootのdestroyは可能ですよ。 またファイルを選択せずキャンセルをした場合等の処理も分岐させた方が良いでしょう。
sakuramochi_py

2020/11/21 22:07

deiconify 必要ないんですね! Tkinter について詳しくないのでとてもありがたいです。 分岐等は後から付け足します
teamikl

2020/11/22 23:21

通常の利用では問題ないので不要かもしれませんが、 一度目のダイアログを「閉じずに」もう一度ダイアログを開こうとした場合、 同様のエラーがでると思います。 スレッドセーフにするには、上記のコードからは askopenfilename の引数に parent=root を明示することで回避できます。
sakuramochi_py

2020/11/23 13:17

その件については大丈夫です、 ファイルダイアログが開いている間は開けないようにしました
guest

0

エラーメッセージの通りで、main loopが無いのです。
tkinterのお作法に則って、main loopを作成しましょう。

また、pyxelとtkinterを同時実行と記載されていますが、これはかなり難しいでしょう。
初心者ならば、使う部分を分けて使うように考えた方が良いかと思います。

例えば、ゲームのシナリオをファイルから読むとかであれば、ゲームの開始前にロード(tkinter)を使い、その後pyxelを使うようにするとか。

投稿2020/11/17 11:56

t_obara

総合スコア5488

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

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

sakuramochi_py

2020/11/17 12:40

やはりmain loopは必要不可欠なんですね… Pyxelでテキストエディタを作っていて、ファイル選択にTkinterを選びました。いつでも自由にファイル選択をしたいので同時実行で頑張ってみることにします。 回答ありがとうございました。
teamikl

2020/11/23 07:57 編集

tkinter の 訂正: (mainloop 自体はダイアログ内で使われていて) ※ 今回のコード場合はダイアログのみの利用の為使われていない エラーはスレッドに関するものなので、1~2行目の原因の説明が違います。 tkinter モジュールは、tcl/tkライブラリと通信する為のrootオブジェクトを 初期化時に作成するのですが、初回作成時に これをモジュール内のグローバル変数に保持し、 2回目以降で再利用されるようになってます。 1回目は同一スレッドで作成される為、問題なし。 2回目は前のスレッドで作成されたオブジェクトの為エラーとなってます。 別回答にあるように、毎回rootオブジェクトを破棄する(destroy) のも解決策の一つです。 ---- pyxel もイベントループを持っているようなので、 tkinterと同時に使うのが難しい点は、同意です。
sakuramochi_py

2020/11/22 03:38

なるほど イベントループ同士の同時実行は具体的にどの部分が難しいのですか?
teamikl

2020/11/22 09:40

事例が少ないのが難しい点でしょうか。 ケース次第なので、具体例を出すのもたくさんあり難しいのですが。 単一スレッドの場合: 例) よくあるのが、GUIが応答なしになる問題 片方のイベントループを実行中はもう片方のイベントが停滞するので、 双方のイベント処理を交互に実行する仕組を実装する事になります。 「イベント駆動型プログラミング」や「イベントループ」の理解がヒント。 マルチスレッドの場合: 今回の件も事例の一つ 「排他制御」や「スレッド間通信」の理解が必要な領域で、 一見正しく動いてるようでも、環境やタイミング次第で問題になる事が有りえます。 質問のコードでは、self.sp に別スレッドから代入してる部分は、 排他制御なしに代入してるので、スレッド安全な操作ではありません。 (これ自体がすぐにエラーになるわけではありませんが、注意が必要な箇所) ダイアログのみの利用なら、現状の利用方法でも差し支えなさそうですが、 スレッド内でTkウィジェットの利用を完結しなければならないという 制限があります。 他のウィジェットも作りたい場合は、この制限内で利用する工夫が必要です。 対策 → スレッドは維持し、 スレッド間で安全な操作ができる同期Queue を使って データのやり取り(スレッド間通信)をする。
sakuramochi_py

2020/11/23 01:13

なるほど! 同時に変数に代入してしまった場合に問題が発生するんですね。 Queue を使って import queue q = queue.LifoQueue() q.put(selected_file_path) while not q.empty(): self.sp = q.get() このように書けば解決しますか?
teamikl

2020/11/23 07:26

同時アクセスの問題についてはそれで解決出来ますが、 「イベントループの中で処理をブロッキングする」のは別問題で、 キューが入るまで処理が止まる → イベントループが停滞 → 応答なしになってしまいます イベントループ内でキューの読み出しをする場合は、 ノンブロッキング版の get_nowait を使ったり別スレッドで読み出すようにします。 出来れば複数のスレッドからの代入は、一つのスレッドからのみに制限した方が良いですが、 必ずしも問題が出るわけではないので、現状の使い方(片方から代入)であれば許容範囲かな。
sakuramochi_py

2020/11/23 13:15

ありがとうございます! そしてt_obataさん通知行ってしまっていたら申し訳ありません。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.50%

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

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

質問する

関連した質問