在PyQt5中,如何使用拖放正确移动QTableView中的行?

5
我希望能够使用QTableView的拖放机制来移动现有行。我找到了很多资源(例如这里这里这里),它们描述了拖放、插入等一些方面,但我仍然在努力使其适用于我的情况。
以下是我正在寻找的解决方案应该具备的功能:
  • 使用“无Qt”的数据结构,例如元组列表。
  • 对数据结构进行操作。当视图中的项目顺序被修改时,应该在数据结构中进行修改。
  • 外观和标准拖放启用列表相似:
    • 选择/移动整个行
    • 为整行显示一个投放指示器
  • 必须仍然可以执行进一步的操作,例如删除/编辑单元格,即不受拖放方法的影响

此教程展示了一个非常接近我所需要的解决方案,但它使用QStandardItemModel而不是QAbstractTableModel,这对我来说看起来不太理想,因为我必须在基于 QStandardItem 的“镜像”数据结构上进行操作,而这正是QStandardItemModel所需的(我是对的吗?)

下面附上代表我的当前进展的代码。

目前我看到有两种可能的方法:

方法一:针对 QAbstractTableModel 实现并实现所有必要的事件/槽来修改底层数据结构: * 优点:最通用的方法 * 优点:没有冗余数据 * 缺点:我不知道如何得到有关已完成拖放操作和移动到哪个索引的信息

在我附加的代码中,我跟踪了我所知道的所有相关方法并打印出所有参数。当我将第2行拖到第3行时,这是我得到的结果。

dropMimeData(data: ['application/x-qabstractitemmodeldatalist'], action: 2, row: -1, col: -1, parent: '(row: 2, column: 0, valid: True)')
insertRows(row=-1, count=1, parent=(row: 2, column: 0, valid: True))
setData(index=(row: 0, column: 0, valid: True), value='^line1', role=0)
setData(index=(row: 0, column: 1, valid: True), value=1, role=0)
removeRows(row=1, count=1, parent=(row: -1, column: -1, valid: False))

这个输出对我提出了以下问题:

  • moveRow/moveRows 为什么没有被调用?它们何时会被调用?
  • insertRow/removeRow 为什么不被调用,只有 insertRows/removeRows 被调用?
  • -1 行索引代表什么意思?
  • dropMimeData 中提供的 MIME 数据可以做什么?我以后应该使用它来复制数据吗?

方法2: 使用QStandardItemModel并与由其管理的数据并行修改您的数据。 * 正面:有一个工作示例 * 反面:您需要管理一个冗余的数据结构,该结构必须与另一个内部管理的数据结构一致。 * 反面:也没有确切地找出如何做到这一点

这是我目前使用QAbstractTableModel的方法:

from PyQt5 import QtWidgets, QtCore, QtGui

class MyModel(QtCore.QAbstractTableModel):
    def __init__(self, data, parent=None, *args):
        super().__init__(parent, *args)
        self._data = data

    def columnCount(self, parent):
        return 2

    def rowCount(self, parent):
        return len(self._data)

    def headerData(self, column: int, orientation, role: QtCore.Qt.ItemDataRole):
        return (('Regex', 'Category')[column] 
                if role == QtCore.Qt.DisplayRole and orientation == QtCore.Qt.Horizontal
                else None)

    def data(self, index, role: QtCore.Qt.ItemDataRole):
        if role not in {QtCore.Qt.DisplayRole, QtCore.Qt.EditRole}:
            return None

        print("data(index=%s, role=%r)" % (self._index2str(index), self._role2str(role)))
        return (self._data[index.row()][index.column()] 
               if index.isValid()
               and role in {QtCore.Qt.DisplayRole, QtCore.Qt.EditRole} 
               and index.row() < len(self._data)
               else None)

    def setData(self, index: QtCore.QModelIndex, value, role: QtCore.Qt.ItemDataRole):

        print("setData(index=%s, value=%r, role=%r)" % (self._index2str(index), value, role))
        return super().setData(index, value, role)

    def flags(self, index):
        return (
           super().flags(index) 
            | QtCore.Qt.ItemIsDropEnabled
            | (QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsDragEnabled)
              if index.isValid() else QtCore.Qt.NoItemFlags)

    def dropMimeData(self, data, action, row, col, parent: QtCore.QModelIndex):
        """Always move the entire row, and don't allow column 'shifting'"""
        print("dropMimeData(data: %r, action: %r, row: %r, col: %r, parent: %r)" % (
            data.formats(), action, row, col, self._index2str(parent)))
        assert action == QtCore.Qt.MoveAction
        return super().dropMimeData(data, action, row, 0, parent)

    def supportedDragActions(self):
        return QtCore.Qt.MoveAction

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

    def removeRow(self, row: int, parent=None):
        print("removeRow(row=%r):" % (row))
        return super().removeRow(row, parent)

    def removeRows(self, row: int, count: int, parent=None):
        print("removeRows(row=%r, count=%r, parent=%s)" % (row, count, self._index2str(parent)))
        return super().removeRows(row, count, parent)

    def insertRow(self, index, parent=None):
        print("insertRow(row=%r, count=%r):" % (row, count))
        return super().insertRow(row, count, parent)

    def insertRows(self, row: int, count: int, parent: QtCore.QModelIndex = None):
        print("insertRows(row=%r, count=%r, parent=%s)" % (row, count, self._index2str(parent)))
        return super().insertRows(row, count, parent)

    @staticmethod
    def _index2str(index):
        return "(row: %d, column: %d, valid: %r)" % (index.row(), index.column(), index.isValid())

    @staticmethod
    def _role2str(role: QtCore.Qt.ItemDataRole) -> str:
        return "%s (%d)" % ({
            QtCore.Qt.DisplayRole: "DisplayRole",
            QtCore.Qt.DecorationRole: "DecorationRole",
            QtCore.Qt.EditRole: "EditRole",
            QtCore.Qt.ToolTipRole: "ToolTipRole",
            QtCore.Qt.StatusTipRole: "StatusTipRole",
            QtCore.Qt.WhatsThisRole: "WhatsThisRole",
            QtCore.Qt.SizeHintRole: "SizeHintRole",

            QtCore.Qt.FontRole: "FontRole",
            QtCore.Qt.TextAlignmentRole: "TextAlignmentRole",
            QtCore.Qt.BackgroundRole: "BackgroundRole",
            #QtCore.Qt.BackgroundColorRole:
            QtCore.Qt.ForegroundRole: "ForegroundRole",
            #QtCore.Qt.TextColorRole
            QtCore.Qt.CheckStateRole: "CheckStateRole",
            QtCore.Qt.InitialSortOrderRole: "InitialSortOrderRole",
        }[role], role)


