Search code examples
python-3.xfunctiontkinterframeautosize

Inherit subframes in widget creation auto sizing (tkinter, python 3)


What I am trying to do is:

  1. Create a main frame (pc_gui) and two sub frames (video_frame) and (side_frame) respectively.
  2. Allow the user to be able to resize the application window to suit and have all frames, widgets, images, etc. resize accordingly.
  3. Reference frames elsewhere in the gui.

In attempt to keep things tidy, possibly in error, I have an Application class, as well as functions: init, app_size, create_frames, create_widgets, and an updater function. There is quite a bit more, but if I can get this core part to work right I should be able to keep making progress.

This begins with the relatively standard tkinter UI initialization which includes the starting size of the application window.

# Initialize Application Window
pc_gui = tk.Tk()
pc_gui.geometry("1280x720")

# Initialize Updater Variables
UPDATE_RATE = 1000

# Run application
app = Application(pc_gui)
pc_gui.mainloop()

Then I create the class, main frame (pc_gui), and I call the various sub frame, application size, and images functions.

class Application(tk.Frame):
        """ GUI """
        def __init__(self, pc_gui):
                """ Initialize the frame """
                tk.Frame.__init__(self, pc_gui)
                self.app_size()
                self.grid()
                self.create_frames()
                self.create_images()
                self.updater()

This is the app_size function. It's purpose is to parse the current size of the application window and the display monitor assuming the user will change these to suit while the program is running.

    def app_size(self):
            pc_gui.update_idletasks() # Get Current Application Values
            app_width  = pc_gui.winfo_width() # Current Application Width
            app_height = pc_gui.winfo_height() # Current Application Height
            disp_width  = pc_gui.winfo_screenwidth() # Monitor (Screen) Width
            disp_height = pc_gui.winfo_screenheight() # Monitor (Screen) Height
            return app_width, app_height, disp_width, disp_height

Then I create the sub frames using the app_size values. The colors are simply there to help me keep track of things during development.

   def create_frames(self):
            """ Create Frames """
            app_size = self.app_size() # Get size wxh of application window and display
            app_width = app_size[0]
            app_height = app_size[1]
            disp_width = app_size[2]
            disp_height = app_size[3]
            geometry = "%dx%d" % (app_width, app_height) # Create text value for geometry
            pc_gui.geometry(geometry) # Set Application Window Size

            # Section of Application window dedicated to source video
            video_frame = tk.Frame(
                    master = pc_gui,
                    width = app_width*.75,
                    height = app_height,
                    bg = "blue"
            )
            video_frame.place(
                    x = 0,
                    y = 0
            )

            # Section of Application window dedicated to calculations
            side_frame = tk.Frame(
                    master = pc_gui,
                    width = app_width*.25,
                    height = app_height,
                    bg = "red"
            )
            side_frame.place(
                    x = app_width*.75,
                    y = 0
            )

            pc_gui.update_idletasks()
            sf_x = side_frame.winfo_x()
            sf_y = side_frame.winfo_y()
            sf_w = side_frame.winfo_width()
            sf_h = side_frame.winfo_height()

            return sf_x, sf_y, sf_w, sf_h

    def updater(self):
            #self.create_frames() # Update Application Window
            self.create_images() # Update Images Widget
            self.after(UPDATE_RATE, self.updater) # Call "updater" function after 1 second

Then I start creating widgets. For the image (Label) widgets I would like to use grid, but I am having the hardest time figuring out how to reference the sub frame (side_frame) in the create_images function. I have truncated the important bits to make this question more to the point.

   def create_images(self):

            sf_dims = self.create_frames()
            sf_x = sf_dims[0]
            sf_y = sf_dims[1]
            sf_w = sf_dims[2]
            sf_h = sf_dims[3]

...

            fp1_lbl = tk.Label(
                    master = pc_gui,
                    image = fp1
            )
            fp1_lbl.image = fp1
            fp1_lbl.place(
                    x=sf_x + img_padding,
                    y=sf_y + 20,
                    anchor='nw'
            )

Everything up to this point "works" admittedly rather inefficiently. What I would like to do is not have those sf_x, _y, _w, and _h hanging out there in the wind and it follows that I would also not have to call the frame function from widget function in order to get them.

The reason is that I feel it's not in the right spirit of python frames as I would like to use Grid on the side frame only but create (or use) Grid from the widget function (the point of creating the side_frame in the first place) and I would prefer to only refresh the sub parts of the application window that need to be refreshed, and not the whole smash every 1 second.

What ends up happening is the images flicker every 1 second, even when nothing needs to be updated and while the images do resize according to the application window I am doing this with the Place functionality and effectively ignoring the existence of side_frame.

My current attempts have been around the following

        fp1_lbl = tk.Label(
                master = self.side_frame,
                image = fp1
        )
        fp1_lbl.image = fp1
        fp1_lbl.place(
                x=sf_x + img_padding,
                y=sf_y + 20,
                anchor='nw'
        )

I get versions of the following error:

AttributeError: 'Application' object has no attribute 'side_frame'


Solution

  • You need to name things with the "self" prefix in order to use them in other methods. So when you create the subframes:

            # Section of Application window dedicated to calculations
            self.side_frame = tk.Frame(
                    master = pc_gui,
                    width = app_width*.25,
                    height = app_height,
                    bg = "red"
            )
            self.side_frame.place(
                    x = app_width*.75,
                    y = 0
            )
    

    Now you can use them anywhere in your class as you have tried:

        fp1_lbl = tk.Label(
                master = self.side_frame,
                image = fp1
        )
    

    Remember there is no implied relationship between a variable named foo and self.foo. They are 2 completely unrelated names.


    As for your resizing, tkinter will do that for you. You can use the relwidth and relheight arguments to set the width and height to a fraction between 0 and 1. Here's a demo:

    import tkinter as tk
    
    class Application(tk.Frame):
        """ GUI """
        def __init__(self, pc_gui):
            """ Initialize the frame """
            tk.Frame.__init__(self, pc_gui)
            self.create_frames()
    
        def create_frames(self):
            # insert the subframes in *this* Frame (self), not in the master Frame
            self.video_frame = tk.Frame(self, bg = "blue")
            self.video_frame.place(relheight=1.0, relwidth=.25)
    
            self.side_frame = tk.Frame(self, bg = "red")
            self.side_frame.place(relheight=1.0, relwidth=.75, relx=1.0, anchor='ne')
    
    pc_gui = tk.Tk()
    pc_gui.geometry("1280x720")
    win = Application(pc_gui)
    win.pack(fill=tk.BOTH, expand=True)
    tk.Button(pc_gui, text = "Don't click me!").pack()
    pc_gui.mainloop()