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

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


已验证环境:

相关推荐
沫璃染墨21 小时前
《Qt从零入门系列(十四):QThread多线程详解——从线程创建到客户端阻塞处理》
开发语言·c++·笔记·qt·ui·交互·线程
代钦塔拉21 小时前
【C++/Qt 进阶】设计模式系列(5):适配器模式——给“不兼容的接口“贴一层翻译膜
c++·qt·设计模式
把我格子衫拿来1 天前
VTK 9.5 与 Qt 6 联合开发:从嵌入三维窗口到可维护的桌面架构
c++·python·qt·vtk·三维可视化
把我格子衫拿来2 天前
VTK 与 Qt 的 CMake 工程怎么组织?模块、工具链与二进制兼容排错
c++·qt·vtk·cmake·工程实践
代钦塔拉2 天前
【C++/Qt 进阶】设计模式系列(4):原型模式——用“克隆“代替“重新构造“
c++·qt·设计模式
Python图像识别2 天前
56-【2027毕设】YOLO11X光安检危险物品检测系统 - Python完整源码+PyQt5界面+训练模型+数据集
python·qt·课程设计
把我格子衫拿来2 天前
鼠标点到哪里才算选中?Qt 与 VTK 的拾取坐标、DPI 与体素定位
python·qt·vtk·坐标变换·三维交互
Lhan.zzZ2 天前
Qt WebEngineView 在 Linux 上无法加载网页:从“看不见 HTML“到“禁用沙箱“
linux·c++·qt
Quz3 天前
QML TabBar 标签栏交互效果合集
qt·动画·qml
zhanghaha13143 天前
Qt 工程实例:逐词逐句保姆级讲解
qt