Qt 枚举变量的高阶技巧 - 枚举值和字符串之间的相互转换 - Q_ENUM,Q_FLAG,Q_NAMESPACE,Q_ENUM_NS,Q_FLAG_NS

本文介绍了Qt中枚举变量的高级技巧,包括Q_ENUM和Q_FLAG宏的作用,以及如何手动实现枚举值与字符串的转换。详细讲解了Q_ENUM的使用场景和注意事项,Q_FLAG解决的多枚举值组合问题,以及Qt自带的Q_NAMESPACE, Q_ENUM_NS和Q_FLAG_NS在命名空间中的应用。最后总结了Qt枚举扩展的演变和发展。" 110738870,10296037,OpenCV Python: 将白色像素转为黑色像素的图像处理,"['图像处理', 'OpenCV', 'Python编程']

目录

一、引用 枚举值宏 所解决的问题:

1、Q_ENUM

2、Q_FLAG

二、手写枚举值和字符串之间的相互转化

1.使用 switch-case 进行装换

2.使用数组

三、QT 自带宏命令

1、Q_ENUM 的使用

Q_ENUM 使用的几个注意事项:

2、Q_FLAG 的使用

先看一下其发展路径:

第一个问题:

第二个问题:

看一下完整示例

3、Q_NAMESPACE, Q_ENUM_NS 和 Q_FLAG_NS 命名空间

四、总结:


一、引用 枚举值宏 所解决的问题:

1、Q_ENUM

能够轻松完成枚举量字符串之间的相互转化。

枚举参数显示的是 int 类型,如:3。3 这个信息对于我们 调试或打印日志 很不友好。

在方法内,我们并不知道这个 3 代表的是什么。为了解决这个问题,有了个很有用的特性:Q_ENUM

2、Q_FLAG

弥补 C++ 中结构体 无法组合使用,和 缺乏类型检查 的缺点,可以 拥有多个身份,如:

enum Roles
{
    Admin = 1,
    Member = 2
}

上述定义了两个身份:Admin 和 Member,如果一个用户想拥有两个身份应该怎么做?

Q_FLAG 就是为了解决这个问题而创造出来的。

 

二、手写枚举值字符串之间的相互转化

typedef enum 
{
	SUNDAY,
	MONDAY,
	TUESDAY,
	WEDNESDAY,
	THURSDAY,
	FRIDAY,
	SATURDAY
} WeekEnum;

1.使用 switch-case 进行装换

