同时滚动多个Tkinter列表框

10

我有多个Tkinter列表框,使用单个滚动条进行滚动,但我也希望它们在任何一个列表框上进行鼠标滚轮活动时一起滚动。

如何实现这一点?

我的当前代码基于此处讨论的最后一个模式:http://effbot.org/tkinterbook/listbox.htm。 当仅使用滚动条时,它可以正常工作,但是当使用鼠标滚轮时,列表框会独立滚动。


如果可能的话,禁用每个列表框上的鼠标滚轮也是可以接受的。 - BobC
6个回答

16

我知道这个问题很老了,但我认为解决方案比这里提供的要简单一些。假设你总是希望列表框达成一致,那么以上两个答案甚至都不是完整的解决方案 - 使用箭头键更改选择会滚动一个列表框,而另一个列表框则不会。

所以,看着这两个答案,我问自己 - 他们为什么不挂钩yscrollcommand回调而只是直接将其发送到滚动条呢?因此,我就这样做了:

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()

14
以将两个小部件连接到单个滚动条的方式解决这个问题的方法基本上与你相同:创建自定义绑定来控制鼠标滚轮,使这些绑定影响两个列表框而不仅仅是一个。
唯一需要注意的是,根据平台的不同,鼠标滚轮接收不同的事件:Windows和Mac接收事件,Linux接收和事件。
以下是在我的Mac上使用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()

1
@BobC:如果您完全按照上面发布的代码使用,是的,您会看到那个问题。您有没有看到我说Ubuntu是Linux,并且Linux对于鼠标滚轮有不同的事件的部分? - Bryan Oakley
糟糕!在Linux上,鼠标滚轮使用事件<Button-4>和<Button-5>。为了更通用,我绑定了所有3个事件。 - BobC
2
我遇到了同样的问题。只是注意到了一件事情。我创建了一个列表框,并使用for循环将相同事件绑定到每个列表框上,在OnMouseWheel()内部再使用另一个for循环来迭代每个列表框。它没有使用event.delta,因为这些列表框从不同方向滚动,而并非我鼠标所在的方向。只需说-(event.delta)就可以解决这个问题。适用于Windows、Python 3.5.1。 - Milan Todorovic
这个不会对键盘箭头滚动做出反应。 - Danon
@Danon:当然可以。只需添加适当的绑定即可。这个问题不是关于使用箭头键的。 - Bryan Oakley
显示剩余2条评论

1

这是我的当前解决方案,编码为独立的函数(是的,它应该是一个对象)。

特点/要求:

  • 它可以处理任意数量的列表(最少1个)。
  • 所有列表目前必须具有相同的长度。
  • 每个列表框的宽度会根据内容进行调整。
  • 列表框可以使用鼠标滚轮或滚动条一起滚动。
  • 应该在Windows、OSX和Linux上工作,但只在Linux上进行了测试。

代码:

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()

欢迎提出改进建议!


刚刚注意到我硬编码了root.title:你可能想要不同的东西。 - BobC

0

