在 C++ 中, 怎么和 QML 对象交互 ?
以下内容为本人的学习笔记,如需要转载,请声明原文链接 [englyf] https://www.jianshu.com/p/66649d0e9bb6
请注意这里使用的环境是
IDE:Qt5.12
Lang:C++、QML
Compiler:vs2017x64
所有 QML 对象都是 QObject 的派生类型, 无论这个对象是引擎的内部实现或者是由第三方源定义而来。也就是说,QML 引擎可以利用 Qt 的元对象系统(Meta Object System
)去动态实例化任何的 QML 对象类型,以及检查被创建的对象。
所以说,在 C++ 代码中,无论是因为要显示一个可渲染的 QML 对象,或者需要集成非可视化的 QML 对象数据,创建 QML 对象都是非常容易实现的。当一个 QML 对象被创建之后,为了读写这个对象的属性,调用这个对象的方法和接收这个对象的信号通知,都能够在 C++ 中对此进行检查。
怎么在 C++ 中加载 QML 对象呢?
我们可以使用 Qt 提供的两个类来加载 QML 文档,分别是
QQmlComponent 和 QQuickView。
QQmlComponent 加载一个 QML 文档后,生成一个 C++ 对象,可以在 C++ 代码中对这个对象进行修改。
QQuickView 同样可以做到这些,但 QQuickView 是 QWindow 的一个派生类型,加载之后的对象也会被渲染出来。 QQuickView 通常被用来将可视化的 QML 对象集成到应用的用户界面中。
下面看个举个栗子,
// textItem.qml
import QtQuick 2.0
Item {
width: 100; height: 100
}
既可以用 QQmlComponent 也可以用 QQuickView 将上面这个 QML 文件加载到 C++ 代码中。如果使用 QQmlComponent 则需要调用 QQmlComponent::create() 创建组件的实例并返回对象(实例)指针,而使用 QQuickView
就会自动创建组件的实例,然后调用 QQuickView::rootObject()
获取实例指针。
// Using QQmlComponent
QQmlEngine engine;
QQmlComponent component(&engine,
QUrl::fromLocalFile("textItem.qml"));
QObject *object = component.create();
...
delete object;
// Using QQuickView
QQuickView view;
view.setSource(QUrl::fromLocalFile("textItem.qml"));
view.show();
QObject *object = view.rootObject();
怎么在 C++ 中设置 QML 对象的属性呢?
现在可以调用上面实例的 QObject::setProperty() 或者借用 QQmlProperty::write() 来修改 Item 的属性:
object->setProperty("width", 500);
QQmlProperty(object, "width").write(500);
这两种方式是有区别的,后者 QQmlProperty::write() 除了设置属性之外,还会移除原来的绑定,所以这里要特别注意一下。
比如,假设在 QML 文件中,已将 width 绑定到 height:
width: height
如果设置属性的方式是调用 object->setProperty("width", 500),那么 width 的值只是临时被设置为 500,一旦 height 改变了,width也是会跟随改变的,因为绑定关系没有被移除。但是,如果设置属性的方式是调用 QQmlProperty(object, "width").write(500) ,那么width 的值不会再跟随 height 的改变而改变,因为原来的绑定关系已被移除。
此外呢,设置属性还有一种方法就是,先将对象强制转换为实际类型,然后使用编译时安全性调用方法。在上面的文件 textItem.qml 中,Item 由类 QQuickItem 定义:
QQuickItem *item = qobject_cast<QQuickItem*>(object);
item->setWidth(500);
怎么在 C++ 中按照对象名访问已加载的 QML 对象呢?
QML 组件实际上是一组具有子节点的对象树,子节点同样有兄弟对象和子对象。可以使用 QObject::findChild() 并传入属性值 QObject::objectName(也即是对象名) 来定位到 QML 组件的子对象。下面看看我这的栗子大不大:
// textItem.qml
import QtQuick 2.0
Item {
width: 100; height: 100
Rectangle {
anchors.fill: parent
objectName: "rect"
}
}
可以看到根项是 Item,然后还有个子项 Rectangle。可以通过下面的方式定位子对象:
QObject *rect = object->findChild<QObject*>("rect");
if (rect)
rect->setProperty("color", "red");
这里要注意一下,一个对象可以有多个相同 objectName(属性) 的子对象。比如,ListView 创建其委托的多个实例,如果使用特定的 objectName 声明其委托,则 ListView 将具有多个相同 objectName 的子节点。这种情况下,可以使用 QObject::findChildren() 来查找符合 objectName 的所有子节点。
特别注意:虽然可以在 C++ 中访问并且操作 QML 对象,但是除了测试和原型设计之外,这种方法是不推荐的!QML 和 C++ 集成的优势之一就是实现与 C++ 逻辑和数据集后端分离的 UI 界面,如果在 C++ 中直接操作 QML 将意味着放弃优势。这种方法也使得在不影响对应 C++ 部分的前提下去改动 QML UI 变得困难。
怎么在 C++ 中访问 QML 对象类型的成员呢?
访问属性
任何 QML 对象中声明的属性都自动可以在 C++ 代码中访问。下面再来个栗子:
// textItem.qml
import QtQuick 2.0
Item {
property int a: 100
}
在上面 QML 文件中声明的属性 a 的值可以使用 QQmlProperty 来读写,或者用 QObject::setProperty() 来写属性值和用 QObject::property() 来读属性值:
QQmlEngine engine;
QQmlComponent component(&engine, "textItem.qml");
QObject *object = component.create();
qDebug() << "Property value:" << QQmlProperty::read(object, "someNumber").toInt();
QQmlProperty::write(object, "someNumber", 5000);
qDebug() << "Property value:" << object->property("someNumber").toInt();
object->setProperty("someNumber", 100);
为了确保 QML 引擎知道属性的改变,你应该始终采用 QObject::setProperty(), QQmlProperty 或者 QMetaProperty::write() 来设置 QML 对象的属性值。比如,假如你有个自定义类型 PushButton, 在内部有个属性 buttonText 并且和成员变量 m_buttonText 关联。 像下面这样子直接修改成员变量 m_buttonText 是不受推荐的:
// un-recommended
QQmlComponent component(engine, "textItem.qml");
PushButton *button = qobject_cast<PushButton*>(component.create());
button->m_buttonText = "clicked !";
如果变量 m_buttonText 被直接修改,那么QML 引擎将不会知道属性改变了,因为这种操作完美地躲开了 Qt 的 meta-object system。后果就是,绑定的 buttonText 属性不会被更新,而且属性变更信号槽 onButtonTextChanged() 不会被调用。
访问 QML 方法
由于所有的 QML 方法都暴露给了元对象系统 Meta-object system, 所以在 C++ 代码中可以通过 QMetaObject::invokeMethod() 调用对应的 QML 方法,并且输入的参数和来自 QML 中的返回值在 C++ 中通常被转换成 QVariant 值。下面有个例子:
// textItem.qml
import QtQuick 2.0
Item {
function qmlFunction(msg) {
console.log("Got msg:", msg)
return "return value"
}
}
// main.cpp
QQmlEngine engine;
QQmlComponent component(&engine, "textItem.qml");
QObject *object = component.create();
QVariant returnedValue;
QVariant msg = "hi from C++";
QMetaObject::invokeMethod(object, "qmlFunction",
Q_RETURN_ARG(QVariant, returnedValue),
Q_ARG(QVariant, msg));
qDebug() << "value returned from QML :" << returnedValue.toString();
delete object;
这里要注意一下,参数 Q_RETURN_ARG() and Q_ARG() 必须指定为 QVariant 类型, 因为 QVariant 是用于 QML 方法输入参数和返回值的通用数据类型。
连接 QML 信号
所有 QML 信号都自动适用于 C++ 代码,就像任何普通 Qt C++ 信号一样用 QObject::connect() 连接。反过来,任何 C++ 信号都可以被 QML 对象的信号处理程序接收到。
这里来个栗子,有个 QML 组件定义了一个带 string 类型参数的信号 qmlSignal. 这个信号通过 QObject::connect() 连接到 C++ 对象的信号槽 cppSlot(),所以每当 QML 的信号 qmlSignal 被发送时都会调用 C++ 里的 cppSlot()。
// textItem.qml
import QtQuick 2.0
Item {
id: item
width: 200; height: 200
signal qmlSignal(string message)
MouseArea {
anchors.fill: parent
onClicked: item.qmlSignal("QML say Hi ")
}
}
class MyClass : public QObject
{
Q_OBJECT
public slots:
void cppSlot(const QString &message) {
qDebug() << "C++ got message:" << message;
}
};
int main(int argc, char *argv[]) {
QGuiApplication app(argc, argv);
QQuickView view(QUrl::fromLocalFile("textItem.qml"));
QObject *item = view.rootObject();
MyClass myClass;
QObject::connect(item, SIGNAL(qmlSignal(QString)),
&myClass, SLOT(cppSlot(QString)));
view.show();
return app.exec();
}
当 QML 对象定义的信号带参数并且参数类型为 QML 对象类型时,参数类型应该声明为 var 并且 C++ 的对应接收类型应该使用 QVariant 类型。临走带个栗子吧:
// textItem.qml
import QtQuick 2.0
Item {
id: item
width: 200; height: 200
signal qmlSignal(var anObject)
MouseArea {
anchors.fill: parent
onClicked: item.qmlSignal(item)
}
}
class MyClass : public QObject
{
Q_OBJECT
public slots:
void cppSlot(const QVariant &v) {
qDebug() << "C++ slot got value:" << v;
QQuickItem *item =
qobject_cast<QQuickItem*>(v.value<QObject*>());
qDebug() << "Item w & h:" << item->width()
<< item->height();
}
};
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
QQuickView view(QUrl::fromLocalFile("textItem.qml"));
QObject *item = view.rootObject();
MyClass myClass;
QObject::connect(item, SIGNAL(qmlSignal(QVariant)),
&myClass, SLOT(cppSlot(QVariant)));
view.show();
return app.exec();
}
//
参考英文资料[Qt]https://doc.qt.io/qt-5/qtqml-cppintegration-interactqmlfromcpp.html