Q_OBJECT宏的作用


轉載https://www.cnblogs.com/WushiShengFei/p/9820835.html

QT框架里面最大的特色就是在C++的基礎上增加了元對象系統(Meta-Object System),而元對象系統里面最重要的內容就是信號與槽機制,這個機制是在C++語法的基礎上實現的,使用了函數、函數指針、回調函數等概念。當然與我們自己去寫函數所不同的是槽與信號機制會自動幫我們生成部分代碼,比如我們寫的信號函數就不需要寫它的實現部分,這是因為在我們編譯程序的時候,編譯器會自動生成這一部分代碼,當我們調用connect函數的時候,系統會自動將信號函數與槽函數相連接,於是當我們調用信號函數的時候,系統就會自動回調槽函數,不管你是在同一線程下調用或者在不同線程下調用,系統都會自動評估,並在合理的時候觸發函數,以此來保證線程的安全。信號與槽機制是線程安全的,這可以使得我們在調用的時候不用再額外的增加過多保證線程同步的代碼,為了實現元對象系統,QT把所有相關實現寫在了QObject類中,所以當你想使用元對象系統的時候,你所寫的類需要繼承自QObject,包括QT自帶的所有類都是繼承自QObject,所以分析QObject的代碼,對了解QT的元對象機制有非常大的幫助,我並不打算把QObject類的每一行代碼都寫下來,只想把其中比較關鍵的內容或者對分析QT源碼有幫助的內容介紹一下。

1.宏Q_OBJECT

這個宏展開以后是如下定義:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define Q_OBJECT \
public : \
     QT_WARNING_PUSH \
     Q_OBJECT_NO_OVERRIDE_WARNING \
     static  const  QMetaObject staticMetaObject; \
     virtual  const  QMetaObject *metaObject()  const ; \
     virtual  void  *qt_metacast( const  char  *); \
     virtual  int  qt_metacall(QMetaObject::Call,  int void  **); \
     QT_TR_FUNCTIONS \
private : \
     Q_OBJECT_NO_ATTRIBUTES_WARNING \
     Q_DECL_HIDDEN_STATIC_METACALL  static  void  qt_static_metacall(QObject *, QMetaObject::Call,  int void  **); \
     QT_WARNING_POP \
     struct  QPrivateSignal {}; \
QT_ANNOTATE_CLASS(qt_qobject,  "" )

  你可以看到這個宏定義了一些函數,並且函數名都帶有meta,所以不難猜到這些函數和QT的元對象系統是有關系的,實際上你在qobject.cpp里面是找不到這些函數的實現的,它們的實現都在moc_qobject.cpp里面。QT的元對象系統是這樣處理的,當你編譯你的工程時,它會去遍歷所有C++文件,當發現某一個類的私有部分有聲明Q_OBJECT這個宏時,就會自動生成一個moc_*.cpp的文件,這個文件會生成信號的實現函數,Q_OBJECT宏里面定義的那些函數也會在這個文件里面實現,並生成與類相關的元對象。這就是為什么我們定義一個信號的時候,不需要實現它,因為它的實現已經寫在moc_*.cpp文件里面了。

 

2.Q_PROPERTY(QString objectName READ objectName WRITE setObjectName NOTIFY objectNameChanged)

這個宏是定義一個屬性,屬性也是元對象系統的內容之一,實際上我們在做界面設計的時候經常會用到屬性,比如修改Label的顯示內容,需要用到Text屬性,修改窗體長寬等等,在你做界面設計的時候,屬性編輯框里面所顯示的就是當前對象的所有屬性,而這些屬性的定義就是用上面的宏來定義的。實際上屬性和變量是有點相似的,都是讀值和寫值的功能,那為什么不直接對變量操作就好了?雖然看起來相似,但是還是有不同點,第一屬性可以定義為可讀寫的,也可以定義為只讀的,比如有些數據我們只在類的內部做修改不允許在外部做修改,但是有時候又需要在外部查看這個值,就可以設置為只讀屬性,而變量是做不到這點的,你把變量放在public部分,那么這個變量就可以在任何地方被修改,這就破壞了類的封裝性。第二屬性可以定義信號,當屬性變化的時候觸發信號,這樣我們可以在信號觸發時做一些工作,比如當你設置LineEdit為readonly時,你會發現輸入框的背景顏色被改變了,這就可以通過屬性變化的信號來處理。

 

3.Q_DECLARE_PRIVATE(QObject)

這個宏的定義如下:

1
2
3
4
#define Q_DECLARE_PRIVATE(Class) \
     inline  Class##Private* d_func() {  return  reinterpret_cast <Class##Private *>(qGetPtrHelper(d_ptr)); } \
     inline  const  Class##Private* d_func()  const  return  reinterpret_cast < const  Class##Private *>(qGetPtrHelper(d_ptr)); } \
