Related
eWithin a tkinter application I am catching several events to gracefully shutdown some threads within the application, before the main thread terminates.
This is all working swiftly as long as I use a bound key combination or the
window control, the cross in the red circle.
On macos the application automatically gets a 'python' menu with a close function bound to key combination ⌘Q. This event is not handled properly. It seems to kill the main thread but other threads are not closed properly.
Following bindings are used to catch all closing events:
self.root.bind('<Control-x>', self.exitapp)
self.root.protocol("WM_DELETE_WINDOW", self.exitapp)
atexit.register(self.catch_atexit)
Recently found that the left and right ⌘ keys are repesented as Meta_L and Meta_R but cannot be combined with a second key, i.e. '<Meta_L-q>'.
Can anyone explain howto catch ⌘Q?
Please find code example below:
#!/usr/bin/env python3
import sys
from tkinter import *
from tkinter import ttk
import threading
import time
import atexit
class subthread():
def __init__(self):
self.thr = None
self.command = ''
self.proof = ""
def start(self):
if not self.thr or not self.thr.is_alive():
self.command = 'run'
self.thr = threading.Thread(target=self.loop)
self.thr.start()
else:
print('thread already running')
def stop(self):
self.command = 'stop'
if self.thr and self.thr.is_alive():
print('stopping thread')
else:
print('thread not running')
def running(self):
return True if self.thr and self.thr.is_alive() else False
def get_proof(self):
return self.proof
def loop(self):
while self.command == 'run':
time.sleep(0.5)
print('+', end='')
self.proof += '+'
if len(self.proof) > 30:
self.proof = ""
def __del__(self):
print('del instance subthread')
self.command = 'stop'
if self.thr and self.thr.is_alive():
self.thr.join(2)
class app():
def __init__(self, rootframe):
self.root = rootframe
self.gui = ttk.Frame(self.root)
self.gui.pack(fill=BOTH)
row = 0
self.checkvar = IntVar()
self.checkvar.trace('w', self.threadchange)
ttk.Label(self.gui, text="Use checkbox to start and stop thread").grid(row=row, column=0, columnspan=2)
ttk.Checkbutton(self.gui, text='thread', variable=self.checkvar).grid(row=1, column=0)
self.threadstatus = StringVar()
self.threadstatus.set('not running')
row += 1
ttk.Label(self.gui, textvariable=self.threadstatus).grid(row=row, column=1)
row += 1
self.alivestring = StringVar()
ttk.Entry(self.gui, textvariable=self.alivestring).grid(row=row, column=0, padx=10, sticky="ew",
columnspan=3)
row += 1
ttk.Separator(self.gui, orient="horizontal").grid(row=row, column=0, padx=10, sticky="ew",
columnspan=3)
row += 1
ttk.Label(self.gui, text="- Available options to close application: [ctrl]-x,"
" window-control-red, [CMD]-q").grid(row=row, column=0, padx=10, columnspan=3)
row += 1
ttk.Label(self.gui, text="1. Try all three without thread running").grid(row=row, column=0,
columnspan=3, sticky='w')
row += 1
ttk.Label(self.gui, text="2. Retry all three after first starting the thread").grid(row=row, column=0,
columnspan=3, sticky='w')
row += 1
ttk.Label(self.gui, text="3. Experience that only [CMD]-q fails").grid(row=row, column=0,
columnspan=3, sticky='w')
self.subt = subthread()
self.root.bind('<Control-x>', self.exitapp1)
self.root.protocol("WM_DELETE_WINDOW", self.exitapp2)
atexit.register(self.catch_atexit)
self.root.after(500, self.updategui)
def threadchange(self, a, b, c):
""" checkbox change handler """
try:
if self.checkvar.get() == 1:
self.subt.start()
else:
self.subt.stop()
except Exception as ex:
print('failed to control subt', str(ex))
def updategui(self):
""" retriggering timer handler to update status label gui """
try:
if self.subt.running():
self.threadstatus.set("thread is running")
else:
self.threadstatus.set("thread not running")
self.alivestring.set(self.subt.get_proof())
except:
pass
else:
self.root.after(500, self.updategui)
def __del__(self):
print('app del called')
def exitapp1(self, a):
print('exitapp1 called')
self.subt.stop()
sys.exit(0)
def exitapp2(self):
print('exitapp2 called')
self.subt.stop()
sys.exit(0)
def catch_atexit(self):
print('exitapp called')
self.subt.stop()
self.subt = None
sys.exit(0)
if __name__ == '__main__':
root = Tk()
dut = app(rootframe=root)
root.mainloop()
print('main exiting')
sys.exit(0)
You can catch ⌘Q with <Command-q>:
...
def action(event):
print("bind!")
root.bind_all("<Command-q>", action)
...
This worked for me on macOS HightSierra
#EddyHoogevorst Yes, it does not work on Big Sur.
The code that works is:
root.createcommand("::tk::mac::Quit", action).
NB: need to change function signature for action as event is not passed as an argument in this context.
Without clicking the search button everything works fine and returns a value.
However, using the search button will cause the code to pause.
The main issue is the Check() nested in chooseBoard() which calls in on itself causing a constant 'hidden loop'.
This means that the code doesn't carry on from:
board = tempPreset.chooseBoard(None)
and unable to return wanted value.
In short, my question is how do I fix the nested loop issue of not carrying on after the check() is completed when the search button is pressed. As the value is correct when I print it from there.
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
class PresetUpdate:
def __init__(self,master,area,board):
self.root = master
self.root.title('Preset')
self.root.geometry(area)
self.board = board
def boardId(self,name):
self.name = name
self.var.set(name)
self.root.destroy()
def chooseBoard(self, newBoard):
def check():
#set variables
self.newBoard = {}
r = re.compile(self.e.get(), re.IGNORECASE)
#regex to see words in list
for k in self.board:
if r.search(k) != None:
self.newBoard.update({r.search(k).string : self.board[r.search(k).string]})
if self.newBoard == {}:
self.newBoard = self.board
self.root.destroy()
#creating new root
master = tk.Tk()
self.__init__(master,'460x475',self.board)
#re-calling function with update list
self.chooseBoard(self.newBoard)
return self.board[self.name]
#print(self.board[self.name])
#set variables
self.newBoard = self.board if newBoard is None else newBoard
self.var = tk.StringVar()
self.button = {}
#setting search bar / button
self.e = tk.Entry(self.root,width=30)
self.e.grid(row=0,column=2,padx=10,pady=10)
click = tk.Button(self.root, text='Search', height=1,width=5,command=check)
click.grid(row=0,column=1,padx=10,pady=10)
#creating buttons of dct
for i,boardName in enumerate(self.newBoard):
#print(i,boardName,self.board[boardName])
self.button[i] = tk.Button(self.root, text=boardName,height=2,width=30,command=lambda name=boardName:self.boardId(name))
self.button[i].grid(row=i+1,column=1,padx=10,pady=10)
#pause until a button has been pressed
self.button[i].wait_variable(self.var)
return self.board[self.name]
r = {'test1' : '0','test2' : '1','test3' : '2','test4' : '3','test5' : '4'}
def main():
if __name__ == '__main__':
master = tk.Tk()
tempPreset = PresetUpdate(master,'460x475',r)
board = tempPreset.chooseBoard(None)
print(board)
master.mainloop()
main()
I found the solution:
def chooseBoard(self, board):
def check():
#set variables
self.newBoard = {}
r = re.compile(self.e.get(), re.IGNORECASE)
#regex to see words in list
for k in self.board:
if r.search(k) != None:
self.newBoard.update({r.search(k).string : self.board[r.search(k).string]})
if self.newBoard == {}:
self.newBoard = self.board
for i,item in enumerate(self.board):
if item not in self.newBoard:
self.button[i].destroy()
if self.newBoard == self.board:
for i,item in enumerate(self.board):
self.button[i].destroy()
createButtons()
#set variables
self.newBoard = self.board if self.newBoard is None else self.newBoard
self.var = tk.StringVar()
self.button = {}
#setting search bar / button
self.e = tk.Entry(self.root,width=30)
self.e.grid(row=0,column=2,padx=10,pady=10)
click = tk.Button(self.root, text='Search', height=1,width=5,command=check)
click.grid(row=0,column=1,padx=10,pady=10)
#creating buttons of dct
def createButtons():
for i,boardName in enumerate(self.newBoard):
#print(i,boardName,self.board[boardName])
self.button[i] = tk.Button(self.root, text=boardName,height=2,width=30,command=lambda name=boardName:self.boardId(name))
self.button[i].grid(row=i+1,column=1,padx=10,pady=10)
return i
i = createButtons()
#check()
#pause until a button has been pressed
self.button[i].wait_variable(self.var)
return self.board[self.name]
I am making this PDF tool, and I want the buttons to be disabled until a file or files are successfully imported. This is what the app looks like at the launch:
Right after running the callback for the import files button, the active state looks like this:
I want the colors of the buttons to turn maroon instead of the original grey. They only turn back to maroon once you hover the mouse over them. Any thoughts for how to fix this? Here is the callback for the import button:
def import_callback():
no_files_selected = False
global files
files = []
try:
ocr_button['state'] = DISABLED
merge_button['state'] = DISABLED
status_label.pack_forget()
frame.pack_forget()
files = filedialog.askopenfilenames()
for f in files:
name, extension = os.path.splitext(f)
if extension != '.pdf':
raise
if not files:
no_files_selected = True
raise
if frame.winfo_children():
for label in frame.winfo_children():
label.destroy()
make_import_file_labels(files)
frame.pack()
ocr_button['state'] = ACTIVE
merge_button['state'] = ACTIVE
except:
if no_files_selected:
status_label.config(text='No files selected.', fg='blue')
else:
status_label.config(text='Error: One or more files is not a PDF.', fg='red')
status_label.pack(expand='yes')
import_button = Button(root, text='Import Files', width=scaled(20), bg='#5D1725', bd=0, fg='white', relief='groove',
command=import_callback)
import_button.pack(pady=scaled(50))
I know this was asked quite a while ago, so probably already solved for the user. But since I had the exact same problem and do not see the "simplest" answer here, I thought I would post:
Just change the state from "active" to "normal"
ocr_button['state'] = NORMAL
merge_button['state'] = NORMAL
I hope this helps future users!
As I understand you right you want something like:
...
ocr_button['state'] = DISABLED
ocr_button['background'] = '#*disabled background*'
ocr_button.bind('<Enter>', lambda e:ocr_button.configure(background='#...'))
ocr_button.bind('<Leave>', lambda e:ocr_button.configure(background='#...'))
merge_button['state'] = DISABLED
merge_button['background'] = '#*disabled background*'
merge_button.bind('<Enter>', lambda e:ocr_button.configure(background='#...'))
merge_button.bind('<Leave>', lambda e:ocr_button.configure(background='#...'))
...
...
ocr_button['state'] = ACTIVE
ocr_button['background'] = '#*active background*'
ocr_button.unbind('<Enter>')
ocr_button.unbind('<Leave>')
merge_button['state'] = ACTIVE
merge_button['background'] = '#*active background*'
merge_button.unbind('<Enter>')
merge_button.unbind('<Leave>')
...
If there are any errors, since I wrote it out of my mind or something isnt clear, let me know.
Update
the following code reproduces the behavior as you stated. The reason why this happens is how tkinter designed the standart behavior. You will have a better understanding of it if you consider style of ttk widgets. So I would recommand to dont use the automatically design by state rather write a few lines of code to configure your buttons how you like, add and delete the commands and change the background how you like. If you dont want to write this few lines you would be forced to use ttk.Button and map a behavior you do like
import tkinter as tk
root = tk.Tk()
def func_b1():
print('func of b1 is running')
def disable_b1():
b1.configure(bg='grey', command='')
def activate_b1():
b1.configure(bg='red', command=func_b1)
b1 = tk.Button(root,text='B1', bg='red',command=func_b1)
b2 = tk.Button(root,text='disable', command=disable_b1)
b3 = tk.Button(root,text='activate',command=activate_b1)
b1.pack()
b2.pack()
b3.pack()
root.mainloop()
I've wrote this simple app that I think could help all to reproduce the problem.
Notice that the state of the button when you click is Active.
#!/usr/bin/python3
import sys
import tkinter as tk
from tkinter import ttk
from tkinter import messagebox
class Main(ttk.Frame):
def __init__(self, parent, *args, **kwargs):
super().__init__()
self.parent = parent
self.init_ui()
def cols_configure(self, w):
w.columnconfigure(0, weight=0, minsize=100)
w.columnconfigure(1, weight=0)
w.rowconfigure(0, weight=0, minsize=50)
w.rowconfigure(1, weight=0,)
def get_init_ui(self, container):
w = ttk.Frame(container, padding=5)
self.cols_configure(w)
w.grid(row=0, column=0, sticky=tk.N+tk.W+tk.S+tk.E)
return w
def init_ui(self):
w = self.get_init_ui(self.parent)
r = 0
c = 0
b = ttk.LabelFrame(self.parent, text="", relief=tk.GROOVE, padding=5)
self.btn_import = tk.Button(b,
text="Import Files",
underline=1,
command = self.on_import,
bg='#5D1725',
bd=0,
fg='white')
self.btn_import.grid(row=r, column=c, sticky=tk.N+tk.W+tk.E,padx=5, pady=5)
self.parent.bind("<Alt-i>", self.switch)
r +=1
self.btn_ocr = tk.Button(b,
text="OCR FIles",
underline=0,
command = self.on_ocr,
bg='#5D1725',
bd=0,
fg='white')
self.btn_ocr["state"] = tk.DISABLED
self.btn_ocr.grid(row=r, column=c, sticky=tk.N+tk.W+tk.E,padx=5, pady=5)
r +=1
self.btn_merge = tk.Button(b,
text="Merge Files",
underline=0,
command = self.on_merge,
bg='#5D1725',
bd=0,
fg='white')
self.btn_merge["state"] = tk.DISABLED
self.btn_merge.grid(row=r, column=c, sticky=tk.N+tk.W+tk.E,padx=5, pady=5)
r +=1
self.btn_reset = tk.Button(b,
text="Reset",
underline=0,
command = self.switch,
bg='#5D1725',
bd=0,
fg='white')
self.btn_reset.grid(row=r, column=c, sticky=tk.N+tk.W+tk.E,padx=5, pady=5)
b.grid(row=0, column=1, sticky=tk.N+tk.W+tk.S+tk.E)
def on_import(self, evt=None):
self.switch()
#simulate some import
self.after(5000, self.switch())
def switch(self,):
state = self.btn_import["state"]
if state == tk.ACTIVE:
self.btn_import["state"] = tk.DISABLED
self.btn_ocr["state"] = tk.NORMAL
self.btn_merge["state"] = tk.NORMAL
else:
self.btn_import["state"] = tk.NORMAL
self.btn_ocr["state"] = tk.DISABLED
self.btn_merge["state"] = tk.DISABLED
def on_ocr(self, evt=None):
state = self.btn_ocr["state"]
print ("ocr button state is {0}".format(state))
def on_merge(self, evt=None):
state = self.btn_merge["state"]
print ("merge button state is {0}".format(state))
def on_close(self, evt=None):
self.parent.on_exit()
class App(tk.Tk):
"""Main Application start here"""
def __init__(self, *args, **kwargs):
super().__init__()
self.protocol("WM_DELETE_WINDOW", self.on_exit)
self.set_style()
self.set_title(kwargs['title'])
Main(self, *args, **kwargs)
def set_style(self):
self.style = ttk.Style()
#('winnative', 'clam', 'alt', 'default', 'classic', 'vista', 'xpnative')
self.style.theme_use("clam")
def set_title(self, title):
s = "{0}".format('Simple App')
self.title(s)
def on_exit(self):
"""Close all"""
if messagebox.askokcancel(self.title(), "Do you want to quit?", parent=self):
self.destroy()
def main():
args = []
for i in sys.argv:
args.append(i)
kwargs = {"style":"clam", "title":"Simple App",}
app = App(*args, **kwargs)
app.mainloop()
if __name__ == '__main__':
main()
I have a code that correctly validates email, but this happens only once. So if the email is invalid, it wont let me validate it again. Is there a way how to validate it over and over?
import tkinter as tk
from tkinter import *
from tkinter import ttk
import re
class Test(tk.Frame):
def __init__(self, root):
self.root = root
self.root.geometry("{}x{}+250+150".format(500, 500))
self.entry()
def testAlphaValue(self, value):
if value.isalpha():
return True
else:
return False
def testEmail(self, email):
regex = '^[a-z0-9]+[\._]?[a-z0-9]+[#]\w+[.]\w{2,3}$'
wdg = self.wdgLst
if(re.search(regex, email)):
return wdg.configure(text='Email is valid')
else:
return wdg.configure(text='Email is invalid')
def entry(self):
self.formFrame = LabelFrame(self.root, bg='grey', bd=1)
self.formFrame.place(x=50, y=50, width=400, height=400)
regEmail = self.root.register(self.testEmail)
regAlpha = self.root.register(self.testAlphaValue)
nameEnt = Entry(self.formFrame)
nameEnt.config(validate="key", validatecommand=(regAlpha, '%S'))
nameEnt.grid(row=0, column=1)
emailEnt = Entry(self.formFrame)
emailEnt.config(validate="focusout", validatecommand=(regEmail, '%P'))
emailEnt.grid(row=0, column=0)
emailLbl = Label(self.formFrame, text='Email', font=("Helvetica", 8))
emailLbl.grid(row=1, column=0)
self.wdgLst = emailLbl
root=tk.Tk()
test = Test(root)
root.mainloop()
Thank you!
Here is a working code for ya which validates email on focus-out event (as you wanted) - (must read the reason with explanation below the code)
import tkinter as tk
from tkinter import *
from tkinter import ttk
import re
class Test(tk.Frame):
def __init__(self, root):
self.root = root
self.root.geometry("{}x{}+250+150".format(500, 500))
self.entry()
def testAlphaValue(self, value):
if value.isalpha():
return True
else:
return False
def checkEmail(self, val):
if re.search(regex, val):
self.wdgLst.configure(text='Email is valid')
return True
else:
self.wdgLst.configure(text='Email is Invalid')
return False
def entry(self):
self.formFrame = LabelFrame(self.root, bg='grey', bd=1)
self.formFrame.place(x=50, y=50, width=400, height=400)
regEmail = self.root.register(self.checkEmail)
regAlpha = self.root.register(self.testAlphaValue)
nameEnt = Entry(self.formFrame)
nameEnt.config(validate="key", validatecommand=(regAlpha, '%S'))
nameEnt.grid(row=0, column=1)
emailEnt = Entry(self.formFrame)
emailEnt.config(validate="focusout", validatecommand=(regEmail, '%P'))
emailEnt.grid(row=0, column=0)
emailLbl = Label(self.formFrame, text='Email', font=("Helvetica", 8))
emailLbl.grid(row=1, column=0)
self.wdgLst = emailLbl
root=tk.Tk()
regex = '^[a-z0-9]+[\._]?[a-z0-9]+[#]\w+[.]\w{2,3}$'
test = Test(root)
root.mainloop()
***REASON FOR THE ISSUE ***
From what I got to know about the issue, there seems to be a rule which is::
The function registered to the validate command must return either True or False.
In any other case, if the function returns something else,
it DISABLES the validation for the respected widget.
In your code you were making it return wdg.configure(<something here>) which was disabling the validation right after the first run.
You can also use the method suggested by #ShaneLoyd above if you wish to change your approach but if you just want to stick to validate commands, go with the above code.
I can't explain why your code doesn't work except that perhaps the Entry widget must only allow the validate code to run once. This might work for your needs.
import tkinter as tk
from tkinter import *
from tkinter import ttk
import re
class Test(tk.Frame):
def __init__(self, root):
self.root = root
self.root.geometry("{}x{}+250+150".format(500, 500))
self.entry()
def testAlphaValue(self, value):
if value.isalpha():
return True
else:
return False
def testEmail(self, sv):
email = sv.get()
regex = '^[a-z0-9]+[\._]?[a-z0-9]+[#]\w+[.]\w{2,3}$'
wdg = self.wdgLst
if (re.search(regex, email)):
return wdg.configure(text='Email is valid')
else:
return wdg.configure(text='Email is invalid')
def entry(self):
self.formFrame = LabelFrame(self.root, bg='grey', bd=1)
self.formFrame.place(x=50, y=50, width=400, height=400)
sv_email = StringVar()
sv_email.trace("w", lambda name, index, mode,
sv=sv_email: self.testEmail(sv_email))
regAlpha = self.root.register(self.testAlphaValue)
nameEnt = Entry(self.formFrame)
nameEnt.config(validate="key", validatecommand=(regAlpha, '%S'))
nameEnt.grid(row=0, column=1)
emailEnt = Entry(self.formFrame)
emailEnt.config(textvariable=sv_email)
emailEnt.grid(row=0, column=0)
emailLbl = Label(self.formFrame, text='Email', font=("Helvetica", 8))
emailLbl.grid(row=1, column=0)
self.wdgLst = emailLbl
root = tk.Tk()
test = Test(root)
root.mainloop()
Right now, after I press the 'Time Range' button and call the calculateTime function, the text widget would appear with the results that I've inserted into it. However, after that, whenever I close the GUI window, the program would freeze and I'll have to forced quit it. This is my code:
import tkinter
from tkinter import *
import math
from tkinter import messagebox
class MyClass(tkinter.Frame):
def __init__(self, *args, **kwargs):
tkinter.Frame.__init__(self, *args, **kwargs)
#Setting up frame and widgets
vcmd1 = (self.register(self.__vcmd1), '%P', '%S')
vcmd2 = (self.register(self.__vcmd2), '%P')
vcmd3 = (self.register(self.__vcmd3), '%P', '%S')
label_iso = Label(self,text="Isotope A, Element")
label_vol = Label(self, text="Voltage")
label_range = Label(self, text="Charge Range")
label_iso.grid(row=0, column=0, sticky=E)
label_vol.grid(row=1, column=0, sticky=E)
label_range.grid(row=2, column=0, sticky=E)
self.entry1 = tkinter.Entry(self, validate="key", validatecommand=vcmd1)
self.entry2 = tkinter.Entry(self, validate="key", validatecommand=vcmd2)
self.entry3 = tkinter.Entry(self, validate="key", validatecommand=vcmd3)
self.entry1.grid(row=0, column=1)
self.entry2.grid(row=1, column=1)
self.entry3.grid(row=2, column=1)
def __vcmd1(self, P, S):
validString = 'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM,1234567890'
if not S in validString:
return False
if "," in P:
if (len(P) - 1) > len(P.replace(",","")):
return False
messagebox.showinfo("Error", "Expected Form: ex. 133,Cs")
else:
return True
def __vcmd2(self, P):
if P == '':
return True
try:
float(P)
return True
except ValueError:
messagebox.showinfo("Error", "Entry is not a float or integer")
return False
def __vcmd3(self, P, S):
if "," in P:
if len(P.split(",")) > 2:
return False
a = P.split(",")[0]
b = P.split(",")[1]
if a != '' and b != '':
try:
int(a)
int(b)
except ValueError:
messagebox.showinfo("Error", "Expected form: ex. 1,12")
return False
else:
return True
class TimeGenerator:
def __init__(self,master):
frame = MyClass(master)
frame.grid(columnspan=2)
button = Button(root, text='Time Range', command=self.calculateTime)
button.grid(row=3, columnspan=2)
self.text = Text(root)
self.iso = frame.entry1
self.vol = frame.entry2
self.r = frame.entry3
def calculateTime(self):
x = 5
if self.r.get() == "" or self.iso.get() == "" or self.vol.get() == "":
messagebox.showinfo("Error", "No field can be empty")
return None
self.iso = self.iso.get().replace(" ", "")
list = []
for e in self.iso.split(","):
list.append(e)
f = open("/Users/LazyLinh/PycharmProjects/mass.mas12.txt", "r")
i = 0
while (i < 40):
header = f.readline()
i += 1
self.mass = 0
#iterate through text file
for line in f:
line = line.strip()
columns = line.split()
if (list[0] == columns[3]):
if (list[1].lower() == columns[4].lower()):
if (len(columns) == 16):
self.mass = float(columns[13].replace("#","")) + float(columns[14].replace("#",""))
else:
self.mass = float(columns[12].replace("#","")) + float(columns[13].replace("#",""))
#Calculation
self.r = self.r.get().replace(" ", "")
tup = tuple(int(x) for x in self.r.split(","))
list = []
for q in range(tup[0], tup[1] + 1):
y = (x * math.sqrt(self.mass * 1.6605402e-27 / (2 * q * float(self.vol.get())))) * 10e6
list.append(y)
i = tup[0]
#inserting to text widget
for time in list:
self.text.insert("end", "%d: %s\n" % (i, time))
i = i + 1
self.text.pack()
root = Tk()
b = TimeGenerator(root)
root.mainloop()
I've tried to searched up on this topic, but I'm not really using any weird update() function, and text shows up after the function is finished, so how likely that it is an event loop problem? Am I also doing something wrong that could cause this problem?
Thank you!
You have widgets in the root window that use both pack and grid. You cannot do this. Within a given container (root window, frame, etc) you can use one or the other, but not both.
The reason your program freezes is due to pack and grid fighting to gain control of the layout. When you pack the text widget it causes a change in the size and/or position of other widgets in the root window. That triggers grid to try to re-layout the widgets it is responsible for. This triggers pack to try to re-layout the widgets it is responsible for, and on and on until the end of time.
My guess is that you need to use grid with self.text since you use grid everywhere else.