[TOC]([Qt開發探幽(二)]淺談關於元對象,巨集和Q_ENUM) # [Qt開發探幽(二)]淺談關於元對象,巨集和Q_ENUM ## 前言 最近在開發的時候,我自己寫了一套虛函數。這也是我第一次寫這麼大一個框架,遇到了一些有點莫名其妙的問題(也不能算莫名奇妙,只能說有點玩不明白),詳情可以見 [[ ...
目錄
[Qt開發探幽(二)]淺談關於元對象,巨集和Q_ENUM
前言
最近在開發的時候,我自己寫了一套虛函數。這也是我第一次寫這麼大一個框架,遇到了一些有點莫名其妙的問題(也不能算莫名奇妙,只能說有點玩不明白),詳情可以見
前兩天我寫了一些demo驗證了一些我的想法,算是在元對象編程里簡單的游了一游。
一、元對象
Qt的元對象是一個讓人又愛又恨的東西。讓人愛是因為它確實功能強大,可以允許我們從類、枚舉類型、獲得一些我們在正常C++開發中可能無法正常獲取到的東西。比如最簡單的:在正常C++開發中,枚舉類型的類型名稱對於C++而言只是一個有一個的十六進位碼,而不是字元串的形式,也不可能獲得字元串,那麼可能就有如下的奇技淫巧:
沒錯,以上就是通過 Qt的元對象類型將一個枚舉類型的成員轉換成字元串,或者將字元串轉回枚舉類型的值
更變態的是什麼?
更變態的是,通過元對象類型我們可以實現一個更誇張的功能:讓一個類和一個Json字元串之間做轉換:
當然了,做轉換的前提是使用Q_PROPERTY巨集包裹著屬性,這樣這個屬性就被註冊進了這個類的元對象系統內,然後就可以通過一些奇技淫巧,來實現類成員變數和字元串之間的轉換了,以下是一個例子:
#pragma region Lev_Json
/// <summary>
/// name:Lev_Json
/// 說明:此類用作輔助參數類與json字元串之間的轉換,使用此類請使用Q_PROPERTY聲明所有的類成員變數
/// </summary>
class Lev_Json : QObject {
public:
template<class T1>
static bool ValidateJsonKeys(const QString& jsonString, const T1* T_Class) {
QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonString.toUtf8());
if (!jsonDoc.isObject()) {
return false;
}
QJsonObject jsonObject = jsonDoc.object();
const QMetaObject* metaObject = T_Class->metaObject();
for (int i = 0; i < metaObject->propertyCount(); ++i) {
QMetaProperty property = metaObject->property(i);
QString propName = property.name();
if (propName.contains("objectName"))
continue;
if (!jsonObject.contains(propName)) {
return false;
}
}
return true;
}
/// <summary>
/// 判斷這個Json字元串對於這個Object而言是否合法
/// </summary>
/// <typeparam name="T1"></typeparam>
/// <param name="jsonString"></param>
/// <returns></returns>
template<class T1>
static bool ValidateJsonKeys(const QString& jsonString, QSharedPointer<T1> T_Class_1) {
QObject* T_Class = dynamic_cast<QObject*>(T_Class_1.data());
QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonString.toUtf8());
if (!jsonDoc.isObject()) {
return false; // Return false if JSON is not an object
}
QJsonObject jsonObject = jsonDoc.object();
const QMetaObject* metaObject = T_Class->metaObject();
for (int i = 0; i < metaObject->propertyCount(); ++i) {
QMetaProperty property = metaObject->property(i);
QString propName = property.name();
if (!jsonObject.contains(propName)) {
return false;
}
}
return true;
}
/// <summary>
/// 推薦,序列化Qt對象,請用Q_PROPERTY包裹成員變數,使用記憶體安全的QSharedPointer
/// </summary>
/// <typeparam name="T1">模板對象,可以不聲明,會自動識別</typeparam>
/// <param name="T_Class_1">輸入的對象</param>
/// <returns></returns>
template<class T1>
static QString JsonSerialization(QSharedPointer<T1> T_Class_1) {
QJsonObject ret;
QObject* T_Class = dynamic_cast<QObject*>(T_Class_1.data());
const QMetaObject* metaObject = T_Class->metaObject();
for (int i = 0; i < metaObject->propertyCount(); ++i) {
QMetaProperty property_ = metaObject->property(i);
QVariant propValue = property_.read(T_Class);
if (!QString(property_.name()).contains("objectName")) {
ret.insert(property_.name(), variantToJsonValue(propValue));
}
}
QJsonDocument jsonDoc(ret);
return jsonDoc.toJson(QJsonDocument::Compact);
}
/// <summary>
/// 推薦,反序列化Qt對象,請用Q_PROPERTY包裹成員變數,會返回一個記憶體安全的QSharedPointer
/// </summary>
/// <typeparam name="T1"></typeparam>
/// <param name="jsonString"></param>
/// <returns></returns>
template<class T1>
static QSharedPointer<T1> JsonDeserialization(const QString& jsonString) {
QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonString.toUtf8());
if (!jsonDoc.isObject()) {
return QSharedPointer<T1>();
}
QJsonObject jsonObject = jsonDoc.object();
QSharedPointer<T1> result = QSharedPointer<T1>::create();
const QMetaObject* metaObject = result->metaObject();
for (int i = 0; i < metaObject->propertyCount(); ++i) {
QMetaProperty property = metaObject->property(i);
QString propName = property.name();
if (jsonObject.contains(propName)) {
QJsonValue propJsonValue = jsonObject[propName];
QVariant propValue = jsonValueToVariant(propJsonValue, property.userType());
if (propValue.isValid()) {
property.write(result.data(), propValue);
}
}
}
return result;
}
/// <summary>
/// 可以用,序列化Qt對象,請用Q_PROPERTY包裹成員變數
/// </summary>
/// <typeparam name="T1">模板對象,可以不聲明,會自動識別</typeparam>
/// <param name="T_Class_1">輸入的對象</param>
/// <returns></returns>
template<class T1>
static QString JsonSerialization(const T1* T_Class) {
QJsonObject ret;
const QMetaObject* metaObject = T_Class->metaObject();
for (int i = 0; i < metaObject->propertyCount(); ++i) {
QMetaProperty property_ = metaObject->property(i);
QVariant propValue = property_.read(T_Class);
if (!QString(property_.name()).contains("objectName")) {
ret.insert(property_.name(), variantToJsonValue(propValue));
}
}
QJsonDocument jsonDoc(ret);
return jsonDoc.toJson(QJsonDocument::Compact);
}
/// <summary>
/// 不推薦使用,不安全的記憶體方案
/// </summary>
/// <typeparam name="T1"></typeparam>
/// <param name="result"></param>
/// <param name="jsonString"></param>
/// <returns></returns>
template<class T1>
static QSharedPointer<T1> JsonDeserialization(T1* result, const QString& jsonString) {
QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonString.toUtf8());
if (!jsonDoc.isObject()) {
return QSharedPointer<T1>();
}
QJsonObject jsonObject = jsonDoc.object();
const QMetaObject* metaObject = result->metaObject();
for (int i = 0; i < metaObject->propertyCount(); ++i) {
QMetaProperty property = metaObject->property(i);
QString propName = property.name();
if (jsonObject.contains(propName)) {
QJsonValue propJsonValue = jsonObject[propName];
QVariant propValue = jsonValueToVariant(propJsonValue, property.userType());
if (propValue.isValid()) {
property.write(result.data(), propValue);
}
}
}
return result;
}
private:
static QJsonValue variantToJsonValue(const QVariant& variant) {
if (variant.canConvert<QString>()) {
return QJsonValue::fromVariant(variant.toString());
}
else if (variant.canConvert<int>()) {
return QJsonValue::fromVariant(variant.toInt());
}
else if (variant.canConvert<double>()) {
return QJsonValue::fromVariant(variant.toDouble());
}
else if (variant.canConvert<bool>()) {
return QJsonValue::fromVariant(variant.toBool());
}
else if (variant.userType() == qMetaTypeId<QList<int>>()) {
return listToJsonArray<int>(variant.value<QList<int>>());
}
else if (variant.userType() == qMetaTypeId<QList<QString>>()) {
return listToJsonArray<QString>(variant.value<QList<QString>>());
}
else if (variant.userType() == qMetaTypeId<QList<bool>>()) {
return listToJsonArray<bool>(variant.value<QList<bool>>());
}
return QJsonValue::Null;
}
template<typename T>
static QJsonArray listToJsonArray(const QList<T>& list) {
QJsonArray jsonArray;
for (const T& value : list) {
jsonArray.append(QJsonValue::fromVariant(value));
}
return jsonArray;
}
static QVariant jsonValueToVariant(const QJsonValue& jsonValue, int userType) {
QVariant result;
if (jsonValue.isString()) {
result = jsonValue.toString();
}
else if (jsonValue.isDouble()) {
if (userType == QMetaType::Int) {
result = jsonValue.toInt();
}
else if (userType == QMetaType::Double) {
result = jsonValue.toDouble();
}
}
else if (jsonValue.isBool()) {
if (userType == QMetaType::Bool) {
result = jsonValue.toBool();
}
}
else if (jsonValue.isArray()) {
QJsonArray jsonArray = jsonValue.toArray();
if (userType == qMetaTypeId<QList<int>>()) {
QList<int> intList;
for (const QJsonValue& element : jsonArray) {
intList.append(element.toInt());
}
result = QVariant::fromValue(intList);
}
else if (userType == qMetaTypeId<QList<QString>>()) {
QList<QString> stringList;
for (const QJsonValue& element : jsonArray) {
stringList.append(element.toString());
}
result = QVariant::fromValue(stringList);
}
// Add more cases for other QList types if needed
}
return result;
}
};
#pragma endregion
當然了,Qt的元對象類型還有很多很強大的功能,比如對象名稱等等,各種各樣的功能,可以拿著Qt當C#來用了(笑)
但是
Qt的元對象類型也有很多局限性。正如我在前言中提到的,正因為Q_OBJECT巨集的存在,QObject的對象是不能使用模板類繼承的,也不能使用模板類多繼承。這個實際上相當限制了Qt程式員的開發能力。模板類作為功能非常強大的一個功能,也正是C++能如此蓬勃發展的一個重要原因,結果在Qt上用不了,這是令人扼腕嘆息的。
另外,值得一提的是,我們可以看到,在自己寫繼承的時候,從一個繼承了QObject類和聲明瞭Q_OBJECT巨集的類中繼承下來的子類仍然帶有Q_OBJECT巨集 這件事經常會通不過編譯,我不知道自己是觸犯了哪個規則,但是之後我的底層框架中最底層的部分都不會使用Q_OBJECT巨集,直到我搞懂這件事,因為真的為了這個問題做了太多的妥協了。
二、關於Q_OBJECT等巨集屬性
如果要聊這個巨集,我們得看一下這個巨集做了什麼,找到Qt Document:
Q_OBJECT巨集必須出現在類定義的私有部分中,該類定義聲明自己的信號和槽,或者使用Qt的元對象系統提供的其他服務。
#include <QObject>
class Counter : public QObject
{
Q_OBJECT
public:
Counter() { m_value = 0; }
int value() const { return m_value; }
public slots:
void setValue(int value);
signals:
void valueChanged(int newValue);
private:
int m_value;
};
註意:這個巨集要求類是QObject的子類。使用Q_GADGET或Q_GADGET_EXPORT而不是Q_OBJECT來啟用元對象系統對非QObject子類中的枚舉的支持。
Q_OBJECT巨集我們可以看到,主要是做了三件事:
1.將指定的類註冊進入到元對象系統內,至於什麼是元對象系統,我們接下來會說,你先知道是註冊進元對象系統就行了
2.添加信號與槽函數的註冊
3.註冊Qt的屬性系統
這三個功能其實也構成了Qt這套框架的全部,可以說Qt整套系統都是圍繞著Q_OBJECT巨集來做的。
1.元對象系統
元對象系統
Qt的元對象系統(Meta-Object System)為對象間通信、運行時類型信息和動態屬性系統提供了信號和槽機制。元對象系統基於三個方面:
-
QObject類為可以利用元對象系統的對象提供了一個基類。
-
類聲明的私有部分中的Q_OBJECT巨集用於啟用元對象功能,如動態屬性、信號和插槽。
-
元對象編譯器(moc)為每個QObject子類提供實現元對象特性所需的代碼。
我們可以理解為,元對象系統就是Qt的一個“C#化”的嘗試,即將原來在C++中不可見的一切
moc工具讀取一個C++源文件。如果它找到一個或多個包含Q_OBJECT巨集的類聲明,它將生成另一個C++源文件,該文件包含每個類的元對象代碼。這個生成的源文件要麼被#包含到類的源文件中,要麼更常見的是,被編譯並鏈接到類的實現中。
除了提供用於對象之間通信的信號和槽機制(引入該系統的主要原因)之外,元對象代碼還提供以下附加功能:
-
QObject::metaObject()返回類的關聯元對象。
-
QMetaObject::className()在運行時以字元串形式返回類名,而不需要通過C++編譯器支持本機運行時類型信息(RTTI)。
-
函數返回對象是否是繼承QObject繼承樹中指定類的類的實例。
-
QObject::tr()轉換字元串以進行國際化。
-
QObject::setProperty()和QOobject::property()按名稱動態設置和獲取屬性。
-
QMetaObject::newInstance()構造類的一個新實例。
還可以使用qobject_cast()對qobject類執行動態強制轉換。qobject_cast()函數的行為類似於標準C++dynamic_cast(),其優點是不需要RTTI支持,並且可以跨動態庫邊界工作。它嘗試將其參數強制轉換為尖括弧中指定的指針類型,如果對象的類型正確(在運行時確定),則返回非零指針,如果對象類型不相容,則返回nullptr。
雖然可以在沒有Q_OBJECT巨集和元對象代碼的情況下使用QObject作為基類,但如果不使用Q_OBJECT巨集,則信號和插槽以及此處描述的其他功能都不可用。
從元對象系統的角度來看,一個沒有元代碼的QObject子類等價於它最接近的有元對象代碼的祖先。
這意味著,例如,QMetaObject::className()不會返回類的實際名稱,而是返回該祖先的類名。
因此,我們強烈建議QObject的所有子類使用Q_OBJECT巨集,無論它們是否實際使用信號、槽和屬性。
2.信號與槽
在Qt中的信號與槽可以說是Qt的頭牌系統,也是Qt這套東西能夠如此流行的重要原因,也是整個Qt框架最重要的基石。
當然了,其實自己實現一套Qt的Signal - Slot的系統其實並不複雜,而且肯定很多人已經能開發一套類似的東西了。比如我簡單打個樣:
class Caller {
public:
using CallMethod = void(*)(const QString& sModule, const QString& sDescribe, const QString& sVariable, const QVariant& extra);
using SendCMD = void(*)(const QString& sModule, const QString& sDescribe, const QString& sVariable, const QVariant& extra);
void RegisterCallMethod(CallMethod callback) {
callbacks_.append(callback);
}
void RegisterSendCMD(SendCMD callback) {
sendcmds_.append(callback);
}
void Signal_CallMethod(const QString& sModule, const QString& sDescribe, const QString& sVariable, const QVariant& extra) {
for (CallMethod callback : callbacks_) {
if (callback) {
callback(sModule, sDescribe, sVariable, extra);
}
}
}
void Signal_SendCMD(const QString& sModule, const QString& sDescribe, const QString& sVariable, const QVariant& extra) {
for (SendCMD callback : callbacks_) {
if (callback) {
callback(sModule, sDescribe, sVariable, extra);
}
}
}
private:
QList<CallMethod> callbacks_;
QList<SendCMD> sendcmds_;
};
但是Qt的signal - slot 強大的地方就在於它的封裝性和靈活性,各種註銷註冊操作相對自己寫回調函數還是簡單很多很多的。你想啊,原先需要這麼多代碼的地方,現在只需要一個巨集,或者一句話,難易程度幾乎無法比較。
由於Qt獨特的signal索引機制,導致其網路相關的庫效率可能是C++回調函數的百分之一,這是非常誇張的性能損失,但是這在某些性能不關鍵的場景仍然是可以接受的。
Signals & Slots
Signals 和Slots用於對象之間的通信。Signals 和Slots機制是Qt的一個核心功能,可能也是與其他框架提供的功能最不同的部分。Qt的元對象系統使Signals 和Slots成為可能。
其他工具包使用回調來實現這種通信。回調是指向函數的指針,因此,如果您希望處理函數通知您某個事件,您可以將指向另一個函數的指針(回調)傳遞給處理函數。然後,處理函數在適當的時候調用回調。雖然使用這種方法的成功框架確實存在,但回調可能是不直觀的,並且在確保回調參數的類型正確性方面可能會遇到問題。
在Qt中,我們有一種替代回調技術的方法:我們使用Signals 和Slots。當特定事件發生時,會發出一個信號。Qt的小部件有許多預定義的Signals ,但我們總是可以對小部件進行子類化,以向它們添加我們自己的Signals。Slots是響應特定信號而調用的函數。Qt的小部件有許多預定義的Slots,但通常的做法是對小部件進行子類化,並添加自己的Slots,以便處理您感興趣的Signals。
Signal和Slot機制是類型安全的:Signal的簽名必須與接收Slot的簽名匹配。(事實上,Slot的簽名可能比它接收到的Signal更短,因為它可以忽略額外的參數。)
由於簽名是相容的,編譯器可以在使用基於函數指針的語法時幫助我們檢測類型不匹配。基於字元串的SIGNAL和SLOT語法將在運行時檢測類型不匹配。
Signal和Slot是鬆散耦合的:發出Signal的類既不知道也不關心哪個Slot接收Signal。Qt的Signal和Slot機制確保,如果您將Signal連接到Slot,Slot將在正確的時間使用Signal的參數進行調用。Signal和Slot可以採用任何類型的任意數量的參數。
它們是完全類型安全的。 所有繼承自QObject或其子類之一(例如,QWidget)的類都可以包含Signal和Slot。當對象以其他對象可能感興趣的方式改變其狀態時,它們會發出Signal。
這就是對象所做的所有通信。它不知道或不關心是否有任何東西正在接收它發出的Signal。這是真正的信息封裝,並確保對象可以用作軟體組件。
Slot可以用於接收Signal,但它們也是正常的成員功能。就像一個對象不知道是否有任何東西接收到它的Signal一樣,一個Slot也不知道它是否有任何Signal連接到它。這確保了可以用Qt創建真正獨立的組件。
您可以將任意數量的Signal連接到單個Slot,也可以將Signal連接到任意數量的Slot。甚至可以將一個Signal直接連接到另一個Signal。(無論何時發出第一個Signal,都會立即發出第二個Signal。)
Signal和Slot共同構成了一個強大的組件編程機制。 Signal 當對象的內部狀態以某種可能對對象的客戶端或所有者感興趣的方式發生變化時,對象會發出Signal。Signal是公共訪問函數,可以從任何地方發出,但我們建議只從定義Signal及其子類的類發出Signal。
當一個Signal發出時,連接到它的Slot通常會立即執行,就像正常的函數調用一樣。當這種情況發生時,Signal和Slot機制完全獨立於任何GUI事件迴圈。一旦所有Slot都返回,就會執行emit語句後面的代碼。使用排隊連接時,情況略有不同;
在這種情況下,emit關鍵字後面的代碼將立即繼續,稍後將執行Slot。 如果多個Slot連接到一個Signal,則當Signal發出時,這些Slot將按照連接的順序依次執行。 Signal由moc自動生成,不得在.cpp文件中實現。它們永遠不能有返回類型(即使用void)。
關於arguments的註意事項:我們的經驗表明,如果Signal和Slot不使用特殊類型,它們將更易於重用。如果QScrollBar::valueChanged()使用一種特殊類型,如假設的QScrollBar::Range,則它只能連接到專門為QScrollBar設計的Slot。
將不同的輸入小部件連接在一起是不可能的。 Slot 當連接到Slot的Signal發出時,就會調用該Slot。Slot是正常的C++函數,可以正常調用;它們唯一的特點是Signal可以連接到它們。 由於Slot是正常的成員函數,因此當直接調用時,它們遵循正常的C++規則。
但是,作為Slot,它們可以由任何組件通過SignalSlot連接調用,而不管其訪問級別如何。這意味著,從任意類的實例發出的Signal可以導致在不相關類的實例中調用專用Slot。 您還可以將Slot定義為虛擬Slot,我們發現這在實踐中非常有用。
與回調相比,Signal和Slot的速度稍慢,因為它們提供了更大的靈活性,儘管實際應用程式的差異並不顯著。通常,發射連接到某些Slot的Signal比直接調用接收器(使用非虛擬函數調用)慢大約十倍。這是定位連接對象、安全地迭代所有連接(即檢查後續接收器在發射過程中是否未被破壞)以及以通用方式整理任何參數所需的開銷。雖然十個非虛擬函數調用聽起來可能很多,但它的開銷比任何新操作或刪除操作都要小得多。
一旦執行了一個字元串、向量或列表操作,而該操作在後臺需要新建或刪除,則Signal和Slot開銷只占整個函數調用成本的一小部分。無論何時進行系統調用都是如此
3.屬性系統
Qt提供了一個複雜的屬性系統,類似於一些編譯器供應商提供的屬性系統。然而,作為一個獨立於編譯器和平臺的庫,Qt不依賴於__property或[property]等非標準編譯器功能。Qt解決方案可與Qt支持的每個平臺上的任何標準C++編譯器配合使用。它基於元對象系統,該系統還通過信號和插槽提供對象間通信。
他其實更像是C#中的一個get set方法,相當於是將這個屬性註冊到元對象系統中去,並且給每個對象提供了一個get set方法(當然了,get set方法也只是你定義的,這又不是真的c#)
具體的屬性系統這裡我不做過多介紹,詳情可以參考Qt Document
其中有非常詳盡的解釋。
三、關於Q_ENUMS
Q_ENUM這個巨集經過了幾次修改,早期貌似可以隨意註冊Q_ENUMS,但是在後續貌似只剩下了兩種枚舉類型的註冊方法:
一個是在類內聲明枚舉類型,然後在類內聲明這個Q_ENUM,當然了,用這個巨集去註冊枚舉類型的前提是使用了Q_OBJECT巨集
現在假設我們想在元對象系統中使用這個枚舉類,也就是我想通過它的int值獲得其映射的key(字元串形式),比如如下這個枚舉類型
test_enum::Test_Enum_1 tester = test_enum::Test_Enum_1::none;
我現在可能是傳遞Json字元串,或者是別的什麼,反正我就是要獲得none這個關鍵字,那我該怎麼做?
這個時候你有兩個做法,但是實際上都是將其註冊到元對象
1.將其註冊到Q_NAMESPACE下
啟用一個單獨的namespace,通過Q_NAMESPACE巨集的形式將這個命名空間註冊到Qt的元對象系統內,舉個例子:
namespace test_enum {
Q_NAMESPACE //Q_NAMESPACE巨集將整個命名空間註冊進元對象列表中去
enum class Test_Enum_1 {
none,
open,
close,
stop
};
Q_ENUM_NS(Test_Enum_1) //Q_ENUM_NS巨集將我們需要的枚舉類型對象註冊進
}
2.類內註冊
除此之外,還有另一種方法,那就是將枚舉類型寫入到用Q_OBJECT, Q_GADGET or Q_GADGET_EXPORT這三個巨集之一標記的類內
需要註意的一點:Q_GADGET是Q_OBJECT巨集的輕量化版本,用Q_GADGET意味著這個類不一定需要繼承QObject類了
適用於不繼承QObject但仍希望使用QMetaObject提供的一些反射功能的類。就像Q_OBJECT巨集一樣,它必須出現在類定義的私有部分中。
Q_GADGET可以有Q_ENUM、Q_PROPERTY和Q_INVOKABLE,但不能有信號或插槽。
Q_GADGET使類成員staticMetaObject可用。staticMetaObject的類型為QMetaObject,並提供對用Q_ENUM聲明的枚舉的訪問。
如以下代碼:
class TSG_Device : public TSG_Caller {
/// <summary>
/// 設備狀態
/// </summary>
public:
enum class DeviceState
{
DS_None,
DS_Unknown,
DS_Disconnected,
DS_Connected,
DS_Working,
DS_Pause,
DS_Stop
}; Q_ENUM(DeviceState)
enum class DeviceOpen {
DO_Open,
DO_Close
}; Q_ENUM(DeviceOpen)
}
這樣一個內嵌的枚舉類,也可以用QMetaEnum做到之前我們想要做的事