如果还要支持搜索过滤,再加上一层 QSortFilterProxyModel(排序过滤模型),搜索性能和代码结构都能上一个台阶。
这篇文章用两个联系人列表的例子来演示:第一个用 QAbstractListModel 做一个支持搜索、增删改的基础 C++ 模型;第二个在它的基础上引入 QSortFilterProxyModel,做到 10 万条数据实时过滤不卡顿。
C++ 数据模型:联系人列表
一个由 C++ 模型驱动的联系人列表,支持搜索、添加、编辑、删除,数据量不大但功能齐全。
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++ 模型头文件
<span>class</span> <span>ContactItemCpp</span> {
<span>public</span>:
<span>ContactItemCpp</span>(<span>const</span> QString &name, <span>const</span> QString &phone)
: <span>m_name</span>(name), <span>m_phone</span>(phone) {}
<span>QString <span>name</span><span>()</span> <span>const</span> </span>{ <span>return</span> m_name; }
<span>QString <span>phone</span><span>()</span> <span>const</span> </span>{ <span>return</span> m_phone; }
<span>QString <span>firstLetter</span><span>()</span> <span>const</span> </span>{ <span>return</span> m_name.<span>isEmpty</span>() ? <span>"?"</span> : m_name.<span>left</span>(<span>1</span>).<span>toUpper</span>(); }
<span>private</span>:
QString m_name, m_phone;
};
<span>class</span> <span>DataModelCpp</span> : <span>public</span> QAbstractListModel
{
Q_OBJECT
<span>public</span>:
<span>enum</span> <span>Roles</span> {
NameRole = Qt::UserRole + <span>1</span>,
PhoneRole,
FirstLetterRole
};
<span><span>explicit</span> <span>DataModelCpp</span><span>(QObject *parent = <span>nullptr</span>)</span></span>;
<span><span>int</span> <span>rowCount</span><span>(<span>const</span> QModelIndex &parent = QModelIndex())</span> <span>const</span> <span>override</span></span>;
<span>QVariant <span>data</span><span>(<span>const</span> QModelIndex &index, <span>int</span> role = Qt::DisplayRole)</span> <span>const</span> <span>override</span></span>;
<span>QHash<<span>int</span>, QByteArray> <span>roleNames</span><span>()</span> <span>const</span> <span>override</span></span>;
<span>Q_INVOKABLE <span>bool</span> <span>addContact</span><span>(<span>const</span> QString &name, <span>const</span> QString &phone)</span></span>;
<span>Q_INVOKABLE <span>bool</span> <span>removeContact</span><span>(<span>int</span> index)</span></span>;
<span>Q_INVOKABLE <span>bool</span> <span>editContact</span><span>(<span>int</span> index, <span>const</span> QString &name, <span>const</span> QString &phone)</span></span>;
<span>Q_INVOKABLE QVariantList <span>searchContacts</span><span>(<span>const</span> QString &keyword)</span></span>;
<span>Q_INVOKABLE <span>void</span> <span>clearSearch</span><span>()</span></span>;
<span>private</span>:
QList<ContactItemCpp> m_items;
QList<ContactItemCpp> m_originalItems;
};
继承 QAbstractListModel 必须实现三个函数:rowCount() 告诉 ListView 有多少行,data() 根据索引和角色返回对应的数据,roleNames() 把角色枚举映射成 QML 里能用的字段名字符串——比如 NameRole 对应 "name",这样 QML 的 delegate 里直接写 name 就能取到值。
C++ 模型核心实现
<span>QHash<<span>int</span>, QByteArray> <span>DataModelCpp::roleNames</span><span>()</span> <span>const</span>
</span>{
QHash<<span>int</span>, QByteArray> roles;
roles[NameRole] = <span>"name"</span>;
roles[PhoneRole] = <span>"phone"</span>;
roles[FirstLetterRole] = <span>"firstLetter"</span>;
<span>return</span> roles;
}
<span><span>bool</span> <span>DataModelCpp::addContact</span><span>(<span>const</span> QString &name, <span>const</span> QString &phone)</span>
</span>{
<span>if</span> (name.<span>isEmpty</span>() || phone.<span>isEmpty</span>()) <span>return</span> <span>false</span>;
<span>beginInsertRows</span>(<span>QModelIndex</span>(), m_items.<span>count</span>(), m_items.<span>count</span>());
m_items.<span>append</span>(<span>ContactItemCpp</span>(name, phone));
m_originalItems = m_items;
<span>endInsertRows</span>();
<span>return</span> <span>true</span>;
}
<span><span>bool</span> <span>DataModelCpp::removeContact</span><span>(<span>int</span> index)</span>
</span>{
<span>if</span> (index < <span>0</span> || index >= m_items.<span>count</span>()) <span>return</span> <span>false</span>;
<span>beginRemoveRows</span>(<span>QModelIndex</span>(), index, index);
m_items.<span>removeAt</span>(index);
<span>endRemoveRows</span>();
<span>// ... 同步删除 m_originalItems 中的对应项 ...</span>
<span>return</span> <span>true</span>;
}
<span><span>bool</span> <span>DataModelCpp::editContact</span><span>(<span>int</span> index, <span>const</span> QString &name, <span>const</span> QString &phone)</span>
</span>{
<span>if</span> (index < <span>0</span> || index >= m_items.<span>count</span>() || name.<span>isEmpty</span>() || phone.<span>isEmpty</span>())
<span>return</span> <span>false</span>;
m_items[index] = <span>ContactItemCpp</span>(name, phone);
<span>emit <span>dataChanged</span><span>(createIndex(index, <span>0</span>), createIndex(index, <span>0</span>))</span></span>;
<span>// ... 同步更新 m_originalItems 中的对应项 ...</span>
<span>return</span> <span>true</span>;
}
<span>QVariantList <span>DataModelCpp::searchContacts</span><span>(<span>const</span> QString &keyword)</span>
</span>{
<span>if</span> (keyword.<span>isEmpty</span>()) {
<span>beginResetModel</span>();
m_items = m_originalItems;
<span>endResetModel</span>();
<span>return</span> <span>QVariantList</span>();
}
<span>beginResetModel</span>();
m_items.<span>clear</span>();
<span>for</span> (<span>const</span> ContactItemCpp &item : m_originalItems) {
<span>if</span> (item.<span>name</span>().<span>contains</span>(keyword, Qt::CaseInsensitive) ||
item.<span>phone</span>().<span>contains</span>(keyword, Qt::CaseInsensitive)) {
m_items.<span>append</span>(item);
}
}
<span>endResetModel</span>();
<span>return</span> <span>QVariantList</span>();
}
写 C++ 模型最容易忘的就是那几对 begin/end 调用。添加数据前调 beginInsertRows(),加完调 endInsertRows();删除前调 beginRemoveRows(),删完调 endRemoveRows();修改完发 dataChanged() 信号。少了任何一步,ListView 都不会正确刷新。
搜索的实现比较直接——用 m_originalItems 存原始数据,搜索时把匹配结果放进 m_items,然后 beginResetModel() + endResetModel() 全量刷新。数据量小时没问题,但如果数据量很大,每次搜索都重置整个模型,视图得把所有项扔掉重建,会明显变卡,这时候就该上 ProxyModel 了。
适用场景:数据来自 C++ 后端、需要跟业务逻辑层对接的中小型列表。
ProxyModel 实时过滤:10 万条联系人
同样是联系人列表,但底层有 10 万条数据,搜索框输入一个字就实时过滤,滚动照样丝滑。靠的就是 QSortFilterProxyModel。
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 实现
<span>class</span> <span>ContactProxyModel</span> : <span>public</span> QSortFilterProxyModel
{
<span>Q_OBJECT
<span>Q_PROPERTY</span><span>(QString filterString READ filterString WRITE setFilterString NOTIFY filterStringChanged)</span>
<span>public</span>:
explicit ContactProxyModel(QObject *parent =</span> <span>nullptr</span>);
<span>QString <span>filterString</span><span>()</span> <span>const</span> </span>{ <span>return</span> m_filterString; }
<span><span>void</span> <span>setFilterString</span><span>(<span>const</span> QString &filterString)</span></span>;
<span>Q_INVOKABLE <span>bool</span> <span>removeContact</span><span>(<span>int</span> index)</span></span>;
<span>Q_INVOKABLE <span>bool</span> <span>editContact</span><span>(<span>int</span> index, <span>const</span> QString &name, <span>const</span> QString &phone)</span></span>;
signals:
<span><span>void</span> <span>filterStringChanged</span><span>()</span></span>;
<span>protected</span>:
<span><span>bool</span> <span>filterAcceptsRow</span><span>(<span>int</span> sourceRow, <span>const</span> QModelIndex &sourceParent)</span> <span>const</span> <span>override</span></span>;
<span>private</span>:
QString m_filterString;
};
QSortFilterProxyModel 是一个代理模型,它不存数据,而是套在源模型外面做过滤和排序。核心是重写 filterAcceptsRow(),返回 true 的行才会显示出来。
<span><span>bool</span> <span>ContactProxyModel::filterAcceptsRow</span><span>(<span>int</span> sourceRow, <span>const</span> QModelIndex &sourceParent)</span> <span>const</span>
</span>{
<span>if</span> (m_filterString.<span>isEmpty</span>()) <span>return</span> <span>true</span>;
QModelIndex nameIndex = <span>sourceModel</span>()-><span>index</span>(sourceRow, <span>0</span>, sourceParent);
QString name = <span>sourceModel</span>()-><span>data</span>(nameIndex, DataModelProxy::NameRole).<span>toString</span>();
QString phone = <span>sourceModel</span>()-><span>data</span>(nameIndex, DataModelProxy::PhoneRole).<span>toString</span>();
<span>return</span> name.<span>contains</span>(m_filterString, Qt::CaseInsensitive) ||
phone.<span>contains</span>(m_filterString, Qt::CaseInsensitive);
}
<span><span>void</span> <span>ContactProxyModel::setFilterString</span><span>(<span>const</span> QString &filterString)</span>
</span>{
<span>if</span> (m_filterString != filterString) {
m_filterString = filterString;
<span>emit <span>filterStringChanged</span><span>()</span></span>;
<span>invalidateFilter</span>();
}
}
<span><span>bool</span> <span>ContactProxyModel::removeContact</span><span>(<span>int</span> index)</span>
</span>{
<span>if</span> (DataModelProxy *model = <span>qobject_cast</span><DataModelProxy*>(<span>sourceModel</span>())) {
QModelIndex sourceIndex = <span>mapToSource</span>(<span>this</span>-><span>index</span>(index, <span>0</span>));
<span>return</span> model-><span>removeContact</span>(sourceIndex.<span>row</span>());
}
<span>return</span> <span>false</span>;
}
过滤条件变了之后调 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
文中给出可直接参考的选型对比与踩坑点,适合正在用 QML 做列表、需要接入 C++ 后端或优化大数据量搜索的开发者。