天天看點

Q_OBJECT 宏

轉載自http://www.devbean.info/2012/08/qt-study-road-2-custom-signal-slot/

上一節我們詳細分析了 connect() 函數。使用 connect() 可以讓我們連接配接系統提供的信号和槽。但是,Qt 的信号槽機制并不僅僅是使用系統提供的那部分,還會允許我們自己設計自己的信号和槽。這也是 Qt 架構的設計思路之一,用于我們設計解耦的程式。本節将講解如何在自己的程式中自定義信号槽。

信号槽不是 GUI 子產品提供的,而是 Qt 核心特性之一。是以,我們可以在普通的控制台程式使用信号槽。

經典的觀察者模式在講解舉例的時候通常會舉報紙和訂閱者的例子。有一個報紙類 Newspaper,有一個訂閱者類 Subscriber。Subscriber 可以訂閱 Newspaper。這樣,當 Newspaper 有了新的内容的時候,Subscriber 可以立即得到通知。在這個例子中,觀察者是 Subscriber,被觀察者是 Newspaper。在經典的實作代碼中,觀察者會将自身注冊到被觀察者的一個容器中(比如 subscriber.registerTo(newspaper))。被觀察者發生了任何變化的時候,會主動周遊這個容器,依次通知各個觀察者(newspaper.notifyAllSubscribers())。

下面我們看看使用 Qt 的信号槽,如何實作上述觀察者模式。注意,這裡我們僅僅是使用這個案例,我們的代碼并不是去實作一個經典的觀察者模式。也就是說,我們使用 Qt 的信号槽機制來獲得同樣的效果。

//!!! Qt5
#include <QObject>
 
// newspaper.h
class Newspaper : public QObject
{
    Q_OBJECT
public:
    Newspaper(const QString & name) :
        m_name(name)
    {
    }
 
    void send() const
    {
        emit newPaper(m_name);
    }
 
signals:
    void newPaper(const QString &name);
 
private:
    QString m_name;
};
 
// reader.h
#include <QObject>
#include <QDebug>
 
class Reader : public QObject
{
    Q_OBJECT
public:
    Reader() {}
 
    void receiveNewspaper(cosnt QString & name) const
    {
        qDebug() << "Receives Newspaper: " << name;
    }
};
 
// main.cpp
#include <QCoreApplication>
 
#include "newspaper.h"
#include "reader.h"
 
int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);
 
    Newspaper newspaper("Newspaper A");
    Reader reader;
    QObject::connect(&newspaper, &Newspaper::newPaper,
                     &reader,    &Reader::receiveNewspaper);
    newspaper.send();
 
    return app.exec();
}
           

當我們運作上面的程式時,會看到終端輸出 Receives Newspaper: Newspaper A 這樣的字樣。

下面我們來分析下自定義信号槽的代碼。

這段代碼放在了三個檔案,分别是 newspaper.h,reader.h 和 main.cpp。為了減少檔案數量,可以把 newspaper.h 和 reader.h 都放在 main.cpp 的 main() 函數之前嗎?答案是,可以,但是需要有額外的操作。具體問題,我們在下面會詳細說明。

首先看 Newspaper 這個類。這個類繼承了 QObject 類。隻有繼承了 QObject 類的類,才具有信号槽的能力。所謂,為了使用信号槽,必須繼承 QObject。凡是 QObject 類(不管是直接子類還是間接子類),都應該在第一行代碼寫上 Q_OBJECT。不管是不是使用信号槽,都應該添加這個宏。這個宏的展開将為我們的類提供信号槽機制、國際化機制以及 Qt 提供的不基于 C++ RTTI 的反射能力。是以,如果你覺得你的類不需要使用信号槽,就不添加這個宏,就是錯誤的。其它很多操作都會依賴于這個宏。注意,這個宏将由 moc(我們會在後面章節中介紹 moc。這裡你可以将其了解為一種預處理器,是比 C++ 預處理器更早執行的預處理器。) 做特殊處理,不僅僅是宏展開這麼簡單。moc 會讀取标記了 Q_OBJECT 的頭檔案,生成以 moc_ 為字首的檔案,比如 newspaper.h 将生成 moc_newspaper.h。你可以到建構目錄檢視這個檔案,看看到底增加了什麼内容。注意,由于 moc 隻處理頭檔案中的标記了 Q_OBJECT 的類聲明,不會處理 cpp 檔案中的類似聲明。是以,如果我們的 Newspaper 和 Reader 類位于 main.cpp 中,是無法得到 moc 的處理的。解決方法是,我們手動調用 moc 工具處理 main.cpp,并且将 main.cpp 中的 include “newspaper.h” 改為 include “moc_newspaper.h” 就可以了。不過,這是相當繁瑣的步驟,為了避免這樣修改,我們還是将其放在頭檔案中。許多初學者會遇到莫名其妙的錯誤,一加上 Q_OBJECT 就出錯,很大一部分是因為沒有注意到這個宏應該放在頭檔案中。

