Qt:訊號與槽(Signals and Slots) 下
訊號(Signals)
只有定義了訊號的類及其子類可以發出訊號。
一個訊號發出後,連線的槽通常會立即執行,就像一個普通的函式呼叫。訊號與槽機制完全獨立於GUI的事件迴圈(event loop)。emit語句後的程式碼在所有的槽函式返回後執行。當使用queued connections時情形稍有不同。在這種情況下emit關鍵字後的程式碼會立即執行,而槽函式會稍後執行。
如果幾個槽連線一個訊號,當訊號發出後這幾個槽會按照連線的順序一個接一個執行。
訊號是由moc自動生成的,不能在.cpp檔案中實現。訊號沒有返回型別(比如,void)。
關於引數有一個要注意的地方:我們的經驗表明如果訊號與槽不是使用特殊的型別是可以很好的重用的。如果QScrollBar::valueChanged()使用了特殊的型別比如QScrollBar::Range,它就只能連線為QScrollBar特定設計的槽。連線兩個不同的輸入Widget就不可能了。
槽(Slots)
當訊號發出後,連線的槽就會被呼叫。槽是普通的c++函式,可以正常的呼叫;槽唯一的特性就是訊號可以和它連線。
槽就是普通的c++函式,它遵守c++的規則。然而,作為槽,它們可以通過建立訊號與槽的連線被任何元件呼叫而不考慮訪問級別(private,public)。這意味著一個任意類例項發出的訊號會導致另一個不相關類例項的私有槽被呼叫。
你也可以把槽定義為虛擬函式,實踐中這個很有用。
和回撥相比,訊號與槽由於增加了靈活性,稍微慢一點,但是在現實的應用中這種差別是很細微的。總的來說,訊號與槽比直接呼叫非虛擬函式慢大約10倍。開銷用在了定位連線物件,安全的遍歷所有連線(比如檢查訊號的發出的過程中,接受者沒有被銷燬),封送任何泛型風格的引數。10倍的非虛擬函式呼叫開銷也許聽著很多,但其實比任何new或delete操作的開銷要小。
元物件資訊
源物件編譯器(moc)解析c++檔案中的類定義,生成初始化元物件的c++程式碼。源物件包含了所有訊號和槽成員的名稱,以及指向這些函式的指標。
源物件包含了額外的資訊,比如,物件的類名稱。你可以檢查一個物件是否繼承自某個特定的類。
if (widget->inherits("QAbstractButton")) {
QAbstractButton *button = static_cast<QAbstractButton *>(widget);
button->toggle();
}
qobject_cast<T>()同樣可以使用源物件資訊,這有點類似QObject::inherits(),但是容易出錯。
button->toggle();
一個真實的例子
#ifndef LCDNUMBER_H
#define LCDNUMBER_H
#include <QFrame>
class LcdNumber : public QFrame
{
Q_OBJECT
LcdNumber繼承自QObject,包含了大多數的訊號與槽。有點類似內建的QLCDNumber Widget.
為了宣告moc生成的成員函式,前處理器擴充套件Q_OBJECT巨集;如果你看到"undefined reference to vtable forLcdNumber“編譯器錯誤,可能是忘了執行moc或是未把moc的輸出包含在連結命令中。
public:
LcdNumber(QWidget *parent = 0);
儘管和moc的聯絡不明顯,但是如果繼承自QWidget,幾乎肯定類構造器需要有父類的引數,並把它傳給父類的構造器。
析構器和成員函式在此省略了;moc會忽略成員函式。
signals: void overflow();當LcdNumber被請求顯示一個不可能的值時,就會發出一個訊號。
如果你不關心溢位,或你確定不會發生溢位,可以忽略overflow()訊號,比如不和任何槽做連線。
另一方面當數字溢位了你需要呼叫兩個不同錯誤處理函式,只需簡單的把訊號和兩個不同的槽做連線。Qt將呼叫兩個槽(任意順序)。
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
有一個用於接收其他Widget狀態改變資訊的槽。LcdNumber使用它設定顯示的數字。因為display()是類介面的一部分,所以槽式公開的。
幾個例項程式把QScrollBal的valueChanged()訊號和display()槽做連線,所以LCD數字可以持續的顯示滾動條的值。
注意display()是過載的;Qt會自動選擇合適的版本。而使用回撥,你不得不找出5個不同的名稱並且自己跟蹤型別。
這個例子省略了不相關的成員函式。
訊號與槽的預設引數
訊號與槽的簽名可以包含引數,引數還可以有預設值。思考一下QObject::destroyed():
void destroyed(QObject* = 0);
當一個QObject物件被刪除時,它會發出QObject::destroyed()訊號。我們希望捕捉到這個訊號,無論什麼情況下有一個被刪除QObject的懸垂指標,我們可以清除它。一個合適得槽函式簽名可能是這樣:
void objectDestroyed(QObject* obj = 0);
我們使用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。這個連線將報一個執行時錯誤。
訊號與槽的高階用法
當需要訊號傳送者資訊的時候,Qt提供了QObject::sender()功能,返回一個指向訊號傳送物件的指標。
當多個訊號連線同一個槽時,QSignalAMapper類可以幫助槽處理每一個不同的訊號。
假設你有三個按鈕,分別對應稅務檔案,賬戶檔案,報表檔案。
為了開啟正確的檔案,使用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, SIGNAL(clicked()),
signalMapper, SLOT (map()));
connect(accountFileButton, SIGNAL(clicked()),
signalMapper, SLOT (map()));
connect(reportFileButton, SIGNAL(clicked()),
signalMapper, SLOT (map()));
接著連線mapped()訊號和readFile()。最後根據點選按鈕,不同的檔案將會被開啟。
connect(signalMapper, SIGNAL(mapped(const QString &)),
this, SLOT(readFile(const QString &)));
Qt使用第三方訊號與槽
Qt可以使用第三方的訊號與槽機制。你甚至可以在同一個專案裡使用兩種訊號與槽機制。只需要把下面一行加到你的專案檔案(.pro file)
CONFIG += no_keywords
這行命令式告訴Qt在使用第三方類庫如boost時不定義signals,slots和emit關鍵字。在no_keywords標誌下繼續使用Qt的訊號與槽,只要簡單替換原始碼中Qt的moc關鍵字為相對應的Qt巨集Q_SIGNALS (Q_SIGNAL), Q_SLOTS (Q_SLOT) 和Q_EMIT。