Search code examples
pythonpython-3.xtkintertkinter-canvasttk

Scrollable page in Tkinter Notebook


For a project I need to specify a certain value for N subfiles (sets of data), and this value can either be evenly spaced (omitted here), requiring only a starting value and an increment, or unevenly spaced, which means each subfile has its own value. I've decided to use a Notebook to separate the two methods of entry.

As the number of subfiles can get into hundreds, I would need a scrollbar, and after consulting Google I've found out that to use a scrollbar in such manner I would need to use a canvas and place a frame in it with everything I would want to scroll through.

The number can vary each time, so I decided to use a dictionary, that would be iteratively filled, to contain all 'entry frames' that each contain a label, an entry field and a variable, rolled up into one custom class IterEntryField. After a class instance is created, it's packed inside one container frame. After the for loop is over, the container frame is placed on a canvas and the scrollbar is given a new scrollregion.

from tkinter import *
from tkinter.ttk import Notebook

N = 25

class IterEntryField:
    def __init__(self, frame, label):
        self.frame = frame
        self.label = label
    def pack(self):
        self.valLabel = Label(self.frame, text = self.label, anchor = 'w')
        self.valLabel.pack(fill = X, side = LEFT)
        self.variable = StringVar()
        self.variable.set('0')
        self.valEntry = Entry(self.frame, textvariable = self.variable)
        self.valEntry.pack(fill = X, side = RIGHT)

def notebookpopup():
    zSetupWindow = Toplevel(root)
    zSetupWindow.geometry('{}x{}'.format(800, 300))
    notebook = Notebook(zSetupWindow)

    evspace = Frame(notebook)
    notebook.add(evspace, text = "Evenly spaced values")

    sOverflow = Label(evspace, text = 'Ignore this')
    sOverflow.pack()

    uevspace = Frame(notebook)
    notebook.add(uevspace, text = "Individual values")
    canvas = Canvas(uevspace, width = 800, height = 400)
    vsb = Scrollbar(canvas, command=canvas.yview)
    canvas.config(yscrollcommand = vsb.set)
    canvas.pack(side = LEFT, fill = BOTH, expand = True)
    vsb.pack(side = RIGHT, fill = Y)
    entryContainer = Frame(canvas)
    entryContainer.pack(fill = BOTH)

    frameDict = {}

    for i in range(0, N):
        frameDict[i] = Frame(entryContainer)
        frameDict[i].pack(fill = X)
        entry = IterEntryField(frameDict[i], 'Z value for subfile {}'.format(i+1))
        entry.pack()
    canvas.create_window(200, 0, window = entryContainer)
    canvas.config(scrollregion = (0,0,100,1000))
    notebook.pack(fill = X)

root = Tk()
button = Button(root, text = 'new window', command = notebookpopup)
button.pack()

root.mainloop()

I'm having three problems with this code:

  1. The pages are incredibly short, only showing a couple lines.

  2. I can't figure out the "proper" offset in create_window. I thought 0, 0 would place it in upper left corner of the canvas, but apparently the upper left corner of the window is taken instead. This could probably fixed by some reverse of the canvasx and canvasy methods, but I haven't been able to find any.

  3. The entry fields and labels are cramped together instead of taking up the entire width of the canvas. This wasn't a problem when I only used the notebook page frame as the container.


Solution

  • Your first problem goes back to how you pack your notebook. Simply change notebook.pack(...) to below:

    notebook.pack(fill="both", expand=True)
    

    The second one can be solved by specifying the anchor position in your create_window method:

    canvas.create_window(0, 0, window = entryContainer, anchor="nw")
    

    I don't understand what the 3rd problem is - it looks exactly as expected.