After looking at this question a while ago and not understanding the answers, I have now made and understood my own implementation of this problem with the mouse wheel, as below:
The bind argument for scrolling is different between operating systems. On Windows it is '<MouseWheel>'
, on Mac it is <'Button-4'>
, and on Linux it is <'Button-5'>
. So, first you can find the operating system of the user like this:
from sys import platform
global OS
if platform == 'linux' or platform == 'linux2':
OS = 'Linux'
elif platform == 'darwin':
OS = 'Mac'
elif platform == 'win32' or platform == 'win64':
OS = 'Windows'
else:
raise Exception("An error occurred in determining user's operating system.")
Declaring and binding listbox widgets:
Tkinter will only allow one listbox to have a selection at any time if the exportselection = False
argument is not put into the widget declaration. Even if the listboxes can scroll together, there are still ways the user can mess up the scroll synchronization by scrolling through means other than the mouse wheel.
<'B1-Leave'>
allows the user to scroll listboxes by clicking the mouse and dragging out of the widget.
<'ButtonRelease-1'>
allows the user to change the secondary selection of the listbox by clicking and dragging
<'Key'>
allows the user to scroll listboxes with the arrow keys
These events have to be stopped to maintain scroll synchronization, which has to be done by the event hitting a break
statement. Even if the event is passed to another function, if it doesn't hit a break
, it will still proceed. To break off an event in a bind statement, 'break'
must be a string. For example:
LB1 = Listbox(parent, bg = White, height = 20, width = 1, relief = 'sunken', exportselection = False, selectmode = 'single') #Width = 1 note on line 75
LB1.grid(row = 0, column = 0)
LB1.bind('<<ListboxSelect>>', lambda event: SearchListboxClick(event, listOfListboxes, LB1))
LB1.bind('<MouseWheel>', lambda event: CombinedListboxScroll(event, listOfListboxes))
LB1.bind('<Button-4>', lambda event: CombinedListboxScroll(event, listOfListboxes))
LB1.bind('<Button-5>', lambda event: CombinedListboxScroll(event, listOfListboxes))
LB1.bind('<B1-Leave>', lambda event: 'break') #Stops listbox scrolling by clicking mouse and dragging
LB1.bind('<ButtonRelease-1>', lambda event: 'break') #Stops listbox secondary selection being changed by mouse click and drag
LB1.bind('<Key>', lambda event: 'break') #Stops arrow keys from
Repeat this for all listboxes, and create a list of the listboxes you want to scroll synchronously:
listOfListboxes = [LB1, LB2, LB3, LB4, LB5]
Then, to scroll your listboxes, you can either do so for whatever operating system you know you will be running on, or accomodating for any operating system, like this (the scroll speeds are different between operating systems, necessitating the different math):
def CombinedListboxScroll(event, listOfListboxes):
'''Takes list of listboxes and scrolls all of them simultaneously.'''
if OS == 'Windows':
for lb in listOfListboxes:
lb.yview_scroll(int(-1 * (event.delta/120)), 'units')
elif OS == 'Mac':
for lb in listOfListboxes:
lb.yview_scroll(int(-1 * event.delta), 'units')
else: #OS == Linux
for lb in listOfListboxes:
lb.yview_scroll(int(-1 * (event.delta/120)), 'units')
Also, to combine the selection of all the listboxes into one row for the user, you could use a function like this:
def CombinedListboxSelect(event, listOfListboxes, boxClicked):
'''Takes a list of listboxes, and sets their selection to an index clicked by the user in boxClicked.'''
if boxClicked.curselection() != (): #To stop blank tuples erroring the function. I don't know why they are passed sometimes
selectedIndex = (boxClicked.curselection())[0] #Parenthesis have to be like this
boxClicked.selection_set(selectedIndex)
for listbox in listOfListboxes:
listbox.selection_clear(0, END) #If this is not done, the last selected item will stay selected on top of the new one also being selected.
listbox.selection_set(selectedIndex)