【发布时间】:2020-09-05 21:32:56
【问题描述】:
我想要一个带有进度条和一些条目、标签和按钮小部件的顶级窗口/对话框。我希望从 main_window 窗口更新对话框。 main_window 完成了这项工作,我需要将其反映在对话框中。我希望主窗口保持活动状态,以便您可以停止该过程。我还希望能够在对话框中停止该进程。
如果不使用多处理和线程,我无法让它工作。看来我要走错路了,还是我?另外我是多处理和线程的新手,所以我希望无论如何我都能正确地做到这一点。如果有人知道更好的方法,请告诉我。
下面是我做我想做的事情的尝试,它有效,但它是正确的方法吗?
我的第一次尝试:
import tkinter as tk
import tkinter.ttk as ttk
from time import sleep
from queue import Empty
from threading import Thread
from multiprocessing import Process, Queue
HIDE = -1
STOP = -2
BREAK = -3
PAUSE = -4
RESUME = -5
class App(tk.Tk):
def __init__(self, **kwargs):
title = kwargs.pop('title', '')
theme = kwargs.pop('theme', 'clam')
geometry = kwargs.pop('geometry', None)
exit_callback = kwargs.pop('exit_callback', None)
super().__init__(**kwargs)
self.title(title)
self.style = ttk.Style()
self.style.theme_use(theme)
if geometry:
self.geometry(geometry)
if exit_callback:
self.protocol('WM_DELETE_WINDOW', exit_callback)
def main_window(out_que, in_que, maximum):
def worker():
if app.running:
return
app.running = True
app.finished = False
for count in range(0, maximum + 1):
try:
message = in_que.get_nowait()
if message:
if message == PAUSE:
message = in_que.get()
if message == BREAK:
break
elif message == STOP:
app.destroy()
except Empty:
pass
sleep(0.1) # Simulate work.
out_que.put(count)
app.running = False
app.finished = True
start_btn.config(state=tk.NORMAL)
def app_stop():
out_que.put(STOP)
app.destroy()
def test_stop():
if app.running:
out_que.put(HIDE)
elif app.finished:
out_que.put(HIDE)
in_que.get()
stop_btn.config(state=tk.DISABLED)
start_btn.config(state=tk.NORMAL)
def test_start():
while not in_que.empty():
in_que.get()
stop_btn.config(state=tk.NORMAL)
start_btn.config(state=tk.DISABLED)
thread = Thread(target=worker, daemon=True)
thread.daemon = True
thread.start()
app = App(title='Main Window', theme='alt', geometry='350x150', exit_callback=app_stop)
app.running = False
app.finished = True
app.rowconfigure(0, weight=1)
app.rowconfigure(1, weight=1)
app.columnconfigure(0, weight=1)
start_btn = ttk.Button(app, text='Start Test', command=test_start)
start_btn.grid(padx=10, pady=5, sticky=tk.NSEW)
stop_btn = ttk.Button(app, text='Stop Test', state=tk.DISABLED, command=test_stop)
stop_btn.grid(padx=10, pady=5, sticky=tk.NSEW)
app.mainloop()
def progress_window(in_que, out_que, maximum):
def hide():
out_que.put(BREAK)
pause_btn.config(text='Pause')
app.withdraw()
def pause():
if progress_bar['value'] < progress_bar['maximum']:
text = pause_btn.cget('text')
text = 'Resume' if text == 'Pause' else 'Pause'
pause_btn.config(text=text)
out_que.put(PAUSE)
else:
pause_btn.config(text='Pause')
def worker():
while True:
data = in_que.get()
print(data)
if data == HIDE:
hide()
elif data == STOP:
app.destroy()
out_que.put(STOP)
break
elif not data:
app.deiconify()
progress_bar["value"] = 0
else:
progress_bar["value"] = data
app.update_idletasks()
app = App(title='Progress', theme='clam', geometry='350x150', exit_callback=hide)
app.rowconfigure(0, weight=1)
app.rowconfigure(1, weight=1)
app.columnconfigure(0, weight=1)
app.columnconfigure(1, weight=1)
progress_bar = ttk.Progressbar(app, orient=tk.HORIZONTAL, mode='determinate')
progress_bar["maximum"] = maximum
progress_bar.grid(padx=10, sticky=tk.EW, columnspan=1000)
pause_btn = ttk.Button(app, text='Pause', command=pause)
pause_btn.grid()
cancel_btn = ttk.Button(app, text='Cancel', command=hide)
cancel_btn.grid(row=1, column=1)
thread = Thread(target=worker)
thread.daemon = True
thread.start()
app.withdraw()
app.mainloop()
if __name__ == '__main__':
jobs = []
que1 = Queue()
que2 = Queue()
process = 50 # The maximum amount of work to process, # items.
for target in (main_window, progress_window):
p = Process(target=target, args=(que1, que2, process))
jobs.append(p)
p.start()
for j in jobs:
j.join()
这是我的第二次尝试,没有多处理,只是线程。
我已将代码更新为不使用多处理,只使用线程。线程是必要的还是可以在没有它的情况下完成并完成同样的事情?
代码似乎工作正常,但我做得对吗?我是线程新手,只是想确保在继续我的项目之前我做的事情是正确的。
import tkinter as tk
import tkinter.ttk as ttk
from time import sleep
from queue import Empty
from threading import Thread
from multiprocessing import Queue
HIDE = -1
STOP = -2
DONE = -3
BREAK = -4
PAUSE = -5
class App(tk.Tk):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.running = False
self.finished = True
self.app_que = Queue()
self.dialog_que = Queue()
self.process_items = 50
self.rowconfigure(0, weight=1)
self.rowconfigure(1, weight=1)
self.columnconfigure(0, weight=1)
self.title('Main Window')
self.geometry('350x150')
self.style = ttk.Style()
self.style.theme_use('clam')
wdg = self.start_btn = ttk.Button(self, text='Start Test', command=self.test_start)
wdg.grid(padx=10, pady=5, sticky=tk.NSEW)
wdg = self.stop_btn = ttk.Button(self, text='Stop Test', state=tk.DISABLED, command=self.test_stop)
wdg.grid(padx=10, pady=5, sticky=tk.NSEW)
self.dlg = ProgressDialog(self, title='Progress', geometry='350x150', process=self.process_items)
self.dlg.app_que = self.app_que
self.dlg.dialog_que = self.dialog_que
self.protocol('WM_DELETE_WINDOW', self.app_stop)
thread = Thread(target=self.dlg.worker, daemon=True)
thread.start()
def worker(self):
self.dlg.cancel_btn.config(text='Cancel')
self.dlg.pause_btn.config(state=tk.NORMAL)
for count in range(0, self.process_items + 1):
try:
message = self.app_que.get_nowait()
if message:
if message == PAUSE:
message = self.app_que.get()
if message == BREAK:
self.stop_btn.config(state=tk.DISABLED)
break
elif message == STOP:
self.destroy()
except Empty:
pass
sleep(0.1) # Simulate work.
self.dialog_que.put(count)
self.dialog_que.put(DONE)
self.dlg.cancel_btn.config(text='Close')
self.finished = True
self.start_btn.config(state=tk.NORMAL)
self.stop_btn.config(state=tk.DISABLED)
def app_stop(self):
self.dialog_que.put(STOP)
self.destroy()
def test_stop(self):
if self.running or self.finished:
self.dialog_que.put(HIDE)
self.stop_btn.config(state=tk.DISABLED)
self.start_btn.config(state=tk.NORMAL)
def test_start(self):
while not self.app_que.empty():
self.app_que.get()
thread = Thread(target=self.worker, daemon=True)
thread.start()
self.stop_btn.config(state=tk.NORMAL)
self.start_btn.config(state=tk.DISABLED)
self.dlg.deiconify()
class ProgressDialog(tk.Toplevel):
def __init__(self, parent, *args, **kwargs):
title = kwargs.pop('title', '')
process = kwargs.pop('process', 0)
geometry = kwargs.pop('geometry', None)
super().__init__(parent, *args, **kwargs)
self.withdraw()
self.app_que = None
self.dialog_que = None
self.rowconfigure(0, weight=1)
self.rowconfigure(1, weight=1)
self.columnconfigure(0, weight=1)
self.columnconfigure(1, weight=1)
self.title(title)
if geometry:
self.geometry(geometry)
wdg = self.progress_bar = ttk.Progressbar(self, orient=tk.HORIZONTAL, mode='determinate')
wdg["value"] = 0
wdg["maximum"] = process
wdg.grid(padx=10, sticky=tk.EW, columnspan=1000)
wdg = self.pause_btn = ttk.Button(self, text='Pause', command=self.pause)
wdg.grid()
wdg = self.cancel_btn = ttk.Button(self, text='Cancel', command=self.hide)
wdg.grid(row=1, column=1)
self.protocol('WM_DELETE_WINDOW', self.hide)
def worker(self):
while True:
message = self.dialog_que.get()
print(message)
if message == HIDE:
self.hide()
elif message == STOP:
self.app_que.put(DONE)
break
elif message == DONE:
self.pause_btn.config(state=tk.DISABLED)
else:
self.progress_bar["value"] = message
def hide(self):
self.app_que.put(BREAK)
self.pause_btn.config(text='Pause')
self.withdraw()
def pause(self):
if self.progress_bar['value'] < self.progress_bar['maximum']:
text = self.pause_btn.cget('text')
text = 'Resume' if text == 'Pause' else 'Pause'
self.pause_btn.config(text=text)
self.app_que.put(PAUSE)
else:
self.pause_btn.config(text='Pause')
if __name__ == '__main__':
app = App()
app.mainloop()
【问题讨论】:
-
从不同线程做 UI 通常是个坏主意,将主线程用于 UI 并在必要时创建工作线程。
-
@Mark Ransom,我认为在这种情况下我想要实现的目标是必要的。我试图在没有线程和进程的情况下做到这一点,但我无法让它在 tkinter 中以任何其他方式为我工作。
-
我给你这个建议是有原因的。如果您尝试从多个线程中使用它们,大多数 GUI 框架会严重崩溃;我不知道
tkinter,但没有理由会有所不同。 -
@Mark Ransom,是的,我明白这一点,但我仍然需要实现我的目标。如果我不能以我知道的任何其他方式做我想做的事,那会有什么不同。这就是重点,我需要知道制作工作模式的正确方法,例如。
-
使用 tkinter 标记是正确的。您必须在主线程中运行 tkinter。如果你不这样做,坏事就会发生。只要您注意如何与线程中的 GUI 元素进行交互,您需要在 GUI 本身之外执行的任何操作都可以在线程中。
标签: python multithreading tkinter multiprocessing ttk