【问题标题】:Python tkinter update_idletasks is blanking the windowPython tkinter update_idletasks 正在空白窗口
【发布时间】:2015-10-03 02:41:28
【问题描述】:

[根据 cmets 对原始帖子进行了相当大的编辑,并使上下文 - 2 个模块 - 更清晰,并总结我认为的关键潜在问题。代码也更新了。我有一个工作版本,但完全不确定它是否以正确的方式完成。] (免责声明......我正在学习 Tkinter!)

我试图在应用程序运行时显示进度条(例如,遍历音乐库文件夹树,但这在这里无关紧要)。

我想将它作为一个与主应用程序分开的模块中的类来实现,这样我就可以在其他地方使用它(应用程序本身实际上也是在 2 个模块中)。

出于这个原因,也因为我不想打乱应用程序的主窗口设计,我希望进度条出现在单独的窗口中。

我已经尝试了这两种方法......我自己使用文本小部件粗略绘制的进度条,并且 - 一旦我发现它 - ttk.Progressbar。我现在专注于使用 ttk.Progressbar。

但是请注意,这两种方法都存在基本相同的问题,即在不阻止控制恢复到调用模块的情况下正确显示进度窗口的内容。

我的这个 (ProgressBar) 类具有启动、更新和停止进度条的方法。据我了解,有三种方法可以强制刷新类方法中的状态窗口。这三个似乎都有缺点。

  • root.master.mainloop() 将控制权保留在进度窗口中,并且应用程序停止执行。这基本上达不到目的。
  • root.master.update_idletasks() 将控制权交还给调用应用程序,但状态窗口显示为空白。出于不同的原因,也违背了目的。
  • root.master.update() 似乎工作正常,状态窗口更新为可见内容,控制权返回到调用应用程序。但是我在几个地方读到过这是一种危险的使用方法。

所以基本问题是 - 强制窗口更新的正确方法是什么(例如 Set 方法);以及为什么 update_idletasks() 会清空进度窗口。

我相信以下代码反映了所提出的建议,但我已对其进行了调整以反映预期的导入类。

# dummy application importing the StatusBar class.
# this reflects app is itslef using tkinter

from ProgressBar12 import ProgressBar

import tkinter as Tk
import time
import os

def RunAppProcess():

    print('App running')
    Bar = ProgressBar(tkroot)      # '' arg to have ProgressBar create its tkroot
    Bar.start('Progress...', 0)   # >0 determinate (works) / 0 for indeterminate (doesnt!)

    print('starting process')
    # this simulates some process, (eg for root, dirs, files = os.walk(lib))
    for k in range(10):
        Bar.step(5)                # (should be) optional for indeterminate
        time.sleep(.2)
    Bar.stop('done')               # '' => kill the window; or 'message' to display in window

def EndAppProcess():
    tkroot.withdraw()
    tkroot.destroy()

# Application init code, the application is using tkinter
# (should probably be in an init procedure etc, but this will serve)
tkroot = Tk.Tk()
tkroot.title("An Application")
tkroot.geometry("100x100")
tkroot.configure(bg='khaki1')
# a 2 button mini window:  [Start] and [Quit]
Tk.Button(tkroot, text='Start', bg='orange', command=RunAppProcess).grid(sticky=Tk.W)
Tk.Button(tkroot, text="Quit", bg="orange", command=EndAppProcess).grid(sticky=Tk.W)
tkroot.mainloop()

进度条模块

# determinate mode
import tkinter as Tk
import tkinter.font as TkF
from tkinter import ttk
import time

# print statements are for tracing execution
# changes from the sample code previsouly given reflect:
# - suggestions made in the answer and in comments
# - to reflect the actual usage with the class imported into a calling module rather than single module solution
# - consistent terminology (progress not status)
# - having the class handle either determinate or indeterminate progress bar


