重新排序包含跨列的 QTableView 行
Posted
技术标签:
【中文标题】重新排序包含跨列的 QTableView 行【英文标题】:Re-ordering QTableView rows that include spanned columns 【发布时间】:2021-11-01 15:37:51 【问题描述】:我在下面有一个有效的拖放示例,用于使用 PyQt5 为 qtableview 重新排序具有相同列长度的行(在此 *** 问题here 的帮助下)。但是,我希望在 qtableview 表上执行相同的操作,其中一或两行合并了跨越总列数的单元格(如下图中的第二行)。
最好的方法是什么?我应该在拖放点删除合并(clearSpans),然后根据单元格值进行重新合并(尽管当我尝试这样做时它不起作用),还是有办法使用单元格拖放重新排序原封不动地合并?
这是适用于相等列的行数据的代码,但在合并行时会失败
from PyQt5.QtGui import QBrush
from PyQt5.QtWidgets import *
from PyQt5.QtCore import QAbstractTableModel, Qt, QModelIndex
class myModel(QAbstractTableModel):
def __init__(self, data, parent=None, *args):
super().__init__(parent, *args)
self._data = data or []
self._headers = ['Type', 'result', 'count']
def rowCount(self, index=None):
return len(self._data)
def columnCount(self, index=None):
return len(self._headers)
def headerData(self, section, orientation, role=Qt.DisplayRole):
if role == Qt.DisplayRole:
if orientation == Qt.Horizontal:
if section < 0 or section >= len(self._headers):
return ""
else:
return self._headers[section]
return None
def data(self, index, role=None):
if role == Qt.TextAlignmentRole:
return Qt.AlignHCenter
if role == Qt.ForegroundRole:
return QBrush(Qt.black)
if role == Qt.BackgroundRole:
if (self.index(index.row(), 0).data().startswith('second')):
return QBrush(Qt.green)
else:
if (self.index(index.row(), 1).data()) == 'abc':
return QBrush(Qt.yellow)
if (self.index(index.row(), 1).data()) == 'def':
return QBrush(Qt.blue)
if (self.index(index.row(), 1).data()) == 'ghi':
return QBrush(Qt.magenta)
if role in (Qt.DisplayRole, Qt.EditRole):
return self._data[index.row()][index.column()]
def flags(self, index: QModelIndex) -> Qt.ItemFlags:
return Qt.ItemIsDropEnabled | Qt.ItemIsEnabled | Qt.ItemIsEditable | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled
def supportedDropActions(self) -> bool:
return Qt.MoveAction | 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(QModelIndex(), row_a, row_a, QModelIndex(), row_b)
self._data.insert(row_target, self._data.pop(row_source))
self.endMoveRows()
class myTableView(QTableView):
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)
def dropEvent(self, event):
if (event.source() is not self or
(event.dropAction() != Qt.MoveAction and
self.dragDropMode() != QAbstractItemView.InternalMove)):
super().dropEvent(event)
selection = self.selectedIndexes()
#self.clearSpans()
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 sample_data(QMainWindow):
def __init__(self):
super().__init__()
tv = myTableView(self)
tv.setModel(myModel([
["first", 'abc', 123],
["second"],
["third", 'def', 456],
["fourth", 'ghi', 789],
]))
self.setCentralWidget(tv)
tv.setSpan(1, 0, 1, 3)
self.show()
if __name__ == '__main__':
app = QApplication([])
test = sample_data()
raise SystemExit(app.exec_())
【问题讨论】:
垂直表头can be made movable的部分,所以不需要自己实现这个功能。这显然意味着垂直标题将可见,但这可以通过将部分设为空白来缓解,这将导致标题相对较窄。您还可以添加键盘快捷键来切换垂直标题的可见性。 @ekhumoro 这只是将 self.verticalHeader().hide() 替换为 self.verticalHeader().setSectionsMovable(True) 的情况吗?虽然我试过了,但垂直标题仍然隐藏。 不,还有更多。如果您有兴趣,我可以提供一个工作示例的答案。我可能应该提到的一件事是移动部分纯粹是视觉 - 底层模型永远不会被修改。不过,这在实践中并不重要,因为标题提供了从逻辑索引转换为视觉索引的方法。而且它确实带来了一些额外的好处——例如,恢复原始状态非常容易。 我对一个工作示例非常感兴趣,非常感谢。我只是对模型/视图设置有所了解。 好的 - 我已将我的 cmets 转换为答案并包含一个工作演示。 【参考方案1】:垂直标题can be made movable的部分,因此无需自己实现此功能。这显然意味着垂直标题将是可见的,但这可以通过将部分设为空白来缓解,这将导致标题相对较窄:
请注意,移动部分(而不是行)纯粹是视觉 - 永远不会修改底层模型。不过,这在实践中并不重要,因为标头提供了从logical 转换为visual 索引的方法。而且它确实带来了一些额外的好处——例如,很容易返回到以前的状态(即通过使用标头的saveState 和restoreState 方法)。
以下是基于您的示例的工作演示。可以通过拖放部分标题或在选择行时按 Alt+Up / Alt+Down 对行重新排序。按 F6 可以切换垂直标题。 逻辑行可以通过按F7来打印。
更新:
我还通过拖放行本身添加了对移动部分的支持。
from PyQt5.QtGui import QBrush
from PyQt5.QtWidgets import *
from PyQt5.QtCore import QAbstractTableModel, Qt, QModelIndex
class myModel(QAbstractTableModel):
def __init__(self, data, parent=None, *args):
super().__init__(parent, *args)
self._data = data or []
self._headers = ['Type', 'result', 'count']
def rowCount(self, index=None):
return len(self._data)
def columnCount(self, index=None):
return len(self._headers)
def headerData(self, section, orientation, role=Qt.DisplayRole):
if role == Qt.DisplayRole:
if orientation == Qt.Horizontal:
if section < 0 or section >= len(self._headers):
return ""
else:
return self._headers[section]
else:
return ''
return None
def data(self, index, role=None):
if role == Qt.TextAlignmentRole:
return Qt.AlignHCenter
if role == Qt.ForegroundRole:
return QBrush(Qt.black)
if role == Qt.BackgroundRole:
if (self.index(index.row(), 0).data().startswith('second')):
return QBrush(Qt.green)
else:
if (self.index(index.row(), 1).data()) == 'abc':
return QBrush(Qt.yellow)
if (self.index(index.row(), 1).data()) == 'def':
return QBrush(Qt.blue)
if (self.index(index.row(), 1).data()) == 'ghi':
return QBrush(Qt.magenta)
if role in (Qt.DisplayRole, Qt.EditRole):
return self._data[index.row()][index.column()]
def flags(self, index: QModelIndex) -> Qt.ItemFlags:
return Qt.ItemIsDropEnabled | Qt.ItemIsEnabled | Qt.ItemIsEditable | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled
def supportedDropActions(self) -> bool:
return Qt.MoveAction | Qt.CopyAction
class myTableView(QTableView):
def __init__(self, parent):
super().__init__(parent)
header = self.verticalHeader()
header.setSectionsMovable(True)
header.setSectionResizeMode(QHeaderView.Fixed)
header.setFixedWidth(10)
QShortcut('F7', self, self.getLogicalRows)
QShortcut('F6', self, self.toggleVerticalHeader)
QShortcut('Alt+Up', self, lambda: self.moveRow(True))
QShortcut('Alt+Down', self, lambda: self.moveRow(False))
self.setSelectionBehavior(self.SelectRows)
self.setSelectionMode(self.SingleSelection)
self.setDragDropMode(self.InternalMove)
self.setDragDropOverwriteMode(False)
def dropEvent(self, event):
if (event.source() is not self or
(event.dropAction() != Qt.MoveAction and
self.dragDropMode() != 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):
header = self.verticalHeader()
from_index = header.visualIndex(from_index)
to_index = header.visualIndex(to_index)
header.moveSection(from_index, to_index)
event.accept()
super().dropEvent(event)
def toggleVerticalHeader(self):
self.verticalHeader().setHidden(self.verticalHeader().isVisible())
def moveRow(self, up=True):
selection = self.selectedIndexes()
if selection:
header = self.verticalHeader()
row = header.visualIndex(selection[0].row())
if up and row > 0:
header.moveSection(row, row - 1)
elif not up and row < header.count() - 1:
header.moveSection(row, row + 1)
def getLogicalRows(self):
header = self.verticalHeader()
for vrow in range(header.count()):
lrow = header.logicalIndex(vrow)
index = self.model().index(lrow, 0)
print(index.data())
class sample_data(QMainWindow):
def __init__(self):
super().__init__()
tv = myTableView(self)
tv.setModel(myModel([
["first", 'abc', 123],
["second"],
["third", 'def', 456],
["fourth", 'ghi', 789],
]))
self.setCentralWidget(tv)
tv.setSpan(1, 0, 1, 3)
if __name__ == '__main__':
app = QApplication(['Test'])
test = sample_data()
test.setGeometry(600, 100, 350, 185)
test.show()
app.exec_()
【讨论】:
感谢您的帮助@ekhumoro,您的示例在从垂直标题侧边栏中拖放时有效,有没有办法通过选择您知道的行本身来拖放?不管怎样,你让我向前迈了一步,谢谢。 @MikG 我通过调整您的拖放事件处理程序添加了拖放支持,以便它移动部分。请参阅我的更新答案。 谢谢,这正是我想要实现的。我会花一些时间学习你写的东西,谢谢你的帮助。 嗨@ekhumoro,在重新排序行时,我注意到我的self._data 排序没有反映移动行的视觉变化。因此,如果我将最后一行移动到第二行,当我遍历每一行(例如 self._data 中的行)时,它会跳过第二行,只在最后执行可视的第二行(之前是最后一行) )。在 self._data 中反映拖放的最佳方式是什么? @MikG 正如我在回答中所说,底层模型从未被修改过——运动纯粹是视觉。因此,您只需在 logical 和 visual 索引之间进行转换即可获得所需的行。这是设计中经过深思熟虑的部分,因为它允许恢复原始状态。以上是关于重新排序包含跨列的 QTableView 行的主要内容,如果未能解决你的问题,请参考以下文章
pyqt qt4 QTableView 如何禁用某些列的排序?
QSqlQueryModel 配合Qtableview的表格,怎么实现点击头部排序