Scrolling multiple Tkinter listboxes together

Solve the problem pretty much the same way as you did to connect the two widgets to a single scrollbar: create custom bindings for the mousewheel and have those bindings affect both listboxes rather than just one.

The only real trick is knowing that you get different events for the mousewheel depending on the platform: windows and the Mac gets <MouseWheel> events, linux gets <Button-4> and <Button-5> events.

Here's an example, tested on my Mac with python 2.5:

import Tkinter as tk

class App:
    def __init__(self):
        self.root=tk.Tk()
        self.vsb = tk.Scrollbar(orient="vertical", command=self.OnVsb)
        self.lb1 = tk.Listbox(self.root, yscrollcommand=self.vsb.set)
        self.lb2 = tk.Listbox(self.root, yscrollcommand=self.vsb.set)
        self.vsb.pack(side="right",fill="y")
        self.lb1.pack(side="left",fill="x", expand=True)
        self.lb2.pack(side="left",fill="x", expand=True)
        self.lb1.bind("<MouseWheel>", self.OnMouseWheel)
        self.lb2.bind("<MouseWheel>", self.OnMouseWheel)
        for i in range(100):
            self.lb1.insert("end","item %s" % i)
            self.lb2.insert("end","item %s" % i)
        self.root.mainloop()

    def OnVsb(self, *args):
        self.lb1.yview(*args)
        self.lb2.yview(*args)

    def OnMouseWheel(self, event):
        self.lb1.yview("scroll", event.delta,"units")
        self.lb2.yview("scroll",event.delta,"units")
        # this prevents default bindings from firing, which
        # would end up scrolling the widget twice
        return "break"

app=App()

Here's my current solution, coded as a stand-alone function (yes, it should be an object).

Features/requirements:

  • It handles any number of lists (minimum 1).
  • All lists must presently have the same length.
  • The width of each listbox width is adjusted to match the content.
  • The listboxes scroll together using either the mouse wheel or the scrollbar.
  • Should work on Windows, OSX and Linux, but has been tested only on Linux.

Code:

def showLists(l, *lists):
    """
    Present passed equal-length lists in adjacent scrollboxes.
    """
    # This exists mainly for me to start learning about Tkinter.
    # This widget reqires at least one list be passed, and as many additional
    # lists as desired.  Each list is displayed in its own listbox, with
    # additional listboxes added to the right as needed to display all lists.
    # The width of each listbox is set to match the max width of its contents.
    # Caveat: Too wide or too many lists, and the widget can be wider than the screen!
    # The listboxes scroll together, using either the scrollbar or mousewheel.

    # :TODO: Refactor as an object with methods.
    # :TODO: Move to a separate file when other widgets are built.

    # Check arguments
    if (l is None) or (len(l) < 1):
        return
    listOfLists = [l]     # Form a list of lists for subsequent processing
    listBoxes = []  # List of listboxes
    if len(lists) > 0:
        for list in lists:
            # All lists must match length of first list
            # :TODO: Add tail filling for short lists, with error for long lists
            if len(list) != len(l):
                return
            listOfLists.append(list)

    import Tkinter

    def onVsb(*args):
        """
        When the scrollbar moves, scroll the listboxes.
        """
        for lb in listBoxes:
            lb.yview(*args)

    def onMouseWheel(event):
        """
        Convert mousewheel motion to scrollbar motion.
        """
        if (event.num == 4):    # Linux encodes wheel as 'buttons' 4 and 5
            delta = -1
        elif (event.num == 5):
            delta = 1
        else:                   # Windows & OSX
            delta = event.delta
        for lb in listBoxes:
            lb.yview("scroll", delta, "units")
        # Return 'break' to prevent the default bindings from
        # firing, which would end up scrolling the widget twice.
        return "break"

    # Create root window and scrollbar
    root = Tkinter.Tk()
    root.title('Samples w/ time step < 0')
    vsb = Tkinter.Scrollbar(root, orient=Tkinter.VERTICAL, command=onVsb)
    vsb.pack(side=Tkinter.RIGHT, fill=Tkinter.Y)

    # Create listboxes
    for i in xrange(0,len(listOfLists)):
        lb = Tkinter.Listbox(root, yscrollcommand=vsb.set)
        lb.pack(side=Tkinter.LEFT, fill=Tkinter.BOTH)
        # Bind wheel events on both Windows/OSX & Linux;
        lb.bind("<MouseWheel>", onMouseWheel)
        lb.bind("<Button-4>", onMouseWheel)
        lb.bind("<Button-5>", onMouseWheel)
        # Fill the listbox
        maxWidth = 0
        for item in listOfLists[i]:
            s = str(item)
            if len(s) > maxWidth:
                maxWidth = len(s)
            lb.insert(Tkinter.END, s)
        lb.config(width=maxWidth+1)
        listBoxes.append(lb)        # Add listbox to list of listboxes

    # Show the widget
    Tkinter.mainloop()
# End of showLists()

Suggestions for improvements are welcome!


I know this is pretty old, but I think the solution is a bit more simple than those proferred here. Assuming that you always want the listboxes to be in agreement, then the above two answers aren't even complete solutions - changing selection by means of the arrow keys will scroll one listbox but not the other.

So, looking at the answers, I asked - why aren't they hooking the yscrollcommand callback instead of just sending it straight to the scrollbar? So, I did just that:

try:
    from Tkinter import *
except ImportError:
    from tkinter import *


class MultipleScrollingListbox(Tk):

    def __init__(self):
        Tk.__init__(self)
        self.title('Scrolling Multiple Listboxes')

        #the shared scrollbar
        self.scrollbar = Scrollbar(self, orient='vertical')

        #note that yscrollcommand is set to a custom method for each listbox
        self.list1 = Listbox(self, yscrollcommand=self.yscroll1)
        self.list1.pack(fill='y', side='left')

        self.list2 = Listbox(self, yscrollcommand=self.yscroll2)
        self.list2.pack(expand=1, fill='both', side='left')

        self.scrollbar.config(command=self.yview)
        self.scrollbar.pack(side='right', fill='y')

        #fill the listboxes with stuff
        for x in xrange(30):
            self.list1.insert('end', x)
            self.list2.insert('end', x)

    #I'm sure there's probably a slightly cleaner way to do it than this
    #Nevertheless - whenever one listbox updates its vertical position,
    #the method checks to make sure that the other one gets updated as well.
    #Without the check, I *think* it might recurse infinitely.
    #Never tested, though.
    def yscroll1(self, *args):
        if self.list2.yview() != self.list1.yview():
            self.list2.yview_moveto(args[0])
        self.scrollbar.set(*args)

    def yscroll2(self, *args):
        if self.list1.yview() != self.list2.yview():
            self.list1.yview_moveto(args[0])
        self.scrollbar.set(*args)

    def yview(self, *args):
        self.list1.yview(*args)
        self.list2.yview(*args)


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