Search code examples
pythonlambdatkintertkinter-canvaspython-bindings

Tkinter: scrollable Frame in Canvas: Auto-resize binding error


This script when run, opens a window that is divided into 3 frames:

  • A big frame into which data will be shown (label widgets).
  • A smaller frame underneath it with user input widgets.
  • A small frame in the bottom-right corner with a textbox widget.

The big frame will have a lot of data (= label-widgets) so I need it to be scrollable (vertically). This I have done by creating a canvas widget alonside a scrollbar widget. In the canvas, a frame widget is placed.

Everything seems to be working, however my resizing function does not. My frame widget does not get its dimensions updated! This is probably because of an error that I can't manage to fix.

Fundamental question:

The script gives an error on the "lambda: resize_frame(self)" command on line 43. How do i fix this?

Side-note: My issue probably has more to do with an improper binding on the canvas widget. Because I'm not sure I wanted to give enough context (script).

Many thanks in advance.

from Tkinter import *
import math

class Processing(Toplevel):
    def __init__(self, master, *args, **kwargs):
        Toplevel.__init__(self, master)
        self.master = master
        self.title("Process Window")
        for r in range(6):
            self.rowconfigure(r, weight = 1)    
        for c in range(4):
            self.columnconfigure(c, weight = 1)

        ### WINDOW size and position definitions ###
        ScreenSizeX = master.winfo_screenwidth()
        ScreenSizeY = ( master.winfo_screenheight() - 75 ) #about 75pixels for taskbar on bottom of screen (Windows)
        ScreenRatio = 0.9
        FrameSizeX  = int(ScreenSizeX * ScreenRatio)
        FrameSizeY  = int(ScreenSizeY * ScreenRatio)
        FramePosX   = (ScreenSizeX - FrameSizeX)/2
        FramePosY   = (ScreenSizeY - FrameSizeY)/2
        self.geometry("%sx%s+%s+%s"%(FrameSizeX,FrameSizeY,FramePosX,FramePosY))

        ### Creating 3 "sub-frames" ###
        # Frame 1 - canvas container with scrollbar#
        self.Canvas1 = Canvas(self, bg = "white")
        self.Canvas1.grid(row = 0, column = 0, rowspan = 5, columnspan = 4, sticky = N+E+S+W)
        self.Canvas1.rowconfigure(1, weight = 1)
        self.Canvas1.columnconfigure(1, weight = 1)
        self.myscrollbar=Scrollbar(self, orient = "vertical", command = self.Canvas1.yview)
        self.Canvas1.configure(yscrollcommand = self.myscrollbar.set)
        self.myscrollbar.grid(row = 0, column = 4, rowspan = 5, sticky = N+S)

        # Frame 1 - Frame widget in canvas #
        self.Frame1 = Frame(self.Canvas1, bg = "white")
        self.Frame1.rowconfigure(0, weight = 1)    
        for c in range(2):
            self.Frame1.columnconfigure(1 + (2 * c), weight = 1)#1,3 - columns for small icons in the future
        for cb in range(3):
            self.Frame1.columnconfigure((cb * 2), weight = 9)#0,2,4 - columns for data

        self.CFrame1 = self.Canvas1.create_window(0, 0, window = self.Frame1, width = FrameSizeX, anchor = N+W)
        self.Canvas1.bind("<Configure>", lambda: resize_frame(self)) # !!!!! Doesn't work & gives error !!!!!! #
        self.Frame1.bind("<Configure>", lambda: scrollevent(self))

        self.Canvas1.config(scrollregion=self.Canvas1.bbox("all"))

        # Frame 2 #
        self.Frame2 = Frame(self, bg= "yellow")
        self.Frame2.grid(row = 5, column = 0, rowspan = 1, columnspan = 3, sticky = W+E+N+S)
        for r in range(3):
            self.Frame2.rowconfigure(r, weight=1)    
        for c in range(3):
            self.Frame2.columnconfigure(c, weight = 1)
        # Frame 3 #
        self.Frame3 = Frame(self)
        self.Frame3.grid(row = 5, column = 3, rowspan = 1, columnspan = 2, sticky = W+E+N+S)
        self.Frame3.rowconfigure(0, weight = 1)
        self.Frame3.columnconfigure(0, weight = 1)

        # Propagation #
        #self.grid_propagate(False)        # All widgets (the 3 subframes) need to fit in Toplevel window. Minimal window size will be implemented later.
        self.Canvas1.grid_propagate(False) # canvas works with scrollbar, widgets dont need to fit in window size.
        #self.Frame1.grid_propagate(False) # Frame1 should resize to hold all data (label-widgets)
        self.Frame2.grid_propagate(False)  # fixed frame dimensions
        self.Frame3.grid_propagate(False)  # fixed textbox dimensions
        self.Frame1.update_idletasks() # just to make sure

        ### Widgets for the multiple frames ###
        # Frame1 - further populated by button command in frame 2#
        self.lblaa = Label(self.Frame1, bg="white", text = "Processing...", justify = "left")
        self.lblaa.grid(row = 0, column = 0, sticky = N+W)
        self.LSlabelsr = []
        self.LSlabelsa = []
        self.LSlabelsb = []
        # Frame 2 #
        self.Wbuttontest=Button(self.Frame2, text="Start listing test", command = lambda: refresh(self))
        self.Wbuttontest.grid(row = 0, column = 0, columnspan = 3)
        self.Wentry = Entry(self.Frame2)
        self.Wentry.grid(row = 2, column = 0, columnspan = 3, sticky = E+W, padx = 10)
        self.Wentry.delete(0, END)
        self.Wentry.insert(0, "user input here")
        # Frame3 #
        self.Wtext = Text(self.Frame3)
        self.Wscrollb = Scrollbar(self.Frame3)
        self.Wscrollb.config(command = self.Wtext.yview)
        self.Wtext.config(yscrollcommand = self.Wscrollb.set)
        self.Wtext.grid(row = 0, column = 0, sticky = N+E+W+S)        


        ### Test-Lists ### Last character in the left column is "ez" !! ###
        self.LSa = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
        self.LSb = [1, 2, 3, 4, 66, 6, 7, 8, 9, 67, 11, 12, 13, 14, 68]
        self.LSr = []
        ib = 0
        prefix = ""
        for i in range(104):
            if ib > 25:
                prefix = chr(ord("a") + (i/ib - 1))
                ib = 0
            else:
                pass
            self.LSr.append(prefix + chr(97+ib))
            ib += 1

        ### FUNCTIONS ###
        def resize_frame(self, event):
            self.Canvas1.itemconfig(self.CFrame1, width = e.width) #height of frame should depend on the contents.

        def scrollevent(event):
            self.Canvas1.configure(scrollregion=self.Canvas1.bbox("all"),width=200,height=200)

        def refresh(self): ### Button-command: data will be shown ###
            if self.lblaa.winfo_exists() == 1:
                self.lblaa.destroy()
            for i in range(len(self.LSr)):
                self.Frame1.rowconfigure(i, weight = 0)
            del self.LSlabelsr[:] # remove any previous labels from if the callback was called before
            del self.LSlabelsa[:] # remove any previous labels from if the callback was called before
            del self.LSlabelsb[:] # remove any previous labels from if the callback was called before
            Vlabelheight = 1
            # Left List #
            for i in range(len(self.LSr)):
                self.LSlabelsr.append(Label(self.Frame1, text = str(self.LSr[i]), bg = "LightBlue", justify = "left", height = Vlabelheight))
                self.LSlabelsr[i].grid(row = i, column = 0, sticky = E+W)
            # Middle List #
            for i in range(len(self.LSa)):
                self.LSlabelsa.append(Label(self.Frame1, text = str(self.LSa[i]), bg = "LightBlue", fg = "DarkViolet", justify = "left", height = Vlabelheight))
                self.LSlabelsa[i].grid(row = i, column = 2, sticky = E+W)
            # Right List #
            for i in range(len(self.LSb)):
                self.LSlabelsb.append(Label(self.Frame1, text = str(self.LSb[i]), bg = "LightBlue", fg = "DarkGreen", justify = "left", height = Vlabelheight))
                self.LSlabelsb[i].grid(row = i, column = 4, sticky = E+W)
            self.Frame1.update()
            self.Frame1.update_idletasks()
            print("done")

