信號(signals)和槽(slots) 精講
2010-11-01 22:54

信號(signals)和槽(slots)

信號和信號槽被用于對象(object)之間的通信。信號和槽機(jī)制是QT的重要特征并且也許是QT與其他框架最不相同的部分。

前言

在GUI程序設(shè)計中,通常我們希望當(dāng)對一個窗口部件(widget)進(jìn)行改變時能告知另一個對此改變感興趣的窗口部件。更一般的,我們希望任何一類的對象(object)都能和其他對象進(jìn)行通信。例如,如果用戶單擊一個關(guān)閉按鈕,我們可能就希望窗口的 close() 函數(shù)被調(diào)用。

早期的工具包用回調(diào)(backcalls)的方式實現(xiàn)上面所提到的對象間的通信。回調(diào)是指一個函數(shù)的指針,因此如果你希望一個處理函數(shù)通知你一些事情,你可以傳遞另一個函數(shù)(回調(diào)函數(shù))指針給這個處理函數(shù)。這個處理函數(shù)就會在適當(dāng)?shù)臅r候調(diào)用回調(diào)函數(shù)。回調(diào)有兩個重要的缺陷:首先,它們不是類型安全的。我們無法確定處理函數(shù)是用正確的參數(shù)調(diào)用這個回調(diào)函數(shù)。其次,回調(diào)與處理函數(shù)緊密的聯(lián)系在一起以致處理函數(shù)必須知道調(diào)用哪個回調(diào)。

消息和槽

在QT中,我們使用一種可替代回調(diào)的技術(shù):信號和槽機(jī)制。當(dāng)一個特別的事件產(chǎn)生時則發(fā)出一個信號。QT的窗口部件有很多已經(jīng)預(yù)定義好的信號,我們也可以通過繼承,給窗口部件的子類添加他們自己信號。槽就是一個可以被調(diào)用處理特定信號的函數(shù)。QT的窗口部件有很多預(yù)定義好的槽,但是通常的做法是給子類窗口部件添加自己的信號,這樣就可以操縱自己加入的信號了。

  




上面這個圖一定要好好理解,每個signal和Slot都是一個Object的屬性,不同Object的signal可以對應(yīng)不用的Object的Slot。


信號和槽機(jī)制是類型安全的:一個信號的簽名必須和該信號接受槽的簽名相匹配。(事實上以一個槽的簽名可以比他可接受的信號的簽名少,因為它可以忽略一些簽名)。因此簽名是一致的,編譯器就可以幫助我們檢測類型匹配。信號和槽是松耦合的:一個類不知道也不關(guān)心哪個槽接受了它所發(fā)出的信號。QT的信號和槽機(jī)制確保他們自生的正確連接,槽會在正確的時間使用信號參數(shù)而被調(diào)用。信號和槽可以使用任何數(shù)量、任何類型的參數(shù)。他們完全是類型安全的。

所有繼承至QObject或是其子類(如 QWidget)的類都可包含信號和槽。當(dāng)對象改變它們自身狀態(tài)的時候,信號被發(fā)送,從某種意義上講,它們也許對外面的世界感興趣。這就是所有對象在通訊時所做的一切。它不知道也不關(guān)心有沒有其他的東西接受它發(fā)出的信號。這就是真正的消息封裝,并且確保對象可用作一個軟件組件。

槽被用于接收信號,但是他們也是正常的成員函數(shù)。正如一個對象不知道是否有東西接受了他信號,一個槽也不知道它是否被某個信號連接。這就確保QT能創(chuàng)建真正獨(dú)立的組件。

你可以將任意個信號連接到你想連接的信號槽,并且在需要時可將一個信號連接到多個槽。將信號直接連接到另一個信號也是可能的(這樣無論何時當(dāng)?shù)谝粋€信號被發(fā)出后會立即發(fā)出第二個)。

總體來看,信號和槽構(gòu)成了一個強(qiáng)有力的組件編程機(jī)制。

簡單示例

一個極小的 C++ 類 聲明如下:

class Counter
{
public:
Counter() {m_value = 0;}

int value() const {return m_value;}
void setValue(int Value);
private:
int m_value;
};


一個小型的 QObject 子類聲明為:

#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版本的類與前一個C++類有著相同的域,并且提供公有函數(shù)接受這個域,但是它還增加了對信號和槽(signals-slots)組件編程的支持。這個類可以通過valueChanged()發(fā)送信號告訴外部世界他的域發(fā)生了改變,并且它有一個可以接受來自其他對象發(fā)出信號的槽。

所有包含信號和槽的類都必須在他們聲明中的最開始提到Q_OBJECT。并且他們必須繼承至(直接或間接)QObject。

槽可以由應(yīng)用程序的編寫者來實現(xiàn)。這里是Counter::setVaule()的一個可能的實現(xiàn):

void Counter::setValue(int value)
{
if(value != m_value)
{
m_value = value;
emit valueChanged(value);
}
}

emit所在的這一行從對象發(fā)出valueChanged信號,并使用新值做為參數(shù)。

在下面的代碼片段中,我們創(chuàng)建兩個Counter對象并且使用QObject::connect()函數(shù)將第一個對象的valueChanged()信號連接到第二個對象的setValue()槽。

Counter a, b;
QObject::connect (&a, SIGNAL(valueChanged(int)),
&b, SLOT(setValue(int)));
a.setValue(12);    // a.value() == 12, b.value() == 12
b.setValue(48);    // a.value() == 12, b.value() == 48

