3

I have a gui application that has several windows and buttons to go forward and backward. To do this I am using a controller and raising the frames to the top with each window change. Here is my code for the controller and a typical frame.

import Tkinter as tk   # python
from tkFileDialog import askopenfilename, asksaveasfilename
from analyzer import Options

TITLE_FONT = ("Helvetica", 18, "bold")

class TennisProgram(tk.Tk):

    def __init__(self, *args, **kwargs):
        tk.Tk.__init__(self, *args, **kwargs)

        # the container is where we'll stack a bunch of frames
        # on top of each other, then the one we want visible
        # will be raised above the others
        container = tk.Frame(self)
        #Allow the window to be resized
        # container.resizable(width=True, height=True)
        container.pack(side="top", fill="both", expand=True)
        container.grid_rowconfigure(0, weight=1)
        container.grid_columnconfigure(0, weight=1)
        #List of options bundles. One bundle for each video
        self.options_bundle_list = {}
        self.frames = {}
        #Init empty array to hold the list of files
        #Placed in the container so that all views can access it
        self.files = []

        for F in (UploadPage, AnalysisOptionsPage, FormAnalysisOptions, MatchAnalysisOptions, MatchAnalysisOptionsPage2, OutputPage, ProcessingPage):
            page_name = F.__name__
            frame = F(container, self)
            self.frames[page_name] = frame

            # put all of the pages in the same location;
            # the one on the top of the stacking order
            # will be the one that is visible.
            frame.grid(row=0, column=0, sticky="nsew")
        # Name the window
        self.title("Tennis Analyzer")
        self.show_frame("UploadPage")

    def show_frame(self, page_name):
        '''Show a frame for the given page name'''
        frame = self.frames[page_name]
        frame.tkraise()


class UploadPage(tk.Frame):
#TODO Add logic to remove a file path from the list
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.controller = controller
        self.createView()


    def createView(self):
        label = tk.Label(self, text="Upload Videos for Analysis", font=TITLE_FONT)
        label.pack(side="top", fill="x", pady=10)

        #Listbox to display the list of files to be processed
        self.path_list = tk.Listbox(self)
        #Additional options allow listbox to expand as the window is resized
        self.path_list.pack(fill=tk.BOTH ,expand=True)

        for path in self.controller.files:
            self.path_list.insert(tk.END, path)

        add_vidoes_button = tk.Button(self, text="Add Videos",
                            command=self.choose_files)
        continue_button = tk.Button(self, text="Continue",
                            command=lambda: self.controller.show_frame("AnalysisOptionsPage"))
        add_vidoes_button.pack(side=tk.TOP)
        continue_button.pack(side=tk.BOTTOM)

    def choose_files_paths(self):
        #don't want a full GUI, so keep the root window from appearing
        #self.controller.withdraw()
        #Get a file name from the user
        filename = askopenfilename()
        #Add it to the list of files to be processed
        self.controller.files.append(filename)
        self.path_list.insert(tk.END, filename)

The code that I write in init gets executed once and creates the view but I was wondering if it was possible to have a function which runs every time the frame got raised, similar to an onResume function in Android. I would want to do this in case some underlying data has changed like in this upload page. For example, what if an item is deleted from the array which populates the listview, I would want to be able to refresh it.

Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
user3282276
  • 3,674
  • 8
  • 32
  • 48
  • 1
    you can add code to `show_frame` – furas Jan 27 '16 at 04:57
  • @furas obvious I could add some way and hack together something that works, I was asking to find out if there was some convention that existed or documentation that I overlooked – user3282276 Jan 27 '16 at 23:05

2 Answers2

13

Tkinter has low level events such as <Visibility> and <Map> which should fire when the pages change. Unfortunately, these don't work reliably on all platforms.

The simplest and most robust solution is to generate your own event. You can create and bind to a custom event by specifying the event inside << and >> (eg: <<ShowFrame>>).

First, change show_frame to send an event to a window when it is shown:

def show_frame(self, page_name):
    ...
    frame.event_generate("<<ShowFrame>>")

Next, each page can bind to this event if it needs to be notified when it is made visible:

class UploadPage(tk.Frame):
    def __init__(self, parent, controller):
        ...
        self.bind("<<ShowFrame>>", self.on_show_frame)

    def on_show_frame(self, event):
        print("I am being shown...")
Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
  • Thank you, this is exactly what I was looking for – user3282276 Feb 03 '16 at 05:22
  • This looks great. What if on_show_frame returned variables which I needed to asign? – Luke.py Oct 20 '16 at 15:31
  • 1
    @Luke.py: functions called from bindings can't return data because the caller is not your code. The caller is mainloop, which ignores (almost) all data that gets returned (with the "almost" part being it treats returning the string `"break"` specially) – Bryan Oakley Oct 20 '16 at 16:54
  • Ok thanks Brian. Does this mean returning vars triggered by a frame change is only possible with an if statement in the show_frame function? - basically checking the page_name and running the required function for that frame? – Luke.py Oct 21 '16 at 07:49
  • I can put a sperate question together if you would prefer. – Luke.py Oct 21 '16 at 10:02
  • Do the names of custom events have to be specified using a particular syntax, such as. `"<>"` — i.e. surrounded by double `<<` and `>>` brackets? – martineau Apr 28 '19 at 16:28
  • 1
    @martineau: yes. – Bryan Oakley Apr 28 '19 at 16:38
0

It has passed a bit of time, but i found a solution to your problem (in case anyone needs this), that does not require some integrated function, but just the time library.

I made an easy to use function

def updatewindow(fps=60):
    time.sleep(1/fps)
    root.update()
    #root.update_idletasks() # usually you don't need this

This is an example snippet that uses this function

from tkinter import *
import time


root=Tk()

root.title("Clock")

timelbl=Label(root,font=('helvetica',40,'bold'),fg='red',bg='black')


def updatewindow(fps=60):
    time.sleep(1/fps)
    root.update()
    #root.update_idletasks() # usually you don't need this


timelbl.pack()

while True:
    timelbl["text"]=time.strftime("%H:%M:%S")
    updatewindow(fps=30)

You can run anything in the while, and it will work every frame. Not the best solution, but a solution nontheless.

AGO061
  • 11
  • 3