if __name__ == "__main__":
    root = Tk()
    root.title("Invisible")
    root.resizable(FALSE,FALSE)
    root.withdraw()
    app = Processing(root)
    root.mainloop()

Working version after suggestions by R4PH4EL:

  • Proper indentation: the functions were defined as part of the init.
  • Some tweaks on the lambda commands on line 43/107 & 44/110.

    from Tkinter import *
    import math
    
    class Processing(Toplevel):
        def __init__(self, master, *args, **kwargs):
            Toplevel.__init__(self, master)
            self.master = master
            self.title("Process Window")
            for r in range(6):
                self.rowconfigure(r, weight = 1)    
            for c in range(4):
                self.columnconfigure(c, weight = 1)
    
            ### WINDOW size and position definitions ###
            ScreenSizeX = master.winfo_screenwidth()
            ScreenSizeY = ( master.winfo_screenheight() - 75 ) #about 75pixels for taskbar on bottom of screen (Windows)
            ScreenRatio = 0.9
            FrameSizeX  = int(ScreenSizeX * ScreenRatio)
            FrameSizeY  = int(ScreenSizeY * ScreenRatio)
            FramePosX   = (ScreenSizeX - FrameSizeX)/2
            FramePosY   = (ScreenSizeY - FrameSizeY)/2
            self.geometry("%sx%s+%s+%s"%(FrameSizeX,FrameSizeY,FramePosX,FramePosY))
    
            ### Creating 3 "sub-frames" ###
            # Frame 1 - canvas container with scrollbar#
            self.Canvas1 = Canvas(self, bg = "white")
            self.Canvas1.grid(row = 0, column = 0, rowspan = 5, columnspan = 4, sticky = N+E+S+W)
            self.Canvas1.rowconfigure(1, weight = 1)
            self.Canvas1.columnconfigure(1, weight = 1)
            self.myscrollbar=Scrollbar(self, orient = "vertical", command = self.Canvas1.yview)
            self.Canvas1.configure(yscrollcommand = self.myscrollbar.set)
            self.myscrollbar.grid(row = 0, column = 4, rowspan = 5, sticky = N+S)
    
            # Frame 1 - Frame widget in canvas #
            self.Frame1 = Frame(self.Canvas1, bg = "white")
            self.Frame1.rowconfigure(0, weight = 1)    
            for c in range(2):
                self.Frame1.columnconfigure(1 + (2 * c), weight = 1)#1,3 - columns for small icons in the future
            for cb in range(3):
                self.Frame1.columnconfigure((cb * 2), weight = 9)#0,2,4 - columns for data
    
            self.CFrame1 = self.Canvas1.create_window(0, 0, window = self.Frame1, width = FrameSizeX, anchor = N+W)
            self.Canvas1.bind("<Configure>", lambda event: self.resize_frame(event)) # !!!!! Doesn't work & gives error !!!!!! #
            self.Frame1.bind("<Configure>", lambda event: self.scrollevent(event))
    
            self.Canvas1.config(scrollregion=self.Canvas1.bbox("all"))
    
            # Frame 2 #
            self.Frame2 = Frame(self, bg= "yellow")
            self.Frame2.grid(row = 5, column = 0, rowspan = 1, columnspan = 3, sticky = W+E+N+S)
            for r in range(3):
                self.Frame2.rowconfigure(r, weight=1)    
            for c in range(3):
                self.Frame2.columnconfigure(c, weight = 1)
            # Frame 3 #
            self.Frame3 = Frame(self)
            self.Frame3.grid(row = 5, column = 3, rowspan = 1, columnspan = 2, sticky = W+E+N+S)
            self.Frame3.rowconfigure(0, weight = 1)
            self.Frame3.columnconfigure(0, weight = 1)
    
            # Propagation #
            #self.grid_propagate(False)        # All widgets (the 3 subframes) need to fit in Toplevel window. Minimal window size will be implemented later.
            self.Canvas1.grid_propagate(False) # canvas works with scrollbar, widgets dont need to fit in window size.
            #self.Frame1.grid_propagate(False) # Frame1 should resize to hold all data (label-widgets)
            self.Frame2.grid_propagate(False)  # fixed frame dimensions
            self.Frame3.grid_propagate(False)  # fixed textbox dimensions
            self.Frame1.update_idletasks() # just to make sure
    
            ### Widgets for the multiple frames ###
            # Frame1 - further populated by button command in frame 2#
            self.lblaa = Label(self.Frame1, bg="white", text = "Processing...", justify = "left")
            self.lblaa.grid(row = 0, column = 0, sticky = N+W)
            self.LSlabelsr = []
            self.LSlabelsa = []
            self.LSlabelsb = []
            # Frame 2 #
            self.Wbuttontest=Button(self.Frame2, text="Start listing test", command = lambda: self.refresh())
            self.Wbuttontest.grid(row = 0, column = 0, columnspan = 3)
            self.Wentry = Entry(self.Frame2)
            self.Wentry.grid(row = 2, column = 0, columnspan = 3, sticky = E+W, padx = 10)
            self.Wentry.delete(0, END)
            self.Wentry.insert(0, "user input here")
            # Frame3 #
            self.Wtext = Text(self.Frame3)
            self.Wscrollb = Scrollbar(self.Frame3)
            self.Wscrollb.config(command = self.Wtext.yview)
            self.Wtext.config(yscrollcommand = self.Wscrollb.set)
            self.Wtext.grid(row = 0, column = 0, sticky = N+E+W+S)        
    
    
            ### Test-Lists ### Last character in the left column is "ez" !! ###
            self.LSa = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
            self.LSb = [1, 2, 3, 4, 66, 6, 7, 8, 9, 67, 11, 12, 13, 14, 68]
            self.LSr = []
            ib = 0
            prefix = ""
            for i in range(104):
                if ib > 25:
                    prefix = chr(ord("a") + (i/ib - 1))
                    ib = 0
                else:
                    pass
                self.LSr.append(prefix + chr(97+ib))
                ib += 1
    
        ### FUNCTIONS ###
        def resize_frame(self, e):
            self.Canvas1.itemconfig(self.CFrame1, width = e.width) #height of frame should depend on the contents.
    
        def scrollevent(self, event):
            self.Canvas1.configure(scrollregion=self.Canvas1.bbox("all"),width=200,height=200)
    
        def refresh(self): ### Button-command: data will be shown ###
            if self.lblaa.winfo_exists() == 1:
                self.lblaa.destroy()
            for i in range(len(self.LSr)):
                self.Frame1.rowconfigure(i, weight = 0)
            del self.LSlabelsr[:] # remove any previous labels from if the callback was called before
            del self.LSlabelsa[:] # remove any previous labels from if the callback was called before
            del self.LSlabelsb[:] # remove any previous labels from if the callback was called before
            Vlabelheight = 1
            # Left List #
            for i in range(len(self.LSr)):
                self.LSlabelsr.append(Label(self.Frame1, text = str(self.LSr[i]), bg = "LightBlue", justify = "left", height = Vlabelheight))
                self.LSlabelsr[i].grid(row = i, column = 0, sticky = E+W)
            # Middle List #
            for i in range(len(self.LSa)):
                self.LSlabelsa.append(Label(self.Frame1, text = str(self.LSa[i]), bg = "LightBlue", fg = "DarkViolet", justify = "left", height = Vlabelheight))
                self.LSlabelsa[i].grid(row = i, column = 2, sticky = E+W)
            # Right List #
            for i in range(len(self.LSb)):
                self.LSlabelsb.append(Label(self.Frame1, text = str(self.LSb[i]), bg = "LightBlue", fg = "DarkGreen", justify = "left", height = Vlabelheight))
                self.LSlabelsb[i].grid(row = i, column = 4, sticky = E+W)
            self.Frame1.update()
            self.Frame1.update_idletasks()
            print("done")
    
    if __name__ == "__main__":
        root = Tk()
        root.title("Invisible")
        root.resizable(FALSE,FALSE)
        root.withdraw()
        app = Processing(root)
        root.mainloop()
    

Solution

  • Either your indentation is wrong or your getting something wrong in general.

    All of your functions are defined inside your __init__ function

    Second: if you want to call a class function, you call it by obj.function

    Your error on lambda: resize(self) may occur as it should be lambda: self.resize.

    Give it a shot with this one and try it. And please make sure your indentations are correct.

    I totally agree with Bryan here - ommiting the lambdas it would be (my personal opinion) easier to read and kind of "better" in a meaning of more structured and pragmatic coding style.