函數(shù)a.setValue(12)的調(diào)用導(dǎo)致信號valueChange(12)被發(fā)出,對象b的setValue()槽接受該信號,即函數(shù)setValue()被調(diào)用。然后b同樣發(fā)出信號valueChange(),但是由于沒有槽連接到b到valueChange()信號,所以該信號被忽略。

注意,只有當(dāng) value != m_value 時,函數(shù) setValue() 才會設(shè)置新值并發(fā)出信號。這樣就避免了在循環(huán)連接的情況下(比如b.valueChanged() 和a.setValue()連接在一起)出現(xiàn)無休止的循環(huán)的情況。

信號將被發(fā)送給任何你建立了連接的槽;如果重復(fù)連接,將會發(fā)送兩個信號。總是可以使用QObject::disconnect()函數(shù)斷開一個連接。

這個例子說明了對象之間可以不需要知道相互間的任何信息而系協(xié)同工作。為了實現(xiàn)這一目的,只需要將對象通過函數(shù)QObject::connect()的調(diào)用相連接(connect),或者利用uic的automatic connections的特性。

編譯這個示例

C++預(yù)編譯器會改變或去除關(guān)鍵字signals,slots,和emit,這樣就可以使用標(biāo)準(zhǔn)的C++編譯器。

在一個定義有信號和槽的類上運(yùn)行moc,這樣就會生成一個可以和其它對象文件編譯和連接成應(yīng)用程序的C++源文件。如果使用qmake工具,將會在你的makefile文件里加入自動調(diào)用moc的規(guī)則。

信號

當(dāng)對象的內(nèi)部狀態(tài)發(fā)生改變,信號就被發(fā)射,在某些方面對于對象代理或者所有者也許是很有趣的。只有定義了信號的對象或其子對象才能發(fā)射該信號。

當(dāng)一個信號被發(fā)出,被連接的槽通常會立刻運(yùn)行,就像執(zhí)行一個普通的函數(shù)調(diào)用。當(dāng)這一切發(fā)生時,信號和槽機(jī)制是完全獨(dú)立于任何GUI事件循環(huán)之外的。槽會在emit域下定義的代碼執(zhí)行完后返回。當(dāng)使用隊列連接(queued connections)時會有一些不同;這種情況下,關(guān)鍵字emit后的代碼會繼續(xù)執(zhí)行,而槽在此之后執(zhí)行。

如果幾個槽被連接到一個信號,當(dāng)信號被發(fā)出后,槽會以任意順序一個接一個的執(zhí)行。

關(guān)于參數(shù)需要注意:我們的經(jīng)驗顯示如果信號和槽不使用特殊的類型將會變得更具重用性。如果QScrollBar::valueChanged() 使用了一個特殊的類型,比如hypothetical QRangeControl::Range,它就只能被連接到被設(shè)計成可以處理QRangeControl的槽。再沒有象教程5這樣簡單的例子。



當(dāng)一個信號被發(fā)出時連接他的槽被調(diào)用。槽是一個普通的C++函數(shù)并按普通方式調(diào)用;他的特點僅僅是可以被信號連接。

由于槽只是普通的成員函數(shù),當(dāng)調(diào)用時直接遵循C++規(guī)則。然而,對于槽,他們可以被任何組件通過一個信號-槽連接(signal-slot connection)調(diào)用,而不管其訪問權(quán)限。也就是說,一個從任意的類的實例發(fā)出的信號可導(dǎo)致一個不與此類相關(guān)的另一個類的實例的私有槽被調(diào)用。

你還可以定義一個虛擬槽,在實踐中被發(fā)現(xiàn)也是非常有用的。

由于增加來靈活性,與回調(diào)相比,信號和槽稍微慢一些,盡管這對真實的應(yīng)用程序來說是可以忽略掉的。通常,發(fā)出一連接了某個槽的信號,比直接調(diào)用那些非虛擬調(diào)用的接受器要慢十倍。這是定位連接對象所需的開銷,可以安全地重復(fù)所有地連接(例如在發(fā)射期間檢查并發(fā)接收器是否被破壞)并且可以按一般的方式安排任何參數(shù)。當(dāng)十個非虛函數(shù)調(diào)用聽起來很多時,實際上他比任何new和delete操作的開銷都少,例如,當(dāng)你執(zhí)行一個字符串、矢量或列表操作時,就需要用到new和delete,而信號和槽的開銷只是全部函數(shù)調(diào)用花費(fèi)的一小部分。

無論何時你用槽進(jìn)行一個系統(tǒng)調(diào)用和間接的調(diào)用超過10個以上的函數(shù)時間都是一樣的。在i586-500機(jī)器上,每秒鐘你可以發(fā)送超過2,000,000個信號給一個接受者,或者每秒發(fā)送1,200,000個信號給兩個接受者。相對于信號和槽機(jī)制的簡潔性和靈活性,他的時間開銷是完全值得的,你的用戶甚至察覺不出來。

注意:若其他的庫將變量定義為signals和slots,可能導(dǎo)致編譯器在連接基于QT的應(yīng)用程序時出錯或警告。為了解決這個問題,請使用#undef預(yù)處理符號。

元對象信息

元對象編譯器(moc)解析一個C++文件中的類聲明并且生成初始化元對象的C++代碼。元對象包括信號和槽的名字,和指向這些函數(shù)的指針。

if (widget->inherits("QAbstractButton")) {
QAbstractButton *button = static_cast<QAbstractButton *>(widget);
button->toggle();
}

元對象信息的使用也可以是qobject_cast<T>(), 他和QObject::inherits() 相似,但更不容易出錯。

if (QAbstractButton *button = qobject_cast<QAbstractButton *>(widget))
button->toggle();

查看Meta-Object系統(tǒng)可獲取更多信息。

一個實例

這是一個注釋過的簡單的例子(代碼片斷選自qlcdnumber.h)。

#ifndef LCDNUMBER_H
#define LCDNUMBER_H

#include <QFrame>

class LcdNumber : public QFrame
{
Q_OBJECT

LcdNumber通過QFrame和QWiget繼承至QObject,它包含了大部分signal-slot知識。這是有點類似于內(nèi)置的QLCDNumber部件。

Q_OBJECT宏由預(yù)處理器展開,用來聲明由moc實現(xiàn)的機(jī)個成員函數(shù);如果你的編譯器出現(xiàn)錯誤如下"undefined reference to vtable for LcdNumber", 你可能忘了運(yùn)行moc或者沒有用連接命令包含moc輸出。

public:
LcdNumber(QWidget *parent = 0);

LcdNumber并不明顯的與moc相關(guān),但是如果你繼承了QWidege,那么可以幾乎肯定在你的構(gòu)造函數(shù)中有父對象的變量,并且希望把它傳給基類的構(gòu)造函數(shù)。

析構(gòu)函數(shù)和一些成員函數(shù)在這里省略;moc會忽視成員函數(shù)。

signals:
void overflow();

當(dāng)LcdNumbe被要求顯示一個不可能的值時,便發(fā)出信號。

如果你沒有留意溢出,或者你知道溢出不會出現(xiàn),你可以忽略overflow()信號,比如不將其連接到任何槽。

如果另一方面,當(dāng)有數(shù)字溢出時你想調(diào)用兩個不同的錯誤處理函數(shù),可以將這個信號簡單的連接到兩個不同的槽。QT將調(diào)用兩個函數(shù)(無序的)。

public slots:
void display(int num);
void display(double num);
void display(const QString &str);
void setHexMode();
void setDecMode();
void setOctMode();
void setBinMode();
void setSmallDecimalPoint(bool point);
};

#endif

一個槽是一個接受函數(shù),用于獲得其他窗口部件的信息變化。LcdNumber使用它,就像上面的代碼一樣,來設(shè)置顯示的數(shù)字。因為display()是這個類和程序的其它的部分的一個接口,所以這個槽是公有的。

幾個例程把QScrollBar的valueChanged()信號連接到display()槽,所以LCD數(shù)字可以繼續(xù)顯示滾動條的值。

請注意display()被重載了,當(dāng)將一個信號連接到槽時QT將選擇一個最適合的一個。而對于回調(diào),你會發(fā)現(xiàn)五個不同的名字并且自己來跟蹤類型。

一個不相干的成員函數(shù)在例子中被忽略。

高級信號和槽的使用

在當(dāng)你需要信號發(fā)送者的信息時,QT提供了一個函數(shù)QObject::sender(),他返回指向一個信號發(fā)送對象的指針。

當(dāng)有幾個信號被連接到同一槽上,并且槽需要處理每個不同的信號,可使用 QSignalMapper類。

假設(shè)你用三個按鈕來決定打開哪個文件:Tax File", "Accounts File", or "Report File"。

為了能打開真確的文件,你需要分別將它們的信號 QPushButton::clicked()連接到 readFile()。然后用QSignalMapper 的 setMapping()來映射所有 clicked()信號到一個 QSignalMapper對象。

signalMapper = new QSignalMapper(this);
signalMapper->setMapping(taxFileButton, QString("taxfile.txt"));
signalMapper->setMapping(accountFileButton, QString("accountsfile.txt"));
signalMapper->setMapping(reportFileButton, QString("reportfile.txt"));

connect(taxFileButton, SIGNAL(clicked()),
signalMapper, SLOT (map()));
connect(accountFileButton, SIGNAL(clicked()),
signalMapper, SLOT (map()));
connect(reportFileButton, SIGNAL(clicked()),
signalMapper, SLOT (map()));

然后,連接信號 mapped()到 readFile() ,根據(jù)被按下的按鈕,就可以打開不同的文件。

connect(signalMapper, SIGNAL(mapped(const QString &)),
this, SLOT(readFile(const QString &)));

在QT中使用第三方signals slots

在QT中使用第三方signals slots是可能的。你甚至可以在同一類中使用兩種機(jī)制。僅僅需要在你的qmake工程文件(.pro)中加入下面語句:

CONFIG += no_keywords

它告訴QT不要定義moc關(guān)鍵字signals,slots和emit,因為這些名字可能將被用于第三方庫,例如Boost。你只需簡單的用QT宏將他們替換為 Q_SIGNALS, Q_SLOTS,和 Q_EMIT,就可以繼續(xù)使用信號和槽了。

Qt源碼分析之信號和槽機(jī)制
2009/09/17 13:21

