在C++中與QML物件互動
所有的 QML 物件型別 - 無論由引擎內部實現還是由第三方源定義,都是 QObject 派生的型別。這意味著,QML 引擎可以使用 Qt 元物件系統動態例項化任何 QML 物件型別並檢查建立的物件。
這對於在 C++ 中建立 QML 物件非常有用,無論是顯示一個視覺化呈現的 QML 物件,還是將非可視 QML 物件資料整合到 C++ 應用程式中。一旦 QML 物件被建立,就可以從 C++ 中檢查它,以便讀取和寫入屬性、呼叫方法、以及接收訊號通知。
在C++載入QML物件
可以使用 QQuickView 或 QQmlComponent 來載入 QML 文件。QQmlComponent 將 QML 文件載入為一個 C++ 物件,然後可以從 C++ 程式碼中修改該物件。QQuickView 也做到了這一點,但由於 QQuickView 是一個基於 QWindow 的派生類,載入的物件也將被渲染至視覺化顯示,QQuickView 通常用於將一個視覺化的 QML 物件整合到應用程式的使用者介面中。
例如,有一個 QML 檔案,如下所示:
import QtQuick 2.3
Item {
width: 100; height: 100
}
可以使用 QQmlComponent 或 QQuickView 的 C++ 程式碼載入該 QML 文件。當使用 QQmlComponent 時,需要呼叫 QQmlComponent::create() 來建立元件的新例項:
// 使用 QQmlComponent QQmlEngine engine; QQmlComponent component(&engine, QUrl("qrc:/main.qml")); QObject *object = component.create(); //... delete object;
而 QQuickView 會自動建立元件的例項,該例項可以通過 QQuickView::rootObject() 來訪問:
// 使用 QQuickView
QQuickView view;
view.setSource(QUrl("qrc:/main.qml"));
view.show();
QObject *object = view.rootObject();
例項(object)建立後,就可以使用 QObject::setProperty() 或者 QQmlProperty 來修改其屬性:
object->setProperty("width", 300); QQmlProperty(object, "width").write(300);
或者,將 object 轉換為其實際型別,並使用編譯時安全性呼叫方法。在這種情況下,main.qml 的基本物件是一個 Item,由 QQuickItem 類定義:
QQuickItem *item = qobject_cast<QQuickItem*>(object);
item->setWidth(300);
根據objectName 訪問載入的QML物件
QML 元件實質上是具有子物件的物件樹,子物件有兄弟,也有孩子。可以使用 QObject::objectName 屬性和 QObject::findChild() 來定位 QML 元件的子物件。
例如,如果 QML 中的根 Item 有一個 Rectangle 子項:
// main.qml
import QtQuick 2.3
Item {
width: 100; height: 100
Rectangle {
anchors.fill: parent
objectName: "rect"
}
}
可以通過這樣來定位孩子:
QObject *rect = object->findChild<QObject*>("rect");
if (rect)
rect->setProperty("color", "red");
注意: 一個物件可能有多個具有相同 objectName 的子項,這種情況下,QObject::findChildren() 可用於查詢具有匹配 objectName 的所有子項。
警告: 雖然可以使用 C++ 深入物件樹中訪問和操作 QML 物件,但建議不要在應用程式測試和原型設計之外採用此方法。QML 和 C++ 整合的一個優勢是實現 QML 使用者介面獨立於 C++ 邏輯和資料集後端,如果 C++ 端深入到 QML 元件中直接操作它們,這種策略就會被打破。對於 C++ 實現,最好儘可能少地瞭解 QML 使用者介面實現和 QML 物件樹的組成。
在C++中訪問QML物件型別的成員
屬性
QML 中宣告的任何屬性都可以從 C++ 中訪問。
例如,下面的 QML 聲明瞭一個簡單的字串:
// main.qml
import QtQuick 2.3
Item {
property string hey: "Hello, Qter!"
}
在 C++ 中,屬性 hey 的值可以使用 QQmlProperty 來設定和讀取,也可使用 QObject::setProperty() 和 QObject::property():
QQmlEngine engine;
QQmlComponent component(&engine, QUrl("qrc:/main.qml"));
QObject *object = component.create();
qDebug() << "Property value:" << QQmlProperty::read(object, "hey").toString();
QQmlProperty::write(object, "hey", "Hello, Qt!");
qDebug() << "Property value:" << object->property("hey").toString();
object->setProperty("hey", "Hello, QML!");
qDebug() << "Property value:" << object->property("hey").toString();
注意: 應該始終使用 QObject::setProperty()、QQmlProperty 或 QMetaProperty::write() 來改變 QML 的屬性值,以確保 QML 引擎感知屬性的變化。
例如,有一個自定義型別 PushButton,它有一個 buttonText 屬性。在內部,該屬性以成員變數 m_buttonText 來反映值,可以像下面這樣直接修改成員變數:
// 糟糕的程式碼
QQmlComponent component(engine, "MyButton.qml");
PushButton *button = qobject_cast<PushButton*>(component.create());
button->m_buttonText = "Click me";
但這並不是一個好主意。由於值被直接改變,繞過了 Qt 元物件系統,QML 引擎並沒有意識到屬性的變化。這意味著,繫結到 buttonText 的屬性不會被更新,並且 onButtonTextChanged 處理程式也不會被呼叫。
呼叫QML方法
所有的 QML 方法都被暴露給了 Qt 元物件系統,可以使用 QMetaObject::invokeMethod() 從 C++ 中呼叫。從 QML 傳遞的方法引數和返回值在 C++ 中被轉換為 QVariant 值。
寫一個簡單的 QML,併為其新增一個方法:
// main.qml
import QtQuick 2.3
Item {
function myQmlFunction(msg) {
console.log("Got message:", msg)
return "Hello, Qter!"
}
}
然後,在 C++ 中使用 QMetaObject::invokeMethod() 進行呼叫:
// main.cpp
QQmlEngine engine;
QQmlComponent component(&engine, QUrl("qrc:/main.qml"));
QObject *object = component.create();
QVariant returnedValue; // 返回值
QVariant msg = "Hello, QML!"; // 方法引數
// 呼叫 QML 方法
QMetaObject::invokeMethod(object, "qmlFunction",
Q_RETURN_ARG(QVariant, returnedValue),
Q_ARG(QVariant, msg));
qDebug() << "QML function returned:" << returnedValue.toString();
delete object;
注意: QMetaObject::invokeMethod() 的 Q_RETURN_ARG() 和 Q_ARG() 引數必須被指定為 QVariant 型別,因為這是用於 QML 方法引數和返回值的通用資料型別。
連線到QML 訊號
所有的 QML 訊號在 C++ 中都是可用的,和普通的 Qt C++ 訊號一樣,可以使用 QObject::connect() 進行連線。反過來,任何 C++ 訊號可以由 QML 物件使用訊號處理器來接收。
基本型別的訊號引數
這裡有一個 QML 元件,包含一個名為 qmlSignal 的訊號,該訊號包含一個 string 型別引數:
// main.qml
import QtQuick 2.3
Item {
id: item
width: 100; height: 100
// QML 訊號
signal qmlSignal(string msg)
MouseArea {
anchors.fill: parent
// 點選滑鼠,發射訊號
onClicked: item.qmlSignal("Hello, Qter!")
}
}
寫一個 C++ 類,並實現一個槽函式,用於接收 QML 發射的訊號:
// qter.h
#ifndef QTER_H
#define QTER_H
#include <QObject>
#include <qDebug>
class Qter : public QObject
{
Q_OBJECT
public slots:
// 槽函式
void cppSlot(const QString &msg) {
qDebug() << "Called the C++ slot with message:" << msg;
}
};
#endif // QTER_H
將訊號連線至 C++ 物件的槽函式,當發出 qmlSignal 訊號時,就會呼叫:
// qter.h
#include <QGuiApplication>
#include <QQuickView>
#include <QQuickItem>
#include "qter.h"
int main(int argc, char *argv[]) {
QGuiApplication app(argc, argv);
QQuickView view(QUrl("qrc:/main.qml"));
QObject *item = view.rootObject();
Qter qter;
// 連線訊號槽
QObject::connect(item, SIGNAL(qmlSignal(QString)), &qter, SLOT(cppSlot(QString)));
view.show();
return app.exec();
}
物件型別的訊號引數
當訊號的引數為 QML 物件型別時,應使用 var 作為引數型別,並且在 C++ 中應使用 QVariant 型別接收該值。
例如,將上述示例中的引數改為 QML 物件型別:
// main.qml
import QtQuick 2.3
Item {
id: item
width: 100; height: 100
// QML 訊號
signal qmlSignal(var object)
MouseArea {
anchors.fill: parent
// 點選滑鼠,發射訊號
onClicked: item.qmlSignal(item)
}
}
要接收該訊號,C++ 類中的槽函式的引數應該改為 QVariant 型別:
// qter.h
#ifndef QTER_H
#define QTER_H
#include <QObject>
#include <QQuickItem>
#include <qDebug>
class Qter : public QObject
{
Q_OBJECT
public slots:
// 槽函式
void cppSlot(const QVariant &v) {
qDebug() << "Called the C++ slot with value:" << v;
QQuickItem *item = qobject_cast<QQuickItem*>(v.value<QObject*>());
qDebug() << "Item Size:" << item->width() << item->height();
}
};
#endif // QTER_H
當然,連線訊號槽的的引數型別也需要修改:
QObject::connect(item, SIGNAL(qmlSignal(QVariant)), &qter, SLOT(cppSlot(QVariant)));