2023-07-21

Trouble with TTK Scrollbar

I'm working on building a GUI using ttk. One component involves dynamically adjusting the number of rows of Entry objects with a Scale / Spinbox. During the range of acceptable values, the number of fields exceeds the size of the window, so a scroll bar and canvas are used to accommodate. The issue arises with when the frame gets updated from one that had a scrollbar to another that has a scrollbar.

In this version, no matter if there is a scrollbar/canvas or not, the entries are placed in an innerFrame within the entriesFrame. When the number of entries is updated, there was a scrollbar, and there still needs a scrollbar the scrolable region only ever shows 8 Entries.

The current debug readouts are the objects in the innerFrame if there's a scrollbar after attempting to delete them all, the number of entries from the scale / spinbox, and the objects in the innerFrame if there's a scrollbar.

from tkinter import Canvas, Tk, LEFT, BOTH, RIGHT, VERTICAL, X, Y, ALL, IntVar, TOP
from tkinter.ttk import Frame, Scale, Entry, Scrollbar, Spinbox, Style

def validateSpin(P):
    if (P.isdigit() or P =="") and int(P) > 0 and int(P) < 11:
        return True
    else:
        return False
def updateScale(event):
    def inner():
        spnVal = spin.get()
        if spnVal != "":
            scale.set(float(spnVal))
            entries.set(float(spnVal))
            refreshEntries()
    root.after(1, inner)
def updateSpin(event):
    def inner():
        reportVal = round(float(scale.get()))
        spin.set(reportVal)
        entries.set(reportVal)
        refreshEntries()
    root.after(1, inner)
def scrollbarNeeded():
    if entries.get() > 7:
        return True
    else:
        return False
def makeScrollbarAndCanvas():
    scrollingCanvas = Canvas(entriesFrame, width = 200, highlightthickness=0)
    scrollingCanvas.pack(side=LEFT,fill=X,expand=1)
    
    scrollbar = Scrollbar(entriesFrame, orient=VERTICAL,command=scrollingCanvas.yview)
    scrollbar.pack(side=RIGHT,fill=Y)
    
    scrollingCanvas.configure(yscrollcommand=scrollbar.set)
    scrollingCanvas.bind("<Configure>",lambda e: scrollingCanvas.config(scrollregion=scrollingCanvas.bbox(ALL)))
    
    innerFrame = Frame(scrollingCanvas)
    scrollingCanvas.create_window((0,0),window= innerFrame, anchor="nw")
    
    return innerFrame
def scrollbarFound():
    for child in entriesFrame.pack_slaves():
        if isinstance(child, Canvas):
            return True
    return False
def populateEntries(frm):
    print(entries.get())
    for i in range(entries.get()):
        E = Entry(frm, width=15)
        E.grid(column=0, row = i, pady=2)

def refreshEntries():
    def searchAndDestory(obj):
        if hasattr(obj, 'winfo_children') and callable(getattr(obj, 'winfo_children')):
            for child in obj.winfo_children():
                searchAndDestory(child)
            obj.destroy()
        elif isinstance(obj, list):
            for child in obj:
                searchAndDestory(child)
        else:
            obj.destroy()
    if scrollbarNeeded():
        if scrollbarFound():
            for child in entriesFrame.winfo_children():
                if isinstance(child, Canvas):
                    frm = child.winfo_children()[0]
                    searchAndDestory(frm.grid_slaves())
                    #searchAndDestory(child.winfo_children()[0])
                    #frm = Frame(child)
                    #child.create_window((0,0),window= frm, anchor="nw")
            for child in entriesFrame.winfo_children():
                if isinstance(child, Scrollbar):
                    child.update()
        else:
            searchAndDestory(entriesFrame.winfo_children()[0])
            frm = makeScrollbarAndCanvas()
        print(frm.grid_slaves())
        populateEntries(frm)
        print(frm.grid_slaves())
    else:
        searchAndDestory(entriesFrame.winfo_children())
        innerFrame = Frame(entriesFrame)
        populateEntries(innerFrame)
        innerFrame.pack(fill=BOTH)

root = Tk()
root.resizable(False,False)
root.geometry("275x250")

outerFrame = Frame(root, padding = 10)

entries = IntVar()
entries.set(5)

topFrame = Frame(outerFrame, padding = 10)

spin = Spinbox(topFrame, from_=1, to=10, validate="key", validatecommand=(topFrame.register(validateSpin), "%P"))
spin.grid(column=0, row=0)
spin.set(entries.get())
spin.bind("<KeyRelease>", updateScale)
spin.bind("<ButtonRelease>", updateScale)
spin.bind("<MouseWheel>", updateScale)

scale = Scale(topFrame, from_=1, to=10)
scale.grid(column=1, row=0)
scale.set(entries.get())
scale.bind("<Motion>", updateSpin)
scale.bind("<ButtonRelease>", updateSpin)

topFrame.pack(side=TOP, fill=BOTH)

entriesFrame = Frame(outerFrame)
sty = Style()
refreshEntries()
entriesFrame.pack(fill=BOTH)
outerFrame.pack(fill=BOTH)

root.mainloop()

The Scale and Spinbox work together fine and I've been able to make them control the number of fields. I've verified that with a constant number of entries, the scroll bar and Entry objects are completely operational. Additionally, the correct number of entries are being created and displayed, (you can verify by changing the root.geometry() term to "275x350"). I'm not quite grasping what needs to change to get the scrollbar to expand its range of motion.



No comments:

Post a Comment