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

问题描述

我有一个 Python 3.x 报表创建器,它的 I/O 绑定非常多(由于 SQL,而不是 python),以至于主窗口将锁定".分钟,同时创建报告.

I have a Python 3.x report creator that is so I/O bound (due to SQL, not python) that the main window will "lock up" for minutes while the reports are being created.

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

All that is needed is the ability to use the standard window actions (move, resize/minimize, close, etc.) while the GUI is locked-up (everything else on the GUI can stay "frozen" until all reports have finished).

添加 20181129:换句话说,tkinter 必须只控制应用程序窗口的内容,并将所有标准(外部)窗口控件的处理留给操作系统.如果我能做到这一点,我的问题就会消失,并且我不需要全部使用线程/子进程(冻结成为可接受的行为,类似于禁用执行报告"按钮).

Added 20181129 : In other words, tkinter must only control the CONTENTS of the application window and leave handling of all standard (external) window controls to the O/S. If I can do that my problem disappears and I don't need to use threads/subprocesses all (the freezeups become acceptable behaviour similar in effect to disabling the "Do Reports" button).

这样做的最简单/最简单的方法(= 对现有代码的干扰最小)是什么 - 理想情况下,以与 Python >= 3.2.2 和跨平台方式一起使用的方式(即至少适用于 Windows 和 linux).

What is the easiest/simplest way (= minimum disturbance to existing code) of doing this - ideally in a way that works with Python >= 3.2.2 and in a cross-platform way (i.e. works on at least Windows & linux).

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

Everything below is supporting information that explains the issue in greater detail, approaches tried, and some subtle issues encountered.

需要考虑的事项:

  • 用户选择他们的报告,然后推送创建报告".主窗口上的按钮(当实际工作开始并发生冻结时).完成所有报告后,报告创建代码将显示(顶层)完成".窗户.关闭此窗口会启用主窗口中的所有内容,允许用户退出程序或创建更多报告.

添加 20181129:我可以以明显随机的时间间隔(相隔几秒)移动窗口.

Added 20181129: At apparently random intervals (several seconds apart) I can move the window.

除了显示完成"窗口中,报表创建代码不涉及 GUI 或 tkinter.

Except for displaying the "Done" window, the report creation code does not involve the GUI or tkinter in any way.

报告创建代码生成的某些数据必须出现在完成"页面上.窗口.

Some data produced by the report creation code must appear on the "Done" window.

没有理由并行化";报告创建,特别是因为相同的 SQL 服务器 &数据库用于创建所有报告.

There's no reason to "parallelize" report creation especially since the same SQL server & database is used to create all reports.

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

In case it affects the solution : I'll eventually need to display the report names (now shown on the console) on the GUI as each report gets created.

第一次使用 python 进行线程/子处理,但熟悉其他语言.

First time doing threading/subprocessing with python but am familiar with both from other languages.

添加 20181129:开发环境是 Win 10 上的 64 位 Python 3.6.4,使用 Eclipse Oxygen(pydev 插件).应用程序必须至少可移植到 linux.

Added 20181129 : Development environment is 64 bit Python 3.6.4 on Win 10 using Eclipse Oxygen (pydev plugin). Application must be portable to at least linux.

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

The simplest answer seems to be to use threads. Only one additional thread is needed (the one that creates the reports). The affected line:

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 上显示它们).

successfully produces the reports with their names being displayed on the console as they get created.
However, the GUI remains frozen and the "Done" window (now invoked by the new thread) never appears. This leaves the user in limbo, unable to do anything and wondering what, if anything, has happened (which is why I want to display the filenames on the GUI as they get created).

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

BTW, After the reports are done the report creation thread must quietly commit suicide before (or after) the Done window is shown.

我也尝试过使用

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

但这与主程序if (_name_ == '_main_) :' 相冲突"测试.

but that fell afoul of the main programs "if (_name_ == '_main_) :' " test.

添加 20181129:刚刚发现 wait_variable() 通用小部件方法).基本思想是将创建报告代码作为由该方法控制的永久线程(守护进程?)启动(执行由 GUI 中的 Do reports 按钮控制).

Added 20181129 : Just discovered the wait_variable() universal widget method). Basic idea is to launch the create report code as an do-forever thread (daemon?) controlled by this method (with execution controlled by the Do reports button in the GUI).

通过网络研究,我知道所有 tkinter 操作都应该在主(父)线程中进行,意味着我必须移动完成"那个线程的窗口.
我还需要那个窗口来显示它从孩子"接收到的一些数据(三个字符串).线.我正在考虑使用应用程序级全局变量作为信号量(仅由创建报告线程写入并且仅由主程序读取)来传递数据.我知道这对于两个以上的线程可能会有风险,但在我的简单情况下做更多的事情(例如使用队列?)似乎有点矫枉过正.

From web research I know that all tkinter actions should be made from within the main (parent) thread, meaning that I must move the "Done" window to that thread.
I also need that window to display some data (three strings) it receives from the "child" thread. I'm thinking of using use application-level globals as semaphores (only written to by the create report thread and only read by the main program) to pass the data. I'm aware that this can be risky with more than two threads but doing anything more (e.g. using queues?) for my simple situation seems like overkill.

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

To summarize: What's the easiest way to allow the user to manipulate (move, resize, minimize, etc.) an application's main window while the window is frozen for any reason. In other words, the O/S, not tkinter, must control the frame (outside) of the main window.
The answer needs to work on python 3.2.2+ in a cross-platform way (on at least Windows & linux)


解决方案

你需要两个函数:第一个封装你的程序长时间运行的工作,第二个创建一个线程来处理第一个函数.如果用户在线程仍在运行时关闭程序,您需要立即停止线程(不推荐),请使用 daemon 标志或查看 Event 对象.如果您不希望用户在完成之前再次调用该函数,请在该按钮启动时禁用该按钮,然后在结束时将该按钮恢复为正常状态.

You'll need two functions: the first encapsulates your program's long-running work, and the second creates a thread that handles the first function. If you need the thread to stop immediately if the user closes the program while the thread is still running (not recommended), use the daemon flag or look into Event objects. If you don't want the user to be able to call the function again before it's finished, disable the button when it starts and then set the button back to normal at the end.

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()

相关文章