本文主要介绍 QML 中 TreeView 的基本使用方法,包括:TreeView的适用场景;
控件简介
QML TreeView 是 Qt Quick 中的一个组件,用于显示树形结构的数据。它提供了一种以层次结构方式展示数据的方式,其中每个节点可以包含子节点。
以下是 QML TreeView 的一些关键概念和特点:
- Model-View 架构:QML TreeView 遵循 Model-View 架构,其中数据模型(通常是 QAbstractItemModel 的子类)负责提供数据,而视图组件则负责显示和交互。数据模型提供了节点层次结构以及每个节点的数据。
- Delegate 委托:TreeView 使用委托来定义每个节点的外观和行为。委托可以自定义,允许您根据需求定制每个节点的呈现方式。
- 展开和折叠:TreeView 支持展开和折叠节点的功能。当节点具有子节点时,可以通过点击节点旁边的展开/折叠按钮或者通过编程方式来切换节点的展开状态。
- 自定义节点样式:您可以自定义节点的样式,包括文本颜色、背景色、字体、图标等。通过使用不同的委托和属性设置,您可以实现丰富多样的外观效果。
- 选择和焦点:TreeView 支持节点的选择和焦点管理。您可以根据需要选择一个或多个节点,并可以通过编程方式操作当前焦点节点。
- 事件处理:TreeView 允许处理节点上的各种事件,例如点击、双击、拖放等。您可以根据需要响应这些事件并执行相应的操作。
QML TreeView 在许多应用中都有广泛的应用场景,特别是需要展示层次结构数据的场景。例如,文件浏览器、目录结构、组织架构图等都可以使用 TreeView 来呈现数据并提供交互性。
QML TreeView 提供了灵活的自定义选项,可以根据需求定制节点的样式和交互行为。通过自定义委托元素,可以为每个节点定义不同的显示方式,以满足特定的设计要求。
前置准备
阅读本节内容前请确保您了解 Qt 中与模型/视图架构相关的内容、与 Python 中调用 QML 相关的内容,包括:
- 模型(model)、视图(View)和委托(Delegate)之间的关系。
- QML 调用 Python 模型。
如不是特别熟悉,请先阅读《QML 与 Python 交互》,若您想更深入了解,请参阅官方文档《Python-QML integration》、《》。
环境准备:
- OS:Windows 10 x64
- Python环境:Python 3.8.x 及以上
- Python包:PySide6(version 6.3.x 及以上)
使用方法
通过查阅官方文档,我们发现,官方提供了 QML 的 TreeView 的示例代码,但是还需要我们自己实现其对应的 model 部分,下边我们使用 Qt 官方提供的示例代码,给它匹配一个 model ,完成一个完整的节点树,主要我们可以概括为两部分:
- 在 Python 文件中定义 TreeView 对应的 model。
- 将 model 暴露给 QML ,绑定到 TreeView。
定义 model
在 QML 中,TreeView 使用的 model 必须是继承自 QAbstractItemModel 类的子类,在对 QAbstractItemModel
进行子类实例化时,至少必须实现 index()
, parent()
, rowCount()
, columnCount()
和 data()
。简而言之,就是:在 Python 文件中定义一个类,这个类继承自 QAbstractItemModel ,并且在这个子类中要重写这几个方法。
可选需重写的虚函数:
- flag——是为了实现可编辑才重写的。
- setData——是为了编辑之后把数据保存到数据结构才重写的。
要在模型中启用编辑,还必须实现:
- setData()
- flags()(要确保返回 ItemIsEditable)
控制模型标题的显示方式,可以实现:
- headerData()
- setHeaderData()
在重新实现 setData()
和 setHeaderData()
函数时,必须分别显式地发出 dataChanged()
和 headerDataChanged()
信号。
绑定 model 给 TreeView
在 main.py 中添加这行代码,表示将 treeModel 暴露给 QML 上下文环境中,在 QML 中可以当组件直接调用:
# ...
engine.rootContext().setContextProperty('treeModel', treeModel)
# ...
在 QML 中可以直接使用,如:
// ...
model: treeModel
// ...
这样就完成了 Python 数据模型和 QML 文件的绑定。
完整示例
项目目录结构:
Project
├─ main.qml
├─ man.py
└─ TreeModel.py
完整代码:
// main.qml
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Window {
width: 600
height: 400
visible: true
TreeView {
anchors.fill: parent
// The model needs to be a QAbstractItemModel
model: treeModel
delegate: Item {
id: treeDelegate
implicitWidth: padding + label.x + label.implicitWidth + padding
implicitHeight: label.implicitHeight * 1.5
readonly property real indent: 20
readonly property real padding: 5
// Assigned to by TreeView:
required property TreeView treeView
required property bool isTreeNode
required property bool expanded
required property int hasChildren
required property int depth
TapHandler {
onTapped: treeView.toggleExpanded(row)
}
Text {
id: indicator
visible: treeDelegate.isTreeNode && treeDelegate.hasChildren
x: padding + (treeDelegate.depth * treeDelegate.indent)
anchors.verticalCenter: label.verticalCenter
text: "▶"
rotation: treeDelegate.expanded ? 90 : 0
}
Text {
id: label
x: padding + (treeDelegate.isTreeNode ? (treeDelegate.depth + 1) * treeDelegate.indent : 0)
width: treeDelegate.width - treeDelegate.padding - x
clip: true
text: model.display
}
}
}
}
这是一个使用 QML 语言编写的树形视图(TreeView)的界面定义。让我们逐行解释:
TreeView
: 树形视图组件,用于展示树状结构的数据。anchors.fill: parent
: 将 TreeView 组件的四个边界锚定到父级窗口,使其充满整个窗口空间。model: treeModel
: 指定 TreeView 使用的数据模型。在这里,treeModel
是一个 QAbstractItemModel 类型的模型,它提供了树形结构的数据。delegate
: 定义了用于绘制每个树节点的委托元素。implicitWidth
和implicitHeight
: 定义委托元素的隐式宽度和高度。这些属性决定了每个委托元素的大小。indent
和padding
: 定义了缩进和填充的大小,用于控制节点在水平方向上的位置。TapHandler
: 定义了一个用于处理点击事件的处理器。当用户点击委托元素时,将触发onTapped
事件。indicator
: 一个用于显示展开/折叠状态的文本元素。根据节点是否是父节点(isTreeNode
)和是否有子节点(hasChildren
)来决定是否显示。label
: 显示节点的文本内容。根据节点的深度(depth
)和是否是父节点来确定水平偏移量。
创建一个带有树形结构的 TreeView,并为每个节点定义了自定义的委托元素。委托元素根据节点的深度和展开状态来显示节点的文本和展开/折叠指示符。你需要提供一个名为treeModel
的 QAbstractItemModel 类型的模型来作为 TreeView 的数据源。
# TreeModel.py
from PySide6.QtCore import QAbstractItemModel, QModelIndex, Qt
# 自定义的树形节点类
class TreeNode:
# 初始化节点数据和父亲节点
def __init__(self, data, parent=None):
# 存储节点数据
self._data = data
# 存储父节点
self._parent = parent
# 存储子节点
self._children = []
# 添加一个子节点
def appendChild(self, child):
self._children.append(child)
# 删除一个子节点
def removeChild(self, item):
self._children.remove(item)
# 获取某一行的子节点
def child(self, row):
return self._children[row]
# 获取子节点的个数
def childCount(self):
return len(self._children)
# 获取列数
def columnCount(self):
return 1
# 获取节点某一列的数据
def data(self, column):
if column == 0:
return self._data
# 获取父节点
def parent(self):
return self._parent
# 获取该节点在父节点中的行号
def row(self):
if self._parent:
return self._parent._children.index(self)
# 判断节点是否包含子节点
def hasChildren(self):
return len(self._children) > 0
# 自定义的树形数据模型
class TreeModel(QAbstractItemModel):
# 初始化根节点
def __init__(self, root, parent=None):
super().__init__(parent)
# 如果没有传入根节点,则创建一个空的根节点作为根
self._root = root or TreeNode(None)
# self._root = root
# 获取某个节点在模型中的Index
def index(self, row, column, parent=QModelIndex()):
if not self.hasIndex(row, column, parent):
return QModelIndex()
if not parent.isValid():
parentItem = self._root
else:
parentItem = parent.internalPointer()
childItem = parentItem.child(row)
if childItem:
return self.createIndex(row, column, childItem)
else:
return QModelIndex()
# 获取某个节点的父节点的Index
def parent(self, index):
if not index.isValid():
return QModelIndex()
childItem = index.internalPointer()
parentItem = childItem.parent()
if parentItem == self._root:
return QModelIndex()
return self.createIndex(parentItem.row(), 0, parentItem)
# 获取某个节点的子节点个数
def rowCount(self, parent=QModelIndex()):
if parent.column() > 0:
return 0
if not parent.isValid():
parentItem = self._root
else:
parentItem = parent.internalPointer()
return parentItem.childCount()
# 获取列数,这里只有一列
def columnCount(self, parent=QModelIndex()):
return 1
# 获取某个节点的数据
def data(self, index, role=Qt.DisplayRole):
if not index.isValid():
return None
item = index.internalPointer()
if role == Qt.DisplayRole:
return item.data(index.column())
def flags(self, index):
if not index.isValid():
return Qt.NoItemFlags
return super().flags(index)
这段代码是一个用于创建树形数据模型的类 TreeModel
,它是基于 QAbstractItemModel
的子类,用于在 PySide6 中实现树形视图的数据模型。
代码中定义了一个自定义的树形节点类 TreeNode
,每个节点包含一个数据项和对父节点和子节点的引用。TreeNode
类提供了一些方法来操作节点的子节点,如添加子节点、删除子节点等。还提供了一些方法用于获取节点的数据、父节点、子节点数量等。
TreeModel
类的构造函数接受一个根节点作为参数,并将其存储在成员变量 _root
中。如果没有传入根节点,则创建一个空的根节点。该类还实现了一些必要的方法,使得树形数据模型能够与视图进行交互。
以下是对 TreeModel
类中重要方法的解读:
index(self, row, column, parent=QModelIndex())
: 返回指定行和列的节点在模型中的索引。如果索引无效,则返回QModelIndex()
。如果父索引无效,则返回根节点;否则,返回父节点的子节点。parent(self, index)
: 返回给定索引的父节点的索引。如果索引无效,则返回QModelIndex()
。如果父节点是根节点,则返回无效索引;否则,返回父节点的索引。rowCount(self, parent=QModelIndex())
: 返回给定父索引下的子节点数量。如果父索引的列数大于 0,则返回 0。如果父索引无效,则返回根节点的子节点数量;否则,返回父节点的子节点数量。columnCount(self, parent=QModelIndex())
: 返回模型中的列数,这里固定为 1。data(self, index, role=Qt.DisplayRole)
: 返回给定索引处节点的数据。如果索引无效,则返回None
。如果角色是Qt.DisplayRole
,则返回节点的数据。flags(self, index)
: 返回给定索引的标志。如果索引无效,则返回Qt.NoItemFlags
,表示没有任何标志。
# main.py
from PySide6.QtCore import QUrl
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine
from TreeModel import TreeModel,TreeNode
import sys
if __name__ == '__main__':
# 创建树形结构
rootItem = TreeNode("root")
childItem1 = TreeNode("child1", rootItem)
childItem2 = TreeNode("child2", rootItem)
childItem3 = TreeNode("child3", rootItem)
subChildItem1 = TreeNode("sub_child1", childItem1)
subChildItem2 = TreeNode("sub_child2", childItem1)
rootItem.appendChild(childItem1)
rootItem.appendChild(childItem2)
rootItem.appendChild(childItem3)
childItem1.appendChild(subChildItem1)
childItem1.appendChild(subChildItem2)
# 创建树形数据模型
treeModel = TreeModel(rootItem)
# 启动
app = QGuiApplication([])
engine = QQmlApplicationEngine()
engine.rootContext().setContextProperty('treeModel', treeModel)
engine.load(QUrl.fromLocalFile('main.qml'))
engine.quit.connect(app.quit)
sys.exit(app.exec())
这是一个使用 PySide6 构建的应用程序的入口点 main.py
。它创建了一个树形结构,并将其与自定义的树形数据模型 TreeModel
结合使用,最后通过 Qt Quick(QML)界面进行展示。
- 首先,代码导入了必要的 PySide6 模块和类,以及自定义的
TreeModel
和TreeNode
类。 - 在
if __name__ == '__main__':
块中,首先创建了树形结构。根节点是一个名为 “root” 的节点,然后创建了几个子节点,并将它们作为根节点的子节点。子节点也可以有它们自己的子节点。 - 接下来,通过将根节点传递给
TreeModel
类的构造函数,创建了一个树形数据模型treeModel
。 - 之后,创建了
QGuiApplication
实例app
,用于启动应用程序。 - 创建了
QQmlApplicationEngine
实例engine
,用于加载和运行 QML 界面。 - 通过
engine.rootContext().setContextProperty('treeModel', treeModel)
将treeModel
设置为 QML 上下文属性,以便在 QML 中可以访问该树形数据模型。 - 使用
engine.load(QUrl.fromLocalFile('main.qml'))
加载 QML 文件,该文件定义了应用程序的界面。 - 最后,通过
engine.quit.connect(app.quit)
将engine
的quit
信号连接到app
的quit
槽,以便在关闭应用程序时能够正确退出。 - 最后使用
sys.exit(app.exec())
启动应用程序的事件循环。
运行命令:
python main.py
未实现
- 双击编辑节点
参考
[1].TreeView QML Type.[EB/OL].[2023-6-15].https://doc.qt.io/qt-6/qml-qtquick-treeview.html
[2].QAbstractItemModel.[EB/OL].[2023-6-15].https://doc.qt.io/qtforpython-6/PySide6/QtCore/QAbstractItemModel.html#PySide6.QtCore.PySide6.QtCore.QAbstractItemModel