在QML工程中,一般QML界面只负责前端交互,而真正的业务逻辑都是C++模块实现的。为了实现前端和后端的顺利衔接,我们需要做好QML界面与C++的交互。这里就介绍一下如何在QML中调用对应的C++模块。在QML中调用C++模块的方法主要有三种,分别是:
1.设置上下文属性(setContextProperty())
2.在QML引擎里面注册新类型(qmlRegisterType)
3.导出对应的QML扩展插件。
下面介绍一下三个方法的优缺点:
对于小型应用来说,方法一设置上下文属性是最简单实用的方法。开发者只需要将对应的接口和变量暴露给QML就行。由于设置在QML中的变量是全局的,一定要注意避免名称冲突。
在QML引擎里面注册新的类型,允许用户在QML文件中控制C++对象的生命周期,这是设置上下文属性这种方法无法实现的。同时注册新类型的方法,不会污染全局命名空间。但是这种方法也有一个缺点,就是QML中的类型都需要提前注册,所有用到的库都需要在程序启动的时候链接,无法动态链接。但在绝大多数情况下,这并不是一个问题。
QML扩展插件是弹性最好,但也是最复杂的方法。QML允许用户在插件里面注册对应的新类型。这些新类型在QML第一次导入对应的符号的时候被加载。同时,通过使用QML单例引入,我们的新类型不会污染全局命名空间。由于新类型被插件化了,我们可以很轻松的在多个项目中复用我们之前定义的新类型。
下面分别通过实例介绍一下三种方法的调用过程
首先在QML工程中添加一个C++类FileIO,该类主要负责文件的打开读取和保存。类继承自QObject,导出了QML需要访问的接口和成员变量,代码如下:
//fileio.h
#ifndef FILEIO_H
#define FILEIO_H
#include
//用来打开的保存对应的文件
class FileIO : public QObject
{
Q_OBJECT
//定义QML可以访问的属性,定义格式如下
//Q_PROPERTY(变量类型 访问名称 READ 读方法 WRITE 写方法 NOTIFY 发生变化的通知信号)
//需要定义在Q_OBJECT之后第一个public之前
Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY sourceChanged)
Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged)
//ui_title是在QML使用的别名,m_title_content是对应的变量名称
//CONSTANT说明是只读的
Q_PROPERTY(QString ui_title MEMBER m_title_content CONSTANT)
public:
FileIO(QObject *parent = 0);
~FileIO();
//定义QML可以访问的方法
Q_INVOKABLE void read();
Q_INVOKABLE void write();
QUrl source() const;
QString text() const;
public slots:
void setSource(QUrl source);
void setText(QString text);
signals:
void sourceChanged(QUrl arg);
void textChanged(QString arg);
private:
QUrl m_file_source;
QString m_file_content;
//用来测试的只读title数据
QString m_title_content;
};
#endif // FILEIO_H
//fileio.cpp
#include "fileio.h"
FileIO::FileIO(QObject *parent)
: QObject(parent),
m_title_content(QString("fileio"))
{
}
FileIO::~FileIO()
{
}
void FileIO::read()
{
if(m_file_source.isEmpty()) {
return;
}
QFile file(m_file_source.toLocalFile());
if(!file.exists()) {
qWarning() << "Does not exits: " << m_file_source.toLocalFile();
return;
}
if(file.open(QIODevice::ReadOnly)) {
QTextStream stream(&file);
m_file_content = stream.readAll();
emit textChanged(m_file_content);
}
}
void FileIO::write()
{
if(m_file_source.isEmpty()) {
return;
}
QFile file(m_file_source.toLocalFile());
if(file.open(QIODevice::WriteOnly)) {
QTextStream stream(&file);
stream << m_file_content;
}
}
QUrl FileIO::source() const
{
return m_file_source;
}
QString FileIO::text() const
{
return m_file_content;
}
void FileIO::setSource(QUrl source)
{
if (m_file_source == source)
return;
m_file_source = source;
emit sourceChanged(source);
}
void FileIO::setText(QString text)
{
if (m_file_content == text)
return;
m_file_content = text;
emit textChanged(text);
}
定义完对应的C++类型之后,我们就可以在QML工程中添加对应的上下文属性了,添加方法如下:
#include
#include
#include
#include
#include "fileio.h"
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
//根据不同的QT版本设置对应的编码
app.setFont(QFont("Microsoft Yahei", 9));
#if (QT_VERSION <= QT_VERSION_CHECK(5,0,0))
#if _MSC_VER
QTextCodec *codec = QTextCodec::codecForName("gbk");
#else
QTextCodec *codec = QTextCodec::codecForName("utf-8");
#endif
QTextCodec::setCodecForLocale(codec);
QTextCodec::setCodecForCStrings(codec);
QTextCodec::setCodecForTr(codec);
#else
QTextCodec *codec = QTextCodec::codecForName("utf-8");
QTextCodec::setCodecForLocale(codec);
#endif
//定义对应的类型指针
QScopedPointer current_file_io(new FileIO());
QQmlApplicationEngine engine;
//在加载对应的URL之前, 设置上下文属性
engine.rootContext()->setContextProperty("qmlfileio",current_file_io.data());
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
if (engine.rootObjects().isEmpty())
return -1;
return app.exec();
}
在QML引擎里面添加了对应的C++类型之后,我们就可以在QML文件中使用对应的类型了,QML中的调用方法如下:
import QtQuick 2.8
import QtQuick.Window 2.2
import Qt.labs.platform 1.0
Window {
visible: true
width: 440
height: 300
title: qsTr("Context fileIO")
Column{
Rectangle{
id:contentRect
x:10; y:10
width: 400
height: 150
//用于显示打开的文本文件的内容
Text{
id:content
anchors.top :contentRect.top
anchors.bottom: contentRect.bottom
anchors.left: contentRect.left
anchors.right: contentRect.right
text: qmlfileio.text
}
border.color: "#CCCCCC"
}
//点击的按钮用来选择对应的文件
Rectangle{
anchors.horizontalCenter:contentRect.horizontalCenter
color: "#4D9CF8"
width:200
height: 30
Text{
anchors.centerIn: parent
text:"点击打开文件"
}
MouseArea {
anchors.fill: parent
onClicked: {
fileDialog.open();
}
}
}
}
//文件选择窗口,选择需要打开的文件
//并读取文件中对应的内容
FileDialog{
id: fileDialog
folder: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0]
onFileChanged: {
qmlfileio.source = fileDialog.file;
qmlfileio.read();
}
}
}
显示效果如下图所示:
通过注册的方法使用新类型的话,我们首先在main函数里面注册一下上面创建的C++类型,注册方法如下:
#include
#include
#include
#include
#include "fileio.h"
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
//在engine声明之前注册C++类型
//@1:类在qml中别名 @2:版本主版本号 @3:版本的次版本号 @4类的名称
qmlRegisterType("org.fileio",1,0,"FileIO");
QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
if (engine.rootObjects().isEmpty())
return -1;
return app.exec();
}
注册完成之后,我们就可以在QML文件中导入对应的模块并使用新创建的类型了,导入方法如下:
import org.fileio 1.0
导入完成之后,我们就可以在QML中使用对应的类型了,使用方法如下:
import QtQuick 2.8
import QtQuick.Window 2.2
import Qt.labs.platform 1.0
import org.fileio 1.0
Window {
visible: true
width: 440
height: 300
title: qsTr("Context fileIO")
Column{
Rectangle{
id:contentRect
x:10; y:10
width: 400
height: 150
//用于显示打开的文本文件的内容
Text{
id:content
anchors.top :contentRect.top
anchors.bottom: contentRect.bottom
anchors.left: contentRect.left
anchors.right: contentRect.right
text: fileIO.text
}
border.color: "#CCCCCC"
}
//点击的按钮用来选择对应的文件
Rectangle{
anchors.horizontalCenter:contentRect.horizontalCenter
color: "#4D9CF8"
width:200
height: 30
Text{
anchors.centerIn: parent
text:"点击打开文件"
}
//点击按钮弹出选择文件的对话框
MouseArea {
anchors.fill: parent
onClicked: {
fileDialog.open();
}
}
}
}
//文件选择窗口,选择需要打开的文件
//并读取文件中对应的内容
FileDialog{
id: fileDialog
folder: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0]
onFileChanged: {
fileIO.source = fileDialog.file;
fileIO.read();
}
}
//外部导入的C++类型,可以直接定义使用
//外部通过ID来访问该模块
FileIO{
id:fileIO
}
}
首先新建一个QML扩展插件的工程,创建方法如下图所示:
模块路径和导出类型的名称如下所示:
创建完成之后,就会在对应的工程里面创建一个名为qmldir的文件,用来标记这个插件的模块名称和插件名称。qmldir文件的内容如下:
//qmldir
module MyPlugin
plugin TestPlugin
工程中会增加两个插件的导出接口文件,文件内容如下:
//testplugin_plugin.h
#pragma once
#include
//继承自QQmlExtensionPlugin的类型
class TestPluginPlugin : public QQmlExtensionPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID QQmlExtensionInterface_iid)
public:
void registerTypes(const char *uri);
};
//testplugin_plugin.cpp
#include "testplugin_plugin.h"
#include "fileio.h"
#include
//该接口在插件被加载的时候调用,根据模块名称注册对应的类型
void TestPluginPlugin::registerTypes(const char *uri)
{
// @uri MyPlugin
qmlRegisterType(uri, 1, 0, "FileIO");
}
将一开始我们定义的C++类FileIO添加到插件工程中,我们就可以将自定义的C++类型添加到插件模块导出类型中了。编译工程,我们就可以输出对应的插件扩展库了,其实本质上qml扩展库就是一个共享动态库。
分别编译debug版本和release版本的插件,为插件的调用做好准备。
为了让QtCreator找到我们自定义的插件,我们需要将对应的插件拷贝到工程目录下,并将其添加到调用工程中。插件的存放目录名称为plugin,目录结构如下所示:
├── project
# │ └── plugin
# │ └── debug
# │ │ └── libTestPlugind.a
# │ │ └── TestPlugind.dll
# │ └── release
# │ │ └── libTestPlugin.a
# │ │ └── TestPlugin.dll
# ├── project.pro
在工程中添加了对应的插件目录之后,我们就可以根据需要导入对应的插件了,在pro文件下添加对应的插件目录,代码如下:
#构建对应的配置项
CONFIG(debug, release|debug) {
QML_IMPORT_PATH = $$PWD/plugin/release
}
else
{
QML_IMPORT_PATH = $$PWD/plugin/debug
}
添加完成之后,我们就可以在QML开发的时候导入使用对应的自定义模块了,示例代码如下:
import QtQuick 2.8
import QtQuick.Window 2.2
import Qt.labs.platform 1.0
//导入自定义的插件
import MyPlugin 1.0
Window {
visible: true
width: 440
height: 300
title: qsTr("Context fileIO")
Column{
Rectangle{
id:contentRect
x:10; y:10
width: 400
height: 150
//用于显示打开的文本文件的内容
Text{
id:content
anchors.top :contentRect.top
anchors.bottom: contentRect.bottom
anchors.left: contentRect.left
anchors.right: contentRect.right
text: fileIO.text
}
border.color: "#CCCCCC"
}
//点击的按钮用来选择对应的文件
Rectangle{
anchors.horizontalCenter:contentRect.horizontalCenter
color: "#4D9CF8"
width:200
height: 30
Text{
anchors.centerIn: parent
text:"点击打开文件"
}
MouseArea {
anchors.fill: parent
onClicked: {
fileDialog.open();
}
}
}
}
//文件选择窗口,选择需要打开的文件
//并读取文件中对应的内容
FileDialog{
id: fileDialog
folder: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0]
onFileChanged: {
fileIO.source = fileDialog.file;
fileIO.read();
}
}
//引用自定义插件中的类型
FileIO{
id:fileIO
}
}
在pro文件中添加了插件的导入路径之后,虽然编译成功了,但是在运行的时候,却报插件没有安装,提示信息如下:
qrc:/main.qml:5 module "MyPlugin" is not installed
这是因为程序运行的时候找不到对应的插件导致的。解决这个问题有两个方案。
方案一:将插件放到对应的系统qml目录里面
//我本地的qml目录
C:\Qt\Qt5.9.0\5.9\mingw53_32\qml
方案二:动态添加自定义插件所在的目录,直接使用
相比于方案一,方案二更加灵活建议采用。
首先我们建立一个文件夹用来存放用户自定义的插件,名称可以随意起。我创建一个名为plugin的目录用来存放自定义插件,在该目录下创建一个和插件名称相同的目录来存放对应的插件。qmldir文件中说明了插件的名称,我的插件名称是TestPlugin。这里要区别一下插件的名称和模块的名称。
//qmldir
//模块名称
module MyPlugin
//插件名称
plugin TestPlugin
创建完成插件目录之后,我们还需要在插件目录下创建对应的模块目录,这里的模块名称是MyPlugin.在对应的模块目录下拷贝对应的插件和插件描述文件。记住别忘了拷贝插件的描述文件也就是qmldir文件。拷贝完成之后的目录结构如下所示:
├── outputdir
# │ └── plugin
# │ └── TestPlugin
# │ └── MyPlugin
# │ └── TestPlugind.dll
# │ └── TestPlugin.dll
# | └── qmldir
# ├── ContextProperty.exe
这里说明一下如果模块是多级路径的话需要创建对应的子目录,假如模块的名称是
org.module.test,那么模块路径应该是TestPlugin/org/module/test/。在普通的小项目中不建议采用多级路径的模块。
插件目录完成之后,我们将插件目录拷贝到执行程序的输出路径。然后在调用程序的入口中将插件目录动态添加进去,添加方法如下:
#include
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
QString plugin_path = app.applicationDirPath() + "/plugin/TestPlugin/";
engine.addImportPath(plugin_path);
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
if (engine.rootObjects().isEmpty())
return -1;
return app.exec();
}
添加完成之后,我们就可以正常的调用对应的自定义插件了。
相比于前两种方法来说,第三种方法配置起来繁琐不少,但是总体来说使用插件的方式程序可扩展性更好,便于对自定义类型的复用。
对应的例子下载地址如下:
链接:https://pan.baidu.com/s/1bigN779S94IMXJUdqvw_xw
提取码:nlfl