0

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:

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

  1. 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.

  2. 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?

  • everthing before line with `mainloop` is executed before window is opened. `mainloop` starts window and it runs till you close window. You have to use button or `after` to execute function when window is open - and then it can change elements in window. – furas Apr 08 '19 at 23:49
  • if you want to change screen like in mobil app using buttons then create buttons and assign functions to buttons `Button(..., command=function_name)`. Using `threads` can make some problems because only main thread should change elements in GUI. Using `after` can be better method if you don't use buttons and you have to change screen with some delay. – furas Apr 08 '19 at 23:55
  • instead of changing elements in canvas you can create two canvas with different elements and `pack()`/`unpack()` canvas. – furas Apr 08 '19 at 23:56
  • long time ago on Stackoverflow was example how to use `Frame` to create few pages and replace them in main window. – furas Apr 08 '19 at 23:59
  • [Switch between two frames in tkinter](https://stackoverflow.com/questions/7546050/switch-between-two-frames-in-tkinter) – figbeam Apr 09 '19 at 00:33
  • 1
    Personally I don't recommend that example for beginners. Too many people copy it and use it without understanding it, which causes even more confusion. :-\ – Bryan Oakley Apr 09 '19 at 00:39

1 Answers1

2

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)
Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
  • Great answer, thank you. Now I have better understanding of Tkinter, but also can achieve what I initially wanted. Both methods tested and working. I'll use classes in order to write better code. To others, don't forget to add mainloop after every change you want to show! – davaradijator Apr 09 '19 at 23:15
  • @davaradijator: I'm not sure what you mean by that last sentence. You must only call `mainloop()` exactly once in your program. It's a rule that can be broken, but only when you understand why you should never break that rule. – Bryan Oakley Apr 09 '19 at 23:33
  • for me, the code didn't work before I have put root.main() at the end of every if..enif or at the end of __init__ function of every class. I have had mainloop() at the end of the code. What I am doing wrong? – davaradijator Apr 10 '19 at 21:51
  • @davaradijator: if you're calling mainloop more than once, _that's_ what you're doing wrong. There may be other things you're doing wrong, but without seeing your code it's impossible to say. – Bryan Oakley Apr 10 '19 at 22:35