Tkinter进度条如何在模态对话框中正确实现

7

我想要一个包含进度条、几个输入框、标签和按钮小部件的顶层窗口/对话框。我希望对话框从主窗口更新。主窗口完成工作,我需要在对话框中反映出来。我希望主窗口保持活动状态,以便您可以停止该过程。我还希望能够在对话框中停止该过程。

如果没有使用多处理和线程,我无法使其正常工作。看起来我走了错误的路线,或者是吗?另外,我对多处理和线程还不熟悉,所以我希望我已经正确地做了这些。如果有人知道更好的方法,请告诉我。

以下是我尝试做到我想要的东西的代码,它可以工作,但是这是否是正确的方法呢?

我的第一次尝试:

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

@Mark Ransom,我认为在这种情况下,这是我想要实现的必要条件。我已经尝试过不使用线程和进程来完成这个任务,但在tkinter中我无法找到其他方法使其正常工作。 - Daniel Huckson
1
我给你那个建议是有原因的。大多数GUI框架在尝试从多个线程中使用时会出现严重问题;我不知道tkinter具体情况,但没有理由它会有所不同。 - Mark Ransom
@Mark Ransom,是的,我理解你的意思,但我仍然需要实现我的目标。如果我不知道其他方法,那么这有什么区别呢?这就是关键,我需要知道制作一个可工作的模态框的正确方法,例如一个示例。 - Daniel Huckson
如果你的目标与世界运作方式不一致,就不要期望得到太多有用的帮助。我的建议仍然是使用线程(如果需要),但不要让它们接触 tkinter;使用其他机制来传递线程内外的信息。 - Mark Ransom
1
使用tkinter时,Mark是正确的。你必须在主线程中运行tkinter,否则会发生糟糕的事情。如果你需要在GUI之外做任何事情,只要你小心地从线程中与GUI元素交互,就可以在一个线程中完成。 - Mike - SMT
显示剩余2条评论
1个回答

5
如果您不想使用thread,也许可以尝试使用asyncio。我不知道我的代码是否正确,但是在我的电脑上它运行得很好。
欢迎指出我的代码中的错误,我真的不知道这是否是一个好习惯。
import tkinter as tk
from tkinter import ttk
import asyncio, time
import warnings


class App(tk.Tk):
    def __init__(self):
        super(App, self).__init__()

        self.start_btn = ttk.Button(self, text="Start Test", command=self.test_start)
        self.start_btn.pack(padx=10, pady=5, fill="both", expand=True)

        self.stop_btn = ttk.Button(self, text="Stop Test", command=self.test_stop, state=tk.DISABLED)
        self.stop_btn.pack(padx=10, pady=5, fill="both", expand=True)

        self.test_window = tk.Toplevel()
        self.test_window.progressbar = ttk.Progressbar(self.test_window, orient=tk.HORIZONTAL)
        self.test_window.progressbar.grid(padx=10, pady=5, sticky=tk.NSEW, columnspan=2, column=0, row=0)

        self.test_window.switch_btn = ttk.Button(self.test_window, text="Pause", command=self.switch)
        self.test_window.switch_btn.grid(padx=10, pady=5, sticky=tk.NSEW, column=0, row=1)
        self.test_window.cancel_btn = ttk.Button(self.test_window, text="Cancel", command=self.test_cancel)
        self.test_window.cancel_btn.grid(padx=10, pady=5, sticky=tk.NSEW, column=1, row=1)

        self.test_window.withdraw()

    def test_start(self):
        self.stop_btn['state'] = tk.NORMAL
        self.test_window.deiconify()
        self.test_window.after(0, self.work)

    def work(self):
        async def async_work(): # define a async task
            try:
                await asyncio.sleep(3)  # could be another async work.
            except asyncio.CancelledError:
                print("cancel or stop")
                raise  # if don't raise the error ,it won't cancel

        async def progressbar_add():
            self.task = asyncio.create_task(async_work())
            timeout = 0
            while True: # wait the async task finish
                done, pending = await asyncio.wait({self.task}, timeout=timeout)
                self.test_window.update()
                if self.task in done:
                    self.test_window.progressbar['value'] += 10  # if finished, value += 10
                    print(self.test_window.progressbar['value'])
                    await self.task
                    break

        if self.test_window.progressbar['value'] >= 100:
            return
        asyncio.run(progressbar_add())
        self.test_window.after(0, self.work)

    def test_stop(self):
        self.test_window.progressbar['value'] = 0
        self.stop_btn['state'] = tk.DISABLED
        try:
            all_tasks = asyncio.Task.all_tasks()
            for task in all_tasks:
                task.cancel()
        except RuntimeError:  # if you have cancel the task it will raise RuntimeError
            pass

    def switch(self):
        if self.test_window.switch_btn['text'] == 'Pause':
            self.test_window.switch_btn['text'] = 'Resume'
            try:
                all_tasks = asyncio.Task.all_tasks()
                for task in all_tasks:
                    task.cancel()
            except RuntimeError:  # if you have cancel the task it will raise RuntimeError
                pass
        else:
            self.test_window.switch_btn['text'] = 'Pause'
            return self.work()

    def test_cancel(self):
        # self.test_window.progressbar['value'] = 0
        print(self.test_window.progressbar['value'])
        self.test_window.withdraw()
        self.task.cancel()


app = App()
app.mainloop()

在 Python 3.7 及以下版本中,无法使用 asyncio.run(async)。该方法是在 Python 3.7 中添加的。需要使用 get_event_loop()run_until_complete() 方法。(由 @Saad 指出)

2
如果不进行修改,你的代码在Python 3.7以下版本将无法正常工作,因为asyncio的一些方法在Python 3.7中已经发生了变化。在此处查看更改 - Saad
你的机器会发生什么?它是Linux吗? - jizhihaoSAMA
@jizhihaoSAMA,是的,我使用Linux。当我点击暂停按钮时,程序会出现异常错误。 - Daniel Huckson
正确的写法是CancelledError。否则它将无法停止异步任务。 - jizhihaoSAMA
@jizhihaoSAMA,哪种方式更好?你做的那种方式好还是我做的那种方式好?我认为我做的那种方式更好,因为asyncio需要至少Python 3.7才能工作。 - Daniel Huckson
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接