本文使用 ISO C++ 一步一步實現了一個極度簡化的信號與槽的系統 (整個程序4個文件共121行代碼) 。希望能有助於剛進入Qt世界的C++用戶理解Qt最核心的信號槽與元對象系統是如何工作的。
另:你可能會對 從 C++ 到 Qt 一文感興趣
dbzhang800 2011.04.30
注:Qt5 staging倉庫已經引入一種全新的信號與槽的語法:信號可以和普通的函數、類的普通成員函數、lambda函數連接(而不再侷限於信號函數和槽函數),詳見 信號與槽的新語法(Qt5) dbzhang800 2011.06.15
Qt信號與槽
GUI程序中,當我們我們點擊一個按鈕時,我們會期待我們自定義的某個函數被調用。對此,較老的工具集(toolkits)都是通過回調函數(callback)來實現的,Qt的神奇之處就在於,它使用信號(signal)與槽(slot)的技術來取代了回調。
在繼續之前,我們先看一眼最最常用的 connnect 函數:
connect(btn, "2clicked()", this, "1onBtnClicked()")
可能你會覺得稍有點眼生,因爲爲了清楚起見,我沒有直接使用大家很熟悉的SIGNAL和SLOT兩個宏,宏定義如下:
-
# define SLOT(a) "1"#a # define SIGNAL(a) "2"#a
程序運行時,connect藉助兩個字符串,即可將信號與槽的關聯建立起來,那麼,它是如果做到的呢?C++的經驗可以告訴我們:
- 類中應該保存有信號和槽的字符串信息
- 字符串和信號槽函數要關聯
而這,就是通過神奇的元對象系統所實現的(Qt的元對象系統預處理器叫做moc,對文件預處理之後生成一個moc_xxx.cpp文件,然後和其他文件一塊編譯即可)。
接下來,我們不妨嘗試用純C++來實現自己的元對象系統(我們需要有一個自己的預處理器,本文中用雙手來代替了,預處理生成的文件是db_xxx.cpp)。
繼續之前,我們可以先看一下我們最終的類定義
class Object
{
DB_OBJECT
public:
Object();
virtual ~Object();
static void db_connect(Object *, const char *, Object *, const char *);
void testSignal();
db_signals:
void sig1();
public db_slots:
void slot1();
friend class MetaObject;
private:
ConnectionMap connections;
};
引入元對象系統
首先定義自己的信號和槽
- 爲了和普通成員進行區別(以使得預處理器可以知道如何提取信息),我們需要創造一些"關鍵字"
- db_signals
- db_slots
class Object
{
public:
Object();
virtual ~Object();
db_signals:
void sig1();
public db_slots:
void slot1();
};
- 通過自己的預處理器,將信息提取取來,放置到一個單獨的文件中(比如db_object.cpp):
- 規則很簡單,將信號和槽的名字提取出來,放到字符串中。可以有多個信號或槽,按順序"sig1/nsig2/n"
static const char sig_names[] = "sig1/n";
static const char slts_names[] = "slot1/n";
- 這些信號和槽的信息,如何才能與類建立關聯,如何被訪問呢?
我們可以定義一個類,來存放信息:
struct MetaObject
{
const char * sig_names;
const char * slts_names;
};
然後將其作爲一個Object的靜態成員(注意哦,這就是我們的元對象啦 ):
class Object
{
static MetaObject meta;
...
這樣一來,我們的預處理器可以生成這樣的 db_object.cpp 文件:
#include "object.h"
static const char sig_names[] = "sig1/n";
static const char slts_names[] = "slot1/n";
MetaObject Object::meta = {sig_names, slts_names};
信息提取的問題解決了:可是,還有一個嚴重問題,我們定義的關鍵字 C++ 編譯器不認識啊,怎麼辦?
呵呵,好辦,通過定義一下宏,問題是不是解決了:
-
# define db_slots # define db_signals protected
建立信號槽鏈接
我們的最終目的就是:當信號被觸發的時候,能找到並觸發相應的槽。所以有了信號和槽的信息,我們就可以建立信號和槽的連接了。我們通過 db_connect 將信號和槽的對應關係保存到一個 mutlimap 中:
struct Connection
{
Object * receiver;
int method;
};
class Object
{
public:
...
static void db_connect(Object*, const char*, Object*, const char*);
...
private:
std::multimap<int, Connection> connections;
上面應該不需要什麼解釋了,我們直接看看db_connect該怎麼寫:
void Object::db_connect(Object* sender, const char* sig, Object* receiver, const char* slt)
{
int sig_idx = find_string(sender->meta.sig_names, sig);
int slt_idx = find_string(receiver->meta.slts_names, slt);
if (sig_idx == -1 || slt_idx == -1) {
perror("signal or slot not found!");
} else {
Connection c = {receiver, slt_idx};
sender->connections.insert(std::pair<int, Connection>(sig_idx, c));
}
}
首先從元對象信息中查找信號和槽的名字是否存在,如果存在,則將信號的索引和接收者的信息存入信號發送者的的一個map中。如果信號或槽無效,就什麼都不用做了。
我們這兒定義了一個find_string函數,就是個簡單的字符串查找(此處就不列出了)。
信號的激活
連接信息有了,我們看看信號到底是怎麼發出的。
在 Qt 中,我們都知道用 emit 來發射信號:
class Object
{
public:
void testSignal()
...
};
void Object::testSignal()
{
db_emit sig1();
}
這兒 db_emit 是神馬東西?C++編譯器不認識啊,沒關係,看仔細嘍,加一行就行了
#define db_emit
從前面我的Object定義中可以看到,所謂的信號或槽,都只是普普通通的C++類的成員函數。既然是成員函數,就需要函數定義:
- 槽函數:由於它包含我們需要的功能代碼,我們都會想到在 object.cpp 文件中去定義它,不存在問題。
- 信號函數:它的函數體不需要自己編寫。那麼它在哪兒呢?這就是本節的內容了
信號函數由我們的"預處理器"來生成,也就是它要定義在我們的 db_object.cpp 文件中:
void Object::sig1()
{
MetaObject::active(this, 0);
}
我們預處理源文件時,就知道它是第幾個信號。所以根據它的索引去調用和它關聯的槽即可。具體工作交給了MetaObject類:
class Object;
struct MetaObject
{
const char * sig_names;
const char * slts_names;
static void active(Object * sender, int idx);
};
這個函數該怎麼寫呢:思路很簡單
- 從前面的保存連接的map中,找出與該信號關聯的對象和槽
- 調用該對象這個槽
typedef std::multimap<int, Connection> ConnectionMap;
typedef std::multimap<int, Connection>::iterator ConnectionMapIt;
void MetaObject::active(Object* sender, int idx)
{
ConnectionMapIt it;
std::pair<ConnectionMapIt, ConnectionMapIt> ret;
ret = sender->connections.equal_range(idx);
for (it=ret.first; it!=ret.second; ++it) {
Connection c = (*it).second;
//c.receiver->metacall(c.method);
}
}
補遺:
槽的調用
這個最後一個關鍵問題了,槽函數如何根據一個索引值進行調用。
- 直接調用槽函數我們都知道了,就一個普通函數
- 可現在通過索引調用了,那麼我們必須定義一個接口函數
class Object
{
void metacall(int idx);
...
該函數如何實現呢?這個又回到我們的元對象預處理過程中了,因爲在預處理的過程,我們能將槽的索引和槽的調用關聯起來。
所以,在預處理生成的文件(db_object.cpp)中,我們很容易生成其定義:
void Object::metacall(int idx)
{
switch (idx) {
case 0:
slot1();
break;
default:
break;
};
}
至此,我們已經實現的一個簡化的自己的信號與槽的程序。下面我們總體上看看程序的所有代碼:
全家福
- 類定義文件 object.h
#ifndef DB_OBJECT
#define DB_OBJECT
#include <map>
# define db_slots
# define db_signals protected
# define db_emit
class Object;
struct MetaObject
{
const char * sig_names;
const char * slts_names;
static void active(Object * sender, int idx);
};
struct Connection
{
Object * receiver;
int method;
};
typedef std::multimap<int, Connection> ConnectionMap;
typedef std::multimap<int, Connection>::iterator ConnectionMapIt;
class Object
{
static MetaObject meta;
void metacall(int idx);
public:
Object();
virtual ~Object();
static void db_connect(Object*, const char*, Object*, const char*);
void testSignal();
db_signals:
void sig1();
public db_slots:
void slot1();
friend class MetaObject;
private:
ConnectionMap connections;
};
#endif
- 類實現文件 object.cpp
#include <stdio.h>
#include <string.h>
#include "object.h"
Object::Object()
{
}
Object::~Object()
{
}
static int find_string(const char * str, const char * substr)
{
if (strlen(str) < strlen(substr))
return -1;
int idx = 0;
int len = strlen(substr);
bool start = true;
const char * pos = str;
while (*pos) {
if (start && !strncmp(pos, substr, len) && pos[len]=='/n')
return idx;
start = false;
if (*pos == '/n') {
idx++;
start = true;
}
pos++;
}
return -1;
}
void Object::db_connect(Object* sender, const char* sig, Object* receiver, const char* slt)
{
int sig_idx = find_string(sender->meta.sig_names, sig);
int slt_idx = find_string(receiver->meta.slts_names, slt);
if (sig_idx == -1 || slt_idx == -1) {
perror("signal or slot not found!");
} else {
Connection c = {receiver, slt_idx};
sender->connections.insert(std::pair<int, Connection>(sig_idx, c));
}
}
void Object::slot1()
{
printf("hello dbzhang800!");
}
void MetaObject::active(Object* sender, int idx)
{
ConnectionMapIt it;
std::pair<ConnectionMapIt, ConnectionMapIt> ret;
ret = sender->connections.equal_range(idx);
for (it=ret.first; it!=ret.second; ++it) {
Connection c = (*it).second;
c.receiver->metacall(c.method);
}
}
void Object::testSignal()
{
db_emit sig1();
}
- 我們自己的預處理需要生成這樣一個文件 db_object.cpp
- 注意看這個文件:其實內容非常簡單
- 將信號和槽的信息存放到字符串中 ==>按順序排放,所以有了索引值
- 信號發射 其實就是 信號函數==> 信號的索引
- metacall 其實就是 槽的索引==> 槽函數
#include "object.h"
static const char sig_names[] = "sig1/n";
static const char slts_names[] = "slot1/n";
MetaObject Object::meta = {sig_names, slts_names};
void Object::sig1()
{
MetaObject::active(this, 0);
}
void Object::metacall(int idx)
{
switch (idx) {
case 0:
slot1();
break;
default:
break;
};
}
- 最後,我們可以寫一個小小的例子main.cpp :
#include "object.h"
int main()
{
Object obj1, obj2;
Object::db_connect(&obj1, "sig1", &obj2, "slot1");
obj1.testSignal();
return 0;;
}
- 程序的編譯就不用多數了,用你熟悉的msvc或者g++
cl main.cpp object.cpp db_object.cpp -o dbzhang800
g++ main.cpp object.cpp db_object.cpp -o dbzhang800
零零散散,寫在後面
我不確定是不是已經元對象系統和信號槽最基本的概念表達清楚了。反正我想,如果你對Qt感興趣,相對Qt的信號和槽進一步的瞭解,但是目前尚對閱讀Qt的源碼覺得無比恐怖,本文可能會對你有幫助。
文中將東西精簡到我個人能做到的極限了,所以有很多很多沒提到的東西:
Q_OBJECT
用Qt,我們都知道這個宏,可是我們前面壓根沒提。因爲我怕打亂思路,這兒補上吧。我的前面的代碼可以替換爲:
# define DB_OBJECT static MetaObject meta; void metacall(int idx);
class Object
{
DB_OBJECT
DB_OBJECT 還可以作爲一個標記:如果我們寫好了自己的類似於Qt中的moc的預處理器,如何判斷一個文件是否需要預處理來生成 db_object.cpp 文件呢?此時就可以根據類定義中是否有宏來判斷。
題外: 爲什麼添加宏後會容易遇到鏈接錯誤?你能看到原因麼?因爲它展開後就是類的成員,可是其定義要通過預處理進行生成。如果你沒有運行預處理器,也就沒有 db_object.cpp 這種文件,肯定要出錯了。
Connection
我們前面在Connection只保存了接收者的指針和槽的索引,我們可以保存更多一點的信息的:可以看看Qt保存了哪些東西
QObjectPrivate::Connection *c = new QObjectPrivate::Connection;
c->sender = s;
c->receiver = r;
c->method = method_index;
c->connectionType = type;
c->argumentTypes = types;
c->nextConnectionList = 0;
應該很容易看懂,不做解釋了。
Qt中信號和槽主要有直接連接和隊列連接兩種方式,我們這兒只提到了前者,後者和Qt的事件系統攪和在一起。只要搞清楚了Qt事件系統,就會發現和直接連接沒有什麼區別了。
其他
信號和槽的參數
這個,例子中舉的都是信號和槽都是無參數的例子。加上參數,儘管概念上沒變化,但複雜度就大大提高了。所以本文對此不想涉及,也沒必要吧,直接去看Qt的源碼吧。
信號和信號連接
信號和槽一樣,都可以被調用,本例進行擴展也很容易,需要metacall那個函數,以及信號和槽要加個區別的標記(回到最前面不妨看看Qt的SLOT和SIGNAL究竟是神馬東西)。