0

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.

liamhp
  • 111
  • 1
  • 7

2 Answers2

1

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)
Delrius Euphoria
  • 14,910
  • 3
  • 15
  • 46
  • 1
    Great, that works perfectly. Still curious why rebinding and unbinding don't work even in the simple test case, but thanks. – liamhp Jul 20 '22 at 21:26
  • Actually, this solution (single bind) doesn't work quite as well as I had hoped. As one would expect, the Tkinter window doesn't scroll when the mouse is hovering over any element other than the empty frame itself. In some cases this might be okay, but if the window has large chunks of text it makes using the mousewheel to scroll spotty and difficult. So back to my original question, why am I not able to bind_all and then unbind a specific component such as a combobox? – liamhp Jul 21 '22 at 15:12
  • @MongooseMan I answered to the code in question. If you have any more question, you will have to provide a code because I understood very little. It is better to ask a new question though. Maybe to answer a part of your question, the scroll that's responsible for combobox seems to be triggered by a listbox widget within the combobox, that is not kept referenced by tkinter. Any unbinding has to be done to it and not the `sel`. But I cannot find a way to access it within tkinter. – Delrius Euphoria Jul 21 '22 at 16:03
-1

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

liamhp
  • 111
  • 1
  • 7
  • Seems to be a bug, scrolling with the dropdown open leads to the listbox remaining open and scrolling with it in its position. And why would the answer above this not work with this method – Delrius Euphoria Jul 22 '22 at 17:18
  • That's an issue with combo boxes themselves as the subcomponent listboxes do not seem to be statically linked to the entries and can move on scroll while the entries stay. However, this is very easy to work around by adding ` w.event_generate('') ` to the on_leave method, which closes the listbox when the mouse stops hovering over it. I already explained why the method you suggested earlier doesn't work- it doesn't let you scroll if your mouse hovers over any widget. – liamhp Jul 22 '22 at 18:00
  • Ah yes, I can see what you mean, check my edit for a more cleaner solution which when I tested with multiple widget, works. – Delrius Euphoria Jul 22 '22 at 19:06
  • Sure, that also works, and moving the escape event generation to the _on_mouse_wheel event is a bit smoother (though you don't need the extra check in there). Just updated the full program link accordingly. – liamhp Jul 22 '22 at 19:39
  • No, if there are no checks then scrolling inside the listbox wont work and it will jus close it immediately. You don't have to create 2 binds and then an unbind rather just one bind for everything is the more _real_ way IMO – Delrius Euphoria Jul 22 '22 at 20:55