「スタート」ボタンとプログレスバーを備えた小さなGUIテストがあります。望ましい動作は次のとおりです。
観察された動作は、「開始」ボタンが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クラス内からスレッドを処理する必要がある場合、メインスレッドに干渉しないスレッドをどのように作成しますか?
メインスレッドで新しいスレッドに参加すると、スレッドが終了するまで待機するため、マルチスレッドを使用している場合でもGUIはブロックされます。
ロジック部分を別のクラスに配置する場合は、スレッドを直接サブクラス化し、ボタンを押したときにこのクラスの新しいオブジェクトを開始できます。 ThreadのこのサブクラスのコンストラクターはQueueオブジェクトを受け取ることができ、それをGUIパーツと通信できるようになります。だから私の提案は:
次に、ユーザーが同じボタンを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")
代替ソリューションの基礎を提出します。 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が閉じられていても実行されます!
問題は、t.join()がクリックイベントをブロックし、メインスレッドがイベントループに戻って再描画を処理しないことです。 Tkinterでの処理後にttkプログレスバーが表示される理由 または 電子メール送信時にブロックされるTTKプログレスバー を参照してください