Editable reorderable (by drag and drop) Qt5 QTreeView example

主宰稳场 提交于 2019-12-13 05:43:21

问题


after being unable to find a decent generic hierarchical reorderable drag and drop example for Qt5's QTreeView, I tried to transform the Editable Tree Model example code accordingly.

There's an related question recorded at: QTreeView with drag and drop support in PyQt, but while it's PyQt4, which isn't a problem in itself (I'm going to convert this to PyQt anyway ;)), the treeview + abstract model doesn't work properly. At least, it doesn't reorder any items here.

This example code doesn't work as well: it allows moving items, but dropping them results in an empty row, but the entry isn't moved.

diff -up editabletreemodel.orig/mainwindow.cpp editabletreemodel/mainwindow.cpp
--- editabletreemodel.orig/mainwindow.cpp   2016-06-10 08:48:56.000000000 +0200
+++ editabletreemodel/mainwindow.cpp        2016-10-25 23:20:09.909671875 +0200
@@ -67,6 +67,7 @@ MainWindow::MainWindow(QWidget *parent)
     file.close();

     view->setModel(model);
+    view->setDragDropMode(QAbstractItemView::InternalMove);
     for (int column = 0; column < model->columnCount(); ++column)
         view->resizeColumnToContents(column);

diff -up editabletreemodel.orig/treemodel.cpp editabletreemodel/treemodel.cpp
--- editabletreemodel.orig/treemodel.cpp    2016-06-10 08:48:56.000000000 +0200
+++ editabletreemodel/treemodel.cpp 2016-10-25 23:23:47.408024344 +0200
@@ -96,10 +96,12 @@ QVariant TreeModel::data(const QModelInd
 //! [3]
 Qt::ItemFlags TreeModel::flags(const QModelIndex &index) const
 {
-    if (!index.isValid())
-        return 0;
+    Qt::ItemFlags defaultFlags = Qt::ItemIsEditable | QAbstractItemModel::flags(index);

-    return Qt::ItemIsEditable | QAbstractItemModel::flags(index);
+    if (index.isValid())
+   return Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | defaultFlags;
+    else
+        return Qt::ItemIsDropEnabled | defaultFlags;
 }
 //! [3]

@@ -295,3 +297,8 @@ void TreeModel::setupModelData(const QSt
         ++number;
     }
 }
+
+Qt::DropActions TreeModel::supportedDropActions() const
+{
+    return Qt::MoveAction;
+}
diff -up editabletreemodel.orig/treemodel.h editabletreemodel/treemodel.h
--- editabletreemodel.orig/treemodel.h      2016-06-10 08:48:56.000000000 +0200
+++ editabletreemodel/treemodel.h   2016-10-25 23:19:18.884870266 +0200
@@ -95,6 +95,7 @@ public:
                     const QModelIndex &parent = QModelIndex()) Q_DECL_OVERRIDE;
     bool removeRows(int position, int rows,
                     const QModelIndex &parent = QModelIndex()) Q_DECL_OVERRIDE;
+    Qt::DropActions supportedDropActions() const Q_DECL_OVERRIDE;

 private:
     void setupModelData(const QStringList &lines, TreeItem *parent);

In theory, this is all, what is needed to be able to reorder items.

Here's the PyQt5 version: --- editabletreemodel.py.orig 2015-07-17 13:39:33.000000000 +0200 +++ editabletreemodel.py 2016-10-26 00:24:51.857176297 +0200 @@ -44,7 +44,7 @@

 from PyQt5.QtCore import (QAbstractItemModel, QFile, QIODevice,
         QItemSelectionModel, QModelIndex, Qt)
-from PyQt5.QtWidgets import QApplication, QMainWindow
+from PyQt5.QtWidgets import QApplication, QMainWindow, QAbstractItemView

 import editabletreemodel_rc
 from ui_mainwindow import Ui_MainWindow
@@ -151,10 +151,12 @@ class TreeModel(QAbstractItemModel):
         return item.data(index.column())

     def flags(self, index):
-        if not index.isValid():
-            return 0
+        defaultFlags = Qt.ItemIsEditable | super(TreeModel, self).flags(index)

-        return Qt.ItemIsEditable | Qt.ItemIsEnabled | Qt.ItemIsSelectable
+        if index.isValid():
+            return defaultFlags | Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled
+        else:
+            return defaultFlags | Qt.ItemIsDropEnabled

     def getItem(self, index):
         if index.isValid():
@@ -296,6 +298,9 @@ class TreeModel(QAbstractItemModel):

             number += 1

+    def supportedDropActions(self):
+        return Qt.MoveAction
+

 class MainWindow(QMainWindow, Ui_MainWindow):
     def __init__(self, parent=None):
