在 Tkinter 中执行函数期间程序 Toplevel 冻结
Program Toplevel freezing during the execution of a function in Tkinter
我有一个结构如下的 tkinter 应用程序:
import tkinter as tk
import tkinter.ttk as ttk
class TopLevelWindow(tk.Toplevel):
def __init__(self, root, *args, **kargs):
super().__init__(root, *args, **kargs)
self.root = root
self.button= ttk.Button(self, text="Exportar excel", command=self.go)
self.button.pack()
self.prog_bar = ttk.Progressbar(self, orient = "horizontal", mode= "determinate")
self.prog_bar.pack()
def go(self):
global dic_Data
# Create word file
value_bar = 100/len(dic_Data)
for k, v in dic_Data.items():
# Calculate some stuff
# Create Pil images and add them to the word file
# Create Matplotlib images and add them to the word file
self.prog_bar["value"] += value_bar
self.root.update_idletasks()
#Save the word file
self.quit()
self.destroy()
class App():
def __init__(self, root, *params):
self.root = root
# Code
# dic_Data gets populated
def open_window(self):
popup = TopLevelWindow(self.root)
popup.mainloop()
dic_Data = {}
root = tk.Tk()
app = App(root)
root.mainloop()
我遇到的问题是函数 go
最终冻结了我的 Toplevel window。
奇怪的是它开始为 for 循环的第一项做 okey,
然后它冻结但功能保持 运行 正常(栏上没有视觉更新),
直到 for 循环结束,然后 windows 按预期关闭,所有内容再次解冻。
为什么会这样,是否可以解决?
已编辑:
一个答案建议使用 root.after 函数来避免冻结,但我在实现它时遇到了问题。这就是我所拥有的:
import tkinter as tk
import tkinter.ttk as ttk
class TopLevelWindow(tk.Toplevel):
def __init__(self, root, *args, **kargs):
super().__init__(root, *args, **kargs)
self.root = root
self.button = ttk.Button(self, text="Exportar excel", command=self.go)
self.button.pack()
self.prog_bar = ttk.Progressbar(self, orient = "horizontal", mode= "determinate")
self.prog_bar.pack()
def heavy_function(self, k, v):
# Calculate some stuff
# Create Pil images and add the to the word file
# Create Matplotlib images and add the to the word file
# Add changes to word file
self.prog_bar["value"] += value_bar
def save_word(self):
#Save the word file
self.quit()
self.destroy()
def go(self):
global dic_Data
# Create word file
value_bar = 100/len(dic_Data)
for k, v in dic_Data.items():
self.root.after(10, lambda: self.heavy_function(k, v))
self.root.after(10, self.save_word)
class App():
def __init__(self, root, *params):
self.root = root
# Code
# dic_Data gets populated
def open_window(self):
popup = TopLevelWindow(self.root)
popup.mainloop()
dic_Data = {}
root = tk.Tk()
app = App(root)
root.mainloop()
您已将按钮命名为与函数 (go) 同名的名称。相反,您必须将其命名为其他名称并将其与进度条一起打包:
import time
import tkinter as tk
import tkinter.ttk as ttk
class TopLevelWindow(tk.Toplevel):
def __init__(self, root, *args, **kargs):
super().__init__(root, *args, **kargs)
self.root = root
self.button = ttk.Button(self, text="Exportar excel", command=self.go)
self.button.pack() # packing button
self.prog_bar = ttk.Progressbar(self, orient="horizontal", mode="determinate")
self.prog_bar.pack() # packing progress bar
def go(self):
global dic_Data
# Create word file
value_bar = 100 / len(dic_Data)
for k, v in dic_Data.items():
time.sleep(1) # to visualize the progressbar
# Calculate some stuff
# Create Pil images and add them to the word file
# Create Matplotlib images and add them to the word file
self.prog_bar["value"] += value_bar
self.root.update_idletasks()
# Save the word file
self.quit()
self.destroy()
class App:
def __init__(self, root, *params):
self.root = root
# Code
# dic_Data gets populated
def open_window(self):
popup = TopLevelWindow(self.root)
popup.mainloop()
# some data to avoid divide by zero
dic_Data = {'a': 'Something', 'B': 'Something else', 'C': 'Nothing', 'D': 'Nothing else'}
root = tk.Tk()
app = App(root)
app.open_window() # calling the open window function
root.mainloop()
tl;dr 发生的事情是 self.go
只是花费了太长时间并停止了事件的处理。
像 self.go
这样的回调会在 在 中执行 mainloop
每当您激活控件时。
只要回调是运行,事件处理就会中断。
因此,如果所述回调花费的时间超过 50 毫秒,你会注意到它。
在使用 event-driven 工具包(如 tkinter)的程序中,基本上有三种方法可以完成 long-running 工作。
将较长的 运行 作业分成小块,使用 after
方法执行。这使得更新进度条变得容易。下面显示了我自己的代码示例。
使用 multiprocessing
在单独的进程中执行 long-running 作业。这绝对不会 阻止 GUI。但是您将需要使用通信原语来告诉 GUI 操作已完成,并且 GUI 必须使用 after
方法定期检查这些操作。
在单独的线程中执行 long-running 作业。另一个复杂因素是在 CPython 中一次只能有一个线程可以执行 Python 字节码。因此,不能保证不会阻止 GUI。但总的来说,Python 3 会经常尝试切换线程。
您将需要一个在启用线程的情况下构建的 tkinter
。
示例如下所示。
示例 (1):使用小步骤解锁 excel 个文件
"""Remove passwords from modern excel 2007+ files (xlsx, xlsm)."""
from types import SimpleNamespace
import os
import re
import shutil
import stat
import sys
import zipfile
from tkinter import filedialog
from tkinter import ttk
from tkinter.font import nametofont
import tkinter as tk
__version__ = "2022.01.28"
widgets = SimpleNamespace()
state = SimpleNamespace()
def create_widgets(root, w):
"""Create the window and its widgets.
Arguments:
root: the root window.
w: SimpleNamespace to store widgets.
"""
# Set the font.
default_font = nametofont("TkDefaultFont")
default_font.configure(size=12)
root.option_add("*Font", default_font)
# General commands and bindings
root.bind_all('q', do_exit)
root.wm_title('Unlock excel files v' + __version__)
root.columnconfigure(3, weight=1)
root.rowconfigure(5, weight=1)
# First row
ttk.Label(root, text='(1)').grid(row=0, column=0, sticky='ew')
w.fb = ttk.Button(root, text="Select file", command=do_file)
w.fb.grid(row=0, column=1, columnspan=2, sticky="w")
w.fn = ttk.Label(root)
w.fn.grid(row=0, column=3, columnspan=2, sticky="ew")
# Second row
ttk.Label(root, text='(2)').grid(row=1, column=0, sticky='ew')
w.backup = tk.IntVar()
w.backup.set(0)
ttk.Checkbutton(root, text='backup', variable=w.backup,
command=on_backup).grid(row=1, column=1, sticky='ew')
w.suffixlabel = ttk.Label(root, text='suffix:', state=tk.DISABLED)
w.suffixlabel.grid(row=1, column=2, sticky='ew')
w.suffix = tk.StringVar()
w.suffix.set('-orig')
se = ttk.Entry(root, justify='left', textvariable=w.suffix, state=tk.DISABLED)
se.grid(row=1, column=3, columnspan=1, sticky='w')
w.suffixentry = se
# Third row
ttk.Label(root, text='(3)').grid(row=2, column=0, sticky='ew')
w.gobtn = ttk.Button(root, text="Go!", command=do_start, state=tk.DISABLED)
w.gobtn.grid(row=2, column=1, sticky='ew')
# Fourth row
ttk.Label(root, text='(4)').grid(row=3, column=0, sticky='ew')
ttk.Label(root, text='Progress:').grid(row=3, column=1, sticky='w')
# Fifth row
sb = tk.Scrollbar(root, orient="vertical")
w.status = tk.Listbox(root, width=60, yscrollcommand=sb.set)
w.status.grid(row=4, rowspan=5, column=1, columnspan=3, sticky="nsew")
sb.grid(row=4, rowspan=5, column=5, sticky="ns")
sb.config(command=w.status.yview)
# Ninth row
ttk.Button(root, text="Quit", command=do_exit).grid(row=9, column=1, sticky='ew')
def initialize_state(s):
"""
Initialize the global state.
Arguments:
s: SimpleNamespace to store application state.
"""
s.interval = 10
s.path = ''
s.inzf, s.outzf = None, None
s.infos = None
s.currinfo = None
s.worksheets_unlocked = 0
s.workbook_unlocked = False
s.directory = None
s.remove = None
def statusmsg(text):
"""Append a message to the status listbox, and make sure it is visible."""
widgets.status.insert(tk.END, text)
widgets.status.see(tk.END)
# Step functions to call in the after() method.
def step_open_zipfiles():
path = widgets.fn['text']
state.path = path
statusmsg(f'Opening “{path}”...')
first, last = path.rsplit('.', maxsplit=1)
if widgets.backup.get():
backupname = first + widgets.suffix.get() + '.' + last
else:
backupname = first + '-orig' + '.' + last
state.remove = backupname
shutil.move(path, backupname)
state.inzf = zipfile.ZipFile(backupname, mode="r")
state.outzf = zipfile.ZipFile(
path, mode="w", compression=zipfile.ZIP_DEFLATED, compresslevel=1
)
root.after(state.interval, step_discover_internal_files)
def step_discover_internal_files():
statusmsg(f'Reading “{state.path}”...')
state.infos = [name for name in state.inzf.infolist()]
state.currinfo = 0
statusmsg(f'“{state.path}” contains {len(state.infos)} internal files.')
root.after(state.interval, step_filter_internal_file)
def step_filter_internal_file():
current = state.infos[state.currinfo]
stat = f'Processing “{current.filename}” ({state.currinfo+1}/{len(state.infos)})...'
statusmsg(stat)
# Doing the actual work
regex = None
data = state.inzf.read(current)
if b'sheetProtect' in data:
regex = r'<sheetProtect.*?/>'
statusmsg(f'Worksheet "{current.filename}" is protected.')
elif b'workbookProtect' in data:
regex = r'<workbookProtect.*?/>'
statusmsg('The workbook is protected')
else:
state.outzf.writestr(current, data)
if regex:
text = data.decode('utf-8')
newtext = re.sub(regex, '', text)
if len(newtext) != len(text):
state.outzf.writestr(current, newtext)
state.worksheets_unlocked += 1
statusmsg(f'Removed password from "{current.filename}".')
# Next iteration or next step.
state.currinfo += 1
if state.currinfo >= len(state.infos):
statusmsg('All internal files processed.')
state.currinfo = None
root.after(state.interval, step_close_zipfiles)
else:
root.after(state.interval, step_filter_internal_file)
def step_close_zipfiles():
statusmsg(f'Writing “{state.path}”...')
state.inzf.close()
state.outzf.close()
state.inzf, state.outzf = None, None
root.after(state.interval, step_finished)
def step_finished():
if state.remove:
os.chmod(state.remove, stat.S_IWRITE)
os.remove(state.remove)
state.remove = None
else:
statusmsg('Removing temporary file')
statusmsg(f'Unlocked {state.worksheets_unlocked} worksheets.')
statusmsg('Finished!')
widgets.gobtn['state'] = 'disabled'
widgets.fn['text'] = ''
state.path = ''
# Widget callbacks
def do_file():
"""Callback to open a file"""
if not state.directory:
state.directory = ''
available = [os.environ[k] for k in ('HOME', 'HOMEDRIVE') if k in os.environ]
if available:
state.directory = available[0]
fn = filedialog.askopenfilename(
title='Excel file to open',
parent=root,
defaultextension='.xlsx',
filetypes=(
('excel files', '*.xls*'), ('all files', '*.*')
),
)
if not fn:
return
state.directory = os.path.dirname(fn)
state.worksheets_unlocked = 0
state.workbook_unlocked = False
state.path = fn
widgets.fn['text'] = fn
widgets.gobtn['state'] = 'enabled'
widgets.status.delete(0, tk.END)
def on_backup():
if widgets.backup.get() == 1:
widgets.suffixlabel['state'] = 'enabled'
widgets.suffixentry['state'] = 'enabled'
else:
widgets.suffixlabel['state'] = 'disabled'
widgets.suffixentry['state'] = 'disabled'
def do_start():
root.after(state.interval, step_open_zipfiles)
def do_exit(arg=None):
"""
Callback to handle quitting.
"""
root.destroy()
if __name__ == '__main__':
# Detach from the command line on UNIX systems.
if os.name == 'posix':
if os.fork():
sys.exit() # Create the GUI window.
root = tk.Tk(None)
# Use a dialog window so that it floats even when using a tiling window
# manager.
root.attributes('-type', 'dialog')
# Don't show hidden files in the file dialog
#
try:
# call a dummy dialog with an impossible option to initialize the file
# dialog without really getting a dialog window; this will throw a
# TclError, so we need a try...except :
try:
root.tk.call('tk_getOpenFile', '-foobarbaz')
except tk.TclError:
pass
# now set the magic variables accordingly
root.tk.call('set', '::tk::dialog::file::showHiddenBtn', '1')
root.tk.call('set', '::tk::dialog::file::showHiddenVar', '0')
except Exception:
pass
create_widgets(root, widgets)
initialize_state(state)
root.mainloop()
(3) 使用线程解锁 excel 个文件的示例
"""Remove passwords from modern excel 2007+ files (xlsx, xlsm).
This is a multithreaded version of unlock-excel.pyw. All the work that was
there done in steps in the mainloop is now done in a single additional thread.
There is some confusion whether tkinter is thread-safe. That is, if one can
call tkinter functions and methods from any but the main thread. The
documentation for Python 3 says “yes”. Comments in the C source code for
tkinter say “its complicated” depending on how tcl is built. *Many* online
sources say “no”, but that could just be an echo chamber effect.
The author has tested this code on FreeBSD 12.1-STABLE amd64 using CPython
3.7.7 combined with a tcl built with threading enabled. There at least it
seems to work without problems.
"""
from types import SimpleNamespace
import os
import re
import shutil
import stat
import sys
import threading
import zipfile
from tkinter import filedialog
from tkinter import ttk
from tkinter.font import nametofont
import tkinter as tk
__version__ = "2022.01.28"
widgets = SimpleNamespace()
state = SimpleNamespace()
def create_widgets(root, w):
"""Create the window and its widgets.
Arguments:
root: the root window.
w: SimpleNamespace to store widgets.
"""
# Set the font.
default_font = nametofont("TkDefaultFont")
default_font.configure(size=12)
root.option_add("*Font", default_font)
# General commands and bindings
root.bind_all('q', do_exit)
root.wm_title('Unlock excel files v' + __version__)
root.columnconfigure(3, weight=1)
root.rowconfigure(5, weight=1)
# First row
ttk.Label(root, text='(1)').grid(row=0, column=0, sticky='ew')
w.fb = ttk.Button(root, text="Select file", command=do_file)
w.fb.grid(row=0, column=1, columnspan=2, sticky="w")
w.fn = ttk.Label(root)
w.fn.grid(row=0, column=3, columnspan=2, sticky="ew")
# Second row
ttk.Label(root, text='(2)').grid(row=1, column=0, sticky='ew')
w.backup = tk.IntVar()
w.backup.set(0)
ttk.Checkbutton(root, text='backup', variable=w.backup,
command=on_backup).grid(row=1, column=1, sticky='ew')
w.suffixlabel = ttk.Label(root, text='suffix:', state=tk.DISABLED)
w.suffixlabel.grid(row=1, column=2, sticky='ew')
w.suffix = tk.StringVar()
w.suffix.set('-orig')
se = ttk.Entry(root, justify='left', textvariable=w.suffix, state=tk.DISABLED)
se.grid(row=1, column=3, columnspan=1, sticky='w')
w.suffixentry = se
# Third row
ttk.Label(root, text='(3)').grid(row=2, column=0, sticky='ew')
w.gobtn = ttk.Button(root, text="Go!", command=do_start, state=tk.DISABLED)
w.gobtn.grid(row=2, column=1, sticky='ew')
# Fourth row
ttk.Label(root, text='(4)').grid(row=3, column=0, sticky='ew')
ttk.Label(root, text='Progress:').grid(row=3, column=1, sticky='w')
# Fifth row
sb = tk.Scrollbar(root, orient="vertical")
w.status = tk.Listbox(root, width=60, yscrollcommand=sb.set)
w.status.grid(row=4, rowspan=5, column=1, columnspan=3, sticky="nsew")
sb.grid(row=4, rowspan=5, column=5, sticky="ns")
sb.config(command=w.status.yview)
# Ninth row
ttk.Button(root, text="Quit", command=do_exit).grid(row=9, column=1, sticky='ew')
def initialize_state(s):
"""
Initialize the global state.
Arguments:
s: SimpleNamespace to store application state.
"""
s.directory = None
def statusmsg(text):
"""Append a message to the status listbox, and make sure it is visible."""
widgets.status.insert(tk.END, text)
widgets.status.see(tk.END)
def process_zipfile_thread():
"""Function to process a zip-file. This is to be run in a thread."""
path = widgets.fn['text']
statusmsg(f'Opening “{path}”...')
first, last = path.rsplit('.', maxsplit=1)
if widgets.backup.get():
backupname = first + widgets.suffix.get() + '.' + last
remove = None
else:
backupname = first + '-orig' + '.' + last
remove = backupname
shutil.move(path, backupname)
with zipfile.ZipFile(backupname, mode="r") as inzf, \
zipfile.ZipFile(
path, mode="w", compression=zipfile.ZIP_DEFLATED, compresslevel=1
) as outzf:
statusmsg(f'Reading “{path}”...')
infos = [name for name in inzf.infolist()]
statusmsg(f'“{path}” contains {len(infos)} internal files.')
worksheets_unlocked = 0
for idx, current in enumerate(infos, start=1):
smsg = f'Processing “{current.filename}” ({idx}/{len(infos)})...'
statusmsg(smsg)
# Doing the actual work
regex = None
data = inzf.read(current)
if b'sheetProtect' in data:
regex = r'<sheetProtect.*?/>'
statusmsg(f'Worksheet "{current.filename}" is protected.')
elif b'workbookProtect' in data:
regex = r'<workbookProtect.*?/>'
statusmsg('The workbook is protected')
else:
outzf.writestr(current, data)
if regex:
text = data.decode('utf-8')
newtext = re.sub(regex, '', text)
if len(newtext) != len(text):
outzf.writestr(current, newtext)
worksheets_unlocked += 1
statusmsg(f'Removed password from "{current.filename}".')
statusmsg('All internal files processed.')
statusmsg(f'Writing “{path}”...')
if remove:
os.chmod(remove, stat.S_IWRITE)
os.remove(remove)
else:
statusmsg('Removing temporary file')
statusmsg(f'Unlocked {state.worksheets_unlocked} worksheets.')
statusmsg('Finished!')
widgets.gobtn['state'] = 'disabled'
widgets.fn['text'] = ''
# Widget callbacks
def do_file():
"""Callback to open a file"""
if not state.directory:
state.directory = ''
available = [os.environ[k] for k in ('HOME', 'HOMEDRIVE') if k in os.environ]
if available:
state.directory = available[0]
fn = filedialog.askopenfilename(
title='Excel file to open',
parent=root,
defaultextension='.xlsx',
filetypes=(('excel files', '*.xls*'), ('all files', '*.*')),
)
if not fn:
return
state.directory = os.path.dirname(fn)
state.worksheets_unlocked = 0
state.workbook_unlocked = False
widgets.fn['text'] = fn
widgets.gobtn['state'] = 'enabled'
widgets.status.delete(0, tk.END)
def on_backup():
if widgets.backup.get() == 1:
widgets.suffixlabel['state'] = 'enabled'
widgets.suffixentry['state'] = 'enabled'
else:
widgets.suffixlabel['state'] = 'disabled'
widgets.suffixentry['state'] = 'disabled'
def do_start():
worker = threading.Thread(target=process_zipfile_thread)
worker.start()
def do_exit(arg=None):
"""
Callback to handle quitting.
"""
root.destroy()
if __name__ == '__main__':
# Detach from the command line on UNIX systems.
if os.name == 'posix':
if os.fork():
sys.exit()
# Create the GUI window.
root = tk.Tk(None)
# Use a dialog window so that it floats even when using a tiling window manager.
if os.name == 'posix':
root.attributes('-type', 'dialog')
# Don't show hidden files in the file dialog
#
try:
# call a dummy dialog with an impossible option to initialize the file
# dialog without really getting a dialog window; this will throw a
# TclError, so we need a try...except :
try:
root.tk.call('tk_getOpenFile', '-foobarbaz')
except tk.TclError:
pass
# now set the magic variables accordingly
root.tk.call('set', '::tk::dialog::file::showHiddenBtn', '1')
root.tk.call('set', '::tk::dialog::file::showHiddenVar', '0')
except Exception:
pass
create_widgets(root, widgets)
initialize_state(state)
root.mainloop()
我已经成功实现了 Roland Smith 的答案选项 1。
现在它可以工作了,希望这是有道理的:
import tkinter as tk
import tkinter.ttk as ttk
class TopLevelWindow(tk.Toplevel):
def __init__(self, root, *args, **kargs):
super().__init__(root, *args, **kargs)
self.root = root
self.button = ttk.Button(self, text="Exportar excel", command=self.go)
self.button.pack()
self.prog_bar = ttk.Progressbar(self, orient = "horizontal", mode= "determinate")
self.prog_bar.pack()
def heavy_function(self):
try:
k, v = next(self.order)
except StopIteration:
self.after(10, self.save_word)
return
# Calculate some stuff
# Create Pil images and add the to the word file
# Create Matplotlib images and add the to the word file
self.prog_bar["value"] += self.value_bar
self.after(10, self.heavy_function)
def save_word(self):
#Save the word file
self.quit()
self.destroy()
def go(self):
global dic_Data
# Create word file
self.value_bar = 100/len(dic_Data)
self.order = iter(dic_Data.items())
self.after(10, self.heavy_function)
class App():
def __init__(self, root, *params):
self.root = root
# Code
# dic_Data gets populated
def open_window(self):
popup = TopLevelWindow(self.root)
popup.mainloop()
dic_Data = {}
root = tk.Tk()
app = App(root)
root.mainloop()
我有一个结构如下的 tkinter 应用程序:
import tkinter as tk
import tkinter.ttk as ttk
class TopLevelWindow(tk.Toplevel):
def __init__(self, root, *args, **kargs):
super().__init__(root, *args, **kargs)
self.root = root
self.button= ttk.Button(self, text="Exportar excel", command=self.go)
self.button.pack()
self.prog_bar = ttk.Progressbar(self, orient = "horizontal", mode= "determinate")
self.prog_bar.pack()
def go(self):
global dic_Data
# Create word file
value_bar = 100/len(dic_Data)
for k, v in dic_Data.items():
# Calculate some stuff
# Create Pil images and add them to the word file
# Create Matplotlib images and add them to the word file
self.prog_bar["value"] += value_bar
self.root.update_idletasks()
#Save the word file
self.quit()
self.destroy()
class App():
def __init__(self, root, *params):
self.root = root
# Code
# dic_Data gets populated
def open_window(self):
popup = TopLevelWindow(self.root)
popup.mainloop()
dic_Data = {}
root = tk.Tk()
app = App(root)
root.mainloop()
我遇到的问题是函数 go
最终冻结了我的 Toplevel window。
奇怪的是它开始为 for 循环的第一项做 okey, 然后它冻结但功能保持 运行 正常(栏上没有视觉更新), 直到 for 循环结束,然后 windows 按预期关闭,所有内容再次解冻。
为什么会这样,是否可以解决?
已编辑: 一个答案建议使用 root.after 函数来避免冻结,但我在实现它时遇到了问题。这就是我所拥有的:
import tkinter as tk
import tkinter.ttk as ttk
class TopLevelWindow(tk.Toplevel):
def __init__(self, root, *args, **kargs):
super().__init__(root, *args, **kargs)
self.root = root
self.button = ttk.Button(self, text="Exportar excel", command=self.go)
self.button.pack()
self.prog_bar = ttk.Progressbar(self, orient = "horizontal", mode= "determinate")
self.prog_bar.pack()
def heavy_function(self, k, v):
# Calculate some stuff
# Create Pil images and add the to the word file
# Create Matplotlib images and add the to the word file
# Add changes to word file
self.prog_bar["value"] += value_bar
def save_word(self):
#Save the word file
self.quit()
self.destroy()
def go(self):
global dic_Data
# Create word file
value_bar = 100/len(dic_Data)
for k, v in dic_Data.items():
self.root.after(10, lambda: self.heavy_function(k, v))
self.root.after(10, self.save_word)
class App():
def __init__(self, root, *params):
self.root = root
# Code
# dic_Data gets populated
def open_window(self):
popup = TopLevelWindow(self.root)
popup.mainloop()
dic_Data = {}
root = tk.Tk()
app = App(root)
root.mainloop()
您已将按钮命名为与函数 (go) 同名的名称。相反,您必须将其命名为其他名称并将其与进度条一起打包:
import time
import tkinter as tk
import tkinter.ttk as ttk
class TopLevelWindow(tk.Toplevel):
def __init__(self, root, *args, **kargs):
super().__init__(root, *args, **kargs)
self.root = root
self.button = ttk.Button(self, text="Exportar excel", command=self.go)
self.button.pack() # packing button
self.prog_bar = ttk.Progressbar(self, orient="horizontal", mode="determinate")
self.prog_bar.pack() # packing progress bar
def go(self):
global dic_Data
# Create word file
value_bar = 100 / len(dic_Data)
for k, v in dic_Data.items():
time.sleep(1) # to visualize the progressbar
# Calculate some stuff
# Create Pil images and add them to the word file
# Create Matplotlib images and add them to the word file
self.prog_bar["value"] += value_bar
self.root.update_idletasks()
# Save the word file
self.quit()
self.destroy()
class App:
def __init__(self, root, *params):
self.root = root
# Code
# dic_Data gets populated
def open_window(self):
popup = TopLevelWindow(self.root)
popup.mainloop()
# some data to avoid divide by zero
dic_Data = {'a': 'Something', 'B': 'Something else', 'C': 'Nothing', 'D': 'Nothing else'}
root = tk.Tk()
app = App(root)
app.open_window() # calling the open window function
root.mainloop()
tl;dr 发生的事情是 self.go
只是花费了太长时间并停止了事件的处理。
像 self.go
这样的回调会在 在 中执行 mainloop
每当您激活控件时。
只要回调是运行,事件处理就会中断。
因此,如果所述回调花费的时间超过 50 毫秒,你会注意到它。
在使用 event-driven 工具包(如 tkinter)的程序中,基本上有三种方法可以完成 long-running 工作。
将较长的 运行 作业分成小块,使用
after
方法执行。这使得更新进度条变得容易。下面显示了我自己的代码示例。使用
multiprocessing
在单独的进程中执行 long-running 作业。这绝对不会 阻止 GUI。但是您将需要使用通信原语来告诉 GUI 操作已完成,并且 GUI 必须使用after
方法定期检查这些操作。在单独的线程中执行 long-running 作业。另一个复杂因素是在 CPython 中一次只能有一个线程可以执行 Python 字节码。因此,不能保证不会阻止 GUI。但总的来说,Python 3 会经常尝试切换线程。 您将需要一个在启用线程的情况下构建的
tkinter
。 示例如下所示。
示例 (1):使用小步骤解锁 excel 个文件
"""Remove passwords from modern excel 2007+ files (xlsx, xlsm)."""
from types import SimpleNamespace
import os
import re
import shutil
import stat
import sys
import zipfile
from tkinter import filedialog
from tkinter import ttk
from tkinter.font import nametofont
import tkinter as tk
__version__ = "2022.01.28"
widgets = SimpleNamespace()
state = SimpleNamespace()
def create_widgets(root, w):
"""Create the window and its widgets.
Arguments:
root: the root window.
w: SimpleNamespace to store widgets.
"""
# Set the font.
default_font = nametofont("TkDefaultFont")
default_font.configure(size=12)
root.option_add("*Font", default_font)
# General commands and bindings
root.bind_all('q', do_exit)
root.wm_title('Unlock excel files v' + __version__)
root.columnconfigure(3, weight=1)
root.rowconfigure(5, weight=1)
# First row
ttk.Label(root, text='(1)').grid(row=0, column=0, sticky='ew')
w.fb = ttk.Button(root, text="Select file", command=do_file)
w.fb.grid(row=0, column=1, columnspan=2, sticky="w")
w.fn = ttk.Label(root)
w.fn.grid(row=0, column=3, columnspan=2, sticky="ew")
# Second row
ttk.Label(root, text='(2)').grid(row=1, column=0, sticky='ew')
w.backup = tk.IntVar()
w.backup.set(0)
ttk.Checkbutton(root, text='backup', variable=w.backup,
command=on_backup).grid(row=1, column=1, sticky='ew')
w.suffixlabel = ttk.Label(root, text='suffix:', state=tk.DISABLED)
w.suffixlabel.grid(row=1, column=2, sticky='ew')
w.suffix = tk.StringVar()
w.suffix.set('-orig')
se = ttk.Entry(root, justify='left', textvariable=w.suffix, state=tk.DISABLED)
se.grid(row=1, column=3, columnspan=1, sticky='w')
w.suffixentry = se
# Third row
ttk.Label(root, text='(3)').grid(row=2, column=0, sticky='ew')
w.gobtn = ttk.Button(root, text="Go!", command=do_start, state=tk.DISABLED)
w.gobtn.grid(row=2, column=1, sticky='ew')
# Fourth row
ttk.Label(root, text='(4)').grid(row=3, column=0, sticky='ew')
ttk.Label(root, text='Progress:').grid(row=3, column=1, sticky='w')
# Fifth row
sb = tk.Scrollbar(root, orient="vertical")
w.status = tk.Listbox(root, width=60, yscrollcommand=sb.set)
w.status.grid(row=4, rowspan=5, column=1, columnspan=3, sticky="nsew")
sb.grid(row=4, rowspan=5, column=5, sticky="ns")
sb.config(command=w.status.yview)
# Ninth row
ttk.Button(root, text="Quit", command=do_exit).grid(row=9, column=1, sticky='ew')
def initialize_state(s):
"""
Initialize the global state.
Arguments:
s: SimpleNamespace to store application state.
"""
s.interval = 10
s.path = ''
s.inzf, s.outzf = None, None
s.infos = None
s.currinfo = None
s.worksheets_unlocked = 0
s.workbook_unlocked = False
s.directory = None
s.remove = None
def statusmsg(text):
"""Append a message to the status listbox, and make sure it is visible."""
widgets.status.insert(tk.END, text)
widgets.status.see(tk.END)
# Step functions to call in the after() method.
def step_open_zipfiles():
path = widgets.fn['text']
state.path = path
statusmsg(f'Opening “{path}”...')
first, last = path.rsplit('.', maxsplit=1)
if widgets.backup.get():
backupname = first + widgets.suffix.get() + '.' + last
else:
backupname = first + '-orig' + '.' + last
state.remove = backupname
shutil.move(path, backupname)
state.inzf = zipfile.ZipFile(backupname, mode="r")
state.outzf = zipfile.ZipFile(
path, mode="w", compression=zipfile.ZIP_DEFLATED, compresslevel=1
)
root.after(state.interval, step_discover_internal_files)
def step_discover_internal_files():
statusmsg(f'Reading “{state.path}”...')
state.infos = [name for name in state.inzf.infolist()]
state.currinfo = 0
statusmsg(f'“{state.path}” contains {len(state.infos)} internal files.')
root.after(state.interval, step_filter_internal_file)
def step_filter_internal_file():
current = state.infos[state.currinfo]
stat = f'Processing “{current.filename}” ({state.currinfo+1}/{len(state.infos)})...'
statusmsg(stat)
# Doing the actual work
regex = None
data = state.inzf.read(current)
if b'sheetProtect' in data:
regex = r'<sheetProtect.*?/>'
statusmsg(f'Worksheet "{current.filename}" is protected.')
elif b'workbookProtect' in data:
regex = r'<workbookProtect.*?/>'
statusmsg('The workbook is protected')
else:
state.outzf.writestr(current, data)
if regex:
text = data.decode('utf-8')
newtext = re.sub(regex, '', text)
if len(newtext) != len(text):
state.outzf.writestr(current, newtext)
state.worksheets_unlocked += 1
statusmsg(f'Removed password from "{current.filename}".')
# Next iteration or next step.
state.currinfo += 1
if state.currinfo >= len(state.infos):
statusmsg('All internal files processed.')
state.currinfo = None
root.after(state.interval, step_close_zipfiles)
else:
root.after(state.interval, step_filter_internal_file)
def step_close_zipfiles():
statusmsg(f'Writing “{state.path}”...')
state.inzf.close()
state.outzf.close()
state.inzf, state.outzf = None, None
root.after(state.interval, step_finished)
def step_finished():
if state.remove:
os.chmod(state.remove, stat.S_IWRITE)
os.remove(state.remove)
state.remove = None
else:
statusmsg('Removing temporary file')
statusmsg(f'Unlocked {state.worksheets_unlocked} worksheets.')
statusmsg('Finished!')
widgets.gobtn['state'] = 'disabled'
widgets.fn['text'] = ''
state.path = ''
# Widget callbacks
def do_file():
"""Callback to open a file"""
if not state.directory:
state.directory = ''
available = [os.environ[k] for k in ('HOME', 'HOMEDRIVE') if k in os.environ]
if available:
state.directory = available[0]
fn = filedialog.askopenfilename(
title='Excel file to open',
parent=root,
defaultextension='.xlsx',
filetypes=(
('excel files', '*.xls*'), ('all files', '*.*')
),
)
if not fn:
return
state.directory = os.path.dirname(fn)
state.worksheets_unlocked = 0
state.workbook_unlocked = False
state.path = fn
widgets.fn['text'] = fn
widgets.gobtn['state'] = 'enabled'
widgets.status.delete(0, tk.END)
def on_backup():
if widgets.backup.get() == 1:
widgets.suffixlabel['state'] = 'enabled'
widgets.suffixentry['state'] = 'enabled'
else:
widgets.suffixlabel['state'] = 'disabled'
widgets.suffixentry['state'] = 'disabled'
def do_start():
root.after(state.interval, step_open_zipfiles)
def do_exit(arg=None):
"""
Callback to handle quitting.
"""
root.destroy()
if __name__ == '__main__':
# Detach from the command line on UNIX systems.
if os.name == 'posix':
if os.fork():
sys.exit() # Create the GUI window.
root = tk.Tk(None)
# Use a dialog window so that it floats even when using a tiling window
# manager.
root.attributes('-type', 'dialog')
# Don't show hidden files in the file dialog
#
try:
# call a dummy dialog with an impossible option to initialize the file
# dialog without really getting a dialog window; this will throw a
# TclError, so we need a try...except :
try:
root.tk.call('tk_getOpenFile', '-foobarbaz')
except tk.TclError:
pass
# now set the magic variables accordingly
root.tk.call('set', '::tk::dialog::file::showHiddenBtn', '1')
root.tk.call('set', '::tk::dialog::file::showHiddenVar', '0')
except Exception:
pass
create_widgets(root, widgets)
initialize_state(state)
root.mainloop()
(3) 使用线程解锁 excel 个文件的示例
"""Remove passwords from modern excel 2007+ files (xlsx, xlsm).
This is a multithreaded version of unlock-excel.pyw. All the work that was
there done in steps in the mainloop is now done in a single additional thread.
There is some confusion whether tkinter is thread-safe. That is, if one can
call tkinter functions and methods from any but the main thread. The
documentation for Python 3 says “yes”. Comments in the C source code for
tkinter say “its complicated” depending on how tcl is built. *Many* online
sources say “no”, but that could just be an echo chamber effect.
The author has tested this code on FreeBSD 12.1-STABLE amd64 using CPython
3.7.7 combined with a tcl built with threading enabled. There at least it
seems to work without problems.
"""
from types import SimpleNamespace
import os
import re
import shutil
import stat
import sys
import threading
import zipfile
from tkinter import filedialog
from tkinter import ttk
from tkinter.font import nametofont
import tkinter as tk
__version__ = "2022.01.28"
widgets = SimpleNamespace()
state = SimpleNamespace()
def create_widgets(root, w):
"""Create the window and its widgets.
Arguments:
root: the root window.
w: SimpleNamespace to store widgets.
"""
# Set the font.
default_font = nametofont("TkDefaultFont")
default_font.configure(size=12)
root.option_add("*Font", default_font)
# General commands and bindings
root.bind_all('q', do_exit)
root.wm_title('Unlock excel files v' + __version__)
root.columnconfigure(3, weight=1)
root.rowconfigure(5, weight=1)
# First row
ttk.Label(root, text='(1)').grid(row=0, column=0, sticky='ew')
w.fb = ttk.Button(root, text="Select file", command=do_file)
w.fb.grid(row=0, column=1, columnspan=2, sticky="w")
w.fn = ttk.Label(root)
w.fn.grid(row=0, column=3, columnspan=2, sticky="ew")
# Second row
ttk.Label(root, text='(2)').grid(row=1, column=0, sticky='ew')
w.backup = tk.IntVar()
w.backup.set(0)
ttk.Checkbutton(root, text='backup', variable=w.backup,
command=on_backup).grid(row=1, column=1, sticky='ew')
w.suffixlabel = ttk.Label(root, text='suffix:', state=tk.DISABLED)
w.suffixlabel.grid(row=1, column=2, sticky='ew')
w.suffix = tk.StringVar()
w.suffix.set('-orig')
se = ttk.Entry(root, justify='left', textvariable=w.suffix, state=tk.DISABLED)
se.grid(row=1, column=3, columnspan=1, sticky='w')
w.suffixentry = se
# Third row
ttk.Label(root, text='(3)').grid(row=2, column=0, sticky='ew')
w.gobtn = ttk.Button(root, text="Go!", command=do_start, state=tk.DISABLED)
w.gobtn.grid(row=2, column=1, sticky='ew')
# Fourth row
ttk.Label(root, text='(4)').grid(row=3, column=0, sticky='ew')
ttk.Label(root, text='Progress:').grid(row=3, column=1, sticky='w')
# Fifth row
sb = tk.Scrollbar(root, orient="vertical")
w.status = tk.Listbox(root, width=60, yscrollcommand=sb.set)
w.status.grid(row=4, rowspan=5, column=1, columnspan=3, sticky="nsew")
sb.grid(row=4, rowspan=5, column=5, sticky="ns")
sb.config(command=w.status.yview)
# Ninth row
ttk.Button(root, text="Quit", command=do_exit).grid(row=9, column=1, sticky='ew')
def initialize_state(s):
"""
Initialize the global state.
Arguments:
s: SimpleNamespace to store application state.
"""
s.directory = None
def statusmsg(text):
"""Append a message to the status listbox, and make sure it is visible."""
widgets.status.insert(tk.END, text)
widgets.status.see(tk.END)
def process_zipfile_thread():
"""Function to process a zip-file. This is to be run in a thread."""
path = widgets.fn['text']
statusmsg(f'Opening “{path}”...')
first, last = path.rsplit('.', maxsplit=1)
if widgets.backup.get():
backupname = first + widgets.suffix.get() + '.' + last
remove = None
else:
backupname = first + '-orig' + '.' + last
remove = backupname
shutil.move(path, backupname)
with zipfile.ZipFile(backupname, mode="r") as inzf, \
zipfile.ZipFile(
path, mode="w", compression=zipfile.ZIP_DEFLATED, compresslevel=1
) as outzf:
statusmsg(f'Reading “{path}”...')
infos = [name for name in inzf.infolist()]
statusmsg(f'“{path}” contains {len(infos)} internal files.')
worksheets_unlocked = 0
for idx, current in enumerate(infos, start=1):
smsg = f'Processing “{current.filename}” ({idx}/{len(infos)})...'
statusmsg(smsg)
# Doing the actual work
regex = None
data = inzf.read(current)
if b'sheetProtect' in data:
regex = r'<sheetProtect.*?/>'
statusmsg(f'Worksheet "{current.filename}" is protected.')
elif b'workbookProtect' in data:
regex = r'<workbookProtect.*?/>'
statusmsg('The workbook is protected')
else:
outzf.writestr(current, data)
if regex:
text = data.decode('utf-8')
newtext = re.sub(regex, '', text)
if len(newtext) != len(text):
outzf.writestr(current, newtext)
worksheets_unlocked += 1
statusmsg(f'Removed password from "{current.filename}".')
statusmsg('All internal files processed.')
statusmsg(f'Writing “{path}”...')
if remove:
os.chmod(remove, stat.S_IWRITE)
os.remove(remove)
else:
statusmsg('Removing temporary file')
statusmsg(f'Unlocked {state.worksheets_unlocked} worksheets.')
statusmsg('Finished!')
widgets.gobtn['state'] = 'disabled'
widgets.fn['text'] = ''
# Widget callbacks
def do_file():
"""Callback to open a file"""
if not state.directory:
state.directory = ''
available = [os.environ[k] for k in ('HOME', 'HOMEDRIVE') if k in os.environ]
if available:
state.directory = available[0]
fn = filedialog.askopenfilename(
title='Excel file to open',
parent=root,
defaultextension='.xlsx',
filetypes=(('excel files', '*.xls*'), ('all files', '*.*')),
)
if not fn:
return
state.directory = os.path.dirname(fn)
state.worksheets_unlocked = 0
state.workbook_unlocked = False
widgets.fn['text'] = fn
widgets.gobtn['state'] = 'enabled'
widgets.status.delete(0, tk.END)
def on_backup():
if widgets.backup.get() == 1:
widgets.suffixlabel['state'] = 'enabled'
widgets.suffixentry['state'] = 'enabled'
else:
widgets.suffixlabel['state'] = 'disabled'
widgets.suffixentry['state'] = 'disabled'
def do_start():
worker = threading.Thread(target=process_zipfile_thread)
worker.start()
def do_exit(arg=None):
"""
Callback to handle quitting.
"""
root.destroy()
if __name__ == '__main__':
# Detach from the command line on UNIX systems.
if os.name == 'posix':
if os.fork():
sys.exit()
# Create the GUI window.
root = tk.Tk(None)
# Use a dialog window so that it floats even when using a tiling window manager.
if os.name == 'posix':
root.attributes('-type', 'dialog')
# Don't show hidden files in the file dialog
#
try:
# call a dummy dialog with an impossible option to initialize the file
# dialog without really getting a dialog window; this will throw a
# TclError, so we need a try...except :
try:
root.tk.call('tk_getOpenFile', '-foobarbaz')
except tk.TclError:
pass
# now set the magic variables accordingly
root.tk.call('set', '::tk::dialog::file::showHiddenBtn', '1')
root.tk.call('set', '::tk::dialog::file::showHiddenVar', '0')
except Exception:
pass
create_widgets(root, widgets)
initialize_state(state)
root.mainloop()
我已经成功实现了 Roland Smith 的答案选项 1。 现在它可以工作了,希望这是有道理的:
import tkinter as tk
import tkinter.ttk as ttk
class TopLevelWindow(tk.Toplevel):
def __init__(self, root, *args, **kargs):
super().__init__(root, *args, **kargs)
self.root = root
self.button = ttk.Button(self, text="Exportar excel", command=self.go)
self.button.pack()
self.prog_bar = ttk.Progressbar(self, orient = "horizontal", mode= "determinate")
self.prog_bar.pack()
def heavy_function(self):
try:
k, v = next(self.order)
except StopIteration:
self.after(10, self.save_word)
return
# Calculate some stuff
# Create Pil images and add the to the word file
# Create Matplotlib images and add the to the word file
self.prog_bar["value"] += self.value_bar
self.after(10, self.heavy_function)
def save_word(self):
#Save the word file
self.quit()
self.destroy()
def go(self):
global dic_Data
# Create word file
self.value_bar = 100/len(dic_Data)
self.order = iter(dic_Data.items())
self.after(10, self.heavy_function)
class App():
def __init__(self, root, *params):
self.root = root
# Code
# dic_Data gets populated
def open_window(self):
popup = TopLevelWindow(self.root)
popup.mainloop()
dic_Data = {}
root = tk.Tk()
app = App(root)
root.mainloop()