class MyTableView(QtWidgets.QTableView):
    class DropmarkerStyle(QtWidgets.QProxyStyle):
        def drawPrimitive(self, element, option, painter, widget=None):
            """Draw a line across the entire row rather than just the column we're hovering over.
            This may not always work depending on global style - for instance I think it won't
            work on OSX."""
            if element == self.PE_IndicatorItemViewItemDrop and not option.rect.isNull():
                option_new = QtWidgets.QStyleOption(option)
                option_new.rect.setLeft(0)
                if widget:
                    option_new.rect.setRight(widget.width())
                option = option_new
            super().drawPrimitive(element, option, painter, widget)

    def __init__(self):
        super().__init__()
        self.setStyle(self.DropmarkerStyle())
        # only allow rows to be selected
        self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
        # disallow multiple rows to be selected
        self.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
        self.setDragEnabled(True)

        self.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove)
        self.setDropIndicatorShown(True) # default
        self.setAcceptDrops(False)           # ?
        self.viewport().setAcceptDrops(True) # ?
        self.setDragDropOverwriteMode(False)


class HelloWindow(QtWidgets.QMainWindow):
    def __init__(self) -> None:
        super().__init__()

        model = MyModel([("^line0", 0),
                         ("^line1", 1),
                         ("^line2", 2),
                         ("^line3", 3)])

        table_view = MyTableView()
        table_view.setModel(model)
        table_view.verticalHeader().hide()
        table_view.setShowGrid(False)

        self.setCentralWidget(table_view)


def main():
    app = QtWidgets.QApplication([])
    window = HelloWindow()
    window.show()
    app.exec_()

if __name__ == "__main__":
    main()
2个回答

3

我还不知道如何按照描述使 QAbstractTableModelQAbstractItemModel 起作用,但我终于找到了一种方法来让 QTableView 处理拖放操作并只移动一行。

以下是代码:

from PyQt5 import QtWidgets, QtCore

class ReorderTableModel(QtCore.QAbstractTableModel):
    def __init__(self, data, parent=None, *args):
        super().__init__(parent, *args)
        self._data = data

    def columnCount(self, parent=None) -> int:
        return 2

    def rowCount(self, parent=None) -> int:
        return len(self._data) + 1

    def headerData(self, column: int, orientation, role: QtCore.Qt.ItemDataRole):
        return (('Regex', 'Category')[column]
                if role == QtCore.Qt.DisplayRole and orientation == QtCore.Qt.Horizontal
                else None)

    def data(self, index: QtCore.QModelIndex, role: QtCore.Qt.ItemDataRole):
        if not index.isValid() or role not in {QtCore.Qt.DisplayRole, QtCore.Qt.EditRole}:
            return None
        return (self._data[index.row()][index.column()] if index.row() < len(self._data) else
                "edit me" if role == QtCore.Qt.DisplayRole else "")

    def flags(self, index: QtCore.QModelIndex) -> QtCore.Qt.ItemFlags:
        # https://doc.qt.io/qt-5/qt.html#ItemFlag-enum
        if not index.isValid():
            return QtCore.Qt.ItemIsDropEnabled
        if index.row() < len(self._data):
            return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsDragEnabled
        return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable

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

    def relocateRow(self, row_source, row_target) -> None:
        row_a, row_b = max(row_source, row_target), min(row_source, row_target)
        self.beginMoveRows(QtCore.QModelIndex(), row_a, row_a, QtCore.QModelIndex(), row_b)
        self._data.insert(row_target, self._data.pop(row_source))
        self.endMoveRows()


