目录
引言
在上一篇中介绍了基于ListView调用外部模型进行增删改查,本文在此基础之上进行性能优化,由于篇幅有限,会省略部分代码,完整代码请看本文最后的下载链接。
本文将以一个能够流畅处理10万条联系人数据的QML应用为例,详细介绍如何利用Qt的模型-视图架构和QSortFilterProxyModel实现高性能的数据筛选和展示功能。通过这个实例,读者可以了解Qt Model & View在大数据量处理方面的优势以及相关的开发技巧。
相关阅读
工程结构
该示例项目采用了典型的Qt/QML混合开发架构,主要由C++实现数据模型,QML负责用户界面设计。下面通过mermaid图展示工程的整体结构:
核心文件说明:
- main.cpp: 应用入口,创建模型并连接到QML
- datamodel.h/cpp: 数据模型定义和实现
- Main.qml: 主界面设计
- ContactDialog.qml: 联系人添加/编辑对话框
- components/: 自定义控件目录
数据模型设计
本项目的核心在于高效的数据模型设计,主要采用了两层模型结构:
- 基础数据模型 (DataModel):继承自QAbstractListModel,负责基础数据的存储和管理
- 代理模型 (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,主要职责是存储和管理联系人数据。它具有以下几个关键特性:
- 使用了轻量级的
ContactItem
类来封装每个联系人的信息 - 定义了自定义角色枚举,用于在QML中访问数据
- 实现了必要的虚函数:rowCount、data和roleNames
- 提供了添加、删除和编辑联系人的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) << "毫秒";
}
这里有几个值得注意的性能优化点:
- 使用
reserve
预分配空间,避免频繁的内存重分配 - 使用
beginInsertRows
和endInsertRows
包裹大量数据的插入,这是一种批处理技术,减少了模型更新通知的次数 - 添加计时代码,用于测量大量数据处理的性能
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,主要负责数据的过滤和展示。它具有以下特点:
- 定义了一个Q_PROPERTY属性
filterString
,用于接收来自QML的搜索文本 - 重写了
filterAcceptsRow
方法,实现自定义的过滤逻辑 - 为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中非常强大的一个类,在本项目中使用它主要有以下几个优势:
数据与视图分离:原始数据模型(DataModel)只负责数据的存储和管理,而过滤和排序逻辑由代理模型(ContactProxyModel)处理,实现了关注点分离
高效的数据过滤:QSortFilterProxyModel内部实现了高效的过滤机制,即使在10万条数据的情况下,也能实现实时过滤而不影响UI的响应性
无需修改原始数据:过滤和排序不会修改原始数据,仅影响视图的展示,保持了数据的完整性
懒加载机制:QSortFilterProxyModel采用了懒加载机制,只有当数据需要显示时才会进行过滤操作,大大提高了性能
索引映射:代理模型自动处理了索引的映射,在删除或编辑项目时,不必手动计算筛选后的索引与原始数据的对应关系
下面的代码片段展示了这一映射机制:
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();
}
这里的关键点是:
- 创建DataModel实例作为基础数据模型
- 创建ContactProxyModel实例并通过
setSourceModel
设置其数据源 - 通过
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万条数据的过程中,采用了以下性能优化策略:
- 数据批量处理:使用beginInsertRows/endInsertRows批量添加数据,减少模型更新通知
- 内存预分配:使用reserve预分配内存空间,避免频繁的内存重分配
- 懒加载机制:利用QSortFilterProxyModel的懒加载特性,只处理需要显示的数据
- 高效数据结构:使用轻量级的ContactItem类,减少内存占用
- 视图优化: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秒),但在此基础上进行搜索过滤操作,会比较卡。以下是一些可能的扩展和优化方向:
- 分页加载:对于超大数据量,可以考虑分页加载。
- 多线程数据处理:将数据加载和处理放在单独的线程中,避免阻塞UI线程。
- 数据持久化:添加数据库支持,实现数据的持久化存储。
- 更复杂的过滤和排序:支持多条件过滤和自定义排序规则。
总结
本文详细介绍了如何利用Qt的模型-视图架构和QSortFilterProxyModel实现高性能的数据处理和展示。关键要点包括:
- 使用QAbstractListModel实现基础数据模型,负责数据的存储和管理
- 使用QSortFilterProxyModel实现代理模型,负责数据的过滤和排序
- 采用批量处理、内存预分配等技术提高性能
- 利用Qt的模型-视图架构实现数据和视图的分离,提高代码的可维护性
下载链接
完整项目源码可从以下链接获取:GitCode -> QML ListView 示例