Qt的信號和槽機(jī)制是Qt的一大特點,實際上這是和MFC中的消息映射機(jī)制相似的東西,要完成的事情也差不多,就是發(fā)送一個消息然后讓其它窗口響應(yīng),當(dāng)然,這里的消息是廣義的
說法,簡單點說就是如何在一個類的一個函數(shù)中觸發(fā)另一個類的另一個函數(shù)調(diào)用,而且還要把相關(guān)的參數(shù)傳遞過去.好像這和回調(diào)函數(shù)也有點關(guān)系,但是消息機(jī)制可比回調(diào)函數(shù)有用
多了,也復(fù)雜多了

MFC中的消息機(jī)制沒有采用C++中的虛函數(shù)機(jī)制,原因是消息太多,虛函數(shù)開銷太大.在Qt中也沒有采用C++中的虛函數(shù)機(jī)制,原因與此相同.其實這里還有更深層次上的原因,大體說來,
多態(tài)的底層實現(xiàn)機(jī)制只有兩種,一種是按照名稱查表,一種是按照位置查表,兩種方式各有利弊,而C++的虛函數(shù)機(jī)制無條件的采用了后者,導(dǎo)致的問題就是在子類很少重載基類實現(xiàn)
的時候開銷太大,再加上象界面編程這樣子類眾多的情況,基本上C++的虛函數(shù)機(jī)制就廢掉了,于是各家?guī)斓木帉懻呔椭缓米灾\生路了,說到底,這確實是C++語言本身的缺陷

示例代碼:
#include <QApplication>
#include <QPushButton>
#include <QPointer>

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

    QPushButton quit("Quit");
quit.resize(100, 30);
quit.show();
QObject::connect(&quit, SIGNAL(clicked()), &app, SLOT(quit()));

return app.exec();
}

這里主要是看QPushButton的clicked()信號和app的quit()槽如何連接?又是如何響應(yīng)?
前面已經(jīng)說過了,Qt的信號槽機(jī)制其實就是按照名稱查表,因此這里的首要問題是如何構(gòu)造這個表?
和C++虛函數(shù)表機(jī)制類似的,在Qt中,這個表就是元數(shù)據(jù)表,Qt中的元數(shù)據(jù)表最大的作用就是支持信號槽機(jī)制,當(dāng)然,也可以在此基礎(chǔ)上擴(kuò)展出更多的功能,因為
元數(shù)據(jù)是我們可以直接訪問到的,不再是象虛函數(shù)表那樣被編譯器遮遮掩掩的藏了起來,不過Qt似乎還沒有完全發(fā)揮元數(shù)據(jù)的能力,動態(tài)屬性,反射之類的機(jī)制好像還沒有

任何從QObject派生的類都包含了自己的元數(shù)據(jù)模型,一般是通過宏Q_OBJECT定義的
#define Q_OBJECT \
public: \
static const QMetaObject staticMetaObject; \
virtual const QMetaObject *metaObject() const; \
virtual void *qt_metacast(const char *); \
QT_TR_FUNCTIONS \
virtual int qt_metacall(QMetaObject::Call, int, void **); \
private:

首先聲明了一個QMetaObject類型的靜態(tài)成員變量,這就是元數(shù)據(jù)的數(shù)據(jù)結(jié)構(gòu)

struct Q_CORE_EXPORT QMetaObject
{
...
struct { // private data
const QMetaObject *superdata;
const char *stringdata;
const uint *data;
const QMetaObject **extradata;
} d;
}
QMetaObject中有一個嵌套類封裝了所有的數(shù)據(jù)
const QMetaObject *superdata;//這是元數(shù)據(jù)代表的類的基類的元數(shù)據(jù)
const char *stringdata;//這是元數(shù)據(jù)的簽名標(biāo)記
const uint *data;//這是元數(shù)據(jù)的索引數(shù)組的指針
const QMetaObject **extradata;//這是擴(kuò)展元數(shù)據(jù)表的指針,一般是不用的       

這里的三個虛函數(shù)metaObject,qt_metacast,qt_metacall是在moc文件中定義的
metaObject的作用是得到元數(shù)據(jù)表指針
qt_metacast的作用是根據(jù)簽名得到相關(guān)結(jié)構(gòu)的指針,注意它返回的可是void*指針
qt_metacall的作用是查表然后調(diào)用調(diào)用相關(guān)的函數(shù)

宏QT_TR_FUNCTIONS是和翻譯相關(guān)的
#  define QT_TR_FUNCTIONS \
static inline QString tr(const char *s, const char *c = 0) \
{ return staticMetaObject.tr(s, c); }

好了,看看實際的例子吧:

QPushButton的元數(shù)據(jù)表如下:
static const uint qt_meta_data_QPushButton[] = {

 // content:
1,       // revision
0,       // classname
0,    0, // classinfo
2,   10, // methods
3,   20, // properties
0,    0, // enums/sets

 // slots: signature, parameters, type, tag, flags
13,   12,   12,   12, 0x0a,
24,   12,   12,   12, 0x08,

 // properties: name, type, flags
44,   39, 0x01095103,
56,   39, 0x01095103,
64,   39, 0x01095103,

       0        // eod
};

static const char qt_meta_stringdata_QPushButton[] = {
"QPushButton\0\0showMenu()\0popupPressed()\0bool\0autoDefault\0default\0"
"flat\0"
};

const QMetaObject QPushButton::staticMetaObject = {
{ &QAbstractButton::staticMetaObject, qt_meta_stringdata_QPushButton,
qt_meta_data_QPushButton, 0 }
};

