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

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)

Related

How to solve Tkinter Button Binding Causing errors on Mac?

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

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.

Tkinter '<Configure>' event prevents widgets from loading properly

I am currently coding a small app in Tkinter and need to resize a Frame any time the window is resized to keep the aspect ratio of the frame.
I have used the Configure event for that. However, when using this event, the widgets of the window do not show up until I hover my mouse above them.
I, of course, don't want this behaviour to happen.
I have tried binding the configure event to the root window before and after the creation of all the widgets. Without success.
Furthermore, I have tried waiting before I call the configure event with the sleep method.
I have also tried adding a thread which executes binds the event a few seconds later. It does work but when I create new widgets, those new widgets are not properly loaded. I was wondering I there was a way around this.
The bind is done in the init function of my app class
self.root = tk.Tk()
self.root.bind("<Configure>", self.changeWindowSize)
The call back is another class function
def changeWindowSize(self, *args):
self.root.update()
# SOME CALCULATIONS
# Replace the UI
self.ui.place_forget()
self.ui.place(relx=0.25+(0.75 - ui_width)/2, rely=0.05+(0.75 - ui_height)/2, relwidth=ui_width, relheight=ui_height)
Edit:
Here is how to reproduce it:
import tkinter as tk
root = tk.Tk()
root.geometry("1100x600")
def callback(event):
root.update()
root.bind("<Configure>", callback)
for i in range(10):
btn = tk.Button(root, text="Button " + str(i) )
btn.place(relx=0.1*i, rely=0, relwidth=0.1, relheight=1)
root.mainloop()
This issue is now obvious. The root.update causes this behaviour.

Python Tkinter how to hide a widget without removing it

I know similar things have been asked a lot, but I've tried to figure this out for two hours now and I'm not getting anywhere. I want to have a button in a Tkinter window that is only visible on mouseover. So far I failed at making the button invisible in the first place (I'm familiar with events and stuff, that's not what this question is about) pack_forget() won't work, because I want the widget to stay in place. I'd like some way to do it like I indicated in the code below:
import tkinter as tki
class MyApp(object):
def __init__(self, root_win):
self.root_win = root_win
self.create_widgets()
def create_widgets(self):
self.frame1 = tki.Frame(self.root_win)
self.frame1.pack()
self.btn1 = tki.Button(self.frame1, text='I\'m a button')
self.btn1.pack()
self.btn1.visible=False #This doesnt't work
def main():
root_win = tki.Tk()
my_app = MyApp(root_win)
root_win.mainloop()
if __name__ == '__main__':
main()
Is there any way to set the visibility of widgets directly? If not, what other options are there?
Use grid as geometry manager and use:
self.btn1.grid_remove()
which will remember its place.
You can try using event to call function.
If "Enter" occurs for button then call a function that calls pack()
and if "Leave" occurs for button then call a function that calls pack_forget().
Check this link for event description:List of All Tkinter Events
If you wish your button to stay at a defined place then you can use place(x,y) instead of pack()

Importing tkinter button from separate module

We have a functioning program that uses Tkinter as its GUI. Everything works fine however different branches of the code are now using different hardware which realistically need different buttons. Hence we'd like to have the main GUI import modules representing the buttons depending on what hardware is being used.
I've cut out some of the code below, I'm interested in removing the makemenu() function to a separate module, hence when it is called in the Application __init__ (self.makemenu(master)) I would like to make that a reference to a separate module. I've tried doing this and am having trouble. Is this even possible?
I'm a little confused on the parent structure, what needs to be passed to my button module, etc.? I know this is a poorly constructed question but if anyone is able to advise if this is possible and put my on the right track that would be great. For example if someone could show how to modify this code to have the buttons defined in a separate module I could figure out how to do the same in my module.
# Import necessary libraries
import sys
import os
import Tkinter as tk
class Application(tk.Frame):
##################################################################
## Final functions are designed to initialize the GUI and
## connect various mouse movements to useful functions.
##################################################################
def definevars(self):
'''Original definition of all of the key variables that
we need to keep track of while running the GUI
'''
self.disable = True
self.savimgstatus = 'off'
self.mode = 'Standby'
self.status = 'Not Ready'
def makemenu(self,master):
''' Function to create the main menu bar across
the top of the GUI.
'''
self.menubar = tk.Menu(master)
## Motor Submenu
motormenu = tk.Menu(self.menubar,tearoff=1)
motormenu.add_command(label='ALT',state='disabled')
motormenu.add_command(label='error check',
command=lambda: self.geterror('alt'))
motormenu.add_separator()
motormenu.add_command(label='AZ',state='disabled')
motormenu.add_command(label='error check',
command=lambda: self.geterror('az'))
self.menubar.add_cascade(label='Tracker Motors',menu=motormenu)
## Set the big menu as the main menu bar.
master.config(menu=self.menubar)
def __init__(self,tcpconn,DOME,TRACKERSTAGE, master=None):
'''Main function to initialize the GUI. Will scale
the size of the GUI to fit any size screen... to a
point. It will not allow it to be smaller than
600x800.
'''
self.buf = 1024
## Check resolution of screen. Make GUI 2/3rds of size
## unless that means under 600x800.
fh = round(master.winfo_screenheight()*2./3.)
fw = round(master.winfo_screenwidth()*2./3.)
if fh < 600: fh = 600
if fw < 800: fw = 800
print 'GUI resolution set to {0} x {1}'.format(fw,fh)
self.fw = fw
self.fh = fh
self.imwidth = int(0.45*self.fw)
self.imheight = int(0.45*self.fh)
self.imcentx = self.imwidth/2
self.imcenty = self.imheight/2this
## Initialize Frame
tk.Frame.__init__(self, master, height=fh,width=fw)
self.grid()
self.grid_propagate(0)
## Initialize Various variables.
self.definevars()
## Create buttons, etc.
self.createWidgets()
self.makemenu(master)
self.disableall()
## Main Loop function
self.checkoutput()
###################################################################
# Initialize GUI window.
root = tk.Tk()
root.title('Hardware') # window title
app = Application(master=root)
app.mainloop() # go into the main program loop
sys.exit()
If you want to move makemenu to a separate module, that should be pretty simple. However, you'll need to change a few things.
Since makemenu no longer has a reference to self (or has a different reference, if you implement it as a separate class), you need to replace calls like command=lambda: self.geterror('alt')) to be command=lambda: master.geterror('alt')).
The other thing I recommend is to remove the call to add the menu to the root. I believe that modules shouldn't have side effects like this -- the function should make a menu and return it, and let the caller decide how to use it, ie:
self.menubar=makemenu(master)
master.configure(menu=self.menubar)
Roughly speaking, this is a variation of the MVC (model/view/controller) architectural pattern where the Application instance is your controller (and also part of the view unless you make modules of all your UI code). The menu is part of the view, and forwards UI functions to the controller for execution.
Your application then looks something like this:
from makemenu import makemenu
class Application(...):
def __init__(...):
...
self.menubar = makemenu(master)
master.config(menu=self.menubar)
...

Categories