因为 QML 自带的控件不算多,所以不得不写一些基础控件,将这些控件放在一个动态库中也方便其他程序使用。
在学习开发控件库的时候遇到一些问题,网上的好多文章都写的不清不楚,很多细节也忽略了,自己折腾了一天总算成功了。
本文内容在 Qt 5.7.1 版下而写,可能在更新的版本中有些类名或方法发生了变化。
import 时会发生什么?
在 QML 中,导入一个模块的时候程序会从哪里去找?
1 | import com.mycompany.Controls 1.0 |
我用 Process Monitor 工具监控发现程序会从根目录开始搜索:
1 | ./com/mycompany/Controls.1.0/qmldir |
按照.
分层,从最后一层开始,加上版本号向上找。找不到就换下一个目录(QQmlEngine::importPathList 是模块目录列表)。
所有目录都找不到的话,则将小版本号丢弃,仅从大版本号再从开搜一遍:
1 | ./com/mycompany/Controls.1/qmldir |
还找不到就干脆不带版本号找
1 | ./com/mycompany/Controls/qmldir |
最后找不到就报错退出程序。
1 | qrc:/main.qml:5 module "MyControls" is not installed |
所以路径中有版本号的话优先级更高。
默认情况下,QQmlEngine::importPathList 返回3个目录:
1 | "C:/demo/debug" |
第一个是应用所在目录。第二个是 Qt 自己的资源目录,但是什么都没有。第三个是 Qt 安装目录。
可以用 QQmlEngine::addImportPath 方法添加模块目录,添加后会插入到第一位。
qmldir 文件
当模块目录存在时,Qt 会尝试读取模块目录下的qmldir
文件,这个文件的作用就是告诉 Qt 如何找到相关资源,相当于一个索引文件,比如一个最小的qmldir
:
1 | module com.mycompany.controls |
module
指定了模块的完整路径,也是import
的路径。
在我的例子中,整个目录结构就是这样的:
1 | app.exe |
qmldir
还有一些其他属性,可以查看官方手册了解:Module Definition qmldir Files
封装模块
实际中,我们不太可能会直接将.qml
文件暴露给用户,还有另一种可能是控件由 C++ 绘制,没有.qml
文件,这种时候更好的选择的是将控件封装到一个动态库中。
新建一个动态库项目,从 QQmlExtensionPlugin 继承并覆盖其纯虚方法。
主要就是在 QQmlExtensionPlugin::registerTypes 方法内注册所有控件。
1 | void MyPlugin::registerTypes(const char *uri) { |
当我们以插件形式提供 QML 模块时,qmldir
文件内容就要修改下:
1 | module com.mycompany.controls |
MyPlugin
就是不带后缀的动态库文件名,完整目录结构:
1 | app.exe |
Qt 在找到模块目录后会根据qmldir
文件内容,加载.dll
插件,并调用registerTypes
方法,这样就成功注册了控件等资源。
另外有个小细节,debug 版本程序会优先加载d
后缀的动态库文件。
比如qmldir
文件中写的是plugin MyPlugin
,对于 debug 程序来说,会先尝试加载MyPlugind.dll
,找不到才会加载MyPlugin.dll
。
release 版程序则不会。
生成元数据
事情还没完,QtCreator 中对于自定义控件全是红色警告:
因为默认情况下,IDE 并不知道这些自定义控件有哪些属性、方法。这就需要我们提供一个.qmltypes
元数据文件来告诉 IDE,让其有智能提示和静态分析的能力。
Qt 目录下有个qmlplugindump.exe
工具,将模块搜索路径传递给它,并给定一个模块名称就能生成一个.qmltypes
文件。
1 | qmlplugindump -nonrelocatable MyControls 1.0 . > plugin.qmltypes |
这表示在当前目录下搜索MyControls
模块的1.0
版本,并生成元数据到plugin.qmltypes
文件中。
然后在qmldir
文件中加入一行:
1 | typeinfo plugin.qmltypes |
这是标准做法,但是我发现 QtCreator 会自动查找目录下.qmltypes
后缀的文件作为元数据,并不需要使用typeinfo
去标注。
另外要注意的是,Debug 版的程序不能用来生成元数据。
最后,对于 cmake 项目,要在CMakeLists.txt
中设置QML_IMPORT_PATH
环境变量,告诉 IDE 去哪里查找模块元数据。
1 | set(QML_IMPORT_PATH C:/app/bin/debug/qml/ CACHE STRING "" FORCE) |
总结
- 开发 QML 插件,需要一个继承自 QQmlExtensionPlugin 的类,并在其中注册所有的模块。
import
后面的名称就是模块的名称。- 模块路径可以用 QQmlEngine::addImportPath 添加。
- 模块目录必须包含
qmldir
文件,文件内至少提供module
属性,对于动态库插件,还需要提供plugin
属性,用于指明动态库文件名(不含后缀)。 - 对于纯 C++ 实现的 QML 控件,可以不用元数据文件
.qmltypes
,IDE 会有代码提示,但是对于纯 QML 控件则需要用qmlplugindump.exe
来生成元数据文件,否则 IDE 会发出警告。 - 元数据文件只是告诉 IDE 相关信息,提供代码提示等功能,在程序运行时是不需要的。
qmldir
中的typeinfo
属性可以省略,Qt 会自动查找.qmltypes
后缀的文件作为元数据。
相关阅读
Qt5官方demo解析集20——Chapter 6: Writing an Extension Plugin
Qt/QML 插件系统
QML Modules
Module Definition qmldir Files
Writing QML Extensions with C++