friend  class  Class##Private;

  這個宏首先創建了兩個內聯函數,返回值都是QObjectPrivate *,並且聲明QObjectPrivate 為友元類,QObjectPrivate這個類是在qobject_p.h中定義,它繼承至QObjectData,你可以看到d_func()是將d_prt強制轉換為QObjectPrivate *類型,而d_prt這個指針在QObject里面定義的是QObjectData的指針類型,所以這里可以進行強轉,QObjectPrivate這個類主要存放QOject類需要用到的一些子對象,變量等。為什么要介紹這個宏,如果你有看QT源碼習慣的話,你會發現幾乎每一個類都用到了這個宏,我們自己寫的類會經常把類內部用的變量聲明在private部分,但是QT源碼並不是這樣做的,它的做法是給每個類創建一個以類名+Private的類,例如QObject對應的就是QObjectPrivate,這個類實際上就是用來存放QObject需要用到的所有私有變量和私有對象,而QObject更多的是函數實現,你去看其他的源碼也是如此,子對象聲明在Q*Private中,而本類只實現函數

 

4.構造函數

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
QObject::QObject(QObject *parent)
     : d_ptr( new  QObjectPrivate)
{
     Q_D(QObject);
     d_ptr->q_ptr =  this ;
     d->threadData = (parent && !parent-> thread ()) ? parent->d_func()->threadData : QThreadData::current();
     d->threadData->ref();
     if  (parent) {
         QT_TRY {
             if  (!check_parent_thread(parent, parent ? parent->d_func()->threadData : 0, d->threadData))
                 parent = 0;
             setParent(parent);
         } QT_CATCH(...) {
             d->threadData->deref();
             QT_RETHROW;
         }
     }
#if QT_VERSION < 0x60000
     qt_addObject( this );
#endif
     if  (Q_UNLIKELY(qtHookData[QHooks::AddQObject]))
         reinterpret_cast <QHooks::AddQObjectCallback>(qtHookData[QHooks::AddQObject])( this );
}

 

(1)首先第一步就創建d_ptr指針。

(2)Q_D(QObject);這個宏你可以在QT的很多源碼里面看到。它展開以后是下面的樣子:#define Q_D(Class) Class##Private * const d = d_func();

    d_fun()函數前面講到了,其實就是返回d_ptr了。所以這個宏的意思是定義一個指針d指向d_ptr;

(3)d_ptr->q_ptr = this;

   q_ptrQOject類型,這里把this指針賦給了它,所以使得QObjectPrivate可以回調QOject的函數。

(4)初始化threadData

 

5.moveToThread

(1)如果要移動的線程和Object本身就是同一線程,那么直接返回

 

1
2
3
4
5
Q_D(QObject);
     if  (d->threadData-> thread  == targetThread) {
         // object is already in this thread
         return ;
     }

 

(2)如果parent不為空,不允許移動到其他線程,子類必需與父類在同一線程。

 

1
2
3
4
if  (d->parent != 0) {
         qWarning( "QObject::moveToThread: Cannot move objects with a parent" );
         return ;
     }

 

(3)如果對象是窗體類,不允許移動到線程,窗體類必需在主線程運行,在子線程去直接調用窗體控件都是不安全的,可能導致程序崩潰,合理的做法是通過信號槽機制。

1
2
3
4
if  (d->isWidget) {
         qWarning( "QObject::moveToThread: Widgets cannot be moved to a new thread" );
         return ;
}

