Edited at

QMLとC++のバインディング

More than 3 years have passed since last update.

[2016/08/17 19:51 - 少し手直し]

Qtをさわり始めてQMLとC++のバインディングで引っかかったのでまとめてみました。間違いやもっと良い方法があればご指摘下さい。

以下では、QtCreatorでQtQuickアプリケーションのプロジェクトを作成した際に自動で生成される下記のソースコードをベースにします。(ここではui.qmlは使用しません)


main.cpp

#include <QGuiApplication>

#include <QQmlApplicationEngine>

int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);

QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));

return app.exec();
}



main.qml

import QtQuick 2.5

import QtQuick.Window 2.2

Window {
visible: true
width: 640
height: 480
title: qsTr("Hello World")

MouseArea {
anchors.fill: parent
onClicked: {
Qt.quit();
}
}

Text {
text: qsTr("Hello World")
anchors.centerIn: parent
}
}



C++からQMLオブジェクトへアクセス

いろいろな方法があるようですが、ここではobjectNameでアクセスする方法を取り上げます。

main.cppではroot要素へのアクセスを経て、子要素であるTextへQML側で定義するobjectNameでアクセスする。


main.cpp

#include <QGuiApplication>

#include <QQmlApplicationEngine>

int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);

QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));

/* added */
//(1) Get an access to root object and qml object by objectName "textObject" defined in main.qml
QObject *rootObject = engine.rootObjects().first();
QObject *qmlObject = rootObject->findChild<QObject*>("textObject");
// (2) set an object property
qmlObject->setProperty("text", "Text from C++");
//----------

return app.exec();
}


QML側ではアクセスしたい子要素にobjectNameを定義する。


main.qml

import QtQuick 2.5

import QtQuick.Window 2.2

Window {
visible: true
width: 640
height: 480
title: qsTr("Hello World")

MouseArea {
anchors.fill: parent
onClicked: {
Qt.quit();
}
}

Text {
/*added*/
objectName: textObject
// ----------
text: qsTr("Hello World")
anchors.centerIn: parent
}
}


結果

cpp2qml.png

(見にくかったので色をつけてフォントを大きくしています)

なお、オフィシャルではC++からQMLオブジェクトへのアクセスはC++とQMLの独立性の観点から推奨していないようです。


Warning: While it is possible to use C++ to access and manipulate QML objects deep into the object tree, we recommend that you do not take this approach outside of application testing and prototyping. One strength of QML and C++ integration is the ability to implement the QML user interface separately from the C++ logic and dataset backend, and this strategy breaks if the C++ side reaches deep into the QML components to manipulate them directly. This would make it difficult to, for example, swap a QML view component for another view, if the new component was missing a required objectName. It is better for the C++ implementation to know as little as possible about the QML user interface implementation and the composition of the QML object tree.



QMLからC++のクラスへアクセス

ContextPropertyにQObjectインスタンスを埋め込んで、QMLからメソッドをコールする例を挙げてみます。(オフィシャルドキュメントではQDateTimeを返すようになっていましたがもっとシンプルにstringを返す形にしてみました)

まずヘッダファイルの準備。


applicationdata.h

#ifndef APPLICATIONDATA_H

#define APPLICATIONDATA_H

#include <QObject>
#include <QString>

class ApplicationData:public QObject
{
Q_OBJECT
public:
Q_INVOKABLE QString getTextFromCpp(){
return QString("This is the text from C++");
}
};

#endif // APPLICATIONDATA_H


続いて、cppファイルとqmlファイルを変更。


main.cpp

#include <QGuiApplication>

#include <QQmlApplicationEngine>

/*added*/
#include <QQmlContext>
#include "applicationdata.h"
//----------

int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);

QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));

/*added*/
ApplicationData data;
engine.rootContext()->setContextProperty("applicationData",&data);
//----------

return app.exec();
}



main.qml

import QtQuick 2.5

import QtQuick.Window 2.2

Window {
visible: true
width: 640
height: 480
title: qsTr("Hello World")

MouseArea {
anchors.fill: parent
onClicked: {
Qt.quit();
}
}

Text {
/*changed*/
//text: qsTr("Hello World")
text: applicationData.getTextFromCpp()
//----------
anchors.centerIn: parent
}

}


結果

qml2cpp.png


Signal & Slot

今回は、QMLのボタンクリックでC++側へシグナルを出し、C++側のスロットで受け取ったら、逆にC++側からQML側へシグナルを送るというのを例にします。


C++でConnect

まずは、C++側でConnectする例。cppからのシグナルでQML側のSlotが認識されない、No such slotというエラーで泣かされました。引数をQVariantにするとうまくいく。


main.qml

import QtQuick 2.5

import QtQuick.Window 2.2
/*added*/
import QtQuick.Controls 2.0
//----------

