Qt Quick 技术的引入,使得你能够快速构建 UI ,具有动画、各种绚丽效果的 UI 都不在话下。但它不是万能的,也有很多局限性,原来 Qt 的一些技术,比如低阶的网络编程如 QTcpSocket ,多线程,又如 XML 文档处理类库 QXmlStreamReader / QXmlStreamWriter 等等,在 QML 中要么不可用,要么用起来不方便,所以呢,很多时候我们是会基于这样的原则来混合使用 QML 和 C++: QML 构建界面, C++ 实现非界面的业务逻辑和复杂运算。
QML 的很多基本类型原本就是在 C++ 中实现的,比如 Item 对应 QQuickItem , Image 对应 QQuickImage , Text 对应 QQuickText ,……这样看来,在 QML 中访问 C++ 对象必然不成问题。然也!反过来,在 C++ 中其实也可以使用 QML 对象。
在 QML 中使用 C++ 类和对象
我们知道, QML 其实是对 JavaScript 的扩展,融合了 Qt Object 系统,它是一种新的解释型的语言, QML 引擎虽然由 Qt C++ 实现,但 QML 对象的运行环境,说到底和 C++ 对象的上下文环境是不同的,是平行的两个世界。如果你想在 QML 中访问 C++ 对象,那么必然要找到一种途径来在两个运行环境之间建立沟通桥梁。
Qt 提供了两种在 QML 环境中使用 C++ 对象的方式:
- 在 C++ 中实现一个类,注册到 QML 环境中, QML 环境中使用该类型创建对象
- 在 C++ 中构造一个对象,将这个对象设置为 QML 的上下文属性,在 QML 环境中直接使用改属性
不管哪种方式,对要导出的 C++ 类都有要求,不是一个类的所有方法、变量都可以被 QML 使用,因此我们先来看看怎样让一个方法或属性可以被 QML 使用。
实现可以导出的 C++ 类
前提条件
- 从 QObject 或 QObject 的派生类继承
- 使用 Q_OBJECT 宏
信号,槽
我们首先来看一个完整类的实现。
LogicMaker.h
#include <QObject> class LogicMaker:public QObject { Q_OBJECT Q_ENUMS(kGameType) //直接调用函数,非槽函数 Q_INVOKABLE void qmlCallCfunction(); Q_PROPERTY(int width READ width WRITE setWidth NOTIFY widthChanged) public: LogicMaker(QObject *p); LogicMaker(){ } enum kGameType{ TYPE_DOTA=2, TYPE_WAR3, TYPE_RPG, }; int width(); void setWidth(int); signals: void widthChanged(int newwidth); public slots: void qmlCallCSlotfunction(kGameType type); private: int _width; QObject* pparent; };
LogicMaker.cpp
#include "logicmaker.h"
#include<QDebug> #include <QQuickView> #include <QQuickItem> LogicMaker::LogicMaker(QObject *obj) { _width=0; pparent=obj; qDebug()<<"parent"<<pparent; } void LogicMaker::qmlCallCSlotfunction(kGameType type){ qDebug()<<"qml call C++ slots function"<<type; setWidth(5); qDebug()<<"=======parent"<< this->parent(); QObject *quitButton = this->parent()->findChild<QObject*>("qmlbtn2");//要在qml中设置其对应的objname if(quitButton!=NULL) { QObject::connect(quitButton, SIGNAL(clicked()), this, SLOT(qmlCallCfunction())); //setText这个一定会调用失败,因为并没有setText这个属性 bool bRet = QMetaObject::invokeMethod(quitButton, "setText", Q_ARG(QString, "world hello")); qDebug() << "call setText return - " << bRet; quitButton->setProperty("width", 200); quitButton->setProperty("text", QString(tr("hello,world"))); } else { qDebug()<<"get button failed"; } } void LogicMaker::qmlCallCfunction(){ qDebug()<<"qml call C++ function"; } int LogicMaker::width(){ return _width; } void LogicMaker::setWidth(int w){ _width=w; emit widthChanged(w); }
main.cpp
#include <QGuiApplication> #include <QQuickView> #include <QQuickItem> #include <QQmlContext> include "logicmaker.h" int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); // QQmlApplicationEngine engine; //engine.load(QUrl(QStringLiteral("qrc:///main.qml"))); //在qml环境准备好之前,注册好qml类型 qmlRegisterType<LogicMaker>("seanyxie.qt.logicMaker", 1, 0,"LogicMaker"); QQuickView viwer; viwer.setSource(QUrl(QStringLiteral("qrc:///main.qml"))); // QObject *rootItem=NULL; QQuickItem *rootItem = viwer.rootObject(); qDebug()<<rootItem; viwer.rootContext()->setContextProperty("cpplogicMaker",new LogicMaker(rootItem)); viwer.show(); return app.exec(); }
main.qml
import QtQuick 2.2 import QtQuick.Window 2.1 import QtQuick.Controls 1.1 import seanyxie.qt.logicMaker 1.0 Rectangle { visible: true width: 360 height: 360 id:rect MouseArea { anchors.fill: parent onClicked: { Qt.quit(); } } Text { text: qsTr("Hello World") anchors.centerIn: parent } LogicMaker{ id:qml2Cmaker; } Row{ Button{ id:btn1 onClicked: { qml2Cmaker.qmlCallCSlotfunction(LogicMaker.TYPE_RPG); // cpplogicMaker.qmlCallCSlotfunction(LogicMaker.TYPE_RPG); } } Button{ id:btn2 objectName:"qmlbtn2" onClicked: { // qml2Cmaker.qmlCallCfunction(); } } } //这个对象是用来绑定一个从C++来的信号,对应的曹函数 Connections{ target:qml2Cmaker onWidthChanged: { btn1.width = newwidth // console.log("width change slot %d",newwidth); } } }
第一步,在main.cpp里面我们注册了一个类,可以在qml中直接被使用,就是这段代码
qmlRegisterType("seanyxie.qt.logicMaker", 1, 0,"LogicMaker");
这个过程大概分四个步骤:
- 实现 C++ 类
- 注册 QML 类型
- 在 QML 中导入类型
- 在 QML 创建由 C++ 导出的类型的实例并使用
要注册一个 QML 类型,有多种方法可用,如 qmlRegisterSingletonType() 用来注册一个单例类型, qmlRegisterType() 注册一个非单例的类型, qmlRegisterTypeNotAvailable() 注册一个类型用来占位, qmlRegisterUncreatableType() 通常用来注册一个具有附加属性的附加类型
qmlRegisterType()是一个模板函数
template<typename T> int qmlRegisterType(const char *uri, int versionMajor, int versionMinor, const char *qmlName); template<typename T, int metaObjectRevision> int qmlRegisterType(const char *uri, int versionMajor, int versionMinor, const char *qmlName);
所以这里注册的类,在qml中使用的话,就要先
import seanyxie.qt.logicMaker 1.0
在 QML 中创建 C++ 导入类型的实例
LogicMaker{
id:qml2Cmaker;
}
我们看到,LogicMaker和Rectangle等用法没有什么不同,指定一个id,就可以在qml中直接使用这个对象。
我们在LogicMaker中定义了槽函数qmlCallCSlotfunction(),可以直接在qml中使用qml2Cmaker对象来调用这个槽函数,但是还有一个参数,这个参数是C++类里的枚举,这时候需要要QENUM宏来导出这组枚举。
Q_ENUMS
一旦你使用 Q_ENUMS 宏注册了你的枚举类型,在 QML 中就可以用 ${CLASS_NAME}.${ENUM_VALUE} 的形式来访问,比如 LogicMaker.TYPE_DOTA,上节展示的 QML 代码片段已经使用了导出的枚举类型。
Q_INVOKABLE 宏
Q_PROPERTY
Q_PROPERTY(type name
(READ getFunction [WRITE setFunction] | MEMBER memberName [(READ getFunction | WRITE setFunction)]) [RESET resetFunction] [NOTIFY notifySignal] [REVISION int] [DESIGNABLE bool] [SCRIPTABLE bool] [STORED bool] [USER bool] [CONSTANT] [FINAL])
是不是很复杂?你可以为一个属性命名,可以设定的选项数超过10个……我是觉得有点儿头疼。不过,不是所有的选项都必须设定,看一个最简短的属性声明:
Q_PROPERTY(int width READ width WRITE setWidth NOTIFY widthChanged)
- READ 标记,如果你没有为属性指定 MEMBER 标记,则 READ 标记必不可少;声明一个读取属性的函数,该函数一般没有参数,返回定义的属性。
- WRITE 标记,可选配置。声明一个设定属性的函数。它指定的函数,只能有一个与属性类型匹配的参数,必须返回 void 。
- NOTIFY 标记,可选配置。给属性关联一个信号(该信号必须是已经在类中声明过的),当属性的值发生变化时就会触发该信号。信号的参数,一般就是你定义的属性。
所以上述定义的width属性,可以用width,setWidth来读写,并且在width发生变化时候,关联一个信号widthChanged的信号。
所以这段代码的表现效果是:
点击了btn1按钮后,会通过LogicMaker的对象qml2Cmaker调用C++里LogicMaker的槽函数qmlCallCSlotFunction,并且带一个枚举类型的参数。然后在qmlCallCSlotFunction方法里,调用setWidth来设置width属性,并且发射出信号widthChanged的信号。
这个信号将会被qml捕获处理,在qml中有下面一段代码:
Connections{
target:qml2Cmaker onWidthChanged: { btn1.width = newwidth // console.log("width change slot %d",newwidth); } }
Connections的解释:
A Connections object creates a connection to a QML signal.
When connecting to signals in QML, the usual way is to create an “on
MouseArea { onClicked: { foo(parameters) }
}
就是用来绑定一个QML信号的处理对象,它的槽函数使用on+信号名的格式,所以qml中onWidthChanged,在设定了target属性后,就会绑定qml2Cmaker对象的信号WidthChanged,这个信号在C++中发出,并在qml中处理。从而修改了btn1按钮的宽度。
导出一个 C++ 对象为 QML 的属性
我们看main.cpp里代码
QQuickItem *rootItem = viwer.rootObject();
qDebug()< viwer.rootContext()->setContextProperty("cpplogicMaker",new LogicMaker(rootItem));正式这行代码从堆上分配了一个 LogicMaker 对象,然后注册为 QML 上下文的属性,起了个名字就叫 cpplogicMaker 。viewer.rootContext() 返回的是 QQmlContext 对象。 QQmlContext 类代表一个 QML 上下文,它的 setContextProperty() 方法可以为该上下文设置一个全局可见的属性。要注意的是,你 new 出来的对象, QQmlContext 只是使用,不会帮你删除,你需要自己找一个合适的时机来删除它。还有一点要说明,因为我们去掉了 qmlRegisterType() 调用,所以在 main.qml 中不能再访问LogicMaker 类了,比如你不能通过类名来引用它定义的枚举类,也不能定义LogicMaker对象了。
然后我们就可以在qml中使用这个全局可见对象cpplogicMaker了。
现在来看如何在 QML 中使用我们导出的属性在 QML 中使用关联到 C++ 对象的属性
一旦调用 setContextProperty() 导出了属性,就可以在 QML 中使用了,不需要 import 语句哦。下面cpplogicMaker.qmlCallCSlotfunction(LogicMaker.TYPE_RPG);当然这里因为到处了LogicMaker类,所以才能访问LogicMaker.TYPE_RPG枚举。
在 C++ 中使用 QML 对象
看过了如何在 QML 中使用 C++ 类型或对象,现在来看如何在 C++ 中使用 QML 对象。我们可以使用 QML 对象的信号、槽,访问它们的属性,都没有问题,因为很多 QML 对象对应的类型,原本就是 C++ 类型,比如 Image 对应 QQuickImage , Text 对应 QQuickText……但是,这些与 QML 类型对应的 C++ 类型都是私有的,你写的 C++ 代码也不能直接访问。肿么办?Qt 最核心的一个基础特性,就是元对象系统,通过元对象系统,你可以查询 QObject 的某个派生类的类名、有哪些信号、槽、属性、可调用方法等等信息,然后也可以使用 QMetaObject::invokeMethod() 调用 QObject 的某个注册到元对象系统中的方法。而对于使用 Q_PROPERTY 定义的属性,可以使用 QObject 的 property() 方法访问属性,如果该属性定义了 WRITE 方法,还可以使用 setProperty() 修改属性。所以只要我们找到 QML 环境中的某个对象,就可以通过元对象系统来访问它的属性、信号、槽等。查找一个对象的孩子
QObject 类的构造函数有一个 parent 参数,可以指定一个对象的父亲, QML 中的对象其实借助这个组成了以根 item 为父的一棵对象树。而 QObject 定义了一个属性 objectName ,这个对象名字属性,就可以用于查找对象。现在该说到查找对象的方法了: findChild() 和 findChildren() 。它们的函数原型如下:
T QObject::findChild(const QString & name = QString(),\ Qt::FindChildOptions options = \ Qt::FindChildrenRecursively) const; QList<T> QObject::findChildren(const QString & name = \ QString(), Qt::FindChildOptions options = \ Qt::FindChildrenRecursively) const; QList<T> QObject::findChildren(const QRegExp & regExp, \ Qt::FindChildOptions options = \ Qt::FindChildrenRecursively) const; QList<T> QObject::findChildren(const QRegularExpression & re,\ Qt::FindChildOptions options = \ Qt::FindChildrenRecursively) const;
示例 1 :
- QPushButton *button = parentWidget->findChild
(“button1”); 查找 parentWidget 的名为 “button1” 的类型为 QPushButton 的孩子。
示例 2 :
- QList
widgets = parentWidget.findChildren (“widgetname”); 返回 parentWidget 所有名为 “widgetname” 的 QWidget 类型的孩子列表。
使用元对象调用一个对象的方法
QMetaObject 的 invokeMethod() 方法用来调用一个对象的信号、槽、可调用方法。它是个静态方法,其函数原型如下:bool QMetaObject::invokeMethod(QObject * obj, const char * member, Qt::ConnectionType type, QGenericReturnArgument ret, QGenericArgument val0 = QGenericArgument( 0 ), QGenericArgument val1 = QGenericArgument(), QGenericArgument val2 = QGenericArgument(), QGenericArgument val3 = QGenericArgument(), QGenericArgument val4 = QGenericArgument(), QGenericArgument val5 = QGenericArgument(), QGenericArgument val6 = QGenericArgument(), QGenericArgument val7 = QGenericArgument(), QGenericArgument val8 = QGenericArgument(), QGenericArgument val9 = QGenericArgument()) [static]
其实 QMetaObject 还有三个 invokeMethod() 函数,不过都是上面这个原型的重载,所以我们只要介绍上面这个就 OK 了。先说返回值吧,返回 true 说明调用成功。返回 false ,要么是因为没有你说的那个方法,要么是参数类型不匹配。第一个参数是被调用对象的指针。第二个参数是方法名字。第三个参数是连接类型,看到这里你就知道, invokeMethod 为信号与槽而生,你可以指定连接类型,如果你要调用的对象和发起调用的线程是同一个线程,那么可以使用 Qt::DirectConnection 或 Qt::AutoConnection 或 Qt::QueuedConnection ,如果被调用对象在另一个线程,那么建议你使用 Qt::QueuedConnection 。第四个参数用来接收返回指。然后就是多达 10 个可以传递给被调用方法的参数。嗯,看来信号与槽的参数个数是有限制的,不能超过 10 个。对于要传递给被调用方法的参数,使用 QGenericArgument 来表示,你可以使用 Q_ARG 宏来构造一个参数,它的定义是:QGenericArgument Q_ARG( Type, const Type & value)
返回类型是类似的,使用 QGenericReturnArgument 表示,你可以使用 Q_RETURN_ARG 宏来构造一个接收返回指的参数,它的定义是:
QGenericReturnArgument Q_RETURN_ARG( Type, Type & value)
假设一个对象有这么一个槽 compute(QString, int, double) ,返回一个 QString 对象,那么你可以这么调用(同步方式):
QString retVal; QMetaObject::invokeMethod(obj, "compute", Qt::DirectConnection, Q_RETURN_ARG(QString, retVal), Q_ARG(QString, "sqrt"), Q_ARG(int, 42), Q_ARG(double, 9.7));
如果你要让一个线程对象退出,可以这么调用(队列连接方式):
QMetaObject::invokeMethod(thread, "quit", Qt::QueuedConnection);
所以在LogicMaker类的函数中可以这样来调用qml中的对象
void LogicMaker::qmlCallCSlotfunction(kGameType type){ qDebug()<<"qml call C++ slots function"<<type; setWidth(5); qDebug()<<"=======parent"<< this->parent(); QObject *quitButton = this->parent()->findChild<QObject*>("qmlbtn2");//要在qml中设置其对应的objname if(quitButton!=NULL) { QObject::connect(quitButton, SIGNAL(clicked()), this, SLOT(qmlCallCfunction())); //setText这个一定会调用失败,因为并没有setText这个属性 bool bRet = QMetaObject::invokeMethod(quitButton, "setText", Q_ARG(QString, "world hello")); qDebug() << "call setText return - " << bRet; quitButton->setProperty("width", 200); quitButton->setProperty("text", QString(tr("hello,world"))); } else { qDebug()<<"get button failed"; } }