Qt Quick /将C/C++中的枚举定义导出到Qml中

概述

当将 C++ 中的枚举类型用于 C# 代码时,最常规的做法是,在 C# 中重新定义相应的枚举类型,并手动映射 C++ 枚举类型的值;还有些投机的方法,如将枚举值转为字符串等。将C/C++枚举导出到qml中使用的方法,也被坑了一把。最期待的是,能将相关定义直接地从C++定义中拿到qml中进行使用。如果这些枚举类型是定义在QObject派生类中的,那很简单,书上都讲了。本文将重点去研究,如果枚举定义不是在Qt C++类之下定义的,此时该如何导出相应枚举类型到qml系统中去使用?尤其是,对于Qt无关的那些DLL中导出的枚举类型,如何将他们以一种简约的方式引入到QML中去使用。

导出Qt类中的枚举类型

这里的Qt类泛指从QObject直接继承或隔辈继承的所有类。Qt提供了Q_ENUM宏函数,方法很简单,可参见后续小节示例代码。

导出命名空间中的枚举类型

刚开始的时候,我奔着Q_ENUM_NS和Q_NAMESPACE等信息搜索相关导出方法,并没有找到很多有用信息,帮助中也没有,GPT也给我一本正经的胡说八道。直到浏览到了 qmlRegisterUncreatableMetaObject 的帮助信息,

// 函数声明
int qmlRegisterUncreatableMetaObject(const QMetaObject &staticMetaObject, const char *uri, int versionMajor, int versionMinor, const char *qmlName, const QString &reason)

// Q_NAMESPACE 宏定义
#define Q_NAMESPACE \
    extern const QMetaObject staticMetaObject; \
    QT_ANNOTATE_CLASS(qt_qnamespace, "") \

//示例代码 //定义在类外的枚举变量
namespace MyNamespace {
  Q_NAMESPACE
  enum MyEnum {
      Key1,
      Key2,
  };
  Q_ENUMS(MyEnum)
}

//示例代码  //使用方法 /用以注册命名空间
qmlRegisterUncreatableMetaObject(MyNamespace::staticMetaObject, "io.qt", 1, 0, "MyNamespace", "Access to enums & flags only");

//示例代码  //在QML中使用注册的命名空间内的枚举类型
Component.onCompleted: console.log(MyNamespace.Key2)

This function registers the staticMetaObject and its extension in the QML system with the name qmlName in the library imported from uri having version number composed from versionMajor and versionMinor. 我英语底子差,理解这句话废了好几分钟。句子中的介词with在这里表示一种动作或关系,这里该是表动作,意为以某种形式。后的半句断句如下,
with the name qmlName / in the library imported / from uri having version number composed from versionMajor and versionMinor.
其中 library imported 直译为导入库(只qml中import用的qml包或称qml模块),第一个from补充说明library imported,第二个from补充说明version 。整句翻译如下,该函数将可以把 staticMetaObject 以 uri +versionMajor+versionMinor为QML模块表述,以qmlName为qml对象名称,注册到QML系统中。
An instance of the meta object cannot be created. An error message with the given reason is printed if the user attempts to create it.
This function is useful for registering Q_NAMESPACE namespaces. Returns the QML type id. 注册失败时reason会被打印。

补充,宏Q_ENUM_NS 和 和Q_ENUM
Q_ENUM_NS宏的后缀_NS是NameSpace的意思,它适用于将枚举类型导出到命名空间级别,而Q_ENUM宏适用于将枚举类型导出到QObject派生类级别,他们的目的都是使目标枚举类型在QML中可见。Q_ENUM_NS需要配合Q_NAMESPACE宏来注册命名空间。
This macro registers an enum type with the meta-object system. It must be placed after the enum declaration in a namespace that has the Q_NAMESPACE macro. It is the same as Q_ENUM but in a namespace.
在示例代码中用的是Q_ENUMS,不是 Q_ENUM_NS 也不是 和Q_ENUM,经过实际测试,在命名空间内使用Q_ENUM会编译报错,使用Q_ENUMS 和 Q_ENUM_NS宏都是可以的。他们的关系如下:

#define Q_ENUMS(x) QT_ANNOTATE_CLASS(qt_enums, x)
#define Q_ENUM(x) Q_ENUMS(x) Q_ENUM_IMPL(x)
#define Q_ENUM_NS(x) Q_ENUMS(x) Q_ENUM_NS_IMPL(x)

