Max Friedman
Max Friedman

Reputation: 11

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!

Upvotes: 0

Views: 77

Answers (2)

Max Friedman
Max Friedman

Reputation: 11

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()

Upvotes: 1

Tim Roberts
Tim Roberts

Reputation: 54767

The problem is not the ScrollBar, of course. You can prove that by reducing the number of controls back down, then back up. The problem is the transition. If you go from "no scrollbar" to "scrollbar", then it works fine. If you go from "scrollbar" to "scrollbar", then it doesn't.

The reason becomes clear if you add some debug code:

def refreshEntries():
    for slave in entriesFrame.grid_slaves():
        print('*',end='')
        slave.destroy()
    for slave in entriesFrame.pack_slaves():
        print('=',end='')
        slave.destroy()
    print()
    populateEntries()

If you do that, you will see unexpected results. When you have a scrollbar, you add an additional frame in there. The child controls are not children of entriesFrame. Instead, entriesFrame contains a canvas, and the child controls are children of THAT. Those children do not get deleted (nothing does), and the scrollbar gets confused.

If you add more code to clean up the "with scrollbar" case correctly, this will all work.

Upvotes: 0

Related Questions