只用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,前期多写几行代码,后面省事很多。
已验证环境:
- Qt 版本:Qt 6.11.1
- 操作系统:Windows 11
- GitHub:QML-Minimal-Demos/qml_listview