class ProgressBar():

    def __init__(self, root):
        print('progress bar instance init')
        if root == '':
            root = tkInit()
        self.master=Tk.Toplevel(root)
        # Tk.Button(root, text="Quit all", bg="orange", command=root.quit).grid()   A bit rude to mod the callers window
        self.customFont2 = TkF.Font(family="Calibri", size=12, weight='bold')
        self.customFont5 = TkF.Font(family="Cambria", size=16, weight='bold')
        self.master.config(background='ivory2')
        self.create_widgets()
        self.N = 0
        self.maxN = 100 # default for %

    def create_widgets(self):

        self.msg = Tk.Label(self.master, text='None', bg='ivory2', fg='blue4') #,                           font=self.customFont2)
        self.msg.grid(row=0, column=0, sticky=Tk.W)

        self.bar = ttk.Progressbar(self.master, length=300, mode='indeterminate')
        self.bar.grid(row=1, column=0, sticky=Tk.W)

        #self.btn_abort = Tk.Button(self.master, text=' Abort ', command=self.abort, font=self.customFont2, fg='maroon')
        #self.btn_abort.grid(row=2,column=0, sticky=Tk.W)
        #self.master.rowconfigure(2, pad=3)

        print('progress bar widgets done')

    def start(self, msg, maxN):
        if maxN <= 0:
            #indeterminate
            self.msg.configure(text=msg)
            self.bar.configure(mode='indeterminate')
            self.maxN = 0
            self.bar.start()
            self.master.update()

        else: # determinate
            self.msg.configure(text=msg)
            self.bar.configure(mode='determinate')
            self.maxN = maxN
            self.N = 0
            self.bar['maximum'] = maxN
            self.bar['value'] = 0

    def step(self, K):
        #if self.maxN == 0: return    # or raise error?
        self.N = min(self.maxN, K+self.N)
        self.bar['value'] = self.N
        self.master.update()         # see  set(..)

    def set(self, K):
        #if self.maxN == 0: return
        self.N = min(self.maxN, K)
        self.bar['value'] = self.N
        #self.master.mainloop()         # <<< calling module does not regain control.  Pointless.
        #self.master.update_idletasks   # <<< works, EXCEPT statusbar window is blank! Also pointless.  But calling module regains control
        self.master.update()            # <<< works in all regards, BUT I've read this is dangerous.


    def stop(self, msg):
        print('progress bar stopping')
        self.msg.configure(text=msg)
        if self.maxN <= 0:
            self.bar.stop()
        else:
            self.bar['value'] = self.maxN
            #self.bar.stop()
        if msg == '':
            self.master.destroy()
        else: self.master.update()

    def abort(self):
        # eventually will raise an error to the calling routine to stop the process
        self.master.destroy()

def tkInit():
    print('progress bar tk init')
    tkroot = Tk.Tk()
    tkroot.title("Progress Bar")
    tkroot.geometry("250x50")
    tkroot.configure(bg='grey77')
    tkroot.withdraw()
    return tkroot

if (__name__ == '__main__'):
    print('start progress bar')
    tkroot = tkInit()
    tkroot.configure(bg='ivory2')
    Bar = ProgressBar(tkroot)
    Bar.start('Demo', 10)
    for k in range(11):
        Bar.set(k)
        time.sleep(.2)
    Bar.stop('done, you can close me')

else:
    # called from another module
    print('progress bar module init. (nothing) done.')

这是基于答案中的第一个解决方案;作为替代方案,我将尝试使用 after() ...。我首先必须确切了解它的作用。

