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,先看代码:
1 #include <QObject>
2
3 class MyEnum : public QObject
4 {
5 Q_OBJECT
6 public:
7 explicit MyEnum(QObject *parent = nullptr);
8
9 enum Priority
10 {
11 High = 1,
12 Low = 2,
13 VeryHigh = 3,
14 VeryLow = 4
15 };
16 Q_ENUM(Priority)
17 };
这就是在类中定义了一个普通的枚举变量之后,额外加入了Q_ENUM(枚举类型名)这样的一个宏语句,那么加入了这个Qt引入的宏语句后,我们能得到什么收益呢?
1 qDebug()<< MyEnum::High<< MyEnum::Low; //qDebug()可以直接打印出枚举类值的字符串名称
2 QMetaEnum m = QMetaEnum::fromType<MyEnum::Priority>(); //since Qt5.5
3 qDebug()<< "keyToValue:"<< m.keyToValue("VeryHigh");
4 qDebug()<< "valueToKey:"<< m.valueToKey(MyEnum::VeryHigh);
5 qDebug()<< "keyCount:"<< m.keyCount();
本文福利,莬费领取Qt开发学习资料包、技术视频,内容包括(C++语言基础,Qt编程入门,QT信号与槽机制,QT界面开发-图像绘制,QT网络,QT数据库编程,QT项目实战,QT嵌入式开发,Quick模块,面试题等等)↓↓↓↓↓↓见下面↓↓文章底部点击莬费领取↓↓
输出是
1 MyEnum::High MyEnum::Low
2 keyToValue: 4
3 valueToKey: VeryHigh
4 keyCount: 4
可见,使用Q_ENUM注册过的枚举类型,可以不加修饰直接被qDebug()打印出来,另外通过静态函数QMetaEnum::fromType()可以获得一个QMetaEnum 对象,以此作为中介,能够轻松完成枚举量和字符串之间的相互转化。这一点恐怕是引入Q_ENUM机制最直接的好处。
除此以外,QMetaEnum还提供了一个内部的索引,从1开始给每个枚举量按自然数顺序编号(注意和枚举量本身的数值是两回事),提供了int value(int index) 和const char *key(int index)两个便捷函数分别返回枚举量对应的数值和枚举量对应的字符串,配合keyCount() 函数可以实现枚举量的遍历:
1 qDebug()<<m.name()<<":";
2 for (int i = 0; i < m.keyCount(); ++i) {
3 qDebug()<<m.key(i)<<m.value(i);
4 }
输出
1 Priority :
2 High 1
3 Low 2
4 VeryHigh 4
5 VeryLow 8
其中name()函数返回枚举类型名字。
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被弃用的原因)。
Q_FLAG的引入解决什么问题?
除了Q_ENUM,Qt中还有另一个类似的宏——Q_FLAG,着力弥补C++中结构体无法组合使用,和缺乏类型检查的缺点,怎么理解呢?我们看一个例子:
在经典C++中,如果我们要定义一个表示方位的结构体:
1 enum Orientation
2 {
3 Up = 1,
4 Down = 2,
5 Left = 4,
6 Right = 8
7 };
注意这里枚举值被定义成等比数列,这个技巧给使用"|“操作符扩展留下了空间,比如,左上可以用Up | Left来简单表示,但是这里也带来了一个问题,Up | Left值是一个整型,并不在枚举结构Orientation中,如果函数使用Orientation作为自变量,编译器是无法通过的,为此往往把函数自变量类型改为整型,但因此也就丢掉了类型检查,输入量有可能是其他无意义的整型量,在运行时带来错误。
Qt引入QFlags类,配合一组宏命令完美地解决了这个问题。
QFlags是一个简单的类,可以装入一个枚举量,并重载了与或非等运算符,使得枚举量能进行与或非运算,且运算结果还是一个QFlags包装的枚举量。一个普通的枚举类型包装成QFlags型,需要使用Q_DECLARE_FLAGS宏,在全局任意地方使用”|"操作符计算自定义的枚举量,需要使用Q_DECLARE_OPERATORS_FOR_FLAGS宏。
再看一段代码:
1 class MyEnum : public QObject
2 {
3 Q_OBJECT
4 public:
5 explicit MyEnum(QObject *parent = nullptr);
6
7 enum Orientation
8 {
9 Up = 1,
10 Down = 2,
11 Left = 4,
12 Right = 8,
13 };
14 Q_ENUM(Orientation) //如不使用Orientation,可省略
15 Q_DECLARE_FLAGS(OrientationFlags, Orientation)
16 Q_FLAG(OrientationFlags)
17 };
18
19 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()调用。
看一下使用代码:
1 qDebug()<<(MyEnum::Up|MyEnum::Down);
2 QMetaEnum m = QMetaEnum::fromType<MyEnum::OrientationFlags>(); //since Qt5.5
3 qDebug()<< "keyToValue:"<<m.keyToValue("Up|Down");
4 qDebug()<< "valueToKey:"<<m.valueToKey(Up|Down);
5 qDebug()<< "keysToValue:"<<m.keysToValue("Up|Down");
6 qDebug()<< "valueToKeys:"<<m.valueToKeys(Up|Down)<<endl;
7
8 qDebug()<< "isFlag:"<<m.isFlag();
9 qDebug()<< "name:"<<m.name();
10 qDebug()<< "enumName:"<<m.enumName(); //since Qt5.12
11 qDebug()<< "scope:"<<m.scope()<<endl;
执行结果
1 QFlags<MyEnum::Orientation>(Up|Down)
2 keyToValue: -1
3 valueToKey:
4 keysToValue: 3
5 valueToKeys: "Up|Down"
6
7 isFlag: true
8 name: OrientationFlags
9 enumName: Orientation
10 scope: MyEnum
可以看到,经过Q_FLAG包装之后,QMetaEnum具有了操作复合枚举量的能力,注意这时应当使用keysToValue()和valueToKeys()函数,取代之前的keyToValue()和valueToKey()函数。另外,isFlag()函数返回值变成了true,name()和enumName()分别返回Q_FLAG包装后和包装前的结构名。
实际上此时类中是存在两个结构体的,如果在定义时加上了Q_ENUM(Orientation),则Orientation和OrientationFlags都能被QMetaEnum识别并使用,只不过通常我们只关注Q_FLAG包装后的结构体。
这样我们顺便明白了Qt官方定义的许多枚举结构都是成对出现的原因,比如
1 enum Qt::AlignmentFlag
2 flags Qt::Alignment
1 enum Qt::MatchFlag
2 flags Qt::MatchFlags
1 enum Qt::MouseButton
2 flags Qt::MouseButtons
再总结下Q_FLAG以及Q_DECLARE_FLAGS、Q_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就好了。
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的功能。
看一个例子:
1 namespace MyNamespace
2 {
3 Q_NAMESPACE
4 enum Priority
5 {
6 High = 1,
7 Low = 2,
8 VeryHigh = 4,
9 VeryLow = 8,
10 };
11 Q_ENUM_NS(Priority) //如不使用Priority,可省略
12 Q_DECLARE_FLAGS(Prioritys, Priority)
13 Q_FLAG_NS(Prioritys)
14 Q_DECLARE_OPERATORS_FOR_FLAGS(Prioritys)
15 }
命名空间中Q_ENUM_NS和Q_FLAG_NS的使用和之前相类似,不再赘述。Q_DECLARE_OPERATORS_FOR_FLAGS则需要定义在命名空间之内。
使用代码:
1 using namespace MyNamespace;
2 qDebug()<<(High|Low);
3 QMetaEnum m = QMetaEnum::fromType<MyNamespace::Prioritys>(); //since Qt5.5
4 qDebug()<< "keyToValue:"<<m.keyToValue("High|Low");
5 qDebug()<< "valueToKey:"<<m.valueToKey(High|Low);
6
7 qDebug()<< "keysToValue:"<<m.keysToValue("High|Low");
8 qDebug()<< "valueToKeys:"<<m.valueToKeys(High|Low)<<endl;
9
10 qDebug()<< "isFlag:"<<m.isFlag();
11 qDebug()<< "name:"<<m.name();
12 qDebug()<< "enumName:"<<m.enumName(); //since Qt5.12
13 qDebug()<< "scope:"<<m.scope()<<endl;
运行结果
1 QFlags<MyNamespace::Priority>(High|Low)
2 keyToValue: -1
3 valueToKey:
4 keysToValue: 3
5 valueToKeys: "High|Low"
6
7 isFlag: true
8 name: Prioritys
9 enumName: Priority
10 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)宏放到定义之前,代码从略。
使用上:
1 QMetaObject object = MyEnum::staticMetaObject; //before Qt5.5
2 int index = object.indexOfEnumerator("Orientation");
3 QMetaEnum m = object.enumerator(index);
对比改进后的
QMetaEnum m = QMetaEnum::fromType<MyEnum::Orientation>(); //since Qt5.5
不仅仅是3行代码简化成1行,更重要的是Qt程序员终于不用再显式调用元对象QMetaObject了。改进的代码元对象机制同样在起着作用,但却变得更加隐蔽,更加沉默,使得程序员可以把精力更多放到功能的实现上,这大概就是Qt发展的方向。
本文福利,莬费领取Qt开发学习资料包、技术视频,内容包括(C++语言基础,Qt编程入门,QT信号与槽机制,QT界面开发-图像绘制,QT网络,QT数据库编程,QT项目实战,QT嵌入式开发,Quick模块,面试题等等)↓↓↓↓↓↓见下面↓↓文章底部点击莬费领取↓↓