在這里我們看到了靜態(tài)成員staticMetaObject被填充了
const QMetaObject *superdata;//這是元數(shù)據(jù)代表的類的基類的元數(shù)據(jù),被填充為基類的元數(shù)據(jù)指針&QAbstractButton::staticMetaObject
const char *stringdata;//這是元數(shù)據(jù)的簽名標(biāo)記,被填充為qt_meta_stringdata_QPushButton
const uint *data;//這是元數(shù)據(jù)的索引數(shù)組的指針,被填充為qt_meta_data_QPushButton
const QMetaObject **extradata;//這是擴(kuò)展元數(shù)據(jù)表的指針,一般是不用的,被填充為0

首先應(yīng)該看qt_meta_data_QPushButton,因為這里是元數(shù)據(jù)的主要數(shù)據(jù),它被填充為一個整數(shù)數(shù)組,正因為這里只有整數(shù),不能有任何字符串存在,因此才有
qt_meta_stringdata_QPushButton發(fā)揮作用的機(jī)會,可以說真正的元數(shù)據(jù)應(yīng)該是qt_meta_data_QPushButton加上qt_meta_stringdata_QPushButton,那么為什么
不把這兩個東西合在一起呢?估計是兩者都不是定長的結(jié)構(gòu),合在一起反而麻煩吧

qt_meta_data_QPushButton實際上是以以下結(jié)構(gòu)開頭的

struct QMetaObjectPrivate
{
int revision;
int className;
int classInfoCount, classInfoData;
int methodCount, methodData;
int propertyCount, propertyData;
int enumeratorCount, enumeratorData;
};

一般使用中是直接使用以下函數(shù)做個轉(zhuǎn)換
static inline const QMetaObjectPrivate *priv(const uint* data)
{ return reinterpret_cast<const QMetaObjectPrivate*>(data); }

這種轉(zhuǎn)換怎么看都有些黑客的味道,這確實是十足的C風(fēng)格

再結(jié)合實際的數(shù)據(jù)看一看
static const uint qt_meta_data_QPushButton[] = {

 // content:
1,       // revision  版本號是1
0,       // classname 類名存儲在qt_meta_stringdata_QPushButton中,索引是0,因此就是QPushButton了
0,    0, // classinfo  類信息數(shù)量為0,數(shù)據(jù)也是0
2,   10, // methods  QPushButton有2個自定義方法,方法數(shù)據(jù)存儲在qt_meta_data_QPushButton中,索引是10,就是下面的slots:開始的地方
3,   20, // properties QPushButton有3個自定義屬性,屬性數(shù)據(jù)存儲在qt_meta_data_QPushButton中,索引是20,就是下面的properties:開始的地方
0,    0, // enums/sets QPushButton沒有自定義的枚舉

 // slots: signature, parameters, type, tag, flags
13,   12,   12,   12, 0x0a,
第一個自定義方法的簽名存儲在qt_meta_data_QPushButton中,索引是13,就是showMenu()了
24,   12,   12,   12, 0x08,
第二個自定義方法的簽名存儲在qt_meta_data_QPushButton中,索引是24,popupPressed()了

 // properties: name, type, flags
44,   39, 0x01095103,  
第一個自定義屬性的簽名存儲在qt_meta_data_QPushButton中,索引是44,就是autoDefault了
第一個自定義屬性的類型存儲在qt_meta_data_QPushButton中,索引是39,就是bool
56,   39, 0x01095103,  
第二個自定義屬性的簽名存儲在qt_meta_data_QPushButton中,索引是56,就是default了
第二個自定義屬性的類型存儲在qt_meta_data_QPushButton中,索引是39,就是bool
64,   39, 0x01095103,  
第三個自定義屬性的簽名存儲在qt_meta_data_QPushButton中,索引是64,就是flat了
第三個自定義屬性的類型存儲在qt_meta_data_QPushButton中,索引是39,就是bool

       0        // eod 元數(shù)據(jù)的結(jié)束標(biāo)記
};

static const char qt_meta_stringdata_QPushButton[] = {
"QPushButton\0\0showMenu()\0popupPressed()\0bool\0autoDefault\0default\0"
"flat\0"
};

QPushButton\\showMenu()\popupPressed()\bool\autoDefault\default\flat\
這里把\0直接替換為\是為了數(shù)數(shù)的方便

當(dāng)然我們還可以看看QPushButton的基類QAbstractButton的元數(shù)據(jù)
static const uint qt_meta_data_QAbstractButton[] = {

 // content:
1,       // revision
0,       // classname
0,    0, // classinfo
12,   10, // methods
9,   70, // properties
0,    0, // enums/sets

 // signals: signature, parameters, type, tag, flags
17,   16,   16,   16, 0x05,
27,   16,   16,   16, 0x05,
46,   38,   16,   16, 0x05,
60,   16,   16,   16, 0x25,
70,   38,   16,   16, 0x05,

 // slots: signature, parameters, type, tag, flags
89,   84,   16,   16, 0x0a,
113,  108,   16,   16, 0x0a,
131,   16,   16,   16, 0x2a,
146,   16,   16,   16, 0x0a,
154,   16,   16,   16, 0x0a,
163,   16,   16,   16, 0x0a,
182,  180,   16,   16, 0x1a,

 // properties: name, type, flags
202,  194, 0x0a095103,
213,  207, 0x45095103,
224,  218, 0x15095103,
246,  233, 0x4c095103,
260,  255, 0x01095103,
38,  255, 0x01195103,
270,  255, 0x01095103,
281,  255, 0x01095103,
295,  255, 0x01094103,

       0        // eod
};

