python3,Tkinter和threading是一个长时间运行的进程,面向对象

2024-09-30 18:23:19 发布

您现在位置:Python中文网/ 问答频道 /正文

我的小程序有一个潜在的长期运行过程。从控制台执行时这不是问题,但是现在我想添加一个GUI。理想情况下,我希望使用Tkinter(a)因为它很简单,(b)因为它可能更容易跨平台实现。从我所读到的和经历来看,(几乎)所有的gui都有同样的问题

在我所有关于线程和GUI的阅读中,似乎有两条流。1-底层工作进程正在轮询(如等待获取数据),2-工作进程正在做大量工作(如在for循环中复制文件)。我的程序属于后者

我的代码有一个类的“层次结构”。
MIGUI类处理GUI并与接口类MediaImporter交互。 MediaImporter类是用户界面(控制台或GUI)和工作类之间的接口。 Import类是长期运行的worker。它不知道接口或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()


Tags: importselfmasterdefupdategui线程state
2条回答

再次感谢你。join()肯定是问题的一部分。问题的另一部分是MIGUI类无法知道长时间运行的进程何时完成(可能是因为它已经运行了自己的进程,也可能是因为它被中止了)。在低级工作进程和UI层之间需要一个额外的消息传递层。我确实尝试过使用threading.Event,但没有成功,并且考虑过使用队列

我的解决方案是使用pubsub(https://github.com/schollii/pypubsub)工作层可以发送关于各种主题的消息,UI层和接口层可以设置侦听器来对接收到的数据执行操作

在我的例子中,Import.doImport方法在完成时发送一条状态消息。然后,MIGUI侦听器可以相应地触发Start/Abort按钮

为了确保pubsub的实现按计划进行,我还设置了一个tkinterprogressbar。doImport方法发送一个带有完成百分比的proges消息。这反映在屏幕上的Progressbar中

一个旁注-在我的原始版本中,我必须在按钮上使用.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()

GUI被阻塞的原因是调用self.x.join(),它阻塞直到doImport函数完成,请参阅join文档。相反,我将在abortCallback()函数中调用join(),因为这将导致线程停止运行

相关问题 更多 >