我有一个Python 3.x报表创建器,该报表创建器受I / O约束(由于SQL,而不是python),因此在报表创建过程中主窗口将“锁定” 分钟创建。
所有需要做的就是能够在GUI处于锁定状态时使用标准的窗口操作(移动,调整大小/最小化,关闭等)(GUI上的所有其他内容都可以保持“冻结”状态,直到所有报告都已冻结)。完成)。
添加了20181129:换句话说,tkinter必须仅控制应用程序窗口的内容,并将所有标准(外部)窗口控件的处理留给操作系统。如果可以的话,我的问题就消失了,并且我不需要全部使用线程/子进程(冻结成为可接受的行为,类似于禁用“执行报告”按钮)。
执行此操作的最简单/最简单的方法(=对现有代码的最小干扰)是什么?理想情况下,该方法应与Python> = 3.2.2一起使用,并且应采用跨平台的方式(即至少可以在Windows和linux上运行)。
下面的所有内容都是支持性信息,可以更详细地说明问题,尝试的方法以及遇到的一些细微问题。
注意事项:
用户选择他们的报告,然后在主窗口上按“创建报告”按钮(当实际工作开始并且冻结时)。完成所有报告后,报告创建代码将显示一个(顶级)“完成”窗口。关闭此窗口将启用主窗口中的所有内容,从而允许用户退出程序或创建更多报告。
添加了20181129:显然,我可以以随机的间隔(相隔几秒钟)移动窗口。
除了显示“完成”窗口外,报告创建代码不以任何方式涉及GUI或tkinter。
由报表创建代码生成的某些数据必须出现在“完成”窗口中。
没有理由“并行化”报告的创建,尤其是因为使用相同的SQL Server和数据库来创建所有报告。
以防影响解决方案:创建每个报告时,我最终需要在GUI上显示报告名称(现在显示在控制台上)。
第一次使用python进行线程处理/子处理,但同时熟悉其他语言。
添加了20181129:开发环境是Win 10上使用Eclipse Oxygen(pydev插件)的64位Python 3.6.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保持冻结状态,“ Done”窗口(现在由新线程调用)从不出现。这使用户陷入困境,无法做任何事情,
想知道发生了什么(如果有的话)(这就是为什么我想在创建文件名时在GUI上显示它们)。
顺便说一句,在完成报告之后,报告创建线程必须在显示“完成”窗口之前(或之后)悄悄地自杀。
我也尝试使用
from multiprocessing import Process
ReportCreationProcess = Process( target = DoChosenReports )
ReportCreationProcess.start()
但这与主程序“ if(__name__ =='__main__):'”测试相抵触。
添加了20181129:刚刚发现了“ waitvariable”通用小部件方法(请参见http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/universal.html)。基本思想是将创建报告代码作为由该方法控制的永久执行线程(守护进程?)启动(执行由GUI上的“执行报告”按钮控制)。
根据网络研究,我知道所有tkinter动作都应在主(父)线程内进行,
表示我必须将“完成”窗口移至该线程。
我还需要该窗口来显示它从“子”线程接收的一些数据(三个字符串)。我正在考虑使用使用应用程序级全局变量作为信号量(仅由create report线程写入,而仅由主程序读取)以传递数据。我知道,使用两个以上的线程可能会有风险,但为我的简单情况做更多的事情(例如使用队列?)似乎有点过头了。
总结:允许用户在由于任何原因冻结窗口时对其进行操作(移动,调整大小,最小化等)的最简单方法是什么?换句话说,O / S,而不是tkinter,必须控制主窗口的框架(外部)。
答案需要以跨平台方式(至少在Windows和linux上)在python 3.2.2+上工作
答案 0 :(得分:1)
您将需要两个函数:第一个封装了程序的长时间运行的工作,第二个创建了处理第一个函数的线程。如果您需要线程在用户仍在运行时关闭程序时立即停止线程(不建议),请使用daemon
标志或查看Event
对象。如果您不希望用户在功能完成之前再次调用该功能,请在启动时禁用该按钮,然后在最后将其设置回正常状态。
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()
答案 1 :(得分:0)
我从其中一本书中找到了一个与您想做的事情类似的好例子,我认为这是在tkinter中使用线程的一种好方法。在Alex Martinelli和David Ascher的第一本书 Python Cookbook 中,它是9.6版的将Tkinter和异步I / O与线程结合。该代码是为Python 2.x编写的,但仅需进行少量修改即可在Python 3中工作。
正如我在评论中所说,如果您希望能够与之交互或者只是调整窗口大小或移动窗口,则需要保持GUI事件循环运行。下面的示例代码通过使用Queue
将数据从后台处理线程传递到主GUI线程来完成此操作。
Tkinter具有一个称为after()
的通用函数,可以使用该函数来计划要经过一定时间后要调用的函数。在下面的代码中,有一个名为periodic_call()
的方法,该方法处理队列中的所有数据,然后在短暂的延迟后调用after()
来调度对自身的另一个调用,以便继续进行队列数据处理。
由于after()
是tkinter的一部分,因此它允许mainloop()
继续运行,从而使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.
答案 2 :(得分:0)
我修改了问题,以包括意外省略但很关键的一行。避免GUI冻结的答案非常简单:
Don't call ".join()" after launching the thread.
除上述之外,完整的解决方案还包括:
使用multiprocessing.dummy模块(从3.0和2.6开始可用)的一种简单方法是:
from multiprocessing.dummy import Process
ReportCreationProcess = Process( target = DoChosenReports )
ReportCreationProcess.start()
再次,请注意没有.join()行。
作为临时黑客,创建报告线程仍会在退出之前立即创建“完成”窗口。可以,但是会导致运行时错误:
RuntimeError: Calling Tcl from different appartment
但是该错误似乎并未引起问题。并且,正如其他问题所指出的那样,可以通过将“ DONE”窗口的创建移至主线程中来消除错误(并使创建报告线程发送事件以“启动”该窗口)。
最后,我要感谢@ TigerhawkT3(他们很好地概述了我所采用的方法)和@martineau,他们介绍了如何处理更一般的情况,并提供了对看起来有用的资源的参考。这两个答案都值得一读。