static const char qt_meta_stringdata_QAbstractButton[] = {
"QAbstractButton\0\0pressed()\0released()\0checked\0clicked(bool)\0"
"clicked()\0toggled(bool)\0size\0setIconSize(QSize)\0msec\0"
"animateClick(int)\0animateClick()\0click()\0toggle()\0setChecked(bool)\0"
"b\0setOn(bool)\0QString\0text\0QIcon\0icon\0QSize\0iconSize\0"
"QKeySequence\0shortcut\0bool\0checkable\0autoRepeat\0autoExclusive\0"
"down\0"
};

QAbstractButton00pressed()0released()0checked0clicked(bool)0clicked()0toggled(bool)0size0setIconSize(QSize)0msec0animateClick(int)0animateClick()0click()0toggle()0setChecked(bool)0b0setOn(bool)0QString0text0QIcon0icon0QSize0iconSize0QKeySequence0shortcut0bool0checkable0autoRepeat0autoExclusive0down0

基本上都是大同小異的

  QObject::connect(&quit, SIGNAL(clicked()), &app, SLOT(quit()));
下面開始看信號和槽連接的源碼了

// connect的源碼
connect函數(shù)是連接信號和槽的橋梁,非常關(guān)鍵
bool QObject::connect(const QObject *sender, const char *signal,
const QObject *receiver, const char *method,
Qt::ConnectionType type)
{
#ifndef QT_NO_DEBUG
bool warnCompat = true;
#endif
if (type == Qt::AutoCompatConnection) {
type = Qt::AutoConnection;
#ifndef QT_NO_DEBUG
warnCompat = false;
#endif
}

 // 不允許空輸入
if (sender == 0 || receiver == 0 || signal == 0 || method == 0) {
#ifndef QT_NO_DEBUG
qWarning("Object::connect: Cannot connect %s::%s to %s::%s",
sender ? sender->metaObject()->className() : "(null)",
signal ? signal+1 : "(null)",
receiver ? receiver->metaObject()->className() : "(null)",
method ? method+1 : "(null)");
#endif
return false;
}
QByteArray tmp_signal_name;

#ifndef QT_NO_DEBUG
// 檢查是否是信號標(biāo)記
if (!check_signal_macro(sender, signal, "connect", "bind"))
return false;
#endif
// 得到元數(shù)據(jù)類
const QMetaObject *smeta = sender->metaObject();
++signal; //skip code跳過信號標(biāo)記,直接得到信號標(biāo)識
// 得到信號的索引
int signal_index = smeta->indexOfSignal(signal);
if (signal_index < 0) {
// check for normalized signatures
tmp_signal_name = QMetaObject::normalizedSignature(signal).prepend(*(signal - 1));
signal = tmp_signal_name.constData() + 1;
signal_index = smeta->indexOfSignal(signal);
if (signal_index < 0) {
#ifndef QT_NO_DEBUG
err_method_notfound(QSIGNAL_CODE, sender, signal, "connect");
err_info_about_objects("connect", sender, receiver);
#endif
return false;
}
}

    QByteArray tmp_method_name;
int membcode = method[0] - '0';

#ifndef QT_NO_DEBUG
// 檢查是否是槽,用QSLOT_CODE 1標(biāo)記
if (!check_method_code(membcode, receiver, method, "connect"))
return false;
#endif
++method; // skip code

  // 得到元數(shù)據(jù)類
const QMetaObject *rmeta = receiver->metaObject();
int method_index = -1;
// 這里是一個case,信號即可以和信號連接也可以和槽連接
switch (membcode) {
case QSLOT_CODE:
// 得到槽的索引
method_index = rmeta->indexOfSlot(method);
break;
case QSIGNAL_CODE:
// 得到信號的索引
method_index = rmeta->indexOfSignal(method);
break;
}
if (method_index < 0) {
// check for normalized methods
tmp_method_name = QMetaObject::normalizedSignature(method);
method = tmp_method_name.constData();
switch (membcode) {
case QSLOT_CODE:
method_index = rmeta->indexOfSlot(method);
break;
case QSIGNAL_CODE:
method_index = rmeta->indexOfSignal(method);
break;
}
}

    if (method_index < 0) {
#ifndef QT_NO_DEBUG
err_method_notfound(membcode, receiver, method, "connect");
err_info_about_objects("connect", sender, receiver);
#endif
return false;
}
#ifndef QT_NO_DEBUG
// 檢查參數(shù),信號和槽的參數(shù)必須一致,槽的參數(shù)也可以小于信號的參數(shù)
if (!QMetaObject::checkConnectArgs(signal, method)) {
qWarning("Object::connect: Incompatible sender/receiver arguments"
"\n\t%s::%s --> %s::%s",
sender->metaObject()->className(), signal,
receiver->metaObject()->className(), method);
return false;
}
#endif

    int *types = 0;
if (type == Qt::QueuedConnection
&& !(types = ::queuedConnectionTypes(smeta->method(signal_index).parameterTypes())))
return false;

#ifndef QT_NO_DEBUG
{
// 得到方法的元數(shù)據(jù)
QMetaMethod smethod = smeta->method(signal_index);
QMetaMethod rmethod = rmeta->method(method_index);
if (warnCompat) {
if(smethod.attributes() & QMetaMethod::Compatibility) {
if (!(rmethod.attributes() & QMetaMethod::Compatibility))
qWarning("Object::connect: Connecting from COMPAT signal (%s::%s).", smeta->className(), signal);
} else if(rmethod.attributes() & QMetaMethod::Compatibility && membcode != QSIGNAL_CODE) {
qWarning("Object::connect: Connecting from %s::%s to COMPAT slot (%s::%s).",
smeta->className(), signal, rmeta->className(), method);
}
}
}
#endif
// 調(diào)用元數(shù)據(jù)類的連接
QMetaObject::connect(sender, signal_index, receiver, method_index, type, types);
// 發(fā)送連接的通知,現(xiàn)在的實現(xiàn)是空的
const_cast<QObject*>(sender)->connectNotify(signal - 1);
return true;
}

