Search code examples
user-interfacetkinterscrolltkinter-canvas

Doing a kind of Excel scrollable table with tkinter


I want to display a table (variable number of rows) with tkinter where:

  • The size of the window is adjsuted to the content
  • Vertical scrolling is proposed if the there are too many rows
  • Vertical scrolling can be done with the wheelmouse inside the table (as in Excel)
  • Copying the value of one cell is possible (right click on the cell)

This post (Adding a scrollbar to a group of widgets in Tkinter) is helpful to understand scrolling with tkinter...but doesn't fulfill with all requirements above.


Solution

  • The main tricks are:

    • Only few tkinter objects are scrollables (Canvas are scrollable)
    • Inside the canvas, don't use grid or pack (you would loose scrollability) but the method dedicated to canvas called 'create_window'
    • Inside the window of your canvas create a frame...like that inside your frame you can well use pack/grid methods.
    • Resize the canvas to the content....each time content is added into the frame (<Configure> event)
    • The way mousewheel event is caught depends on your OS (in the example below event.delta management is for windows)
    • Well break any mousewheel event in the scrollbar (to not conflict with mousewheel event you programmed)

    Here a solution example:

    # Standard libraries
    import tkinter as tk
    
    
    class ResultWindow(tk.Tk):
    
        def __init__(self):
    
            # Initialize the GUI
            super().__init__()
    
            # Create the main widgets
            self.canvas = tk.Canvas(self)
            self.vsb = tk.Scrollbar(self, orient="vertical", width=25)
            self.frame = tk.Frame(self.canvas)
    
            # Add them to the toplevel layout
            self.canvas.pack(side="left", fill="y")
            self.vsb.pack(side="right", fill="y")
            self.canvas.create_window((0, 0), window=self.frame, anchor="nw")
    
            # Bind events to the widgets
            self.canvas.bind_all("<MouseWheel>", self.on_mousewheel)
            self.vsb.bind("<MouseWheel>", lambda event: "break")
            self.frame.bind("<Configure>", self.on_frame_configure)
    
            # Link Canvas scroll with Scrollbar
            self.canvas.configure(yscrollcommand=self.vsb.set)
            self.vsb.configure(command=self.canvas.yview)
    
            # Add the frame to define the way
            self.l_cell = None
    
            # Add arbitrary content (to create our table)
            self.add_content(200, 5)
            
        def add_content(self, rows, cols):
            
            # Declare functions to manage code labels highlighting
            set_color = lambda event: event.widget.config(bg="grey")
            reset_color = lambda event: event.widget.config(bg="SystemButtonFace")
            
            for col in range(cols):
                for row in range(rows):
                    # Add the cells to the grid
                    text = f"row: {row}, col: {col}"
                    self.l_cell = tk.Label(self.frame, text=text, relief="solid")
                    self.l_cell.grid(row=row, column=col, sticky="nsew")
                    
                    # Bind each cell to events
                    self.l_cell.bind("<Enter>", set_color)
                    self.l_cell.bind("<Leave>", reset_color)
                    self.l_cell.bind("<Button-3>", self.on_click)
                    
        def on_frame_configure(self, event):
            # Resize the canvas to fit with the content
            self.canvas.config(width=self.canvas.bbox("all")[2],
                               height=self.canvas.bbox("all")[3])
    
            # Adjust the scrollable scope to the new size of the canvas
            self.canvas.configure(scrollregion=self.canvas.bbox("all"))
    
        def on_mousewheel(self, event):
            # Update the scroll according to the mousewheel scroll size
            self.canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
    
        def on_click(self, event):
            # Clear the clipboard
            self.clipboard_clear()
            # Get the Label widget code and add it to the clipboard
            self.clipboard_append(event.widget.cget("text"))
            
    app = ResultWindow()
    app.mainloop()