代码之家  ›  专栏  ›  技术社区  ›  frans

在PyQt5中,如何使用QTableView上的拖放正确交换行

  •  0
  • frans  · 技术社区  · 6 年前

    QTableView s&用于移动现有行的拖放机制。我找到了很多来源(例如。 here here here )描述 拖放、插入等方面,但我仍在努力使其适用于我的案例。

    以下是我正在寻找的解决方案应该具备的功能:

    • 对数据结构进行操作。i、 e.当项目的顺序 在视图中修改应在数据结构中修改
      • 选择/移动整行
      • 显示整行的下降指示器
    • 还必须能够执行进一步的操作,如删除/编辑单元格 i、 e.不得被拖拽物触碰&下降法

    This tutorial 非常 接近我需要的,但它使用 QStandardItemModel QAbstractTableModel 这在我看来是半最优的,因为我必须在基于 QStandardItem 这是我们所需要的 (我说得对吗?)

    下面附加了表示我当前进度的代码。

    方法1 :针对 QAbstractTable模型 并实现所有需要的事件/插槽以修改底层数据结构: *赞成:没有冗余数据 *缺点:我不知道如何得知一个已经完成的拖拽&的消息;滴

    在我附加的代码中,我跟踪了我知道的所有相关方法,并打印出所有参数。这是我将第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 ? 我以后是否应该使用它来复制数据?

    方法2 :使用 QS标准模型 QS标准模型 *赞成者:有一个 working example *相反:您管理的冗余数据结构必须一致 使用另一个内部管理的数据结构。 *康特拉:我也不知道该怎么做

    下面是我目前使用的方法 QAbstractTable模型 :

    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()
    
    0 回复  |  直到 6 年前
        1
  •  1
  •   oetzi    6 年前

    MyData类应从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()
    
        2
  •  1
  •   frans    6 年前

    我还不知道该怎么做 QAbstractTableModel QAbstractItemModel 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_())