我的小程序可能会长时间运行。从控制台执行此操作不是问题,但现在我想添加一个GUI。理想情况下,我想使用Tkinter(a)因为它很简单,而(b)因为它可能更容易在跨平台上实现。根据我的阅读和经验,(几乎)所有GUI都会遇到相同的问题。
在我对线程和GUI的所有阅读中,似乎有两个流。 1-基础工作进程正在轮询(例如,等待获取数据),2-工作进程正在做大量工作(例如,在for循环中复制文件)。我的程序属于后者。
我的代码具有类的“层次结构”。
MIGUI类处理GUI,并与接口类MediaImporter交互。
MediaImporter类是用户界面(控制台或GUI)和辅助类之间的接口。
Import类是长期运行的工作者。它不知道接口或GUI类是否存在。
问题:单击“开始”按钮后,GUI被阻止,因此我无法单击“中止”按钮。好像我根本不使用线程。我怀疑问题与在startCallback方法中启动线程的方式有关。
我还尝试了对整个MediaImporter类进行线程化的方法。参见注释掉的行。
import tkinter as tk
from tkinter import ttk
from tkinter import filedialog
import threading
import time
class MIGUI():
def __init__(self, master):
self.master = master
self.mediaImporter = MediaImporter()
self.startButton = ttk.Button(self.master, text='Start', command=self.startCallback)
self.startButton.pack()
self.abortButton = ttk.Button(self.master, text='Abort', command=self.abortCallback)
self.abortButton.state(['disabled'])
self.abortButton.pack()
def startCallback(self):
print('startCallback')
self.abortButton.state(['!disabled'])
self.startButton.state(['disabled'])
self.abortButton.update() # forcing the update seems unnecessary
self.startButton.update()
#print(self.startButton.state())
#print(self.abortButton.state())
self.x = threading.Thread(target=self.mediaImporter.startImport)
self.x.start()
self.x.join()
#self.mediaImporter.startImport()
self.startButton.state(['!disabled'])
self.abortButton.state(['disabled'])
self.abortButton.update()
self.startButton.update()
#print(self.startButton.state())
#print(self.abortButton.state())
def abortCallback(self):
print('abortCallback')
self.mediaImporter.abortImport()
self.startButton.state(['!disabled'])
self.abortButton.state(['disabled'])
class MediaImporter():
#class MediaImporter(threading.Thread):
""" Interface between user (GUI / console) and worker classes """
def __init__(self):
#threading.Thread.__init__(self)
self.Import = Import()
#other worker classes exist too
def startImport(self):
print('mediaImporter - startImport')
self.Import.start()
def abortImport(self):
print('mediaImporter - abortImport')
self.Import.abort()
class Import():
""" Worker
Does not know anything about other non-worker classes or UI.
"""
def __init__(self):
self._wantAbort = False
def start(self):
print('import - start')
self._wantAbort = False
self.doImport()
def abort(self):
print('import - abort')
self._wantAbort = True
def doImport(self):
print('doImport')
for i in range(0,10):
#actual code has nested for..loops
print(i)
time.sleep(.25)
if self._wantAbort:
print('doImport - abort')
return
def main():
gui = True
console = False
if gui:
root = tk.Tk()
app = MIGUI(root)
root.mainloop()
if console:
#do simple console output without tkinter - threads not necessary
pass
if __name__ == '__main__':
main()
答案 0 :(得分:2)
GUI被阻止的原因是因为您调用self.x.join()
,该调用在doImport
功能完成之前一直处于阻止状态,请参见join文档。相反,我会在您的join()
函数中调用abortCallback()
,因为那样会导致线程停止运行。
答案 1 :(得分:0)
再次感谢您XORNAND。 join()绝对是问题的一部分。问题的另一部分是,MIGUI类没有办法知道长时间运行的进程何时完成(因为它已经按照自己的过程运行,或者因为它已中止了。)低层工作人员和UI层。我确实尝试使用threading.Event没有成功,并考虑使用Queues。
我的解决方案是使用pubsub。 (https://github.com/schollii/pypubsub)工作层可以发送有关各种主题的Message,UI和界面层可以设置侦听器以对接收到的数据执行操作。
就我而言,Import.doImport方法在完成时会发送STATUS消息。然后,MIGUI侦听器可以相应地翻转“开始/中止”按钮。
为了确保pubsub的实施能够按计划进行,我还设置了一个tkinter Progressbar。 doImport方法发送完成百分比的PROGESS消息。这反映在屏幕上的进度栏中。
一个旁注-在我的原始问题中,我必须使用按钮上的.update()使其显示。现在我们不再阻塞,这是没有必要的。
在此处发布完整的工作解决方案,并显示pubsub实现。
import tkinter as tk
from tkinter import ttk
import threading
import time
from pubsub import pub
class MIGUI():
def __init__(self, master):
self.master = master
self.mediaImporter = MediaImporter()
self.startButton = ttk.Button(self.master, text='Start', command=self.startCallback)
self.startButton.pack()
self.abortButton = ttk.Button(self.master, text='Abort', command=self.abortCallback)
self.abortButton.state(['disabled'])
self.abortButton.pack()
self.progress = ttk.Progressbar(self.master, length=300)
self.progress.pack()
pub.subscribe(self.statusListener, 'STATUS')
pub.subscribe(self.progressListener, 'PROGRESS')
def statusListener(self, status, value):
print('MIGUI', status, value)
if status == 'copying' and (value == 'finished' or value == 'aborted'):
self.startButton.state(['!disabled'])
self.abortButton.state(['disabled'])
def progressListener(self, value):
print('Progress %d' % value)
self.progress['maximum'] = 100
self.progress['value'] = value
def startCallback(self):
print('startCallback')
self.abortButton.state(['!disabled'])
self.startButton.state(['disabled'])
self.x = threading.Thread(target=self.mediaImporter.startImport)
self.x.start()
# original issue had join() here, which was blocking.
def abortCallback(self):
print('abortCallback')
self.mediaImporter.abortImport()
class MediaImporter():
""" Interface between user (GUI / console) and worker classes """
def __init__(self):
self.Import = Import()
#other worker classes exist too
pub.subscribe(self.statusListener, 'STATUS')
def statusListener(self, status, value):
#perhaps do something
pass
def startImport(self):
self.Import.start()
def abortImport(self):
self.Import.abort()
class Import():
""" Worker
Does not know anything about other non-worker classes or UI.
It does use pubsub to publish messages - such as the status and progress.
The UI and interface classes can subsribe to these messages and perform actions. (see listener methods)
"""
def __init__(self):
self._wantAbort = False
def start(self):
self._wantAbort = False
self.doImport()
def abort(self):
pub.sendMessage('STATUS', status='abort', value='requested')
self._wantAbort = True
def doImport(self):
self.max = 13
pub.sendMessage('STATUS', status='copying', value='started')
for i in range(1,self.max):
#actual code has nested for..loops
progress = ((i+1) / self.max * 100.0)
pub.sendMessage('PROGRESS', value=progress)
time.sleep(.1)
if self._wantAbort:
pub.sendMessage('STATUS', status='copying', value='aborted')
return
pub.sendMessage('STATUS', status='copying', value='finished')
def main():
gui = True
console = False
if gui:
root = tk.Tk()
app = MIGUI(root)
root.mainloop()
if console:
#do simple console output without tkinter - threads not necessary
pass
if __name__ == '__main__':
main()