QML与C++:基于ListView调用外部模型进行增删改查(性能优化版)

发布于:2025-04-16 ⋅ 阅读:(11) ⋅ 点赞:(0)

引言

在上一篇中介绍了基于ListView调用外部模型进行增删改查,本文在此基础之上进行性能优化,由于篇幅有限,会省略部分代码,完整代码请看本文最后的下载链接

本文将以一个能够流畅处理10万条联系人数据的QML应用为例,详细介绍如何利用Qt的模型-视图架构和QSortFilterProxyModel实现高性能的数据筛选和展示功能。通过这个实例,读者可以了解Qt Model & View在大数据量处理方面的优势以及相关的开发技巧。

相关阅读


工程结构

该示例项目采用了典型的Qt/QML混合开发架构,主要由C++实现数据模型,QML负责用户界面设计。下面通过mermaid图展示工程的整体结构:

main.cpp
DataModel.h/cpp
Main.qml
QAbstractListModel
ContactProxyModel
QSortFilterProxyModel
ContactDialog.qml
components
CustomTextField.qml
IconButton.qml
CustomButton.qml

核心文件说明:

  • main.cpp: 应用入口,创建模型并连接到QML
  • datamodel.h/cpp: 数据模型定义和实现
  • Main.qml: 主界面设计
  • ContactDialog.qml: 联系人添加/编辑对话框
  • components/: 自定义控件目录

数据模型设计

本项目的核心在于高效的数据模型设计,主要采用了两层模型结构:

  1. 基础数据模型 (DataModel):继承自QAbstractListModel,负责基础数据的存储和管理
  2. 代理模型 (ContactProxyModel):继承自QSortFilterProxyModel,负责数据筛选和排序

下面详细分析这两个模型的实现:

DataModel 类
class ContactItem {
public:
    ContactItem(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;
    QString m_phone;
};

class DataModel : public QAbstractListModel
{
    Q_OBJECT

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

    explicit DataModel(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);

private:
    QList<ContactItem> m_items;
};

DataModel类继承自QAbstractListModel,主要职责是存储和管理联系人数据。它具有以下几个关键特性:

  1. 使用了轻量级的ContactItem类来封装每个联系人的信息
  2. 定义了自定义角色枚举,用于在QML中访问数据
  3. 实现了必要的虚函数:rowCount、data和roleNames
  4. 提供了添加、删除和编辑联系人的Q_INVOKABLE方法,使其可从QML直接调用

在构造函数中生成了10万条测试数据:

DataModel::DataModel(QObject *parent)
    : QAbstractListModel(parent)
{
    // 记录开始时间
    qint64 startTime = QDateTime::currentMSecsSinceEpoch();

    // 预分配空间以提高性能
    m_items.reserve(100000);

    // 开始批量插入
    beginInsertRows(QModelIndex(), 0, 99999);
    
    // 添加10万条测试数据
    for(int i = 0; i < 100000; ++i) {
        m_items.append(ContactItem(generateRandomName(), generateRandomPhone()));
    }
    
    endInsertRows();

    // 计算并输出耗时
    qint64 endTime = QDateTime::currentMSecsSinceEpoch();
    qDebug() << "添加10万条数据耗时:" << (endTime - startTime) << "毫秒";
}

这里有几个值得注意的性能优化点:

  1. 使用reserve预分配空间,避免频繁的内存重分配
  2. 使用beginInsertRowsendInsertRows包裹大量数据的插入,这是一种批处理技术,减少了模型更新通知的次数
  3. 添加计时代码,用于测量大量数据处理的性能
ContactProxyModel 类
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;
};

ContactProxyModel是整个应用的关键部分,它继承自QSortFilterProxyModel,主要负责数据的过滤和展示。它具有以下特点:

  1. 定义了一个Q_PROPERTY属性filterString,用于接收来自QML的搜索文本
  2. 重写了filterAcceptsRow方法,实现自定义的过滤逻辑
  3. 为QML提供了代理方法,将操作转发至底层的DataModel

实现部分:

ContactProxyModel::ContactProxyModel(QObject *parent)
    : QSortFilterProxyModel(parent)
{
    setFilterCaseSensitivity(Qt::CaseInsensitive);
    setSortCaseSensitivity(Qt::CaseInsensitive);
}

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

bool ContactProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
    if (m_filterString.isEmpty())
        return true;

    QModelIndex nameIndex = sourceModel()->index(sourceRow, 0, sourceParent);
    QModelIndex phoneIndex = sourceModel()->index(sourceRow, 0, sourceParent);

    QString name = sourceModel()->data(nameIndex, DataModel::NameRole).toString();
    QString phone = sourceModel()->data(phoneIndex, DataModel::PhoneRole).toString();

    return name.contains(m_filterString, Qt::CaseInsensitive) ||
           phone.contains(m_filterString, Qt::CaseInsensitive);
}

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

bool ContactProxyModel::editContact(int index, const QString &name, const QString &phone)
{
    if (DataModel *model = qobject_cast<DataModel*>(sourceModel())) {
        QModelIndex sourceIndex = mapToSource(this->index(index, 0));
        return model->editContact(sourceIndex.row(), name, phone);
    }
    return false;
}

为什么使用QSortFilterProxyModel?

