1. 引言
QML(Qt Meta-Object Language)作为Qt框架中的声明式UI语言,与传统的命令式UI开发方式不同,QML通过声明式语法和JavaScript的结合,提供了极其灵活的组件动态处理能力。
QML提供的高响应式、动态并发的操作界面本身就是动态的,但是更多的是大家对组件中各属性的动态运用,对于组件整体的动态使用往往并不多,使用的少了也就很难把组件像属性一样的灵活使用。
官网对于动态创建组件的文档:https://doc.qt.io/qt-6/qtqml-javascript-dynamicobjectcreation.html
在阐述之前,需要对以下概念区分开,不能混淆:
首先,有两个组件需要分清楚:
Item
:QML中实例化得到Item,Button等系列组件都可以说继承自ItemComponent
:QML中封装为模板得到Component,模板可以实例化为组件
这两者都可以像组件一样使用,但两者并不是简单的继承关系。
其次,有两个概念需要分清楚:
-
加载:Component的加载过程是从源解析成Component组件模板,不是直接使用的UI组件;
-
实例化:根据已加载的组件模板创建具体的对象实例,设定具体的属性,用于直接的UI。
一个Component一次加载就可以多次实例化,像Button
、Rectangle
这样的内置基础组件可以说没有加载过程,或者说已从内部加载过了,甚至在使用时就已实例化。
2. 详解 Component 和 Loader
2.1 Component
Component 本身不是直接作用的组件,而是作为创建组件对象的模板。
与直接声明的组件不同,Component 定义的组件不会立即实例化,只有在需要时才会创建。
属性:
url
:url,只读,此组件的url,用于创建此组件status
:enumeration,只读,反映此组件被加载的状态Component.Null
:此组件内无数据Component.Ready
:此组件已被加载,可用于创建实例Component.Loading
:此组件正在被加载Component.Error
:此组件被加载时发生错误,通过errorString()
返回错误描述
progress
:real,只读,显示此组件被加载的进度,从0.0(没有被加载)到1.0(完成)
信号:
completed()
:组件被实例化后触发。在其他组件中也有,就是常用的Component.onCompleted
。destruction()
:组件被销毁后触发。
方法:
object createObject(QtObject parent, object properties)
:创建并返回该组件的组件实例,该实例将具有给定的父组件和属性。parent指定父组件,properties是JsonObject类型的值,用于设定组件的属性,见示例。object incubateObject(QtObject parent, object properties, enumeration mode)
:为此组件的实例创建孵化器,孵化器可用于实例化组件对象。孵化器允许使用mode指定是否异步实例化新的组件,使之不影响UI。见示例。string errorString()
Component {
id: blueButton
Rectangle {
width: 120
height: 50
color: "blue"
radius: 5
Text {
text: "Click Me"
anchors.centerIn: parent
color: "white"
}
}
}
// 加载组件
function create(){
const component = Qt.createComponent("Button.qml"); // 通用Component对象
if(component.status === Component.Ready){
component.createObject(parent, { x: 100, y: 100 }); // 实例化
}
}
// 创建并使用孵化器
function incubate(){
const component = Qt.createComponent("Button.qml"); // 通用Component对象
const incubator = component.incubateObject(parent, { x: 10, y: 10 }); // 孵化器
if (incubator.status !== Component.Ready) {
incubator.onStatusChanged = function(status) {
if (status === Component.Ready) {
print("Object", incubator.object, "is now ready!");
}
};
} else {
print("Object", incubator.object, "is ready immediately!");
}
}
2.2 Loader
Loader 是动态加载组件的专用组件,它提供了加载和卸载组件的接口。可以使用 source 属性加载一个 QML 文件或者使用 sourceComponent 属性加载一个组件对象。
属性:
-
active
:bool,控制是否加载组件 -
source
:url,指定要加载的QML文件作为组件 -
sourceComponent
:Component,用于加载已定义的组件 -
item
:QtObject,只读,指向已加载的组件实例,便于访问 -
status
:enumeration,只读,反映加载组件的状态Loader.Null
:Loader处于非活动状态,组件未加载Loader.Ready
:组件已加载Loader.Loading
:组件正在加载中Loader.Error
:组件加载时发生失败
-
progress
:real,只读,显示文件加载组件的进度,从0到1(因为一般QML文件比较小,所以变化很快) -
asynchronous
:bool,控制是否异步加载组件,默认为false
信号:
loaded()
:当status
变为Loader.Ready
时触发
方法:
object setSource(url source, object properties)
:手动加载组件,source为QML文件,properties是JsonObject类型的值,用于设定组件的属性,见示例。
注意:Loader不设定大小时自动匹配大小至加载的组件,与Loader交互时需要焦点在指定Loader中。
Loader {
id: buttonLoader
anchors.centerIn: parent
width: active ? item.width : 0
height: active ? item.height : 0
onLoaded: console.log("buttonLoader is ready")
}
// 控制加载状态
function toggle() {
active = !active
}
// 动态切换组件
function loadNewComponent() {
source = "NewComponent.qml"
}
Component.onCompleted: {
buttonLoader.setSource("MyButton.qml", {"text": "myButton"});
}
3 Qt提供的 create*() 方法
Qt提供了两种方法动态创建对象,可以调用Qt.createComponent()
来动态创建Component对象,也可以使用Qt.createQmlObject()
从QML字符串创建对象。
3.1 Qt.createComponent()
官网文档:https://doc.qt.io/qt-6/qml-qtqml-qt.html#createComponent-method
格式:Component createComponent(url url, enumeration mode, QtObject parent)
- 返回一个Component的组件对象;
- 通过
url
指定QML文件作为创建对象的源; - 通过
mode
指定是否异步创建,从而不影响UI操作; - 通过
parent
指定关联的父组件。
// 从QML文件创建Component
var fileComponent = Qt.createComponent("Button.qml")
if (fileComponent.status === Component.Ready) {
var obj = fileComponent.createObject(parentItem)
}
3.2 Qt.createQmlObject()
官网文档:https://doc.qt.io/qt-6/qml-qtqml-qt.html#createQmlObject-method
格式:object createQmlObject(string qml, object parent, url url)
- 直接返回一个新对象,如果创建组件或对象时发生错误,则返回空;
- 通过
qml
指定QML字符串,用于直接编译到组件中,QML字符串是QML创建组件的语句,见下示例; - 通过
parent
指定关联的父组件; - 通过
url
指定对象的URL,标识对象的来源,可作为虚拟文件名,用于错误调试和报告,发生错误时会显示此URL。
// 直接从QML字符串创建对象
const newObject = Qt.createQmlObject(`
import QtQuick
Rectangle {
color: "red"
width: 20
height: 20
}
`,
parentItem,
"myDynamicSnippet"
);
4. 组件的 destroy() 方法
动态创建的组件往往需要手动管理内存,直接对对象调用它的destroy()
方法即可,提供了设定延时的参数,单位为毫秒。
// 销毁单个组件
function destroyComponent(obj) {
obj.destroy()
// 或者使用延迟销毁
obj.destroy(1000) // 1秒后销毁
}
// 批量销毁示例
function cleanup() {
for (var i = 0; i < dynamicChildren.length; i++) {
dynamicChildren[i].destroy()
}
dynamicChildren = []
}
注意事项:
- 销毁后对象引用变为undefined
- 推荐在Component.onDestruction中执行清理操作
- 避免在过渡动画期间销毁
- 对于频繁创建销毁的对象,考虑对象池技术
5. 活用 children 和 data 属性
5.1 children
每个Item都有children属性,用于管理子元素,只有继承自Item的组件才有children属性,比如Rectangle、button等常用组件。
类型为list<Item>
,是一个列表,可以像列表一样使用。
可以认为是与parent属性对立的。
// 访问子项
Item {
id: root
Rectangle { id: rect1 }
Text { id: text1 }
Component.onCompleted: {
console.log(children.length) // 2
console.log(children[0] === rect1) // true
console.log(children.includes(text1)) // true
}
}
// 动态修改层级
Item {
Repeater {
model: ["red", "green", "blue"]
delegate: Rectangle {
color: modelData
width: 50; height: 50
z: index // 通过z控制实际渲染顺序
}
}
function bringToFront(item) {
// 将子项移到children数组末尾(最后渲染=显示在最前)
item.parent = null
item.parent = this
}
}
5.2 data
所有QML对象都有data属性。
类型为list<Object>
,是一个列表,可以像列表一样使用。
默认包含显式声明的子对象以及通过 children属性(如果是 Item 派生类)暴露的子项。
// 显示访问子对象
Item {
id: parentItem
Rectangle { id: rect; width: 50 }
Text { id: text; text: "Hello" }
Component.onCompleted: {
console.log(data.length) // 输出 2(rect和text)
console.log(data[0] === rect) // true
}
}
// 动态添加对象
Item {
Component.onCompleted: {
var child = Qt.createQmlObject(`
import QtQuick 2.0
QtObject { property string name: "dynamic" }
`, parentItem)
console.log(data.includes(child)) // true
}
}