导出枚举类型到qml的示例代码

#include <QObject>
//类外的枚举定义
namespace MyNamespace {
    Q_NAMESPACE
    enum MyEnum {
        EValue1 = 100,
        EValue2,
        EValue3
    };
    Q_ENUM_NS(MyEnum)
}

//可导出到qml的类
class MyObject : public QObject
{
    Q_OBJECT

public:
    explicit MyObject(QObject *parent = nullptr);

    //类内的枚举
    enum MyEnum {
        EValue1 = 10,
        EValue2,
        EValue3
    };
    Q_ENUM(MyEnum)

signals:
    void mySignal();
};

main.cpp

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include "myobject.h"

int main(int argc, char *argv[])
{
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);

    QGuiApplication app(argc, argv);

    //导出MyObject类的枚举到qml中
    qmlRegisterType<MyObject>("river.qtqml.custom", 1, 0, "MyObject");
    //导出MyNamespace命名空间下的枚举到qml中
    qmlRegisterUncreatableMetaObject(MyNamespace::staticMetaObject, "river.qtqml.custom", 1, 0, "MyNamespace", "Access to enums & flags only");

    QQmlApplicationEngine engine;
    const QUrl url(QStringLiteral("qrc:/main.qml"));
    QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
                     &app, [url](QObject *obj, const QUrl &objUrl) {
        if (!obj && url == objUrl)
            QCoreApplication::exit(-1);
    }, Qt::QueuedConnection);
    engine.load(url);

    return app.exec();
}

main.qml

import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Controls 2.12
import river.qtqml.custom 1.0

Window {
    visible: true
    width: 640
    height: 480
    title: qsTr("Hello World")

    MouseArea {
        anchors.fill: parent;
        onClicked: {
            //命名空间中的枚举
            console.log(MyNamespace.EValue2);  //101
            //类中的枚举
            console.log(MyObject.EValue2);     //11
        }
    }
}

导出no-Qt的枚举定义

尤其是在使用一些DLL时,其导出头文件中,往往含有枚举定义、或者是宏定义的枚举。我始终不愿意接受,像C#那样,还要在目标新环境里重新定义他们。有些所谓的方法,如将枚举转字符串,其实还不如在新环境下复制定义,因为所谓的转换,还是一种一对一映射,那当然是不如完全拷贝这种一对一映射干脆利,都枚举定义变动时,代码的变动是一样的,都逃不掉。

//dll_c_if.h
enum DLL_C_Enum {
    C_E_Value1 = 1000,
    C_E_Value2,
    C_E_Value3
};

尝试使用Q_ENUM直接声明在MyObject类中,
这种方案存在理论上的错误,因为在qml中对枚举值的调用是依靠"类名"的,准确的说是QML对象名,如之前的示例代码中,调用语句 MyObject.EValue2 那样。但是如果在 MyObject 类声明中执行 Q_ENUM(DLL_C_Enum) ,虽然不会报告编译错误,但是却没有意义,因为DLL_C_Enum压根不属于类MyObject 啊。在qml中,写下 console.log(MyObject.C_E_Value2); 这样的代码也不会报错,当你运行时,会在结果里显示 qml: undefined

尝试使用Q_ENUM_NS声明在命名空间MyNamespaceC中,

//myobject.h //注意不要直接定义在main.cpp中 //否则编译告警error: undefined reference to `MyNamespaceB::staticMetaObject'
#include "dll_c_if.h"
namespace MyNamespaceB{
    Q_NAMESPACE
    Q_ENUM_NS(DLL_C_Enum)
}
//int main(int argc, char *argv[])
qmlRegisterUncreatableMetaObject(MyNamespaceB::staticMetaObject, "river.qtqml.custom", 1, 0, "MyNamespaceB", "Access to enums & flags only");

按照上述导出方式,在qml中编写执行 console.log(MyNamespaceB.C_E_Value2); 依然打印 qml: undefined。即使我为dll_c_if中的DLL_C_Enum定义也套上一层MyNamespaceB,没有任何的改善。因此,可以断定,我们想在Q_NAMESPACE之前来定义枚举的小伎俩,并不奏效。为此,我还不死心的尝试了,