Window {
/*added*/
signal qmlSignal(string msg);
function qmlSlot(text)
{
console.log("qmlSlot is called with the text: " + text)
textField.text = text;
}
//----------

visible: true
width: 640
height: 480
title: qsTr("Hello World")

/*deleted*/
// MouseArea {
// anchors.fill: parent
// onClicked: {
// Qt.quit();
// }
// }

Text {
/*added*/
id:textField
//----------
text: qsTr("Hello World")
anchors.centerIn: parent
}

/*added*/
Button{
id:aButton
text:"Emit Signal!"
anchors.centerIn: parent
anchors.verticalCenterOffset: 30
onClicked: qmlSignal("Hello from QML")
}
//----------

}



cppsignalslot.h

#ifndef CPPSIGNALSLOT_H

#define CPPSIGNALSLOT_H

#include <QObject>
#include <QVariant>

class CppSignalSlot : public QObject
{
Q_OBJECT
public:
explicit CppSignalSlot(QObject *parent = 0);

signals:
void cppSignal(QVariant text);

public slots:
void cppSlot(QString msg);
};

#endif // CPPSIGNALSLOT_H



cppsignalslot.cpp

#include "cppsignalslot.h"

#include <QDebug>

CppSignalSlot::CppSignalSlot(QObject *parent) : QObject(parent)
{

}

void CppSignalSlot::cppSlot(QString msg)
{
qDebug() << "cppSlot is called with the message: " << msg;
emit cppSignal("Hello from C++");
}



main.cpp

#include <QGuiApplication>

#include <QQmlApplicationEngine>
/*added*/
#include <QQmlContext>
#include "cppsignalslot.h"
//----------

int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);

QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));

CppSignalSlot obj;

QObject *root = engine.rootObjects().first();

// Connect QML Signal to C++ Slot
QObject::connect(root, SIGNAL(qmlSignal(QString)),
&obj, SLOT(cppSlot(QString)));
// Connect C++ Signal to QML Slot
QObject::connect(&obj, SIGNAL(cppSignal(QVariant)),
root, SLOT(qmlSlot(QVariant)));

return app.exec();
}


結果

クリック前

signalslot_beforeClicking.png

クリック後

signalslot_afterClicked.png

(上のセクションとwindowが違うのは試験環境が違うためです。。。それ以上の理由はありません)


[コンソール出力]

cppSlot is called with the message: "Hello from QML"

qml: qmlSlot is called with the text: Hello from C++



QMLでConnect

次は同じ例でQML側でConnectする例です。後述の通り、Cannot assign to non-existent property とReferenceErrorに泣かされました。


main.qml

import QtQuick 2.5

import QtQuick.Window 2.2
import QtQuick.Controls 2.0

Window {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
/*deleted*/
// MouseArea {
// anchors.fill: parent
// onClicked: {
// Qt.quit();
// }
// }

Text {
id: textField
text: qsTr("Hello World")
anchors.centerIn: parent
}

Button{
text:"Emit Signal!"
anchors.centerIn: parent
anchors.verticalCenterOffset: 30
onClicked: cppSignalSlot.cppSlot("Hello from QML")
}

Connections
{
target:cppSignalSlot
onCppSignal:{
console.log("received cppSignal:" + text)
textField.text = text;
}
}
}



cppsignalslot.h

#ifndef CPPSIGNALSLOT_H

#define CPPSIGNALSLOT_H

#include <QObject>

class CppSignalSlot : public QObject
{
Q_OBJECT
public:
explicit CppSignalSlot(QObject *parent = 0);

signals:
void cppSignal(QString text);

public slots:
void cppSlot(QString text);
};

#endif // SIGNALSLOT_H



cppsignalslot.cpp

#include "cppsignalslot.h"

#include <QDebug>

CppSignalSlot::CppSignalSlot(QObject *parent) : QObject(parent)
{

}

void CppSignalSlot::cppSlot(QString text)
{
qDebug() << "cppSlot is called with text: " + text;
emit cppSignal(QString("Hello from C++"));
}



main.cpp

#include <QGuiApplication>

#include <QQmlApplicationEngine>
#include <QQmlContext>
#include "cppsignalslot.h"

int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);

QQmlApplicationEngine engine;

SignalSlot obj;
// Put this before lading qml file!
engine.rootContext()->setContextProperty("cppSignalSlot",&obj);

engine.load(QUrl(QStringLiteral("qrc:/main.qml")));

return app.exec();
}


ここでミソなのは、C++からcppSignal()でシグナルを出した時の動作をonCppSignalと書くことです。(要するにC++側はLower Camelで書いて、QML側はon+Upper Camelで書きましょうということみたいです)

あと、コメントにも書きましたが、main.cppでRoot ObjectのContextに設定する前にmain.qmlをロードすると、下記のエラーが出ます。(でも、動作はする。。。)


QML Connections: Cannot assign to non-existent property "OnCppSignal"

ReferenceError: cppSignalSlot is not defined



参考

そして、結局Qtのオフィシャルドキュメントが一番わかりやすいというオチ。。。