转自:C++程序中使用QML绑定机制
原文地址:http://doc.qt.digia.com/4.7-snapshot/qtbinding.html
QML被定为一种可容易使用C++扩展,并可扩展C++的语言.使用Qt Declarative模块中的类可在C++中加载和操作QML中的组件,通过Qt的元对象系统,QML和C++对象可轻易的使用信号和槽机制进行通信.此外,QML插件可以创建发布可重用QML组件.
你可能有很多种理由要将QML和C++混合使用.如:
- 使用C++源码中的函数/功能 (如使用基于Qt的C++数据模型,或调用三方C++库中的函数)
- 访问Qt Declarative模块中的函数/功能 (如使用QDeclarativeImageProvider动态创建图像)
- 创建QML组件(用于自己的项目或发布给其他人使用)
要使用Qt Declarative模块,必须包含和链接相应的模块,请见module index page.Qt Declarative UI Runtime 文档展示如何使用这个模块创建基于C++的应用程序.
核心模块类
Qt Declarative模块提供了一组C++ API用于在C++中扩展QML应用程序,并可将QML嵌入到C++应用程序中.Qt Declarative模块中有几个核心类为实现这个目标提供了必要的支持:
- QDeclarativeEngine: QML引擎提供了运行QML的环境.每个应用程序都需要至少一个引擎实例.
- QDeclarativeComponent:一个组件封装了一个QML文档(QML document).
- QDeclarativeContext: 上下文用来使应用程序向引擎创建的QML组件暴露数据.
QDeclarativeEngine 用来为其中的所有QML组件实例配置全局选项:如用于网络通信的QNetworkAccessManager 和用于持久化存储的文件路径等.
QDeclarativeComponent 用于加载QML文档.每个QDeclarativeComponent 实例代表一个单一文档.组件可使用代表QML文档的URL或文件路径,QML代码来创建.组件实例化是通过QDeclarativeComponent::create()方法完成的,如下所示:
QDeclarativeEngine engine;
QDeclarativeComponent component(&engine, QUrl::fromLocalFile("MyRectangle.qml"));
QObject *rectangleInstance = component.create();
// ...
delete rectangleInstance;
QML文档也可使用QDeclarativeView来加载.这个类为基于QWidget的视图加载QML组件提供了方便.(向基于QWidget的应用程序中整合QML的其他方法请见Integrating QML Code with existing Qt UI code)
QML与C++结合的方式
使用C++扩展QML应用程序有很多种方式.例如::
- 在C++中加载QML组件并进行操作(可操作其子元素)
- 直接将C++对象及其属性嵌入到QML组件中(例如,在QML中调用指定的C++对象,或使用数据集来模拟一个列表模型)
- 定义新的QML元素(QObject继承)并可在QML代码中直接创建
这些方式在下面做展示.当然这些方式相互间不冲突,在应用程序中可根据需要组合使用.
在C++中加载QML组件
QML文档可使用QDeclarativeComponent 或QDeclarativeView来加载.QDeclarativeComponent 将QML组件作为一个C++对象加载;QDeclarativeView 也是这样的,但他将QML组件直接加载到一个QGraphicsView中. 可方便的将QML组件加载到一个基于QWidget应用程序中.
例如,有如下所示的一个MyItem.qml文件:
import QtQuick 1.0
Item {
width: 100; height: 100
}
下面的C++代码将这个QML文档加载到QDeclarativeComponent 或QDeclarativeView .使用QDeclarativeComponent 需要调用QDeclarativeComponent::create()来创建一个新的组件实例,而QDeclarativeView 自动创建组件实例,可通过QDeclarativeView::rootObject()来访问:
|
|
这样就创建了一个MyItem.qml组件的实例--object.可使用QObject::setProperty() 或QDeclarativeProperty修改项目的属性:
object->setProperty("width", 500);
QDeclarativeProperty(object, "width").write(500);
当然,也可将对象转换为其实际类型,以便于在编译时期安全的调用方法.本例中基于MyItem.qml的对象是一个Item,由QDeclarativeItem类来定义:
QDeclarativeItem *item = qobject_cast<QDeclarativeItem*>(object);
item->setWidth(500);
也可使用QMetaObject::invokeMethod() 和QObject::connect()来连接或调用定义在组件中的信号或函数.更多信息见Exchanging data between QML and C++ .
定位子对象
QML组件本质上是一个具有兄弟和子节点的对象树.可使用QObject::findChild()传递一个对象名称获取QML组件的子对象.例如MyItem.qml中的根对象具有一个Rectangle子元素:
import QtQuick 1.0
Item {
width: 100; height: 100
Rectangle {
anchors.fill: parent
objectName: "rect"
}
}
可这样获取子对象:
QObject *rect = object->findChild<QObject*>("rect");
if (rect)
rect->setProperty("color", "red");
如果objectName被用于ListView,Repeater代理,或其他生成多个实例的代理上,将会有多个子对象具有相同的名称(objectName).这时,使用QObject::findChildren()获取所有叫做objectName的子元素.
警告: 由于这种方法可以在C++中获取并操作对象树中内部的QML元素,除了测试和建立原型外我们不建议采用这种方式.QML和C++整合在一起的一个优势就是将QML的用户界面与C++逻辑和数据集相隔离,如果在C++中直接获取并操作QML对象中的内部组件会打破这个策略. 这将使开发变得困难,如更改了QML视图,新的组件中不含objectName子元素,会发生错误.最好的情况是C++实现对QML用户界面实现和内部组成QML对象树不做任何假设.
在QML组件中嵌入C++对象
当在C++应用程序中加载QML场景时,将C++数据嵌入到QML对象中是很有帮助的.QDeclarativeContext 可以向QML组件暴漏数据,将数据从C++注入到QML中.
例如,下面的QML项中有一个currentDateTime值,但并没有在这个上下文中声明:
// MyItem.qml
import QtQuick 1.0
Text { text: currentDateTime }
这个currentDateTime值可以直接由加载QML组件的C++应用程序使用QDeclarativeContext::setContextProperty()进行设置:
QDeclarativeView view;
view.rootContext()->setContextProperty("currentDateTime", QDateTime::currentDateTime());
view.setSource(QUrl::fromLocalFile("MyItem.qml"));
view.show();
上下文属性可以存储为QVariant或者QObject*类型.这意味着自定义的C++对象也可以使用这种方式注入,而且可以直接在QML中读取或修改这些对象.我们将上例中的QDateTime值修改为一个嵌入的QObject实例,让QML代码调用对象实例的方法:
| |
// MyItem.qml import QtQuick 1.0 Text { text: applicationData.getCurrentDateTime() }
(注意C++向QML返回的date/time值可使用Qt.formatDateTime() 及相关函数进行格式化.)
如果QML需要接收上下文的信号,可使用Connections元素进行连接.例如,如果ApplicationData有一个叫做dataChanged()的信号,这个信号可以使用Connections对象连接到一个信号处理器上:
Text { text: applicationData.getCurrentDateTime() Connections { target: applicationData onDataChanged: console.log("The application data changed!") } }
上下文属性在QML视图中使用基于C++的数据模型时很有用.见String ListModel,Object ListModel 和 AbstractItemModel 模型,展示了在QML视图中使用QStringListModel模型,基于QObjectList的模型 和QAbstractItemModel模型 .
更多信息见QDeclarativeContext .
定义新的QML元素
QML中可以定义新的QML元素,同样也可在C++中定义;事实上很多QML元素都是通过C++类实现的.当使用这些元素创建一个QML对象时,只是简单的创建了这个基于QObject的C++类的实例,并设置了属性.
要创建与Qt Quick元素兼容的项,需要使用QDeclarativeItem作为基类.然后实现自绘和像其他QGraphicsObject一样的功能.注意在QDeclarativeItem中默认设置了QGraphicsItem::ItemHasNoContents,因为其不绘制任何东西;如果项目需要绘制则需要清除这个标志(相反的情况是只作为输入处理和逻辑分组的情况).
例如,下面是一个带有image属性的ImageViewer类:
-
#include <QtCore>
-
#include <QtDeclarative>
-
class ImageViewer : public QDeclarativeItem
-
{
-
Q_OBJECT
-
Q_PROPERTY(QUrl image READ image WRITE setImage NOTIFY imageChanged)
-
public:
-
void setImage(const QUrl &url);
-
QUrl image() const;
-
signals:
-
void imageChanged();
-
};
除了从QDeclarativeItem继承外,这都可作为与QML无关的常规类.然而,使用qmlRegisterType()注册到QML引擎后:
qmlRegisterType<ImageViewer>("MyLibrary", 1, 0, "ImageViewer");
加载到C++应用程序或插件中的QML代码就可以操作ImageViewer对象:
import MyLibrary 1.0 ImageViewer { image: "smile.png" }
这里建议不要使用QDeclarativeItem文档指定属性之外的功能.这是因为GraphicsView后台依赖QML的实现细节,因此QtQuick项可再向底层移动,在QML角度上可以应用但不能修改.要最小化自定义可视项的可移植要求,就应尽量坚持使用QDeclarativeItem文档标记的属性.从QDeclarativeItem中继承但没有文档化的属性都是与实现细节相关的;他们不受官方支持可能在相关的发布版本中被去掉.
注意自定义的C++类不必从QDeclarativeItem继承;只有在需要显示时才是必须的.如果项不可见,可从QObject继承.
创建QML元素的更多信息,见Writing QML extensions with C++ 和Extending QML Functionalities using C++ .
在QML和C++之间交换数据
QML和C++对象之间可通过信号槽,属性修改等机制进行通信.对于一个C++对象,任何暴露在Qt的元对象系统中的数据--属性,信号,槽和使用Q_INVOKABLE标记的方法都可在QML中访问.在QML端,所有QML对象的数据都可在Qt元对象系统和C++中访问.
调用函数
QML函数可在C++中调用,反之亦然.
所有的QML函数都被暴漏在了元数据系统中,并可通过QMetaObject::invokeMethod()调用.C++应用程序调用QML函数:
// MyItem.qml import QtQuick 1.0 Item { function myQmlFunction(msg) { console.log("Got message:", msg) return "some return value" } }
|
|
注意QMetaObject::invokeMethod()中Q_RETURN_ARG() 和Q_ARG()的参数必须指定为QVariant类型,这是QML函数和返回值的通用数据类型.
在QML中调用C++函数,函数必须是Qt的槽或标记了Q_INVOKABLE宏的函数,才能在QML中访问.下面范例中,QML代码调用了(使用QDeclarativeContext::setContextProperty()设置到QML中的)myObject对象的方法:
// MyItem.qml import QtQuick 1.0 Item { width: 100; height: 100 MouseArea { anchors.fill: parent onClicked: { myObject.cppMethod("Hello from QML") myObject.cppSlot(12345) } } }
|
|
QML支持调用C++的重载函数.如果C++中有多个同名不同参的函数,将根据参数数量和类型调用正确的函数.
接收信号
所有QML信号都可在C++中访问,像任何标准的Qt C++信号一样可使用QObject::connect()进行连接.相反,任何C++信号都可被QML对象的信号处理函数接收.
下面的QML组件具有一个叫做qmlSignal的信号.这个信号使用QObject::connect()连接到了一个C++对象的槽上,当qmlSignal触发时会调用cppSlot()函数:
// MyItem.qml import QtQuick 1.0 Item { id: item width: 100; height: 100 signal qmlSignal(string msg) MouseArea { anchors.fill: parent onClicked: item.qmlSignal("Hello from QML") } }
|
|
要在QML中连接Qt C++的信号,使用on<SignalName>语法访问信号句柄.如果C++对象可直接在QML中创建(见上面的Defining new QML elements),信号处理函数可在对象定义时指定.在下面例子中,QML代码创建了一个ImageViewer对象,C++对象的imageChanged和loadingError信号连接到QML中的onImageChanged和onLoadingError信号处理函数:
ImageViewer { onImageChanged: console.log("Image changed!") onLoadingError: console.log("Image failed to load:", errorMsg) }
| |
(注意如果信号被声明为属性的NOTIFY信号,QML就允许使用on<Property>Changed句柄访问这个信号,即使信号的名称不是<Property>Changed.上例中,如果将imageChanged信号改为imageModified,onImageChanged信号处理函数还是会被调用的.)
然而对于不是从QML中创建的对象,QML中的元素只能访问已创建的对象--例如如果对象是通过QDeclarativeContext::setContextProperty()设置的--就可使用Connections元素来设置信号处理函数了:
// MyItem.qml import QtQuick 1.0 Item { Connections { target: imageViewer onImageChanged: console.log("Image has changed!") } }
| |
C++信号可以使用枚举值作为参数,枚举定义在类中随信号触发而传递,这个枚举必须使用Q_ENUMS宏注册.见Using enumerations of a custom type.
修改属性
C ++中可以访问QML对象的所有属性.对如下QML对象:
// MyItem.qml import QtQuick 1.0 Item { property int someNumber: 100 }
使用QDeclarativeProperty, 或QObject::setProperty() 和QObject::property()可以设置和读取someNumber属性:
-
QDeclarativeEngine engine;
-
QDeclarativeComponent component(&engine, "MyItem.qml");
-
QObject *object = component.create();
-
qDebug() << "Property value:" << QDeclarativeProperty::read(object, "someNumber").toInt();
-
QDeclarativeProperty::write(object, "someNumber", 5000);
-
qDebug() << "Property value:" << object->property("someNumber").toInt();
-
object->setProperty("someNumber", 100);
你应该总使用QObject::setProperty(),QDeclarativeProperty 或QMetaProperty::write()修改QML属性值,使QML引擎知道属性已经被修改.例如,假设有一个自定义的元素PushButton,带有一个buttonText属性,反映内部的m_buttonText成员变量值.直接修改成员变量值是不明智的:
-
// BAD!
-
QDeclarativeComponent component(engine, "MyButton.qml");
-
PushButton *button = qobject_cast<PushButton*>(component.create());
-
button->m_buttonText = "Click me";
由于直接修改了成员变量的值,越过了Qt的元对象系统,QML引擎就无法知道值被修改过.这样绑定到buttonText的属性就不会更新,任何onButtonTextChanged处理函数都不会被调用.
任何使用Q_PROPERTY宏声明的Qt属性都可在QML中访问.下面修改本文档前面例子,ApplicationData类具有一个backgroundColor属性.这个属性可在QML中进行读写:
// MyItem.qml import QtQuick 1.0 Rectangle { width: 100; height: 100 color: applicationData.backgroundColor MouseArea { anchors.fill: parent onClicked: applicationData.backgroundColor = "red" } }
| |
注意backgroundColorChanged被标记为backgroundColor属性的NOTIFY信号.如果Qt属性没有相关的NOTIFY信号,属性就不能用于QML的属性绑定,因为当属性值被修改时QML引擎不会得到通知.如果在QML中使用自定义类型,确保属性具有NOTIFY信号,以便于用于属性绑定中.
在QML中使用QML属性的更多信息见Tutorial: Writing QML extensions with C++ .
支持的数据类型
用于QML中的任何C++数据--自定义属性,或信号和函数的参数,QML都必须支持其类型.
默认QML支持如下数据类型:
- bool
- unsigned int, int
- float, double, qreal
- QString
- QUrl
- QColor
- QDate, QTime, QDateTime
- QPoint, QPointF
- QSize, QSizeF
- QRect, QRectF
- QVariant
- QVariantList,QVariantMap
- QObject*
- 由Q_ENUMS()声明的枚举类型
为了可以在QML创建和使用自定义C++类型,C++类必须使用qmlRegisterType()注册为QML类型,请见上面的Defining new QML elements 小节.
JavaScript数组和对象
QML内建支持在QVariantList和JavaScript数组之间,QVariantMap和JavaScript对象间的转换.
例如,如下定义在QML中的函数需要两个参数,一个数组一个对象,使用标准的JavaScript语法访问数组和对象输出其中的内容.C++代码调用了这个函数,传递QVariantList 和QVariantMap参数,将自动转换为JavaScript的数组和对象:
// MyItem.qml Item { function readValues(anArray, anObject) { for (var i=0; i<anArray.length; i++) console.log("Array item:", anArray[i]) for (var prop in anObject) { console.log("Object item:", prop, "=", anObject[prop]) } } }
|
|
This produces output like:
-
Array item: 10
-
Array item: #00ff00
-
Array item: bottles
-
Object item: language = QML
-
Object item: released = Tue Sep 21 2010 00:00:00 GMT+1000 (EST)
同样,如果C++定义了QVariantList 或QVariantMap 类型的属性或函数参数,在QML访问时,可创建JavaScript的数组或对象,并自动被转换为QVariantList 或QVariantMap 传递给C++.
使用自定义枚举类型
要在自定义C++组件中使用枚举,枚举类型必须使用Q_ENUMS宏注册到Qt的元对象系统.例如,如下C++类型具有一个Status枚举类型:
-
class ImageViewer : public QDeclarativeItem
-
{
-
Q_OBJECT
-
Q_ENUMS(Status)
-
Q_PROPERTY(Status status READ status NOTIFY statusChanged)
-
public:
-
enum Status {
-
Ready,
-
Loading,
-
Error
-
};
-
Status status() const;
-
signals:
-
void statusChanged();
-
};
假设ImageViewer类已经使用qmlRegisterType()进行注册,现在其Status枚举可用在QML中:
ImageViewer { onStatusChanged: { if (status == ImageViewer.Ready) console.log("Image viewer is ready!") } }
要使用内置的枚举,C++类必须注册到QML中.如果C++类型不可实例化,可使用qmlRegisterUncreatableType()注册.在QML中枚举值其首字母必须大写.
更多信息见Writing QML extensions with C++ 和Extending QML Functionalities using C++.
枚举值作为信号参数
C++信号可以向QML中传递一个枚举类型参数,假设枚举和信号定义在同一个类中,或枚举值定义在Qt命名空间(Qt Namespace)中.
此外,如果C++信号带有一个枚举参数,应该使用connect()函数与QML中的函数相关联,枚举类型必须使用qRegisterMetaType()注册.
对于QML信号,作为信号参数的枚举值使用int类型替代:
ImageViewer { signal someOtherSignal(int statusValue) Component.onCompleted: { someOtherSignal(ImageViewer.Loading) } }
从字符串做自动类型转换
为了方便,在QML中一些基本类型的值可使用格式化字符串指定,便于在QML中向C++传递简单的值.
Type | String format | Example |
---|---|---|
QColor | 颜色名称, "#RRGGBB", "#RRGGBBAA" | "red", "#ff0000", "#ff000000" |
QDate | "YYYY-MM-DD" | "2010-05-31" |
QPoint | "x,y" | "10,20" |
QRect | "x,y,宽x高" | "50,50,100x100" |
QSize | "宽x高" | "100x200" |
QTime | "hh:mm:ss" | "14:22:55" |
QUrl | URL字符串 | "http://www.example.com" |
QVector3D | "x,y,z" | "0,1,0" |
枚举值 | 枚举值名称 | "AlignRight" |
(更多字符串格式和类型见basic type documentation.)
这些字符串格式用于设置QML属性值和向C++函数传递参数.本文档中有很多范例进行演示;在上面的范例中,ApplicationData类有一个QColor类型的backgroundColor属性,在QML中使用字符串"red"而不是一个QColor对象进行赋值.
如果喜欢使用显式类型赋值,Qt对象提供了便利的全局函数来创建对象的值.例如Qt.rgba()创建一个基于RGBA的QColor值.这个函数返回的QColor类型的值可用于设置QColor类型的属性,或调用需要QColor类型参数的C++函数.
创建QML插件
Qt Declarative模块包含一个QDeclarativeExtensionPlugin类,这个抽象类用于创建QML插件.可在QML应用程序中动态加载QML扩展类型.
更多信息见QDeclarativeExtensionPlugin 文档和How to Create Qt Plugins .
使用Qt资源系统管理资源文件
Qt resource system 可将资源文件存储在二进制可执行文件中.这对创建QML/C++联合的应用程序很有帮助,可通过资源系统的URI(像其他图片和声音资源文件一样)调度访问QML文件,而不是使用相对或绝对文件系统路径.注意如果使用资源系统,当QML资源文件被修改后必须重新编译可执行应用程序,以便于更新包中的资源.
要在QML/C++应用程序中使用资源系统:
- 创建一个.qrc资源集合文件,以XML格式例举资源文件
- 在C++中,使用:/prefix或qrc调度URL加载主QML文件资源
这样做后,QML中所有已相对路径指定的文件都从资源文件中加载.使用资源系统完全对QML层透明;即QML代码可以用相对路径来访问资源文件,而不带有qrc调度.这个调度(qrc)只用于在C++中引用资源文件.
这是使用Qt资源系统的应用程序包.目录结构如下:
-
project
-
|- example.qrc
-
|- main.qml
-
|- images
-
|- background.png
-
|- main.cpp
-
|- project.pro
main.qml 和 background.png 文件作为资源文件打包.这在example.qrc资源文件中指定:
-
<!DOCTYPE RCC>
-
<RCC version="1.0">
-
<qresource prefix="/">
-
<file>main.qml</file>
-
<file>images/background.png</file>
-
</qresource>
-
</RCC>
由于background.png 是一个资源文件,main.qml可在example.qrc中使用的相当路径引用它:
// main.qml import QtQuick 1.0 Image { source: "images/background.png" }
要使QML文件正确的定位资源文件,main.cpp加载主QML文件--main.qml,访问资源文件需要使用qrc调度(scheme):
-
int main(int argc, char *argv[])
-
{
-
QApplication app(argc, argv);
-
QDeclarativeView view;
-
view.setSource(QUrl("qrc:/main.qml"));
-
view.show();
-
return app.exec();
-
}
最后在project.pro中将RESOURCES 变量设置为用来构建应用程序资源的example.qrc 文件:
-
QT += declarative
-
SOURCES += main.cpp