namespace MyNamespaceB {
    Q_NAMESPACE
    #include "dll_c_if.h"
    Q_ENUM_NS(DLL_C_Enum)
}

我做完上述修改后,闭上了眼睛,执行了测试过程。等我睁开眼睛,天哪,简直不敢相信,我竟然成功了!下节将附上完整示例。

导出C头文件中的枚举类型到QML中

//dll_c_if.h
#ifndef DLL_C_IF_H_
#define DLL_C_IF_H_

#ifdef __cplusplus
 extern "C" {
#endif

    enum DLL_C_Enum {
        C_E_Value1 = 1000,
        C_E_Value2,
        C_E_Value3
    };

#ifdef __cplusplus
 }
#endif

#endif // DLL_C_IF_H_
//qmlwrap.h
#ifndef MYOBJECT_H
#define MYOBJECT_H

#include <QObject>

namespace Namespace_Wrap_C_Enum {
    Q_NAMESPACE
    #include "dll_c_if.h"
    Q_ENUM_NS(DLL_C_Enum)
}
#endif // MYOBJECT_H
//main.cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include "qmlwrap.h"

int main(int argc, char *argv[])
{
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
    QGuiApplication app(argc, argv);

    //导出MyNamespace命名空间下的枚举到qml中
    qmlRegisterUncreatableMetaObject(Namespace_Wrap_C_Enum::staticMetaObject, "river.qtqml.custom", 1, 0, "Namespace_Wrap_C_Enum", "Access to enums & flags only");

    QQmlApplicationEngine engine;
    const QUrl url(QStringLiteral("qrc:/main.qml"));
    QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
                     &app, [url](QObject *obj, const QUrl &objUrl) {
        if (!obj && url == objUrl)
            QCoreApplication::exit(-1);
    }, Qt::QueuedConnection);
    engine.load(url);

    return app.exec();
}
//main.qml
import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Controls 2.12
import river.qtqml.custom 1.0

Window {
    visible: true
    width: 640
    height: 480
    title: qsTr("Hello World")

    MouseArea {
        anchors.fill: parent;
        onClicked: {
            //使用C头文件中的枚举
            console.log(Namespace_Wrap_C_Enum.C_E_Value2);     //print 1001
        }
    }
}