char * StringDirectionT(WeekEnum mWeek)
{
        switch(mWeek)
        {
                case SUNDAY : return "SUNDAY";
                case MONDAY  : return "MONDAY";
                case TUESDAY : return "TUESDAY";
                case WEDNESDAY  : return "WEDNESDAY";
                case THURSDAY  : return "THURSDAY";
                case FRIDAY  : return "FRIDAY";
                case SATURDAY  : return "SATURDAY";
                default    : printf("Illegal direction value!\n;
        }
}

输出:

printf("The Week is %s \n",StringDirectionT(m_Week));

2.使用数组

char *a_Week[] = { "SUNDAY", "MONDAY", "TUESDAY","WEDNESDAY","THURSDAY","FRIDAY","FRIDAY","SATURDAY"}; 

输出:

然后直接根据vl_Week的值去对应的数组元素

3.使用宏定义

#define enumToStr(WEEEK)  "\""#WEEK"\""

输出:

printf("The Week is %s",enumToStr(vl_Week));

 

三、QT 自带宏命令

Qt 使用一组宏命令来完成枚举量扩展功能的(正如Qt的其他核心机制一样),这些宏包括

Q_ENUM, Q_FLAG, Q_DECLARE_FLAGS,Q_DECLARE_OPERATORS_FOR_FLAGS,

Q_ENUM_NS, Q_FLAG_NS,

这些宏的实现原理和如何展开如何注册到 Qt 内核均不在本文的讲解范围,本文只讲应用。

1、Q_ENUM 的使用

首先讲解最简单明了的宏 Q_ENUM,先看代码:

#include <QObject>

class MyEnum : public QObject
{
    Q_OBJECT
public:
    explicit MyEnum(QObject *parent = nullptr);
    
    enum Priority
    {
        High = 1,
        Low = 2,
        VeryHigh = 3,
        VeryLow = 4
    };
    Q_ENUM(Priority)
};

这就是在类中定义了一个普通的枚举变量之后,额外加入了 Q_ENUM(枚举类型名)这样的一个宏语句,那么加入了这个 Qt 引入的宏语句后,我们能得到什么收益呢?

qDebug()<< MyEnum::High<< MyEnum::Low;      //qDebug()可以直接打印出枚举类值的字符串名称

QMetaEnum m = QMetaEnum::fromType<MyEnum::Priority>();  //since Qt5.5

qDebug()<< "keyToValue:"<< m.keyToValue("VeryHigh");
qDebug()<< "valueToKey:"<< m.valueToKey(MyEnum::VeryHigh);
qDebug()<< "keyCount:"<< m.keyCount();

输出是

MyEnum::High MyEnum::Low
keyToValue: 4
valueToKey: VeryHigh
keyCount: 4 

使用 Q_ENUM 注册过的枚举类型,可以不加修饰直接被 qDebug() 打印出来,另外通过静态函数 QMetaEnum::fromType() 可以获得一个 QMetaEnum 对象。以此作为中介,能够轻松完成枚举量和字符串之间的相互转化

这一点恐怕是引入Q_ENUM机制最直接的好处。

 

除此以外,QMetaEnum 还提供了一个内部的索引,从 1 开始给每个枚举量按自然数顺序编号(注意和枚举量本身的数值是两回事),提供了 int value(int index) 和 const char *key(int index)
两个便捷函数分别返回枚举量对应的数值和枚举量对应的字符串,配合 keyCount() 函数可以实现枚举量的遍历:

qDebug()<<m.name()<<":"; // name()函数返回枚举类型名字

for (int i = 0; i < m.keyCount(); ++i) 
{
    qDebug()<<m.key(i)<<m.value(i);
}

输出

Priority :
High 1
Low 2
VeryHigh 4
VeryLow 8

Q_ENUM 使用的几个注意事项:

  1. Q_ENUM 只能在使用了 Q_OBJECT 或者 Q_GADGET类中,类可以不继承自 QObject,但一定要有上面两个宏之一( Q_GADGET 是 Q_OBJECT 的简化版,提供元对象的一部分功能,但不支持信号槽);
  2. Q_ENUM 宏只能放置于所包含的结构体定义之后,放在前面编译器会报错,结构体定义和 Q_ENUM 宏之间可以插入其他语句,但不建议这样做;
  3. 一个类头文件中可以定义多个 Q_ENUM 加持的结构体,结构体和 Q_ENUM 是一一对应的关系;
  4. Q_ENUM 加持的结构体必须是公有的;
  5. Q_ENUM 宏引入自 Qt5.5 版本,之前版本的 Qt 请使用 Q_ENUMS 宏,但 Q_ENUMS 宏不支持 QMetaEnum::fromType() 函数 (这也是 Q_ENUMS 被弃用的原因)。

 

2、Q_FLAG 的使用

这个宏是为了解决拥有多个身份(枚举值)的问题的。

 

先看一下其发展路径:

enum Roles
{
    Admin = 1,
    Member = 2
}

对于上述例子,要表示拥有多个身份,用 | 运算:Roles.Admin | Roles.Member 

Roles.Admin | Roles.Member 的值是 3,由于 Roles 枚举类型中并没有定义一个值为 3 的字段,所以在方法内 roles 参数显示的是 3。

第一个问题:

但是这里也带来了一个问题,3 这个值并不在枚举结构 Roles,如果函数使用 Roles 作为自变量,编译器是无法通过的,为此往往把函数自变量类型改为整型,但因此也就丢掉了类型检查,输入量有可能是其他无意义的整型量,在运行时带来错误。


Qt引入 QFlags 类,配合一组宏命令完美地解决了这个问题。

QFlags 是一个简单的类,可以装入一个枚举量,并重载了与或非等运算符,使得枚举量能进行与或非运算,且运算结果还是一个 QFlags 包装的枚举量。一个普通的枚举类型包装成 QFlags 型,需要使用Q_DECLARE_FLAGS 宏,在全局任意地方使用”|"操作符计算自定义的枚举量,需要使用 Q_DECLARE_OPERATORS_FOR_FLAGS 宏。

加上这个 Flags 特性后,我们再来调试时,roles 参数的值就会显示为 Admin|Member 了

第二个问题:

到这,枚举类型 Roles 一切看上去没什么问题,但如果现在要增加一个角色:Mananger,会发生什么情况?按照数字值递增的规则,Manager 的值应当设为 3

但是,(Admin | Member) 的值也是 3,这就出现了 枚举值冲突 问题,如何解决呢?

解决枚举值冲突:2 的幂

既然是二进制逻辑运算 “或” 会和成员值产生冲突,那就利用逻辑运算 或 的规律来解决。

我们知道 “或” 运算的逻辑是两边只要出现一个 1 结果就会 1,比如 1|1、1|0 结果都是 1,只有 0|0 的情况结果才是 0。

那么我们就要避免任意两个值在相同的位置上出现 1。根据二进制满 2 进 1 的特点,只要保证枚举的各项值都是 2 的幂即可。

比如:

1:  00000001
2:  00000010
4:  00000100
8:  00001000

再往后增加的话就是 16、32、64...,其中各值不论怎么相加都不会和成员的任一值冲突。这样问题就解决了,所以我们要这样定义 Roles 枚举的值:

enum Roles
{
    Admin = 1,
    Member = 2,
    Manager = 4,
    Operator = 8
}

不过在定义值的时候要在心中小小计算一下,如果你想懒一点,可以用下面这种“位移”的方法来定义:

enum Roles
{
    Admin    = 1 << 0,
    Member   = 1 << 1,
    Manager  = 1 << 2,
    Operator = 1 << 3
}

一直往下递增编值即可,阅读体验好,也不容易编错。

两种方式是等效的,常量位移的计算是在编译的时候进行的,所以相比不会有额外的开销。

看一下完整示例

class MyEnum : public QObject
{
    Q_OBJECT
public:
    explicit MyEnum(QObject *parent = nullptr);

	enum Orientation
    {
        Up = 1,
        Down = 2,
        Left = 4,
        Right = 8,
    };
    Q_ENUM(Orientation)		//如不使用Orientation,可省略
    Q_DECLARE_FLAGS(OrientationFlags, Orientation)  //实际上是 QFlags 的定义式,之后才能使用 Q_FLAG(Flags)把定义的Flags注册到元对象系统
    Q_FLAG(OrientationFlags)
};

// 赋予了Flags一个全局操作符“|”,没有这个宏语句,Flags量之间进行与操作后的结果将是一个int值,而不是Flags值
Q_DECLARE_OPERATORS_FOR_FLAGS(MyEnum::OrientationFlags)

上面这段代码展示了使用Q_FLAG包装枚举定义的方法,代码中Q_DECLARE_FLAGS(Flags, Enum) 实际上被展开成 typedef QFlags< Enum > Flags,

所以 Q_DECLARE_FLAGS 实际上是 QFlags 的定义式,之后才能使用 Q_FLAG(Flags)把定义的Flags注册到元对象系统。

Q_FLAG 完成的功能和 Q_ENUM 是类似的,使得枚举量可以被 QMetaEnum::fromType() 调用。

看一下使用代码:

qDebug()<<(MyEnum::Up|MyEnum::Down);
QMetaEnum m = QMetaEnum::fromType<MyEnum::OrientationFlags>();  //since Qt5.5
qDebug()<< "keyToValue:"<<m.keyToValue("Up|Down");
qDebug()<< "valueToKey:"<<m.valueToKey(Up|Down);
qDebug()<< "keysToValue:"<<m.keysToValue("Up|Down");
qDebug()<< "valueToKeys:"<<m.valueToKeys(Up|Down)<<endl;

qDebug()<< "isFlag:"<<m.isFlag();
qDebug()<< "name:"<<m.name();
qDebug()<< "enumName:"<<m.enumName();               //since Qt5.12
qDebug()<< "scope:"<<m.scope()<<endl;

执行结果

QFlags<MyEnum::Orientation>(Up|Down)
keyToValue: -1
valueToKey: 
keysToValue: 3
valueToKeys: "Up|Down" 

isFlag: true
name: OrientationFlags
enumName: Orientation
scope: MyEnum 

可以看到,经过 Q_FLAG 包装之后,QMetaEnum 具有了操作复合枚举量的能力,注意这时应当使用 keysToValue() 和 valueToKeys() 函数,取代之前的 keyToValue() 和 valueToKey() 函数。另外,isFlag() 函数返回值变成了 true,name() 和 enumName() 分别返回 Q_FLAG 包装后和包装前的结构名。


实际上此时类中是存在两个结构体的,如果在定义时加上了 Q_ENUM(Orientation),则 Orientation 和 OrientationFlags 都能被 QMetaEnum 识别并使用,只不过通常我们只关注 Q_FLAG 包装后的结构体。
这样我们顺便明白了 Qt 官方定义的许多枚举结构都是成对出现的原因,比如

enum Qt::AlignmentFlag
flags Qt::Alignment

enum Qt::MatchFlag
flags Qt::MatchFlags

enum Qt::MouseButton
flags Qt::MouseButtons

再总结下 Q_FLAG 以及 Q_DECLARE_FLAGSQ_DECLARE_OPERATORS_FOR_FLAGS 使用的要点:

  1. Q_DECLARE_FLAGS(Flags, Enum) 宏将普通结构体 Enum 重新定义成了一个可以自由进行与或非操作的安全的结构体 Flags。Q_DECLARE_FLAG 出现在 Enum 定义之后,且定义之后 Enum 和 Flags是同时存在的;
  2. Q_DECLARE_OPERATORS_FOR_FLAGS(Flags) 赋予了Flags一个全局操作符“|”,没有这个宏语句,Flags 量之间进行与操作后的结果将是一个 int 值,而不是 Flags 值。Q_DECLARE_OPERATORS_FOR_FLAGS 应当定义在类外;
  3. Q_DECLARE_OPERATORS_FOR_FLAGS 只提供了“或”操作,没有提供“与”“非”操作;
  4. Q_DECLARE_FLAGS 和 Q_DECLARE_OPERATORS_FOR_FLAGS 都是和元对象系统无关的,可以脱离 Q_FLAG 单独使用,事实上这两个宏在 Qt4 就已经存在(不确定更早是否存在),而Q_FLAG是在 Qt5.5 版本才加入的;
  5. 如果在我们的程序中不需要枚举变量的组合扩展,那么只用简单的 Q_ENUM 就好了。

 

3、Q_NAMESPACE, Q_ENUM_NS 和 Q_FLAG_NS - 命名空间

在 Qt5.8 之后,Qt 引入了 Q_NAMESPACE 宏,这个宏能够让命名空间具备简化的元对象能力,但不支持信号槽(类似类里的 Q_GADGET)。
在使用了 Q_NAMESPACE 的命名空间中,可以使用 Q_ENUM_NS和Q_FLAG_NS,实现类中 Q_ENUM 和 Q_FLAG 的功能。
看一个例子

namespace MyNamespace
{
Q_NAMESPACE
enum Priority
{
    High = 1,
    Low = 2,
    VeryHigh = 4,
    VeryLow = 8,
};
Q_ENUM_NS(Priority)			//如不使用Priority,可省略
Q_DECLARE_FLAGS(Prioritys, Priority)
Q_FLAG_NS(Prioritys)
Q_DECLARE_OPERATORS_FOR_FLAGS(Prioritys)
}

命名空间中 Q_ENUM_NS 和 Q_FLAG_NS 的使用和之前相类似,不再赘述。Q_DECLARE_OPERATORS_FOR_FLAGS 则需要定义在命名空间之内。

使用代码:

using namespace MyNamespace;
qDebug()<<(High|Low);
QMetaEnum m = QMetaEnum::fromType<MyNamespace::Prioritys>();  //since Qt5.5
qDebug()<< "keyToValue:"<<m.keyToValue("High|Low");
qDebug()<< "valueToKey:"<<m.valueToKey(High|Low);

qDebug()<< "keysToValue:"<<m.keysToValue("High|Low");
qDebug()<< "valueToKeys:"<<m.valueToKeys(High|Low)<<endl;

qDebug()<< "isFlag:"<<m.isFlag();
qDebug()<< "name:"<<m.name();
qDebug()<< "enumName:"<<m.enumName();               //since Qt5.12
qDebug()<< "scope:"<<m.scope()<<endl;

运行结果

QFlags<MyNamespace::Priority>(High|Low)
keyToValue: -1
valueToKey: 
keysToValue: 3
valueToKeys: "High|Low" 

isFlag: true
name: Prioritys
enumName: Priority
scope: MyNamespace 

可以看到,从定义到使用,和之前 Q_FLAG 几乎一模一样。

在命名空间中使用 Q_ENUM_NS 或者 Q_FLAG_NS,能让枚举结构体定义不再局限在类里,使我们有更多的选择。另外,在今后Qt的发展中,相信Q_NAMESPACE能带来更多的功能,我们拭目以待。

 

四、总结:

Qt一直是一个发展的框架,不断有新特性加入,使得Qt变得更易用。
本文介绍的内容都是在Qt5.5版本以后才引入的,Q_NAMESPACE的内容甚至要到Qt5.8版本才引入,在之前Qt中也存在着枚举量的扩展封装——主要是Q_ENUMS和Q_FLAGS,这套系统虽然已经不建议使用,但是为了兼容性,还是予以保留

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值