檢查信號標(biāo)記其實比較簡單,就是用signal的第一個字符和用QSIGNAL_CODE=2的標(biāo)記比較而已
static bool check_signal_macro(const QObject *sender, const char *signal,
const char *func, const char *op)
{
int sigcode = (int)(*signal) - '0';
if (sigcode != QSIGNAL_CODE) {
if (sigcode == QSLOT_CODE)
qWarning("Object::%s: Attempt to %s non-signal %s::%s",
func, op, sender->metaObject()->className(), signal+1);
else
qWarning("Object::%s: Use the SIGNAL macro to %s %s::%s",
func, op, sender->metaObject()->className(), signal);
return false;
}
return true;
}

得到信號的索引實際上要依次找每個基類的元數(shù)據(jù),得到的偏移也是所有元數(shù)據(jù)表加在一起后的一個索引
int QMetaObject::indexOfSignal(const char *signal) const
{
int i = -1;
const QMetaObject *m = this;
while (m && i < 0) {
// 根據(jù)方法的數(shù)目倒序的查找
for (i = priv(m->d.data)->methodCount-1; i >= 0; --i)
// 得到該方法的類型
if ((m->d.data[priv(m->d.data)->methodData + 5*i + 4] & MethodTypeMask) == MethodSignal
&& strcmp(signal, m->d.stringdata
// 得到方法名稱的偏移
+ m->d.data[priv(m->d.data)->methodData + 5*i]) == 0) {
//如果找到了正確的方法,再增加所有基類的方法偏移量
i += m->methodOffset();
break;
}
// 在父類中繼續(xù)找
m = m->d.superdata;
}
#ifndef QT_NO_DEBUG
// 判斷是否于基類中的沖突
if (i >= 0 && m && m->d.superdata) {
int conflict = m->d.superdata->indexOfMethod(signal);
if (conflict >= 0)
qWarning("QMetaObject::indexOfSignal:%s: Conflict with %s::%s",
m->d.stringdata, m->d.superdata->d.stringdata, signal);
}
#endif
return i;
}

// 這里是所有基類的方法偏移量算法,就是累加基類所有的方法數(shù)目
int QMetaObject::methodOffset() const
{
int offset = 0;
const QMetaObject *m = d.superdata;
while (m) {
offset += priv(m->d.data)->methodCount;
m = m->d.superdata;
}
return offset;
}

// 得到方法的元數(shù)據(jù)
QMetaMethod QMetaObject::method(int index) const
{
int i = index;
// 要減去基類的偏移
i -= methodOffset();
// 如果本類找不到,就到基類中去找
if (i < 0 && d.superdata)
return d.superdata->method(index);

 // 如果找到了,就填充QMetaMethod結(jié)構(gòu)
QMetaMethod result;
if (i >= 0 && i < priv(d.data)->methodCount) {
// 這里是類的元數(shù)據(jù)
result.mobj = this;
// 這里是方法相關(guān)數(shù)據(jù)在data數(shù)組中的偏移量
result.handle = priv(d.data)->methodData + 5*i;
}
return result;
}

bool QMetaObject::connect(const QObject *sender, int signal_index,
const QObject *receiver, int method_index, int type, int *types)
{
// 得到全局的連接列表
QConnectionList *list = ::connectionList();
if (!list)
return false;
QWriteLocker locker(&list->lock);
// 增加一個連接
list->addConnection(const_cast<QObject *>(sender), signal_index,
const_cast<QObject *>(receiver), method_index, type, types);
return true;
}

void QConnectionList::addConnection(QObject *sender, int signal,
QObject *receiver, int method,
int type, int *types)
{
// 構(gòu)造一個連接
QConnection c = { sender, signal, receiver, method, 0, 0, types };
c.type = type; // don't warn on VC++6
int at = -1;
// 如果有中間被刪除的連接,可以重用這個空間
for (int i = 0; i < unusedConnections.size(); ++i) {
if (!connections.at(unusedConnections.at(i)).inUse) {
// reuse an unused connection
at = unusedConnections.takeAt(i);
connections[at] = c;
break;
}
}
if (at == -1) {
// append new connection
at = connections.size();
// 加入一個連接
connections << c;
}
// 構(gòu)造sender,receiver連接的哈希表,加速搜索速度
sendersHash.insert(sender, at);
receiversHash.insert(receiver, at);
}

通過connect函數(shù),我們建立了信號和槽的連接,并且把信號類+信號索引+槽類,槽索引作為記錄寫到了全局的connect列表中

