Редактируемый переставляемый (перетаскиванием) пример Qt5 QTreeView

После того, как я не смог найти приличного общего иерархического переупорядочиваемого примера перетаскивания для QtreeView Qt5, я попытался соответствующим образом преобразовать пример кода Editable Tree Model.

Есть связанный вопрос, записанный по адресу: QTreeView с поддержкой перетаскивания в PyQt, но, хотя это PyQt4, что само по себе не является проблемой (в любом случае я собираюсь преобразовать это в PyQt;)), TreeView + абстрактная модель не не работает должным образом. По крайней мере, он не меняет порядок здесь.

Этот пример кода работает не так хорошо: он позволяет перемещать элементы, но удаление их приводит к пустой строке, но запись не перемещается.

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

Теоретически это все, что нужно, чтобы иметь возможность переупорядочивать товары.

Вот версия PyQt5: --- 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 ответ

Мне удалось расширить ответ на указанный вопрос, чтобы перетаскивать элементы модели с помощью перетаскивания. Тем не менее, я должен отметить, что и приведенный пример, и мой ответ на самом деле имеют дело с моделью, напоминающей модель списка, а не с древовидной, потому что drag-n-drop не влияет на отношения родитель-потомок между элементами модели, вместо этого обработка отбрасывания реализована так, чтобы всегда выполнять переупорядочение элементов.

Поскольку drag-n-drop требует сериализации некоторых данных о перемещаемом элементе, переупорядочение с помощью drag-n-drop может быть реализовано следующим образом:

  1. При перетаскивании мы сериализуем некоторую информацию о перетаскиваемых объектах
  2. По капле мы:
    • десериализовать эту информацию обратно
    • используйте эту информацию, чтобы найти исходную позицию каждого перетаскиваемого элемента в модели
    • удалите расположенные предметы из модели, в результате чего оставшиеся предметы сместятся; Qt сделает это за нас, если модель правильно реализует removeRows метод.
    • вставьте удаленный предмет обратно в модель, но на этот раз в положение либо до предмета, на который он был сброшен, либо после него. Первый вариант полезен для перехода на первый элемент, а второй - для других случаев.

Вот полный код решения для 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. На всякий случай, если кому-то интересно, я расширил эту демонстрацию, чтобы правильно обработать выделение после перетаскивания, код для PyQt5 и Python 3 можно найти здесь.

Другие вопросы по тегам