Tkinter进度条的图形用户界面

23

我有一个简单的Tk GUI和一个耗时较长的函数,该函数已附加到按钮上。当我点击按钮时,我希望出现一个进度条,就像开始一个长时间运行的进程一样。

我该如何实现呢?以下是我的当前代码:

from tkinter import Button, Tk, HORIZONTAL

from tkinter.ttk import Progressbar
import time


class MonApp(Tk):
    def __init__(self):
        super().__init__()

        bt1 = Button(self, text='Traitement', command=self.traitement)
        bt1.grid()
        self.progress = Progressbar(self, orient=HORIZONTAL, length=100, mode='indeterminate')
        self.progress.grid()
        self.progress.grid_forget()

    def traitement(self):
        self.progress.grid()
        self.progress.start()
        time.sleep(15) 
        ## Just like you have many, many code lines...

        self.progress.stop()


if __name__ == '__main__':
    app = MonApp()
    app.mainloop()

我该如何在这个应用程序中添加进度条?


ttk 包含一个 ttk.Progressbar 类。 - Luke Taylor
1
带有进度条的工作示例:https://dev59.com/VIHba4cB1Zd3GeqPSo7K#24770800 - furas
5个回答

33
你可以在tkdocs找到ttk.Progressbar
import time
from tkinter import *
from tkinter.ttk import *

tk = Tk()
progress = Progressbar(tk, orient=HORIZONTAL, length=100, mode='determinate')


def bar():
    progress['value'] = 20
    tk.update_idletasks()
    time.sleep(1)
    progress['value'] = 50
    tk.update_idletasks()
    time.sleep(1)
    progress['value'] = 80
    tk.update_idletasks()
    time.sleep(1)
    progress['value'] = 100

progress.pack()
Button(tk, text='foo', command=bar).pack()
mainloop()

最好使用线程并在另一个线程中运行您的代码。

像这样:

import threading
import time
from tkinter import Button, Tk, HORIZONTAL
from tkinter.ttk import Progressbar

class MonApp(Tk):
    def __init__(self):
        super().__init__()

        self.btn = Button(self, text='Traitement', command=self.traitement)
        self.btn.grid(row=0, column=0)
        self.progress = Progressbar(self, orient=HORIZONTAL, length=100, mode='indeterminate')

    def traitement(self):
        def real_traitement():
            self.progress.grid(row=1,column=0)
            self.progress.start()
            time.sleep(5)
            self.progress.stop()
            self.progress.grid_forget()

            self.btn['state']='normal'

        self.btn['state']='disabled'
        threading.Thread(target=real_traitement).start()


if __name__ == '__main__':
    app = MonApp()
    app.mainloop()

感谢您的回答。我不想在用户点击EXE按钮之前显示进度条... 我不明白如何在-> def sous_ens中显示进度条... 当我尝试将进度条放入函数sous_ens的代码中时,它只会在我退出函数时出现... - j666
1
当您想要显示进度条实例时,只需使用 pack(或 grid)函数进行打包,并使用 pack_forget(或 grid_forget)函数来隐藏它。 - xmcp
非常感谢。但我不明白如何...我放了一些简单的代码...当我开始处理函数时,如何添加进度条? - j666
2
你的代码几乎正确。只需使用 threading 库并在另一个线程中运行 traitement。请查看我的更新答案。 - xmcp
我的线程从主类外部开始。在那里它显示进度条未定义。有没有办法解决这个问题? - Naazneen Jatu
显示剩余3条评论

3
为了让所有GUI元素自我修改(在您的情况下,让进度条移动),执行必须命中app.mainloop()
在您的情况下,def traitement(self):启动并停止进度条而未命中主循环,因此未能在GUI上反映出预期的进度条移动。这里的问题是,当执行命中主循环时,进度条被配置为“停止”状态。
因此,按照@xmcp所示,在不同的线程上执行耗时活动是一个好主意。
但是,如果您不想使用线程,则可以使用after方法实现您想要的效果:
def stop_progressbar(self):
    self.progress.stop()

def traitement(self):
    self.progress.grid()
    self.progress.start()
    self.after(15000, self.stop_progressbar) 
    ## Call Just like you have many, many code lines...

上面的代码使用了 self.after() 方法来执行 stop_progressbar 方法,在15秒后停止进度条,而不是使用会阻塞主线程的 time.sleep() 方法。

你不必在tkinter应用程序中使用mainloop。你可以使用一个while true循环来更新窗口...它也可以工作(除了关闭窗口时会出现错误)。 - YJiqdAdwTifMxGR

1

tqdm是一个流行的进度条库,也支持tkinter(API链接)的实验性功能。它有效地是ttk.Progressbar的包装器。 使用方式没有很好地记录(并且存在明显的bug),但这里有一个最小化工作示例:

from tqdm.tk import tqdm
from time import sleep
from tkinter import Tk, Button   

window = Tk()    

pbar = tqdm(total=30, tk_parent=window)    

def run_task():
    for _ in range(30):
        sleep(0.1)
        pbar.update(1)
    pbar.close() # intended usage, might be buggy
    #pbar._tk_window.destroy() # workaround
    
start_button = Button(window, text="Start", command=run_task)
start_button.pack()
    
window.mainloop()

将会在一个单独的窗口中显示进度条,外观类似于这样:

enter image description here


0

分享我使用TQDM和TK的实验,参考了可爱的@xmcp和@runDOSrun的答案

不好的例子

进度条在一个方法中创建并完成 - 结果是程序似乎会挂起,直到完成的进度条弹出。

