PyQt6 QTableWidget 如何按 CellWidget 属性对指定列排序?
可以在控件所在的单元格中同时设置一个 QTableWidgetItem,用它保存排序值并参与比较。文本列使用普通 item,控件列使用重写了 __lt__() 的 item,就能按各自的依据排序,无须接管整个排序过程。
为什么设置 sort-value 后没有效果?
问题中的代码只给 QPushButton 或 QComboBox 设置了动态属性,再通过 setCellWidget() 把控件放进表格。sort-value 是应用自行定义的属性名,表格不会自动把它当作排序键。
QTableWidget 排序时比较的是 QTableWidgetItem。Qt 的默认比较实现读取 item 的 Qt.ItemDataRole.DisplayRole 数据;用字符串构造 item 时,这个数据就是设置的文本。
单元格可以同时包含控件和 item:控件负责显示和交互,item 负责保存排序数据。
用自定义 item 实现控件列排序
将排序键写入 Qt.ItemDataRole.UserRole,再重写 __lt__(),让该列比较这个角色的数据。仅设置 UserRole 不会改变默认排序规则,还需要提供对应的比较逻辑。QTableWidgetItem 支持重写比较运算,参见类文档。
下面的示例包含一列普通文本,以及一列混合放置 QPushButton 和 QComboBox 的控件。示例中的排序键均为整数。
import sys
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import (
QApplication,
QComboBox,
QPushButton,
QTableWidget,
QTableWidgetItem,
)
class SortValueItem(QTableWidgetItem):
def __init__(self, value):
super().__init__()
self.setData(Qt.ItemDataRole.UserRole, int(value))
self.setFlags(self.flags() & ~Qt.ItemFlag.ItemIsEditable)
def __lt__(self, other):
return (
self.data(Qt.ItemDataRole.UserRole)
< other.data(Qt.ItemDataRole.UserRole)
)
app = QApplication(sys.argv)
table = QTableWidget(3, 2)
table.setHorizontalHeaderLabels(["名称", "控件"])
table.setSortingEnabled(False)
def update_combo_sort_value(combo, item):
value = int(combo.currentData())
combo.setProperty("sort-value", value)
item.setData(Qt.ItemDataRole.UserRole, value)
# 排序键变化后,按当前表头指示的列和方向重新排序。
if table.isSortingEnabled():
header = table.horizontalHeader()
column = header.sortIndicatorSection()
if column >= 0:
table.sortItems(column, header.sortIndicatorOrder())
rows = [("Gamma", 34), ("Alpha", 2), ("Beta", 10)]
for row, (name, value) in enumerate(rows):
table.setItem(row, 0, QTableWidgetItem(name))
if row == 1:
widget = QComboBox()
widget.addItem("低", 2)
widget.addItem("中", 10)
widget.addItem("高", 34)
widget.setCurrentIndex(widget.findData(value))
else:
widget = QPushButton(f"值:{value}")
widget.setProperty("sort-value", value)
sort_item = SortValueItem(widget.property("sort-value"))
table.setItem(row, 1, sort_item)
table.setCellWidget(row, 1, widget)
if isinstance(widget, QComboBox):
widget.currentIndexChanged.connect(
lambda _index, combo=widget, item=sort_item:
update_combo_sort_value(combo, item)
)
table.setSortingEnabled(True)
table.sortItems(0, Qt.SortOrder.AscendingOrder)
table.resize(480, 240)
table.show()
sys.exit(app.exec())
点击“名称”列表头时,表格使用普通 QTableWidgetItem 的文本进行比较。点击“控件”列表头时,表格使用整数排序键比较。示例中的初始值按升序排列为 2 < 10 < 34,与按钮文字和下拉框标签无关。
这里通过 item 数据保存控件属性的副本,因此修改控件的 sort-value 时,也要同步更新 item。示例中的下拉框通过信号完成同步,并显式调用 sortItems() 刷新顺序。
是否有排序开始前的事件?
QTableWidget 没有名为 aboutToSort 的专用公开信号。表头提供 sortIndicatorChanged(logicalIndex, order),用于通知排序指示列或方向发生变化,但它并不保证在每次排序前执行,不能作为拦截接口,参见信号文档。例如,排序值改变后对同一列再次排序,排序指示本身可以保持不变。
如果只需要更换比较依据,重写 item 的 __lt__() 就足够了。不要依赖表头信号与内部排序槽的执行顺序来临时准备数据,也不要在 __lt__() 内修改表格或触发新一轮排序。
只需要数值排序时,可以更简单
如果控件单元格不需要保留独立的 item 显示文本,可以直接把数值写入普通 item 的 DisplayRole,沿用默认比较:
# 插入和配置单元格时,先关闭排序。
item = QTableWidgetItem()
item.setData(
Qt.ItemDataRole.DisplayRole,
int(qpb.property("sort-value")),
)
table.setItem(y, x, item)
table.setCellWidget(y, x, qpb)
这种方式无需自定义 item 类。应保存整数 34,而不是字符串 "34";两者表达的排序依据不同。如果需要将显示文本与排序键分开保存,可以使用前面的 UserRole 方案。
使用时需要注意什么?
- 批量填充或更新一行的多个单元格前,先关闭排序,完成后再恢复。启用排序时,向当前排序列插入 item 可能立即移动整行,导致后续操作使用错误的行号。QTableWidget 文档也提醒了这一行为。
- 示例中的自定义控件列,每一行都需要设置
SortValueItem,并提供有效的整数排序键。缺失值、字符串或小数需要另行定义比较规则,不能直接套用整数转换。 - 排序后行号会变化。示例在信号回调中保留对应的 item 引用,没有捕获创建时的行号。如果之后删除或替换 item,也应处理相关回调,避免使用已失效的对象。
- 降序由表格的排序方向控制,
__lt__()始终表达正常的“小于”关系即可。
备注:内容仅供参考。
