在 PyQt5 中,如何使用拖放正确移动 QTableView 中的行
In PyQt5 how do I properly move rows in a QTableView using Drag&Drop
我(只是)希望能够使用 QTableView
s 拖放机制来移动现有行。我发现很多资源(例如 here, here or here)描述了拖放、插入等一些方面,但我仍在努力使其适用于我的情况。
这是我正在寻找的解决方案应该能够做到的:
- 处理 'Qt-free' 数据结构,例如元组列表。
- 对数据结构进行操作。即当物品的顺序得到
在视图中修改它应该在数据结构中修改
- 标准拖放启用列表的外观:
- select/move整行
- 显示整条线的下降指示器
- 单元格的 deleting/editing 之类的进一步操作必须仍然是可能的
即不被拖放方法所触及
This tutorial 显示了一个 非常 接近我需要的解决方案,但它使用 QStandardItemModel
而不是 QAbstractTableModel
看起来半- 对我来说是最佳的,因为我必须在 QStandardItem
的基础上对 'mirrored' 数据结构进行操作,这是 QStandardItemModel
所需要的(对吗?)
代表我当前进度的代码附在下面。
目前我看到两种可能的方法:
方法 1:针对 QAbstractTableModel
实施并实施所有需要的 events/slots 以修改底层数据结构:
* pro:最通用的方法
* 亲:没有冗余数据
*缺点:我不知道如何获得有关完成的拖放的通知
操作和什么索引移动到哪里
在我附加的代码中,我跟踪了我知道的所有相关方法并打印出所有参数。这是我将第 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
并与 QStandardItemModel
管理的数据并行修改您的数据。
*亲:有一个working example
* 相反:你管理一个冗余的数据结构,它必须是一致的
与另一个内部管理的数据结构。
* contra: 也没有找到确切的方法
Here is my current approach using 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()
MyData class 应该继承自 QStandardItemModel
修改了您的代码以解决拖放和扩展 class 函数调用问题。
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()
我还不知道如何使 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_())
我(只是)希望能够使用 QTableView
s 拖放机制来移动现有行。我发现很多资源(例如 here, here or here)描述了拖放、插入等一些方面,但我仍在努力使其适用于我的情况。
这是我正在寻找的解决方案应该能够做到的:
- 处理 'Qt-free' 数据结构,例如元组列表。
- 对数据结构进行操作。即当物品的顺序得到 在视图中修改它应该在数据结构中修改
- 标准拖放启用列表的外观:
- select/move整行
- 显示整条线的下降指示器
- 单元格的 deleting/editing 之类的进一步操作必须仍然是可能的 即不被拖放方法所触及
This tutorial 显示了一个 非常 接近我需要的解决方案,但它使用 QStandardItemModel
而不是 QAbstractTableModel
看起来半- 对我来说是最佳的,因为我必须在 QStandardItem
的基础上对 'mirrored' 数据结构进行操作,这是 QStandardItemModel
所需要的(对吗?)
代表我当前进度的代码附在下面。
目前我看到两种可能的方法:
方法 1:针对 QAbstractTableModel
实施并实施所有需要的 events/slots 以修改底层数据结构:
* pro:最通用的方法
* 亲:没有冗余数据
*缺点:我不知道如何获得有关完成的拖放的通知
操作和什么索引移动到哪里
在我附加的代码中,我跟踪了我知道的所有相关方法并打印出所有参数。这是我将第 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
并与 QStandardItemModel
管理的数据并行修改您的数据。
*亲:有一个working example
* 相反:你管理一个冗余的数据结构,它必须是一致的
与另一个内部管理的数据结构。
* contra: 也没有找到确切的方法
Here is my current approach using
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()
MyData class 应该继承自 QStandardItemModel 修改了您的代码以解决拖放和扩展 class 函数调用问题。
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()
我还不知道如何使 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_())