Qt中的枚举变量,Q_ENUM,Q_FLAG,Q_NAMESPACE,Q_ENUM_NS,Q_FLAG_NS以及其他

Qt中的枚举变量,Q_ENUM,Q_FLAG,Q_NAMESPACE,Q_ENUM_NS,Q_FLAG_NS以及其他

前言
之前做一个比较大工程,核心数据里面有很多是枚举变量,需要频繁地使用枚举量到字符串和字符串到枚举量的操作,为了实现这些操作,我把每个枚举类型后面都附加了两个类似Enum_to_String()和String_to_Enum()的函数,程序显得很臃肿。这时候就非常羡慕C#或者java等兄弟语言,内核内置了枚举量和字符串转换的方法。
最近读Qt文档时偶然间发现,Qt内核其实已经提供了这个转换机制,使得我们能用很少的代码完成枚举量和字符串的转换,甚至还能实现其他更酷更强大的功能,下面我们就来看看如何使用Qt的这个功能。
简单来讲,Qt还是使用一组宏命令来完成枚举量扩展功能的(正如Qt的其他核心机制一样),这些宏包括Q_ENUM,Q_FLAG,Q_ENUM_NS,Q_FLAG_NS,Q_DECLARE_FLAGS,Q_DECLARE_OPERATORS_FOR_FLAGS,
这些宏的实现原理和如何展开如何注册到Qt内核均不在本文的讲解范围,本文只讲应用。

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();
qDebug() << m.scope() << m.name();

输出结果如下:

注意:如果没有用Q_ENUM(Priority),则会报如下错误:

且此时qDebug() << MyEnum::High << MyEnum::Low;      输出的是枚举类型表示的数值即1和2而不是字符串。  m.scope()返回枚举所在类名,本类中的枚举在MyEnum类的内部定义,所以返回的是MyEnum,m.name()返回是枚举名,本例的枚举名是Priority,如果将枚举定义放到内的外部,则m.scope()返回空。另外对于Qt的Qt's Property System、Meta-Object System默认是不支持枚举类型的,即如下代码:

Q_PROPERTY(EnFlowOrient floworient READ flowOrient WRITE setFlowOrient STORED true DESIGNABLE true)

中的EnFlowOrient枚举定义如下:


	// 油液流动的方向
	enum class EnFlowOrient
	{
		eRightOrDownFlow,  
		eLeftOrUpFlow,     
	};

 默认是不支持的,要使其支持枚举,也必须用用Q_ENUM(EnFlowOrient);声明且枚举必须放在类内部声明且必须是public才行。

可见,使用Q_ENUM注册过的枚举类型,可以不加修饰直接被qDebug()打印出来,另外通过静态函数QMetaEnum::fromType()可以获得一个QMetaEnum 对象,以此作为中介,能够轻松完成枚举量和字符串之间的相互转化。这一点恐怕是引入Q_ENUM机制最直接的好处。
除此以外,QMetaEnum还提供了一个内部的索引,从1开始给每个枚举量按自然数顺序编号(注意和枚举量本身的数值是两回事),提供了int value(int index) 和const char *key(int index)
两个便捷函数分别返回枚举量对应的数值和枚举量对应的字符串,配合keyCount() 函数可以实现枚举量的遍历:
 

qDebug()<<m.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

其中name()函数返回枚举类型名字。
Q_ENUM使用起来很很简单,对不对?但是还是有几个注意事项需要说明:

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

Q_FLAG的引入解决什么问题?

除了Q_ENUM,Qt中还有另一个类似的宏——Q_FLAG,着力弥补C++中结构体无法组合使用,和缺乏类型检查的缺点,怎么理解呢?我们看一个例子:
在经典C++中,如果我们要定义一个表示方位的结构体:

enum Orientation
{ 
Up = 1, 
Down = 2, 
Left = 4, 
Right = 8
};

注意这里枚举值被定义成等比数列,这个技巧给使用"|“操作符扩展留下了空间,比如,左上可以用Up | Left来简单表示,但是这里也带来了一个问题,Up | Left值是一个整型,并不在枚举结构Orientation中,如果函数使用Orientation作为自变量,编译器是无法通过的,为此往往把函数自变量类型改为整型,但因此也就丢掉了类型检查,输入量有可能是其他无意义的整型量,在运行时带来错误。
Qt引入QFlags类,配合一组宏命令完美地解决了这个问题。
QFlags是一个简单的类,可以装入一个枚举量,并重载了与或非等运算符,使得枚举量能进行与或非运算,且运算结果还是一个QFlags包装的枚举量。一个普通的枚举类型包装成QFlags型,需要使用Q_DECLARE_FLAGS宏,在全局任意地方使用”|"操作符计算自定义的枚举量,需要使用Q_DECLARE_OPERATORS_FOR_FLAGS宏。
再看一段代码:
 

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)
    Q_FLAG(OrientationFlags)
};

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_FLAGS、Q_DECLARE_OPERATORS_FOR_FLAGS使用的要点吧:

Q_DECLARE_FLAGS(Flags, Enum)宏将普通结构体Enum重新定义成了一个可以自由进行与或非操作的安全的结构体Flags。Q_DECLARE_FLAG出现在Enum定义之后,且定义之后Enum和Flags是同时存在的;
Q_DECLARE_OPERATORS_FOR_FLAGS(Flags)赋予了Flags一个全局操作符“|”,没有这个宏语句,Flags量之间进行与操作后的结果将是一个int值,而不是Flags值。Q_DECLARE_OPERATORS_FOR_FLAGS应当定义在类外;
Q_DECLARE_OPERATORS_FOR_FLAGS只提供了“或”操作,没有提供“与”“非”操作;
Q_DECLARE_FLAGS和Q_DECLARE_OPERATORS_FOR_FLAGS都是和元对象系统无关的,可以脱离Q_FLAG单独使用,事实上这两个宏在Qt4就已经存在(不确定更早是否存在),而Q_FLAG是在Qt5.5版本才加入的;
如果在我们的程序中不需要枚举变量的组合扩展,那么只用简单的Q_ENUM就好了。
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,这套系统虽然已经不建议使用,但是为了兼容性,还是予以保留。我们看看之前的系统如何使用的:
枚举量定义基本一致,就是Q_ENUMS(Enum)宏放到定义之前,代码从略。
使用上:

QMetaObject object = MyEnum::staticMetaObject;				//before Qt5.5
int index = object.indexOfEnumerator("Orientation");
QMetaEnum m = object.enumerator(index);

对比改进后的:

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

不仅仅是3行代码简化成1行,更重要的是Qt程序员终于不用再显式调用元对象QMetaObject了。改进的代码元对象机制同样在起着作用,但却变得更加隐蔽,更加沉默,使得程序员可以把精力更多放到功能的实现上,这大概就是Qt发展的方向。

结语
很多Qt程序员喜欢用旧版本编程,但是我是坚定的新版本拥趸,在给程序编写带来便利的同时,还能满足自己的好奇心,何乐而不为呢?
 
原文链接:https://blog.csdn.net/qq_36179504/article/details/100895133

  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值