(本文测试环境为 Qt5.15.2 64bit + MSVC2019 + QtCreator9.0.2)
制作插件(或者说动态库)是很常见的需求,QtQuick 也提供了这一功能,插件的源码既可以是 QML 的,也可以是 C++ 的。QML 一般可以封装一些组件的 Plugin,类似于 QtQuick.Controls。
1.做一个最简单的插件
(建议先看官方示例:plugin qml 和 c++ extensions qml with writing)
1.1.创建 Qt Quick 2 Extension Plugin 项目
在QtCreator中点新建项目,在对话框中选择 Qt Quick 2 Extension Plugin,就可以创建一个简单的插件工程。
除了工程名和 pro 中的 uri 名,一路默认就行了,主要先熟悉整个流程(uri 由 com.mycompany.qmlcomponents 改为和 module 模块名一致,也可以和我一样模块名( module )和 dll 名( plugin )也写一样)。可以看到,它默认生成了一个 lib 项目(TEMPLATE = lib),里面还包含一个 qmldir 模块定义文件,打包的时候是要和插件 dll 放一起的。此外,工程默认生成了一个 QQuickItem 的派生类,并在 xxx_plugin.cpp 文件里注册为了 QML 类型, QQuickItem 对应QML 中的 Item 类型,对于非可视类型,我们继承 QObject 就行了。
1.2.修改 MyItem
既然工程默认生成了 MyItem ,那我直接用这个类来自定义。作为演示,我定义了一个属性和一个函数。
//myitem.h
#pragma once
#include <QQuickItem>
class MyItem : public QQuickItem
{
Q_OBJECT
QML_ELEMENT
Q_DISABLE_COPY(MyItem)
// 自定义属性可以在 QML 中直接访问
Q_PROPERTY(QString name READ getName WRITE setName NOTIFY nameChanged)
public:
explicit MyItem(QQuickItem *parent = nullptr);
~MyItem() override;
QString getName() const;
void setName(const QString &name);
// Q_INVOKABLE 标记的函数或者信号槽可以在 QML 中直接访问
Q_INVOKABLE int getStringLength(const QString &str);
signals:
void nameChanged();
private:
QString _name;
};
//myitem.cpp
#include "myitem.h"
MyItem::MyItem(QQuickItem *parent):
QQuickItem(parent)
{
// By default, QQuickItem does not draw anything. If you subclass
// QQuickItem to create a visual item, you will need to uncomment the
// following line and re-implement updatePaintNode()
// setFlag(ItemHasContents, true);
}
MyItem::~MyItem()
{
}
QString MyItem::getName() const
{
return _name;
}
void MyItem::setName(const QString &name)
{
if (_name != name) {
_name = name;
emit nameChanged();
}
}
int MyItem::getStringLength(const QString &str)
{
return str.length();
}
Qt5.15 之前的旧的注册为 QML 组件的方式是继承 QObject 及其子类,使用 qmlRegisterType 等系列函数手动注册:
qmlRegisterType<MyItem>(uri, 1, 0, "MyItem"); //uri也就是import的插件名称
Qt5.15 推出了新的方式,在 pro 中写上对应配置,然后直接在组件类中加一句 QML_ELEMENT 宏:
# 自动生成 plugins.qmltypes
CONFIG += qmltypes
# QML_IMPORT_NAME 同uri表示import的插件名称,如TestQmlPlugin
QML_IMPORT_NAME = TestQmlPlugin
QML_IMPORT_MAJOR_VERSION = 1
class MyItem: public QQuickPaintedItem
{
Q_OBJECT
QML_ELEMENT
//...
}
修改完后点击构建进行编译,会生成 dll,这里先手动创建一个 dll 模块同名的文件夹,然后把 dll 以及 qmldir 文件放进去,并把文件夹放到安装目录的 qml 文件夹下。(相当于 install 到了安装目录的 qml 文件夹下,只是为了测试方便,实际应用时避免破坏环境)
除了把文件夹放到运行环境 qml 目录里,也可以使用 QQmlApplicationEngine 的 addImportPath 指定文件夹所在的目录(qml 文件夹本身也在 importPath 列表中),如:
如结构为:
Project/src/src.pro
Project/bin/app.exe
Project/bin/qml/TestQmlPlugin/TestQmlPlugin.dll
main函数中:
QQmlEngine engine;
engine.addImportPath(QString("%1/qml").arg(QCoreApplication::applicationDirPath()));
非安装目录 pro 也加上 QML_IMPORT_PATH,以便 QtCreator 获取类型信息:
QML_IMPORT_PATH += $$PWD/../bin/qml
其中,我们的模块文件夹在路径的下一级。
1.3.使用插件
新建一个 QtQuick 项目,测试刚才生成的插件:
import QtQuick 2.12
import QtQuick.Window 2.12
import TestQmlPlugin 1.0
Window {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
MyItem{
name: "test"
Component.onCompleted: {
console.log(name)
let length=getStringLength("asdaa")
console.log(length)
}
}
}
如果操作正确,可以正常运行:
1.4.提供类型信息
虽然现在能调用了,但是 QtCreator 还没检测到我们的类型信息,所以会出一些红色下划线的提示。如果同时打开的 Plugin 工程和 App 工程,可能 QtCreator 会正常提示,这时候重启下 QtCreator 只打开 App 工程。
如果是 Qt5.15 以后的新版本,可以重新打开 Plugin 工程,pro 加上设置:
CONFIG += qmltypes #自动生成 plugins.qmltypes
QML_IMPORT_NAME = TestQmlPlugin
QML_IMPORT_MAJOR_VERSION = 1
重新构建后输出目录会多一个 qmltypes 文件,里面会有 C++ 注册类的接口信息,把他放到我们刚才的 dll 和 qmldir 同一个目录下。
重启 QtCreator 清下缓存,重新开打 App 工程,可以发现红色下划线已经没了。
老版本可以用 Qt 自带的 qmlplugindump 工具生成 qmltypes 文件,先在 qmldir 末尾中加一句:
typeinfo plugins.qmltypes
然后使用 qmlplugindump 命令:
格式:qmlplugindump -nonrelocatable 插件名称 插件版本 插件文件夹所在路径 > 插件qmltypes生成路径,比如:
qmlplugindump -nonrelocatable TestQmlPlugin 1.0 D:\git_space\TestQmlPlugin\bin > D:\git_space\TestQmlPlugin\bin\TestQmlPlugin\plugins.qmltypes
或者多级的插件名:
qmlplugindump -nonrelocatable Gt.Component 1.0 D:\git_space\QmlExtensionPlugin\bin > D:\git_space\QmlExtensionPlugin\bin\Gt\Component\plugins.qmltypes
如果报错:QWidget: Cannot create a QWidget without QApplication,加上 -qapp 这个命令行参数
至此,一个简单的 QML 插件就横空出世了。
1.5.总结
第一点:我没发现 Qt 有提供编译后执行 install 安装步骤的 CONFIG,一般是使用 copy 命令或者 make 参数加上 install(示例有写 CONFIG += install_ok,但是没发现有任何用;我把生成的插件放到 Qt 安装目录下是方便 QtCreator 检测到,以便有编码提示,手动设置的路径有时候会抽风);
第二点:qmldir 中的 module 就是 QML 中 import 那个模块名,plugin 就是 dll 的文件名,如果 module 使用小数点隔开,那么对应多级文件夹目录结构;
第三点:之前以为 qmltypes 文件没用,其实是我同时打开了 plugin 工程和 app 工程,Qt Creator 正好匹配到了,不然是检测不到类型信息的;
第四点:如果插件中封装了 qml 文件写的组件,如果想在使用的时候 F2 跳转到源码,把 qml 文件也放到组件文件夹里,同时 qmldir 进行声明;
第五点:Qt 的 plugin 也就是个动态库,所以 qrc 资源在一个进程里是共享的,注意路径不要重复;
第六点:如果注册了 qml 文件封装的类型,qmlplugindump 也是能提取出接口信息的,qml 可以放到 qrc 文件或者相对于指令执行的路径:
qmlRegisterType(QUrl::fromLocalFile("./Gt/Component2/QmlTest.qml"), uri, 1, 0, "QmlTest")
2.将组件封装为插件
在 QQmlExtensionPlugin 派生类中,通过重写 registerTypes 方法,可以注册我们自己的组件,使用 qmlRegisterType 等函数可以将 C++ 类型注册给 QML(和我们平时注册C++类型是一样的操作),也可以使用 Q_INIT_RESOURCE 宏包含 .qrc 文件,文件里放有我们的 QML 代码,达到封装 QML 组件的目录,Qt5.15 之后则可以使用 QML_ELEMENT 宏来注册。
我做了一个简单的示例,包括新旧两种方式,github链接如下:
https://github.com/gongjianbo/QmlExtensionPlugin
(如果你运行这个 Demo 出现问题可以留言)
3.参考
官方文档:Creating C++ Plugins for QML | Qt QML 5.15.6