(4)只有在對象所在線程才能將對象移動到另一個線程,不能在其他線程將對象移動到某個線程,這種操作是不被允許的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
QThreadData *currentData = QThreadData::current();
     QThreadData *targetData = targetThread ? QThreadData::get2(targetThread) : Q_NULLPTR;
     if  (d->threadData-> thread  == 0 && currentData == targetData) {
         // one exception to the rule: we allow moving objects with no thread affinity to the current thread
         currentData = d->threadData;
     else  if  (d->threadData != currentData) {
         qWarning( "QObject::moveToThread: Current thread (%p) is not the object's thread (%p).\n"
                  "Cannot move to target thread (%p)\n" ,
                  currentData-> thread .load(), d->threadData-> thread .load(), targetData ? targetData-> thread .load() : Q_NULLPTR);
 
#ifdef Q_OS_MAC
         qWarning( "You might be loading two sets of Qt binaries into the same process. "
                  "Check that all plugins are compiled against the right Qt binaries. Export "
                  "DYLD_PRINT_LIBRARIES=1 and check that only one set of binaries are being loaded." );
#endif
 
         return ;
}

(5)線程轉移

1
2
3
4
5
6
7
8
9
10
11
12
13
     //為轉移線程准備,遍歷所有子對象,並給每一個子對象發送一個QEvent::ThreadChange的事件。
     d->moveToThread_helper();
     if  (!targetData)
         targetData =  new  QThreadData(0);
//為轉移事件上鎖
     QOrderedMutexLocker locker(&currentData->postEventList.mutex,
                                &targetData->postEventList.mutex);
 
currentData->ref();
//遍歷所有子對象及自身,將currentData的postEventList里面的對象轉移到targetData,將所有子對象及自身的threadData設置為targetData
     d_func()->setThreadData_helper(currentData, targetData);
     locker.unlock();
currentData->deref();

 

6.connect函數

 

 

connet的重構函數很多,這里選擇其中一個來分析。

 

1
2
3
QMetaObject::Connection QObject::connect( const  QObject *sender,  const  QMetaMethod & signal ,
                                      const  QObject *receiver,  const  QMetaMethod &method,
                                      Qt::ConnectionType type)

 

(1)首選senderreceiver不能為空,signal必須是Signal類型,也就是聲明在signals:下面,method不能為構造函數,不滿足這幾個條件則返回。

 

1
2
3
4
5
6
7
8
9
10
11
if  (sender == 0
             || receiver == 0
             ||  signal .methodType() != QMetaMethod::Signal
             || method.methodType() == QMetaMethod::Constructor) {
         qWarning( "QObject::connect: Cannot connect %s::%s to %s::%s" ,
                  sender ? sender->metaObject()->className() :  "(null)" ,
                  signal .methodSignature().constData(),
                  receiver ? receiver->metaObject()->className() :  "(null)" ,
                  method.methodSignature().constData() );
         return  QMetaObject::Connection(0);
     }

 

2)檢查signalmethod是否真實存在,在編譯期即使傳入的信號不存在也不會報錯,在運行期會檢查是否存在,所以在寫connect函數的時候要仔細檢查,盡量使用&ClassName::functionName的方式讓系統自動補全,當然也可以通過connect的返回值來判斷調用是否成功,如調用不成功則拋出異常。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int  signal_index;
     int  method_index;
     {
         int  dummy;
         QMetaObjectPrivate::memberIndexes(sender,  signal , &signal_index, &dummy);
         QMetaObjectPrivate::memberIndexes(receiver, method, &dummy, &method_index);
     }
 
     const  QMetaObject *smeta = sender->metaObject();
     const  QMetaObject *rmeta = receiver->metaObject();
     if  (signal_index == -1) {
         qWarning( "QObject::connect: Can't find signal %s on instance of class %s" ,
                  signal .methodSignature().constData(), smeta->className());
         return  QMetaObject::Connection(0);
     }
     if  (method_index == -1) {
         qWarning( "QObject::connect: Can't find method %s on instance of class %s" ,
                  method.methodSignature().constData(), rmeta->className());
         return  QMetaObject::Connection(0);
}

 

(3)檢查signalmethod的參數個數和類型是否是一致的,不一致則返回。

 

1
2
3
4
5
6
7
if  (!QMetaObject::checkConnectArgs( signal .methodSignature().constData(), method.methodSignature().constData())) {
         qWarning( "QObject::connect: Incompatible sender/receiver arguments"
                  "\n        %s::%s --> %s::%s" ,
                  smeta->className(),  signal .methodSignature().constData(),
                  rmeta->className(), method.methodSignature().constData());
         return  QMetaObject::Connection(0);
}

 

4)如果你設置的連接方式為QueuedConnection,那么所有的參數都必須是元數據類型,自定義的類型,如自定義的結構體或枚舉必須注冊為元數據類型,否則無法作為信號和槽的參數,因為最終要將這些參數加入到消息隊列里面。

 

1
2
3
4
int  *types = 0;
     if  ((type == Qt::QueuedConnection)
             && !(types = queuedConnectionTypes( signal .parameterTypes())))
         return  QMetaObject::Connection(0);

 

5)所有的檢查完畢,調用QMetaObjectConnection函數,而QMetaObjectConnection會創建一個Connection的對象,這個對象會保存信號和槽的函數對象,然后會把這個Connection對象保存到sender的一個數組中,當你觸發信號的時候,因為Connection對象保存在了sender中,所以可以找到原來綁定的槽函數,然后回調槽函數。

 

1
2
3
QMetaObject::Connection handle = QMetaObject::Connection(QMetaObjectPrivate::connect(
         sender, signal_index,  signal .enclosingMetaObject(), receiver, method_index, 0, type, types));
     return  handle;


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM