I am not really a programmer. I asked an AI to generate a matrix of buttons, with rows and columns having text-labels on top and left side of the window. I wanted the matrix to be scrollable but the labels on top/left should always remain visible. The code the AI gave me is actually doing what I described (sort of), but the scrolling somehow displaces the positions of labels and buttons, so that they are not really on the right spot if you scroll too much. It seems to me that the sizes of the label-texts and buttons are not exactly the same size? Does any one have an idea what is this not working well in the code of AI (oh, and also when I run the script, first nothing is visible on the main window!):
import tkinter as tk
class ScrollableMatrix:
def __init__(self, root, rows=20, cols=30):
self.root = root
self.rows = rows
self.cols = cols
self.font = ('TkDefaultFont', 10) # Common font for all elements
# Create main container
self.container = tk.Frame(root)
self.container.pack(fill="both", expand=True)
# Create dummy widget to calculate sizes
self._init_size_constants()
# Create canvas widgets
self.top_header_canvas = tk.Canvas(self.container, height=self.row_height)
self.left_header_canvas = tk.Canvas(self.container, width=self.col_width)
self.main_canvas = tk.Canvas(self.container)
# Create scrollbars
self.h_scroll = tk.Scrollbar(self.container, orient="horizontal")
self.v_scroll = tk.Scrollbar(self.container, orient="vertical")
# Grid layout configuration
self.top_header_canvas.grid(row=0, column=1, sticky="ew")
self.left_header_canvas.grid(row=1, column=0, sticky="ns")
self.main_canvas.grid(row=1, column=1, sticky="nsew")
self.v_scroll.grid(row=1, column=2, sticky="ns")
self.h_scroll.grid(row=2, column=1, sticky="ew")
# Configure grid weights
self.container.grid_rowconfigure(1, weight=1)
self.container.grid_columnconfigure(1, weight=1)
# Create internal frames
self.top_header_frame = tk.Frame(self.top_header_canvas)
self.left_header_frame = tk.Frame(self.left_header_canvas)
self.main_frame = tk.Frame(self.main_canvas)
# Add frames to canvases
self.top_header_canvas.create_window((0, 0), window=self.top_header_frame, anchor="nw")
self.left_header_canvas.create_window((0, 0), window=self.left_header_frame, anchor="nw")
self.main_canvas.create_window((0, 0), window=self.main_frame, anchor="nw")
# Configure scroll commands
self.main_canvas.configure(
xscrollcommand=self._sync_main_x,
yscrollcommand=self._sync_main_y
)
self.h_scroll.configure(command=self._sync_h_scroll)
self.v_scroll.configure(command=self._sync_v_scroll)
# Bind configuration events
self.main_frame.bind("<Configure>", self._on_main_configure)
self.top_header_frame.bind("<Configure>", self._on_top_header_configure)
self.left_header_frame.bind("<Configure>", self._on_left_header_configure)
# Create matrix elements
self._create_labels_and_buttons()
# Configure uniform column/row sizes
self._configure_uniform_sizing()
def _init_size_constants(self):
"""Calculate size constants using dummy widgets"""
dummy_frame = tk.Frame(self.root)
# Create dummy button
dummy_btn = tk.Button(dummy_frame, text="X", font=self.font,
width=10, height=2)
dummy_btn.grid(row=0, column=0)
# Create dummy label
dummy_lbl = tk.Label(dummy_frame, text="X", font=self.font,
width=10, height=2)
dummy_lbl.grid(row=1, column=0)
# Force geometry calculation
dummy_frame.update_idletasks()
# Get dimensions
self.col_width = dummy_btn.winfo_width()
self.row_height = dummy_btn.winfo_height()
# Verify label matches button size
lbl_width = dummy_lbl.winfo_width()
lbl_height = dummy_lbl.winfo_height()
if lbl_width != self.col_width or lbl_height != self.row_height:
self.col_width = max(self.col_width, lbl_width)
self.row_height = max(self.row_height, lbl_height)
dummy_frame.destroy()
def _create_labels_and_buttons(self):
# Create column headers
for j in range(self.cols):
lbl = tk.Label(self.top_header_frame, text=f"Col {j+1}",
font=self.font, width=10, height=2,
relief="ridge", borderwidth=2, bg="red")
lbl.grid(row=0, column=j, sticky="nsew")
# Create row headers
for i in range(self.rows):
lbl = tk.Label(self.left_header_frame, text=f"Row {i+1}",
font=self.font, width=10, height=2,
relief="ridge", borderwidth=2, bg="red")
lbl.grid(row=i, column=0, sticky="nsew")
# Create buttons in main grid
for i in range(self.rows):
for j in range(self.cols):
btn = tk.Button(self.main_frame, text=f"({i+1},{j+1})",
font=self.font, width=10, height=2,
relief="groove", borderwidth=2)
btn.grid(row=i, column=j, sticky="nsew")
def _configure_uniform_sizing(self):
"""Set uniform column widths and row heights"""
for j in range(self.cols):
self.main_frame.columnconfigure(j, minsize=self.col_width, weight=1)
self.top_header_frame.columnconfigure(j, minsize=self.col_width, weight=1)
for i in range(self.rows):
self.main_frame.rowconfigure(i, minsize=self.row_height, weight=1)
self.left_header_frame.rowconfigure(i, minsize=self.row_height, weight=1)
def _sync_h_scroll(self, *args):
self.main_canvas.xview(*args)
self.top_header_canvas.xview(*args)
def _sync_v_scroll(self, *args):
self.main_canvas.yview(*args)
self.left_header_canvas.yview(*args)
def _sync_main_x(self, first, last):
self.h_scroll.set(first, last)
self.top_header_canvas.xview_moveto(first)
def _sync_main_y(self, first, last):
self.v_scroll.set(first, last)
self.left_header_canvas.yview_moveto(first)
def _on_main_configure(self, event):
self.main_canvas.configure(scrollregion=self.main_canvas.bbox("all"))
def _on_top_header_configure(self, event):
self.top_header_canvas.configure(scrollregion=self.top_header_canvas.bbox("all"))
def _on_left_header_configure(self, event):
self.left_header_canvas.configure(scrollregion=self.left_header_canvas.bbox("all"))
if __name__ == "__main__":
root = tk.Tk()
root.title("Uniform Size Matrix")
root.geometry("800x600")
matrix = ScrollableMatrix(root, rows=50, cols=50)
root.mainloop()
I improved your code and solved the scrolling issue. I also improved performance. I made a numerous changes and added new features. If I go to write them all, it will occupy a lot of space. However, if you require, I could provide the list of changes.
Here is the full code:
import tkinter as tk
class OptimizedScrollableMatrix:
def __init__(self, root, rows=20, cols=30):
self.root = root
self.rows = rows
self.cols = cols
self.font = ('TkDefaultFont', 10)
# Pre-calculate cell dimensions
self._calculate_cell_dimensions()
# Create main container
self.container = tk.Frame(root)
self.container.pack(fill="both", expand=True)
# Create canvas widgets with optimized settings
self.top_header_canvas = tk.Canvas(
self.container,
height=self.row_height,
highlightthickness=0,
bg='#f0f0f0'
)
self.left_header_canvas = tk.Canvas(
self.container,
width=self.col_width,
highlightthickness=0,
bg='#f0f0f0'
)
self.main_canvas = tk.Canvas(
self.container,
highlightthickness=0,
bg='white'
)
# Create scrollbars
self.h_scroll = tk.Scrollbar(self.container, orient="horizontal")
self.v_scroll = tk.Scrollbar(self.container, orient="vertical")
# Grid layout configuration
self.top_header_canvas.grid(row=0, column=1, sticky="ew")
self.left_header_canvas.grid(row=1, column=0, sticky="ns")
self.main_canvas.grid(row=1, column=1, sticky="nsew")
self.v_scroll.grid(row=1, column=2, sticky="ns")
self.h_scroll.grid(row=2, column=1, sticky="ew")
# Configure grid weights
self.container.grid_rowconfigure(1, weight=1)
self.container.grid_columnconfigure(1, weight=1)
# Configure scroll commands
self._setup_scrolling()
# Create virtual grid
self._create_virtual_grid()
# Initial drawing
self._draw_visible_area()
# Bind events
self._bind_events()
def _calculate_cell_dimensions(self):
"""Calculate cell dimensions without creating actual widgets"""
# Create temporary frame to calculate sizes
temp = tk.Frame(self.root)
temp.grid_propagate(False)
# Create a test label with typical content
test_label = tk.Label(
temp,
text="Sample",
font=self.font,
width=10,
height=2,
relief="ridge"
)
test_label.grid(row=0, column=0)
# Force geometry calculation
temp.update_idletasks()
# Store dimensions
self.col_width = test_label.winfo_width()
self.row_height = test_label.winfo_height()
temp.destroy()
def _setup_scrolling(self):
"""Configure all scrolling relationships"""
self.main_canvas.configure(
xscrollcommand=self._sync_main_x,
yscrollcommand=self._sync_main_y
)
self.h_scroll.configure(command=self._sync_h_scroll)
self.v_scroll.configure(command=self._sync_v_scroll)
# Configure headers to follow main canvas
self.top_header_canvas.configure(xscrollcommand=self._sync_header_x)
self.left_header_canvas.configure(yscrollcommand=self._sync_header_y)
def _sync_h_scroll(self, *args):
"""Horizontal scroll synchronization"""
self.main_canvas.xview(*args)
self.top_header_canvas.xview(*args)
self._draw_visible_area()
def _sync_v_scroll(self, *args):
"""Vertical scroll synchronization"""
self.main_canvas.yview(*args)
self.left_header_canvas.yview(*args)
self._draw_visible_area()
def _sync_main_x(self, first, last):
"""Sync main canvas horizontal scrolling with scrollbar"""
self.h_scroll.set(first, last)
self.top_header_canvas.xview_moveto(first)
def _sync_main_y(self, first, last):
"""Sync main canvas vertical scrolling with scrollbar"""
self.v_scroll.set(first, last)
self.left_header_canvas.yview_moveto(first)
def _sync_header_x(self, first, last):
"""Sync header horizontal scrolling"""
self.main_canvas.xview_moveto(first)
def _sync_header_y(self, first, last):
"""Sync header vertical scrolling"""
self.main_canvas.yview_moveto(first)
def _create_virtual_grid(self):
"""Create a virtual representation of the grid without actual widgets"""
# Calculate total size needed
self.total_width = self.cols * self.col_width
self.total_height = self.rows * self.row_height
# Configure canvas scrolling regions
self.main_canvas.configure(
scrollregion=(0, 0, self.total_width, self.total_height)
)
self.top_header_canvas.configure(
scrollregion=(0, 0, self.total_width, self.row_height)
)
self.left_header_canvas.configure(
scrollregion=(0, 0, self.col_width, self.total_height)
)
# Draw headers (these are static)
self._draw_headers()
def _draw_headers(self):
"""Draw the column and row headers with red background"""
# Column headers
for col in range(self.cols):
x = col * self.col_width + self.col_width // 2
self.top_header_canvas.create_rectangle(
col * self.col_width, 0,
(col + 1) * self.col_width, self.row_height,
outline="black", fill="red" # Red background
)
self.top_header_canvas.create_text(
x, self.row_height // 2,
text=f"Col {col+1}",
font=self.font,
fill="black"
)
# Row headers
for row in range(self.rows):
y = row * self.row_height + self.row_height // 2
self.left_header_canvas.create_rectangle(
0, row * self.row_height,
self.col_width, (row + 1) * self.row_height,
outline="black", fill="red" # Red background
)
self.left_header_canvas.create_text(
self.col_width // 2, y,
text=f"Row {row+1}",
font=self.font,
fill="black"
)
def _draw_visible_area(self):
"""Draw only the visible portion of the grid"""
# Clear existing items (except headers)
self.main_canvas.delete("cell")
# Get visible area
x_start = int(self.main_canvas.canvasx(0))
y_start = int(self.main_canvas.canvasy(0))
# Calculate visible columns and rows
visible_width = self.main_canvas.winfo_width()
visible_height = self.main_canvas.winfo_height()
x_end = x_start + visible_width + self.col_width
y_end = y_start + visible_height + self.row_height
first_col = max(0, x_start // self.col_width)
last_col = min(self.cols - 1, x_end // self.col_width)
first_row = max(0, y_start // self.row_height)
last_row = min(self.rows - 1, y_end // self.row_height)
# Draw only visible cells
for row in range(first_row, last_row + 1):
for col in range(first_col, last_col + 1):
x1 = col * self.col_width
y1 = row * self.row_height
x2 = x1 + self.col_width
y2 = y1 + self.row_height
# Create cell rectangle
self.main_canvas.create_rectangle(
x1, y1, x2, y2,
outline="gray", fill="white",
tags="cell"
)
# Create cell text
self.main_canvas.create_text(
x1 + self.col_width // 2,
y1 + self.row_height // 2,
text=f"({row+1},{col+1})",
font=self.font,
tags="cell"
)
def _bind_events(self):
"""Set up all necessary event bindings"""
# Bind canvas scrolling
self.main_canvas.bind("<Configure>", self._on_canvas_configure)
# Bind mouse wheel for scrolling
self.main_canvas.bind_all("<MouseWheel>", self._on_mouse_wheel)
self.main_canvas.bind_all("<Shift-MouseWheel>", self._on_shift_mouse_wheel)
def _on_canvas_configure(self, event):
"""Handle canvas resize"""
self._draw_visible_area()
def _on_mouse_wheel(self, event):
"""Handle vertical scrolling with mouse wheel"""
self.main_canvas.yview_scroll(-1 * (event.delta // 120), "units")
self._draw_visible_area()
def _on_shift_mouse_wheel(self, event):
"""Handle horizontal scrolling with shift+mouse wheel"""
self.main_canvas.xview_scroll(-1 * (event.delta // 120), "units")
self._draw_visible_area()
if __name__ == "__main__":
root = tk.Tk()
root.title("Optimized Scrollable Matrix with Red Headers")
root.geometry("800x600")
# Create with large grid - will still load quickly
matrix = OptimizedScrollableMatrix(root, rows=100, cols=100)
root.mainloop()
Output: