TreeView 是 Qt Quick 里用来展示树形结构数据的组件,像文件目录、组织结构、菜单分类这类有层级关系的数据,用 TreeView 展示再合适不过。跟 ListView、TableView 不同,TreeView 的数据依赖 QAbstractItemModel,也就是说树数据得由 C++ 提供,QML 这边只管展示。
这篇文章展示2个简单的例子:第一个例子用内置的 TreeViewDelegate,几行代码就能搭出一个能展开折叠的树;第二个例子完全自绘 delegate,做出文件管理器风格的树。
基础树视图
一个最简单的树,结构清晰,代码量也最少。
演示代码
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
FadeInAnimation {
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 15
// ... 省略标题组件 TitleSeparator ...
// 内容卡片
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: TreeModel {}
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()
}
}
}
}
TreeView 的 model 必须是 QAbstractItemModel 类型,纯 QML 搞不定,所以数据由 C++ 的 TreeModel 提供。这个模型用 QML_NAMED_ELEMENT 注册成了 QML 类型,直接 TreeModel {} 就能用,不用在 main.cpp 里手动注册。
TreeViewDelegate 是 Qt 自带的树节点委托,缩进、展开箭头、选中高亮这些基础功能都封装好了,拿来就能用。行高要写 implicitHeight 而不是 height,因为 TableView 一族的组件会忽略直接设置的 height,只认 implicitHeight。
默认的展开箭头是位图,这里换成了自定义的 SVG 箭头组件 TreeArrow,通过 indicator 属性替换进去。换掉 indicator 之后它就不再自动管理缩进了,所以 x 坐标要自己算:左边距 + 深度 × 每层缩进量。
selectionModel 不设的话单击行不会变成当前项,选中高亮也就出不来。用 ItemSelectionModel 就行,TreeView 会自动接管。
columnWidthProvider 把列宽设成跟视图一样宽,这样行背景色能铺满整行,不然列宽只按文字长度算,选中高亮只有文字那一小截,看起来很怪。
最后 expandRecursively() 让树初始全部展开,一进来就能看到完整层级,不用用户手动点开。
自定义委托
一个文件管理器风格的树,每个节点有文件夹或文件图标。
演示代码
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
FadeInAnimation {
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 15
// ... 省略标题组件 TitleSeparator ...
// 内容卡片
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: TreeModel {}
selectionModel: ItemSelectionModel {}
columnWidthProvider: function(column) { return treeView.width }
Component.onCompleted: expandRecursively()
delegate: Item {
id: nodeItem
required property TreeView treeView
required property bool isTreeNode
required property bool expanded
required property bool hasChildren
required property int depth
required property int row
required property int column
required property var model
readonly property real indentation: 20
readonly property real padding: 6
readonly property real arrowSlot: 16
readonly property bool isCurrent: nodeItem.row === nodeItem.treeView.currentRow
implicitHeight: 28
Rectangle {
anchors.fill: parent
color: nodeItem.isCurrent ? "#E3F1FF" : (hoverHandler.hovered ? "#EDEDED" : "transparent")
}
Rectangle {
anchors { left: parent.left; top: parent.top; bottom: parent.bottom }
width: 3
visible: nodeItem.isCurrent
color: "#1296FF"
}
HoverHandler { id: hoverHandler }
TreeArrow {
id: indicator
x: nodeItem.padding + nodeItem.depth * nodeItem.indentation
anchors.verticalCenter: parent.verticalCenter
visible: nodeItem.isTreeNode && nodeItem.hasChildren
expanded: nodeItem.expanded
TapHandler {
onSingleTapped: {
const index = nodeItem.treeView.index(nodeItem.row, nodeItem.column)
nodeItem.treeView.selectionModel.setCurrentIndex(index, ItemSelectionModel.NoUpdate)
nodeItem.treeView.toggleExpanded(nodeItem.row)
}
}
}
Image {
id: icon
x: nodeItem.padding + nodeItem.depth * nodeItem.indentation + nodeItem.arrowSlot
anchors.verticalCenter: parent.verticalCenter
width: 16
height: 16
source: nodeItem.hasChildren
? (nodeItem.expanded ? "/icons/folder-open.svg" : "/icons/folder.svg")
: "/icons/file.svg"
}
Text {
id: contentLabel
x: icon.x + icon.width + 6
anchors.verticalCenter: parent.verticalCenter
text: nodeItem.model.display
color: nodeItem.isCurrent ? "#0B5FA5" : "#333333"
font.bold: nodeItem.isCurrent
elide: Text.ElideRight
}
}
}
}
}
}
展开箭头用 TreeArrow SVG 组件,只有是树节点且有子节点时才显示,叶节点留空。这里给箭头留了一个固定的 16px 槽位,哪怕叶节点没有箭头,图标也从同一位置开始,保证同层图标垂直对齐。
点击箭头用 TapHandler 来响应单击,点的时候先把这一格设为当前项,再调用 toggleExpanded() 切换展开状态。
图标分三种:展开的文件夹、折叠的文件夹、普通文件。根据 hasChildren 和 expanded 两个属性判断显示哪张。图标放在箭头槽位的右边,跟箭头保持同一缩进深度。
节点文字排在图标右边,当前行变蓝加粗。加了 elide: Text.ElideRight,文字太长的话末尾显示省略号,不会撑破布局。
适用场景:文件浏览器、分类目录、侧边栏导航树、任何需要定制外观的树形展示。
已验证环境:
- Qt 版本:Qt 6.11.1
- 操作系统:Windows 11
- GitHub:QML-Minimal-Demos/qml_treeview