How to solve Tkinter Button Binding Causing errors on Mac? - python

I'm running a python (3.10.2) Tkinter GUI application on Mac OS X (12.1). The code is simply just binding the event to a Combobox to create a new page. This all works fine, but sometimes when you click on the back button, it gives me the error code "Process finished with exit code 139 (interrupted by signal 11: SIGSEGV)". I've tried multiple different combinations with no success. I've tried using entry boxes from Tkinter rather than comboboxes from ttk. This still led to the same error. I then tired using a submit button instead of a combobox widget. I never experienced the error once, but in another one of my projects I wanted to bind the enter key and have a submit button which still gave me the same error. This allowed me to determine it had something to do with event binding. Next, I changed my python version (I changed from Python 3.10.0 to Python 3.9, then I upgraded to Python 3.10.2, both of these had no impact whatsoever). After this, I played around and realized that the button command itself wasn't even executing. The button was being clicked, and this killed the code for some reason. I'm really confused about the entire situation. What makes even less sense is that sometimes it occurs and sometimes it doesn't. Some runs allow it to be successful, while others don't. The other thing that I saw with my other project was that if you ran a different command that deleted the page and created a new one, the key bind would work. All of this confused me even more than I already was.
# Importing tkinter and ttk
from tkinter import *
from tkinter import ttk
# Creating the original page
def HomePage():
# Creating new tkinter window with the size of the screen as the height and width
# I made its name HomePage.root in order to make it accessible to other functions without the hassle of global and local
# I also made height and width like this in order to access them in the next function
HomePage.root = Tk()
height = HomePage.root.winfo_screenheight()
width = HomePage.root.winfo_screenwidth()
HomePage.root.geometry("%dx%d" % (width, height))
# Creating combobox and binding it to NewPage function
combo = ttk.Combobox(HomePage.root)
combo.pack()
combo.bind("<Return>",NewPage)
# Running Mainloop
HomePage.root.mainloop()
# NewPage function to go into new page
# Back command
def Back(control, func):
print("yay I entered the command")
control.destroy()
func()
def NewPage(e):
# Destroying old page and creating new page
HomePage.root.destroy()
NewPage.window = Tk()
height = NewPage.window.winfo_screenheight()
width = NewPage.window.winfo_screenwidth()
NewPage.window.geometry("%dx%d" % (width, height))
# Creating button
back = Button(NewPage.window, text="back", command=lambda: Back(NewPage.window, HomePage))
back.pack(side=BOTTOM)
# Running Mainloop
NewPage.window.mainloop()
# Running HomePage function
HomePage()

I worked with my code changing and modifying it until I had an idea. Rather than setting the button command, why not bind the button with a mouse click? I tried this, and it worked!
Here is my code:
# Importing tkinter and ttk
from tkinter import *
from tkinter import ttk
# Creating the original page
def HomePage():
# Creating new tkinter window with the size of the screen as the height and width
# I made its name HomePage.root in order to make it accessible to other functions without the hassle of global and local
# I also made height and width like this in order to access them in the next function
HomePage.root = Tk()
height = HomePage.root.winfo_screenheight()
width = HomePage.root.winfo_screenwidth()
HomePage.root.geometry("%dx%d" % (width, height))
# Creating combobox and binding it to NewPage function
combo = ttk.Combobox(HomePage.root)
combo.pack()
combo.bind("<Return>",NewPage)
# Running Mainloop
HomePage.root.mainloop()
# Back command
def Back(e,control, func):
print("yay I entered the command")
control.destroy()
func()
# NewPage function to go into new page
def NewPage(e):
# Destroying old page and creating new page
HomePage.root.destroy()
NewPage.window = Tk()
height = NewPage.window.winfo_screenheight()
width = NewPage.window.winfo_screenwidth()
NewPage.window.geometry("%dx%d" % (width, height))
# Creating button
back = Button(NewPage.window, text="back")
back.pack(side=BOTTOM)
back.bind("<Button 1>", lambda event, control=NewPage.window, func=HomePage: Back(event, control, func))
# Running Mainloop
NewPage.window.mainloop()
# Running HomePage function
HomePage()

Related

tkinter: Unbind not working, even workarounds don't work

