最近在试着把自定义的QML控件封装成插件供其他程序调用,由于我使用的还是Qt5,在构建的过程中遇到不少问题,这里做一下简单汇总。
新建项目#
新建库→Qt Quick2 Extension Plugin,Qt 5
版本仅支持使用QMake
作为构建套件。Qt 6
才支持使用CMake
作为QML扩展插件的构建套件。
编写插件#
例如,我新建了一个叫做Test
的插件库,项目会自动创建一个TestPlugin
的类,这个类继承自QQmlExtensionPlugin
,我们需要重写它的两个虚函数。
1
2
3
4
5
6
7
8
9
10
11
12
| /* testplugin.h */
#include <QQmlExtensionPlugin>
class TestPlugin : public QQmlExtensionPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID QQmlExtensionInterface_iid)
public:
void registerTypes(const char *uri) Q_DECL_OVERRIDE;
void initializeEngine(QQmlEngine *engine, const char *uri) Q_DECL_OVERRIDE;
};
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| /* testplugin.cpp */
#include "testplugin.h"
#include "myitem.h"
#include <qqml.h>
void TestPlugin::registerTypes(const char *uri)
{
// @uri com.mycompany.qmlcomponents
qmlRegisterType<MyItem>(uri, 1, 0, "MyItem");
}
void TestPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
{
QQmlExtensionPlugin::initializeEngine(engine, uri);
}
|
其中,MyItem
就是我们自定义的控件。简单看一下示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
| /*myitem.h*/
#include <QtQuick/QQuickPaintedItem>
class MyItem : public QQuickPaintedItem
{
Q_OBJECT
QML_ELEMENT
Q_DISABLE_COPY(MyItem)
public:
explicit MyItem(QQuickItem *parent = Q_NULLPTR);
void paint(QPainter *painter) Q_DECL_OVERRIDE;
~MyItem() Q_DECL_OVERRIDE;
};
|
MyItem
继承自QQuickItem
,并使用QML_ELEMENT
声明了它是一个QML元素,最后再使用qmlRegisterType
注册。这样MyItem
就可以在QML中使用了。
使用这种方式也可以注册继承自QObject
的类。例如:
1
2
3
4
5
6
7
8
| /* myobject.h */
class MyObject : public QObject
{
Q_OBJECT
Q_DISABLE_COPY(MyObject)
public:
explicit MyObject(QObject *parent = Q_NULLPTR);
};
|
然后在Plugin
类中进行注册:
1
2
3
4
5
6
7
| /* testplugin.cpp */
void TestPlugin::registerTypes(const char *uri)
{
// @uri com.mycompany.qmlcomponents
qmlRegisterType<MyItem>(uri, 1, 0, "MyItem");
qmlRegisterType<MyObject>(uri, 1, 0, "MyObject");
}
|
自定义QML控件#
QML插件库当然也可以封装.qml
的自定义插件。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
| /* MyLabel.qml */
import QtQuick 2.15
import QtQuick.Controls 2.15
Label {
id: control
property string backgroundColor
background: Rectangle {
color: backgroundColor
}
}
|
把自定义的qml控件添加到资源文件,然后可以在Plugin
类中进行注册。
1
2
3
4
5
6
7
8
9
10
11
| /* testplugin.cpp */
void TestPlugin::registerTypes(const char *uri)
{
// @uri com.mycompany.qmlcomponents
qmlRegisterType<MyItem>(uri, 1, 0, "MyItem");
qmlRegisterType<MyObject>(uri, 1, 0, "MyObject");
// register qml types
qmlRegisterType(QUrl("qrc:/qml/MyLabel.qml"),
uri, MAJOR, MINOR, "MyLabel");
}
|
qmldir文件#
qmldir
文件用于QML插件模块管理,描述了qml插件的基本信息。示例:
1
2
3
4
5
6
| module com.mycompany.qmlcomponents
plugin testplugin
classname TestPlugin
typeinfo plugins.qmltypes
designersupported
depends QtQuick.Controls 2.12
|
module
和插件的uri配置相同plugin
插件文件名字,通常是小写classname
插件类的名字typeinfo
插件的元数据文件,稍后会写怎么生成这个文件
项目文件配置#
下面是我的一个项目配置,包含插件信息配置,和生成元数据的自定义命令。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| ## TestPlugin.pro
TEMPLATE = lib
TARGET = testplugin
QT += core qml quick
CONFIG += plugin c++11
# CONFIG += qmltypes
# 插件的URI
uri = com.mycompany.qmlcomponents
QML_PLUGIN_NAME = $$TARGET
# 可以通过 import 的方式在qml中使用插件中的控件
QML_IMPORT_NAME = $$uri
# 插件的版本号
QML_IMPORT_MAJOR_VERSION = 1
QML_IMPORT_MINOR_VERSION = 0
# 生成插件的路径
DESTDIR = imports/$$replace(QML_IMPORT_NAME, \., $$QMAKE_DIR_SEP)
QMLTYPES_FILENAME = $$DESTDIR/plugins.qmltypes
TARGET = $$qtLibraryTarget($$TARGET)
HEADERS += ...
SOURCES += ...
# 拷贝qmldir文件到输出目录
DISTFILES = qmldir
!equals(_PRO_FILE_PWD_, $$OUT_PWD) {
copy_qmldir.target = $$DESTDIR/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
}
# 添加构建后生成元数据的自定义命令
qmltypes.commands = $$[QT_INSTALL_PREFIX]/bin/qmlplugindump -nonrelocatable $$QML_IMPORT_NAME "$${QML_IMPORT_MAJOR_VERSION}.$${QML_IMPORT_MINOR_VERSION}" $$OUT_PWD/imports > $$OUT_PWD/$$DESTDIR/plugins.qmltypes
qmltypes.depends = $$QML_PLUGIN_NAME.target
QMAKE_EXTRA_TARGETS += qmltypes
# 确保构建后执行元数据生成
# POST_TARGETDEPS += $$QML_PLUGIN_NAME
POST_TARGETDEPS += qmltypes
|
这里有几点需要注意的:
- 有资料说加入
CONFIG += qmltypes
配置就可以自动生成插件元数据信息,这点我测试确实可以,但生成的元数据是不完整的。这个配置只能生成使用了QML_ELEMENT
宏的类,无法生成QObject
类和QML
控件的元数据,基本上无用。 - 生成元数据必须使用
qmlplugindump
工具,这个工具和qmake
在同一个目录下。
qmlplugindump工具的使用#
这里使用的命令如下:
qmlplugindump -nonrelocatable QML导入URI 插件版本 插件目录 > 生成plugins.qmltypes路径
- QML导入URI:编译插件时指定的uri,如:com.mycompany.qmlcomponents
- 插件本本:编译插件时指定的版本,如:1.0
- 插件目录:这个需要特别注意,这个指的是插件的目录,不是插件库的目录。比如我生成的插件是在
imports
目录,构建套件会根据插件的uri自动生成插件库的目录,如:imports/com/mycompany/qmlcomponents/testplugin.so
,那么这里应该传入参数/path/to/imports
,不要传入插件库所在的目录。 - 生成plugins.qmltypes路径:这个没什么好解释的,但注意
plugins.qmltypes
需要和插件库放在统一目录,所以这里最后传入参数/path/to/imports/com/mycompany/qmlcomponents/plugins.qmltypes
。
注意:
- qmldir文件和生成的插件需要放到同一目录下,否则
qmlplugindump
读不到插件信息。 - 如果生成的插件依赖其他动态库,一定要确保
qmlplugindump
能找到这些库,否则插件无法加载就不能生成元数据。
问题:
- plugin cannot be loaded for module “com.mycompany.qmlcomponents”: Cannot load library testplugin.dll: 找不到指定的模块。(检查动态库的路径,可尝试添加到环境变量)
- QQmlComponent: Component is not ready
file:///path/to/imports/com/mycompany/qmlcomponents/qmldir: plugin cannot be loaded for module “”: Module namespace ‘com.mycompany.qmlcomponents’ does not match import URI ’’
(检查传入参数的插件目录是否正确?)
使用QML插件#
- 确保插件目录中包含以下几个文件:testplugin.so、plugins.qmltypes、qmldir。
- 将插件目录添加到Qml Emgine的导入目录列表:
1
2
3
4
5
6
7
| /* main.cpp */
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine(&app);
...
// 添加插件路径到导入目录列表
engine.addImportPath("imports");
engine.load(url);
|
- 在qml中导入、使用自定义控件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| /* main.qml */
import com.mycompany.qmlcomponents 1.0
Window {
id: window
width: 640
height: 480
visible: true
title: qsTr("Hello World") // @disable-check M16
MyItem {
id: myitem
}
MyLabel {
id: mylabel
text: qsTr("Hello World")
backgroundColor: 'red'
}
}
|
如何使用插件中的静态函数?#
这里需要对静态函数做一下封装。例如,我定义了一个静态函数:
1
2
3
4
5
6
7
8
9
| /* myutils.h */
class MyUtils
{
public:
static void myFunc()
{
printf("Hello from myFunc().");
}
};
|
这里需要定义一个插件的Helper类,然后在插件里面注册这个类,在这个类里面实现静态函数调用。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
| /* testpluginhelper.h */
#include <QObject>
#include "myutils.h"
class TestPluginHelper : public QObject
{
Q_OBJECT
public:
Q_INVOKABLE void callMyFunc()
{
MyUtils::myFunc();
}
};
|
1
2
3
4
5
6
7
| /* testplugin.cpp */
void TestPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
{
QQmlExtensionPlugin::initializeEngine(engine, uri);
// 注册对象到上下文
engine->rootContext()->setContextProperty("testPluginHelper", new TestPluginHelper);
}
|
在QML中调用:
1
2
3
4
5
6
7
| Item {
id: item
Component.onCompleted: {
testPluginHelper.callMyFunc()
}
}
|