python tkinter canvas not resizing

Question:

I’m trying to subclass some tkinter controls to make an auto scrolling frame for use in my program, i only want it to scroll vertically, and when widgets are added that need it to scroll horizontally instead to resize the parent to fit. widgets that go in this frame are not all defined at runtime and are created dynamically, so it must react to events. I have some code that is partly working, in that it i run it, i can use the buttons at the bottom to add and remove labels on the fly, if i do this without manually resizing the window first then it works as expected. however if i manually resize the window the horizontal resizing stops working:

from tkinter import *

__all__ = ["VerticalScrolledFrame"]

class VerticalScrolledFrame(Frame):
    def __init__(self, parent, *args, **kw):
        Frame.__init__(self, parent, *args, **kw)            
        self.grid_columnconfigure(1, weight=1)
        self.grid_rowconfigure(1, weight=1)
        # create a canvas object and a vertical scrollbar for scrolling it
        self.vscrollbar = Scrollbar(self, orient=VERTICAL)
        self.vscrollbar.grid(column=2, row=1, sticky="nesw")
        self.canvas = Canvas(self, bd=0, highlightthickness=0, yscrollcommand=self.vscrollbar.set, height=10, width=10)
        self.canvas.grid(column=1, row=1, sticky="nesw")
        self.vscrollbar.config(command=self.canvas.yview)

        # reset the view
        self.canvas.xview_moveto(0)
        self.canvas.yview_moveto(0)

        # create a frame inside the canvas which will be scrolled with it
        self.interior = Frame(self.canvas)
        self.interior_id = self.canvas.create_window(0, 0, window=self.interior, anchor=NW)

        self.interior.bind('<Configure>', self._configure_interior)

        # track changes to the canvas and frame width and sync them,
        # also updating the scrollbar
    def _configure_interior(self, event=None):
        print("config interior")
        # update the scrollbars to match the size of the inner frame
        size = (self.interior.winfo_width(), self.interior.winfo_height())
        self.canvas.config(scrollregion="0 0 %s %s" % size)
        if self.interior.winfo_reqwidth() > self.canvas.winfo_width():
            # update the canvas's width to fit the inner frame
            self.canvas.config(width=self.interior.winfo_reqwidth())

if __name__ == "__main__":
    labels = []
    labels2 = []
    def add1():
        l = Label(f.interior, text=str(len(labels)+1))
        l.grid(column=len(labels)+1, row=1)
        labels.append(l)

    def remove1():
        l = labels.pop()
        l.grid_forget()
        l.destroy()

    def add2():
        l = Label(f.interior, text=str(len(labels2)+1))
        l.grid(column=1, row=len(labels2)+1)
        labels2.append(l)

    def remove2():
        l = labels2.pop()
        l.grid_forget()
        l.destroy()

    app = Tk()
    app.grid_columnconfigure(1, weight=1)
    app.grid_columnconfigure(2, weight=1)
    app.grid_rowconfigure(1, weight=1)
    f = VerticalScrolledFrame(app)
    f.grid(column=1, row=1, columnspan=2, sticky="nesw")
    Button(app, text="Add-H", command=add1).grid(column=1, row=2, sticky="nesw")
    Button(app, text="Remove-H", command=remove1).grid(column=2, row=2, sticky="nesw")
    Button(app, text="Add-V", command=add2).grid(column=1, row=3, sticky="nesw")
    Button(app, text="Remove-V", command=remove2).grid(column=2, row=3, sticky="nesw")
    app.mainloop()

I’m working solely on windows and using python 3.3, I tried 3.4 as well but no change.

why does resizing the window stop the call to:

self.canvas.config(width=self.interior.winfo_reqwidth())

from working?

EDIT:
Brian Oakley’s response makes sense so I can understand that behavior, however i have now changed my code to also handle changes to the canvas size when the window is resized:

from tkinter import *

__all__ = ["VerticalScrolledFrame"]

