天天看點

Qt學習筆記(二)——信号槽

一、初識信号槽

所謂信号槽,實際就是觀察者模式。當某個事件發生之後,比如,按鈕檢測到自己被點選了一下,它就會發出一個信号(signal)。這種發出是沒有目的的,類似廣播。如果有對象對這個信号感興趣,它就會使用連接配接(connect)函數,意思是,用自己的一個函數(成為槽(slot))來處理這個信号。也就是說,當信号發出時,被連接配接的槽函數會自動被回調。這就類似觀察者模式:當發生了感興趣的事件,某一個操作就會被自動觸發。

#include <QApplication>
#include <QPushButton>

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);

    QPushButton button("Quit");
    QObject::connect(&button, &QPushButton::clicked, &QApplication::quit);
    button.show();

    return app.exec();
}
           

以上面這段代碼為例體驗信号槽的使用

Qt學習筆記(二)——信号槽

點選Quit即可退出。按鈕在 Qt 中被稱為

QPushButton

下面了解一下

QObject::connect()

這個函數。

connect()

函數最常用的一般形式:

connect(sender,   signal,
        receiver, slot);
           

connect()

一般會使用前面四個參數,第一個是發出信号的對象,第二個是發送對象發出的信号,第三個是接收信号的對象,第四個是接收對象在接收到信号之後所需要調用的函數。也就是說,當 sender 發出了 signal 信号之後,會自動調用 receiver 的 slot 函數。

在 Qt 5 中,

QObject::connect()

有五個重載:

QMetaObject::Connection connect(const QObject *, const char *,
                                const QObject *, const char *,
                                Qt::ConnectionType);

QMetaObject::Connection connect(const QObject *, const QMetaMethod &,
                                const QObject *, const QMetaMethod &,
                                Qt::ConnectionType);

QMetaObject::Connection connect(const QObject *, const char *,
                                const char *,
                                Qt::ConnectionType) const;

QMetaObject::Connection connect(const QObject *, PointerToMemberFunction,
                                const QObject *, PointerToMemberFunction,
                                Qt::ConnectionType)

QMetaObject::Connection connect(const QObject *, PointerToMemberFunction,
                                Functor);
           

第一個,sender 類型是

const QObject *

,signal 的類型是

const char *

,receiver 類型是

const QObject *

,slot 類型是

const char *

。這個函數将 signal 和 slot 作為字元串處理。第二個,sender 和 receiver 同樣是

const QObject *

,但是 signal 和 slot 都是

const QMetaMethod &

。我們可以将每個函數看做是

QMetaMethod

的子類。是以,這種寫法可以使用

QMetaMethod

進行類型比對。第三個,sender 同樣是

const QObject *

,signal 和 slot 同樣是

const char *

,但是卻缺少了 receiver。這個函數其實是将 this 指針作為 receiver。第四個,sender 和 receiver 也都存在,都是

const QObject *

,但是 signal 和 slot 類型則是

PointerToMemberFunction

。看這個名字就應該知道,這是指向成員函數的指針。第五個,前面兩個參數沒有什麼不同,最後一個參數是

Functor

類型。這個類型可以接受 static 函數、全局函數以及 Lambda 表達式。

由此我們可以看出,

connect()

函數,sender 和 receiver 沒有什麼差別,都是

QObject

指針;主要是 signal 和 slot 形式的差別。具體到我們的示例,我們的

connect()

函數顯然是使用的第五個重載,最後一個參數是

QApplication

的 static 函數

quit()

。也就是說,當我們的 button 發出了

clicked()

信号時,會調用

QApplication

quit()

函數,使程式退出。

信号槽要求信号和槽的參數類型一緻。如果不一緻,允許的情況是,槽函數的參數可以比信号的少,即便如此,槽函數存在的那些參數的順序也必須和信号的前面幾個一緻起來。

二、自定義信号槽

Qt 的信号槽機制并不僅僅是使用系統提供的那部分,還會允許我們自己設計自己的信号和槽。

下面是通過信号槽實作觀察者模式的一個例子:

經典的觀察者模式在講解舉例的時候通常會舉報紙和訂閱者的例子。有一個報紙類

Newspaper

,有一個訂閱者類

Subscriber

Subscriber

可以訂閱

Newspaper

。這樣,當

Newspaper

有了新的内容的時候,

Subscriber

可以立即得到通知。在這個例子中,觀察者是

Subscriber

,被觀察者是

Newspaper

。在經典的實作代碼中,觀察者會将自身注冊到被觀察者的一個容器中(比如

subscriber.registerTo(newspaper)

)。被觀察者發生了任何變化的時候,會主動周遊這個容器,依次通知各個觀察者(

newspaper.notifyAllSubscribers()

)。

#include <QObject>

// newspaper.h
class Newspaper : public QObject
{
    Q_OBJECT
public:
    Newspaper(const QString & name) :
        m_name(name)
    {
    }

    void send()
    {
        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(const QString & name)
    {
        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();
}
           

首先看

Newspaper

這個類。這個類繼承了

QObject

類。隻有繼承了

QObject

類的類,才具有信号槽的能力。是以,為了使用信号槽,必須繼承

QObject

。凡是

QObject

類(不管是直接子類還是間接子類),都應該在第一行代碼寫上

Q_OBJECT

。不管是不是使用信号槽,都應該添加這個宏。注意,這個宏将由 moc(我們會在後面章節中介紹 moc。這裡你可以将其了解為一種預處理器,是比 C++ 預處理器更早執行的預處理器。) 做特殊處理,不僅僅是宏展開這麼簡單。moc 會讀取标記了 Q_OBJECT 的頭檔案,生成以 moc_ 為字首的檔案,比如 newspaper.h 将生成 moc_newspaper.cpp。你可以到建構目錄檢視這個檔案,看看到底增加了什麼内容。注意,由于 moc 隻處理頭檔案中的标記了

Q_OBJECT

的類聲明,不會處理 cpp 檔案中的類似聲明。是以,如果我們的

Newspaper

Reader

類位于 main.cpp 中,是無法得到 moc 的處理的。解決方法是,我們手動調用 moc 工具處理 main.cpp,并且将 main.cpp 中的

#include "newspaper.h"

改為

#include "moc_newspaper.h"

就可以了。不過,這是相當繁瑣的步驟,為了避免這樣修改,我們還是将其放在頭檔案中。

Newspaper

類的 public 和 private 代碼塊都比較簡單,隻不過它新加了一個 signals。signals 塊所列出的,就是該類的信号。信号就是一個個的函數名,傳回值是 void(因為無法獲得信号的傳回值,是以也就無需傳回任何值),參數是該類需要讓外界知道的資料。

Reader

類更簡單。因為這個類需要接受信号,是以我們将其繼承了

QObject

,并且添加了

Q_OBJECT

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

main()

函數中,我們首先建立了

Newspaper

Reader

兩個對象,然後使用

QObject::connect()

函數。這個函數我們上一節已經詳細介紹過,這裡應該能夠看出這個連接配接的含義。然後我們調用

Newspaper

send()

函數。這個函數隻有一個語句:發出信号。由于我們的連接配接,當這個信号發出時,自動調用 reader 的槽函數,列印出語句。

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

  • 發送者和接收者都需要是

    QObject

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

    QObject::connect()

    函數連接配接信号和槽。

繼續閱讀