一旦我們發(fā)送了信號,就應(yīng)該調(diào)用相關(guān)槽中的方法了,這個過程其實就是查找全局的connect列表的過程,當(dāng)然還要注意其中要對相關(guān)的參數(shù)打包和解包

// emit是發(fā)送信號的代碼
void Foo::setValue(int v)
{
if (v != val)
{
val = v;
emit valueChanged(v);
}
}

// 發(fā)送信號的真正實現(xiàn)在moc里面
// SIGNAL 0
void Foo::valueChanged(int _t1)
{
// 首先把參數(shù)打包
void *_a[] = { 0, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) };
// 調(diào)用元數(shù)據(jù)類的激活
QMetaObject::activate(this, &staticMetaObject, 0, _a);
}

void QMetaObject::activate(QObject *sender, const QMetaObject *m, int local_signal_index,
void **argv)
{
// 增加一個基類偏移量
int offset = m->methodOffset();
activate(sender, offset + local_signal_index, offset + local_signal_index, argv);
}

void QMetaObject::activate(QObject *sender, int from_signal_index, int to_signal_index, void **argv)
{
// 這里得到的是QObject的數(shù)據(jù),首先判斷是否為阻塞設(shè)置
if (sender->d_func()->blockSig)
return;

 // 得到全局鏈表
QConnectionList * const list = ::connectionList();
if (!list)
return;

    QReadLocker locker(&list->lock);

    void *empty_argv[] = { 0 };
if (qt_signal_spy_callback_set.signal_begin_callback != 0) {
locker.unlock();
qt_signal_spy_callback_set.signal_begin_callback(sender, from_signal_index,
argv ? argv : empty_argv);
locker.relock();
}

 // 在sender的哈希表中得到sender的連接
QConnectionList::Hash::const_iterator it = list->sendersHash.find(sender);
const QConnectionList::Hash::const_iterator end = list->sendersHash.constEnd();

    if (it == end) {
if (qt_signal_spy_callback_set.signal_end_callback != 0) {
locker.unlock();
qt_signal_spy_callback_set.signal_end_callback(sender, from_signal_index);
locker.relock();
}
return;
}

    QThread * const currentThread = QThread::currentThread();
const int currentQThreadId = currentThread ? QThreadData::get(currentThread)->id : -1;

 // 記錄sender連接的索引
QVarLengthArray<int> connections;
for (; it != end && it.key() == sender; ++it) {
connections.append(it.value());
// 打上使用標(biāo)記,因為可能是放在隊列中
list->connections[it.value()].inUse = 1;
}

    for (int i = 0; i < connections.size(); ++i) {
const int at = connections.constData()[connections.size() - (i + 1)];
QConnectionList * const list = ::connectionList();
// 得到連接
QConnection &c = list->connections[at];
c.inUse = 0;
if (!c.receiver || (c.signal < from_signal_index || c.signal > to_signal_index))
continue;

  // 判斷是否放到隊列中
// determine if this connection should be sent immediately or
// put into the event queue
if ((c.type == Qt::AutoConnection
&& (currentQThreadId != sender->d_func()->thread
|| c.receiver->d_func()->thread != sender->d_func()->thread))
|| (c.type == Qt::QueuedConnection)) {
::queued_activate(sender, c, argv);
continue;
}

  // 為receiver設(shè)置當(dāng)前發(fā)送者
const int method = c.method;
QObject * const previousSender = c.receiver->d_func()->currentSender;
c.receiver->d_func()->currentSender = sender;
list->lock.unlock();

        if (qt_signal_spy_callback_set.slot_begin_callback != 0)
qt_signal_spy_callback_set.slot_begin_callback(c.receiver, method, argv ? argv : empty_argv);

#if defined(QT_NO_EXCEPTIONS)
c.receiver->qt_metacall(QMetaObject::InvokeMetaMethod, method, argv ? argv : empty_argv);
#else
try {
// 調(diào)用receiver的方法
c.receiver->qt_metacall(QMetaObject::InvokeMetaMethod, method, argv ? argv : empty_argv);
} catch (...) {
list->lock.lockForRead();
if (c.receiver)
c.receiver->d_func()->currentSender = previousSender;
throw;
}
#endif

        if (qt_signal_spy_callback_set.slot_end_callback != 0)
qt_signal_spy_callback_set.slot_end_callback(c.receiver, method);

        list->lock.lockForRead();
if (c.receiver)
c.receiver->d_func()->currentSender = previousSender;
}

    if (qt_signal_spy_callback_set.signal_end_callback != 0) {
locker.unlock();
qt_signal_spy_callback_set.signal_end_callback(sender, from_signal_index);
locker.relock();
}
}

// 響應(yīng)信號也是在moc里實現(xiàn)的
int Foo::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{
// 首先在基類中調(diào)用方法,返回的id已經(jīng)變成當(dāng)前類的方法id了
_id = QObject::qt_metacall(_c, _id, _a);
if (_id < 0)
return _id;
if (_c == QMetaObject::InvokeMetaMethod) {
switch (_id) {
// 這里就是真正的調(diào)用方法了,注意參數(shù)的解包用法
case 0: valueChanged(*reinterpret_cast< int(*)>(_a[1])); break;
case 1: setValue(*reinterpret_cast< int(*)>(_a[1])); break;
}
_id -= 2;
}
return _id;
}