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

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

ただいまの
回答率

88.60%

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

解決済

回答 3

投稿

  • 評価
  • クリップ 0
  • VIEW 121

解決したいこと

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

コード

import pyxel as px
import tkinter as tk
import tkinter.filedialog
import os
import threading

class App:
    def __init__(self):
        self.sp = None #選択されたファイルのファイルパス
        px.init(250,125,caption="")
        px.run(self.update, self.draw)

    def update(self):
        if px.btnp(px.KEY_Q):
            px.quit()

        if px.btnp(px.KEY_F):
            threading.Thread(target = self.open_file).start()#pyxelとtkinterを同時実行

    def draw(self):
        px.cls(0)
        px.text(0,0,str(self.sp),7) #指定されたファイルパスを表示

    def open_file(self):
        fTyp = [("","")]
        iDir = os.path.abspath(os.path.dirname(__file__))
        selected_file_path = tkinter.filedialog.askopenfilename(filetypes = fTyp,initialdir=iDir) #ファイルパスの入力
        self.sp = selected_file_path

App()

エラー

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

バージョン

Python-最新
tkinter-8.6.9
pyxel-1.4.3

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

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

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

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 3

checkベストアンサー

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 を起動しておく解決案。

#!/usr/bin/env python3.8

from threading import Thread, Event
import tkinter as tk
from tkinter.filedialog import askopenfilename

def tk_dummy_loop(event):
    root = tk.Tk()
    root.withdraw()
    root.after_idle(event.set) # mainloop内から呼び出される。イベント待機へ通知
    root.mainloop()


def open_dialog():
    filename = askopenfilename()
    print("Filename: {}".format(filename))


def main():
    event = Event()
    thread1 = Thread(target=tk_dummy_loop, args=(event,), daemon=True)
    thread1.start()
    event.wait() # mainloop起動を待つ

    thread1 = Thread(target=open_dialog)
    thread1.start()

    # NOTE: tkinter のイベントループはひとつなので、スレッドは起動中だが
    # 2つ目のダイアログは1つ目のダイアログが閉じられる迄開かない
    thread2 = Thread(target=open_dialog)
    thread2.start()

    # この方法の場合は、メインスレッドで px.run が使える

    # スレッドの終了待ち
    thread1.join()
    thread2.join()


if __name__ == '__main__':
    main()

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

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/11/23 22:21

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

    キャンセル

0

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

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

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

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/11/23 10:13

    なるほど!
    同時に変数に代入してしまった場合に問題が発生するんですね。
    Queue を使って
    import queue
    q = queue.LifoQueue()
    q.put(selected_file_path)

    while not q.empty():
    self.sp = q.get()

    このように書けば解決しますか?

    キャンセル

  • 2020/11/23 16:26

    同時アクセスの問題についてはそれで解決出来ますが、
    「イベントループの中で処理をブロッキングする」のは別問題で、
    キューが入るまで処理が止まる → イベントループが停滞 → 応答なしになってしまいます

    イベントループ内でキューの読み出しをする場合は、
    ノンブロッキング版の get_nowait を使ったり別スレッドで読み出すようにします。

    出来れば複数のスレッドからの代入は、一つのスレッドからのみに制限した方が良いですが、
    必ずしも問題が出るわけではないので、現状の使い方(片方から代入)であれば許容範囲かな。

    キャンセル

  • 2020/11/23 22:15

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

    キャンセル

0

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

def open_file(self):
    root = tk.Tk()
    root.withdraw()
    fTyp = [("","")]
    iDir = os.path.abspath(os.path.dirname(__file__))
    selected_file_path = tkinter.filedialog.askopenfilename(filetypes = fTyp,initialdir=iDir)
    self.sp = selected_file_path
    root.deiconify()
    root.destroy()


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

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/11/18 12:16

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

    キャンセル

  • 2020/11/22 07:07

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

    キャンセル

  • 2020/11/23 08:21

    通常の利用では問題ないので不要かもしれませんが、
    一度目のダイアログを「閉じずに」もう一度ダイアログを開こうとした場合、
    同様のエラーがでると思います。

    スレッドセーフにするには、上記のコードからは
    askopenfilename の引数に parent=root を明示することで回避できます。

    キャンセル

  • 2020/11/23 22:17

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

    キャンセル

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

  • ただいまの回答率 88.60%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る