[Qt開發探幽(二)]淺談關於元物件,宏和Q_ENUM

2023-09-01 18:01:31

[Qt開發探幽(二)]淺談關於元物件,宏和Q_ENUM

前言

最近在開發的時候,我自己寫了一套虛擬函式。這也是我第一次寫這麼大一個框架,遇到了一些有點莫名其妙的問題(也不能算莫名奇妙,只能說有點玩不明白),詳情可以見

[Qt開發思想探幽]QObject、模板繼承和多繼承

前兩天我寫了一些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)為物件間通訊、執行時型別資訊和動態屬性系統提供了訊號和槽機制。元物件系統基於三個方面:

  1. QObject類為可以利用元物件系統的物件提供了一個基礎類別。

  2. 類宣告的私有部分中的Q_OBJECT宏用於啟用元物件功能,如動態屬性、訊號和插槽。

  3. 元物件編譯器(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

The Property System

其中有非常詳盡的解釋。

三、關於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做到之前我們想要做的事