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