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

Dan*_*son 6 python multithreading tkinter multiprocessing ttk

我想要一个带有进度条和一些条目、标签和按钮小部件的顶级窗口/对话框。我希望从 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()
Run Code Online (Sandbox Code Playgroud)

这是我的第二次尝试,没有多处理只是线程。

我已经更新了代码,不使用多处理,只使用线程。线程是必要的还是可以不用它来完成并完成同样的事情?

代码似乎工作正常,但我做得对吗?我是线程的新手,只是想确保在继续我的项目之前我做的事情是正确的。

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()
Run Code Online (Sandbox Code Playgroud)

jiz*_*AMA 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()
Run Code Online (Sandbox Code Playgroud)

在 Python 3.7 以下,您不能使用 .asyncio.run(async)它是在 Python 3.7中添加的。需要使用get_event_loop()run_until_complete()。(@Saad 指出)

  • 如果不进行修改,您的代码将无法在 python 3.7 以下运行,因为“asyncio”的某些方法在 python 3.7 中已更改。[请参阅此处的更改](https://docs.python.org/3/whatsnew/3.7.html)。 (2认同)