问题
I'm trying to add a constraint to the QTreeWidget drag and drop function to prevent the branches from entering another branch in another root.
Here's an example to make things more clear:
I have 4 objects. Lets call them apple, banana, carrot, durian.
The tree looks like this:
isDelicious (Root)
|-- BackgroundObjects (Branch)
|-- Durian
|-- ForgroundObjects (Branch)
|-- Apple
|-- Banana
|-- Carrot
isSmelly (Root)
|-- BackgroundObjects (Branch)
|-- Apple
|-- Carrot
|-- ForgroundObjects (Branch)
|-- Banana
|-- Durian
So, the objects are allowed to be dragged and dropped from BackgroundObjects to ForgroundObjects, and visa versa on the same root, but they are not allowed to be dragged and dropped onto a branch on a different root.
I have tried reimplementing and subclassing dragMoveEvent, dragEnterEvent, and dropEvent, and if I call accept on the event in dragEnterEvent, it'll call dragMoveEvent after (which I expect). However, dropEvent is only called when I drop outside of the QTreeWidget.
What I want to do is check the grandparent of the selected objects before they are moved, and the proposed new grandparent to see if they are the same. If so, then accept the move. Otherwise ignore the move.
I have searched to see if there is any answers, and so far I haven't seen any for what I'm trying to do. Probably the closest would be these two questions from Stack Overflow:
https://stackoverflow.com/questions/17134289/managing-drag-and-drop-within-qtreewidgets-in-pyside
qt: QTreeView - limit drag and drop to only happen within a particlar grandparent (ancestor)
回答1:
Qt does not seem to make this sort of thing very easy.
The best I could come up with was to temporarily reset the item flags during the drag-enter and drag-move events. The example below calculates the current top-level item dynamically in order to contrain drag and drop. But it could also be done by using setData()
to add an identifier to each item.
from PyQt4 import QtCore, QtGui
class TreeWidget(QtGui.QTreeWidget):
def __init__(self, parent=None):
QtGui.QTreeWidget.__init__(self, parent)
self.setDragDropMode(self.InternalMove)
self.setDragEnabled(True)
self.setDropIndicatorShown(True)
self._dragroot = self.itemRootIndex()
def itemRootIndex(self, item=None):
root = self.invisibleRootItem()
while item is not None:
item = item.parent()
if item is not None:
root = item
return QtCore.QPersistentModelIndex(
self.indexFromItem(root))
def startDrag(self, actions):
items = self.selectedItems()
self._dragroot = self.itemRootIndex(items and items[0])
QtGui.QTreeWidget.startDrag(self, actions)
def dragEnterEvent(self, event):
self._drag_event(event, True)
def dragMoveEvent(self, event):
self._drag_event(event, False)
def _drag_event(self, event, enter=True):
items = []
disable = False
item = self.itemAt(event.pos())
if item is not None:
disable = self._dragroot != self.itemRootIndex(item)
if not disable:
rect = self.visualItemRect(item)
if event.pos().x() < rect.x():
disable = True
if disable:
for item in item, item.parent():
if item is not None:
flags = item.flags()
item.setFlags(flags & ~QtCore.Qt.ItemIsDropEnabled)
items.append((item, flags))
if enter:
QtGui.QTreeWidget.dragEnterEvent(self, event)
else:
QtGui.QTreeWidget.dragMoveEvent(self, event)
for item, flags in items:
item.setFlags(flags)
class Window(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.tree = TreeWidget(self)
self.tree.header().hide()
def add(root, *labels):
item = QtGui.QTreeWidgetItem(self.tree, [root])
item.setFlags(item.flags() &
~(QtCore.Qt.ItemIsDragEnabled |
QtCore.Qt.ItemIsDropEnabled))
for index, title in enumerate(
('BackgroundObjects', 'ForegroundObjects')):
subitem = QtGui.QTreeWidgetItem(item, [title])
subitem.setFlags(
subitem.flags() & ~QtCore.Qt.ItemIsDragEnabled)
for text in labels[index].split():
child = QtGui.QTreeWidgetItem(subitem, [text])
child.setFlags(
child.flags() & ~QtCore.Qt.ItemIsDropEnabled)
add('isDelicious', 'Durian', 'Apple Banana Carrot')
add('isSmelly', 'Apple Carrot', 'Banana Durian')
root = self.tree.invisibleRootItem()
root.setFlags(root.flags() & ~QtCore.Qt.ItemIsDropEnabled)
self.tree.expandAll()
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.tree)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.setGeometry(500, 300, 300, 300)
window.show()
sys.exit(app.exec_())
回答2:
Here's my solution (full code at the end), subclassing a QTreeWidget
. I tried to have something very general that should work for a lot of cases. One issue remains with the visual cues when dragging. The previous version didn't work on windows, I hope this one will. It works absolutely fine on Linux.
Defining categories
Every item in the tree has a category (a string), that I stored in QtCore.Qt.ToolTipRole
. You could also subclass QTreeWidgetItem
to have a specific attribute category
.
We define in a dictionary settings
all the categories, with the list of the categories they can be drop into and the flag to set. For example:
default=QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsEnabled
drag=QtCore.Qt.ItemIsDragEnabled
drop=QtCore.Qt.ItemIsDropEnabled
settings={
"family":(["root"],default|drag|drop),
"children":(["family"],default|drag)
}
Every item of category "family" can receive drag, and can only be drop in "root" (the invisible root item). Every item of category "children" can only be drop into a "family".
Adding items to the tree
The method addItem(strings,category,parent=None)
creates a QTreeWidgetItem(strings,parent)
with a tool tip "category" and the matching flags in setting
. It returns the item. Example:
dupont=ex.addItem(["Dupont"],"family")
robert=ex.addItem(["Robertsons"],"family")
ex.addItem(["Laura"],"children",dupont)
ex.addItem(["Matt"],"children",robert)
...
Reimplementation of Drag and Drop
The item being dragged is determined with self.currentItem()
(multiple selection is not handled). The list of categories where this item can be dropped is okList=self.settings[itemBeingDragged.data(0,role)][0]
.
The item under the mouse, aka "drop target", is self.itemAt(event.pos())
. If the mouse in on blank space, the drop target is set to the root item.
dragMoveEvent
(visual cue for whether the drop will be accepted/ignored)
If the drop target is inokList
, we call the regulardragMoveEvent
. If not, we have to check for "next to drop target". In the image bellow, the item under the mouse is Robertsons, but the real drop target is the root item (see the line bellow Robertsons ?). To fix this, we check it the item can be dragged on the parent of the drop target. If not, we callevent.ignore()
.The only remaining issue is when the mouse is actually on "Robertsons": the drag event is accepted. The visual cue says the drop will be accepted when it's not.
dropEvent
Instead of accepting or ignoring the drop, which is very tricky because of "next to drop target", we always accept the drop, and then fix mistakes.
If the new parent is the same as the old parent, or if it is inokList
, we do nothing. Otherwise, we put back the dragged item in the old parent.Sometimes the dropped item will be collapsed, but this could easily be fixed with
itemBeingDragged.setExpanded()
Finally, the full code with two examples:
import sys
from PyQt4 import QtCore, QtGui
class CustomTreeWidget( QtGui.QTreeWidget ):
def __init__(self,settings, parent=None):
QtGui.QTreeWidget.__init__(self, parent)
#self.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)
self.setItemsExpandable(True)
self.setAnimated(True)
self.setDragEnabled(True)
self.setDropIndicatorShown(True)
self.setDragDropMode(QtGui.QAbstractItemView.InternalMove)
self.settings=settings
root=self.invisibleRootItem()
root.setData(0,QtCore.Qt.ToolTipRole,"root")
def dragMoveEvent(self, event):
role=QtCore.Qt.ToolTipRole
itemToDropIn = self.itemAt(event.pos())
itemBeingDragged=self.currentItem()
okList=self.settings[itemBeingDragged.data(0,role)][0]
if itemToDropIn is None:
itemToDropIn=self.invisibleRootItem()
if itemToDropIn.data(0,role) in okList:
super(CustomTreeWidget, self).dragMoveEvent(event)
return
else:
# possible "next to drop target" case
parent=itemToDropIn.parent()
if parent is None:
parent=self.invisibleRootItem()
if parent.data(0,role) in okList:
super(CustomTreeWidget, self).dragMoveEvent(event)
return
event.ignore()
def dropEvent(self, event):
role=QtCore.Qt.ToolTipRole
#item being dragged
itemBeingDragged=self.currentItem()
okList=self.settings[itemBeingDragged.data(0,role)][0]
#parent before the drag
oldParent=itemBeingDragged.parent()
if oldParent is None:
oldParent=self.invisibleRootItem()
oldIndex=oldParent.indexOfChild(itemBeingDragged)
#accept any drop
super(CustomTreeWidget,self).dropEvent(event)
#look at where itemBeingDragged end up
newParent=itemBeingDragged.parent()
if newParent is None:
newParent=self.invisibleRootItem()
if newParent.data(0,role) in okList:
# drop was ok
return
else:
# drop was not ok, put back the item
newParent.removeChild(itemBeingDragged)
oldParent.insertChild(oldIndex,itemBeingDragged)
def addItem(self,strings,category,parent=None):
if category not in self.settings:
print("unknown categorie" +str(category))
return False
if parent is None:
parent=self.invisibleRootItem()
item=QtGui.QTreeWidgetItem(parent,strings)
item.setData(0,QtCore.Qt.ToolTipRole,category)
item.setExpanded(True)
item.setFlags(self.settings[category][1])
return item
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
default=QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsEnabled|QtCore.Qt.ItemIsEditable
drag=QtCore.Qt.ItemIsDragEnabled
drop=QtCore.Qt.ItemIsDropEnabled
#family example
settings={
"family":(["root"],default|drag|drop),
"children":(["family"],default|drag)
}
ex = CustomTreeWidget(settings)
dupont=ex.addItem(["Dupont"],"family")
robert=ex.addItem(["Robertsons"],"family")
smith=ex.addItem(["Smith"],"family")
ex.addItem(["Laura"],"children",dupont)
ex.addItem(["Matt"],"children",dupont)
ex.addItem(["Kim"],"children",robert)
ex.addItem(["Stephanie"],"children",robert)
ex.addItem(["John"],"children",smith)
ex.show()
sys.exit(app.exec_())
#food example: issue with "in between"
settings={
"food":([],default|drop),
"allVegetable":(["food"],default|drag|drop),
"allFruit":(["food"],default|drag|drop),
"fruit":(["allFruit","fruit"],default|drag|drop),
"veggie":(["allVegetable","veggie"],default|drag|drop),
}
ex = CustomTreeWidget(settings)
top=ex.addItem(["Food"],"food")
fruits=ex.addItem(["Fruits"],"allFruit",top)
ex.addItem(["apple"],"fruit",fruits)
ex.addItem(["orange"],"fruit",fruits)
vegetable=ex.addItem(["Vegetables"],"allVegetable",top)
ex.addItem(["carrots"],"veggie",vegetable)
ex.addItem(["lettuce"],"veggie",vegetable)
ex.addItem(["leek"],"veggie",vegetable)
ex.show()
sys.exit(app.exec_())
来源:https://stackoverflow.com/questions/20618044/pyside-qtreewidget-constrain-drag-and-drop