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

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

ただいまの
回答率

88.80%

asyncioでtkinterのダイアログを開けない

解決済

回答 1

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 1,488

namuyan

score 72

Python3.6を使用してasyncioとtkinterを用いたコードを書きたいと思います。
しかし残念ながらこの二つは相性が悪く併用が難しいです。

dialog.py

from tkinter import filedialog, Tk, Frame
import platform
from pathlib import Path


class Dialogs(Frame):
    def __init__(self):
        self.root = Tk()
        super().__init__(self.root)
        self.root.geometry("0x0")
        self.root.overrideredirect(1)
        self.root.withdraw()
        self.system = platform.system()
        self.pack()

    def open_dialog(self, dialog_open_method=filedialog.askopenfilename, options=None):
        if self.system == "Windows":
            self.root.deiconify()
        self.root.update()
        self.root.lift()
        self.root.focus_force()
        path_str = dialog_open_method(**(options or {}))
        self.root.update()
        if self.system == "Windows":
            self.root.withdraw()
        return Path(path_str)


ダイアログを開きファイルを選択します。

main.py

from dialog import Dialogs
import asyncio

loop = asyncio.get_event_loop()
dialog = Dialogs()


async def looper():
    while True:
        await asyncio.sleep(1)
        print("loop!")


async def main():
    asyncio.ensure_future(looper())
    path = dialogs.open_dialog()
    print(path)


if __name__ == "__main__":
    loop.run_until_complete(main())

上記のコードはEventLoopをブロックしてしまいます。
通常はrun_in_executorを用いるところですがRuntimeError: main thread is not in main loopとエラーを出されます。
どのようなコードを用いればEventLoopをブロックされずに済むのでしょうか?
回答の方を宜しくお願いします。

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

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

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

    クリップを取り消します

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

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

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

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

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

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

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

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

    質問の評価を下げる

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

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

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

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

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

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

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

    詳細な説明はこちら

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

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

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

回答 1

checkベストアンサー

0

適切な回答かどうか自信がない(asyncioの仕様経験が乏しい)のですが、自分は次のように考えました。

  • Tkinterはメインスレッドで動かす。
    run_in_executorを使ってしまうとメインスレッド以外でTkinterが実行されることになるためか、ご質問にあるとおりうまくいかないようです。メインスレッド以外でTkinterのイベントループが動くものかどうかよく調べてはいないですが・・・。よってTkinterに関する処理をasyncioのタスクとして動かすのはあきらめざるを得ないように思います。async関数の中でTkinterを呼び出しても動くことは動くでしょうが、そこで非同期I/O処理は全て止まってしまうため並行して動かしたいならasyncioを動かすスレッドとは別のスレッドで(asyncioの枠組みとは別の世界で)動かす必要があるように思います。

  • 非同期I/Oはメインスレッド以外でも動くのでこちらをサブスレッドで動かすようにする。

  • asyncioの処理はコルーチンをディスパッチしないと進まない
    mainにてタスクの実行を(awaitなどで)待ってないので、コルーチンは生成されるものの、その実行が先に進まないままmainが終わってしまいます。

  • Tkinterの処理をasyncio側で待つ
    asyncio.Futureを作り、メインスレッド側でそのFutureにset_resultで結果を設定してやるとasyncio処理側でそのFutureを待つことで同期および結果が利用できると考えました。他にもっとよい(自然)な方法があるかも知れません・・・

main.py

from dialog import Dialogs
import asyncio
import threading


# 元のコードそのままにしたが、別スレッドで非同期I/Oを行うなら
# サブスレッドで asyncio.new_event_loop() とした方が自然か?
loop = asyncio.get_event_loop()
dialog = Dialogs()


async def looper():
    tname = threading.current_thread().name
    print(f"{tname}: looper start")
    for i in range(10):
        await asyncio.sleep(1)
        print(f"{tname}:{i}: awaken")


class AsyncioThread(threading.Thread):
    def __init__(self, loop, dialog_future, *args, **kwargs):
        self.loop = loop
        self.dialog_future = dialog_future
        super().__init__(*args, **kwargs)

    def run(self):
        future = main(self.dialog_future)
        print(f"{self.name}: thread start")
        print(f"{self.name}: loop = {self.loop}")
        print(f"{self.name}: future = {future}")

        self.loop.run_until_complete(future)
        print(f"{self.name}: completed")


async def main(dialog_future):
    await asyncio.gather(asyncio.ensure_future(looper()), dialog_future)
    print(f"main: path={dialog_future.result()}")


def main1():
    # loop = asyncio.new_event_loop()
    dialog_future = asyncio.Future()
    thread = AsyncioThread(loop=loop, dialog_future=dialog_future, daemon=False)

    thread.start()

    path = dialog.open_dialog()
    print(path)
    dialog_future.set_result(path)

    thread.join()


if __name__ == '__main__':
    main1()

投稿

編集

  • 回答の評価を上げる

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

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

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

  • 回答の評価を下げる

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

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

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

  • 2019/07/07 23:33

    回答して頂きありがとうございます。EventLoopはMainThreadでないと動きませんよ、のはずだったのですが動きますね。KILLSIGが理由でWindowsでは動かなかった記憶がありMainThreadである必要があると考えていました。いったい何と間違えていたのであろう?

    キャンセル

  • 2019/07/08 10:52

    EventLoopはメインスレッド以外でも動くと思います。実際run_in_executorはサブスレッドで動作するわけですからEventLoopがメインスレッド以外でも動いてくれないと困ると思います。

    ひょっとしたらEventLoopがmainスレッドでは最初から用意されているのに対してmainスレッド以外では用意されていない点(get_event_loop()はデフォルト状態ではmainスレッドに対してしか通用しないため、loopの省略を許す多くのasyncio関数がデフォルトでmainスレッド以外では動かない)と混同しておられたりしないでしょうか?

    キャンセル

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

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

関連した質問

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