QML 只能运行在一个受限环境中,这是由于 QML 语言本身有一些限制。为了解决这一问题,我们可以使用 C++ 编写一些功能,供 QML 运行时调用。
为了能够利用 C++ 扩展 QML,首先我们需要理解 QML 的运行机制。
与 C++ 不同,QML 运行在自己的运行时环境中。这个运行时在 QtQml 模块,由 C++ 实现,包含一个负责执行 QML 的引擎,为每个组件保存可访问属性的上下文,以及实例化的 QML 元素组件。
在 Qt Creator 中,我们创建 Qt Quick Application 项目,打开 Qt Creator 自动帮我们生成的 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(); }
在这段代码中,QGuiApplication
封装了有关应用程序实例的相关信息(比如程序名字、命令行参数等)。QQmlApplicationEngine
管理带有层次结构的上下文和组件。QQmlApplicationEngine
需要一个 QML 文件,将其加载作为应用程序的入口点。在这个例子中,这个文件就是 main.qml。Qt Creator 帮我们生成的 QML 文件被作为资源文件,因此需要使用“qrc”前缀访问到。这个 QML 文件内容如下:
import QtQuick 2.3 import QtQuick.Window 2.2 Window { visible: true MouseArea { anchors.fill: parent onClicked: { Qt.quit(); } } Text { text: qsTr("Hello World") anchors.centerIn: parent } }
这个 QML 文件根元素是Window
项目,包含了一个MouseArea
和Text
。对于根元素是Item
的 QML 文件,使用QmlApplicationEngine
加载并不会显示任何内容,甚至连窗口都不会显示。这是由于,单独一个Item
不能作为独立的窗口,因而没有渲染的容器。QmlApplicationEngine
可以加载不带有任何界面的 QML 文件(比如纯逻辑代码),正因为如此,它才不会为你建立一个默认的窗口(这一点与 Qt/C++ 有些不同,Qt/C++ 会为一个独立的QLabel
创建默认窗口,不过这一点也有些争议,因此QLabel
其实也是一个界面元素)。 另一方面,如果我们直接使用 IDE 运行根元素是Item
的 QML 文件,会开启一个 qmlscene 或者新的 QML 运行时,这个新的进程会首先检查主 QML 文件是不是包含窗口作为其根元素,如果没有,则会为其创建一个默认窗口,如果已经有了则会直接设置根元素。这是使用QmlApplicationEngine
代码运行和利用 IDE 的 qmlscene 运行二者的区别。
在这个 QML 文件中,我们引入了两个声明:QtQuick
和QtQuick.Window
。这些声明会触发一个在引入路径上面的查找模块的动作,如果成功,则将所需插件加载到 QML 引擎。这些类型的加载实际是由配置文件 qmldir 管理的。除了这种常规方式,我们也可以使用 C++ 代码直接将插件交给 QML 引擎加载。例如,如果我们有一个父类为QObject
的CurrentTime
类,就可以使用 C++ 这样加载:
QQmlApplicationEngine engine; qmlRegisterType<CurrentTime>("org.example", 1, 0, "CurrentTime"); engine.load(source);
现在,我们就可以直接在 QML 文件中使用CurrentTime
类型了:
import org.example 1.0 CurrentTime { // }
如果你还想更懒一些,那么也可以直接使用上下文属性加载类型:
QScopedPointer<CurrentTime> current(new CurrentTime()); QQmlApplicationEngine engine; engine.rootContext().setContextProperty("current", current.value()) engine.load(source);
注意,不要将setContextProperty()
和setProperty()
两个函数混淆:前者用于 QML 上下文设置上下文属性,后者则是通常意义上的给QObject
添加动态属性。但是这里是不适用动态属性的。
多亏了上下文的层次结构,我们添加到根上下文中的属性可以在整个应用中使用:
import QtQuick 2.4 import QtQuick.Window 2.0 Window { visible: true width: 512 height: 300 Component.onCompleted: { console.log('current: ' + current) } }
看!现在你连import
语句和对象的声明都省掉了!
扩展 QML 一般有三种方式:
- 上下文属性:
setContextProperty()
- 向引擎注册类型:在 main.cpp 调用
qmlRegisterType
- QML 扩展插件
对于小型应用,上下文属性即可满足需要。上下文属性不会造成很大的影响,你只是将自己的系统 API 暴露成全局对象。因此,需要注意的就是,确保系统中不会有名字冲突(例如,像 jQuery 一样,使用特殊符号$
指代this
,就像$.currentTime
)。$
的确是一个合法的 JS 变量。
注册 QML 类型允许用户使用 QML 控制 C++ 对象的生命周期。使用上下文属性是不能达到这一目的的。另外,这种实现也不会污染全局命名空间。但是,所有类型在使用前都必须注册;因此,所有的库都必须在应用程序启动时链接。不过,在大多数情况下,这都不是个问题。
QML 扩展插件是最灵活的方式。当 QML 文件第一次调用import
语句时,插件才会被加载,而类型注册则发生在插件中。另外,通过使用 QML 单例,也无需污染全局命名空间。插件允许跨项目复用模块,这对于使用 QML 开发的多个项目极其有用。
在下面的文章中,我们主要关注 QML 扩展插件,因为它们提供了最大的灵活性和可复用性。
插件是一种满足预定义接口的库,在需要时由系统进行加载。这是插件与普通库的区别之一:普通库在应用启动时就会被链接、加载。在 QML 中,插件必须满足的接口是QQmlExtensionPlugin
。我们主要关心接口中的两个函数:initializeEngine()
和registerTypes()
。当插件被系统加载时,首先会调用initializeEngine()
;该函数允许我们访问 QML 引擎,以便将插件对象暴露给根上下文。大多数情况我们只需要使用registerTypes()
函数;该函数允许我们提供一个 URL,以便向 QML 引擎注册自定义 QML 类型。
在前面的章节,我们提到,QML 缺少直接读取本地文件的功能。下面,我们就利用插件实现一个简单的读写文本文件的 QML 插件。首先,我们先定义一个 QML 插件的占位符:
// FileIO.qml (看起来不错) QtObject { function write(path, text) {}; function read(path) { return "TEXT"} }
这是一个基于 C++ QML API 的纯 QML 实现,可以对外暴露其 API。可以看到,我们需要两个接口的实现:read
和write
。write
函数接受两个参数:写入路径path
和写入内容text
;read
函数接受一个参数:读取路径path
,返回读取到的文本。由于path
和text
都是通用参数,那么,我们就可以将其提升为属性:
// FileIO.qml (更好一些) QtObject { property url source property string text function write() { // 打开文件,写入 text }; function read() { // 读取文件,赋值给 text }; }
没错,这下更像 QML API 了。使用属性,我们就可以允许数据绑定和监听属性值的改变。
下一步,我们需要使用 C++ 创建这个 API:
class FileIO : public QObject { ... Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY sourceChanged) Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged) ... public: Q_INVOKABLE void read(); Q_INVOKABLE void write(); ... }
FileIO
类需要注册到 QML 引擎。我们想将其注册到“org.example.io”模块,也就是可以这么使用:
import org.example.io 1.0 FileIO { }
一个插件可以在一个模块暴露多种类型,但是不允许在一个插件暴露多个模块。因此,模块与插件是一种一对一的关系,而这种关系就是由模块标识符表示的。
最新版的 Qt Creator 提供了一个创建 QtQuick 2 QML Extension Plugin 的向导。利用它,我们可以创建一个名为 fileio 的插件。这个插件包含一个叫作FileIO
的对象,该对象位于模块“org.example.io”。
插件类继承QQmlExtensionPlugin
,实现了registerTypes()
函数。Q_PLUGIN_METADATA
一行强制将该插件识别为一个 QML 扩展插件。除此以外,这段代码并没有什么特别之处。
#ifndef FILEIO_PLUGIN_H #define FILEIO_PLUGIN_H #include <QQmlExtensionPlugin> class FileioPlugin : public QQmlExtensionPlugin { Q_OBJECT Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface") public: void registerTypes(const char *uri); }; #endif // FILEIO_PLUGIN_H
registerTypes()
函数实现很简单,我们使用qmlRegisterType()
函数注册了FileIO
类。
#include "fileio_plugin.h" #include "fileio.h" #include <qqml.h> void FileioPlugin::registerTypes(const char *uri) { // @uri org.example.io qmlRegisterType<FileIO>(uri, 1, 0, "FileIO"); }
到目前为止,我们还没有见到模块 URI,也就是前面提到的“org.example.io”标识符。事实上,这个标识符是在外部文件定义的。我们检查项目目录,可以找到一个 qmldir 文件。这个文件指定了 QML 插件的内容以及插件的 QML 方面的描述。该文件内容大致如下:
module org.example.io plugin fileio
第一行指定了别人在使用你的插件时,需要使用哪一个 URI;第二行则必须与你的插件的文件名一致(Mac 系统中,这个插件的文件名可能是 libfileio_debug.dylib,而在 qmldir 文件中,我们需要填写 fileio)。事实上,这些文件都是由 Qt Creator 基于我们给出的信息自动生成的。模块的 URI 也可以在 .pro 文件中使用,用于构建安装目录。
当在构建文件夹中执行make install
命令时,生成的插件文件将会被复制到 Qt 的 qml 文件夹(Mac 系统中路径可能是 ~/Qt5.5.1/5.5/clang_64/qml,Windows 中可能是 C:\Qt5.5.1\5.5\msvc2013\qml),这个路径实际取决于 Qt 的安装位置和构建时使用的编译器。安装完毕之后,该文件夹下会有“org/example/io”文件夹,其中有两个文件(以 Mac 系统为例):
- libfileio_debug.dylib
- qmldir
当需要导入名为“org.example.io”的模块时,QML 引擎会在预定义的导入路径中进行查找,直到找到一个包含有 qmldir 文件的“org/example/io”文件夹。这个 qmldir 文件告诉 QML 引擎,使用哪个 URI 加载哪个模块作为 QML 扩展插件。如果两个模块有相同的 URI,它们就会相互覆盖。
6 评论
能不能提供一个 FileIO扩展插件 完整的源代码(工程文件)
后面还有实现部分的
楼主想问一下另一个qml的问题,在qt ide里面有一个qmlscene.exe的工具,可以直接运行qml文件,我想问一下用这个工具运行的qml程序性能跟直接编译好后运行的性能是不是有差别? 是不是用qmlscene.exe运行的qml程序性能会差点?
有可能会有性能方面的原因。虽然文档明确说明,qmlscene 不适合用在生产环境,但是也一直没有找到二者之间的区别。不过为谨慎起见,还是按照文档说明的比较合适。
并不是作者原作,是翻译qmlbook的,可以去找这本书的源码
竟然看完了!从决定用qt做一个简单的界面开始就找资料了,然而什么头绪也没有,浪费了两天,幸好遇到豆子哥的文!赞一个。