前言
之前做一個比較大工程,核心數據里面有很多是枚舉變量,需要頻繁地使用枚舉量到字符串和字符串到枚舉量的操作,為了實現這些操作,我把每個枚舉類型后面都附加了兩個類似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,先看代碼:
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();
輸出是
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程序員喜歡用舊版本編程,但是我是堅定的新版本擁躉,在給程序編寫帶來便利的同時,還能滿足自己的好奇心,何樂而不為呢?