信號與槽用於對象之間的通信。信號與槽機制是Qt的核心特性和區別於其他框架的特性。
Introduction
在GUI程序中,當我們改變一個widget,經常需要其他的widget得到通知。更普遍的是,我們需要任意的對象可以與另外的對象進行通信。例如,一個用戶點擊關閉按鈕,我們可以需要windows的close()函數被調用。
舊的工具包通過回調函數實現這種通信。一個回調函數是一個函數指針,所以你需要一個處理函數來通知你關於一些事件,你傳遞一個指向另一個函數的處理函數。這個處理函數在適當的時候調用回調函數。回調函數有兩個基本的缺陷:一是,它們不是類型安全的。我們不能保證處理函數能用正確的參數進行調用回調函數。二是,回調函數和處理函數是強耦合的,因為處理函數必須知道要調用那個回調函數。
Signals and Slots
在Qt中,我們有回調函數的好的替代方案:信號與槽。當發生特定的事件時激發一個信號。Qt的widget有很多預先定義的信號,但是我們可以自定義widget來添加我們自己的信號。槽函數是一個特定的信號的響應函數。Qt的widget有很多預先定義的槽函數,但是我們也可以自定義widget來添加自己的槽函數以處理我們感興趣的信號。
信號與槽機制是類型安全的。信號的簽名必須與接收信號的槽的簽名匹配。(實際上,槽函數的簽名可能比它接收的信號的簽名要短,因為它可以忽略額外的參數)因為簽名是兼容的,編譯器可以幫我們捕獲類型不匹配的情況。信號與槽是輕耦合的:一個類激發一個信號,它既不用知道也不用關心哪個槽接收該信號。Qt的信號與槽機制確保當你連接一個信號到一個槽,這個槽將在適當的時候用信號的參數進行調用。信號和槽可以用任何類型任意數量的參數。它們完全的類型安全的。
所有繼承QObject 或它的子類的類都可以包含信號和槽。當對象狀態發生改變通過激發信號以通知其他對該信號感興趣的對象。這是所有進行通信的對象進行的。它不知道也不關心激發的信號是否被接收。這是真正的信息封裝,而且確保了對象可以用作一個軟件組件。
槽函數可以用來接收信號,但是它們也是正常成員函數。就如一個對象不知道是否有其他對象接收它的信號,一個槽函數不用知道是否有一個信號連接了它。這確保了真正的Qt創建的獨立組件。
你可以給一個槽函數連接很多信號,也可以將一個信號連接到很多槽函數。甚至可以將一個信號連接到另一個信號。(當第一個信號激發的時候,將直接激發第二個信號)
總的來說,信號和槽組成了一個強大的組件編程機制。
A Small Example
一個簡單的C++類聲明:
class Counter
{
public:
Counter() { m_value =0; }
int value() const { return m_value; }
void setValue(int value);
private:
int m_value;
};
一個簡單QObject-based類:
#include <QObject>
class Counter : publicQObject
{
Q_OBJECT
public:
Counter() { m_value =0; }
int value() const { return m_value; }
publicslots:
void setValue(int value);
signals:
void valueChanged(int newValue);
private:
int m_value;
};
QObject-based版本有相同的內部狀態,而且提供了public的方法來獲取狀態。另外,它使用信號和槽以支持組件編程。這個類可以通過激發信號valueChanged(),告訴外部它的狀態的改變,而且它有一個其他對象可以給它發送信號的槽。
所有包含信號或槽的類必須在聲明的開頭包含 Q_OBJECT宏聲明,而且必須直接或間接的繼承自QObject。
槽可以由程序員來實現。這里是Counter::setValue()槽的一個可能的實現:
void Counter::setValue(int value)
{
if (value != m_value) {
m_value = value;
emit valueChanged(value);
}
}
對象激發帶有新的value作為參數的信號valueChanged()
在下面的代碼片段,我們創建了兩個Counter 對象和用 QObject::connect():把第一個對象的valueChanged() 信號連接到第二個對象的setValue() 槽函數。
Counter a, b;
QObject::connect(&a,&Counter::valueChanged,
&b,&Counter::setValue);
a.setValue(12); // a.value() == 12, b.value() == 12
b.setValue(48); // a.value() == 12, b.value() == 48
調用a.setValue(12) 激發了信號 valueChanged(12),b的setValue() 槽接收了信號。調用b.setValue(12) ,吧同樣激發一個valueChanged() 信號,但是沒有哪個槽連接到b的valueChanged() 信號,所以信號被忽略了。
注意,setValue() 只有在value != m_value的時候才設置value和激發信號。這避免了在環形連接情況下出現無限循環。
默認情況,對於你建立的每一個連接發送一個信號。對於重復的連接發送兩個信號。你可以調用disconnect() 中斷連接。如果你傳遞了 Qt::UniqueConnection type,當沒有副本時連接才會建立。如果已經存在一個副本,連接將失敗並返回false。
這個例子闡述了對象進行協作但是不需要知道彼此的任何信息。為了達到這樣,對象僅僅需要簡單的通過QObject::connect() 或 uic's automatic connections 特性連接在一起。
Building the Example
C++預處理器改變或去掉了signals, slots, 和 emit關鍵字,使得給編譯器呈現的是標致的C++。
用moc來運行一個包含信號和槽的類聲明,將生成一個被程序編譯和連接的C++源文件。如果你用qmake,makefile的規則自動調用moc。
Signals
當對象的內部狀態變化時會發出在某種程度上其他對象或者用戶感興趣的信號。信號是公共訪問的函數,可以從任何地方發送,但是我們建議僅僅從定義信號的類或其子類中發送信號。
當信號被激發,連接它的槽函數通常像函數調用一樣被立即執行。在這種情況下,信號和槽機制完全獨立於任何GUI事件循環。一旦所有的槽函數返回之后,跟在emit后面代碼才會執行。當使用queued connections時情況會稍有不同。在這種情況下,跟着emit后面的代碼會立即被執行,槽函數會晚一些執行。
如果有多個槽函數連接到一個信號,當信號被激發時,槽函數會以它們連接的順序一個接一個的執行。
信號由moc自動生成而且不能在.cpp中實現。它們沒有返回類型。
關於參數需要注意的是:我們的經驗顯示,如果信號和槽不要專門的類型,它們會更具可重用性。如果QScrollBar::valueChanged()使用特定的類型如QScrollBar::Range,它就只能連接到為 QScrollBar專門設計的槽函數了。不可能將不同的輸入widget連接在一起。
Slots
當連接到槽函數信號被激發,槽函數將被調用。槽函數是正常的C++函數,可以被正常的調用,它們唯一的特性就是可以連接信號。
由於槽函數是正常的成員函數,所以被直接調用時遵循C++規則。然而,槽函數可以被其他組件通過信號連接調用而不管其訪問屬性。這意味着,任意一個實例激發的信號可以引起一個不相關的類的private槽函數被調用。
你也可以定義槽函數為虛函數。
與回調函數相比較,由於其提供的靈活性,信號與槽稍微慢了一點。盡管對於現實中的程序來說這點不同無關緊要。通常,激發一個連接到多個槽函數的信號,不用虛函數,大概比直接調用接收者慢10倍。這開銷用來定位連接對象,安全的迭代所有的連接和以一致的方式安排參數。雖然這聽起來更像是調用10個非虛函數,但是它比任何new或delete操作的開銷更小。例如,當你執行一個字符串,vector或list等幕后需要new或delete的操作,信號和槽的開銷僅僅占整個函數調用開銷的很小部分。當你的系統調用用槽函數或間接調用超過10個函數也同樣如此。信號和槽機制的簡單性和靈活性是值得的開銷,這點用戶甚至不知道。
注意:如果其他庫的變量我signals 或 slots,當編譯基於Qt 的程序時可能引起警告或錯誤。要解決這個問題,用 #undef 取消預處理的標記。
Meta-Object Information
元對象編譯器moc解析C++文件的類聲明並生成初始化元對象的C++代碼。元對象包含所有的信號和槽的名稱和指向它們的指針。
元對象包含一些額外的信息,比如對象的類名。你也可以檢查一個對象是否繼承指定的類。例如:
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();
A Real Example
一個帶注釋的簡單的例子:
#ifndef LCDNUMBER_H
#define LCDNUMBER_H
#include <QFrame>
class LcdNumber : public QFrame
{
Q_OBJECT
LcdNumber 繼承QObject,,有點類似內置的QLCDNumber widget。
Q_OBJECT宏被與處理器擴展以聲明一些函數是被moc實現的。如果出現編譯錯誤"undefined reference to vtable for LcdNumber,你可能忘記運行moc或者在連接命令行忘記包含moc生成的文件。
public:
LcdNumber(QWidget *parent =0);
這和moc不是明顯相關的,但是如果你繼承了QWidget,你幾乎確定構造函數要帶parent參數並把它傳遞給基類的構造函數。
在這里省略了一些析構函數和成員函數,moc忽略了成員函數。
signals:
void overflow();
當LcdNumber 要顯示一個不可能的值的時發出一個信號。
如果你不關心溢出,或者知道不可能出現溢出。你可以忽略overflow() 信號,不連接到任何槽函數。
如果你想在發生溢出時調用兩個不同的錯誤函數,只需要簡單的把信號連接到兩個不同的槽函數就可以了。
publicslots:
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
槽函數是一個接收其他widget狀態變化的函數。如上面的代碼,LcdNumber 用槽來顯示number,由於display()是類的接口的一部分,槽是公共的。
一些程序例子連接QScrollBar的信號 valueChanged() 到display() 槽,所以LcdNumber 不斷的顯示 scroll bar的值。
注意:display()被重載了,Qt在對它連接信號時要選好適當的版本。如果用回調函數,你必須找五個不同的名稱和自己跟蹤它們的類型。
Signals And Slots WithDefault Arguments
信號和槽的簽名可以包含參數,而且參數可以由默認值。考慮QObject::destroyed():
void destroyed(QObject*=0);
當一個QObject 被刪除,它發出QObject::destroyed() 信號。我們想捕獲這個信號,有時我們可能有一個對被刪除QObject的懸空引用,所以我們可以清楚它。一個合適的槽簽名如下:
void objectDestroyed(QObject* obj =0);
為了為該槽連接信號,我們用QObject::connect(). 。有很多方法可以連接信號和槽,第一種方法是用函數指針:
connect(sender,&QObject::destroyed,this,&MyObject::objectDestroyed);
在 connect() 中用函數指針有不少好處:一是它允許編譯器檢查信號的參數是否與槽的參數兼容。如果需要則對參數進行隱式轉換。
你也可以連接到函數或者C++11的lamdas:
connect(sender,&QObject::destroyed,[=](){ this->m_objects.remove(sender); });
如果你的編譯器不支持C++11的可變參數模板,只有在信號和槽有少於等於六個參數這種語法才有效。
另外一種連接信號和槽的方法是,用 QObject::connect() , SIGNAL 和 SLOT 宏。在 SIGNAL() 和 SLOT() .中是否包含參數的規則是:如果參數帶默認值, 傳遞給SIGNAL()的簽名的參數不能少於傳遞給 SLOT()的簽名的參數個數。
以下都是可行的:
connect(sender, SIGNAL(destroyed(QObject*)),this, SLOT(objectDestroyed(Qbject*)));
connect(sender, SIGNAL(destroyed(QObject*)),this, SLOT(objectDestroyed()));
connect(sender, SIGNAL(destroyed()),this, SLOT(objectDestroyed()));
但是這樣就不行了:
connect(sender, SIGNAL(destroyed()),this, SLOT(objectDestroyed(QObject*)));
因為槽函數期待的是一個 QObject 不好發出的信號。這個鏈接會引發運行時錯誤。
注意:用 QObject::connect() 進行重載時,編譯器不好檢查信號和槽的參數。
Advanced Signals and SlotsUsage
有時你可能需要信號發送方的信息,Qt提供了函數 QObject::sender() ,它返回一個指向信號發送方的指針。
QSignalMapper 類被用在,當很多信號被連接到同一個槽,而且槽需要處理每一個不同的信號。
假設你有三個按鈕來決定你將打開哪個文件:"Tax File", "Accounts File", 或 "Report File".
為了打開正確的文件,你用QSignalMapper::setMapping() 映射所有的 clicked()信號到QSignalMapper對象。然后連接文件的 QPushButton::clicked() 信號到槽 QSignalMapper::map() 。
signalMapper =new QSignalMapper(this);
signalMapper->setMapping(taxFileButton, QString("taxfile.txt"));
signalMapper->setMapping(accountFileButton, QString("accountsfile.txt"));
signalMapper->setMapping(reportFileButton, QString("reportfile.txt"));
connect(taxFileButton,&QPushButton::clicked,
signalMapper,&QSignalMapper::map);
connect(accountFileButton,&QPushButton::clicked,
signalMapper,&QSignalMapper::map);
connect(reportFileButton,&QPushButton::clicked,
signalMapper,&QSignalMapper::map);
然后,你連接 mapped() 信號到依據被按下的按鈕打開不同文件的 readFile()。
UsingQt with 3rd Party Signals and Slots
Qt可以和第三方信號/槽機制一起使用。甚至可以在同一個工程中使用兩種機制。只需要在qmake project 文件添加
CONFIG += no_keywords
它告知Qt不要定義moc關鍵字signals, slots, 和 emit,因為這些名詞可能被第三方庫使用。Qt信號和槽可以繼續和no_keywords標記一起使用。只需要將源代碼中用到的moc關鍵字相應的替換成 Q_SIGNALS (or Q_SIGNAL), Q_SLOTS (or Q_SLOT), 和Q_EMIT.就行了。
http://blog.csdn.net/hai200501019/article/details/9166547
