kfsone
kfsone

Reputation: 24259

Python3 tk.Scrollbar and focus

I'm trying to put together a UI that has to present the user with a long list of input selections. I've put the input fields into a frame with a scroll bar and scrolling with the bar works. But when the user tabs out of the viewport the scroll bar doesn't automatically follow it.

The other problem I have is that the window is not sizing to fit the content; if I call self.frame.grid() after populate() the window does some odd stuff and sizes to content, but the scroll-bar no-longer works if the window is resized or if the number of lines of content exceeds the height of the screen. I want the window to be min(maxSizeForRows, screenHeight) and fixed width but not height.

I'd appreciate any help or pointers getting this to work.

import tkinter as tk
import tkinter.ttk as ttk

class Test(tk.Canvas):
    def __init__(self, root):
        super().__init__(root, borderwidth=0)
        self.root = root

        self.vsb = tk.Scrollbar(root, orient="vertical", command=self.yview)
        self.configure(yscrollcommand=self.vsb.set)
        self.vsb.pack(side="right", fill="y")

        self.frame = tk.Frame(self)
        self.create_window((4,4), window=self.frame, anchor="nw", tags="self.frame")

        self.pack(side="left", fill="both", expand=True)

        self.frame.bind("<Configure>", self.onFrameConfigure)

        self.populate()


    def onFrameConfigure(self, event):
        self.configure(scrollregion=self.bbox("all"))


    def addRow(self, label, ent1, ent2):
        row = self.row

        lab = tk.Label(self.frame, text=label)
        lab.grid(row=row, column=0, sticky='W')

        sv = tk.StringVar()
        sv.set(ent1)
        e1 = tk.Entry(self.frame, width=16, justify=tk.RIGHT, textvariable=sv)
        e1.grid(row=row, column=1)

        sv = tk.StringVar()
        sv.set(ent2)
        e2 = tk.Entry(self.frame, width=16, justify=tk.RIGHT, textvariable=sv)
        e2.grid(row=row, column=2)

        self.row += 1


    def populate(self):

        self.row = 0
        for i in range(0, 10):
            self.addRow("Greeting", "Hello", "World")
            self.addRow("Parting", "Ciao", "Banana")
            self.addRow("Food", "Pepperoni", "Pizza")
            self.addRow("Drink", "Cold", "Beer")
            self.addRow("Actor", "Liam", "Neeson")
            self.addRow("Occupation", "Software", "Engineer")
            self.addRow("Languages", "Python", "Perl")
            self.addRow("Browsers", "Chrome", "Firefox")
            self.addRow("Places", "Irvine", "London")
            self.addRow("States", "Cali", "Texas")


if __name__ == "__main__":
    root = tk.Tk()
    app = Test(root=root)
    app.pack(side="top", fill="both", expand=True)
    app.mainloop()

Upvotes: 1

Views: 1853

Answers (1)

Bryan Oakley
Bryan Oakley

Reputation: 386030

To solve the issue with the tabbed-to widget being outside the viewport, you're going to need to add a binding to the entry widgets so that when they get focus the canvas is scrolled appropriately. This just requires a little math. It looks something like this:

def __init__(...):
    ...
    # this makes it possible to scroll one pixel at a time
    self.configure(yscrollincrement=1)
    ...

def addRow(...):
    ...
    e1.bind("<FocusIn>", self.scroll_into_view)
    e2.bind("<FocusIn>", self.scroll_into_view)
    ...

def scroll_into_view(self, event):
    widget_top = event.widget.winfo_y()
    widget_bottom = widget_top + event.widget.winfo_height()
    canvas_top = self.canvasy(0)
    canvas_bottom = canvas_top + self.winfo_height()

    if widget_bottom > canvas_bottom:
        # subtract 4, because the frame is at 4,4 rather than 0,0
        delta = int(canvas_bottom - widget_bottom) - 4
        self.yview_scroll(-delta, "units")
    elif widget_top < canvas_top:
        delta = int(widget_top - canvas_top) + 4
        self.yview_scroll(delta, "units")

My math might be a tiny bit off, and there's a way to avoid hard-coding the 4 there, I'll leave that as an exercise for the reader.

As for the resizing problem, if I understand you correctly then you need to bind on the <Configure> event of the canvas, so that when the canvas size changes, you change the width of the frame. That looks something like this:

def __init__(...):
    ...
    self.bind("<Configure>", self.onCanvasConfigure)
    ...

def onCanvasConfigure(self, event):
    # subtract 8 to account for a border/margin
    self.itemconfigure('self.frame', width=self.winfo_width()-8)

You might need to tweak the constants to get precisely the right behavior, but hopefully this points you in the right direction.

Upvotes: 2

Related Questions