pythonpython-3.xuser-interfacetkinter

Same size of text-labels and buttons


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

Solution

  • 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
            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
            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")
    
            self.container.grid_rowconfigure(1, weight=1)
            self.container.grid_columnconfigure(1, weight=1)
    
            # Setup scrolling
            self._setup_scrolling()
    
            # Create grid structure
            self._create_virtual_grid()
    
            # Draw initial content
            self._draw_visible_area()
    
            # Setup event bindings
            self._bind_events()
    
        def _calculate_cell_dimensions(self):
            temp = tk.Frame(self.root)
            temp.grid_propagate(False)
            test_label = tk.Label(temp, text="Sample", font=self.font, width=10, height=2, relief="ridge")
            test_label.grid(row=0, column=0)
            temp.update_idletasks()
            self.col_width = test_label.winfo_width()
            self.row_height = test_label.winfo_height()
            temp.destroy()
    
        def _setup_scrolling(self):
            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)
            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):
            self.main_canvas.xview(*args)
            self.top_header_canvas.xview(*args)
            self._draw_visible_area()
    
        def _sync_v_scroll(self, *args):
            self.main_canvas.yview(*args)
            self.left_header_canvas.yview(*args)
            self._draw_visible_area()
    
        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 _sync_header_x(self, first, last):
            self.main_canvas.xview_moveto(first)
    
        def _sync_header_y(self, first, last):
            self.main_canvas.yview_moveto(first)
    
        def _create_virtual_grid(self):
            self.total_width = self.cols * self.col_width
            self.total_height = self.rows * self.row_height
            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))
            self._draw_headers()
    
        def _draw_headers(self):
            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"
                )
                self.top_header_canvas.create_text(x, self.row_height // 2, text=f"Col {col+1}", font=self.font)
    
            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"
                )
                self.left_header_canvas.create_text(self.col_width // 2, y, text=f"Row {row+1}", font=self.font)
    
        def _draw_visible_area(self):
            self.main_canvas.delete("cell")
            x_start = int(self.main_canvas.canvasx(0))
            y_start = int(self.main_canvas.canvasy(0))
            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)
    
            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
    
                    tag = f"cell_{row}_{col}"
    
                    rect_id = self.main_canvas.create_rectangle(
                        x1, y1, x2, y2, outline="gray", fill="white", tags=("cell", tag)
                    )
                    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", tag)
                    )
    
                    # Hover bindings on the group tag (rectangle + text)
                    self.main_canvas.tag_bind(tag, "<Enter>", lambda e, r=tag: self._on_hover_enter(r))
                    self.main_canvas.tag_bind(tag, "<Leave>", lambda e, r=tag: self._on_hover_leave(r))
    
    
        def _on_hover_enter(self, tag):
            items = self.main_canvas.find_withtag(tag)
            for item in items:
                if self.main_canvas.type(item) == "rectangle":
                    self.main_canvas.itemconfig(item, fill="red")
    
        def _on_hover_leave(self, tag):
            items = self.main_canvas.find_withtag(tag)
            for item in items:
                if self.main_canvas.type(item) == "rectangle":
                    self.main_canvas.itemconfig(item, fill="white")
    
    
        def _bind_events(self):
            self.main_canvas.bind("<Configure>", self._on_canvas_configure)
            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):
            self._draw_visible_area()
    
        def _on_mouse_wheel(self, event):
            self.main_canvas.yview_scroll(-1 * (event.delta // 120), "units")
            self._draw_visible_area()
    
        def _on_shift_mouse_wheel(self, event):
            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")
        root.geometry("800x600")
        matrix = OptimizedScrollableMatrix(root, rows=100, cols=100)
        root.mainloop()
    

    Output:

    enter image description here