当前位置: 首页 > 知识库问答 >
问题:

尝试修复tkinter GUI冻结(使用线程)

淳于兴朝
2023-03-14

我有一个Python 3。x报表创建者,其I/O绑定(由于SQL而非python),在创建报表时主窗口将“锁定”数分钟。

所需要的只是在锁定GUI时使用标准窗口操作(移动、调整大小/最小化、关闭等)(GUI上的所有其他内容都可以保持“冻结”,直到所有报告完成)。

添加20181129:换句话说,tkinter必须只控制应用程序窗口的内容,并将所有标准(外部)窗口控件的处理留给O/S。如果我能做到这一点,我的问题就消失了,我不需要使用线程/子进程(Freezeups成为可接受的行为,类似于禁用“做报告”按钮)。

做这件事的最简单/最简单的方法(=对现有代码的干扰最小)是什么?理想的方法是使用Python

下面的所有内容都是支持信息,这些信息更详细地解释了问题、尝试过的方法以及遇到的一些微妙问题。

需要考虑的事情:

>

  • 用户选择他们的报告,然后在主窗口上按下“创建报告”按钮(当真正的工作开始和冻结发生时)。完成所有报表后,报表创建代码将显示一个(Toplevel)完成窗口。关闭此窗口可启用主窗口中的所有内容,允许用户退出程序或创建更多报告。

    新增20181129:在明显的随机间隔(相隔几秒钟)我可以移动窗口。

    除了显示“完成”窗口外,报告创建代码不以任何方式涉及GUI或tkinter。

    报告创建代码生成的某些数据必须显示在“完成”窗口中。

    没有理由“并行化”报表创建,尤其是因为同一台SQL server

    如果它影响解决方案:在创建每个报告时,我最终需要在GUI上显示报告名称(现在显示在控制台上)。

    第一次用python做线程/子处理,但熟悉其他语言。

    新增20181129:开发环境为64位Python 3.6。使用EclipseOxygen(pydev插件)在Win10上运行4次。应用程序必须至少可移植到linux。

    最简单的答案似乎是使用线程。只需要一个额外的线程(创建报告的线程)。受影响的线路:

    DoChosenReports()  # creates all reports (and the "Done" window)
    

    更改为:

    from threading import Thread
    
    CreateReportsThread = Thread( target = DoChosenReports )
    CreateReportsThread.start()
    CreateReportsThread.join()  # 20181130: line omitted in original post, comment out to unfreeze GUI 
    

    成功生成报告,其名称在创建时显示在控制台上
    但是,GUI仍然处于冻结状态,“完成”窗口(现在由新线程调用)从未出现。这会让用户无所适从,无法做任何事情,并且不知道发生了什么(如果有的话)(这就是为什么我希望在创建文件名时在GUI上显示文件名的原因)。

    顺便说一句,报告完成后,报告创建线程必须在显示完成窗口之前(或之后)悄悄自杀。

    我也试过使用

    from multiprocessing import Process
        
    ReportCreationProcess = Process( target = DoChosenReports )
    ReportCreationProcess.start()
    

    但这与主要项目“如果(_name_ == '_主要_):' " 测试”相冲突。

    添加了20181129:刚刚发现了wait_变量()universalwidget方法)。基本思想是将createreport代码作为一个doforever线程(守护进程?)由此方法控制(执行由GUI中的Do reports按钮控制)。

    从web研究中,我知道所有tkinter操作都应该在主(父)线程中执行,这意味着我必须将“完成”窗口移动到该线程
    我还需要该窗口来显示它从“子”线程接收的一些数据(三个字符串)。我正在考虑使用use应用程序级globals作为信号量(仅由createreport线程写入,仅由主程序读取)来传递数据。我知道,如果有两个以上的线程,但执行更多的操作(例如,使用队列?)对我来说,简单的情况似乎有点过分了。

    总而言之:当窗口因任何原因被冻结时,允许用户操作(移动、调整大小、最小化等)应用程序主窗口的最简单方法是什么。换句话说,O/S而不是tkinter必须控制主窗口的框架(外部)
    答案需要在python 3.2上使用。2以跨平台方式(至少在窗户上

  • 共有3个答案

    鄂慈
    2023-03-14

    我在我的一本书中找到了一个很好的例子,类似于你想做的,我认为它展示了一种使用tkinter线程的好方法。这是Alex Martinelli和David Ascher在《Python食谱》第一版中介绍的将Tkinter和异步I/O与线程相结合的方法9.6。代码是为Python2编写的。x、 但在Python3中只需稍作修改即可工作。

    正如我在一篇评论中所说,如果您想与GUI eventloop进行交互,或者只是想调整窗口大小或移动窗口,则需要保持GUI eventloop运行。下面的示例代码通过使用队列将数据从后台处理线程传递到主GUI线程来实现这一点。

    tkinter在()之后有一个名为的通用函数,它可以用来安排一个函数在经过一定时间后被调用。在下面的代码中,有一个名为periodic_call()的方法,它处理队列中的任何数据,然后在()之后调用,以便在短时间延迟后安排另一个调用,以便队列数据处理继续。

    由于()之后的是tkinter的一部分,它允许main循环()继续运行,这使得GUI在这些周期性队列检查之间保持“活性”。如果需要,它还可以调用tkinter来更新GUI(不像在单独的线程中运行的代码)。

    from itertools import count
    import sys
    import tkinter as tk
    import tkinter.messagebox as tkMessageBox
    import threading
    import time
    from random import randint
    import queue
    
    # Based on example Dialog 
    # http://effbot.org/tkinterbook/tkinter-dialog-windows.htm
    class InfoMessage(tk.Toplevel):
        def __init__(self, parent, info, title=None, modal=True):
            tk.Toplevel.__init__(self, parent)
            self.transient(parent)
            if title:
                self.title(title)
            self.parent = parent
    
            body = tk.Frame(self)
            self.initial_focus = self.body(body, info)
            body.pack(padx=5, pady=5)
    
            self.buttonbox()
    
            if modal:
                self.grab_set()
    
            if not self.initial_focus:
                self.initial_focus = self
            self.protocol("WM_DELETE_WINDOW", self.cancel)
            self.geometry("+%d+%d" % (parent.winfo_rootx()+50, parent.winfo_rooty()+50))
            self.initial_focus.focus_set()
    
            if modal:
                self.wait_window(self)  # Wait until this window is destroyed.
    
        def body(self, parent, info):
            label = tk.Label(parent, text=info)
            label.pack()
            return label  # Initial focus.
    
        def buttonbox(self):
            box = tk.Frame(self)
            w = tk.Button(box, text="OK", width=10, command=self.ok, default=tk.ACTIVE)
            w.pack(side=tk.LEFT, padx=5, pady=5)
            self.bind("<Return>", self.ok)
            box.pack()
    
        def ok(self, event=None):
            self.withdraw()
            self.update_idletasks()
            self.cancel()
    
        def cancel(self, event=None):
            # Put focus back to the parent window.
            self.parent.focus_set()
            self.destroy()
    
    
    class GuiPart:
        TIME_INTERVAL = 0.1
    
        def __init__(self, master, queue, end_command):
            self.queue = queue
            self.master = master
            console = tk.Button(master, text='Done', command=end_command)
            console.pack(expand=True)
            self.update_gui()  # Start periodic GUI updating.
    
        def update_gui(self):
            try:
                self.master.update_idletasks()
                threading.Timer(self.TIME_INTERVAL, self.update_gui).start()
            except RuntimeError:  # mainloop no longer running.
                pass
    
        def process_incoming(self):
            """ Handle all messages currently in the queue. """
            while self.queue.qsize():
                try:
                    info = self.queue.get_nowait()
                    InfoMessage(self.master, info, "Status", modal=False)
                except queue.Empty:  # Shouldn't happen.
                    pass
    
    
    class ThreadedClient:
        """ Launch the main part of the GUI and the worker thread. periodic_call()
            and end_application() could reside in the GUI part, but putting them
            here means all the thread controls are in a single place.
        """
        def __init__(self, master):
            self.master = master
            self.count = count(start=1)
            self.queue = queue.Queue()
    
            # Set up the GUI part.
            self.gui = GuiPart(master, self.queue, self.end_application)
    
            # Set up the background processing thread.
            self.running = True
            self.thread = threading.Thread(target=self.workerthread)
            self.thread.start()
    
            # Start periodic checking of the queue.
            self.periodic_call(200)  # Every 200 ms.
    
        def periodic_call(self, delay):
            """ Every delay ms process everything new in the queue. """
            self.gui.process_incoming()
            if not self.running:
                sys.exit(1)
            self.master.after(delay, self.periodic_call, delay)
    
        # Runs in separate thread - NO tkinter calls allowed.
        def workerthread(self):
            while self.running:
                time.sleep(randint(1, 10))  # Time-consuming processing.
                count = next(self.count)
                info = 'Report #{} created'.format(count)
                self.queue.put(info)
    
        def end_application(self):
            self.running = False  # Stop queue checking.
            self.master.quit()
    
    
    if __name__ == '__main__':  # Needed to support multiprocessing.
        root = tk.Tk()
        root.title('Report Generator')
        root.minsize(300, 100)
        client = ThreadedClient(root)
        root.mainloop()  # Display application window and start tkinter event loop.
    

    景鸿晖
    2023-03-14

    我修改了这个问题,把意外遗漏但关键的一行也包括进去。避免GUI冻结的答案非常简单:

    Don't call ".join()" after launching the thread.
    

    除上述之外,完整的解决方案包括:

    • 禁用“Do Reports”按钮,直到“create report”线程完成(技术上不需要,但防止额外的报告创建线程也可以防止用户混淆)
    • 让“创建报告”线程使用以下事件更新主线程:
      • “Completed report X”(在GUI上显示进度的增强功能)和
      • “完成所有报告”(显示“完成”窗口并重新启用“完成报告”按钮)

      使用multiprocessing.dummy模块的一个简单方法是:

          from multiprocessing.dummy import Process
      
          ReportCreationProcess = Process( target = DoChosenReports )
          ReportCreationProcess.start()
      

      同样,请注意缺少一个。join()行。

      作为一种临时攻击,“完成”窗口仍由createreport线程在退出之前创建。这可以工作,但确实会导致此运行时错误:

      RuntimeError: Calling Tcl from different appartment  
      

      然而,这个错误似乎不会引起问题。而且,正如其他问题所指出的,可以通过将“完成”窗口的创建移动到主线程中(并让创建报告线程发送事件“启动”该窗口)来消除错误。

      最后,我感谢@泰格霍克T3(他对我正在采取的方法进行了很好的概述)和@马蒂诺,他们介绍了如何处理更一般的情况,并引用了看起来很有用的资源。这两个答案都值得一读。

    皇甫福
    2023-03-14

    您需要两个函数:第一个封装程序的长期运行工作,第二个创建一个处理第一个函数的线程。如果您需要线程立即停止,如果用户在线程仍在运行时关闭程序(不推荐),请使用守护程序标志或查看事件对象。如果您不希望用户能够在完成之前再次调用该函数,请在启动时禁用按钮,然后在结束时将按钮设置为正常。

    import threading
    import tkinter as tk
    import time
    
    class App:
        def __init__(self, parent):
            self.button = tk.Button(parent, text='init', command=self.begin)
            self.button.pack()
        def func(self):
            '''long-running work'''
            self.button.config(text='func')
            time.sleep(1)
            self.button.config(text='continue')
            time.sleep(1)
            self.button.config(text='done')
            self.button.config(state=tk.NORMAL)
        def begin(self):
            '''start a thread and connect it to func'''
            self.button.config(state=tk.DISABLED)
            threading.Thread(target=self.func, daemon=True).start()
    
    if __name__ == '__main__':
        root = tk.Tk()
        app = App(root)
        root.mainloop()
    
     类似资料:
    • 我在JFace/SWT的帮助下用Java构建了一个应用程序。我主要使用JFace的,有时使用后面的SWT表。 我的表有一个标题(用列名填充),第一行用(过滤器的下拉菜单)中的呈现。 现在我要修复表中的第一行(“filter-row”),所以无论我是否向下滚动,它总是独立显示。 你知道有什么机会可以这样做吗(而不是像我在网上发现的那样,把一个表拆分成两个表)?

    • 我正在上谷歌Android应用程序课程,我遇到了一个奇怪的错误。如果调用,则不会出现布局,如果注释掉,则会出现布局。如果我在调用后返回,我将看到布局,如果我在调用后返回,则没有布局。 布局

    • 我正在使用intellij并遵循此文档: https://www.playframework.com/documentation/2.5.x/Migration25 我更改了插件。sbt如下: 然后它就卡住了: 我检查了这个存储库,没有2.5.3版本。 我做错了什么? 这是我的身材。sbt: 名称:="播放" 版本:="1.0" lazy val播放(文件中的项目(“.”)。enablePlugi

    • 可能重复: Java等待并通知:IllegalMonitorStateException 有什么问题 投掷:

    • 当数据被不可变地借用时,它还会冻结(freeze)。已冻结(frozen)数据无法通过原始对象来修改,直到指向这些数据的所有引用离开作用域为止。 fn main() { let mut _mutable_integer = 7i32; { // 借用 `_mutable_integer` let _large_integer = &_mutable_

    • 我有一个本体文件,正在使用OWL-API。我应该为我的类(#Doc)检索她的个人和他们的对象属性 实际上我尝试了两种方法来获取个人,但我总是遇到以下错误: 线程“main”java中出现异常。lang.NoClassDefFoundError:javax/inject/Provider (我想这意味着编译器找不到我的类!)