QSortFilterProxyModel是Qt中非常强大的一个类,在本项目中使用它主要有以下几个优势:

  1. 数据与视图分离:原始数据模型(DataModel)只负责数据的存储和管理,而过滤和排序逻辑由代理模型(ContactProxyModel)处理,实现了关注点分离

  2. 高效的数据过滤:QSortFilterProxyModel内部实现了高效的过滤机制,即使在10万条数据的情况下,也能实现实时过滤而不影响UI的响应性

  3. 无需修改原始数据:过滤和排序不会修改原始数据,仅影响视图的展示,保持了数据的完整性

  4. 懒加载机制:QSortFilterProxyModel采用了懒加载机制,只有当数据需要显示时才会进行过滤操作,大大提高了性能

  5. 索引映射:代理模型自动处理了索引的映射,在删除或编辑项目时,不必手动计算筛选后的索引与原始数据的对应关系

下面的代码片段展示了这一映射机制:

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

这段代码通过mapToSource将代理模型中的索引映射回源模型中的索引,然后调用源模型的removeContact方法。这种机制在处理筛选后的数据时尤为重要,因为筛选后的索引与原始数据的索引并不一致。

应用初始化与模型连接

在main.cpp中,创建数据模型和代理模型,并将它们连接起来:

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    QQmlApplicationEngine engine;
    QObject::connect(
        &engine,
        &QQmlApplicationEngine::objectCreationFailed,
        &app,
        []() { QCoreApplication::exit(-1); },
        Qt::QueuedConnection);

    // 创建数据模型实例
    DataModel *model = new DataModel(&engine);
    
    // 创建代理模型实例
    ContactProxyModel *proxyModel = new ContactProxyModel(&engine);
    proxyModel->setSourceModel(model);
    
    // 将模型暴露给QML
    engine.rootContext()->setContextProperty("contactModel", proxyModel);

    engine.loadFromModule("qml_listview_model", "Main");

    return app.exec();
}

这里的关键点是:

  1. 创建DataModel实例作为基础数据模型
  2. 创建ContactProxyModel实例并通过setSourceModel设置其数据源
  3. 通过setContextProperty将代理模型暴露给QML,使得QML可以直接访问和操作模型

UI实现

应用的UI部分主要通过QML实现,包括主界面(Main.qml)和联系人编辑对话框(ContactDialog.qml)。UI设计采用了现代的Material Design风格,具有搜索框、联系人列表和操作按钮等组件。

Main.qml的核心部分是ListView组件,它绑定的代理模型:

ListView {
    id: contactListView
    anchors.fill: parent
    anchors.rightMargin: scrollBar.width
    model: contactModel
    spacing: 10
    clip: true

    // 启用滚动条绑定
    ScrollBar.vertical: scrollBar

    delegate: Rectangle {
        // 联系人项的UI实现
        // ...
    }
}

搜索功能的实现非常简洁:

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

当用户在搜索框中输入文本时,onTextChanged事件会将文本赋值给代理模型的filterString属性。然后代理模型会自动应用过滤逻辑并更新视图。


性能分析与优化

在处理10万条数据的过程中,采用了以下性能优化策略:

  1. 数据批量处理:使用beginInsertRows/endInsertRows批量添加数据,减少模型更新通知
  2. 内存预分配:使用reserve预分配内存空间,避免频繁的内存重分配
  3. 懒加载机制:利用QSortFilterProxyModel的懒加载特性,只处理需要显示的数据
  4. 高效数据结构:使用轻量级的ContactItem类,减少内存占用
  5. 视图优化:ListView中使用clip属性限制渲染区域,减少不必要的绘制

在DataModel的构造函数中,增加了性能测量代码:

qint64 startTime = QDateTime::currentMSecsSinceEpoch();
// 添加数据的代码...
qint64 endTime = QDateTime::currentMSecsSinceEpoch();
qDebug() << "添加10万条数据耗时:" << (endTime - startTime) << "毫秒";

在本机上,这段代码只需130毫秒就能完成10万条数据的加载。而在搜索过滤时,即使是10万条数据,响应也是即时的,不会出现明显的延迟。

运行效果

联系人列表

如图所示,应用在加载10万条联系人数据后仍能保持流畅的操作体验,包括:

  • 滚动列表时没有明显卡顿
  • 搜索过滤响应及时
  • 编辑操作即时反映在界面上

扩展思考

本示例已经能够处理10万级的数据量并通过UI展示出来,然后继续尝试加载百万级数据(在本机,ListView通过模型加载1百万条数据需要1.3秒),但在此基础上进行搜索过滤操作,会比较卡。以下是一些可能的扩展和优化方向:

  1. 分页加载:对于超大数据量,可以考虑分页加载。
  2. 多线程数据处理:将数据加载和处理放在单独的线程中,避免阻塞UI线程。
  3. 数据持久化:添加数据库支持,实现数据的持久化存储。
  4. 更复杂的过滤和排序:支持多条件过滤和自定义排序规则。

总结

本文详细介绍了如何利用Qt的模型-视图架构和QSortFilterProxyModel实现高性能的数据处理和展示。关键要点包括:

  1. 使用QAbstractListModel实现基础数据模型,负责数据的存储和管理
  2. 使用QSortFilterProxyModel实现代理模型,负责数据的过滤和排序
  3. 采用批量处理、内存预分配等技术提高性能
  4. 利用Qt的模型-视图架构实现数据和视图的分离,提高代码的可维护性

下载链接

完整项目源码可从以下链接获取:GitCode -> QML ListView 示例

QML-ListView示例


网站公告

今日签到

点亮在社区的每一天
去签到