web-dev-qa-db-ja.com

Tkinter:スレッドを使用してメインイベントループが「フリーズ」するのを防ぐ方法

「スタート」ボタンとプログレスバーを備えた小さなGUIテストがあります。望ましい動作は次のとおりです。

  • スタートをクリック
  • プログレスバーが5秒間振動する
  • プログレスバーが停止する

観察された動作は、「開始」ボタンが5秒間フリーズした後、プログレスバーが表示されることです(振動なし)。

ここに私のコードがあります:

class GUI:
    def __init__(self, master):
        self.master = master
        self.test_button = Button(self.master, command=self.tb_click)
        self.test_button.configure(
            text="Start", background="Grey",
            padx=50
            )
        self.test_button.pack(side=TOP)

    def progress(self):
        self.prog_bar = ttk.Progressbar(
            self.master, orient="horizontal",
            length=200, mode="indeterminate"
            )
        self.prog_bar.pack(side=TOP)

    def tb_click(self):
        self.progress()
        self.prog_bar.start()
        # Simulate long running process
        t = threading.Thread(target=time.sleep, args=(5,))
        t.start()
        t.join()
        self.prog_bar.stop()

root = Tk()
root.title("Test Button")
main_ui = GUI(root)
root.mainloop()

Bryan Oakley here からの情報に基づいて、スレッドを使用する必要があることを理解しています。スレッドを作成しようとしましたが、スレッドはメインスレッド内から開始されるので、役に立たないと思います。

A. Rodas here のサンプルコードと同様に、ロジック部分を別のクラスに配置し、そのクラス内からGUIをインスタンス化するというアイデアがありました。

私の質問:

このコマンドのようにコーディングする方法がわかりません:

self.test_button = Button(self.master, command=self.tb_click)

他のクラスにある関数を呼び出します。これは悪いことですか、それとも可能ですか? self.tb_clickを処理できる2番目のクラスを作成するにはどうすればよいですか? A. Rodasの見事に機能するサンプルコードをたどってみました。しかし、アクションをトリガーするButtonウィジェットの場合、彼のソリューションを実装する方法はわかりません。

代わりに、単一のGUIクラス内からスレッドを処理する必要がある場合、メインスレッドに干渉しないスレッドをどのように作成しますか?

38
Dirty Penguin

メインスレッドで新しいスレッドに参加すると、スレッドが終了するまで待機するため、マルチスレッドを使用している場合でもGUIはブロックされます。

ロジック部分を別のクラスに配置する場合は、スレッドを直接サブクラス化し、ボタンを押したときにこのクラスの新しいオブジェクトを開始できます。 ThreadのこのサブクラスのコンストラクターはQueueオブジェクトを受け取ることができ、それをGUIパーツと通信できるようになります。だから私の提案は:

  1. メインスレッドでキューオブジェクトを作成する
  2. そのキューにアクセスできる新しいスレッドを作成します
  3. メインスレッドのキューを定期的に確認する

次に、ユーザーが同じボタンを2回クリックした場合に発生する問題を解決する必要があります(クリックごとに新しいスレッドが生成されます)が、開始ボタンを無効にし、self.prog_bar.stop()

import Queue

class GUI:
    # ...

    def tb_click(self):
        self.progress()
        self.prog_bar.start()
        self.queue = Queue.Queue()
        ThreadedTask(self.queue).start()
        self.master.after(100, self.process_queue)

    def process_queue(self):
        try:
            msg = self.queue.get(0)
            # Show result of the task if needed
            self.prog_bar.stop()
        except Queue.Empty:
            self.master.after(100, self.process_queue)

class ThreadedTask(threading.Thread):
    def __init__(self, queue):
        threading.Thread.__init__(self)
        self.queue = queue
    def run(self):
        time.sleep(5)  # Simulate long running process
        self.queue.put("Task finished")
51
A. Rodas

代替ソリューションの基礎を提出します。 Tkプログレスバーに固有のものではありませんが、そのために非常に簡単に実装できます。

Tkのバックグラウンドで他のタスクを実行し、必要に応じてTkコントロールを更新し、GUIをロックしないクラスをいくつか紹介します!

TkRepeatingTaskおよびBackgroundTaskクラスは次のとおりです。

import threading

class TkRepeatingTask():

    def __init__( self, tkRoot, taskFuncPointer, freqencyMillis ):
        self.__tk_   = tkRoot
        self.__func_ = taskFuncPointer        
        self.__freq_ = freqencyMillis
        self.__isRunning_ = False

    def isRunning( self ) : return self.__isRunning_ 

    def start( self ) : 
        self.__isRunning_ = True
        self.__onTimer()

    def stop( self ) : self.__isRunning_ = False

    def __onTimer( self ): 
        if self.__isRunning_ :
            self.__func_() 
            self.__tk_.after( self.__freq_, self.__onTimer )

