学习制作 QML 模块
2024-12-06 08:40:53

因为 QML 自带的控件不算多,所以不得不写一些基础控件,将这些控件放在一个动态库中也方便其他程序使用。
在学习开发控件库的时候遇到一些问题,网上的好多文章都写的不清不楚,很多细节也忽略了,自己折腾了一天总算成功了。


本文内容在 Qt 5.7.1 版下而写,可能在更新的版本中有些类名或方法发生了变化。

import 时会发生什么?

在 QML 中,导入一个模块的时候程序会从哪里去找?

1
import com.mycompany.Controls 1.0

我用 Process Monitor 工具监控发现程序会从根目录开始搜索:

1
2
3
./com/mycompany/Controls.1.0/qmldir
./com/mycompany.1.0/Controls/qmldir
./com.1.0/mycompany/Controls/qmldir

按照.分层,从最后一层开始,加上版本号向上找。找不到就换下一个目录(QQmlEngine::importPathList 是模块目录列表)。
所有目录都找不到的话,则将小版本号丢弃,仅从大版本号再从开搜一遍:

1
2
3
./com/mycompany/Controls.1/qmldir
./com/mycompany.1/Controls/qmldir
./com.1/mycompany/Controls/qmldir

还找不到就干脆不带版本号找

1
./com/mycompany/Controls/qmldir

最后找不到就报错退出程序。

1
qrc:/main.qml:5 module "MyControls" is not installed

所以路径中有版本号的话优先级更高。


默认情况下,QQmlEngine::importPathList 返回3个目录:

1
2
3
"C:/demo/debug"
"qrc:/qt-project.org/imports"
"C:/qt/qml"

第一个是应用所在目录。第二个是 Qt 自己的资源目录,但是什么都没有。第三个是 Qt 安装目录。
可以用 QQmlEngine::addImportPath 方法添加模块目录,添加后会插入到第一位。

qmldir 文件

当模块目录存在时,Qt 会尝试读取模块目录下的qmldir文件,这个文件的作用就是告诉 Qt 如何找到相关资源,相当于一个索引文件,比如一个最小的qmldir

1
2
module com.mycompany.controls
MyButton 1.0 MyButton.qml

module指定了模块的完整路径,也是import的路径。
在我的例子中,整个目录结构就是这样的:

1
2
3
4
5
6
app.exe
com/
└── mycompany/
└── controls/
├── qmldir
└── MyButton.qml

qmldir还有一些其他属性,可以查看官方手册了解:Module Definition qmldir Files

封装模块

实际中,我们不太可能会直接将.qml文件暴露给用户,还有另一种可能是控件由 C++ 绘制,没有.qml文件,这种时候更好的选择的是将控件封装到一个动态库中。
新建一个动态库项目,从 QQmlExtensionPlugin 继承并覆盖其纯虚方法。
主要就是在 QQmlExtensionPlugin::registerTypes 方法内注册所有控件。

1
2
3
4
5
6
7
8
9
10
11
12
void MyPlugin::registerTypes(const char *uri) {
// C++实现的控件
qmlRegisterType<FPSText>(uri, 1, 0, "MyRectangle");

// .qml 控件
qmlRegisterType(QUrl("qrc:/MyButton.qml"), uri, 1, 0, "MyButton");
}

void MyPlugin::initializeEngine(QQmlEngine *engine, const char *uri) {
Q_UNUSED(engine);
Q_UNUSED(uri);
}

当我们以插件形式提供 QML 模块时,qmldir文件内容就要修改下:

1
2
module com.mycompany.controls
plugin MyPlugin

MyPlugin就是不带后缀的动态库文件名,完整目录结构:

1
2
3
4
5
6
app.exe
com/
└── mycompany/
└── controls/
├── qmldir
└── MyPlugin.dll

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)

总结

  1. 开发 QML 插件,需要一个继承自 QQmlExtensionPlugin 的类,并在其中注册所有的模块。
  2. import后面的名称就是模块的名称。
  3. 模块路径可以用 QQmlEngine::addImportPath 添加。
  4. 模块目录必须包含qmldir文件,文件内至少提供module属性,对于动态库插件,还需要提供plugin属性,用于指明动态库文件名(不含后缀)。
  5. 对于纯 C++ 实现的 QML 控件,可以不用元数据文件.qmltypes,IDE 会有代码提示,但是对于纯 QML 控件则需要用qmlplugindump.exe来生成元数据文件,否则 IDE 会发出警告。
  6. 元数据文件只是告诉 IDE 相关信息,提供代码提示等功能,在程序运行时是不需要的。
  7. 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++

上一页
2024-12-06 08:40:53
下一页