【问题讨论】:

    标签: python-3.x tkinter


    【解决方案1】:

    所以基本问题是 - 什么是强制 要更新的窗口(例如 Set 方法);为什么是 update_idletasks() 空白进度窗口。

    强制窗口更新的正确方法是通过mainloop 让它自然发生。在极少数情况下,调用update_idletasks 来更新显示是合理的。也可以致电update,但这会产生一些严重后果1

    无法回避的事实是,要使 GUI 具有响应性,它需要能够不断地处理事件。如果您有一个长期运行的流程可以防止这种情况发生,您可以采用几种不同的策略。

    一种解决方案是将长期运行的问题分解成小块,让mainloop 一次运行一个。例如,如果我要编写一个函数来查找一百万行文档中每个出现的单词“the”,我不想一次完成所有搜索。相反,我会一次进行一次搜索(或者可能在 100 毫秒内尽可能多地进行),突出显示它们,然后安排在几毫秒内进行另一次搜索。在这些调用之间,mainloop 能够正常处理事件。

    对于某些类别的问题,这就是全部 - 将问题分解为大约需要 200 毫秒或更短时间的步骤,并一次运行一个步骤。互联网上和这个网站上有几个这样的例子,通常与动画有关(例如在屏幕上移动图像)。

    另一种选择是将所有长时间运行的代码移动到单独的线程或单独的进程中。这需要更多开销并增加复杂性,但如果您无法重构代码以分块工作,那么它是最佳解决方案。

    使用线程或进程的主要困难是这些线程或进程不能安全地直接与小部件交互。 Tkinter 不是线程安全的,因此您必须设置一种机制,GUI 线程可以通过该机制与工作线程或进程进行通信。通常这是通过线程安全队列完成的,其中工作线程将请求放入队列,GUI 线程轮询该队列并代表工作人员工作。


    1 调用update 不仅仅是刷新显示。它将处理任何未决事件,包括按键和按钮点击等事件。如果其中一个按键或按钮单击导致调用代码,该代码也调用update,那么您现在实际上已经运行了两个主循环。如果任何事件都无法启动对update 的另一个调用,那么调用update 是绝对安全的,但很难保证。

    【讨论】:

    • 我有一组完全不同的细节,但你在这里的最后一段直接指出了我自己程序中的问题。底线是我的应用程序有一个工作线程来处理 TCP/IP 套接字交换,在这个线程中我还试图操纵一些 Tkinter 小部件。它大部分时间都在工作,但只要工作线程调用update_idletasks(),整个应用程序就会冻结。根据您在此处的回答,我意识到我需要修改基本方法并将与小部件相关的活动保留在主线程中。
    • 感谢您提供如此详尽的回答。
    【解决方案2】:

    首先,您不会在任何地方调用 mainloop()。以下代码显示一个移动的进度条,直到您点击中止按钮。上面代码中的 for() 循环没有任何作用,因为它除了停止程序执行 0.3*20 秒之外什么都不做。如果您想自己更新进度条,请参阅第二个示例以及它如何使用“after”调用更新函数,直到进度条完成。请注意,与它相关的所有内容都包含在类中,这是您使用类的原因之一。您也可以从类外部调用更新函数,但更新函数仍将位于创建进度条的同一类中。

    import Tkinter as Tk
    import tkFont as TkF
    import ttk
    import time
    
    class StatusBar():
    
        def __init__(self, root):
            self.master=Tk.Toplevel(root)
            Tk.Button(root, text="Quit all", bg="orange", command=root.quit).grid()
            self.customFont2 = TkF.Font(family="Calibri", size=12, weight='bold')
            self.customFont5 = TkF.Font(family="Cambria", size=16, weight='bold')
            self.master.config(background='ivory2')
            self.ctr=0
            self.create_widgets()
    
        def create_widgets(self):
    
            self.msg = Tk.Label(self.master, text='None', bg='ivory2', fg='blue4',
                               font=self.customFont2, width=5)
            self.msg.grid(row=0, column=0, sticky=Tk.W)
    
            self.bar = ttk.Progressbar(self.master, length=300, mode='indeterminate')
            self.bar.grid(row=1, column=0, sticky=Tk.W)
    
            self.btn_abort = Tk.Button(self.master, text=' Abort ', command=self.abort, font=self.customFont2, fg='maroon')
            self.btn_abort.grid(row=2,column=0, sticky=Tk.W)
            self.master.rowconfigure(2, pad=3)
    
            print('widgets done')
    
        def Start(self, msg):
            self.msg.configure(text=msg)
            self.bar.start()
        def Stop(self, msg):
            self.msg.configure(text=msg)
            self.bar.stop()
    
        def abort(self):
            # eventually will raise an error to the calling routine to stop the process
            self.master.destroy()
    
    if (__name__ == '__main__'):
        print('start')
        tkroot = Tk.Tk()
        tkroot.title("Status Bar")
        tkroot.geometry("500x75")
        tkroot.configure(bg='ivory2')
        Bar = StatusBar(tkroot)
        Bar.Start('Demo')
        tkroot.mainloop()
    

    使用 after() 更新进度条

    try:
        import Tkinter as tk     ## Python 2.x
    except ImportError:
        import tkinter as tk     ## Python 3.x
    
    import ttk
    
    class TestProgress():
        def __init__(self):
            self.root = tk.Tk()
            self.root.title('ttk.Progressbar')
    
            self.increment = 0
            self.pbar = ttk.Progressbar(self.root, length=300)
            self.pbar.pack(padx=5, pady=5)
    
            self.root.after(100, self.advance) 
            self.root.mainloop()
    
        def advance(self):
            # can be a float
            self.pbar.step(5)
            self.increment += 5
            if self.increment < 100:
                self.root.after(500, self.advance) 
            else:
                self.root.quit()
    
    
    TP=TestProgress()
    

    【讨论】:

    • for 循环模拟调用模块处理(例如遍历目录树;状态栏会在遇到的每个文件夹上更新......在那种特殊情况下,我还没有决定在不确定的进度条之间,或首先扫描目录树以计算文件或文件夹的数量并使用确定的进度条 - 虚拟代码具有两种方法的元素,我可能没有为帖子编辑好 - 抱歉!)。研究你的代码.....
    • 我将尝试做的另一件事是让模块在从根本不使用 tkinter 的模块调用时以及从从 tkinter 窗口按钮调用的过程调用时工作,
    • 我忘了感谢您查看的固定/示例代码。我现在玩它。在第一个版本中,当它从另一个模块调用时,name == 'main' 当然是 False。我想我可以在这两种情况下解决大部分问题(调用者是否使用 tkinter)。我的问题是.... mainloop() 去哪里了?我们希望显示状态栏,但执行会回到调用模块。如果我把它放在 Start() 方法中,在我点击 [Abort] 之前执行不会返回给调用者
    • - 抱歉,我忘记了 prev cmets 中的 @。 'ping' 你。我仍在为此挣扎
    • 一旦 mainloop 被调用,循环之外什么都不会发生,所以所有代码都必须进入循环内部,或者使用多处理。您当然可以使用按钮来调用函数来读取/更新目录树,但是您必须让 tkinter 知道您何时准备好进行更新,使用 after() 定期调用某些内容,或者在您准备好时按下按钮.
    猜你喜欢
    • 1970-01-01
    • 2022-11-30
    • 1970-01-01
    • 2016-12-25
    • 1970-01-01
    • 2021-08-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多