子类化QSortFilterProxyModel中lessThan()调用次数过多问题排查
问题背景
我实现了自定义QAbstractItemModel,搭配QTreeView使用(数据是一维的,但需要QTreeView的部分显示功能),中间用了重写filterAcceptsRow()和lessThan()方法的QSortFilterProxyModel,已将DynamicSortFilter设为False,通过其他部件的信号手动触发invalidate()。
填充更多数据后,排序耗时剧增。确认lessThan()直接使用模型存储的数据做整数比较,但问题依旧。统计该函数调用次数发现远超预期:15行数据排序时,lessThan()被调用880次,其中仅前55次来自手动触发:
--- 调用链(从上到下)---
调用者:(文件: main.py, 行: 48)
调用者:(文件: mainwindow.py, 行: 60)
调用者: set_filter(文件: eventlistproxymodel.py, 行: 126)
126 self.invalidate()
其余825次调用来自内部,无法追溯完整调用链:
--- 调用链(从上到下)---
调用者:(文件: main.py, 行: 48)
48 app.exec()
数据量越大问题越严重:100多行数据需要7万+次调用,耗时2秒。额外调用次数始终等于行数 × 手动触发的调用次数,像是重复了行数遍,具体数据如下:
| 行数 | 可追溯调用次数 | 总调用次数 | 计算公式 |
|---|---|---|---|
| 6 | 9 | 63 | (1+6) * 9 = 63 |
| 15 | 55 | 880 | (1+15) * 55 = 880 |
| 42 | 184 | 7912 | (1+42) * 184 = 7912 |
我是一名医生,为简化工作自学编程,不清楚如何利用这一线索排查问题,最初怀疑伪树结构的模型或视图,但检查无果,希望得到排查思路。
相关代码片段
自定义QAbstractItemModel
class EventListModel(QAbstractItemModel): COLUMN_COUNT: int = 15 EVT_RECEIVER: int = 0 EVT_TYPE: int = 1 EVT_ID: int = 2 EVT_CATEGORY: int = 3 EVT_NAME: int = 4 EVT_REMAINAMOUNT: int = 5 EVT_TOTALAMOUNT: int = 6 EVT_PERCENTAGE: int = 7 EVT_DUEDATE: int = 8 EVT_PAYMENTTYPE: int = 9 EVT_CREATEDATE: int = 10 EVT_DESCR: int = 11 EVT_RESPONSIBLE: int = 12 EVT_TODAYSHARE: int = 13 EVT_TERMFLAGS: int = 14 internalValueRole = Qt.ItemDataRole.UserRole + 1 customSpanRole = Qt.ItemDataRole.UserRole + 2 sortRole = Qt.ItemDataRole.UserRole + 3 stats_recalculated = Signal(object) def __init__(self, parent=None): super().__init__(parent) self.root = 0 self.event_list: List = [] def rowCount(self, parent: QModelIndex = QModelIndex()) -> int: return len(self.event_list) def columnCount(self, parent: QModelIndex = QModelIndex()) -> int: return self.COLUMN_COUNT def index(self, row, column, parent: QModelIndex = QModelIndex()) -> QModelIndex: return self.createIndex(row, column, self.root) def parent(self, index: QModelIndex) -> QModelIndex: return QModelIndex() def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any: if not index.isValid(): return None try: entry = self.event_list[index.row()] except IndexError: return None if role == Qt.ItemDataRole.DisplayRole: return self.display_value(entry, index.column()) elif role == self.internalValueRole: return list(vars(entry).values())[index.column()] elif role == self.sortRole: return entry.category
自定义QSortFilterProxyModel
class EventListProxyModel(QSortFilterProxyModel): FILTER_TERM: int = 0 FILTER_CATEGORY: int = 1 FILTER_RECEIVER: int = 2 FILTER_RESPONSIBLE: int = 3 FILTER_HEADER: int = 10 FILTER_FOOTER: int = 11 def __init__(self, parent=None): super(EventListProxyModel, self).__init__(parent) self.sort(0) self.setDynamicSortFilter(False) self.sortfilter_enabled: bool = True self.term_filter: int = 0 self.category_filter: int = 0 self.receiver_filter: str = "" self.responsible_filter: str = "" self.header_filter: bool = False self.footer_filter: bool = False def enable_sortfilter(self, enable: bool): self.sortfilter_enabled = enable def set_filter(self, filter_type: int, condition: str | int | bool): if filter_type == self.FILTER_TERM: self.term_filter = condition elif filter_type == self.FILTER_CATEGORY: if condition == 0: self.category_filter = 0 else: self.category_filter = list(self.sourceModel().CATEGORIES.keys())[condition] elif filter_type == self.FILTER_RECEIVER: self.receiver_filter = condition elif filter_type == self.FILTER_RESPONSIBLE: self.responsible_filter = condition elif filter_type == self.FILTER_HEADER: self.header_filter = condition elif filter_type == self.FILTER_FOOTER: self.footer_filter = condition def filterAcceptsRow(self, source_row, source_parent): <...> return True def lessThan(self, source_left, source_right, /): if not self.sortfilter_enabled: return True left_category = self.sourceModel().index(source_left.row(), 0, source_left.parent()).data(EventListModel.sortRole) right_category = self.sourceModel().index(source_right.row(), 0, source_right.parent()).data(EventListModel.sortRole) if left_category != right_category: return left_category < right_category else: left_type = self.sourceModel().index(source_left.row(), EventListModel.EVT_TYPE, source_left.parent()).data(EventListModel.internalValueRole) right_type = self.sourceModel().index(source_right.row(), EventListModel.EVT_TYPE, source_right.parent()).data(EventListModel.internalValueRole) if left_type != right_type: return right_type > left_type else: left_duedate = self.sourceModel().index(source_left.row(), EventListModel.EVT_DUEDATE, source_left.parent()).data(EventListModel.internalValueRole) right_duedate = self.sourceModel().index(source_right.row(), EventListModel.EVT_DUEDATE, source_right.parent()).data(EventListModel.internalValueRole) return left_duedate < right_duedate
QTreeView子类
class EventWidget(QTreeView): def __init__(self, parent=None): super().__init__(parent) self.setUniformRowHeights(True) self.setRootIsDecorated(False) self.setAllColumnsShowFocus(True) self.setSortingEnabled(False) self.setItemsExpandable(False) self.setAutoScroll(True) self.setAcceptDrops(False) self.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) self.setMouseTracking(True) self.header().setFirstSectionMovable(True) self.header().setStretchLastSection(False) self.header().setTextElideMode(Qt.TextElideMode.ElideRight) def set_eventlistmodel(self, model: EventListProxyModel) -> bool: if model: self.setModel(model) if len(self.DEFAULT_COLUMN_WIDTH) != self.model().sourceModel().COLUMN_COUNT: raise ValueError return True else: return False def regain_state_after_model_changes(self): self.model().enable_sortfilter(False) for row in range(self.model().rowCount()): if self.model().index(row, 0, QModelIndex()).data(EventListModel.customSpanRole): self.setFirstColumnSpanned(row, QModelIndex(), True) self.model().enable_sortfilter(True)
触发重排序的调用代码
# Mainwindow.py self.ui.lw_term.currentRowChanged.connect(lambda row: self.ui.trw_event.model().set_filter(EventListProxyModel.FILTER_TERM, row))
问题解决(编辑补充)
在Berti先生的帮助下,修改自定义QAbstractItemModel的以下代码后,问题彻底解决:
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int: if parent.isValid(): return 0 return len(self.event_list) def columnCount(self, parent: QModelIndex = QModelIndex()) -> int: if parent.isValid(): return 0 return self.COLUMN_COUNT def index(self, row, column, parent: QModelIndex = QModelIndex()) -> QModelIndex: return self.createIndex(row, column, QModelIndex()) def parent(self, index: QModelIndex) -> QModelIndex: return QModelIndex() def hasChildren(self, /, parent = ...): if parent.isValid(): return False return self.rowCount(parent) > 0 and self.columnCount(parent) > 0
内容的提问来源于stack exchange,提问作者Aleee