注意,
如果直接将含有Q_NAMESPACE的命名空间定义或者含有Q_OBJECT的Qt类声明,放置在main.cpp文件中是有编译问题的,可能会有 error: undefined reference to `MyNamespaceB::staticMetaObject’ 等常见编译错误。其主要原因是,main.cpp无法被moc解析来生成moc中间文件,而staticMetaObject正是自动创建在moc_qmlwrap.cpp中的。这与之前遇到的,无法在一个h文件中声明两个包含Q_OBJECT的类,其本质原因是差不多的,此处不再赘述。

实际项目中的应用问题

在实际的项目中,我们拿到的DLL的导出头文件,更可能的是,既包含枚举定义,又包含导出接口定义的。如下,

//dll_c_if.h
//#pragma once //确保头文件只被编译一次,防止重复包含 /仅VS适用
#ifndef DLL_C_IF_H_
#define DLL_C_IF_H_

#include <string>

#ifdef COMM_LIBRARY
#ifdef _WIN32 
#define COMM_API_EXPORT /*extern "C"*/ __declspec(dllexport) 
#else
#define COMM_API_EXPORT __attribute__((visibility("default")))
#endif
#else
#ifdef _WIN32
#define COMM_API_EXPORT /*extern "C"*/ __declspec(dllimport)
#else
#define COMM_API_EXPORT __attribute__((visibility("hidden")))
#endif
#endif

//接口中使用了非C的std::string //若强行声明 extern "C" 会有编译告警
//#ifdef __cplusplus
//extern "C" {
//#endif

    enum EnumRegID {
        ID_E_REG_M = 10,
        ID_E_REG_N,
        ID_E_REG_P
    };

    //dll导出的接口
    COMM_API_EXPORT std::string RegTable_Name(EnumRegID u16RegID);

//#ifdef __cplusplus
//}
//#endif

#endif // DLL_C_IF_H_

上述DLL的创建过程如下。在VS - VC++ - Windows 桌面 - 动态库链接(DLL),新建名为 DLL_Of_C 项目,配置不使用预编译头,不关注dllmian实现。其对应的简单实现如下,

#include "dll_c_if.h"
//函数接口实现
std::string RegTable_Name(EnumRegID u16RegID)
{
    return "river.qu@" + std::to_string(u16RegID);
}

使用 VS2017中进行必要的配置,如在项目属性 - C/C++ - 预处理器 - 预处理起定义 配置中,增加 COMM_LIBRARY 宏定义,以导出C接口;在项目属性 - 常规 - 输出目录配置为…/bin/;设置编译平台为x64,设置为Debug模式等。然后编译生成。上述…/bin/ 是指向 QmlA项目的输出目录的,以省却了拷贝 DLL_Of_C.dll 和 DLL_Of_C.lib 到QmlA项目执行目录下的麻烦。

试图在上一节中提到的方案,一起导出dll_c_if.h文件下的枚举和接口,

//qmlwrap.h
#include <QObject>

//导出枚举
namespace NS_WrapC_Enum {
    Q_NAMESPACE
    #include "dll_c_if.h"
    Q_ENUM_NS(EnumRegID)
}

class Object4Qml : public QObject
{
    Q_OBJECT

public:
    explicit Object4Qml(QObject *parent = nullptr);

public:
	//法1 //EnumRegID找不到定义
    //Q_INVOKABLE QString qml_RegTable_Name(EnumRegID u16RegID);
    //法2 //无编辑器告警
    Q_INVOKABLE QString qml_RegTable_Name(NS_WrapC_Enum::EnumRegID u16RegID);
};

//qmlwrap.cpp
QString Object4Qml::qml_RegTable_Name(EnumRegID u16RegID)
{
    std::string strName = RegTable_Name(u16RegID);
    return QString::fromLocal8Bit(strName.c_str());
}

由于上边的方案并行不通,这里不做详细测试代码展示。如上法2实现下,编译错误如下,

  • qmlwrap.obj : error LNK2019: 无法解析的外部符号 “__declspec(dllimport) class std::basic_string<char,struct std::char_traits,class std::allocator > __cdecl NS_WrapC_Enum::RegTable_Name(enum NS_WrapC_Enum::EnumRegID)” (_imp?RegTable_Name@NS_WrapC_Enum@@YA?AV? b a s i c s t r i n g @ D U ? basic_string@DU? basicstring@DU?char_traits@D@std@@V?$allocator@D@2@@std@@W4EnumRegID@1@@Z),该符号在函数 “public: class QString __cdecl Object4Qml::qml_RegTable_Name(enum NS_WrapC_Enum::EnumRegID)” (?qml_RegTable_Name@Object4Qml@@QEAA?AVQString@@W4EnumRegID@NS_WrapC_Enum@@@Z) 中被引用
    …\bin\QmlA.exe : fatal error LNK1120: 1 个无法解析的外部命令

告警的意思大抵是,无法解析函数 NS_WrapC_Enum::RegTable_Name导出函数。这很好理解,DLL_Of_C中导出的是 RegTable_Name 函数 而不是 NS_WrapC_Enum::RegTable_Name 函数。
也就是说,在qml_RegTable_Name的实现中,
std::string strName = RegTable_Name(u16RegID);
在 NS_WrapC_Enum和Q_NAMESPACE 双重作用下,被编译器理解成了,
std::string strName = NS_WrapC_Enum::RegTable_Name(u16RegID);

以此,试图在一个.h中同时包装 一个C++头文件中的 C风格枚举定义 + C风格的接口定义 是行不通的!

如果这个DLL是你自己写的,那么,你只需要将 DLL 的枚举定义独立到一个新的头文件中,然后提供两个头文件给外部使用。如此,再参照上一节的枚举导出方法,便可以完成对整个DLL文件的导出。

如果DLL不是你自己的,修改头文件分离枚举定义和接口定义,重新编译,是肯定不可能啦。咋办?

我经过3个小时的实验,终于有了办法。主要思路是,将DLL头文件中的枚举定义和接口定义分别包装在两个地方,且保证它们没有交集。累了,不多说了,直接上代码。

//qml_enum.h
#ifndef _QML_ENUM_H_
#define _QML_ENUM_H_

#include <QObject>

//qml_enum.h仅用以封装导出DLL头文件中的枚举定义
namespace NS_WrapC_Enum {
    Q_NAMESPACE
    #include "dll_c_if.h"
    Q_ENUM_NS(EnumRegID)
}

//在qmlwrap.h这个封装导出DLL接口定义的文件中,是禁止包含qml_enum.h,否则就会有前文提到的编译报错

#endif // _QML_ENUM_H_
//qmlwrap.h
#ifndef MYOBJECT_H
#define MYOBJECT_H

#include <QObject>

//不可以在此处以如下方式封装DLL中的枚举定义 //否则编译报错如前文
//namespace NS_WrapC_Enum {
//    Q_NAMESPACE
//    #include "dll_c_if.h"
//    Q_ENUM_NS(EnumRegID)
//}

//必须是直接包含DLL头文件
#include "dll_c_if.h"

class Object4Qml : public QObject
{
    Q_OBJECT

public:
    explicit Object4Qml(QObject *parent = nullptr);

public:
    //导出C++的C风格接口到QML中使用
    Q_INVOKABLE QString qml_RegTable_Name(EnumRegID u16RegID);

    //由于EnumRegID并未导出到qml中,其不能在此函数中使用;导出去的是NS_WrapC_Enum.EnumRegID
    Q_INVOKABLE QString qml_RegTable_Name2(unsigned short u16RegID);
};

#endif // MYOBJECT_H
//qmlwrap.cpp
#include "qmlwrap.h"

Object4Qml::Object4Qml(QObject *parent) : QObject(parent)
{
}

QString Object4Qml::qml_RegTable_Name(EnumRegID u16RegID)
{
    std::string strName = RegTable_Name(u16RegID);
    return QString::fromLocal8Bit(strName.c_str());
}

QString Object4Qml::qml_RegTable_Name2(unsigned short u16RegID)
{
    //使用C枚举类型强制转换
    std::string strName = RegTable_Name((EnumRegID)u16RegID);
    //
    return QString::fromLocal8Bit(strName.c_str());
}
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QDebug>
#include "qmlwrap.h"
#include "qml_enum.h"

//注意
//该项目使用Qt Creator + MSVC 64 编译器
//DLL_Of_C 库文件使用 VS2017 x64 编译生成

int main(int argc, char *argv[])
{
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
    QGuiApplication app(argc, argv);

    //导出名空间下的枚举到qml中
    qmlRegisterUncreatableMetaObject(NS_WrapC_Enum::staticMetaObject, "river.qtqml.custom", 1, 0, "NS_WrapC_Enum", "Access to enums & flags only");

    //导出"包装DLL接口"的Qt类
    qmlRegisterType<Object4Qml>("river.qtqml.custom", 1, 0, "Object4Qml");

    QQmlApplicationEngine engine;
    const QUrl url(QStringLiteral("qrc:/main.qml"));
    QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
                     &app, [url](QObject *obj, const QUrl &objUrl) {
        if (!obj && url == objUrl)
            QCoreApplication::exit(-1);
    }, Qt::QueuedConnection);
    engine.load(url);

    return app.exec();
}
import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Controls 2.12
import river.qtqml.custom 1.0

Window {
    visible: true
    width: 640
    height: 480
    title: qsTr("Hello World")

    Object4Qml {
        id: hdlObject4Qml
    }

    MouseArea {
        anchors.fill: parent;
        onClicked: {
            //
            //命名空间中的枚举
            //
            console.log(NS_WrapC_Enum.ID_E_REG_N);  

            //
            //Object4Qml类接口调用测试
            //

            //告警 qrc:/main.qml:23: Error: Unknown method parameter type: EnumRegID
            //console.log(hdlObject4Qml.qml_RegTable_Name(NS_WrapC_Enum.ID_E_REG_P));
            //告警 qrc:/main.qml:23: Error: Unknown method parameter type: EnumRegID
            //console.log(hdlObject4Qml.qml_RegTable_Name(100));

            //注意:qml运行阶段一旦遇到上述类似错误将截止运行;因此后续测试时必须要将上述代码封掉

            //测试正常 printf river.qu@12
            console.log(hdlObject4Qml.qml_RegTable_Name2(NS_WrapC_Enum.ID_E_REG_P));
            //测试正常 printf river.qu@100
            console.log(hdlObject4Qml.qml_RegTable_Name2(100));
        }
    }
}

运行程序,可得到想要的测试结果:
qml: 11
qml: river.qu@12
qml: river.qu@100

好了到此为止吧!如有新问题,欢迎大家留言讨论。

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值