Newspaper 類的 public 和 private 代碼塊都比較簡單,隻不過它新加了一個 signals。signals 塊所列出的,就是該類的信号。信号就是一個個的函數名,傳回值是 void(因為無法獲得信号的傳回值,是以也就無需傳回任何值),參數是該類需要讓外界知道的資料。信号作為函數名,不需要在 cpp 函數中添加任何實作(我們曾經說過,Qt 程式能夠使用普通的 make 進行編譯。沒有實作的函數名怎麼會通過編譯?原因還是在 moc,moc 會幫我們實作信号函數所需要的函數體,是以說,moc 并不是單純的将 Q_OBJECT 展開,而是做了很多額外的操作)。

Newspaper 類的 send() 函數比較簡單,隻有一個語句 emit newPaper(m_name);。emit 是 Qt 對 C++ 的擴充,是一個關鍵字(其實也是一個宏)。emit 的含義是發出,也就是發出 newPaper() 信号。感興趣的接收者會關注這個信号,可能還需要知道是哪份報紙發出的信号?是以,我們将實際的報紙名字 m_name 當做參數傳給這個信号。當接收者連接配接這個信号時,就可以通過槽函數獲得實際值。這樣就完成了資料從發出者到接收者的一個轉移。

Reader 類更簡單。因為這個類需要接受信号,是以我們将其繼承了 QObject,并且添加了 Q_OBJECT 宏。後面則是預設構造函數和一個普通的成員函數。Qt 5 中,任何成員函數、static 函數、全局函數和 Lambda 表達式都可以作為槽函數。與信号函數不同,槽函數必須自己完成實作代碼。槽函數就是普通的成員函數,是以也會受到 public、private 等通路控制符的影響。(我們沒有說信号也會受此影響,事實上,如果信号是 private 的,這個信号就不能在類的外面連接配接,也就沒有任何意義。)

main() 函數中,我們首先建立了 Newspaper 和 Reader 兩個對象,然後使用 QObject::connect() 函數。這個函數我們上一節已經詳細介紹過,這裡應該能夠看出這個連接配接的含義。然後我們調用 Newspaper 的 send() 函數。這個函數隻有一個語句:發出信号。由于我們的連接配接,當這個信号發出時,自動調用 reader 的槽函數,列印出語句。

這樣我們的示例程式講解完畢。我們基于 Qt 的信号槽機制,不需要觀察者的容器,不需要注冊對象,就實作了觀察者模式。

下面總結一下自定義信号槽需要注意的事項:

  • 發送者和接收者都需要是 QObject 的子類(當然,槽函數是全局函數、Lambda 表達式等無需接收者的時候除外);
  • 使用 signals 标記信号函數,信号是一個函數聲明,傳回 void,不需要實作函數代碼;
  • 槽函數是普通的成員函數,會受到 public、private、protected 的影響;
  • 使用 emit 在恰當的位置發送信号;
  • 使用 QObject::connect() 函數連接配接信号和槽。

Qt 4

下面給出 Qt 4 中相應的代碼:

//!!! Qt4
#include <QObject>
 
// newspaper.h
class Newspaper : public QObject
{
    Q_OBJECT
public:
    Newspaper(const QString & name) :
        m_name(name)
    {
    }
 
    void send() const
    {
        emit newPaper(m_name);
    }
 
signals:
    void newPaper(const QString &name);
 
private:
    QString m_name;
};
 
// reader.h
#include <QObject>
#include <QDebug>
 
class Reader : public QObject
{
    Q_OBJECT
public:
    Reader() {}
 
public slots:
    void receiveNewspaper(cosnt QString & name) const
    {
        qDebug() << "Receives Newspaper: " << name;
    }
};
 
// main.cpp
#include <QCoreApplication>
 
#include "newspaper.h"
#include "reader.h"
 
int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);
 
    Newspaper newspaper("Newspaper A");
    Reader reader;
    QObject::connect(&newspaper, SIGNAL(newPaper(QString)),
                     &reader,    SLOT(receiveNewspaper(QString)));
    newspaper.send();
 
    return app.exec();
}
           

注意下 Qt 4 與 Qt 5 的差別。

Newspaper 類沒有什麼差別。

Reader 類,receiveNewspaper() 函數放在了 public slots 塊中。在 Qt 4 中,槽函數必須放在由 slots 修飾的代碼塊中,并且要使用通路控制符進行通路控制。其原則同其它函數一樣:預設是 private 的,如果要在外部通路,就應該是 public slots;如果隻需要在子類通路,就應該是 protected slots。

main() 函數中,QObject::connect() 函數,第二、第四個參數需要使用 SIGNAL 和 SLOT 這兩個宏轉換成字元串(具體事宜我們在上一節介紹過)。注意 SIGNAL 和 SLOT 的宏參數并不是取函數指針,而是除去傳回值的函數聲明,并且 const 這種參數修飾符是忽略不計的。

繼續閱讀