QML TreeView:排序与搜索过滤

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(),性能更好。

适用场景:文件搜索、目录导航、带搜索功能的树形菜单、组织架构查找。


已验证环境:

相关推荐
Quz2 小时前
QML TreeView:基础树视图与自定义委托
qt
Junfeng的技术笔记8 小时前
基于 C++/Qt 的雷达回波信号检测与测距仿真系统
qt
Quz8 小时前
QML TableView:自定义样式与排序表格
qt
Quz8 小时前
QML TableView:可编辑表格与 SQLite 数据持久化
qt·sqlite
Quz8 小时前
QML ListView:C++ 数据模型与 ProxyModel 实时过滤
qt
Quz8 小时前
QML ListView 入门:自定义样式与数据交互
qt
cpp_learner9 小时前
QT 隐藏式密码鉴权:一键开启管理员模式
qt
慧都小项6 天前
当边缘AI上产线:QtitanDocking 如何让工业 HMI 实现多视图协同
人工智能·qt·ui·边缘计算·qt6.3
Quz6 天前
QML SwipeDelegate 滑动委托
qt