跳转到内容
新建笔记

QML 附加属性:作用对象、生命周期与 C++ 扩展

附加属性由一个提供者类型为另一个对象提供,例如 ListView.isCurrentItem、Layout.fillWidth。附加信号处理器则监听相应附加对象的事件,例如 Keys.onReturnPressed、Component.onCompleted。它们不是自动出现在所有对象上的全局变量,也不会因为写在父项上,就自动代表所有子项的状态。

本文以 Qt 6.10 为基线。内置例子可以保存为独立 QML 文件,由 Qt Quick 窗口承载;C++ 例子给出完整的注册与构建文件。

委托内部应明确访问谁的附加对象

跳转到“委托内部应明确访问谁的附加对象”

TypeName.propertyName 是附加属性的限定写法;TypeName.onSignalName 是附加信号处理器。它们所附加的接收对象是语义的一部分。例如 ListView.isCurrentItem 要在委托根对象上判断是否为当前项;位于委托中的另一个矩形或文本可以有自己的附加对象,不能把它的裸名称当成根委托的状态。

保存为 AttachedDelegate.qml。按回车增加计数,列表的当前行用红色显示;文本显式访问 delegateItem.ListView.isCurrentItem。

import QtQuick
Item {
id: root
width: 280
height: 240
focus: true
property int returnCount: 0
Keys.onReturnPressed: function(event) {
root.returnCount += 1
event.accepted = true
}
ListView {
id: view
width: parent.width
height: 180
model: 3
currentIndex: 0
delegate: Item {
id: delegateItem
required property int index
width: view.width
height: 40
Text {
anchors.centerIn: parent
text: "Item " + delegateItem.index
color: delegateItem.ListView.isCurrentItem ? "red" : "black"
}
MouseArea {
anchors.fill: parent
onClicked: {
view.currentIndex = delegateItem.index
root.forceActiveFocus()
}
}
}
}
Text {
anchors.top: view.bottom
text: "Return pressed: " + root.returnCount
}
}

focus 在这里仍是 Item 的普通属性;Keys 才是附加处理的提供者。键盘事件还要求窗口激活和项目拥有活动焦点。与此相似,Layout.fillWidth 是布局读取的附加信息,它不是对任意脱离布局的项目都生效的“自动填充”命令。

Component 内的实例才接收完成事件

跳转到“Component 内的实例才接收完成事件”

Component 可以保存稍后实例化的对象定义,但 Component { ... } 在 id 之外只容纳一个顶层对象定义。原稿把 Component.onCompleted 放在这个顶层矩形外面,在 Qt 6.10.2 会报“Component elements may not contain properties other than id”。应把处理器写在内部矩形上,它才会在每次创建矩形实例时运行。这个完成信号由 Component 类型提供附加机制,并不意味着处理器必须写在 Component {} 对象上。父子对象的完成处理器执行顺序不应作为业务依赖。

下面用 Loader 控制实例的生灭,并用合法的 State 和 onStateChanged 观察状态。保存为 LifecycleDemo.qml:

import QtQuick
Item {
id: root
width: 320
height: 180
property int rootsCompleted: 0
property int instancesCompleted: 0
property int instancesDestroyed: 0
property int stateChanges: 0
property bool activeMode: false
Component {
id: factory
Rectangle {
width: 80
height: 80
color: "tomato"
Component.onCompleted: root.instancesCompleted += 1
Component.onDestruction: root.instancesDestroyed += 1
}
}
Loader {
id: loader
active: false
sourceComponent: factory
}
Rectangle {
id: indicator
x: 120
width: 80
height: 80
color: "gray"
}
states: State {
name: "active"
when: root.activeMode
PropertyChanges {
target: indicator
color: "green"
}
}
onStateChanged: root.stateChanges += 1
Component.onCompleted: root.rootsCompleted += 1
function createOne() { loader.active = true }
function releaseOne() { loader.active = false }
}

调用 createOne() 后,instancesCompleted 增加;释放后再次创建,会产生新的实例。Loader 与销毁调度的细节应按事件循环观察,不要在所有场景中假定销毁信号都在赋值语句中同步完成。销毁处理器适合有限的清理或观察,不能依赖其他对象此时仍然完整存活。

原稿的 State.onActiveChanged 与 if (active) 不是 State 的有效附加接口。这里改变 activeMode 后,when 决定根项目的 state 是否为 "active",根项目的普通 stateChanged 通知再触发 onStateChanged。State.when、Item.state 与附加生命周期信号应分别理解。

C++ 自定义附加属性:对象、工厂与注册

跳转到“C++ 自定义附加属性:对象、工厂与注册”

自定义实现要区分三个角色:接收属性的 QML 对象、保存每个对象状态的附加对象,以及公开限定名称的提供者类型。提供者的静态工厂名是 qmlAttachedProperties(QObject *)。qmlAttachedPropertiesObject<T>() 则是 C++ 获取已缓存附加对象的辅助函数,不能把二者当成同一个待实现接口。

下面采用显式类型注册方式,保存四个文件到同一目录。attached.h:

#pragma once
#include <QObject>
#include <QtQml/qqml.h>
class AttachedValues : public QObject {
Q_OBJECT
Q_PROPERTY(int value READ value WRITE setValue NOTIFY valueChanged)
public:
explicit AttachedValues(QObject *parent = nullptr) : QObject(parent) {}
int value() const { return value_; }
void setValue(int value) {
if (value_ == value) return;
value_ = value;
emit valueChanged();
}
signals:
void valueChanged();
private:
int value_ = 0;
};
class MyAttachedType : public QObject {
Q_OBJECT
public:
explicit MyAttachedType(QObject *parent = nullptr) : QObject(parent) {}
static AttachedValues *qmlAttachedProperties(QObject *object) {
return new AttachedValues(object);
}
};
QML_DECLARE_TYPEINFO(MyAttachedType, QML_HAS_ATTACHED_PROPERTIES)

main.cpp:

#include "attached.h"
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QUrl>
int main(int argc, char **argv) {
QGuiApplication app(argc, argv);
if (app.arguments().size() != 2) return 2;
qmlRegisterUncreatableType<MyAttachedType>(
"Example.Attached", 1, 0, "MyAttachedType", "Use as attached properties");
QQmlApplicationEngine engine;
engine.load(QUrl::fromLocalFile(app.arguments().at(1)));
if (engine.rootObjects().isEmpty()) return 1;
return app.exec();
}

Main.qml:

import QtQuick
import QtQuick.Controls
import Example.Attached 1.0
Window {
id: root
width: 360
height: 180
visible: true
property int firstChanges: 0
Item {
id: first
objectName: "first"
MyAttachedType.value: 42
MyAttachedType.onValueChanged: root.firstChanges += 1
}
Item {
id: second
objectName: "second"
MyAttachedType.value: 7
}
Column {
spacing: 12
Text {
text: "first=" + first.MyAttachedType.value
+ ", second=" + second.MyAttachedType.value
}
Button {
text: "增加 first"
onClicked: first.MyAttachedType.value += 1
}
}
}

CMakeLists.txt:

cmake_minimum_required(VERSION 3.20)
project(AttachedExample LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
find_package(Qt6 REQUIRED COMPONENTS Quick Qml)
qt_add_executable(attached-example main.cpp attached.h)
target_link_libraries(attached-example PRIVATE Qt6::Quick Qt6::Qml)

使用 Qt Creator 选择安装好的 Qt 6 kit,配置、构建此项目;运行参数传入 Main.qml 的绝对路径,并确保运行环境能找到 Qt Quick Controls 模块。给 first 改值不会修改 second,因为两者各有自己的 AttachedValues。附加对象以接收对象为 QObject 父对象;引擎缓存这份关联,不应在接收对象仍存活时擅自删除附加对象。

采用自动模块注册的项目,可在提供者类上使用 QML_ATTACHED(AttachedValues)、QML_ELEMENT,再由 qt_add_qml_module 生成注册信息。那是另一套完整注册方式;不能只加一个宏就省略模块构建与注册。上面的显式示例通过 QML_DECLARE_TYPEINFO 声明附加能力,通过 qmlRegisterUncreatableType 注册限定名称。

依据:Qt 6.10 从 C++ 定义附加对象、Component、State。普通属性与别名见 QML 入门,独立信号连接见 Connections。