0

I am so confused (as you can see from my code which is currently a hodge-podge from various sources). I am trying to have a scrollable grid of widgets (currently tk.Entry's) that fill the frame when it is enlarged. Currently I only am trying to add a vertical scrollbar, but I intend to add a horizontal once I understand how it works.

I have two rows of headers in a 2-dimensional array of HeaderCol's. This is to manage merged cells in the header, which is more-or-less working. My ultimate goal is to have a spreadsheet-like interface but with controlled interactions via buttons, dropdowns, and Entry's in the grid.

Any suggestions would be most welcome!

from tkinter import Menu, Entry, Canvas, Frame, Tk, Label, Scrollbar


    class HeaderCol:

    MAX_COL = 0
    HEADERS = [[]]

    def __init__(self, name, row, col, width=1, header_row=0):
        if row == 0:
            HeaderCol.MAX_COL += col
        self.name = name
        self.row = row
        self.col = col
        self.width = width
        if len(HeaderCol.HEADERS) <= header_row:
            HeaderCol.HEADERS.append([])
        HeaderCol.HEADERS[header_row].append(self)


class TransactionsGrid:
    # create the header information
    HEADER_TOP_ROW = 0
    HEADER_SECOND_ROW = 1
    col = 0
    ENTRY = HeaderCol('Entry', HEADER_TOP_ROW, col, 2)
    col += ENTRY.width
    DUE = HeaderCol('Due', HEADER_TOP_ROW, col, 4)
    col += DUE.width
    PAID = HeaderCol('Paid', HEADER_TOP_ROW, col, 7)
    col += PAID.width
    TENANT_STATUS = HeaderCol('Tenant Status', HEADER_TOP_ROW, col)
    col += TENANT_STATUS.width
    MANAGEMENT = HeaderCol('Management', HEADER_TOP_ROW, col, 2)
    col += MANAGEMENT.width
    NET = HeaderCol('Net', HEADER_TOP_ROW, col)
    col += NET.width
    NOTES = HeaderCol('Notes', HEADER_TOP_ROW, col)
    HeaderCol.MAX_COL = col + NOTES.width

    col = 0
    DATE = HeaderCol('Date', HEADER_SECOND_ROW, col, 1, 1)
    col += DATE.width
    EVENT = HeaderCol('Event', HEADER_SECOND_ROW, col, 1, 1)
    col += EVENT.width
    FEES = HeaderCol('Fees & Charges', HEADER_SECOND_ROW, col, 1, 1)
    col += FEES.width

    def __init__(self, root):
        root.grid_rowconfigure(0, weight=1)
        root.columnconfigure(0, weight=1)

        self.frame_main = Frame(root)
        self.frame_main.grid(sticky="news")

        self.frame_canvas = Frame(self.frame_main)
        self.frame_canvas.grid(row=2, column=0, pady=(5, 0), sticky='nw')
        self.frame_canvas.rowconfigure(0, weight=1)
        self.frame_canvas.columnconfigure(0, weight=1)
        # self.frame_canvas.grid_propagate(False)
        canvas = Canvas(self.frame_canvas, bg="yellow")
        canvas.grid(row=0, column=0, sticky="news")

        # Link a scrollbar to the canvas
        vsb = Scrollbar(self.frame_canvas, orient="vertical", command=canvas.yview)
        vsb.grid(row=0, column=1, sticky='ns')
        canvas.configure(yscrollcommand=vsb.set)

        # Create a frame to contain the buttons
        frame_buttons = Frame(canvas, bg="blue")
        canvas.create_window((0, 0), window=frame_buttons, anchor='nw')
        self.frame_main.pack(fill='none', expand=False)
        canvas.place(relx=.5, rely=.5, anchor="center")
        self.buttons = []
        self.add_headers()

        # add a bunch of empty tk.Entry's
        for row in range(5):
            button_row = []

            for col in range(HeaderCol.MAX_COL):
                button = Entry(self.frame_canvas)
                button.configure(highlightthickness=0)
                button_row.append(button)
                if col == 0:
                    button.insert(0, 'A')

                button.grid(column=col, row=row + 1, sticky="")

            self.buttons.append(button_row)
        frame_buttons.update_idletasks()

    def add_headers(self):
        button_row = []

        for header in HeaderCol.HEADERS[0]:
            button = Label(self.frame_canvas)
            button_row.append(button)
            button.config(text=header.name)
            button.grid(column=header.col, row=0, columnspan=header.width, sticky="")

        # self.buttons.append(button_row)

        button_row = []

        for header in HeaderCol.HEADERS[1]:
            button = Label(self.frame_canvas)
            button_row.append(button)
            button.config(text=header.name)
            button.grid(column=header.col, row=1, columnspan=header.width, sticky="")

        # self.buttons.append(button_row)

    def menubar(self):
        menu = Menu(self.master)
        self.master.config(menu=menu)
        file_menu = Menu(menu)
        menu.add_cascade(label="File", menu=file_menu)

        edit_menu = Menu(menu)
        menu.add_cascade(label="Edit", menu=edit_menu)
        file_menu.add_command(label="Item")
        file_menu.add_command(label="Exit", command=self.exit_program)
        edit_menu.add_command(label="Undo")
        edit_menu.add_command(label="Redo")

    @staticmethod
    def exit_program():
        exit(0)


def main():
    root = Tk()
    # transaction_table = TransactionsTable(root)
    transaction_table = TransactionsGrid(root)
    #transaction_table = AnotherOne(root)

    """
    frame = tk.Frame(root)
    frame.pack()

    pt = Table(frame)
    pt.show()
    """
    root.mainloop()


if __name__ == '__main__':
    main()

Thanks to @Derek's suggestion, I have created the following class with an optional horizontal scrollbar. It more or less works, but when scrolling horizontally, it moves some of the grid of widgets, but not all widgets are seen on the right: enter image description here

self.frame_main = ScrollableFrame(root, horizontal=True)

Here is the modified code:

from tkinter import Frame, Scrollbar, Canvas
from tkinter.constants import RIGHT, Y, LEFT, BOTH, NW, BOTTOM, X

class ScrollableFrame(Frame):
    """
       Make a frame scrollable with scrollbar on the right.
       After adding or removing widgets to the scrollable frame,
       call the update() method to refresh the scrollable area.
    """

    def __init__(self, frame, width=16, horizontal=False):
        self.y_scrollbar = Scrollbar(frame, width=width)
        self.y_scrollbar.pack(side=RIGHT, fill=Y, expand=False)

        if horizontal:
            self.x_scrollbar = Scrollbar(frame, width=width, orient='horizontal')
            self.x_scrollbar.pack(side=BOTTOM, fill=X, expand=False)
            self.canvas = Canvas(frame,
                                 yscrollcommand=self.y_scrollbar.set,
                                 xscrollcommand=self.x_scrollbar.set)
        else:
            self.canvas = Canvas(frame, yscrollcommand=self.y_scrollbar.set)

        self.canvas.pack(side=LEFT, fill=BOTH, expand=True)

        if horizontal:
            self.canvas.pack(side=BOTTOM, fill=BOTH, expand=True)
            self.x_scrollbar.config(command=self.canvas.xview)

        self.y_scrollbar.config(command=self.canvas.yview)
        self.canvas.bind('<Configure>', self.__fill_canvas)

        # base class initialization
        Frame.__init__(self, frame)

        # assign this obj (the inner frame) to the windows item of the canvas
        self.windows_item = self.canvas.create_window(0, 0, window=self, anchor=NW)

    def __fill_canvas(self, event):
        """Enlarge the windows item to the canvas width"""
        canvas_width = event.width
        self.canvas.itemconfig(self.windows_item, width=canvas_width, height=event.height)

    def update(self):
        """Update the canvas and the scrollregion"""
        self.update_idletasks()
        self.canvas.config(scrollregion=self.canvas.bbox(self.windows_item))

Here is my cleaned up code that uses the ScrollableFrame:

def __init__(self, root):
    self.frame_main = ScrollableFrame(root, horizontal=True)
    self.buttons = [[]]
    self.add_headers()

    for row in range(25):
        button_row = []

        for col in range(HeaderCol.MAX_COL):
            button = Entry(self.frame_main)
            button.configure(highlightthickness=0)
            button_row.append(button)
            button.grid(column=col, row=row + 2, sticky="")

        self.buttons.append(button_row)
    self.frame_main.update()
jordanthompson
  • 888
  • 1
  • 12
  • 29
  • This link demonstrates the use of a scrollbar to access widgets. https://stackoverflow.com/questions/3085696/adding-a-scrollbar-to-a-group-of-widgets-in-tkinter – Derek Mar 03 '23 at 11:27
  • Thanks for the suggestion @Derek I was able to get a lot further with my layout and I now (think) I better understand how this works. I took the ScrollableFrame you pointed me to and added a horizontal scrollbar that doesn't quite work (see second half of code abovea) – jordanthompson Mar 03 '23 at 15:26
  • This horizontal scrollbar should be a lot easier that it is :-/ – jordanthompson Mar 03 '23 at 20:15

1 Answers1

0

This should help, it will create a list of Entry widgets with scrollbar. I've inserted commentary at relevant places. There are also a few refinements that might help.

import tkinter as tk

root = tk.Tk()
root.rowconfigure(0, weight = 1)
root.columnconfigure(0, weight = 1)

container = tk.Frame(root)
container.grid(row = 0, column = 0, sticky = tk.NSEW)
container.rowconfigure(0, weight = 1)
container.columnconfigure(0, weight = 1)

# NOTE: yscrollincrement must be defined for smooth scroll effects
canvas = tk.Canvas(container, yscrollincrement = 19, highlightthickness = 0)
canvas.grid(row = 0, column = 0, sticky = tk.NSEW)

scrollbar = tk.Scrollbar(
   root, orient = tk.VERTICAL, width = 14, command = canvas.yview)
scrollbar.grid(row = 0, column = 1, sticky = tk.NS)
canvas.configure(yscrollcommand = scrollbar.set)

# frame contained in canvas
frame = tk.Frame(canvas)
# dynamically change canvas scrollregion
frame.bind(
   "<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))

# canvas window widget contains frame
canvas.create_window((0, 0), window = frame, anchor = tk.NW)
wide, high = 20, 22 # Set width, height of Entry widgets
for i in range(20):

   K = tk.Entry(frame, width = wide, textvariable = tk.StringVar())
   K.grid(row = i, column = 0, sticky = tk.NSEW)

# use access[n].get() to retrieve text from Entry widgets
access = frame.children

root.geometry(f"{wide*7}x{high*7}")
root.minsize(wide*7, high*7)

root.mainloop()
Derek
  • 1,916
  • 2
  • 5
  • 15
  • FWIW, I changed to QT and it is SOOoooo much easier (less code, more intuitive) and it looks like a real GUI (at least on Windows) – jordanthompson Mar 04 '23 at 21:33