@@ -311,6 +316,7 @@ class MainWindow(QMainWindow, Ui_MainWin
         file.close()

         self.view.setModel(model)
+        self.view.setDragDropMode(QAbstractItemView.InternalMove)
         for column in range(model.columnCount()):
             self.view.resizeColumnToContents(column)

回答1:


I've managed to extend the answer from the referenced question to make the model items reorderable by drag-n-drop. However, I have to point out that both the referenced example and my answer actually deal with the model resembling a list model more than a tree one because drag-n-drop doesn't affect the parent-child relationships between the model items, instead the drop handling is implemented to always perform items reordering.

As long as drag-n-drop requires the serialization of some data about the item being repositioned, the reordering via drag-n-drop can be implemented in the following way:

  1. On drag we serialize some info about the dragged items
  2. On drop we:
    • deserialize this info back
    • use this info to locate each dragged item's original position in the model
    • remove the located items from the model thus causing the remaining items to shift their positions; Qt would do it for us if the model properly implements removeRows method.
    • insert the removed item back into the model but this time at position either before the item onto which it has been dropped or after it. The former option is useful for dropping onto the first item and the latter option is useful for other cases.

Here's the full code of the solution for PyQt4:

import sys
from PyQt4 import QtGui, QtCore

class TreeModel(QtCore.QAbstractItemModel):
    def __init__(self):
        QtCore.QAbstractItemModel.__init__(self)
        self.nodes = ['node0', 'node1', 'node2', 'node3', 'node4', 'node5']

    def index(self, row, column, parent):
        if row < 0 or row >= len(self.nodes):
            return QtCore.QModelIndex()
        return self.createIndex(row, column, self.nodes[row])

    def parent(self, index):
        return QtCore.QModelIndex()

    def rowCount(self, index):
        if index.isValid():
            return 0
        if index.internalPointer() in self.nodes:
            return 0
        return len(self.nodes)

    def columnCount(self, index):
        if index.isValid():
            return 0
        return 1

    def data(self, index, role):
        if not index.isValid():
            return None
        if role == 0: 
            return index.internalPointer()
        else:
            return None

    def supportedDropActions(self): 
        return QtCore.Qt.CopyAction | QtCore.Qt.MoveAction         

    def flags(self, index):
        if not index.isValid():
            return QtCore.Qt.ItemIsEnabled
        return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | \
               QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsDropEnabled        

    def insertRows(self, row, count, index):
        if index.isValid():
            return False
        if count <= 0:
            return False
        # inserting 'count' empty rows starting at 'row'
        self.beginInsertRows(QtCore.QModelIndex(), row, row + count - 1)
        for i in range(0, count):
            self.nodes.insert(row + i, '')
        self.endInsertRows()
        return True

    def removeRows(self, row, count, index):
        if index.isValid():
            return False
        if count <= 0:
            return False
        num_rows = self.rowCount(QtCore.QModelIndex())
        self.beginRemoveRows(QtCore.QModelIndex(), row, row + count - 1)
        for i in range(count, 0, -1):
            self.nodes.pop(row - i + 1)
        self.endRemoveRows()
        return True

    def setData(self, index, value, role):
        if not index.isValid():
            return False
        if index.row() < 0 or index.row() > len(self.nodes):
            return False
        self.nodes[index.row()] = str(value)
        self.dataChanged.emit(index, index)

    def mimeTypes(self):
        return ['application/vnd.treeviewdragdrop.list']

    def mimeData(self, indexes):
        mimedata = QtCore.QMimeData()
        encoded_data = QtCore.QByteArray()
        stream = QtCore.QDataStream(encoded_data, QtCore.QIODevice.WriteOnly)
        for index in indexes:
            if index.isValid():
                text = self.data(index, 0)
                stream << QtCore.QString(text)
        mimedata.setData('application/vnd.treeviewdragdrop.list', encoded_data)
        return mimedata

    def dropMimeData(self, data, action, row, column, parent):
        if action == QtCore.Qt.IgnoreAction:
            return True
        if not data.hasFormat('application/vnd.treeviewdragdrop.list'):
            return False
        if column > 0:
            return False

        num_rows = self.rowCount(QtCore.QModelIndex())

        begin_row = 0
        if row != -1:
            begin_row = row
        elif parent.isValid():
            begin_row = parent.row()
        else:
            begin_row = num_rows

        if begin_row != num_rows and begin_row != 0:
            begin_row += 1

        encoded_data = data.data('application/vnd.treeviewdragdrop.list')
        stream = QtCore.QDataStream(encoded_data, QtCore.QIODevice.ReadOnly)
        new_items = []
        rows = 0
        while not stream.atEnd():
            text = QtCore.QString()
            stream >> text
            new_items.append(text)
            rows += 1

        # insert the new rows for the dropped items and set the data to these items appropriately
        self.insertRows(begin_row, rows, QtCore.QModelIndex())
        for text in new_items:
            idx = self.index(begin_row, 0, QtCore.QModelIndex())
            self.setData(idx, text, 0)
            self.dataChanged.emit(idx, idx)
            begin_row += 1

        return True

class MainForm(QtGui.QMainWindow):
    def __init__(self, parent=None):
        super(MainForm, self).__init__(parent)

        self.treeModel = TreeModel()

        self.view = QtGui.QTreeView()
        self.view.setModel(self.treeModel)
        self.view.setDragDropMode(QtGui.QAbstractItemView.InternalMove)

        self.setCentralWidget(self.view)

def main():
    app = QtGui.QApplication(sys.argv)
    form = MainForm()
    form.show()
    app.exec_()

if __name__ == '__main__':
    main()

Upd.: Just in case someone is interested, I've extended this demo to properly deal with the selection after drag-n-drop, the code for PyQt5 and Python 3 can be found here.



来源:https://stackoverflow.com/questions/40250771/editable-reorderable-by-drag-and-drop-qt5-qtreeview-example

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!