訊號(Signal)與槽(Slot)-Qt中的典型機制
因為下一篇關於Boost的文章會涉及到事件處理的問題,裡面用的是訊號和槽的機制,先拿Qt裡的這個機制預研一下。誒,Boost這是夠厲害的,什麼先進就包含什麼!要知道我以前一直以為這是Qt的專利呢。當然,這也是大多數厲害的開源軟體庫的高人之處,像Qt這種GUI庫也包含了很多資料庫,字元處理等內容,多學學吧!
signal/slot是Qt物件以及其派生類物件之間的一種高效通訊介面,它是Qt的核心特性,也是區別與其他工具包的重要地方。它完全獨立於標準的C/C++語言,因此用正確的處理好訊號和槽,必須藉助於一個成為MOC(Meta Object Compiler)的qt工具,該工具是一個C++預處理程式,能為高層次的事件處理自動生成所需要的附加程式碼。
儘管它的機制很像回撥函式,但是這裡要注意它和與回撥函式間的不同,回撥函式傳遞的是函式指標,很容易造成程式崩潰,另一方面,回撥方式緊緊的綁定了圖形使用者介面的功能元素,因此很難開發進行獨立的分類。而signal/slot機制也能攜帶任意數量和任意引數,並且不會像回撥函式那樣產生core dumps。
訊號signal和槽Slot是用來在物件間通訊的方法,當一個特定事件發生的時候,signal會被emit出來,slot呼叫是用來響應相應的signal的。QT物件已經包含了許多預定義的 signal,但我們總是可以在派生類中新增新的signal。QT物件中也已經包含了許多預定義的slog,但我們可以在派生類中新增新的slot來處理我們感興趣的signal。
signal 和 slot 機制是型別安全的,signal 和 slot必須互相匹配(實際上,一個solt的引數可以比對應的signal的引數少,因為它可以忽略多餘的引數)。signal 和 slot是鬆散的配對關係,發出signal的物件不關心是那個物件連結了 這個signal,也不關心是那個或者有多少slot連結到了這個 signal。QT的signal 和 slot機制保證了,如果一個signal和slot相連結,slot會在正確的時機被呼叫,並且是使用正確的引數。Signal和slot都可以攜帶任何數量和型別的引數,他們都是型別安全的。
所有從QObject直接或者間接繼承出來的類都能包含訊號和槽,當一個物件的狀態發生變化的時候,訊號就可以被emit出來,這可能是某個其它的 物件所 關心的。這個物件並不關心有那個物件或者多少個物件連結到這個訊號了,這是真實的資訊封裝,它保證了這個物件可以作為一個軟體元件來被使用。
槽(slot)是用來接收訊號的,但同時他們也是一個普通的類成員函式,就象一個物件不關心有多少個槽連結到了它的某個訊號,一個物件也不關心一個槽連結了多少個訊號。這保證了用QT建立的物件是一個真實的獨立的軟體元件。
一個訊號可以連結到多個槽,一個槽也可以連結多個訊號。同時,一個訊號也可以連結到另外一個訊號。所有使用了訊號和槽的類都必須包含 Q_OBJECT 巨集,而且這個類必須從QObject類派生(直接或者間接派生)出來,
當一個signal被emit出來的時候,連結到這個signal的slot會立刻被呼叫,就好像是一個函式呼叫一樣。當這件事情發生的時候,signal和slot機制與GUI的事件迴圈完全沒有關係,當所有連結到這個signal的slot執行完成之後,在 emit 程式碼行之後的程式碼會立刻被執行。當有多個slot連結到一個signal的時候,這些slot會一個接著一個的、以隨機的順序被執行。
Signal 程式碼會由 moc 自動生成,開發人員一定不能在自己的C++程式碼中實現它,並且,它永遠都不能有返回值。Slot其實就是一個普通的類函式,並且可以被直接呼叫,唯一特殊的地方是它可以與signal相連結。C++的前處理器更改或者刪除 signal, slot, emit 關鍵字,所以,對於C++編譯器來說,它處理的是標準的C++原始檔。
此外,使用者可以將N多個訊號和單個槽相連線,或者將將N個槽和單個訊號連線,甚至是一個訊號和另外一個訊號連線。這樣,當訊號發射時,所以與之相連的訊號或者槽都會按一定的次序(沒有預定的順序,也就是說執行的先後順序是隨機的)執行,當所有與之相連的訊號和槽返回後,emit才會返回。
下面是Qt中關於訊號和槽的函式,Qt中訊號的定義:
siganls:
void mySignal();
void mySignal( int x );
void mySignal( int x, int y );
其中signals是Qt的關鍵字,而不是C/C++的關鍵字。此外訊號與一般函式的區別是,它的所有返回值都是void,並且它沒有函式實現體,它的函式體是moc自動生成的。
Qt中槽的定義:
public slots:
void mySlot();
void mySlot( int x );
不同型別的slot有不同的操作許可權,具體看slot是public、protected還是private。
Qt中訊號與訊號或者與槽的連線:
QObeject::connect( obj1, SIGNAL( mySignal() ), obj2, SLOT( mySlot() ) );
QObeject::connect( obj1, SIGNAL( mySignal() ), obj2, SIGNAL( mySignal2() ) );
Qt中訊號與槽的斷開:
QObeject::disconnect( obj1, SIGNAL( mySignal() ), obj2, SLOT( mySlot() ) );
QObeject::disconnect( obj1, SIGNAL( mySignal() ), obj2, SIGNAL( mySignal2() ) );
這種機制GUI控制元件的操作來說很是方便,當然也要用的恰當,用的規範和科學。
下面是一個簡單的樣例程式,程式中定義了三個訊號和三個槽函式,然後將訊號與槽進行了關聯,每個槽函式都只是彈出一個視窗(widget),訊號和槽函式的宣告一般位於標頭檔案中,同時在類宣告的開始位置必須加上Q_OBJECT語句,這條語句是不可缺少的,它將告訴編譯器在編譯之前必須先應用 moc工具進行擴充套件。關鍵字signals指出隨後開始訊號的宣告,這裡signals用的是複數形式而非單數,siganls沒有public、 private、protected等屬性,這點不同於slots。另外,signals、slots關鍵字是QT自己定義的,不是C++中的關鍵字。訊號的宣告類似於函式的宣告而非變數的宣告,左邊要有型別,右邊要有括號,如果要向槽中傳遞引數的話,在括號中指定每個形式引數的型別,當然,形式引數的個數可以多於一個。
//tsignal.h
...
class TsignalApp:public QMainWindow
{
Q_OBJECT
...
//訊號宣告區
signals:
//宣告訊號mySignal()
void mySignal();
//宣告訊號mySignal(int)
void mySignal(int x);
//宣告訊號mySignalParam(int,int)
void mySignalParam(int x,int y);
//槽宣告區
public slots:
//宣告槽函式mySlot()
void mySlot();
//宣告槽函式mySlot(int)
void mySlot(int x);
//宣告槽函式mySignalParam (int,int)
void mySignalParam(int x,int y);
}
...
//tsignal.cpp
...
TsignalApp::TsignalApp()
{
...
//將訊號mySignal()與槽mySlot()相關聯
connect(this,SIGNAL(mySignal()),SLOT(mySlot()));
//將訊號mySignal(int)與槽mySlot(int)相關聯
connect(this,SIGNAL(mySignal(int)),SLOT(mySlot(int)));
//將訊號mySignalParam(int,int)與槽mySlotParam(int,int)相關聯
connect(this,SIGNAL(mySignalParam(int,int)),SLOT(mySlotParam(int,int)));
}
// 定義槽函式mySlot()
void TsignalApp::mySlot()
{
QMessageBox::about(this,"Tsignal", "This is a signal/slot sample without
parameter.");
}
// 定義槽函式mySlot(int)
void TsignalApp::mySlot(int x)
{
QMessageBox::about(this,"Tsignal", "This is a signal/slot sample with one
parameter.");
}
// 定義槽函式mySlotParam(int,int)
void TsignalApp::mySlotParam(int x,int y)
{
char s[256];
sprintf(s,"x:%d y:%d",x,y);
QMessageBox::about(this,"Tsignal", s);
}
void TsignalApp::slotFileNew()
{
//發射訊號mySignal()
emit mySignal();
//發射訊號mySignal(int)
emit mySignal(5);
//發射訊號mySignalParam(5,100)
emit mySignalParam(5,100);
}