我已经制作了一个非常简单的过程性解决方案。在查看教程点网站上有关如何为一个小部件使用滚动条的信息(https://www.tutorialspoint.com/python/tk_scrollbar.htm)后,我将其改编为同时滚动多个文本框(您可以更改代码以使用列表框)。此解决方案将在使用滚动条时更新所有三个文本框。

import tkinter as tk

HEIGHT = 200
WIDTH = 300

def scroll(x, y):
    l_textbox.yview(x,y)
    m_textbox.yview(x,y)
    r_textbox.yview(x,y)

root = tk.Tk()

canvas = tk.Canvas(root,height = HEIGHT, width = WIDTH, bg = "white")
canvas.pack()

frame = tk.Frame(root, bg ='white')
frame.place(relx=0,rely=0,relwidth=1,relheight=1)

scrollbar = tk.Scrollbar(frame)

l_label = tk.Label (frame, text = "Left")
l_label.place(relx=0, rely=0)

m_label = tk.Label (frame, text= "Middle")
m_label.place(relx=0.3, rely=0)

r_label = tk.Label (frame, text= "Right")
r_label.place(relx=0.6, rely=0)

l_textbox = tk.Text(frame, yscrollcommand = scrollbar.set)
l_textbox.config(font = ('Arial',9))
l_textbox.place(relx=0, rely=0.2,relwidth=0.3,relheight=0.8)

m_textbox = tk.Text(frame, yscrollcommand = scrollbar.set)
m_textbox.config(font = ('Arial',9))
m_textbox.place(relx=0.3, rely=0.2,relwidth=0.3,relheight=0.8)

r_textbox = tk.Text(frame, yscrollcommand = scrollbar.set)
r_textbox.config(font = ('Arial',9))
r_textbox.place(relx=0.6, rely=0.2,relwidth=0.3,relheight=0.8)

scrollbar.config( command = scroll)
scrollbar.place(relx = 0.9, relwidth = 0.1,relheight = 1)

for i in range(0, 100):
    l_textbox.insert(tk.INSERT, str(i)+"\n")
    m_textbox.insert(tk.INSERT, str(i)+"\n")
    r_textbox.insert(tk.INSERT, str(i)+"\n")
    l_textbox.place()
    m_textbox.place()
    r_textbox.place()

root.mainloop()

0
下面的列表是根据John K. Ousterhout的书《Tcl和Tk工具包-2009》第18.9.2节“多个小部件的同步滚动”中的Tcl代码示例实现的Python代码:
“不必直接连接滚动条和小部件。可以使用一个过程来完成一些操作,例如使用一个滚动条滚动多个小部件。实现准确的同步滚动的关键是识别一个主小部件,它将控制滚动条和从属小部件。”
import tkinter as tk
from tkinter import ttk

# Scrolling multiple listboxes together


class App:
    def __init__(self):
        self.root = tk.Tk()
        self.root.config(padx=10, pady=10)
        self.vsb = ttk.Scrollbar(command=self.scrollbar_command)
        self.lb1 = tk.Listbox(
            self.root, yscrollcommand=self.lb1_yscrollcommand)
        self.lb2 = tk.Listbox(
            self.root, yscrollcommand=self.lb2_yscrollcommand)
        self.vsb.pack(side="right", fill="y")
        self.lb1.pack(side="left", fill="both", expand=True)
        self.lb2.pack(side="left", fill="both", expand=True)
        # Generate some dummy data
        for i in range(100):
            self.lb1.insert("end", f"item lb1 {i}")
            self.lb2.insert("end", f"item lb2 {i}")
        self.root.mainloop()

    def lb1_yscrollcommand(self, *args):
        """Only one listbox should update the scrollbar.
        Listbox 'lb1' plays the role of the master listbox.
        It controls the scrollbar and the 'slave' listbox 'lb2' """

        print("lb1_yscrollcommand:", args)
        self.vsb.set(*args)  # update the scrollbar
        self.lb2.yview("moveto", args[0])  # update the 'slave' listbox

    def lb2_yscrollcommand(self, *args):
        """Slave listbox 'lb2' controls the master listbox 'lb1' """

        print("lb2_yscrollcommand:", args)
        self.lb1.yview("moveto", args[0])  # update the 'master' listbox

    def scrollbar_command(self, *args):
        """Scrollbar controls listboxes"""
        self.lb1.yview(*args)
        self.lb2.yview(*args)


app = App()


在Linux和Windows上进行了测试。

0

在一段时间前看过这个问题并不理解答案后,我现在已经制作并理解了自己的实现方法,使用鼠标滚轮,如下所示:

滚动的绑定参数在不同的操作系统之间是不同的。在Windows上是'<MouseWheel>',在Mac上是<'Button-4'>,在Linux上是<'Button-5'>。所以,首先你可以像这样找到用户的操作系统:

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.")

声明和绑定列表框小部件: 如果在小部件声明中没有放入exportselection = False参数,Tkinter只允许一个列表框在任何时候有一个选择。即使列表框可以一起滚动,用户仍然可以通过除鼠标滚轮以外的方式来破坏滚动同步。

  • <'B1-Leave'>允许用户通过点击鼠标并拖出小部件来滚动列表框。
  • <'ButtonRelease-1'>允许用户通过点击和拖动来更改列表框的次要选择。
  • <'Key'>允许用户使用箭头键滚动列表框。

为了保持滚动同步,这些事件必须被停止,这必须通过事件触发一个break语句来实现。即使事件被传递给另一个函数,如果它没有触发break,它仍然会继续执行。为了在绑定语句中中断一个事件,'break'必须是一个字符串。例如:

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 

重复这个步骤对所有的列表框进行操作,并创建一个你想要同步滚动的列表框列表。
listOfListboxes = [LB1, LB2, LB3, LB4, LB5]

然后,要滚动您的列表框,您可以根据您知道将要运行的操作系统进行操作,或者适应任何操作系统,就像这样(由于操作系统之间的滚动速度不同,需要进行不同的数学计算):
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')

此外,为了将所有列表框的选择合并为用户的一行,您可以使用以下函数:
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)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接