I have a tkinter app that is structured like that:
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()
The problem I'm having is that the function go
ends up freezing my Toplevel window.
What is weird is that is starts doing okey for the first items of the for loop, then it freezes but the functions keeps running fine (no visual updates on the bar), until the for loop finishes and after that the windows closes as its suposed to and everything unfreezes again.
Why is that happening and could it be fixed?
CodePudding user response:
You have named the button with the same name as the function (go). Instead, you have to name it something else and pack it as well as the progress bar:
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()
CodePudding user response:
tl;dr What happens is that self.go
simply takes too long and stops the processing of events.
Callbacks like self.go
are executed in the mainloop
whenever you activate a control.
As long as the callback is is running, event processing is interrupted.
So if said callback takes more than say 50 ms, you will notice it.
There are basically three ways to do long-running jobs in programs that use event-driven toolkits like tkinter.
Chop up the long running jobs in small pieces that are executed using the
after
method. This makes it easy to update a progress bar. An example from my own code is shown below.Execute the long-running job in a separate process with
multiprocessing
. This will definitely not block the GUI. But you will need to use communication primitives to tell the GUI that an action is finished, and the GUI has to check those regularly using theafter
method.Execute the long-running job in a separate thread. An additional complication is that in CPython only one thread at a time can be executing Python bytecode. So this is not guaranteed to be free from blocking the GUI. But in general, Python 3 tries to switch threads pretty often. You will need a
tkinter
that is built with threading enabled. An example is shown below.
Example of (1): unlocking excel files using small steps
"""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
# https://stackoverflow.com/questions/53220711/how-to-avoid-hidden-files-in-file-picker-using-tkinter-filedialog-askopenfilenam
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()
Example of (3) unlocking excel files using a thread
"""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
# https://stackoverflow.com/questions/53220711/how-to-avoid-hidden-files-in-file-picker-using-tkinter-filedialog-askopenfilenam
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()