from tqdm.tk import tqdm
from time import sleep
from tkinter import Tk, Button

window = Tk()

def run_task():
    pbar = tqdm(total=30, tk_parent=window)
    for _ in range(30):
        sleep(0.1)
        pbar.update(1)

start_button = Button(window, text="Start", command=run_task)
start_button.pack()

window.mainloop()

基本工作示例

还添加了几行代码,当你按下按钮时禁用它,并在结束时重新启用。如果不加这两行代码,再次按下按钮会重新启动进度条。

from tqdm.tk import tqdm
from time import sleep
from tkinter import Tk, Button

window = Tk()

pbar = tqdm(tk_parent=window)  # Create the progress bar ahead of time
pbar._tk_window.withdraw()  # Hide it immediately

def run_task():
    start_button['state'] = 'disabled'
    pbar._tk_window.deiconify()
    pbar.reset(total=30)
    for _ in range(30):
        sleep(0.1)
        pbar.update(1)
    pbar._tk_window.withdraw()  # don't close the progress bar, just hide it for next time
    # pbar.close()  # intended usage, might be buggy
    # pbar._tk_window.destroy() # workaround
    start_button['state'] = 'normal'


start_button = Button(window, text="Start", command=run_task)
start_button.pack()

window.mainloop()

单个进度条;按钮被禁用 如果您省略禁用按钮的步骤,则再次按下按钮会重置进度条。

线程示例

这个示例不需要等待进度条完成。按钮仍然可用,因此如果您连续点击它,就可以同时启动多个进度条。

from tqdm.tk import tqdm
from time import sleep
from tkinter import Tk, Button
import threading

window = Tk()

def run_task():
    def threaded_task():
        pbar = tqdm(iterable=range(30), total=30, tk_parent=window)
        for _ in pbar:
            sleep(0.1)
            # pbar.update(1)
        # pbar.close()  # intended usage, might be buggy
        pbar._tk_window.destroy() # workaround
    threading.Thread(target=threaded_task).start()


start_button = Button(window, text="Start", command=run_task)
start_button.pack()

window.mainloop()

多个进度条

复杂示例

使用ThreadPoolExecutor同时生成多个进度条。主要的进度条应该保持在前景,并在子进程完成时更新。

import random
from tqdm.tk import tqdm
from time import sleep
from tkinter import Tk, Button
import threading
from concurrent.futures import ThreadPoolExecutor

window = Tk()


# pbar = tqdm(total=30, tk_parent=window)
# pbar._tk_window.withdraw()
# pbar._tk_window.deiconify()

def run_task():
    def threaded_task(iterable: []):
        def inner_task(n: int):
            pbar = tqdm(iterable=range(n), total=n, tk_parent=window, desc=threading.current_thread().name, grab=False)
            for _ in pbar:
                sleep(.1)
            # pbar.close()
            pbar._tk_window.destroy()

        with ThreadPoolExecutor(max_workers=8) as tpe:
            pbar = tqdm(tpe.map(inner_task, iterable), total=len(iterable), grab=True, desc="Main progress bar")
            pbar._tk_window.attributes('-topmost', True)  # Keep the main progress bar on top, or it's hard to see
            pbar._tk_window.focus_get()
            list(pbar)
            # Map makes an iterable, but you have to iterate on it to actually show the progress bar
            #    list is a quick and easy iteration.

            # pbar.close()
            pbar._tk_window.destroy()


    threading.Thread(target=threaded_task, kwargs={'iterable': random.sample(range(1, 100), 20)}).start()


start_button = Button(window, text="Start", command=run_task)
start_button.pack()

window.mainloop()

多个进度条同时运行

为了可用性,你几乎肯定想使用线程示例 - 基本的工作示例会重复使用一个预定义的进度条,而且有点笨拙。使用线程示例,你不必事先知道需要多少进度条。

你也可以完全禁用线程示例中的按钮,以防止多个并发激活。


有没有示例可以将进度条放在网格中的“内部”?类似于 pbar.grid(row=1) 这样的写法? - undefined

0

试试这个 -

from tkinter import *
from tkinter import ttk
import requests
from tqdm.tk import tqdm
from functools import partial

root = Tk()
root.geometry("600x500")
root.minsize(145, 50)
root.maxsize(900, 600)

frm = ttk.Frame(root)
frm.grid()

def down():
    # progress = Progressbar(root, orient=HORIZONTAL, length=100, mode='determinate')
    r = requests.get("https://www.win-rar.com/fileadmin/winrar versions/winrar/winrar-x64-621.exe", stream=True)
    totalbyte = int(r.headers['Content-Length'])

    print(totalbyte)
    byte = 0

    progress_bar = tqdm(total=totalbyte, unit='B', unit_scale=True, tk_parent=root)

    with open("win.rar", "wb") as f:
        for chunk in r.iter_content(chunk_size=120):
        progress_bar.update(120)
        f.write(chunk)
        byte += 120
        root.update()

    progress_bar.close()
    progress_bar._tk_window.destroy()
    print(byte)

ttk.Label(frm, text="Hello World!").grid(column=0, row=0)
ttk.Button(frm, text="Download win.rar file", 
command=partial(down)).grid(column=1, row=0)
ttk.Button(frm, text="Quit", command=root.destroy).grid(column=2, row=0)

root.mainloop()

你的回答可以通过提供更多支持信息来改进。请编辑以添加进一步的细节,例如引用或文档,以便他人可以确认你的答案是正确的。您可以在帮助中心找到有关如何编写良好答案的更多信息。 - Community

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