TreeView 本身不处理排序过滤,得靠代理模型来做一层 QSortFilterProxyModel,排序和过滤都交给它,TreeView 用于展示排序/过滤结果。树的排序过滤跟列表有点不一样,因为有层级关系,处理不好容易把父节点过滤掉导致子节点丢失。
这篇文章用两个例子演示树的数据操作:第一个例子做名称排序,同级节点按字母重排;第二个例子做实时搜索过滤,输入关键字后命中的节点连同它的所有父节点一起保留,过滤完自动全部展开。
下面的代码中主要展示了QML代码,不想让篇幅太长,于是省略了TreeModel、TreeSortFilterModel等重要的c++部分,在文末的Github链接中有完整的工程。
名称排序
简单的文件树,点一下按钮就能在升序和降序之间切换。

演示代码
qml
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
FadeInAnimation {
TreeModel { id: sourceModel }
TreeSortFilterModel { id: proxyModel; sourceModel: sourceModel }
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 15
// ... 省略标题组件 TitleSeparator ...
// 控制条
RowLayout {
Layout.fillWidth: true
spacing: 12
Button {
text: proxyModel.ascending ? "升序 A → Z" : "降序 Z → A"
onClicked: proxyModel.ascending = !proxyModel.ascending
}
Text {
Layout.fillWidth: true
text: "点击按钮切换"
color: "#888"
font.pixelSize: 12
}
}
// 内容卡片
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: "#fafafa"
radius: 8
border.color: "#e0e0e0"
border.width: 1
TreeView {
id: treeView
anchors.fill: parent
anchors.margins: 6
clip: true
ScrollBar.vertical: ScrollBar {}
model: proxyModel
delegate: TreeViewDelegate {
id: nodeDelegate
implicitHeight: 28
palette.highlight: "#1296FF"
indicator: TreeArrow {
x: nodeDelegate.leftMargin + nodeDelegate.depth * nodeDelegate.indentation
y: (parent.height - height) / 2
expanded: nodeDelegate.expanded
}
}
selectionModel: ItemSelectionModel {}
columnWidthProvider: function(column) { return treeView.width }
Component.onCompleted: expandRecursively()
}
}
}
}
排序的思路是「双模型」:sourceModel 存原始数据不动,TreeSortFilterModel 套在外面做排序代理,TreeView 直接绑定代理模型。切换排序的时候只改代理的 ascending 属性,源模型完全不受影响。
代理模型继承自 QSortFilterProxyModel,排序逻辑 Qt 已经封装好了,直接调 sort() 方法就行。
树的排序是按层级进行的,同一父节点下的兄弟节点互相比较排序,父节点始终带着自己的子树一起,不会出现子节点跑到别的父节点下面的情况。
按钮上的文字和 ascending 属性绑定,升序显示「A → Z」,降序显示「Z → A」。排序切换是即时的,TreeView 会自动刷新,不用手动调任何方法。
适用场景:文件浏览器排序、分类列表排序、任何需要按名称排列的树形数据。
搜索过滤
输入关键字对树进行实时过滤,命中的节点会保留下来,它的所有父节点也会一起保留,不然就找不到它在哪一层了。

演示代码
qml
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
FadeInAnimation {
TreeModel { id: sourceModel }
TreeSortFilterModel {
id: proxyModel
sourceModel: sourceModel
filterText: filterField.text
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 15
// ... 省略标题组件 TitleSeparator ...
// 控制条
RowLayout {
Layout.fillWidth: true
spacing: 12
TextField {
id: filterField
Layout.preferredWidth: 230
placeholderText: "输入关键字,如 qml / cpp"
selectByMouse: true
onTextChanged: treeView.expandRecursively()
}
Text {
Layout.fillWidth: true
text: proxyModel.filterText.length > 0
? "命中 " + proxyModel.matchCount + " 个节点"
: "共 " + proxyModel.matchCount + " 个节点"
color: "#888888"
font.pixelSize: 12
}
}
// 内容卡片
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: "#fafafa"
radius: 8
border.color: "#e0e0e0"
border.width: 1
TreeView {
id: treeView
anchors.fill: parent
anchors.margins: 6
clip: true
ScrollBar.vertical: ScrollBar {}
model: proxyModel
delegate: TreeViewDelegate {
id: nodeDelegate
implicitHeight: 28
palette.highlight: "#1296FF"
indicator: TreeArrow {
x: nodeDelegate.leftMargin + nodeDelegate.depth * nodeDelegate.indentation
y: (parent.height - height) / 2
expanded: nodeDelegate.expanded
}
}
selectionModel: ItemSelectionModel {}
columnWidthProvider: function(column) { return treeView.width }
Component.onCompleted: expandRecursively()
}
}
}
}
过滤跟排序用的是同一个代理模型 TreeSortFilterModel,filterText 属性直接绑定搜索框的文字,输入什么就过滤什么。
树的过滤有个关键细节------不能只留下命中的节点。如果一个子节点命中了但父节点被过滤掉了,那这个子节点在树里就找不到了,等于白搭。所以代理模型里打开了属性 setRecursiveFilteringEnabled(true),这个属性的作用就是:只要某个节点的子孙里有命中的,这个节点本身也会被保留。这样过滤完树的结构还是完整的,命中的节点能正常显示出来。
搜索框的 onTextChanged 里调了 expandRecursively(),每次过滤条件变了就全部展开。不然过滤完可能所有节点都是折叠状态,用户还得一个个点开,体验不好。
过滤条件更新用了 beginFilterChange() 和 endFilterChange(),这是 Qt 6.9 以后推荐的写法,替代了以前的 invalidateFilter(),性能更好。
适用场景:文件搜索、目录导航、带搜索功能的树形菜单、组织架构查找。
已验证环境:
- Qt 版本:Qt 6.11.1
- 操作系统:Windows 11
- GitHub:QML-Minimal-Demos/qml_treeview