PAYATHON 2026

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 项内插入,以及修改排序字段导致项目跨越限量边界的情况。这些操作容易暴露限量代理遗漏的更新路径。

备注:内容仅供参考。