[Python] Tkinter Canvas-based text widget: scrollbar drag does not scroll to bottom

Discussão em 'Python' iniciado por Stack, Maio 31, 2025.

  1. Stack

    Stack Membro Participativo

    I'm creating a custom text widget using tk.Canvas in Tkinter to display multiple lines of text, with support for mousewheel and scrollbar scrolling.

    What works:


    1. Mouse wheel scrolling scrolls through the full content correctly


    2. Clicking the scrollbar arrows or scrollbar trough jumps correctly to top/bottom

    What’s broken:

    1. When I drag the scrollbar thumb, it stops before reaching the bottom. The last few lines of content are unreachable until I manually click the scrollbar’s bottom area.

    Code:


    import tkinter as tk
    from tkinter import font

    class CustomText(tk.Canvas):
    def __init__(self, master, **kwargs):
    super().__init__(master, **kwargs)

    # Example content
    self.text_lines = [f"Line {i}" for i in range(100)]
    self.line_height = 20
    self.font = font.Font(family="Courier", size=12)
    self.scroll_offset = 0
    self.max_visible_lines = 1
    self.yscrollcommand = None

    self.bind("<Configure>", self.on_resize)
    self.bind("<MouseWheel>", self.on_mousewheel)
    self.bind("<Button-4>", self.on_mousewheel) # Linux scroll up
    self.bind("<Button-5>", self.on_mousewheel) # Linux scroll down

    def attach_scrollbar(self, scrollbar):
    self.yscrollcommand = scrollbar.set
    scrollbar.config(command=self.yview)
    self.update_scrollbars()

    def on_resize(self, event):
    self.width = event.width
    self.height = event.height
    self.max_visible_lines = self.height // self.line_height
    self.draw_text()

    def on_mousewheel(self, event):
    if hasattr(event, 'delta'):
    direction = -1 if event.delta > 0 else 1
    elif event.num == 4:
    direction = -1
    else:
    direction = 1

    scroll_amount = 1
    self.scroll_offset = max(0, min(
    self.scroll_offset + direction * scroll_amount,
    max(0, len(self.text_lines) - self.max_visible_lines)
    ))
    self.draw_text()

    def yview(self, *args):
    if args[0] == 'moveto':
    fraction = float(args[1])
    max_offset = max(0, len(self.text_lines) - self.max_visible_lines)
    self.scroll_offset = int(fraction * max_offset)
    elif args[0] == 'scroll':
    units = int(args[1])
    self.scroll_offset = max(0, min(self.scroll_offset + units, len(self.text_lines) - self.max_visible_lines))
    self.draw_text()

    def update_scrollbars(self):
    # Vertical
    total_lines = len(self.text_lines)
    if total_lines == 0:
    self.yscrollcommand(0, 1)
    else:
    top = self.scroll_offset / total_lines
    bottom = min(1.0, (self.scroll_offset + self.max_visible_lines) / total_lines)
    if self.yscrollcommand:
    self.yscrollcommand(top, bottom)


    def draw_text(self):
    self.delete("all")
    self.scroll_offset = max(0, min(self.scroll_offset, len(self.text_lines) - self.max_visible_lines))
    start = self.scroll_offset
    end = min(len(self.text_lines), start + self.max_visible_lines)

    for i in range(start, end):
    y = (i - start) * self.line_height
    self.create_text(5, y, anchor='nw', text=self.text_lines, font=self.font)

    self.update_scrollbars()


    # Create root window
    root = tk.Tk()
    root.geometry("400x300")

    # Frame to hold canvas and scrollbar
    frame = tk.Frame(root)
    frame.pack(fill="both", expand=True)

    # Create scrollbar
    scrollbar = tk.Scrollbar(frame)
    scrollbar.pack(side="right", fill="y")

    # Create CustomText widget
    text = CustomText(frame, bg="white")
    text.pack(side="left", fill="both", expand=True)

    # Attach scrollbar
    text.attach_scrollbar(scrollbar)

    root.mainloop()

    Continue reading...

Compartilhe esta Página