Search code examples
pythonpython-3.xtkinterspyderttk

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. The scrollbar is finally updating! But it's updating to size of the previous bbox.

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

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)
    innerFrame.bind("<Map>", lambda e: scrollingCanvas.config(scrollregion=scrollingCanvas.bbox(ALL)))
    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):
    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():
            print(entriesFrame.winfo_children().reverse())
            for child in entriesFrame.winfo_children():
                if isinstance(child, Canvas):
                    frm = child.winfo_children()[0]
                    searchAndDestory(frm.grid_slaves())
                    populateEntries(frm)
                    child.config(scrollregion=child.bbox(ALL))
        else:
            searchAndDestory(entriesFrame.winfo_children()[0])
            populateEntries(makeScrollbarAndCanvas())
    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)
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 think it has something to do with configuring the scrollregion.

Thank you to those who have already helped!


Solution

  • Thanks to everyone that has helped! The original question has been edited a few times, so check out the edits for the original question. Here's the code that worked.

    from tkinter import Canvas, Tk, LEFT, BOTH, RIGHT, VERTICAL, X, Y, ALL, IntVar, TOP
    from tkinter.ttk import Frame, Scale, Entry, Scrollbar, Spinbox
    
    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):
        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())
                        populateEntries(frm)
                        child.config(scrollregion=(0, 0, 96, entries.get() * 25))
            else:
                searchAndDestory(entriesFrame.winfo_children()[0])
                populateEntries(makeScrollbarAndCanvas())
        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)
    refreshEntries()
    entriesFrame.pack(fill=BOTH)
    outerFrame.pack(fill=BOTH)
    
    root.mainloop()