class VerticalScrolledFrame(Frame):
    def __init__(self, parent, *args, **kw):
        Frame.__init__(self, parent, *args, **kw)
        p = parent
        while True:
            if p.winfo_class() in ['Tk', 'Toplevel']:
                break
            else:
                p = self._nametowidget(p.winfo_parent())
        self.root_window = p
        self.grid_columnconfigure(1, weight=1)
        self.grid_rowconfigure(1, weight=1)
        # create a canvas object and a vertical scrollbar for scrolling it
        self.vscrollbar = Scrollbar(self, orient=VERTICAL)
        self.vscrollbar.grid(column=2, row=1, sticky="nesw")
        self.canvas = Canvas(self, bd=0, highlightthickness=0, yscrollcommand=self.vscrollbar.set, height=10, width=10)
        self.canvas.grid(column=1, row=1, sticky="nesw")
        self.vscrollbar.config(command=self.canvas.yview)

        # reset the view
        self.canvas.xview_moveto(0)
        self.canvas.yview_moveto(0)

        # create a frame inside the canvas which will be scrolled with it
        self.interior = Frame(self.canvas)
        self.interior_id = self.canvas.create_window(0, 0, window=self.interior, anchor=NW)

        self.interior.bind('<Configure>', self._configure_interior)

        self.canvas.bind('<Configure>', self._configure_canvas)

        # track changes to the canvas and frame width and sync them,
        # also updating the scrollbar
    def _configure_interior(self, event=None):
        print("config interior")
        # update the scrollbars to match the size of the inner frame
        size = (self.interior.winfo_width(), self.interior.winfo_height())
        self.canvas.config(scrollregion="0 0 %s %s" % size)
        if self.interior.winfo_reqwidth() >= self.canvas.winfo_width():
            # update the canvas's width to fit the inner frame
            # only works before mainloop
            self.canvas.config(width=self.interior.winfo_reqwidth())
        screen_h = self.winfo_screenheight()
        if ((self.root_window.winfo_rooty() + self.root_window.winfo_height() - self.canvas.winfo_height() + self.interior.winfo_reqheight()) < screen_h):
            self.canvas.configure(height=self.interior.winfo_reqheight())

    def _configure_canvas(self, event=None):
        print("config canvas")
        if self.interior.winfo_reqwidth() < self.canvas.winfo_width():
            self.canvas.itemconfigure(self.interior_id, width=self.canvas.winfo_width())
        elif self.interior.winfo_reqwidth() > self.canvas.winfo_width():
            self.canvas.config(width=self.interior.winfo_reqwidth())

        if (self.interior.winfo_reqheight() < self.canvas.winfo_height()) or (self.interior.winfo_height() < self.canvas.winfo_height()):
            self.canvas.itemconfigure(self.interior_id, height=self.canvas.winfo_height())

if __name__ == "__main__":
    labels = []
    labels2 = []
    def add1():
##        app.geometry("")
        l = Label(f.interior, text=str(len(labels)+1))
        l.grid(column=len(labels)+1, row=1)
        f.interior.grid_columnconfigure(len(labels)+1, weight=1, minsize=l.winfo_reqwidth())
        labels.append(l)

    def remove1():
##        app.geometry("")
        l = labels.pop()
        l.grid_forget()
        f.interior.grid_columnconfigure(len(labels)+2, weight=0, minsize=0)
        l.destroy()

    def add2():
        l = Label(f.interior, text=str(len(labels2)+1))
        l.grid(column=1, row=len(labels2)+1)
        f.interior.grid_rowconfigure(len(labels2)+1, weight=1, minsize=l.winfo_reqheight())
        labels2.append(l)

    def remove2():
        l = labels2.pop()
        l.grid_forget()
        f.interior.grid_rowconfigure(len(labels2)+2, weight=0, minsize=0)
        l.destroy()

    app = Tk()
    app.grid_columnconfigure(1, weight=1)
    app.grid_columnconfigure(2, weight=1)
    app.grid_rowconfigure(1, weight=1)
    f = VerticalScrolledFrame(app)
    f.grid(column=1, row=1, columnspan=2, sticky="nesw")
    Button(app, text="Add-H", command=add1).grid(column=1, row=2, sticky="nesw")
    Button(app, text="Remove-H", command=remove1).grid(column=2, row=2, sticky="nesw")
    Button(app, text="Add-V", command=add2).grid(column=1, row=3, sticky="nesw")
    Button(app, text="Remove-V", command=remove2).grid(column=2, row=3, sticky="nesw")
    for i in range(0,10):
        add1()
        add2()
    app.mainloop()

the problem I am now seeing is that if I add items vertically before resizing the window, the _configure_interior function is called for each widget that gets added and the window is either resized vertically to fit (if before window sizing) and it is supposed to update the scrollregion (and scrollbar) if not, however after resizing the window the _configure_interior stops being called as each widget is added, and if i call it manually the scrollbar is still not updated?

Asked By: James Kent

||

Answers:

why does resizing the window stop the call to:

self.canvas.config(width=self.interior.winfo_reqwidth())

That is because your code is bound to the frame changing. When the user resizes the window, you resize the window, not the frame. If you want the function to be called when the window is resized you must bind it or a similar function to the resizing of the window or canvas in addition to the automatic resizing of the interior frame.

You’ll also need to be aware that if the user resizes the window, adding objects to the frame in the canvas won’t make the window bigger. The user’s choice of window size will override the computed size from the interior widgets being resized. You’ll have to reset the geometry to the empty string to get it to automatically grow.

Answered By: Bryan Oakley

Out of all the tkinter "scrollable" frame examples this was the only one that actually works with dynamically added widgets. The key is updating the canvas and subwindow’s frame height in response to the subframe’s request height.

The below snippet in the frame "<Configure>" handler

self.canvas.config(height=self.interior.winfo_reqheight())
self.canvas.itemconfig(self.interior_id, height=self.interior.winfo_reqheight())

fixes the canvas size so that the interior frame will fill it.

Answered By: Justin Parus
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.