简体   繁体   中英

Tkinter Scrollbar on scrollable frame scrolls at 2x speed when it shouldn't

I'm trying to create a scrollable frame in Tkinter using windows, using the code found in this question . I also wanted to have the frame be scrollable using the mouse-wheel, and so I copied over the _on_mousewheel() function from this question . The issue now is that because the mouse-wheel is binded to the canvas, the on_mousewheel() function makes the scrollbar scroll at 2x it's normal speed when using the mouse-wheel over it. The answer in the second question mentioned this issue, but didn't explain how to solve it.

Code:

import tkinter as tk
from tkinter import *
from PIL import Image, ImageTk

SCROLL_CANVAS_H = 500
SCROLL_CANVAS_W = 1000

class ScrollbarFrame(tk.Frame):
    """
    Extends class tk.Frame to support a scrollable Frame 
    This class is independent from the widgets to be scrolled and 
    can be used to replace a standard tk.Frame
    """
    def __init__(self, parent, height, width):
        """ Initiates a scrollable frame with labels using specified
        height and width. Canvas is scrollable both over canvas and scrollbar.
        """

        super().__init__(parent)

        self.height = height
        self.width = width

        # Place the scrollbar on self, layout to the right
        self.v_scrollbar = tk.Scrollbar(self, orient="vertical")
        self.v_scrollbar.pack(side="right", fill="y")

        # The Canvas which supports the Scrollbar Interface, 
        # placed on self and layed out to the left.
        self.canvas = tk.Canvas(self, borderwidth=0, background="#ffffff", 
                                height = height, 
                                width = width)
        self.canvas.pack(side="left", fill="both", expand=True)

        # Attach scrollbar action to scroll of canvas
        self.canvas.configure(yscrollcommand=self.v_scrollbar.set)
        self.v_scrollbar.configure(command=self.canvas.yview)

        ## Allow canvas to be scrolled using mousewheel while hovering 
        ## over the canvas region.
        #self.canvas.bind_all("<MouseWheel>", self._on_mousewheel)

        # Place a frame on the canvas, this frame will hold the child widgets
        # All widgets to be scrolled have to use this Frame as parent
        self.scrolled_frame = tk.Frame(self.canvas, background=self.canvas.cget('bg'))
        self.canvas.create_window((0, 0), window=self.scrolled_frame, anchor="nw")
        
        self.canvas.bind_all('<MouseWheel>', self.on_mousewheel)

        # Configures the scrollregion of the Canvas dynamically
        self.scrolled_frame.bind("<Configure>", self.on_configure)

        # Reset the scroll region to encompass the inner frame
        self.scrolled_frame.bind("<Configure>", self.on_frame_configure)

    def on_configure(self, event):
        """Set the scroll region to encompass the scrolled frame"""
        self.canvas.configure(scrollregion=self.canvas.bbox("all"))
    
    def on_mousewheel(self, event):
        """Allows canvas to be scrolled using mousewheel while hovering
        over canvas.
        """
        self.canvas.yview_scroll(-1 * (event.delta // 120), "units")

    def on_frame_configure(self, event):
        """Reset the scroll region to encompass the inner frame"""
        self.canvas.configure(scrollregion=self.canvas.bbox("all"))


class App(tk.Tk):
    def __init__(self):
        #initializes self as root
        tk.Tk.__init__(self)

        # add a new scrollable frame
        sbf = ScrollbarFrame(self, height = 500, width = 1000)
        sbf.grid(row=0, column=0, sticky='nsew')
        #sbf.pack(side="top", fill="both", expand=True)

        # Some data, layout into the sbf.scrolled_frame
        frame = sbf.scrolled_frame
        for row in range(50):
            text = "%s" % row
            tk.Label(frame, text=text,
                     width=3, borderwidth="1", relief="solid") \
                .grid(row=row, column=0)

            text = "this is the second column for row %s" % row
            tk.Label(frame, text=text,
                     background=sbf.scrolled_frame.cget('bg')) \
                .grid(row=row, column=1)

            text = "this is the third column for row %s" % row
            tk.Label(frame, text=text,
                     background=sbf.scrolled_frame.cget('bg')) \
                .grid(row=row, column=2)

if __name__ == "__main__":
    root = App()
    root.mainloop()

In the on_mousewheel() method, I've tried identifying whether it was the canvas or the scrollbar that the mousewheel was used on, and then using an if statement so that the canvas will only scroll with the mousewheel is not over the scrollbar. But this doesn't, the scrollbar will still scroll at 2x speed when wheeling over it.

if event.widget != self.v_scrollbar:
            self.canvas.yview_scroll(-1 * (event.delta // 120), "units")

I've also tried dividing the movement of the scrollbar by a larger value

if event.widget == self.v_scrollbar: 
            self.canvas.yview_scroll(-1 * (event.delta // 240), "units")
        else:
            self.canvas.yview_scroll(-1 * (event.delta // 120), "units")

but this doesn't work either. Any ideas?

Instead of yview_scroll , use yview_moveto for finer control. The .02 value should probably be tied to a property (ex: scrollspeed) that can be adjusted on a case by case basis. There is no need to check if you are on the scrollbar or canvas with this method.

def on_mousewheel(self, event):
    """Allows canvas to be scrolled using mousewheel while hovering over canvas.
    """
    n = -event.delta / abs(event.delta)     #1 inversion
    p = self.v_scrollbar.get()[0] + (n*.02) #return scrollbar position and adjust it by a fraction
    self.canvas.yview_moveto(p)             #apply new position

This is a basic math problem. When you call self.canvas.yview_scroll , you are telling it how many units to scroll. units is dependent on the widget, and is the value of the yscrollincrement option. If the value of that option is 0, the value is treated as 1/10 the height of the widget.

The more pixels you scroll at any one time, the faster it will scroll. You just need to do basic math on the value being passed to the yview_scroll method and/or you can change the yscrollincrement value to a small integer > 0 to say exactly how many pixels one "unit" is.

Note that the event.delta value is not the same on all platforms. On OSX the value is the number of units you should scroll (ie: no math is required). On Windows systems the value is always at least 120, so you need to divide that value by 120 to get the number of actual "units" to scroll.

The bottom line is that you control the speed of the scrolling by varying the number passed to yview_scroll and/or the value of the yscrollincrement option of the widget you are scrolling.

If what you're saying is that the scrolling works correctly when over the canvas but scrolling at double speed over the scrollbar, that could be due to the built-in behavior of the scrollbar. What might be happening is that your function runs causing it to scroll, and then the default behavior runs which causes it to scroll again. You can either check that your function only runs when over the canvas, or you can have your function return the string "break" which prevents the default binding from happening.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM