QML ListView:C++ 数据模型与 ProxyModel 实时过滤

只用QML中的 ListModel 写起来方便,但数据量大了或者数据来自后端接口,就显得力不从心。这时候就得上 C++ 模型,基于 QAbstractListModel 实现的一个数据层,QML端只处理UI展示,分工清晰。

如果还要支持搜索过滤,再加上一层 QSortFilterProxyModel(排序过滤模型),搜索性能和代码结构都能上一个台阶。

这篇文章用两个联系人列表的例子来演示:第一个用 QAbstractListModel 做一个支持搜索、增删改的基础 C++ 模型;第二个在它的基础上引入 QSortFilterProxyModel,做到 10 万条数据实时过滤不卡顿。

C++ 数据模型:联系人列表

一个由 C++ 模型驱动的联系人列表,支持搜索、添加、编辑、删除,数据量不大但功能齐全。

QML 代码

qml 复制代码
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "./components"

// Demo:C++ 数据模型驱动的联系人列表(少量数据,含搜索/增删改)
FadeInAnimation {

    property int currentEditIndex: -1

    // ... 省略添加/编辑联系人对话框 ContactDialog ...

    ColumnLayout {
        anchors.fill: parent
        anchors.margins: 20
        spacing: 15

        // ... 省略标题组件 TitleSeparator ...

        // 工具栏
        Rectangle {
            Layout.fillWidth: true
            height: 44
            color: "#f0f0f0"
            radius: 5

            RowLayout {
                anchors.fill: parent
                anchors.margins: 6
                spacing: 8

                CustomTextField {
                    id: searchFieldCpp
                    Layout.fillWidth: true
                    placeholderText: "搜索联系人..."
                    leftIcon: "qrc:/icons/find.png"
                    onTextChanged: dataModelCpp.searchContacts(text)
                    onRightIconClicked: {
                        text = ""
                        dataModelCpp.clearSearch()
                    }
                }

                IconButton {
                    text: "添加"
                    iconSource: "qrc:/icons/add.png"
                    showBackground: true
                    backgroundColor: "#BBDEFB"
                    onClicked: addContactDialog.open()
                }
            }
        }

        // 列表
        ListView {
            id: cppListView
            Layout.fillWidth: true
            Layout.fillHeight: true
            model: dataModelCpp
            spacing: 6
            clip: true

            delegate: Rectangle {
                width: cppListView.width
                height: 55
                color: "#f5f5f5"
                radius: 5

                MouseArea {
                    anchors.fill: parent
                    hoverEnabled: true
                    onEntered: parent.color = "#e8e8e8"
                    onExited:  parent.color = "#f5f5f5"
                }

                RowLayout {
                    anchors.fill: parent
                    anchors.margins: 8
                    spacing: 10

                    // 首字母头像
                    Rectangle {
                        width: 36; height: 36; radius: 18
                        color: {
                            const colors = ["#FF6B6B","#4ECDC4","#45B7D1","#96CEB4","#D4A5A5","#9B59B6"]
                            return colors[firstLetter.charCodeAt(0) % colors.length]
                        }
                        Text {
                            anchors.centerIn: parent
                            text: firstLetter
                            color: "white"
                            font.pixelSize: 16
                            font.bold: true
                        }
                    }

                    ColumnLayout {
                        Layout.fillWidth: true
                        spacing: 4
                        Text { text: name;  font.bold: true; font.pixelSize: 14; Layout.fillWidth: true }
                        Text { text: phone; color: "#666"; font.pixelSize: 12; Layout.fillWidth: true }
                    }

                    RowLayout {
                        spacing: 6
                        IconButton {
                            iconSource: "qrc:/icons/edit.png"
                            onClicked: {
                                currentEditIndex = index
                                editContactDialog.currentName  = name
                                editContactDialog.currentPhone = phone
                                editContactDialog.open()
                            }
                        }
                        IconButton {
                            iconSource: "qrc:/icons/delete.png"
                            onClicked: dataModelCpp.removeContact(index)
                        }
                    }
                }
            }
        }
    }
}

QML 这边看起来跟用 ListModel 差不多,model 直接指向 dataModelCpp,delegate 里的 name、phone、firstLetter 这些字段直接用就行。区别在于这个 dataModelCpp 是从 C++ 那边注册进来的,搜索调用 searchContacts()、删除调用 removeContact()、编辑通过对话框回调 editContact(),全是 C++ 暴露的 Q_INVOKABLE 方法。

C++ 模型头文件

cpp 复制代码
class ContactItemCpp {
public:
    ContactItemCpp(const QString &name, const QString &phone)
        : m_name(name), m_phone(phone) {}
    QString name()        const { return m_name; }
    QString phone()       const { return m_phone; }
    QString firstLetter() const { return m_name.isEmpty() ? "?" : m_name.left(1).toUpper(); }
private:
    QString m_name, m_phone;
};

class DataModelCpp : public QAbstractListModel
{
    Q_OBJECT
public:
    enum Roles {
        NameRole = Qt::UserRole + 1,
        PhoneRole,
        FirstLetterRole
    };

    explicit DataModelCpp(QObject *parent = nullptr);

    int     rowCount(const QModelIndex &parent = QModelIndex()) const override;
    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
    QHash<int, QByteArray> roleNames() const override;

    Q_INVOKABLE bool addContact(const QString &name, const QString &phone);
    Q_INVOKABLE bool removeContact(int index);
    Q_INVOKABLE bool editContact(int index, const QString &name, const QString &phone);
    Q_INVOKABLE QVariantList searchContacts(const QString &keyword);
    Q_INVOKABLE void clearSearch();

private:
    QList<ContactItemCpp> m_items;
    QList<ContactItemCpp> m_originalItems;
};

继承 QAbstractListModel 必须实现三个函数:rowCount() 告诉 ListView 有多少行,data() 根据索引和角色返回对应的数据,roleNames() 把角色枚举映射成 QML 里能用的字段名字符串------比如 NameRole 对应 "name",这样 QML 的 delegate 里直接写 name 就能取到值。

C++ 模型核心实现

cpp 复制代码
QHash<int, QByteArray> DataModelCpp::roleNames() const
{
    QHash<int, QByteArray> roles;
    roles[NameRole]        = "name";
    roles[PhoneRole]       = "phone";
    roles[FirstLetterRole] = "firstLetter";
    return roles;
}

bool DataModelCpp::addContact(const QString &name, const QString &phone)
{
    if (name.isEmpty() || phone.isEmpty()) return false;
    beginInsertRows(QModelIndex(), m_items.count(), m_items.count());
    m_items.append(ContactItemCpp(name, phone));
    m_originalItems = m_items;
    endInsertRows();
    return true;
}

bool DataModelCpp::removeContact(int index)
{
    if (index < 0 || index >= m_items.count()) return false;
    beginRemoveRows(QModelIndex(), index, index);
    m_items.removeAt(index);
    endRemoveRows();
    // ... 同步删除 m_originalItems 中的对应项 ...
    return true;
}

bool DataModelCpp::editContact(int index, const QString &name, const QString &phone)
{
    if (index < 0 || index >= m_items.count() || name.isEmpty() || phone.isEmpty())
        return false;
    m_items[index] = ContactItemCpp(name, phone);
    emit dataChanged(createIndex(index, 0), createIndex(index, 0));
    // ... 同步更新 m_originalItems 中的对应项 ...
    return true;
}

QVariantList DataModelCpp::searchContacts(const QString &keyword)
{
    if (keyword.isEmpty()) {
        beginResetModel();
        m_items = m_originalItems;
        endResetModel();
        return QVariantList();
    }
    beginResetModel();
    m_items.clear();
    for (const ContactItemCpp &item : m_originalItems) {
        if (item.name().contains(keyword, Qt::CaseInsensitive) ||
            item.phone().contains(keyword, Qt::CaseInsensitive)) {
            m_items.append(item);
        }
    }
    endResetModel();
    return QVariantList();
}

写 C++ 模型最容易忘的就是那几对 begin/end 调用。添加数据前调 beginInsertRows(),加完调 endInsertRows();删除前调 beginRemoveRows(),删完调 endRemoveRows();修改完发 dataChanged() 信号。少了任何一步,ListView 都不会正确刷新。

搜索的实现比较直接------用 m_originalItems 存原始数据,搜索时把匹配结果放进 m_items,然后 beginResetModel() + endResetModel() 全量刷新。数据量小时没问题,但如果数据量很大,每次搜索都重置整个模型,视图得把所有项扔掉重建,会明显变卡,这时候就该上 ProxyModel 了。

适用场景:数据来自 C++ 后端、需要跟业务逻辑层对接的中小型列表。

ProxyModel 实时过滤:10 万条联系人

同样是联系人列表,但底层有 10 万条数据,搜索框输入一个字就实时过滤,滚动照样丝滑。靠的就是 QSortFilterProxyModel。

QML 代码

qml 复制代码
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "./components"

// Demo:QSortFilterProxyModel 大数据量联系人列表(10万条,实时过滤)
FadeInAnimation {

    property int currentEditIndex: -1

    // ... 省略添加/编辑联系人对话框 ContactDialog ...

    ColumnLayout {
        anchors.fill: parent
        anchors.margins: 20
        spacing: 15

        // ... 省略标题组件 TitleSeparator ...

        // 工具栏
        Rectangle {
            Layout.fillWidth: true
            height: 44
            color: "#f0f0f0"
            radius: 5

            RowLayout {
                anchors.fill: parent
                anchors.margins: 6
                spacing: 8

                CustomTextField {
                    id: searchFieldModel
                    Layout.fillWidth: true
                    placeholderText: "搜索联系人..."
                    leftIcon: "qrc:/icons/find.png"
                    onTextChanged: contactProxyModel.filterString = text
                    onRightIconClicked: {
                        text = ""
                        contactProxyModel.filterString = ""
                    }
                }

                IconButton {
                    text: "添加"
                    iconSource: "qrc:/icons/add.png"
                    showBackground: true
                    backgroundColor: "#BBDEFB"
                    onClicked: addContactDialogModel.open()
                }
            }
        }

        // 列表区域(带滚动条)
        Rectangle {
            Layout.fillWidth: true
            Layout.fillHeight: true
            color: "#ffffff"
            radius: 5

            Item {
                anchors.fill: parent
                anchors.margins: 1

                ListView {
                    id: proxyListView
                    anchors.fill: parent
                    anchors.rightMargin: proxyScrollBar.width
                    model: contactProxyModel
                    spacing: 6
                    clip: true
                    ScrollBar.vertical: proxyScrollBar

                    delegate: Rectangle {
                        width: proxyListView.width
                        height: 55
                        color: "#f5f5f5"
                        radius: 5

                        MouseArea {
                            anchors.fill: parent
                            hoverEnabled: true
                            onEntered: { parent.color = "#e8e8e8"; rowTip.visible = true }
                            onExited:  { parent.color = "#f5f5f5"; rowTip.visible = false }
                        }

                        ToolTip {
                            id: rowTip
                            text: {
                                const n = index + 1
                                return "第 " + n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + " 行"
                            }
                            delay: 500
                            timeout: 5000
                            visible: false
                        }

                        RowLayout {
                            anchors.fill: parent
                            anchors.margins: 8
                            spacing: 10

                            Rectangle {
                                width: 36; height: 36; radius: 18
                                color: {
                                    const colors = ["#FF6B6B","#4ECDC4","#45B7D1","#96CEB4","#D4A5A5","#9B59B6"]
                                    return colors[firstLetter.charCodeAt(0) % colors.length]
                                }
                                Text {
                                    anchors.centerIn: parent
                                    text: firstLetter
                                    color: "white"
                                    font.pixelSize: 18
                                    font.bold: true
                                }
                            }

                            ColumnLayout {
                                Layout.fillWidth: true
                                spacing: 4
                                Text { text: name;  font.bold: true; font.pixelSize: 14; Layout.fillWidth: true }
                                Text { text: phone; color: "#666";  font.pixelSize: 12; Layout.fillWidth: true }
                            }

                            RowLayout {
                                spacing: 6
                                IconButton {
                                    iconSource: "qrc:/icons/edit.png"
                                    onClicked: {
                                        currentEditIndex = index
                                        editContactDialogModel.currentName  = name
                                        editContactDialogModel.currentPhone = phone
                                        editContactDialogModel.open()
                                    }
                                }
                                IconButton {
                                    iconSource: "qrc:/icons/delete.png"
                                    onClicked: contactProxyModel.removeContact(index)
                                }
                            }
                        }
                    }
                }

                ScrollBar {
                    id: proxyScrollBar
                    anchors.right: parent.right
                    anchors.top: parent.top
                    anchors.bottom: parent.bottom
                    active: true
                    interactive: true
                    orientation: Qt.Vertical
                }
            }
        }
    }
}

QML 层的变化不大,model 换成了 contactProxyModel,搜索时给 filterString 属性赋值就行,不用再调搜索函数。删除和编辑也是直接调 proxy 模型的方法,proxy 会自动把索引映射回源模型。

列表右边加了一个常驻的 ScrollBar,数据量大的时候拖动滚动条定位比滑快多了。每行悬停还会出一个 ToolTip 显示当前是第几行,10 万条数据里能有个位置感。

C++ ProxyModel 实现

cpp 复制代码
class ContactProxyModel : public QSortFilterProxyModel
{
    Q_OBJECT
    Q_PROPERTY(QString filterString READ filterString WRITE setFilterString NOTIFY filterStringChanged)

public:
    explicit ContactProxyModel(QObject *parent = nullptr);

    QString filterString() const { return m_filterString; }
    void setFilterString(const QString &filterString);

    Q_INVOKABLE bool removeContact(int index);
    Q_INVOKABLE bool editContact(int index, const QString &name, const QString &phone);

signals:
    void filterStringChanged();

protected:
    bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;

private:
    QString m_filterString;
};

QSortFilterProxyModel 是一个代理模型,它不存数据,而是套在源模型外面做过滤和排序。核心是重写 filterAcceptsRow(),返回 true 的行才会显示出来。

cpp 复制代码
bool ContactProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
    if (m_filterString.isEmpty()) return true;
    QModelIndex nameIndex  = sourceModel()->index(sourceRow, 0, sourceParent);
    QString name  = sourceModel()->data(nameIndex, DataModelProxy::NameRole).toString();
    QString phone = sourceModel()->data(nameIndex, DataModelProxy::PhoneRole).toString();
    return name.contains(m_filterString,  Qt::CaseInsensitive) ||
           phone.contains(m_filterString, Qt::CaseInsensitive);
}

void ContactProxyModel::setFilterString(const QString &filterString)
{
    if (m_filterString != filterString) {
        m_filterString = filterString;
        emit filterStringChanged();
        invalidateFilter();
    }
}

bool ContactProxyModel::removeContact(int index)
{
    if (DataModelProxy *model = qobject_cast<DataModelProxy*>(sourceModel())) {
        QModelIndex sourceIndex = mapToSource(this->index(index, 0));
        return model->removeContact(sourceIndex.row());
    }
    return false;
}

过滤条件变了之后调 invalidateFilter(),proxy 会自动重新跑一遍过滤。它比 C++ 全量重建方案强的地方,在于不用 beginResetModel() + endResetModel() 触发视图整体重建------ListView 不用扔掉已有的项重建,滚动位置、正在显示的 delegate 都更稳,这是它在大数据量下体验好的关键原因。

增删改操作有个容易踩的坑:QML 里拿到的 index 是 proxy 模型的索引,不是源模型的。删除之前必须用 mapToSource() 把 proxy 索引转换成源模型索引,不然删的就不是你想删的那条了。编辑也是同理。

源模型那边就是标准的 QAbstractListModel 实现,构造函数里一次性生成 10 万条随机联系人数据,用 beginInsertRows() + endInsertRows() 批量插入,比一条一条加快很多。

适用场景:大数据量列表、需要实时搜索过滤、需要排序的场景。

两种方案怎么选

C++ 直接实现搜索 QSortFilterProxyModel 过滤
数据量 小到中等(千级以内) 大(万级以上)
刷新方式 每次 beginResetModel() 重建整个视图 invalidateFilter() 重跑过滤,不重建视图
大数据下体验 重置会丢掉滚动位置、重建项,会卡 索引统一由 proxy 映射,滚动/编辑更稳
代码复杂度 简单,一个类搞定 多一层代理,索引要转换
排序支持 自己实现 自带,setSortRole() 就能用
适用场景 简单业务列表、数据量可控 通讯录、商品列表、日志列表

简单说:数据少、功能简单,直接在 QAbstractListModel 里写搜索就行;数据多或者以后可能要加排序,直接上 QSortFilterProxyModel,前期多写几行代码,后面省事很多。


已验证环境:

相关推荐
Quz1 小时前
QML ListView 入门:自定义样式与数据交互
qt
cpp_learner1 小时前
QT 隐藏式密码鉴权:一键开启管理员模式
qt
慧都小项6 天前
当边缘AI上产线:QtitanDocking 如何让工业 HMI 实现多视图协同
人工智能·qt·ui·边缘计算·qt6.3
Quz6 天前
QML SwipeDelegate 滑动委托
qt
Quz6 天前
QML 列表中的四种委托:ItemDelegate、CheckDelegate、RadioDelegate、SwitchDelegate
qt
实心儿儿6 天前
Qt — 显示类控件
qt
江湖人称菠萝包6 天前
【Qt】《Qt 5.9 C++开发指南》笔记-Chapter9-Qt Charts
笔记·qt·qt5
扶尔魔ocy6 天前
【QT android】环境安装及第一个QT项目
qt·andriod开发
江湖人称菠萝包7 天前
【Qt】《Qt 5.9 C++开发指南》笔记-Chapter10-Data Visualization
笔记·qt·qt5