简述
QML 可以很容易地通过 C++ 代码中定义的功能进行扩展。由于 QML 引擎与 Qt 元对象系统的紧密集成,QObject 派生类适当暴露的任何功能都可以从 QML 代码访问,这使得 C++ 中的数据和函数可以直接从 QML 中访问,通常不需要太多修改,甚至不用修改。
通过元对象系统,QML 引擎具有内省 QObject 实例的能力。这意味着,任何 QML 代码都可以访问 QObject 派生类 的以下成员:
- 属性
- 函数
- 信号
一般来说,无论 QObject 派生类是否被注册到 QML 类型系统,这些成员都可以从 QML 中访问。但是,如果 QML 引擎需要访问这个类的附加类型信息(例如,如果类本身被用作一个函数参数或属性,或者以这种方式使用它的一个枚举类型),那么该类可能需要被注册。
版权所有:一去丶二三里,转载请注明出处:http://blog.csdn.net/liang19890820
暴露属性
任何的 QObject 派生类都可以使用 Q_PROPERTY() 宏来指定属性。属性是类的数据成员,具有关联的 READ 读取函数以及可选的 WRITE 写入函数。
QObject 派生类的所有属性都可以从 QML 访问。
基本类型属性
例如,下面是包含 name 属性的 Person 类。正如 Q_PROPERTY 宏所指定那样,该属性可通过 name() 函数来读取,setName() 来写入:
// person.h
#ifndef PERSON_H
#define PERSON_H
#include <QObject>
#include <qDebug>
// 人
class Person : public QObject
{
Q_OBJECT
Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)
public:
void setName(const QString &name) {
if (name != m_name) {
m_name = name;
emit nameChanged();
qDebug() << "Name changed: " << name;
}
}
QString name() const {
return m_name;
}
signals:
void nameChanged();
private:
QString m_name; // 姓名
};
#endif // PERSON_H
为了访问实例中的数据,需要设置上下文属性,将数据暴露给由 QML 引擎实例化的 QML 组件。
通过调用 QQmlContext::setContextProperty() 来定义和更新上下文属性,允许以名称将数据显式地绑定到上下文。
// main.cpp
#include <QGuiApplication>
#include <QQuickView>
#include <QQmlEngine>
#include <QQmlContext>
#include "person.h"
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QQuickView view;
Person person;
// 设置上下文属性
view.engine()->rootContext()->setContextProperty("person", &person);
view.setSource(QUrl("qrc:/main.qml"));
view.show();
return app.exec();
}
然后,就可以从 QML 中访问了:
// main.qml
import QtQuick 2.3
Rectangle {
width: 200; height: 150
Text {
anchors.centerIn: parent
text: person.name // 调用 Person::name() 来获取值
Component.onCompleted: {
person.name = "Qter" // 调用 Person::setName()
}
}
}
加载完成,name 的值就会被显示出来,如下所示:
为了尽可能增强与 QML 的交互性,任何可写的属性都应该有一个相关的 NOTIFY 信号,只要属性值发生改变就发出该信号。这允许该属性应用于属性绑定,属性绑定是 QML 的一个重要特性,每当其依赖的任何关系值发生改变,就会通过自动更新属性来强制执行属性之间的关系。
上述示例中,name 属性相关的 NOTIFY 信号是 nameChanged。这意味着无论何时发出该信号(就像在 Person::setName() 改变 name 时一样),将会通知 QML 引擎,必须更新涉及 name 属性的任何绑定。反过来,引擎将通过调用 Person::name() 来更新 text 属性。
如果 name 属性是可写的,但没有相关的 NOTIFY 信号,则 name 值将由 Person::name() 返回的初始值来初始化,但当该属性后续发生任何更改时不会进行相应的更新。此外,绑定到该属性的任何尝试都会产生运行时警告。
信号命名建议: 将 NOTIFY 信号命名为 <property>Changed
的形式,其中 <property>
是属性的名称。由 QML 引擎生成的关联的属性更改信号处理程序将始终采用 on<Property>Changed
的形式,而无需关心相关 C++ 信号的名称,因此建议信号名称遵循此约定,以避免任何混淆。
使用 Notify 信号的注意事项
为了防止循环或过度评估,应确保属性更改信号仅在属性值更改时发出。此外,如果一个属性或属性组不常被用到,则允许对若干属性使用相同的 NOTIFY 信号。不过,使用时应注意,确保性能不会受到影响。
NOTIFY 信号确实会有较小的开销。有时,有些属性的值仅在对象构造阶段设置,随后就再也不会改变。最常见的情况是当一个类型使用分组属性时,分组属性对象被分配一次,并且只有在销毁对象时才会释放。在这种情况下,属性声明时应该使用 CONSTANT 属性,而不是 NOTIFY 信号。
CONSTANT 属性应该仅用于那些仅在类构造函数中设值置,并且随后不再改变的属性,而所有可能会在绑定中使用的其他属性应该使用 NOTIFY 信号。
对象类型属性
如果对象类型已经被注册到 QML 类型系统,则可以从 QML 访问对象类型属性。
例如,Person 类型有一个 IDCard * 类型的属性:
// person.h
#ifndef PERSON_H
#define PERSON_H
#include <QObject>
#include <qDebug>
// 身份证
class IDCard : public QObject
{
Q_OBJECT
Q_PROPERTY(QString number READ number WRITE setNumber)
Q_PROPERTY(QString validDate READ validDate WRITE setValidDate NOTIFY validDateChanged)
public:
void setNumber(const QString &number) {
if (number != m_number)
m_number = number;
}
QString number() const {
return m_number;
}
void setValidDate(const QString &validDate) {
if (validDate != m_validDate) {
m_validDate = validDate;
emit validDateChanged();
qDebug() << "Valid date changed: " << validDate;
}
}
QString validDate() const {
return m_validDate;
}
signals:
void validDateChanged();
private:
QString m_number; // 身份证号码
QString m_validDate; // 有效日期
};
// 人
class Person : public QObject
{
Q_OBJECT
Q_PROPERTY(IDCard* idCard READ idCard WRITE setIDCard NOTIFY idCardChanged)
public:
IDCard* idCard() const {
return m_idCard;
}
void setIDCard(IDCard *idCard) {
if (idCard != m_idCard) {
m_idCard = idCard;
emit idCardChanged();
qDebug() << "ID card changed: " << idCard->number();
}
}
signals:
void idCardChanged();
private:
IDCard *m_idCard; // 身份证
};
#endif // PERSON_H
任何 QObject 的派生类都可以被注册为 QML 对象类型。一旦使用 QML 类型系统注册,该类就可以像 QML 中的任何其他对象类型(例如:Item)一样被声明和实例化。一旦被创建,类实例便可以从 QML 中操作,作为 C++ 类型的属性暴露给 QML 解释,任何 QObject 派生类的属性、方法和信号都可以从 QML 代码访问。
要将 QObject 派生类注册为可实例化的 QML 对象类型,需要调用 qmlRegisterType() 将类注册为特定类型命名空间中的 QML 类型。然后客户端可以导入该命名空间,以便使用该类型。
例如,将 C++ 类型 Person 注册为名为 Person(双引号中的名称 - 尽量见名知义,对象类型首字母大写,例如:Per) 的 QML 类型,其在版本号为 1.0 的 People 命名空间中可用:
qmlRegisterType<Person>("People", 1, 0, "Person");
由于 IDCard 是 Person 的对象类型属性,所以要访问 IDCard 的附加信息,也需要被注册,完整的代码如下:
// main.cpp
#include <QGuiApplication>
#include <QQuickView>
#include "person.h"
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
// 使用 QML 类型系统注册
qmlRegisterType<Person>("People", 1, 0, "Person");
qmlRegisterType<IDCard>("People", 1, 0, "IDCard");
QQuickView view;
view.setSource(QUrl("qrc:/main.qml"));
view.show();
view.setIcon(QIcon(":/logo.png"));
view.setTitle(QStringLiteral("将C++对象暴露给QML"));
return app.exec();
}
一旦被注册,通过导入指定的类型命名空间和版本号便可以在 QML 中使用该类型:
// main.qml
import QtQuick 2.3
import People 1.0
Rectangle {
width: 200; height: 150
Text {
anchors.centerIn: parent
text: person.idCard.validDate // 先调用 Person::idCard() 来获取 IDCard,再调用 IDCard::validDate()
}
Person {
id: person
idCard: IDCard { // 调用 Person::setIDCard()
number: "610122..." // 调用 IDCard::setNumber()
validDate: "2008.10.01-2018.10.01" // 调用 IDCard::setValidDate()
}
}
}
这里,我们只显示身份证的有效日期,如下所示:
分组属性
如果对象类型属性是只读的,则可以在 QML 中作为分组属性来访问,这种方式可用于暴露一个类型的一组相关属性。
修改上述示例:
// person.h
#ifndef PERSON_H
#define PERSON_H
#include <QObject>
// IDCard 类同上...
// 人
class Person : public QObject
{
Q_OBJECT
Q_PROPERTY(IDCard* idCard READ idCard)
public:
IDCard *idCard() {
return &m_idCard;
}
private:
IDCard m_idCard; // 身份证
};
#endif // PERSON_H
这时,就不能再这么使用了:
// 错误的用法
Person {
id: person
idCard: IDCard {
number: "610122..."
validDate: "2008.10.01-2018.10.01"
}
}
因为 idCard 是一个只读的对象属性,而非可写的,所以会提示如下错误:
Invalid property assignment: “idCard” is a read-only property
正确的姿势是使用分组属性语法,为 idCard 属性赋值:
// 组表示法
Person {
id: person
idCard {
number: "610122..."
validDate: "2008.10.01-2018.10.01"
}
}
或者:
// 点表示法
Person {
id: person
idCard.number: "610122..."
idCard.validDate: "2008.10.01-2018.10.01"
}
对象类型属性与分组属性的区别:
- 分组属性:只读,只能在构造时由父对象初始化为一个有效值,生命周期由 C++ 父对象严格控制。其子属性可以从 QML 中修改,但是分组属性对象本身不能改变。
- 对象类型属性:可以随时被分配新的对象值,通过 QML 代码*创建和销毁。
对象列表类型属性
如果属性包含 QObject 派生类列表,也可以暴露给 QML。但是,属性类型应该使用 QQmlListProperty,而非 QList<T>
。这是因为 QList 不是 QObject 的派生类型,因此不能通过 Qt 元对象系统提供必要的 QML 属性特性。例如,当列表被修改时的信号通知。
QQmlListProperty 是一个模板类,可以很方便的由一个 QList 值构造。
例如,Company 类有一个类型为 QQmlListProperty 的 persons 属性,存储了一个 Person 实例列表:
// person.h
#ifndef PERSON_H
#define PERSON_H
#include <QObject>
#include <QQmlListProperty>
#include <QDebug>
// 人
class Person : public QObject
{
Q_OBJECT
Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)
public:
void setName(const QString &name) {
if (name != m_name) {
m_name = name;
emit nameChanged();
qDebug() << "Name changed: " << name;
}
}
QString name() const {
return m_name;
}
signals:
void nameChanged();
private:
QString m_name; // 姓名
};
// 公司
class Company : public QObject
{
Q_OBJECT
Q_PROPERTY(QQmlListProperty<Person> persons READ persons)
public:
Company(QObject *parent = 0);
QQmlListProperty<Person> persons();
int personCount() const;
Person *person(int) const;
private:
QList<Person *> m_persons;
};
#endif // PERSON_H
注册同上:
// main.cpp
...
qmlRegisterType<Company>("People", 1,0, "Company");
qmlRegisterType<Person>("People", 1,0, "Person");
...
在 QML 中,我们定义一个 ListView 用于显示列表中人的姓名:
// main.qml
import QtQuick 2.7
import QtQuick.Controls 2.0
import People 1.0
Rectangle {
width: 200; height: 150
Component {
id: contactDelegate
Item {
width: 180; height: 40
Row {
spacing: 5
leftPadding : 5
topPadding : 5
bottomPadding : 5
Image { // 头像
source: "logo.png"
sourceSize.width: 25
sourceSize.height: 25
}
Text { // 名字
text: name
height: 25
horizontalAlignment : Text.AlignHCenter
verticalAlignment : Text.AlignVCenter
}
}
}
}
ListView {
id: view
anchors.fill: parent
model: company.persons
delegate: contactDelegate
highlight: Rectangle { color: "lightsteelblue"; radius: 5 }
focus: true
}
Company {
id: company
persons: [
Person { name: "Qter" },
Person { name: "Pythoner" },
Person { name: "Linuxer" }
]
}
// 输出信息
Component.onCompleted: {
for (var i = 0; i < company.persons.length; i++)
console.log("Author: ", i, company.persons[i].name)
}
}
效果如下:
注意: QQmlListProperty 的模板类类型 - 在这种情况下,是 Person - 必须向 QML 类型系统注册。
暴露函数
QML 可以访问 QObject 派生类的函数,但是函数需要满足以下条件之一:
- 使用 Q_INVOKABLE() 宏标记的 public 函数
- public 槽函数
现在,为人添加一些基本的行为。例如:eat、walk。。。吃饱了才有力气减肥,每天三万步,身体倍棒,吃嘛嘛香。
// person.h
#ifndef PERSON_H
#define PERSON_H
#include <QObject>
#include <qDebug>
// 人
class Person : public QObject
{
Q_OBJECT
public:
Q_INVOKABLE bool eat(const QString &food) { // 吃饭
qDebug() << "Food: " << food;
return true;
}
public slots:
void walk() { // 走路
qDebug() << "Thirty thousand steps";
}
};
#endif // PERSON_H
和前面一样,要在 QML 中使用 Person,需要将其设置为 main.qml 的上下文属性:
// main.cpp
...
Person person;
QQuickView view;
// 设置上下文属性
view.engine()->rootContext()->setContextProperty("person", &person);
...
然后,就可以在 QML 中使用这个实例访问这两个函数:
// main.qml
import QtQuick 2.3
import QtQuick.Controls 2.0
Rectangle {
width: 200; height: 150
Label {
id: label
anchors.centerIn: parent
text: "Tip"
}
MouseArea {
anchors.fill: parent
onClicked: {
// Q_INVOKABLE 标记的函数
var result = person.eat("spinach")
// 输出返回值 result
label.text = "Result: " + result
// 槽函数
person.walk();
}
}
}
点击鼠标,标签上显示函数的返回值,如下所示:
如果 C++ 函数的参数包含 QObject* 类型,参数值可以从 QML 中传递,使用一个对象 id 或引用该对象的一个 JavaScript var 值。
QML 支持重载的 C++ 函数调用,如果函数具有相同名称和不同参数,则将根据提供的参数的数量和类型调用正确的函数。
当从 QML 中的 JavaScript 表达式访问 C++ 函数时,函数的返回值将转换为对应的 JavaScript 值。
暴露信号
QML 代码可以访问 QObject 派生类的任何 public 信号。
QML 引擎会为 QObject 派生类的信号自动地创建一个名为 on<Signal>
的信号处理程序,其中 <Signal>
是信号的名称,首字母大写。信号传递的所有参数在信号处理程序中都是可用的,通过参数名来访问。
前面,我们为人添加了走路的行为,其实三万步也是蛮多了。走完之后,人会发出一个信号:我累了,需要休息,具体的休息时间由参数 minute 决定:
// person.h
#ifndef PERSON_H
#define PERSON_H
#include <QObject>
#include <qDebug>
// 人
class Person : public QObject
{
Q_OBJECT
signals:
void tired(int minute); // 累了
public slots:
void walk() { // 走路
qDebug() << "Thirty thousand steps";
emit tired(30);
}
};
#endif // PERSON_H
注册就不再赘述了,和前面类似:
// main.cpp
...
qmlRegisterType<Person>("People", 1, 0, "Person");
...
QML 中声明的 Person 对象可以使用名为 onTired 的信号处理程序来接收 tired() 信号,并使用 minute 参数值:
// main.qml
import QtQuick 2.3
import QtQuick.Controls 2.0
import People 1.0
Rectangle {
width: 200; height: 150
Label {
id: label
anchors.centerIn: parent
text: "Tip"
}
MouseArea {
anchors.fill: parent
onClicked: {
// 槽函数
person.walk();
}
}
Person {
id: person
onTired: { // 信号处理程序
label.text = "Rest: " + minute
}
}
}
点击鼠标,标签上显示信号的参数值,如下所示:
与属性值和方法参数一样,信号的参数的类型也必须能够被 QML 引擎支持。值得注意的是,使用未注册的类型不会生成错误,但是参数值不能从处理程序中访问。
注意: 如果类中包含多个名称相同的信号,只有最后一个信号可以被 QML 访问,因为具有相同名称不同参数的信号无法被区分开来。