【问题标题】:Python 3, Tkinter and threading a long-running process, OOP stylePython 3,Tkinter 和线程一个长时间运行的进程,OOP 风格
【发布时间】:2019-12-11 16:59:31
【问题描述】:

我的小程序可能有一个运行时间很长的进程。从控制台执行此操作时这不是问题,但现在我想添加一个 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()

【问题讨论】:

    标签: python multithreading tkinter


    【解决方案1】:

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

    【讨论】:

    • 谢谢 XORNAND。您的回答为我指明了正确的方向。不管有没有 join() 我仍然无法点击 Abort 按钮。我仍然需要找到一种方法来检测长时间运行的进程 (Import.doImport) 何时完成。目前我还在尝试一些事情,等我有更清晰的结果后会发布。
    【解决方案2】:

    再次感谢 XORNAND。 join() 绝对是问题的一部分。问题的另一部分是 MIGUI 类无法知道长时间运行的进程何时完成(因为它已经运行了它的过程,或者因为它被中止了)。低级工作者和 UI 层。我确实尝试使用 threading.Event 但没有成功,并且确实考虑过使用队列。

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

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

    为了确保 pubsub 的实现按计划进行,我还设置了一个 tkinter 进度条。 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()
    

    【讨论】:

      猜你喜欢
      • 2013-01-17
      • 2011-08-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-09-27
      • 1970-01-01
      • 2011-08-31
      相关资源
      最近更新 更多