Reputation: 2167
Can anyone point me to where I can find info on making a listbox with the ability to drag and drop items for re-arranging? I've found some related to Perl, but I know nothing of that language and I'm pretty new to tkinter, so it was pretty confusing. I know how to generate listboxes, but I'm not sure how to re-order it through drag and drop.
Upvotes: 5
Views: 10279
Reputation: 1604
The following class is a Listbox with EXTENDED
selection mode that enables dragging around multiple selected items.
B1‑Motion
event, extra movement of the mouse is needed to continue the scroll. Feels buggy in longer listboxes.The above means that to drag just one item, it needs to be selected first, then clicked again and dragged.
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'
Upvotes: 2
Reputation: 18953
Here's a modified recipe if you're dealing with MULTIPLE
as the selectmode
(as opposed to SINGLE
).
Changes made:
self.curState
bit that kept track of whether the clicked-on item's state was initially selected or not. When you drag it around, it doesn't lose its state.Button-1
event using add='+'
but that might avoidable by simply keeping it all under setCurrent
.activestyle
equals 'none'
.Listbox
tk.MULTIPLE
instead of tk.SINGLE
.Here is the code:
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
Example demo:
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()
Upvotes: 5
Reputation: 9899
Here is the code from Recipe 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
Upvotes: 8