Python tkinter update_idletasks消隐了窗口

时间:2015-10-03 02:41:29

标签: python-3.x tkinter

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

我正在尝试在应用程序运行时显示进度条(例如,走一个音乐库文件夹树,但这不重要)。

我想在主应用程序的单独模块中将其实现为一个类,以便我可以在其他地方使用它(也就是应用程序本身实际上是在2个模块中)。

出于这个原因,也是因为我不想让应用程序的主要窗口设计ID像进度条一样出现在单独的窗口中。

我已经尝试过两种方式......我自己粗略地使用文本小部件绘制进度条,并且 - 一旦我发现它 - 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()

ProgressBar模块

# 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()后的第二个....我首先要完全理解它的作用。

2 个答案:

答案 0 :(得分:2)

  

所以基本问题是 - 强迫这个问题的正确方法是什么   窗口更新(例如Set方法);为什么是update_idletasks()   消除进度窗口。

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

没有逃避的事实是,要使GUI响应,它需要能够不断处理事件。如果你有一个长期运行的过程阻止了这种情况,你可以采用几种不同的策略。

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

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

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

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

1 调用update不仅仅是刷新显示。它将处理任何待处理事件,包括按键和按钮点击等事件。如果其中一次按键或按键单击导致调用代码也调用update,则实际上现在有两个主循环运行。如果任何事件都无法启动对update的另一次调用,则调用update是绝对安全的,但可能很难做到这一点。

答案 1 :(得分:0)

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

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