Python Tkinter: Unbinding Mouse Scroll Wheel on ComboBox

Question:

I have a combobox within a scrollable canvas frame- when I open the combobox and attempt to scroll through the options, the combobox and the entire window both scroll together. It would be nice to pause canvas scrolling while the combobox is open, but unbinding the mousewheel scroll from the combobox would also work.

Here is the scrollable canvas code:

root = Tk()
width=800
height=1020
root.geometry(str(width)+"x"+str(height)+"+10+10")

main_frame = Frame(root,width=width,height=height)
main_frame.place(x=0,y=0)
canvas = Canvas(main_frame, width=width, height=height)
canvas.place(x=0,y=0)
scrolly = ttk.Scrollbar(main_frame, orient=VERTICAL, command=canvas.yview)
scrolly.place(x=width-15,y=0,height=height)
canvas.configure(yscrollcommand=scrolly.set)
canvas.bind('<Configure>', lambda e: canvas.configure(scrollregion = canvas.bbox("all")))
def _on_mouse_wheel(event):
    canvas.yview_scroll(-1 * int((event.delta / 120)), "units")
canvas.bind_all("<MouseWheel>", _on_mouse_wheel)
w = Frame(canvas,width=width,height=height)
w.place(x=0,y=0)
canvas.create_window((0,0), window=w, anchor="nw")
w.configure(height=3000)

Here is the combobox initialization:

sel = Combobox(w, values=data)
sel.place(x=xval, y=yval)

I have tried unbinding the mousewheel for the combobox

sel.unbind_class("TCombobox", "<MouseWheel>") # windows

as well as rebinding it to an empty function

def dontscroll(event):
    return 'break'

sel.bind('<MouseWheel>', dontscroll)

but neither method worked.


I also attempted both methods in a separate test file (complete code):

from tkinter import *
from tkinter import ttk
from tkinter.ttk import Combobox

root = Tk()
root.geometry(str(300)+"x"+str(300)+"+10+10")

def dontscroll(event):
    return 'break'

sel = Combobox(root, values=[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20])
sel.place(x=10, y=10)
sel.unbind_class("TCombobox", "<MouseWheel>") # on windows
sel.bind('<MouseWheel>', dontscroll)

This still didn’t work. Any help is appreciated, thanks.

Asked By: Mongoose Man

||

Answers:

The reason is that you are binding all "<MouseWheel>" events with bind_all, you can simple just change it to bind, but then you will notice that it doesn’t work with scrolls on the canvas, that is because you are now binding to canvas but the scroll is actually(pretty counter intuitive) happening to w which is a Frame, so just bind to that widget instead:

w.bind("<MouseWheel>", _on_mouse_wheel)

And you can also remove all the unbind and bind related to sel as it is no longer needed.


On an unrelated note, the trick I used to find out which widget triggered the "<MouseWheel>" event could be useful in the future:

def _on_mouse_wheel(event):
    print(event.widget) # Print the widget that triggered the event
    canvas.yview_scroll(-1 * int((event.delta / 120)), "units")

Edit: This seems to work with multiple widget

def _on_mouse_wheel(event):
    if isinstance(event.widget, str): # String because it does not have an actual reference
        if event.widget.endswith('.!combobox.popdown.f.l'): # If it is the listbox
            return 'break'
    canvas.yview_scroll(-1 * int((event.delta / 120)), "units") # Else scroll the canvas
    w.event_generate('<Escape>') # Close combobox

root.bind_all("<MouseWheel>", _on_mouse_wheel)
Answered By: Delrius Euphoria

I’ve come up with a solution which will hopefully be helpful for anyone running into a similar issue:

Basically, comboboxes are composed of two subcomponents: an entry and a listbox. When binding to a combobox, it appears to bind the command solely to the entry and not to the listbox, which caused the issue at hand. I don’t know of way to access the subcomponents of a combobox instance and modify the listbox from there (drop an answer if you know how to do that), but I figured that I could bind the entire listbox class like so:

w.bind_class('Listbox', '<MouseWheel>', dontscroll)

And now, the canvas scrolls while the listboxes don’t. But we can do better:

Instead of binding the listbox class to an antiscrolling method, I decided to bind it to two other methods that work together to pause frame scrolling while the cursor is hovering over the listbox.

def dontscroll(e):
    return "dontscroll"

def on_enter(e):
    w.bind_all("<MouseWheel>", dontscroll)

def on_leave(e):
    w.bind_all("<MouseWheel>", _on_mouse_wheel)
    w.event_generate('<Escape>')

w.bind_class('Listbox', '<Enter>',
                       on_enter)
w.bind_class('Listbox', '<Leave>',
                       on_leave)

Now, the canvas only scrolls when the mouse is not hovering over a listbox widget.

complete code here

Answered By: Mongoose Man