I bring up here a problem, that's been there for ages, but is obviously still not solved and older workarounds don't work on my Python 3.7.2 (64-bit on Win10).
I have this code:
import tkinter as tk
import tkinter.simpledialog
# message box to enter a value where to set the scale to
class EnterValueBox(tk.simpledialog.Dialog):
def body(self, master):
self.e = tk.Entry(self, width=10)
self.e.pack(pady=5)
return self.e # initial focus
def apply(self):
print(self.e.get())
# callback to open message box
def enterValue(event):
EnterValueBox(root, title="Enter Value 0..100")
# create window with scale widget
root = tk.Tk()
scale = tk.Scale(root, orient=tk.HORIZONTAL, from_=0, to=100)
scale.pack()
# unbind any button-3 events
scale.unbind("<ButtonPress-3>")
scale.unbind("<ButtonRelease-3>")
scale.unbind("<Button-3>")
# bind button-3 press event to open message box
scale.bind("<ButtonPress-3>", enterValue)
tk.mainloop()
It creates a window with a single scale widget. I want to bind ButtonPress-3 to open a little dialog to directly enter a new value. The code only prints that value to the shell, but the example shows, that the unbind is not working, because after printing the value, the dialog box is closed (when the user clicks OK) and then the default binding is executed, which sets the slider, where the user clicked in the trough of the slider widget.
I tried the workaround from Deleting and changing a tkinter event binding with a PatchedScale widget (instead of the PatchedCanvas shown there), but that didn't make any difference.
Any help would be greatly appreciated!
The default bindings are not on the widget, they are on the widget class. Calling unbind on a widget for which there is no widget-specific binding won't have any effect.
If you don't want the default binding to run after your widget-specific binding, the normal technique is to have your bound function return the string break.
def enterValue(event):
EnterValueBox(root, title="Enter Value 0..100")
return "break"

Tkinter window not displaying until after program is run

global window
window = Tk()
window.geometry('300x200')
window.minsize(300,200)
window.maxsize(300,200)
window.configure(bg='black')
window.title("testupdate")
global outputtext
outputtext = tk.StringVar()
outputtext.set("starting window...")
my_label = tk.Label(window, textvariable = outputtext, bg = 'black', fg = 'white', font = 'terminal')
my_label.pack()
class ChangeLabel():
def __init__(self, text):
outputtext = tk.StringVar()
outputtext.set(text)
my_label.config(textvariable = outputtext)
Here's the main code: https://replit.com/#YourPetFinch/textadventure#main.py
I've been trying to make a text adventure game with Tkinter so I can package it all nicely as an app. I created a function that I can call from another file to update a label as the game output so I can keep the GUI setup simple, but I've been having a problem where the window won't show up until the code has finished running. I'm new to tkinter (and honestly not very good at Python in general) so this is probably a stupid question.
That's not how global is used. The global statement is only used inside a function, to state that you're going to modify a global. You can't actually make a global that crosses files, but your from gui import * will handle that.
The issue here is understanding event-driven programming. When you create a window, nothing gets drawn. All that does is send a number of messages to signal the core. The messages will not get fetched and dispatched until it gets into the .mainloop(). The main loop processes all the messages and does the drawing, which might queue up more messages.
So, you cannot use time.sleep(2) like that. As you saw, that will interrupt the process and prevent any drawing from being done. Instead, you will have to use window.after to request a callback after some number of seconds. The .mainloop monitors the timers, and will give you a call when time runs out. You cannot use time.sleep inside an event handler either, for the same reason; your GUI will freeze until your event handler returns.

Delete all from TkInter Canvas and put new items while in mainloop

