使用Python实现tkinter Listbox的拖放功能

5 投票
3 回答
9316 浏览
提问于 2025-04-17 13:21

有没有人能告诉我哪里可以找到关于制作一个可以拖放项目以重新排列的列表框的信息?我找到了一些与Perl相关的内容,但我对那种语言一无所知,而且我对tkinter也很陌生,所以看起来有点复杂。我知道怎么生成列表框,但我不太确定怎么通过拖放来重新排序。

3 个回答

1

下面这个类是一个带有 EXTENDED 选择模式的列表框,这种模式允许你拖动多个选中的项目。

  • 默认的选择方式仍然可以使用(通过拖动和点击,包括按住 Ctrl 或 Shift),唯一的例外是,如果你不按 Ctrl,就不能拖动已经选中的项目。
  • 要拖动选中的项目,可以把鼠标拖到最后一个选中的项目下面,或者第一个选中的项目上面。
  • 在拖动选中项目时,如果想要滚动列表框,可以使用鼠标滚轮,或者把光标移动到列表框的顶部或底部附近。 => 这里可以改进一下:因为这个功能绑定在 B1‑Motion 事件上,所以需要额外移动鼠标才能继续滚动。在长列表框中感觉有点不流畅。
  • 如果选中的项目不连续,拖动时会把未选中的项目向上或向下移动,使选中的项目变得连续。

以上意思是,如果你想单独拖动一个项目,首先需要选中它,然后再点击一次并拖动。

import tkinter as tk;

class ReorderableListbox(tk.Listbox):
    """ A Tkinter listbox with drag & drop reordering of lines """
    def __init__(self, master, **kw):
        kw['selectmode'] = tk.EXTENDED
        tk.Listbox.__init__(self, master, kw)
        self.bind('<Button-1>', self.setCurrent)
        self.bind('<Control-1>', self.toggleSelection)
        self.bind('<B1-Motion>', self.shiftSelection)
        self.bind('<Leave>',  self.onLeave)
        self.bind('<Enter>',  self.onEnter)
        self.selectionClicked = False
        self.left = False
        self.unlockShifting()
        self.ctrlClicked = False
    def orderChangedEventHandler(self):
        pass

    def onLeave(self, event):
        # prevents changing selection when dragging
        # already selected items beyond the edge of the listbox
        if self.selectionClicked:
            self.left = True
            return 'break'
    def onEnter(self, event):
        #TODO
        self.left = False

    def setCurrent(self, event):
        self.ctrlClicked = False
        i = self.nearest(event.y)
        self.selectionClicked = self.selection_includes(i)
        if (self.selectionClicked):
            return 'break'

    def toggleSelection(self, event):
        self.ctrlClicked = True

    def moveElement(self, source, target):
        if not self.ctrlClicked:
            element = self.get(source)
            self.delete(source)
            self.insert(target, element)

    def unlockShifting(self):
        self.shifting = False
    def lockShifting(self):
        # prevent moving processes from disturbing each other
        # and prevent scrolling too fast
        # when dragged to the top/bottom of visible area
        self.shifting = True

    def shiftSelection(self, event):
        if self.ctrlClicked:
            return
        selection = self.curselection()
        if not self.selectionClicked or len(selection) == 0:
            return

        selectionRange = range(min(selection), max(selection))
        currentIndex = self.nearest(event.y)

        if self.shifting:
            return 'break'

        lineHeight = 15
        bottomY = self.winfo_height()
        if event.y >= bottomY - lineHeight:
            self.lockShifting()
            self.see(self.nearest(bottomY - lineHeight) + 1)
            self.master.after(500, self.unlockShifting)
        if event.y <= lineHeight:
            self.lockShifting()
            self.see(self.nearest(lineHeight) - 1)
            self.master.after(500, self.unlockShifting)

        if currentIndex < min(selection):
            self.lockShifting()
            notInSelectionIndex = 0
            for i in selectionRange[::-1]:
                if not self.selection_includes(i):
                    self.moveElement(i, max(selection)-notInSelectionIndex)
                    notInSelectionIndex += 1
            currentIndex = min(selection)-1
            self.moveElement(currentIndex, currentIndex + len(selection))
            self.orderChangedEventHandler()
        elif currentIndex > max(selection):
            self.lockShifting()
            notInSelectionIndex = 0
            for i in selectionRange:
                if not self.selection_includes(i):
                    self.moveElement(i, min(selection)+notInSelectionIndex)
                    notInSelectionIndex += 1
            currentIndex = max(selection)+1
            self.moveElement(currentIndex, currentIndex - len(selection))
            self.orderChangedEventHandler()
        self.unlockShifting()
        return 'break'
4

如果你在使用 selectmode 设置为 MULTIPLE(而不是 SINGLE),这里有一个修改过的做法。

做了以下几个改动:

  1. 之前如果你拖动一个已经选中的项目,它会被取消选择,这样的用户体验不好。
  2. 当你点击一个已经选中的项目时,它会因为点击而取消选择。所以我添加了一个 self.curState 的标记,用来记录你点击的项目最开始是否被选中。这样在拖动时,它的状态就不会丢失。
  3. 我还把两个事件绑定到了 Button-1 事件上,使用了 add='+',不过这可能可以通过把所有内容放在 setCurrent 下来避免。
  4. 我更喜欢把 activestyle 设置为 'none'
  5. 把这个 Listbox 设置为 tk.MULTIPLE 而不是 tk.SINGLE

下面是代码:

class Drag_and_Drop_Listbox(tk.Listbox):
  """ A tk listbox with drag'n'drop reordering of entries. """
  def __init__(self, master, **kw):
    kw['selectmode'] = tk.MULTIPLE
    kw['activestyle'] = 'none'
    tk.Listbox.__init__(self, master, kw)
    self.bind('<Button-1>', self.getState, add='+')
    self.bind('<Button-1>', self.setCurrent, add='+')
    self.bind('<B1-Motion>', self.shiftSelection)
    self.curIndex = None
    self.curState = None
  def setCurrent(self, event):
    ''' gets the current index of the clicked item in the listbox '''
    self.curIndex = self.nearest(event.y)
  def getState(self, event):
    ''' checks if the clicked item in listbox is selected '''
    i = self.nearest(event.y)
    self.curState = self.selection_includes(i)
  def shiftSelection(self, event):
    ''' shifts item up or down in listbox '''
    i = self.nearest(event.y)
    if self.curState == 1:
      self.selection_set(self.curIndex)
    else:
      self.selection_clear(self.curIndex)
    if i < self.curIndex:
      # Moves up
      x = self.get(i)
      selected = self.selection_includes(i)
      self.delete(i)
      self.insert(i+1, x)
      if selected:
        self.selection_set(i+1)
      self.curIndex = i
    elif i > self.curIndex:
      # Moves down
      x = self.get(i)
      selected = self.selection_includes(i)
      self.delete(i)
      self.insert(i-1, x)
      if selected:
        self.selection_set(i-1)
      self.curIndex = i

示例演示:

root = tk.Tk()
listbox = Drag_and_Drop_Listbox(root)
for i,name in enumerate(['name'+str(i) for i in range(10)]):
  listbox.insert(tk.END, name)
  if i % 2 == 0:
    listbox.selection_set(i)
listbox.pack(fill=tk.BOTH, expand=True)
root.mainloop()
8

这是来自食谱 11.4的代码:

import Tkinter 

class DragDropListbox(Tkinter.Listbox):
    """ A Tkinter listbox with drag'n'drop reordering of entries. """
    def __init__(self, master, **kw):
        kw['selectmode'] = Tkinter.SINGLE
        Tkinter.Listbox.__init__(self, master, kw)
        self.bind('<Button-1>', self.setCurrent)
        self.bind('<B1-Motion>', self.shiftSelection)
        self.curIndex = None

    def setCurrent(self, event):
        self.curIndex = self.nearest(event.y)

    def shiftSelection(self, event):
        i = self.nearest(event.y)
        if i < self.curIndex:
            x = self.get(i)
            self.delete(i)
            self.insert(i+1, x)
            self.curIndex = i
        elif i > self.curIndex:
            x = self.get(i)
            self.delete(i)
            self.insert(i-1, x)
            self.curIndex = i

撰写回答