class BackgroundTask():

    def __init__( self, taskFuncPointer ):
        self.__taskFuncPointer_ = taskFuncPointer
        self.__workerThread_ = None
        self.__isRunning_ = False

    def taskFuncPointer( self ) : return self.__taskFuncPointer_

    def isRunning( self ) : 
        return self.__isRunning_ and self.__workerThread_.isAlive()

    def start( self ): 
        if not self.__isRunning_ :
            self.__isRunning_ = True
            self.__workerThread_ = self.WorkerThread( self )
            self.__workerThread_.start()

    def stop( self ) : self.__isRunning_ = False

    class WorkerThread( threading.Thread ):
        def __init__( self, bgTask ):      
            threading.Thread.__init__( self )
            self.__bgTask_ = bgTask

        def run( self ):
            try :
                self.__bgTask_.taskFuncPointer()( self.__bgTask_.isRunning )
            except Exception as e: print repr(e)
            self.__bgTask_.stop()

以下は、これらの使用をデモするTkテストです。デモを実際に見たい場合は、これらのクラスを含むモジュールの最後にこれを追加してください。

def tkThreadingTest():

    from tkinter import Tk, Label, Button, StringVar
    from time import sleep

    class UnitTestGUI:

        def __init__( self, master ):
            self.master = master
            master.title( "Threading Test" )

            self.testButton = Button( 
                self.master, text="Blocking", command=self.myLongProcess )
            self.testButton.pack()

            self.threadedButton = Button( 
                self.master, text="Threaded", command=self.onThreadedClicked )
            self.threadedButton.pack()

            self.cancelButton = Button( 
                self.master, text="Stop", command=self.onStopClicked )
            self.cancelButton.pack()

            self.statusLabelVar = StringVar()
            self.statusLabel = Label( master, textvariable=self.statusLabelVar )
            self.statusLabel.pack()

            self.clickMeButton = Button( 
                self.master, text="Click Me", command=self.onClickMeClicked )
            self.clickMeButton.pack()

            self.clickCountLabelVar = StringVar()            
            self.clickCountLabel = Label( master,  textvariable=self.clickCountLabelVar )
            self.clickCountLabel.pack()

            self.threadedButton = Button( 
                self.master, text="Timer", command=self.onTimerClicked )
            self.threadedButton.pack()

            self.timerCountLabelVar = StringVar()            
            self.timerCountLabel = Label( master,  textvariable=self.timerCountLabelVar )
            self.timerCountLabel.pack()

            self.timerCounter_=0

            self.clickCounter_=0

            self.bgTask = BackgroundTask( self.myLongProcess )

            self.timer = TkRepeatingTask( self.master, self.onTimer, 1 )

        def close( self ) :
            print "close"
            try: self.bgTask.stop()
            except: pass
            try: self.timer.stop()
            except: pass            
            self.master.quit()

        def onThreadedClicked( self ):
            print "onThreadedClicked"
            try: self.bgTask.start()
            except: pass

        def onTimerClicked( self ) :
            print "onTimerClicked"
            self.timer.start()

        def onStopClicked( self ) :
            print "onStopClicked"
            try: self.bgTask.stop()
            except: pass
            try: self.timer.stop()
            except: pass                        

        def onClickMeClicked( self ):
            print "onClickMeClicked"
            self.clickCounter_+=1
            self.clickCountLabelVar.set( str(self.clickCounter_) )

        def onTimer( self ) :
            print "onTimer"
            self.timerCounter_+=1
            self.timerCountLabelVar.set( str(self.timerCounter_) )

        def myLongProcess( self, isRunningFunc=None ) :
            print "starting myLongProcess"
            for i in range( 1, 10 ):
                try:
                    if not isRunningFunc() :
                        self.onMyLongProcessUpdate( "Stopped!" )
                        return
                except : pass   
                self.onMyLongProcessUpdate( i )
                sleep( 1.5 ) # simulate doing work
            self.onMyLongProcessUpdate( "Done!" )                

        def onMyLongProcessUpdate( self, status ) :
            print "Process Update: %s" % (status,)
            self.statusLabelVar.set( str(status) )

    root = Tk()    
    gui = UnitTestGUI( root )
    root.protocol( "WM_DELETE_WINDOW", gui.close )
    root.mainloop()

if __== "__main__": 
    tkThreadingTest()

BackgroundTaskについて強調する2つのインポートポイント:

1)バックグラウンドタスクで実行する関数は、呼び出しと尊重の両方を行う関数ポインターを取る必要があります。これにより、可能であればタスクを途中でキャンセルできます。

2)アプリケーションを終了するときに、バックグラウンドタスクが停止していることを確認する必要があります。そのスレッドは、あなたがそれに対処しなければGUIが閉じられていても実行されます!

4
BuvinJ

問題は、t.join()がクリックイベントをブロックし、メインスレッドがイベントループに戻って再描画を処理しないことです。 Tkinterでの処理後にttkプログレスバーが表示される理由 または 電子メール送信時にブロックされるTTKプログレスバー を参照してください

4
jmihalicza