The goal is to achieve different "screens" in TkInter and change between them. The easiest to imagine this is to think of a mobile app, where one clicks on the icon, for example "Add new", and new screen opens. The application has total 7 screens and it should be able to change screens according to user actions.
Setup is on Raspberry Pi with LCD+touchscreen attached. I am using tkinter in Python3. Canvas is used to show elements on the screen.
Since I am coming from embedded hardware world and have very little experience in Python, and generally high-level languages, I approached this with switch-case logic. In Python this is if-elif-elif...
I have tried various things:
Making global canvas object. Having a variable programState which determines which screen is currently shown. This obviously didn't work because it would just run once and get stuck at the mainloop below.
from tkinter import *
import time
root = Tk()
programState = 0
canvas = Canvas(width=320, height=480, bg='black')
canvas.pack(expand=YES, fill=BOTH)
if(programState == 0):
backgroundImage = PhotoImage(file="image.gif")
canvas.create_image(0,0, image=backgroundImage, anchor=NW);
time.sleep(2)
canvas.delete(ALL) #delete all objects from canvas
programState = 1
elif(programState == 1):
....
....
....
root.mainloop()
Using root.after function but this failed and wouldn't show anything on the screen, it would only create canvas. I probably didn't use it at the right place.
Trying making another thread for changing screens, just to test threading option. It gets stuck at first image and never moves to second one.
from tkinter import *
from threading import Thread
from time import sleep
def threadFun():
while True:
backgroundImage = PhotoImage(file="image1.gif")
backgroundImage2 = PhotoImage(file="image2.gif")
canvas.create_image(0,0,image=backgroundImage, anchor=NW)
sleep(2)
canvas.delete(ALL)
canvas.create_image(0,0,image=backgroundImage2, anchor=NW)
root = Tk()
canvas = Canvas(width=320, height=480, bg='black')
canvas.pack(expand=YES, fill=BOTH)
# daemon=True kills the thread when you close the GUI, otherwise it would continue to run and raise an error.
Thread(target=threadFun, daemon=True).start()
root.mainloop()
I expect this app could change screens using a special thread which would call a function which redraws elements on the canvas, but this has been failing so far. As much as I understand now, threads might be the best option. They are closest to my way of thinking with infinite loop (while True) and closest to my logic.
What are options here? How deleting whole screen and redrawing it (what I call making a new "screen") can be achieved?
Tkinter, like most GUI toolkits, is event driven. You simply need to create a function that deletes the old screen and creates the new, and then does this in response to an event (button click, timer, whatever).
Using your first canvas example
In your first example you want to automatically switch pages after two seconds. That can be done by using after to schedule a function to run after the timeout. Then it's just a matter of moving your redraw logic into a function.
For example:
def set_programState(new_state):
global programState
programState = new_state
refresh()
def refresh():
canvas.delete("all")
if(programState == 0):
backgroundImage = PhotoImage(file="image.gif")
canvas.create_image(0,0, image=backgroundImage, anchor=NW);
canvas.after(2000, set_programState, 1)
elif(programState == 1):
...
Using python objects
Arguably a better solution is to make each page be a class based off of a widget. Doing so makes it easy to add or remove everything at once by adding or removing that one widget (because destroying a widget also destroys all of its children)
Then it's just a matter of deleting the old object and instantiating the new. You can create a mapping of state number to class name if you like the state-driven concept, and use that mapping to determine which class to instantiate.
For example:
class ThisPage(tk.Frame):
def __init__(self):
<code to create everything for this page>
class ThatPage(tk.Frame):
def __init__(self):
<code to create everything for this page>
page_map = {0: ThisPage, 1: ThatPage}
current_page = None
...
def refresh():
global current_page
if current_page:
current_page.destroy()
new_page_class = page_map[programstate]
current_page = new_page_class()
current_page.pack(fill="both", expand=True)
The above code is somewhat ham-fisted, but hopefully it illustrates the basic technique.
Just like with the first example, you can call update() from any sort of event: a button click, a timer, or any other sort of event supported by tkinter. For example, to bind the escape key to always take you to the initial state you could do something like this:
def reset_state(event):
global programState
programState = 0
refresh()
root.bind("<Escape>", reset_state)

Tkinter window not filling screen in zoomed state on secondary display

