制作 QML 扩展插件(Qt Quick 2 Extension Plugin)

(本文测试环境为 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

博客:QML插件扩展(一) - 程序创造世界 - 博客园

博客:QML插件扩展2(基于C++的插件扩展) - 程序创造世界 - 博客园

博客:让Qt Creator更懂我们的自定义模块 - 知乎 (zhihu.com) 

  • 11
    点赞
  • 61
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 11
    评论
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龚建波

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值