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

Python 尝试修复tkinter GUI冻结(使用线程),python,python-3.x,multithreading,tkinter,subprocess,Python,Python 3.x,Multithreading,Tkinter,Subprocess,我有一个Python3.x报表创建者,它的I/O绑定(由于SQL,而不是Python),在创建报表时,主窗口将“锁定”几分钟 所需要的只是在锁定GUI时使用标准窗口操作(移动、调整大小/最小化、关闭等)(GUI上的所有其他内容都可以保持“冻结”,直到所有报告完成) 新增20181129:换句话说,tkinter必须只控制应用程序窗口的内容,并将所有标准(外部)窗口控件的处理留给O/S。如果我能做到这一点,我的问题就消失了,我不需要使用线程/子进程所有(冻结成为可接受的行为,类似于禁用“执行报告”

我有一个Python3.x报表创建者,它的I/O绑定(由于SQL,而不是Python),在创建报表时,主窗口将“锁定”几分钟

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

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

最简单/最简单的方法(=对现有代码的最小干扰)是什么?理想的方法是使用Python>=3.2.2并以跨平台的方式(即至少在Windows和linux上工作)


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

需要考虑的事项:

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

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

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

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

  • 没有理由“并行化”报表创建,尤其是因为使用相同的SQL server和数据库创建所有报表

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

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

  • 添加了20181129:开发环境是64位Python 3.6.4,使用Eclipse氧气(pydev插件)在Win 10上运行。应用程序必须至少可移植到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()
但这与主程序的“if(uuu name_uuuuuuu=='uuu main_uuuu):'”测试相冲突


新增20181129:刚刚发现“waitvariable”通用小部件方法(请参阅)。基本思想是作为一个do forever线程(守护进程?)启动create report代码,该线程由该方法控制(执行由GUI上的“do reports”按钮控制)


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


总而言之:当窗口因任何原因被冻结时,允许用户操作(移动、调整大小、最小化等)应用程序主窗口的最简单方法是什么。换句话说,O/S而不是tkinter必须控制主窗口的框架(外部)。

答案需要以跨平台的方式在python 3.2.2+上运行(至少在Windows和linux上)

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

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

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

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

Tkinter有一个被调用的通用函数,它可以用来安排在经过一定时间后调用的函数。在下面的代码中有
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.
Don't call ".join()" after launching the thread.
    from multiprocessing.dummy import Process

    ReportCreationProcess = Process( target = DoChosenReports )
    ReportCreationProcess.start()
RuntimeError: Calling Tcl from different appartment