QML 中 QAbstractListModel 代理链更新后为何出现空项?
可以串联 QAbstractListModel 和多个 QSortFilterProxyModel,并保留增量更新。出现空项时,先检查源模型的更新通知,再检查限量代理是否正确维护索引映射和变更通知。改用 beginResetModel 后恢复显示,只能说明重建状态消除了当前异常,不能证明代理链必须依靠 reset 工作。
未能读取链接中的示例代码,因此无法确定具体出错位置。
先确认代理链的连接顺序
每层代理的 sourceModel 都应指向直接上游,QML 使用最后一层模型:
QAbstractListModel
-> 过滤代理
-> 排序代理
-> 限量代理
-> QML ListView
假设这些模型对象已经创建,连接代码可以写成:
filterProxy->setSourceModel(sourceModel);
sortProxy->setSourceModel(filterProxy);
sortProxy->setDynamicSortFilter(true);
sortProxy->sort(0, Qt::AscendingOrder);
limitProxy->setSourceModel(sortProxy);
普通过滤和排序也可以放在同一个 QSortFilterProxyModel 中。使用自定义角色排序时,还应通过 setSortRole() 指定角色。QSortFilterProxyModel 默认启用动态排序和过滤,但仍需调用 sort() 启用所需排序。Qt 代理模型文档
限量代理必须位于排序之后,才能取得排序结果中的前 X 项。
检查源模型是否正确发送增量通知
删除行时,先调用 beginRemoveRows(),再修改底层容器,最后调用 endRemoveRows()。first 和 last 使用删除前的行号,两端都包含在删除范围内。Qt 行删除约定
下面假设 ItemModel 继承 QAbstractListModel,并使用支持 removeAt() 的 m_items 保存数据:
bool ItemModel::removeRows(int row, int count,
const QModelIndex &parent)
{
if (parent.isValid() || row < 0 || count <= 0
|| row > m_items.size() - count) {
return false;
}
beginRemoveRows(QModelIndex(), row, row + count - 1);
for (int i = 0; i < count; ++i)
m_items.removeAt(row);
endRemoveRows();
return true;
}
还要检查以下几处:
- 新增数据时使用 beginInsertRows() 和 endInsertRows(),并在两次调用之间修改容器。
- 修改已有项后发送 dataChanged(),角色列表应覆盖实际变化的角色。
- 平面列表的 rowCount() 在 parent 有效时返回 0,data() 正确处理索引和角色。
- 如果在后台解析 JSON 数据,应将结果交回模型所属线程再更新模型;连接到 QML 视图的模型通常位于 GUI 线程。Qt 模型线程要求
重点检查“只保留前 X 项”的实现
上游只改动一行,也可能改变其他行是否应该显示。
例如,上游为:
A, B, C, D
限制显示 3 项时,输出为:
A, B, C
删除 B 后,正确输出是:
A, C, D
这次更新中,B 离开结果集,D 补入结果集。最终行数仍然是 3,但显示内容变了。
如果限量代理继承 QSortFilterProxyModel,却只重写 rowCount() 返回 min(X, sourceRowCount),应优先检查这里。减少返回的行数,并没有同步改变基类内部的映射和通知逻辑。
另一种写法是在 filterAcceptsRow() 中判断 source_row < X。它表达了静态筛选条件,但 source_row 是直接上游的行号;插入、删除、移动或重新排序都可能改变其他项的行号。开启 dynamicSortFilter 并不意味着这种位置相关条件在所有更新路径中都会被重新计算。
采用这种过滤方式时,需要在上游结构或顺序变化完成后重新评估限量条件,并检查边界项补入、移出时的通知。可以用 invalidate() 验证重新计算是否影响结果,但一次刷新后显示正常,仍不足以证明完整的增量处理正确。
需要严格增量更新时,单独实现限量代理
对于频繁变化的数据,可以基于 QAbstractProxyModel 实现专门的限量代理。它至少需要正确处理:
- rowCount()、index()、parent() 和 columnCount()。
- mapToSource() 与 mapFromSource(),其中被截断的源行应映射为无效索引。
- 上游插入、删除、移动、布局变化和重置。
- 上游 dataChanged() 在可见范围内的转发。
- X 改变时,可见结果集的变更通知。
这些接口必须共同描述同一个模型状态,不能只修改行数。QAbstractProxyModel 提供代理基础接口,具体的映射和变更处理仍由实现负责。Qt 自定义代理接口
如果暂时无法完成严格的增量限量代理,可以让源模型继续增量更新,仅在限量层进行正确配对的 reset。这样可以缩小重置范围,但限量层之后的视图仍会受到影响,应将其作为过渡方案。
逐层定位空项从哪里开始出现
先让 QML 直接使用源模型,然后依次加入过滤、排序、限量代理。在哪一步开始出现空项,就优先检查对应层。
开发时还可以给源模型和每一层代理分别挂上 QAbstractItemModelTester:
#include <QAbstractItemModelTester>
new QAbstractItemModelTester(
sourceModel,
QAbstractItemModelTester::FailureReportingMode::Warning,
sourceModel);
该工具用于检查模型接口的一致性,需要链接 Qt Test。它能帮助发现行数、索引等契约错误,但还需要单独核对“前 X 项”的业务结果。QAbstractItemModelTester 文档
重点验证删除首项、删除第 X 项、在前 X 项内插入,以及修改排序字段导致项目跨越限量边界的情况。这些操作容易暴露限量代理遗漏的更新路径。
备注:内容仅供参考。
