目錄
1.為什么需要元系統
2.元數據
3.模擬元對象系統
3.1.元對象聲明
3.2.對C++擴展
3.3初始化元對象
3.4.使用元對象
4.QT的元系統
4.1.元對象系統基于QObject類、Q_OBJECT宏、元對象編譯器MOC實現
4.2.元對象系統的功能
4.3.Q_PROPERTY()的使用
4.4.Q_INVOKABLE使用
1.為什么需要元系統
????????Qt 作為跨平臺的GUI框架,在實際項目中應用廣泛,在日常的使用中,隨手使用的一些機制(如著名的信號槽機制),屬性(如Property系統),以及重載各種事件函數來完成定制化;還有qml中直接訪問QObject的Property。
? ? ? ? 在Qt項目中,可以直接通過類名創建對象:
class MyClass : public QObject {Q_OBJECTQ_PROPERTY(int age READ age WRITE setAge NOTIFY ageChanged)
public:MyClass(QObject *parent = nullptr) : QObject(parent) {}void myMethod() { qDebug() << "Hello, world!"; }
};//根據類名創建對象
const QMetaObject *metaObject = MyClass::staticMetaObject();
QObject *myObject = metaObject->newInstance(Q_ARG(QObject*, nullptr));
? ? ? ? 可以函數名直接訪問類的方法:
QMetaObject::invokeMethod(myObject, "myMethod");
????????運行時增加屬性,在運行時根據當前的上下文為一個對象增加或者刪除屬性,并且要做到在其他地方使用的時候無感——就像這個屬性原來就聲明在類中一樣:
MyClass obj;
obj.setProperty("age", 10); //固定屬性,事先聲明定義好的
obj.setProperty("name", 110); //動態屬性
等等,有了元系統,使得在掌握很少類內部信息都能完成類數據的獲取和方法的調用。
2.元數據
元數據是描述數據的數據,它提供了關于數據對象的附加信息。在Qt中,元數據通常用于描述類的屬性、方法、信號和槽等信息。試想一下,我們會怎么描述一個類 MyClass:
class MyClass : public Object
{
public:MyClass();~MyClass();enum Type{//... };
public:virtual void fool() override;void bar();//...
};
- 這個類的類名為MyClass
- 繼承了一個基類 Object
- 有一個無參的構造函數和一個析構函數
- 實現了繼承來的一個虛方法
- 自己有一個名為bar的public方法
- 內定義了一個枚舉類型
- ...
上述描述內容就是元數據,用來描述我們聲明的一個class,如果我們把以上數據封裝為一個類,我們簡單的認為這個類就是元對象。
3.模擬元對象系統
Qt 的元對象系統發展這么久,完善是真的完善,代碼多也是真的多!在迷失于復雜繁瑣的源代碼中之前,不妨先來設計一個簡單的元對象系統來幫助我們理解思想。
3.1.元對象聲明
聯系前面的元數據的說明,樸素的想法是我們可以用另一個對象來描述這些信息,即元對象,在運行時通過這個對象來獲取相關的具體類型等。
根據我們的需要,元對象應該具有以下信息
- 類型名
- 繼承的父類信息
- 成員函數的信息
- 內部定義的枚舉變量可能也是需要的
- ...
看起來像是這樣
class MetaObject
{
public:// 其他成員函數// ...private:// 簡單起見,直接用對象了ClassInfo m_info;ClassInfo* m_superClass;ClassMethod m_methods;ClassEnums m_enums;
};
3.2.對C++擴展
為了使我們能在軟件系統中有效的管理,我們需要對MyClass做一些拓展,現在MyClass看上去像這樣:
// MyClass.h
class MyClass : public Object
{// ... 和之前一樣// 重寫一個來自Object的虛方法virtual const MetaObject *metaObject() const override;static const MetaObject staticMetaObject; // 一個靜態成員
};
現在,只要這個數據能夠正確初始化,如果我們需要,我們就可以借助多態的特性,通過接口來獲得這個類的相關信息了。
3.3初始化元對象
那么問題來了,怎么初始化這個變量呢,C++ 作為靜態語言,想要獲取這些編譯期有關的信息,我們只能選擇在編譯時或者編譯前來做這件事,直覺告訴我們,我們要做編譯器之前來做這件事,有兩個顯而易見的原因
- 不要妄圖修改編譯器,成本巨大且危險
- 直接修改編譯器顯示不是用戶能接受的方式
當然可以手動編寫這個文件,把類的信息一個個提煉出來,但是那樣太不程序員了,我們需要寫一段程序,在編譯器之前來做這個事情(你可以把它當成一段生成代碼的腳本),我們可以這樣做:
- 在我們寫的類里面加上一個標記,來表示該類使用了元對象,需要處理并正確初始化 MetaObejct,我們這里假設就用 DEBUG_OBJ 來表示
- 運行我們的程序,如果在某個文件里面發現了標記,解析這個文件,獲取他的類型信息(ClassInfo),方法信息(ClassMethod),繼承信息等
- 腳本生成了一個 moc_MyClass.cpp 文件,用上述信息初始化 MetaObject,類似于下面這樣:
// 由腳本生成的文件
// moc_MyClass.cpp
#include "MyClass.h"// 這里是腳本解析原來頭文件生成的數據
// 解析了類的名稱,成員,繼承關系等等
// ...const MetaObject MyClass::staticMetaObject = {// 用解析來的數據來初始化元對象內容
};const MetaObject *MyClass::metaObject() const
{return &staticMetaObject;
}
然后把這個文件也為做源文件一起編譯就行了。
3.4.使用元對象
現在再回頭來看前面的問題
1)現在直接通過虛函數多態性質拿到 MetaObject,再拿到元數據,比較兩個類名是不是一致即可,如果我們采用靜態的字符串數組來存類名,甚至我們不需要比較字符串是否一致,只需要比較字符串指針是否相同就可以了。
2)現在直接綁定兩個對象的方法字符串即可,我們可以在 MetaObject 提供兩各方法
- 檢查這兩個字符串是否是類的方法(ClassMethod中有沒有這個字符串以及參數檢查),以判斷綁定是否能成功
- 一個統一的調用形式,內部根據字符串來調用相關方法
3)現在你可添加屬性,實際添加到元數據中,而存取就像你調用get,set方法一樣自然
大功告成,至此,一個簡單的元對象系統就設計好了!
4.QT的元系統
4.1.元對象系統基于QObject類、Q_OBJECT宏、元對象編譯器MOC實現
1) QObject 類
作為每一個需要利用元對象系統的類的基類。
2) Q_OBJECT宏
定義在每一個類的私有數據段,用來啟用元對象功能,比如動態屬性、信號和槽。
在一個QObject類或者其派生類中,如果沒有聲明Q_OBJECT宏,那么類的metaobject對象不會被生成,類實例調用metaObject()返回的就是其父類的metaobject對象,導致的后果是從類的實例獲得的元數據其實都是父類的數據。因此類所定義和聲明的信號和槽都不能使用,所以,任何從QObject繼承出來的類,無論是否定義聲明了信號、槽和屬性,都應該聲明Q_OBJECT 宏。
3) 元對象編譯器MOC (Meta Object Complier)
MOC分析C++源文件,如果發現在一個頭文件(header file)中包含Q_OBJECT 宏定義,會動態的生成一個moc_xxxx命名的C++源文件,源文件包含Q_OBJECT的實現代碼,會被編譯、鏈接到類的二進制代碼中,作為類的完整的一部分。
4.2.元對象系統的功能
qt元對象系統主要提供了三個能力:
- 對象間通信(信號槽機制)
- 運行時信息(類似反射機制)
- 動態的屬性系統
除了這些功能外,還提供了如下功能:
QObject::metaObject()
?返回與該類相關聯的元對象。QMetaObject::className()
?在運行時以字符串形式返回類名,而無需通過 C++ 編譯器提供本地運行時類型信息(RTTI)支持。QObject::inherits()
?函數返回一個對象是否是在 QObject 繼承樹內繼承了指定類的實例。QObject::tr()
?和?QObject::trUtf8()
?用于國際化的字符串翻譯。QObject::setProperty()
?和?QObject::property()
?動態地通過名稱設置和獲取屬性。QMetaObject::newInstance()
?構造該類的新實例。- ?使用qobject_cast()方法在QObject類之間提供動態轉換,qobject_cast()方法的功能類似于標準C++的dynamic_cast(),但qobject_cast()不需要RTTI的支持
4.3.Q_PROPERTY()的使用
#define Q_PROPERTY(text)
Q_PROPERTY定義在/src/corelib/kernel/Qobjectdefs.h文件中,用于被MOC處理。
Q_PROPERTY(type nameREAD getFunction[WRITE setFunction][RESET resetFunction][NOTIFY notifySignal][REVISION int][DESIGNABLE bool][SCRIPTABLE bool][STORED bool][USER bool][CONSTANT][FINAL])
Type:屬性的類型
Name:屬性的名稱
READ getFunction:屬性的訪問函數
WRITE setFunction:屬性的設置函數
RESET resetFunction:屬性的復位函數
NOTIFY notifySignal:屬性發生變化的地方發射的notifySignal信號
REVISION int:屬性的版本,屬性暴露到QML中
DESIGNABLE bool:屬性在GUI設計器中是否可見,默認為true
SCRIPTABLE bool:屬性是否可以被腳本引擎訪問,默認為true
STORED bool:
USER bool:
CONSTANT:標識屬性的值是常量,值為常量的屬性沒有WRITE、NOTIFY
FINAL:標識屬性不會被派生類覆寫
注意:NOTIFY notifySignal聲明了屬性發生變化時發射notifySignal信號,但并沒有實現,因此程序員需要在屬性發生變化的地方發射notifySignal信號。
Object.h:
#ifndef OBJECT_H
#define OBJECT_H#include <QObject>
#include <QString>
#include <QDebug>class Object : public QObject
{Q_OBJECTQ_PROPERTY(int age READ age WRITE setAge NOTIFY ageChanged)Q_PROPERTY(int score READ score WRITE setScore NOTIFY scoreChanged)Q_CLASSINFO("Author", "Scorpio")Q_CLASSINFO("Version", "1.0")Q_ENUMS(Level)
protected:QString m_name;QString m_level;int m_age;int m_score;
public:enum Level{Basic,Middle,Advanced};
public:explicit Object(QString name, QObject *parent = 0):QObject(parent){m_name = name;setObjectName(m_name);connect(this, SIGNAL(ageChanged(int)), this, SLOT(onAgeChanged(int)));connect(this, SIGNAL(scoreChanged(int)), this, SLOT(onScoreChanged(int)));}int age()const{return m_age;}void setAge(const int& age){m_age = age;emit ageChanged(m_age);}int score()const{return m_score;}void setScore(const int& score){m_score = score;emit scoreChanged(m_score);}
signals:void ageChanged(int age);void scoreChanged(int score);
public slots:void onAgeChanged(int age){qDebug() << "age changed:" << age;}void onScoreChanged(int score){qDebug() << "score changed:" << score;}
};#endif // OBJECT_H
Main.cpp:
#include <QCoreApplication>
#include "Object.h"int main(int argc, char *argv[])
{QCoreApplication a(argc, argv);Object ob("object");//設置屬性ageob.setProperty("age", QVariant(30));qDebug() << "age: " << ob.age();qDebug() << "property age: " << ob.property("age").toInt();//設置屬性scoreob.setProperty("score", QVariant(90));qDebug() << "score: " << ob.score();qDebug() << "property score: " << ob.property("score").toInt();//內省intropection,運行時查詢對象信息qDebug() << "object name: " << ob.objectName();qDebug() << "class name: " << ob.metaObject()->className();qDebug() << "isWidgetType: " << ob.isWidgetType();qDebug() << "inherit: " << ob.inherits("QObject");return a.exec();
}
4.4.Q_INVOKABLE使用
#define Q_INVOKABLE
????????Q_INVOKABLE定義在/src/corelib/kernel/Qobjectdefs.h文件中,用于被MOC識別。
????????Q_INVOKABLE宏用于定義一個成員函數可以被元對象系統調用,Q_INVOKABLE宏必須寫在函數的返回類型之前。如下:
Q_INVOKABLE void invokableMethod();
????????invokableMethod()函數使用了Q_INVOKABLE宏聲明,invokableMethod()函數會被注冊到元對象系統中,可以使用 QMetaObject::invokeMethod()調用。
????????Q_INVOKABLE與QMetaObject::invokeMethod均由元對象系統喚起,在Qt C++/QML混合編程、跨線程編程、Qt Service Framework以及?Qt/ HTML5混合編程以及里廣泛使用。
1) 在跨線程編程中的使用
如何調用駐足在其他線程里的QObject方法呢?Qt提供了一種非常友好而且干凈的解決方案:向事件隊列post一個事件,事件的處理將以調用所感興趣的方法為主(需要線程有一個正在運行的事件循環)。而觸發機制的實現是由MOC提供的內省方法實現的。因此,只有信號、槽以及被標記成Q_INVOKABLE的方法才能夠被其它線程所觸發調用。如果不想通過跨線程的信號、槽這一方法來實現調用駐足在其他線程里的QObject方法。另一選擇就是將方法聲明為Q_INVOKABLE,并且在另一線程中用invokeMethod喚起。
2) Qt Service Framework
Qt服務框架是Qt Mobility 1.0.2版本推出的,一個服務(service)是一個獨立的組件提供給客戶端(client)定義好的操作。客戶端可以通過服務的名稱,版本號和服務的對象提供的接口來查×××。 查找到服務后,框架啟動服務并返回一個指針。
服務通過插件(plug-ins)來實現。為了避免客戶端依賴某個具體的庫,服務必須繼承自QObject,保證QMetaObject?系統可以用來提供動態發現和喚醒服務的能力。要使QmetaObject機制充分的工作,服務必須滿足,其所有的方法都是通過 signal、slot、property或invokable method和Q_INVOKEBLE來實現。
QServiceManager manager;
QObject *storage ;
storage = manager.loadInterface("com.nokia.qt.examples.FileStorage");
if(storage) QMetaObject::invokeMethod(storage, "deleteFile", Q_ARG(QString, "/tmp/readme.txt"));
上述代碼通過service的元對象提供的invokeMethod方法,調用文件存儲對象的deleteFile() 方法。客戶端不需要知道對象的類型,因此也沒有鏈接到具體的service庫。?當然在服務端的deleteFile方法,一定要被標記為Q_INVOKEBLE,才能夠被元對象系統識別。
Qt服務框架的一個亮點是它支持跨進程通信,服務可以接受遠程進程。在服務管理器上注冊后,進程通過signal、slot、invokable method和property來通信,就像本地對象一樣。服務可以設定為在客戶端間共享,或針對一個客戶端。?在Qt服務框架推出之前,信號、槽以及invokable method僅支持跨線程。 下圖是跨進程的服務/客戶段通信示意圖。invokable method和Q_INVOKEBLE?是跨進城、跨線程對象之間通信的重要利器。