class ReorderTableView(QtWidgets.QTableView):
    """QTableView with the ability to make the model move a row with drag & drop"""

    class DropmarkerStyle(QtWidgets.QProxyStyle):
        def drawPrimitive(self, element, option, painter, widget=None):
            """Draw a line across the entire row rather than just the column we're hovering over.
            This may not always work depending on global style - for instance I think it won't
            work on OSX."""
            if element == self.PE_IndicatorItemViewItemDrop and not option.rect.isNull():
                option_new = QtWidgets.QStyleOption(option)
                option_new.rect.setLeft(0)
                if widget:
                    option_new.rect.setRight(widget.width())
                option = option_new
            super().drawPrimitive(element, option, painter, widget)

    def __init__(self, parent):
        super().__init__(parent)
        self.verticalHeader().hide()
        self.setSelectionBehavior(self.SelectRows)
        self.setSelectionMode(self.SingleSelection)
        self.setDragDropMode(self.InternalMove)
        self.setDragDropOverwriteMode(False)
        self.setStyle(self.DropmarkerStyle())

    def dropEvent(self, event):
        if (event.source() is not self or
            (event.dropAction() != QtCore.Qt.MoveAction and
             self.dragDropMode() != QtWidgets.QAbstractItemView.InternalMove)):
            super().dropEvent(event)

        selection = self.selectedIndexes()
        from_index = selection[0].row() if selection else -1
        to_index = self.indexAt(event.pos()).row()
        if (0 <= from_index < self.model().rowCount() and
            0 <= to_index < self.model().rowCount() and
            from_index != to_index):
            self.model().relocateRow(from_index, to_index)
            event.accept()
        super().dropEvent(event)


class Testing(QtWidgets.QMainWindow):
    """Demonstrate ReorderTableView"""
    def __init__(self):
        super().__init__()
        view = ReorderTableView(self)
        view.setModel(ReorderTableModel([
            ("a", 1),
            ("b", 2),
            ("c", 3),
            ("d", 4),
        ]))
        self.setCentralWidget(view)

        self.show()


if __name__ == '__main__':
    app = QtWidgets.QApplication([])
    test = Testing()
    raise SystemExit(app.exec_())

1

我的Data类应该继承自QStandardItemModel,修改了你的代码以解决拖放和扩展类函数调用问题。

from PyQt5 import (QtWidgets, QtCore)
from PyQt5.QtWidgets import (QApplication, QTableView)
from PyQt5.QtGui import (QStandardItem, QStandardItemModel)


