tkinter: binding mousewheel to scrollbar
Asked Answered
N

12

71

I have this scroll-able frame (frame inside canvas actually).

import Tkinter as tk
class Scrollbarframe():
    def __init__(self, parent,xsize,ysize,xcod,ycod):
        def ScrollAll(event):
                canvas1.configure(scrollregion=canvas1.bbox("all"),width=xsize,height=ysize,bg='white')
        self.parent=parent
        self.frame1=tk.Frame(parent,bg='white')
        self.frame1.place(x=xcod,y=ycod)
        canvas1=tk.Canvas(self.frame1)
        self.frame2=tk.Frame(canvas1,bg='white',relief='groove',bd=1,width=1230,height=430)
        scrollbar1=tk.Scrollbar(self.frame1,orient="vertical",command=canvas1.yview)
        canvas1.configure(yscrollcommand=scrollbar1.set)
        scrollbar1.pack(side="right",fill="y")
        canvas1.pack(side="left")
        canvas1.create_window((0,0),window=self.frame2,anchor='nw')
        self.frame2.bind("<Configure>",ScrollAll)

I would like to bind mouse wheel to the scrollbar so that user can scroll down the frame without having to use arrow buttons on the scrollbar. After looking around, i added a binding to my canvas1 like this

self.frame1.bind("<MouseWheel>", self.OnMouseWheel)

This is the function:

def OnMouseWheel(self,event):
    self.scrollbar1.yview("scroll",event.delta,"units")
    return "break" 

But the scroll bar won't move when i use mousewheel. Can anyone help me with this? All i want is when the user use mousewheel (inside the frame area/on the scrollbar), the canvas should automatically scroll up or down.

Northeasterly answered 28/6, 2013 at 1:33 Comment(0)
C
121

Perhaps the simplest solution is to make a global binding for the mousewheel. It will then fire no matter what widget is under the mouse or which widget has the keyboard focus. You can then unconditionally scroll the canvas, or you can be smart and figure out which of your windows should scroll.

For example, on windows you would do something like this:

self.canvas = Canvas(...)
self.canvas.bind_all("<MouseWheel>", self._on_mousewheel)
...
def _on_mousewheel(self, event):
    self.canvas.yview_scroll(int(-1*(event.delta/120)), "units")

Note that self.canvas.bind_all is a bit misleading -- you more correctly should call root.bind_all but I don't know what or how you define your root window. Regardless, the two calls are synonymous.

Platform differences:

  • On Windows, you bind to <MouseWheel> and you need to divide event.delta by 120 (or some other factor depending on how fast you want the scroll)
  • on OSX, you bind to <MouseWheel> and you need to use event.delta without modification
  • on X11 systems you need to bind to <Button-4> and <Button-5>, and you need to divide event.delta by 120 (or some other factor depending on how fast you want to scroll)

There are more refined solutions involving virtual events and determining which window has the focus or is under the mouse, or passing the canvas window reference through the binding, but hopefully this will get you started.

