Search code examples
pythontkintertkinter-canvastkinter-entry

Why does the tkinter scrollbar on a canvas with inner frame get disabled on declaring pack_propagate(0)?


My code has a Frame, a Canvas inside the Frame, and an inner Frame inside the Canvas. I want to put Entry boxes inside the inner Frame and fit them to the inner Frame via pack_propagate(0) to avoid pixel/font width conversions with the Entry widget's width option. However, this breaks the inner Frame's scroll functionality. I want to add Entry widgets dynamically above and below the first and last Entry widgets in the inner Frame, which I am currently doing using pack(before=). So I would like to stick with the packer if possible.

How can I get the scrollbar working again? The following minimum working example has frame.pack_propagate(0) commented out, so the Entry widgets are not sized to the column correctly:

import tkinter as tk
import tkinter.ttk as ttk

root = tk.Tk()

myframe = ttk.Frame(root)
myframe.pack()
sb = ttk.Scrollbar(myframe)
sb.pack(side=tk.RIGHT, fill=tk.Y, expand=1)

canvas = tk.Canvas(myframe, width=200, height=300,
                   scrollregion=(0, 0, 200, 300), yscrollcommand=sb.set)
canvas.pack()
frame = ttk.Frame(canvas, width=200, height=300) # inner Frame
canvas.create_window((0, 0), window=frame, anchor='nw')
sb.config(command=canvas.yview)

frame.bind("<Configure>", lambda event: canvas.configure(
    scrollregion=canvas.bbox(tk.ALL)))
# frame.pack_propagate(0) # How to enable this and ensure scrollbar works?

s = tk.StringVar()
s.set("I'm a box")
for _ in range(100):
    eb = ttk.Entry(frame, textvariable=s)
    eb.pack()

root.mainloop()

Edit1: Added StringVar() and changed eb.grid() to eb.pack()


Solution

  • See example which resize inner Frame to Canvas - it uses

    self._canvas.bind('<Configure>', self.inner_resize)
    

    and inside method inner_resize()

    self._canvas.itemconfig(self._window, width=event.width)
    

    Full example

    import tkinter as tk
    import tkinter.ttk as ttk
    
    class ScrolledFrame(tk.Frame):
    
        def __init__(self, parent, vertical=True, horizontal=False):
            super().__init__(parent)
    
            # canvas for inner frame
            self._canvas = tk.Canvas(self, bg='red')
            self._canvas.grid(row=0, column=0, sticky='news') # changed
    
            # create right scrollbar and connect to canvas Y
            self._vertical_bar = tk.Scrollbar(self, orient='vertical', command=self._canvas.yview)
            if vertical:
                self._vertical_bar.grid(row=0, column=1, sticky='ns')
            self._canvas.configure(yscrollcommand=self._vertical_bar.set)
    
            # create bottom scrollbar and connect to canvas X
            self._horizontal_bar = tk.Scrollbar(self, orient='horizontal', command=self._canvas.xview)
            if horizontal:
                self._horizontal_bar.grid(row=1, column=0, sticky='we')
            self._canvas.configure(xscrollcommand=self._horizontal_bar.set)
    
            # inner frame for widgets
            self.inner = tk.Frame(self._canvas)
            self._window = self._canvas.create_window((0, 0), window=self.inner, anchor='nw')
    
            # autoresize inner frame
            self.columnconfigure(0, weight=1) # changed
            self.rowconfigure(0, weight=1) # changed
    
            # resize when configure changed
            self.inner.bind('<Configure>', self.resize)
    
            # resize inner frame to canvas size
            self.resize_width = False
            self.resize_height = False
            self._canvas.bind('<Configure>', self.inner_resize)
    
        def resize(self, event=None): 
            self._canvas.configure(scrollregion=self._canvas.bbox('all'))
    
        def inner_resize(self, event):
            # resize inner frame to canvas size
            if self.resize_width:
                self._canvas.itemconfig(self._window, width=event.width)
            if self.resize_height:
                self._canvas.itemconfig(self._window, height=event.height)
    
    # --- main ----
    
    root = tk.Tk()
    
    sf = ScrolledFrame(root)
    sf.resize_width = True # it will resize frame to canvas
    sf.pack(fill='both', expand=True)
    
    s = tk.StringVar()
    s.set("I'm a box")
    
    for _ in range(100):
        eb = ttk.Entry(sf.inner, textvariable=s)
        eb.pack(fill='x', expand=True)
    
    root.mainloop()