I'm building a small python 3 tkinter program on windows (10). From a main window (root) I'm creating a secondary window (wdowViewer). I want it to be full screen (zoomed) on my secondary display. The code below works on my main setup with two identical screens. If I however take my laptop out of the dock and connect it to (any) external display, the new window only fills about 2/3 of the secondary display.
Two things to note:
- The laptop and external monitor have same resolution.
- The window is appropriately zoomed when overrideredirect is set to 0.
mon_primary_width = str(app.root.winfo_screenwidth()) # width of primary screen
self.wdowViewer = Toplevel(app.root) # create new window
self.wdowViewer.geometry('10x10+' + mon_primary_width + '+0') # move it to the secondary screen
self.wdowViewer.wm_state('zoomed') # full screen
self.wdowViewer.overrideredirect(1) # remove tool bar
app.root.update_idletasks() # Apply changes
After two days of experimenting I finally found a fix.
Consider the following example: Making a toplevel zoomed, works fine on any secondary display when using the following code:
from tkinter import *
# Make tkinter window
root = Tk()
sw = str(root.winfo_screenwidth())
Label(root, text="Hello Main Display").pack()
# Make a new toplevel
w = Toplevel(root)
w.geometry("0x0+" + sw + "+0")
w.state('zoomed')
w.overrideredirect(1)
Label(w, text='Hello Secondary Display').pack()
root.mainloop()
However, in my code I'm making a new Toplevel after running the mainloop command. Then, the issue arises. A code example with the issue:
from tkinter import *
# New Tkinter
root = Tk()
sw = str(root.winfo_screenwidth())
# Function for new toplevel
def new_wdow():
w = Toplevel(root)
w.geometry("0x0+" + sw + "+0")
w.state('zoomed')
w.overrideredirect(1)
Label(w, text='Hello Secondary Display').pack()
# Make button in main window
Button(root, text="Hello", command=new_wdow).pack()
root.mainloop()
Problem: The bug is only present if the DPI scaling in Windows is not set to 100%. Hence, there seems to be a bug in how tkinter handles the DPI scaling subsequent to running mainloop. It does not scale the window properly, but Windows is treating it as if it does.
Fix: Tell Windows that the app takes care of DPI scaling all by itself, and to not resize the window. This is achievable using ctypes. Put the following code early in your python script:
import ctypes
ctypes.windll.shcore.SetProcessDpiAwareness(2)
Hopefully others will find the solution helpful. Hopefully, someone may explain more of what is going on here!

tk destroy() and grid_forget() don't always work

i'm hoping anyone can help me out here. i'm having an issue with a tkinter gui i built. the issue only happens in windows. My GUI creates a results frame with some labels in it, when it's time to calculate something else, the user clicks on the "newPort" button and that button is supposed to remove the results frame and set to False some instance attributes internal to the calculation. The issue i'm having, which is apparent only in windows is that sometimes the results frame, and its descendant labels don't disappear every time. Sometimes they do, sometimes they don't. The instance variable is correctly set to False but the widgets are still visible on the main GUI. The GUI also contains a couple checkboxes and radiobuttons but they don't impact the creation of the results frame nor its expected destruction. I have not been able to pin point a pattern of actions the user takes before clicking on the newPort button which causes the frame and labels to not get destroyed. This happens when i freeze my app with py2exe, as well as running the app from the python interpreter within the eclipse IDE. I have not tried running the app from the python interpreter directly (i.e. without the IDE) and this problem does not happen on my Mac when i run the app using the eclipse python interpreter. Thanks very much all! My code looks like this:
import Tkinter as TK
class widget(object):
def __init__(self,parent=None):
self.parent = TK.Frame(parent)
self.parent.grid()
self.frame = TK.Frame(self.parent)
self.frame.grid()
newLedger = TK.Button(self.parent,command=self.newPort).grid()
self.calcButton = TK.Button(self.frame,command=self.showResults)
self.calcButton.grid()
self.calcVariable = True
def newPort(self):
self.calcVariable = False
try:
self.second.grid_forget()
self.first.grid_forget()
self.resultsFrame.grid_forget()
self.second.destroy()
self.first.destroy()
self.resultsFrame.destroy()
except:
raise
self.frame.update_idletasks()
def showResults(self):
self.resultsFrame = TK.Frame(self.frame)
self.resultsFrame.grid()
self.first = TK.Label(self.resultsFrame,text='first')
self.first.grid()
self.second = TK.Label(self.resultsFrame,text='second')
self.second.grid()
if __name__ == '__main__':
root = TK.Tk()
obj = widget(root)
root.mainloop()
You don't need to destroy or call grid_forget on the labels, and you don't need to call grid_forget on the resultsFrame; when you destroy the resultsFrame it will cause all off its children to be destroyed, and when these widgets are destroyed they will no longer be managed by grid.
The only way I can get widgets to not be destroyed is if I click on the "calc" button twice in a row without clicking on the "new" button in-between. I'm doing this by running your program from the command line.

Categories