class MyModel(QStandardItemModel):
    def __init__(self, data, parent=None, *args):
        super().__init__(parent, *args)
        self._data = data

        for (index, data) in enumerate(data):
            first = QStandardItem('Item {}'.format(index))
            first.setDropEnabled(False)
            first.setEditable(False)
            second = QStandardItem(data[0])
            second.setDropEnabled(False)
            second.setEditable(False)
            self.appendRow([first, second])

    def columnCount(self, parent):
        return 2

    def rowCount(self, parent):
        return len(self._data)

    def headerData(self, column: int, orientation, role: QtCore.Qt.ItemDataRole):
        return (('Regex', 'Category')[column]
                if role == QtCore.Qt.DisplayRole and orientation == QtCore.Qt.Horizontal
                else None)

    def data(self, index, role: QtCore.Qt.ItemDataRole):
        if role not in {QtCore.Qt.DisplayRole, QtCore.Qt.EditRole}:
            return None

        print("data(index=%s, role=%r)" % (self._index2str(index), self._role2str(role)))
        return (self._data[index.row()][index.column()]
                if index.isValid() and role in {QtCore.Qt.DisplayRole, QtCore.Qt.EditRole} and index.row() < len(
            self._data)
                else None)

    def setData(self, index: QtCore.QModelIndex, value, role: QtCore.Qt.ItemDataRole):
        print("setData(index=%s, value=%r, role=%r)" % (self._index2str(index), value, role))
        return super().setData(index, value, role)

    def flags(self, index):
        return (
            super().flags(index)
            | QtCore.Qt.ItemIsDropEnabled
            | (QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsDragEnabled)
            if index.isValid() else QtCore.Qt.NoItemFlags)

    def dropMimeData(self, data, action, row, col, parent: QtCore.QModelIndex):
        """Always move the entire row, and don't allow column 'shifting'"""
        print("dropMimeData(data: %r, action: %r, row: %r, col: %r, parent: %r)" % (
            data.formats(), action, row, col, self._index2str(parent)))
        assert action == QtCore.Qt.MoveAction
        return super().dropMimeData(data, action, row, 0, parent)

    def supportedDragActions(self):
        return QtCore.Qt.MoveAction

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

    def removeRow(self, row: int, parent=None):
        print("removeRow(row=%r):" % (row))
        return super().removeRow(row, parent)

    def removeRows(self, row: int, count: int, parent=None):
        print("removeRows(row=%r, count=%r, parent=%s)" % (row, count, self._index2str(parent)))
        return super().removeRows(row, count, parent)

    def insertRow(self, index, parent=None):
        print("insertRow(row=%r, count=%r):" % (row, count))
        return super().insertRow(row, count, parent)

    def insertRows(self, row: int, count: int, parent: QtCore.QModelIndex = None):
        print("insertRows(row=%r, count=%r, parent=%s)" % (row, count, self._index2str(parent)))
        return super().insertRows(row, count, parent)

    @staticmethod
    def _index2str(index):
        return "(row: %d, column: %d, valid: %r)" % (index.row(), index.column(), index.isValid())

    @staticmethod
    def _role2str(role: QtCore.Qt.ItemDataRole) -> str:
        return "%s (%d)" % ({
                                QtCore.Qt.DisplayRole: "DisplayRole",
                                QtCore.Qt.DecorationRole: "DecorationRole",
                                QtCore.Qt.EditRole: "EditRole",
                                QtCore.Qt.ToolTipRole: "ToolTipRole",
                                QtCore.Qt.StatusTipRole: "StatusTipRole",
                                QtCore.Qt.WhatsThisRole: "WhatsThisRole",
                                QtCore.Qt.SizeHintRole: "SizeHintRole",

                                QtCore.Qt.FontRole: "FontRole",
                                QtCore.Qt.TextAlignmentRole: "TextAlignmentRole",
                                QtCore.Qt.BackgroundRole: "BackgroundRole",
                                # QtCore.Qt.BackgroundColorRole:
                                QtCore.Qt.ForegroundRole: "ForegroundRole",
                                # QtCore.Qt.TextColorRole
                                QtCore.Qt.CheckStateRole: "CheckStateRole",
                                QtCore.Qt.InitialSortOrderRole: "InitialSortOrderRole",
                            }[role], role)


class MyTableView(QTableView):
    class DropMarkerStyle(QtWidgets.QProxyStyle):
        def drawPrimitive(self, element, option, painter, widget=None):
            """Draw a line across the entire row rather than just the column we're hovering over.
            This may not always work depending on global style - for instance I think it won't
            work on OSX."""
            if element == self.PE_IndicatorItemViewItemDrop and not option.rect.isNull():
                option_new = QtWidgets.QStyleOption(option)
                option_new.rect.setLeft(0)
                if widget:
                    option_new.rect.setRight(widget.width())
                option = option_new
            super().drawPrimitive(element, option, painter, widget)

    def __init__(self):
        super().__init__()
        self.setStyle(self.DropMarkerStyle())
        self.verticalHeader().hide()
        self.setShowGrid(False)
        # only allow rows to be selected
        self.setSelectionBehavior(self.SelectRows)
        # disallow multiple rows to be selected
        self.setSelectionMode(self.SingleSelection)
        self.setDragDropMode(self.InternalMove)
        self.setDragDropOverwriteMode(False)


class HelloWindow(QtWidgets.QMainWindow):
    def __init__(self) -> None:
        super().__init__()
        model = MyModel([("^line0", 0),
                         ("^line1", 1),
                         ("^line2", 2),
                         ("^line3", 3)])
        table_view = MyTableView()
        table_view.setModel(model)
        self.setCentralWidget(table_view)


def main():
    app = QApplication([])
    window = HelloWindow()
    window.show()
    app.exec_()


if __name__ == "__main__":
    main()

1
我还找到了一个例子,展示了使用QStandardModel进行拖放操作,这很可能是你所指的,但它存在一些问题:例如,当你将first = ...修复为采用data[0]second =采用data[1]时,应用程序会崩溃(可能是因为data[1]是一个int)。但即使我按照你的示例运行它,当我尝试拖放它们时,仍然会删除一些行。 - frans
我尝试在MyModel类中注释掉事件,但并没有解决拖放功能的问题。在MyModel构造函数中添加代码以将数据定义为QStandardItem及其拖放功能是工作解决方案的主要部分。 - oetzi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接