EDIT: In newer Python versions, canvas.yview_scroll requires an integer (see : pathName yview scroll number what

Cepheus answered 3/7, 2013 at 20:47 Comment(9)
I tried to use this (linux here) but couldnt make it work, until I noticed that - I wonder why - event.delta was always zero. Solved it by calling simply yview_scroll(direction,"units")Debunk
@Bryan Oakley - The above works fine if there's only one scrolling canvas in the app. But if there are two or more, how can you restrict the scrolling to one or the other?Lickspittle
@JDM: You can use winfo_containing to figure out which canvas is under the cursor, and then scroll that canvas.Cepheus
@BryanOakley: OK, I think I understand. I went at it from a different direction, using the widget's <Enter> and <Leave> events to fire .bind_all and .unbind calls. The real hassle was figuring out why Tkinter accepted a callback for the .bind_all but complained that it needed a string instead for the .unbind. (I'd already ruled out a global unbind or unbind_all because I didn't want to foul up other bindings that might exist.) Anyway, after MUCH searching I finally found an article that showed the proper string syntax: mail.python.org/pipermail//tkinter-discuss/2012-May/003152.htmlLickspittle
I found a lot of value in this one-liner based on your answer with minor modification: self.canvas.bind_all('<MouseWheel>', lambda event: self.canvas.yview_scroll(int(-1*(event.delta/120)), "units")). Using Python 3.4, if the number isn't casted as an int, interpreter throws _tkinter.TclError: expected integer but got "1.0".Unchartered
@Debunk where is direction coming from? What is it?Noellenoellyn
@vpap: the direction comes from event.delta which will either be a positive or negative number.Cepheus
@Noellenoellyn - right Bryan, in my old code it was simply +1 or -1Debunk
Please note that this solution won't work if different frames use the same code (only one will work). I recommend using this approach instead.Shorn
A
61

Based on @BryanOakley's answer, here is a way to scroll only the focused widget (i.e. the one you have mouse cursor currently over).

Bind to <Enter> and <Leave> events happening on your scrollable frame which sits inside a canvas, the following way (scrollframe is the frame that is inside the canvas):

    ...

    self.scrollframe.bind('<Enter>', self._bound_to_mousewheel)
    self.scrollframe.bind('<Leave>', self._unbound_to_mousewheel)

    return None

def _bound_to_mousewheel(self, event):
    self.canv.bind_all("<MouseWheel>", self._on_mousewheel)

def _unbound_to_mousewheel(self, event):
    self.canv.unbind_all("<MouseWheel>")

def _on_mousewheel(self, event):
    self.canv.yview_scroll(int(-1*(event.delta/120)), "units")
Actualize answered 16/6, 2016 at 11:50 Comment(5)
This should be the chosen answer, as it provides a targeted approach which most applications will need.Healthy
Thank you, this methodology worked for me. My scrollable frame was being scrolled even if the mouse wasn't hovering above it, which was screwing up my treeview scrolls in the column next to it which has its own builtin scrolling.Kakaaba
Excellent. Worked for me.Handler
As Bryan mentioned above, you can always parse the event and decide whether to process or ignore. The event includes a widget item, so you can say if event.widget in list_of_widgets_to_process:Hinch
Excellent (worked for me). An obvious improvement to this (but not asked for) would be to do the same with horizontal scrollbar, if there were one (it was useful to me). Just add a binding to the "<Shift-MouseWheel>" in the _bound_to_mousewheel method and add another method for this event.Kingsize
B
16

This link gives you an example as to how to use the scrollwheel.

http://www.daniweb.com/software-development/python/code/217059/using-the-mouse-wheel-with-tkinter-python

I hope this helps!

# explore the mouse wheel with the Tkinter GUI toolkit
# Windows and Linux generate different events
# tested with Python25
import Tkinter as tk
def mouse_wheel(event):
    global count
    # respond to Linux or Windows wheel event
    if event.num == 5 or event.delta == -120:
        count -= 1
    if event.num == 4 or event.delta == 120:
        count += 1
    label['text'] = count
count = 0
root = tk.Tk()
root.title('turn mouse wheel')
root['bg'] = 'darkgreen'
# with Windows OS
root.bind("<MouseWheel>", mouse_wheel)
# with Linux OS
root.bind("<Button-4>", mouse_wheel)
root.bind("<Button-5>", mouse_wheel)
label = tk.Label(root, font=('courier', 18, 'bold'), width=10)
label.pack(padx=40, pady=40)
root.mainloop()
Bate answered 3/7, 2013 at 15:35 Comment(2)
Good, working example. Just replace Tkinter with tkinter on Py3Perdita
If this link were to go down, this answer would be useless. "Always quote the most relevant part of an important link, in case the target site is unreachable or goes permanently offline." Please edit your question to avoid this.Coach
V
13

To get rid of the weird factor 120 we could just look at the sign of the event.delta value. This makes it easy to use the same handler under Windows, Linux and Mac OS.

# Mouse wheel handler for Mac, Windows and Linux
# Windows, Mac: Binding to <MouseWheel> is being used
# Linux: Binding to <Button-4> and <Button-5> is being used

def MouseWheelHandler(event):
    global count

    def delta(event):
        if event.num == 5 or event.delta < 0:
            return -1 
        return 1 

    count += delta(event)
    print(count)

import tkinter
root = tkinter.Tk()
count = 0
root.bind("<MouseWheel>",MouseWheelHandler)
root.bind("<Button-4>",MouseWheelHandler)
root.bind("<Button-5>",MouseWheelHandler)
root.mainloop()
Voltz answered 6/1, 2017 at 12:38 Comment(0)
H
3

As an addendum to the above, the "delta" scaling factor is easy to calculate, since platform information is available through the sys and platform modules (and possibly others).

def my_mousewheel_handler(event):
    if sys.platform == 'darwin': # for OS X # also, if platform.system() == 'Darwin':
        delta = event.delta
    else:                            # for Windows, Linux
        delta = event.delta // 120   # event.delta is some multiple of 120
    if event.widget in (widget1, widget2, ):
        'do some really cool stuff...'
Hinch answered 19/12, 2020 at 14:24 Comment(0)
T
2

In case you are interested

How to scroll 2 listbox at the same time

#listbox scrollbar

from tkinter import *
root = Tk()

def scrolllistbox2(event):
    listbox2.yview_scroll(int(-1*(event.delta/120)), "units")


scrollbar = Scrollbar(root)
#scrollbar.pack(side=RIGHT, fill=Y)
listbox = Listbox(root)
listbox.pack()
for i in range(100):
    listbox.insert(END, i)
# attach listbox to scrollbar
listbox.config(yscrollcommand=scrollbar.set)
listbox.bind("<MouseWheel>", scrolllistbox2)

listbox2 = Listbox(root)
listbox2.pack()
for i in range(100):
    listbox2.insert(END, i+100)
listbox2.config(yscrollcommand=scrollbar.set)

#scrollbar.config(command=listbox.yview)

root.mainloop()

Or...

from tkinter import *
root = Tk()
root.geometry("400x400")
def scrolllistbox(event):
    ''' scrolling both listbox '''
    listbox2.yview_scroll(int(-1*(event.delta/120)), "units")
    listbox1.yview_scroll(int(-1*(event.delta/120)), "units")


def random_insert():
    ''' adding some numbers to the listboxes '''
    for i in range(100):
        listbox1.insert(END, i)
        listbox2.insert(END, i + 100)

# SCROLLBAR
scrollbar = Scrollbar(root)
#scrollbar.pack(side=RIGHT, fill=Y)

# LISTBOX 1
listbox1 = Listbox(root)
listbox1.pack()
# attach listbox to scrollbar with yscrollcommand
# listbox1.config(yscrollcommand=scrollbar.set)

# The second one
listbox2 = Listbox(root)
listbox2.pack()
listbox2.config(yscrollcommand=scrollbar.set)
# scroll the first one when you're on the second one
# listbox2.bind("<MouseWheel>", scrolllistbox)
root.bind("<MouseWheel>", scrolllistbox)

# scroll also the second list when you're on the first
listbox1.bind("<MouseWheel>", scrolllistbox)

random_insert()
#scrollbar.config(command=listbox.yview)

root.mainloop()
Trooper answered 19/8, 2019 at 7:56 Comment(0)
G
2

This approach worked for me on Linux:

canvas.bind('<Button-4>', lambda e: canvas.yview_scroll(int(-1*e.num), 'units'))
canvas.bind('<Button-5>', lambda e: canvas.yview_scroll(int(e.num), 'units'))
Gertie answered 10/4, 2023 at 22:48 Comment(0)
B
0

Mikhail T.'s answer worked really well for me. Here is perhaps a more generic set up that others might find useful (I really need to start giving things back)

def _setup_mousewheel(self,frame,canvas):
    frame.bind('<Enter>', lambda *args, passed=canvas: self._bound_to_mousewheel(*args,passed))
    frame.bind('<Leave>', lambda *args, passed=canvas: self._unbound_to_mousewheel(*args,passed))

def _bound_to_mousewheel(self, event, canvas):
    canvas.bind_all("<MouseWheel>", lambda *args, passed=canvas: self._on_mousewheel(*args,passed))

def _unbound_to_mousewheel(self, event, canvas):
    canvas.unbind_all("<MouseWheel>")

def _on_mousewheel(self, event, canvas):
    canvas.yview_scroll(int(-1*(event.delta/120)), "units")

Then setting a canvas/frame up for mousewheel scrolling is just:

self._setup_mousewheel(frame, canvas)
Baku answered 4/4, 2022 at 13:52 Comment(0)
U
0
def onmousewheel(widget, command):
    widget.bind("<Enter>", lambda _: widget.bind_all('<MouseWheel>',command ))
    widget.bind("<Leave>", lambda _: widget.unbind_all('<MouseWheel>'))

onmousewheel(canvas, lambda e:  canvas.yview_scroll(int(-1*(e.delta)), "units"))

Compact solution for just scroll frame which you want.

Thanks for everyone who share their solution.

Unexampled answered 5/2, 2023 at 9:1 Comment(0)
S
0
self.canvas = Canvas(...)
self.canvas.bind_all("<MouseWheel>", lambda event: self._on_mousewheel(where_to_scroll= (-1 * event.delta)))
...
def _on_mousewheel(self, where_to_scroll):
        self.canvas.yview("scroll", where_to_scroll, "units")

bind_all means = <"the thing that you want to target when happening">, then do something()

1- we want to target mouse wheel = <"MouseWheel">

2 - we want to scroll to where the mouse wheel goes when you use the mouse wheel = lambda event: self._on_mousewheel(where_to_scroll=(-1 * event.delta))

when we use bind_all or bind methods they gives us a variable. Because that, we use lambda to take it and use it inside the function

that variable has attributes one of these attributes is delta and it gives an integer value of the direction of the mouse wheel

i multiply it by -1 because delta gives me wrong direction and I think that because my macOS system, try it if gives you wrong direction multiply it by -1 like me

"UPDATE"

I create a frame inside the canvas and I use bind on the new frame instead bind_all on canvas because there was something wrong before which it every time I used the mouse wheel it call _on_mousewheel() method even if I don't inside the canvas. So creating a frame and using bind solve this issue for me

like this tutorial - https://www.youtube.com/watch?v=0WafQCaok6g

Selenium answered 2/8, 2023 at 4:2 Comment(0)
M
0

One person in the comments above has had the problem that MouseWheel event.delta is not populated. That is the same problem I am having and therefore, the scroll can only be incremented in unit steps. While this does work, it is simply too slow for my application.

As a workaround for this, I was forced to take the following measures:

# mouse wheel callback for scrolling
...
self.prevtime = 0
self.sliceinc = 0
...
def mousewheel(self,event,key=None):

    if key is None:
        # if events are separated by < 100 msec, accumulate and return
        if abs(event.time-self.prevtime) < 100:
            if event.num == 4:
                self.sliceinc += 1
            elif event.num == 5:
                self.sliceinc -= 1
            self.prevtime = event.time
            if abs(self.sliceinc) == 5:
                self.canvas.get_tk_widget().event_generate('<<MyMouseWheel>>',when="tail",x=event.x,y=event.y)
            return
        # otherwise, just process the event and update gui
        else:
            self.prevtime = event.time
            if event.num == 4:
                self.sliceinc = 1
            elif event.num == 5:
                self.sliceinc = -1

    newslice = self.currentslice.get() + self.sliceinc

    self.currentslice.set(newslice)
    self.doGuiUpdates()

    # when the virtual event callback finally happens, re-initialize
    if key == 'Key':
        self.prevtime = 0
        self.sliceinc = 0

where the binding is as follows:

    self.canvas.get_tk_widget().bind('<<MyMouseWheel>>',EventCallback(self.mousewheel,key='Key'))

and EventCallback is a convenience class for passing kwargs to a callback:

class EventCallback():
    def __init__(self, callback, *args, **kwargs):
        self.callback = callback
        self.args = args
        self.kwargs = kwargs

    def __call__(self,event):
        return self.callback(event,*self.args, **self.kwargs)

In short, the MouseWheel events are accumulated without updating the GUI, and a virtual event is generated to run after all the pending MouseWheel events have been handled, which applies the accumulated total as an update to the GUI all in one step.

Membership answered 23/11, 2023 at 20:58 Comment(0)
I
0

I was experimenting and got this easy way around. Just as we use Up and Down bind like

SCROLL_STEP = 100

self.window.bind("<Down>", self.scroll_down)
self.window.bind("<Up>", self.scroll_up)

def scroll_down(self, e):
    self.scroll += SCROLL_STEP
    self.draw()

def scroll_up(self, e):
    if self.scroll > 0:
        self.scroll -= SCROLL_STEP
        self.draw()

We can use same in case of Mousescroll like this

# Linux: Binding to <Button-4> and <Button-5> is being used
self.canvas.bind('<Button-4>', self.scroll_up)
self.canvas.bind('<Button-5>', self.scroll_down) 

def scroll_down(self, e):
    self.scroll += SCROLL_STEP
    self.draw()

def scroll_up(self, e):
    if self.scroll > 0:
        self.scroll -= SCROLL_STEP
        self.draw()

self.scroll = 0 in starting.

Inpatient answered 14/3 at 18:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.