希望制作个自己的自定义控件库,最好封装成dll,这样每次调用和使用方便,还可以不断的积累,生成一套自己喜欢的UI,所以到处找封装方法,虽然最后也没有成功,但是还是记录一下,方便后续查看,也希望帮到需要的人。
简要介绍一下 Qt 和 QML 的插件系统,并用几个简单的示例介绍 QML 的几种插件的创建方法。由于时间所限,有些地方可能讲述的不是很到位,欢迎沟通指正。
1. 插件概述
1.1. 什么是插件
插件(Plug-in,又称 addin、add-in、addon 或 add-on,又译外挂)是一种遵循一定规范的应用程序接口编写出来的程序。其只能运行在程序规定的系统平台下(可能同时支持多个平台),而不能脱离指定的系统单独运行。
1.2. 插件系统组成
主系统 —— 通过插件管理器加载插件,并创建插件对象。一旦插件对象被创建,主系统就会获得插件相应的指针或引用,它可以像任何其他对象一样使用。
插件管理器 —— 用于管理插件的生命周期,并将其暴露给主系统使用。它负责查找并加载插件,初始化它们,并且能够进行卸载。它还应该让主系统迭代加载的插件或注册的插件对象。
插件 —— 插件本身应符合插件管理器的协议,并提供符合主系统期望的对象。
1.3. 为什么要使用插件
为了将模块从框架中剥离出来,降低框架和功能间的耦合度,功能的实现作为模块单独开发,而不是功能实现相关的复杂代码与框架揉合在一起。
解决需求不断变化的软件设计场景。
面向未来,可以通过插件来扩展应用程序的功能(例如 vscode、qtcreator 的主流 IDE 的插件)。
2. 插件和动态库区别
2.1. 使用场景
动态库:解决静态库编译时链接符号表导致的程序占用空间大,库升级时相关可执行程序需要重新编译等问题。
插件:对于软件使用的不同场景,功能有所区别时,有选择定制和加载不同的插件,另外插件能降低模块和主功能间的耦合关系。
2.2. 生命周期
动态库:程序启动时加载,程序运行时必须保证 .dll/.so 存在,否则无法正常启动。
插件:程序运行时到需要的时候加载,程序运行时如果 .dll/.so 不存在,也可以正常启动,只是相应插件的功能无法正常加载和使用而已。
2.3. 耦合度
动态库:编译时必须指定动态库依赖关系。
插件:编译时主程序不知道插件的存在。
3. Qt 中插件的分类
3.1. 开源的纯 QML 插件(qmldir)
3.1.1. 创建不带 url 前缀的 QML 插件
创建目录 MyPlugins(本例中我们在完整目录 /home/LiuPC/TestQMLPlugin/ 下创建),此目录是自己定义的,名称也可以随意定义,但是这个目录名称会作为模块名称。
在 MyPlugins 目录中创建和功能相关的 qml 文件(MyRect.qml):
import QtQuick 2.12
import QtQuick.Controls 2.12
Item {
anchors.centerIn: parent
Rectangle{
width: 100
height: 100
color: "teal"
Label {
width: 50
height: 20
text: qsTr("TestRect")
}
}
}
在 qml 同级目录下创建一个名为 qmldir
的文件,并添加如下内容:
module MyExamplePlugins
TestRect 1.0 MyRect.qml
3.1.2. 创建带 url 前缀的 QML 插件
创建目录 NewPlugins (本例中我们在完整目录 /home/dongshuang/TestQMLPlugin/com/mycompany/test/ 下创建),此目录是自己定义的,名称也可以随意定义,但是这个目录名称会作为模块名称。
在 NewPlugins 目录中创建和功能相关的 qml 文件(NewRect.qml):
import QtQuick 2.12
import QtQuick.Controls 2.12
Item {
Rectangle{
width: 100
height: 100
color: "teal"
Label {
width: 50
height: 20
text: qsTr("NewRect")
}
}
}
在 qml 同级目录下创建一个名为 qmldir
的文件,并添加如下内容:
module NewExamplePlugins
NewRect 1.0 NewRect.qml
3.1.3. 使用 QML 插件
在 pro 文件中添加:
# 环境变量的设置只是为了让 ide 能够找到插件位置,进行高亮,自动补全等
QML_IMPORT_PATH += /home/dongshuang/TestQMLPlugin
在 main 函数中添加如下代码即可:
// 此处才是真正告诉程序去哪里加载插件
engine.addImportPath("/home/dongshuang/TestQMLPlugin");
在 main.qml 中的使用实例:
import QtQuick 2.12
import QtQuick.Window 2.12
import MyPlugins 1.0
import com.mycompany.test.NewPlugins 1.0
Window {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
TestRect {
}
NewRect {
}
}
以上插件实际上是将源码目录直接打包发布的过程。
3.2. 隐藏源码的 QML 插件
在实际开发中,我们更多的需要将源码封装打包,而不对外提供源码。那又应该如何处理呢?
此时就需要借助 QQmlExtensionPlugin 这个类,以及 Qt 的资源管理系统了。下面我们用一个简单的实例来讲解如何实现。
3.2.1. 创建插件工程
首先,我们使用 Qt Creator 的新建一工程向导,创建一个 “Library > Qt Quick 2 Extension Plugin”插件工程。工程名字,我们可以叫 qrcmoduleplugin
,Object class-name 可以随便填,因为后面我们要去掉它。 URI 的名称我们定为:com.mycompany.mymodule
3.2.2. 添加 qml 文件
之后我们在 qrcmoduleplugin.pro
文件所在的目录创建三个 qml 文件:
ButtonBase.qml 文件的内容如下:
import QtQuick 2.12
MouseArea {
property alias border: bgObj.border
property alias color: bgObj.color
property alias font: txtObj.font
property alias text: txtObj.text
property alias textAnchors: txtObj.anchors
implicitWidth: 100
implicitHeight: 100
objectName: "ButtonBase"
Rectangle {
id: bgObj
anchors.fill: parent
color: "honeydew"
}
Text {
id: txtObj
text: qsTr("ButtonBase")
}
}
ButtonQrc.qml 文件的内容如下:
import QtQuick 2.12
ButtonBase {
color: "lightcoral"
text: qsTr("ButtonQrc")
}
ButtonQrc2.qml 文件的内容如下:
import QtQuick 2.12
ButtonBase {
color: "slateblue"
text: qsTr("ButtonQrc2")
}
3.3.3. 创建资源文件
之后我们在 qrcmoduleplugin.pro
文件所在的目录创建一个名为 qrcmoduleplugin.qrc
的资源文件,其内容如下:
<RCC>
<qresource prefix="/component">
<file>ButtonQrc.qml</file>
<file>ButtonQrc2.qml</file>
<file>ButtonBase.qml</file>
</qresource>
</RCC>
3.2.4. 修改工程文件的内容
之后我们修改 qrcmoduleplugin.pro
文件,主要涉及到其中用数字标记的 5 处:
TEMPLATE = lib
TARGET = qrcmoduleplugin
QT += qml quick
CONFIG += plugin c++11
TARGET = $$qtLibraryTarget($$TARGET)
uri = com.mycompany.mymodule
# 1. 去掉其他的实现文件
SOURCES += \
qrcmoduleplugin_plugin.cpp
# 2. 去掉其他的头文件
HEADERS += \
qrcmoduleplugin_plugin.h
DISTFILES = qmldir
!equals(_PRO_FILE_PWD_, $$OUT_PWD) {
copy_qmldir.target = $$OUT_PWD/qmldir
copy_qmldir.depends = $$_PRO_FILE_PWD_/qmldir
copy_qmldir.commands = $(COPY_FILE) "$$replace(copy_qmldir.depends, /, $$QMAKE_DIR_SEP)" "$$replace(copy_qmldir.target, /, $$QMAKE_DIR_SEP)"
QMAKE_EXTRA_TARGETS += copy_qmldir
PRE_TARGETDEPS += $$copy_qmldir.target
}
qmldir.files = qmldir
# 3. 增加安装资源文件
qrc.files = qrcmoduleplugin.qrc
unix {
installPath = $$[QT_INSTALL_QML]/$$replace(uri, \., /)
qmldir.path = $$installPath
target.path = $$installPath
# 4.指定安装资源文件位置
qrc.path = $$installPath
INSTALLS += target qmldir qrc
}
# 5. 添加资源文件到工程
RESOURCES += \
qrcmoduleplugin.qrc
3.2.5. 修改插件类的实现
在这之后,我们修改 qrcmoduleplugin_plugin.cpp
文件,这里我们注册了两个 qml 文件,给外部使用,我们的 ButtonBase.qml
不会被暴露:
#include "qrcmoduleplugin_plugin.h"
#include <qqml.h>
void QrcmodulepluginPlugin::registerTypes(const char *uri)
{
// @uri com.mycompany.mymodule
qmlRegisterType(QUrl("qrc:/component/ButtonQrc.qml"), uri, 1, 0, "ButtonQrc");
qmlRegisterType(QUrl("qrc:/component/ButtonQrc2.qml"), uri, 2, 0, "ButtonQrc");
}
3.2.6. 拷贝插件资源到指定目录
之后,我们构建工程,完成之后,就可以在构建目录下生成一系列的文件,我们只要拷贝 libqrcmoduleplugin.so
和 qmldir
这两个文件到目录 /home/dongshuang/TestQMLPlugin/com/mycompany/mymodule
中即可,我们可以看到这个目录结构其实是和我们之前定义的 URI (我们的定义为:com.mycompany.mymodule
)有一定的关联的。而 /home/dongshuang/TestQMLPlugin/
这个目录是上节中我们用到的目录,没错,我们之后还会使用上节介绍的例子进行测试。
3.2.7. 生成 .qmltypes 文件
在命令行运行如下两条命令:
$ cd /home/dongshuang/TestQMLPlugin
$ qmlplugindump com.mycompany.mymodule 1.0 /home/dongshuang/TestQMLPlugin > /home/dongshuang/TestQMLPlugin/com/mycompany/mymodule/mymodule.qmltypes
注:如果 qmlplugindump 找不到,需要添加 Qt 的环境变量。
之后,我们就可以在 /home/dongshuang/TestQMLPlugin/com/mycompany/mymodule
目录中生成 mymodule.qmltypes
文件:
import QtQuick.tooling 1.2
// This file describes the plugin-supplied types contained in the library.
// It is used for QML tooling purposes only.
//
// This file was auto-generated by:
// 'qmlplugindump com.mycompany.mymodule 1.0 /home/dongshuang/TestQMLPlugin'
Module {
dependencies: ["QtQuick 2.12"]
Component {
prototype: "QQuickMouseArea"
name: "ButtonQrc 1.0"
exports: ["ButtonQrc 1.0"]
exportMetaObjectRevisions: [0]
isComposite: true
defaultProperty: "data"
Property { name: "border"; type: "QQuickPen"; isReadonly: true; isPointer: true }
Property { name: "color"; type: "QColor" }
Property { name: "font"; type: "QFont" }
Property { name: "text"; type: "string" }
Property { name: "textAnchors"; type: "QQuickAnchors"; isReadonly: true; isPointer: true }
}
Component {
prototype: "QQuickMouseArea"
name: "ButtonQrc 2.0"
exports: ["ButtonQrc 2.0"]
exportMetaObjectRevisions: [0]
isComposite: true
defaultProperty: "data"
Property { name: "border"; type: "QQuickPen"; isReadonly: true; isPointer: true }
Property { name: "color"; type: "QColor" }
Property { name: "font"; type: "QFont" }
Property { name: "text"; type: "string" }
Property { name: "textAnchors"; type: "QQuickAnchors"; isReadonly: true; isPointer: true }
}
}
3.2.8. 修改 qmldir 文件
之后我们修改 /home/dongshuang/TestQMLPlugin/com/mycompany/mymodule
目录中的 qmldir 文件为如下内容:
module com.mycompany.mymodule
plugin qrcmoduleplugin
typeinfo mymodule.qmltypes
3.2.9. 使用示例
之后,我们修改上节中的示例,将 main.qml 改成如下:
import QtQuick 2.12
import QtQuick.Window 2.12
import MyPlugins 1.0
import com.mycompany.mymodule 1.0
Window {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
TestRect {
}
ButtonQrc {
anchors.centerIn: parent
}
}
上面的代码我们可以再试下不改变别的内容,而只是把 import com.mycompany.mymodule 1.0
这一句中的 1.0 改成 2.0,再运行试试效果。这也是 QML 插件处理不同版本的插件的测试。
3.3. 包含 C++ 的 QML 插件
现实开发中我们的 QML 插件,可能需要 C++ 的功能的支持,因此包含 C++ 的 QML 插件也是需要我们学习和掌握的。有了上面的两节示例的基础,其实 C++ 的部分看起来就很简单了,这部分大家可以直接参考 QML Plugin Example 这个示例。
3.3.1. 创建 MinuteTimer 类
MinuteTimer 类主要作用是创建 QBasicTimer 对象,并启动 QBasicTimer 的 start 方法,之后监听 QBasicTimer 在 time out 之后发出的 timerEvent 事件来产生时间变化的信号,同时计算和更新当前的 hour 和 minute 值。
class MinuteTimer : public QObject
{
Q_OBJECT
public:
MinuteTimer(QObject *parent) : QObject(parent)
{
}
void start()
{
if (!timer.isActive()) {
time = QTime::currentTime();
timer.start(60000-time.second()*1000, this);
}
}
void stop()
{
timer.stop();
}
int hour() const { return time.hour(); }
int minute() const { return time.minute(); }
signals:
void timeChanged();
protected:
void timerEvent(QTimerEvent *) override
{
QTime now = QTime::currentTime();
if (now.second() == 59 && now.minute() == time.minute() && now.hour() == time.hour()) {
// just missed time tick over, force it, wait extra 0.5 seconds
time = time.addSecs(60);
timer.start(60500, this);
} else {
time = now;
timer.start(60000-time.second()*1000, this);
}
emit timeChanged();
}
private:
QTime time;
QBasicTimer timer;
};
上述代码使用了 QBasicTimer 这个类,该类是 Qt 在内部使用的一个快速、轻量级的低级类。如果希望在应用程序中使用计时器,我们建议使用更高级别的 QTimer 类而不是这个类。注意,这个计时器是一个重复计时器,除非调用 stop() 函数,否则它将发送后续计时器事件。
3.3.2. 创建 TimeModel 类
下面是创建用于暴露给 QML 使用的 TimeModel 类,它主要是对 MinuteTimer 类进行单利化的管理和封装,毕竟时间应该是一样的,对吧。其代码如下:
class TimeModel : public QObject
{
Q_OBJECT
Q_PROPERTY(int hour READ hour NOTIFY timeChanged)
Q_PROPERTY(int minute READ minute NOTIFY timeChanged)
public:
TimeModel(QObject *parent=nullptr) : QObject(parent)
{
if (++instances == 1) {
if (!timer)
timer = new MinuteTimer(QCoreApplication::instance());
connect(timer, &MinuteTimer::timeChanged, this, &TimeModel::timeChanged);
timer->start();
}
}
~TimeModel() override
{
if (--instances == 0) {
timer->stop();
}
}
int minute() const { return timer->minute(); }
int hour() const { return timer->hour(); }
signals:
void timeChanged();
private:
QTime t;
static MinuteTimer *timer;
static int instances;
};
int TimeModel::instances=0;
MinuteTimer *TimeModel::timer=nullptr;
3.3.3. 注册 TimeModel 类到 QML 插件
在接下来,就是将 TimeModel 类注册给 QML 插件:
class QExampleQmlPlugin : public QQmlExtensionPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID QQmlExtensionInterface_iid)
public:
void registerTypes(const char *uri) override
{
Q_ASSERT(uri == QLatin1String("TimeExample"));
qmlRegisterType<TimeModel>(uri, 1, 0, "Time");
}
};
这段代码中,作者没有使用 C++ 中的类名,而是给暴露到 QML 插件中的类另起了一个简单和一目了然的名字 Time。
3.3.4. 改造示例内容
接下来我们像上节一样改造一下这个示例,使其 qml 文件和图片资源都能一起发布到 libqmlqtimeexampleplugin.so
文件中,而不是独立的文件。
3.3.4.1. 添加 res.qrc 文件在 pro 目录
res.qrc 的内容如下:
<RCC>
<qresource prefix="/">
<file>imports/TimeExample/center.png?x-oss-process=image/watermark,g_center,image_YXJ0aWNsZS9wdWJsaWMvd2F0ZXJtYXJrLnBuZz94LW9zcy1wcm9jZXNzPWltYWdlL3Jlc2l6ZSxQXzQwCg==,t_20</file>
<file>imports/TimeExample/clock.png?x-oss-process=image/watermark,g_center,image_YXJ0aWNsZS9wdWJsaWMvd2F0ZXJtYXJrLnBuZz94LW9zcy1wcm9jZXNzPWltYWdlL3Jlc2l6ZSxQXzQwCg==,t_20</file>
<file>imports/TimeExample/Clock.qml</file>
<file>imports/TimeExample/hour.png?x-oss-process=image/watermark,g_center,image_YXJ0aWNsZS9wdWJsaWMvd2F0ZXJtYXJrLnBuZz94LW9zcy1wcm9jZXNzPWltYWdlL3Jlc2l6ZSxQXzQwCg==,t_20</file>
<file>imports/TimeExample/minute.png?x-oss-process=image/watermark,g_center,image_YXJ0aWNsZS9wdWJsaWMvd2F0ZXJtYXJrLnBuZz94LW9zcy1wcm9jZXNzPWltYWdlL3Jlc2l6ZSxQXzQwCg==,t_20</file>
</qresource>
</RCC>
3.3.4.2. 修改 qmlextensionplugins.pro 文件的内容
将其改成如下的样子:
TEMPLATE = lib
CONFIG += plugin
QT += qml
DESTDIR = imports/TimeExample
TARGET = qmlqtimeexampleplugin
SOURCES += plugin.cpp
qrc.files = qrcmoduleplugin.qrc
qml.files = plugins.qml \
imports/TimeExample/qmldir
qml.path += $$[QT_INSTALL_EXAMPLES]/qml/qmlextensionplugins
target.path += $$[QT_INSTALL_EXAMPLES]/qml/qmlextensionplugins/imports/TimeExample
qrc.path += $$[QT_INSTALL_EXAMPLES]/qml/qmlextensionplugins/res.qrc
INSTALLS += target qml qrc
CONFIG += install_ok # Do not cargo-cult this!
RESOURCES += \
res.qrc
3.3.4.3. 修改 plugin.cpp 文件中的 QExampleQmlPlugin 实现
将其改成如下形式:
class QExampleQmlPlugin : public QQmlExtensionPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID QQmlExtensionInterface_iid)
public:
void registerTypes(const char *uri) override
{
Q_ASSERT(uri == QLatin1String("TimeExample"));
qmlRegisterType<TimeModel>(uri, 1, 0, "Time");
qmlRegisterType(QUrl("qrc:/imports/TimeExample/Clock.qml"), uri, 1, 0, "Clock");
}
};
3.3.4.4 修改 Clock.qml 文件的内容
接下来我们修改名叫 Clock.qml 的 QML 文件,它主要用于时间显示,其内部主要是使用 SpringAnimation 实现的时钟,我们的修改点主要是图片的资源改为使用 qrc 文件中的资源:
import QtQuick 2.12
Rectangle {
id: clock
width: 200; height: 200; color: "gray"
property alias city: cityLabel.text
property variant hours
property variant minutes
property variant shift : 0
Image { id: background; source: "qrc:/imports/TimeExample/clock.png?x-oss-process=image/watermark,g_center,image_YXJ0aWNsZS9wdWJsaWMvd2F0ZXJtYXJrLnBuZz94LW9zcy1wcm9jZXNzPWltYWdlL3Jlc2l6ZSxQXzQwCg==,t_20" }
Image {
x: 92.5; y: 27
source: "qrc:/imports/TimeExample/hour.png?x-oss-process=image/watermark,g_center,image_YXJ0aWNsZS9wdWJsaWMvd2F0ZXJtYXJrLnBuZz94LW9zcy1wcm9jZXNzPWltYWdlL3Jlc2l6ZSxQXzQwCg==,t_20"
transform: Rotation {
id: hourRotation
origin.x: 7.5; origin.y: 73;
angle: (clock.hours * 30) + (clock.minutes * 0.5)
Behavior on angle {
SpringAnimation{ spring: 2; damping: 0.2; modulus: 360 }
}
}
}
Image {
x: 93.5; y: 17
source: "qrc:/imports/TimeExample/minute.png?x-oss-process=image/watermark,g_center,image_YXJ0aWNsZS9wdWJsaWMvd2F0ZXJtYXJrLnBuZz94LW9zcy1wcm9jZXNzPWltYWdlL3Jlc2l6ZSxQXzQwCg==,t_20"
transform: Rotation {
id: minuteRotation
origin.x: 6.5; origin.y: 83;
angle: clock.minutes * 6
Behavior on angle {
SpringAnimation{ spring: 2; damping: 0.2; modulus: 360 }
}
}
}
Image {
anchors.centerIn: background; source: "qrc:/imports/TimeExample/center.png?x-oss-process=image/watermark,g_center,image_YXJ0aWNsZS9wdWJsaWMvd2F0ZXJtYXJrLnBuZz94LW9zcy1wcm9jZXNzPWltYWdlL3Jlc2l6ZSxQXzQwCg==,t_20"
}
Text {
id: cityLabel; font.bold: true; font.pixelSize: 14; y:200; color: "white"
anchors.horizontalCenter: parent.horizontalCenter
}
}
3.3.4.5 修改 qmldir 文件的内容
修改工程目录中的 qmldir
文件的内容为:
module TimeExample
plugin qmlqtimeexampleplugin
3.3.5. 构建并拷贝资源到指定目录
接下来构建项目,然后找到构建目录,将其中的 libqmlqtimeexampleplugin.so
文件。以及工程目录中的 qmldir
文件拷贝。然后复制到 /home/dongshuang/TestQMLPlugin/TimeExample
这个目录。
3.3.6. 接着使用 3.2 节中的项目中 main.qml 进行测试
修改 main.qml 的内容,其内容其实是参考示例代码中的 plugins.qml 文件的内容:
import TimeExample 1.0 // import types from the plugin
Clock { // this class is defined in QML (imports/TimeExample/Clock.qml)
Time { // this class is defined in C++ (plugin.cpp)
id: time
}
hours: time.hour
minutes: time.minute
}
我们将 main.qml 的内容改为:
import QtQuick 2.12
import QtQuick.Window 2.12
import MyPlugins 1.0
import TimeExample 1.0
Window {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
TestRect {
}
Clock { // this class is defined in QML (imports/TimeExample/Clock.qml)
anchors.centerIn: parent
Time { // this class is defined in C++ (plugin.cpp)
id: time
}
hours: time.hour
minutes: time.minute
}
}
至此,我们完成了在 QML 插件中注册 C++ 类插件的功能,以及将 qml 文件和图片资源文件一起打包发布的示例。