訊號(signals)和槽(slots) 精講
訊號(signals)和槽(slots)
訊號和訊號槽被用於物件(object)之間的通訊。訊號和槽機制是QT的重要特徵並且也許是QT與其他框架最不相同的部分。
前言
在GUI程式設計中,通常我們希望當對一個視窗部件(widget)進行改變時能告知另一個對此改變感興趣的視窗部件。更一般的,我們希望任何一類的物件(object)都能和其他物件進行通訊。例如,如果使用者單擊一個關閉按鈕,我們可能就希望視窗的 close() 函式被呼叫。
早期的工具包用回撥(backcalls)的方式實現上面所提到的物件間的通訊。回撥是指一個函式的指標,因此如果你希望一個處理函式通知你一些事情,你可以傳遞另一個函式(回撥函式)指標給這個處理函式。這個處理函式就會在適當的時候呼叫回撥函式。回撥有兩個重要的缺陷:首先,它們不是型別安全的。我們無法確定處理函式是用正確的引數呼叫這個回撥函式。其次,回撥與處理函式緊密的聯絡在一起以致處理函式必須知道呼叫哪個回撥。
訊息和槽
在QT中,我們使用一種可替代回撥的技術:訊號和槽機制。當一個特別的事件產生時則發出一個訊號。QT的視窗部件有很多已經預定義好的訊號,我們也可以通過繼承,給視窗部件的子類新增他們自己訊號。槽就是一個可以被呼叫處理特定訊號的函式。QT的視窗部件有很多預定義好的槽,但是通常的做法是給子類視窗部件新增自己的訊號,這樣就可以操縱自己加入的訊號了。
上面這個圖一定要好好理解,每個signal和Slot都是一個Object的屬性,不同Object的signal可以對應不用的Object的Slot。
訊號和槽機制是型別安全的:一個訊號的簽名必須和該訊號接受槽的簽名相匹配。(事實上以一個槽的簽名可以比他可接受的訊號的簽名少,因為它可以忽略一些簽名)。因此簽名是一致的,編譯器就可以幫助我們檢測型別匹配。訊號和槽是鬆耦合的:一個類不知道也不關心哪個槽接受了它所發出的訊號。QT的訊號和槽機制確保他們自生的正確連線,槽會在正確的時間使用訊號引數而被呼叫。訊號和槽可以使用任何數量、任何型別的引數。他們完全是型別安全的。
所有繼承至QObject或是其子類(如 QWidget)的類都可包含訊號和槽。當物件改變它們自身狀態的時候,訊號被髮送,從某種意義上講,它們也許對外面的世界感興趣。這就是所有物件在通訊時所做的一切。它不知道也不關心有沒有其他的東西接受它發出的訊號。這就是真正的訊息封裝,並且確保物件可用作一個軟體元件。
槽被用於接收訊號,但是他們也是正常的成員函式。正如一個物件不知道是否有東西接受了他訊號,一個槽也不知道它是否被某個訊號連線。這就確保QT能建立真正獨立的元件。
你可以將任意個訊號連線到你想連線的訊號槽,並且在需要時可將一個訊號連線到多個槽。將訊號直接連線到另一個訊號也是可能的(這樣無論何時當第一個訊號被髮出後會立即發出第二個)。
總體來看,訊號和槽構成了一個強有力的元件程式設計機制。
簡單示例
一個極小的 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++類有著相同的域,並且提供公有函式接受這個域,但是它還增加了對訊號和槽(signals-slots)元件程式設計的支援。這個類可以通過valueChanged()傳送訊號告訴外部世界他的域發生了改變,並且它有一個可以接受來自其他物件發出訊號的槽。
所有包含訊號和槽的類都必須在他們宣告中的最開始提到Q_OBJECT。並且他們必須繼承至(直接或間接)QObject。
槽可以由應用程式的編寫者來實現。這裡是Counter::setVaule()的一個可能的實現:
void Counter::setValue(int value)
{
if(value != m_value)
{
m_value = value;
emit valueChanged(value);
}
}
emit所在的這一行從物件發出valueChanged訊號,並使用新值做為引數。
在下面的程式碼片段中,我們建立兩個Counter物件並且使用QObject::connect()函式將第一個物件的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
函式a.setValue(12)的呼叫導致訊號valueChange(12)被髮出,物件b的setValue()槽接受該訊號,即函式setValue()被呼叫。然後b同樣發出訊號valueChange(),但是由於沒有槽連線到b到valueChange()訊號,所以該訊號被忽略。
注意,只有當 value != m_value 時,函式 setValue() 才會設定新值併發出訊號。這樣就避免了在迴圈連線的情況下(比如b.valueChanged() 和a.setValue()連線在一起)出現無休止的迴圈的情況。
訊號將被髮送給任何你建立了連線的槽;如果重複連線,將會發送兩個訊號。總是可以使用QObject::disconnect()函式斷開一個連線。
這個例子說明了物件之間可以不需要知道相互間的任何資訊而系協同工作。為了實現這一目的,只需要將物件通過函式QObject::connect()的呼叫相連線(connect),或者利用uic的automatic connections的特性。
編譯這個示例
C++預編譯器會改變或去除關鍵字signals,slots,和emit,這樣就可以使用標準的C++編譯器。
在一個定義有訊號和槽的類上執行moc,這樣就會生成一個可以和其它物件檔案編譯和連線成應用程式的C++原始檔。如果使用qmake工具,將會在你的makefile檔案里加入自動呼叫moc的規則。
訊號
當物件的內部狀態發生改變,訊號就被髮射,在某些方面對於物件代理或者所有者也許是很有趣的。只有定義了訊號的物件或其子物件才能發射該訊號。
當一個訊號被髮出,被連線的槽通常會立刻執行,就像執行一個普通的函式呼叫。當這一切發生時,訊號和槽機制是完全獨立於任何GUI事件迴圈之外的。槽會在emit域下定義的程式碼執行完後返回。當使用佇列連線(queued connections)時會有一些不同;這種情況下,關鍵字emit後的程式碼會繼續執行,而槽在此之後執行。
如果幾個槽被連線到一個訊號,當訊號被髮出後,槽會以任意順序一個接一個的執行。
關於引數需要注意:我們的經驗顯示如果訊號和槽不使用特殊的型別將會變得更具重用性。如果QScrollBar::valueChanged() 使用了一個特殊的型別,比如hypothetical QRangeControl::Range,它就只能被連線到被設計成可以處理QRangeControl的槽。再沒有象教程5這樣簡單的例子。
槽
當一個訊號被髮出時連線他的槽被呼叫。槽是一個普通的C++函式並按普通方式呼叫;他的特點僅僅是可以被訊號連線。
由於槽只是普通的成員函式,當呼叫時直接遵循C++規則。然而,對於槽,他們可以被任何元件通過一個訊號-槽連線(signal-slot connection)呼叫,而不管其訪問許可權。也就是說,一個從任意的類的例項發出的訊號可導致一個不與此類相關的另一個類的例項的私有槽被呼叫。
你還可以定義一個虛擬槽,在實踐中被發現也是非常有用的。
由於增加來靈活性,與回撥相比,訊號和槽稍微慢一些,儘管這對真實的應用程式來說是可以忽略掉的。通常,發出一連線了某個槽的訊號,比直接呼叫那些非虛擬呼叫的接受器要慢十倍。這是定位連線物件所需的開銷,可以安全地重複所有地連線(例如在發射期間檢查併發接收器是否被破壞)並且可以按一般的方式安排任何引數。當十個非虛擬函式呼叫聽起來很多時,實際上他比任何new和delete操作的開銷都少,例如,當你執行一個字串、向量或列表操作時,就需要用到new和delete,而訊號和槽的開銷只是全部函式呼叫花費的一小部分。
無論何時你用槽進行一個系統呼叫和間接的呼叫超過10個以上的函式時間都是一樣的。在i586-500機器上,每秒鐘你可以傳送超過2,000,000個訊號給一個接受者,或者每秒傳送1,200,000個訊號給兩個接受者。相對於訊號和槽機制的簡潔性和靈活性,他的時間開銷是完全值得的,你的使用者甚至察覺不出來。
注意:若其他的庫將變數定義為signals和slots,可能導致編譯器在連線基於QT的應用程式時出錯或警告。為了解決這個問題,請使用#undef預處理符號。
元物件資訊
元物件編譯器(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();
檢視Meta-Object系統可獲取更多資訊。
一個例項
這是一個註釋過的簡單的例子(程式碼片斷選自qlcdnumber.h)。
#ifndef LCDNUMBER_H
#define LCDNUMBER_H
#include <QFrame>
class LcdNumber : public QFrame
{
Q_OBJECT
LcdNumber通過QFrame和QWiget繼承至QObject,它包含了大部分signal-slot知識。這是有點類似於內建的QLCDNumber部件。
Q_OBJECT巨集由前處理器展開,用來宣告由moc實現的機個成員函式;如果你的編譯器出現錯誤如下"undefined reference to vtable for LcdNumber", 你可能忘了執行moc或者沒有用連線命令包含moc輸出。
public:
LcdNumber(QWidget *parent = 0);
LcdNumber並不明顯的與moc相關,但是如果你繼承了QWidege,那麼可以幾乎肯定在你的建構函式中有父物件的變數,並且希望把它傳給基類的建構函式。
解構函式和一些成員函式在這裡省略;moc會忽視成員函式。
signals:
void overflow();
當LcdNumbe被要求顯示一個不可能的值時,便發出訊號。
如果你沒有留意溢位,或者你知道溢位不會出現,你可以忽略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
一個槽是一個接受函式,用於獲得其他視窗部件的資訊變化。LcdNumber使用它,就像上面的程式碼一樣,來設定顯示的數字。因為display()是這個類和程式的其它的部分的一個介面,所以這個槽是公有的。
幾個例程把QScrollBar的valueChanged()訊號連線到display()槽,所以LCD數字可以繼續顯示滾動條的值。
請注意display()被過載了,當將一個訊號連線到槽時QT將選擇一個最適合的一個。而對於回撥,你會發現五個不同的名字並且自己來跟蹤型別。
一個不相干的成員函式在例子中被忽略。
高階訊號和槽的使用
在當你需要訊號傳送者的資訊時,QT提供了一個函式QObject::sender(),他返回指向一個訊號傳送物件的指標。
當有幾個訊號被連線到同一槽上,並且槽需要處理每個不同的訊號,可使用 QSignalMapper類。
假設你用三個按鈕來決定開啟哪個檔案: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() ,根據被按下的按鈕,就可以開啟不同的檔案。
connect(signalMapper, SIGNAL(mapped(const QString &)),
this, SLOT(readFile(const QString &)));
在QT中使用第三方signals slots
在QT中使用第三方signals slots是可能的。你甚至可以在同一類中使用兩種機制。僅僅需要在你的qmake工程檔案(.pro)中加入下面語句:
CONFIG += no_keywords
它告訴QT不要定義moc關鍵字signals,slots和emit,因為這些名字可能將被用於第三方庫,例如Boost。你只需簡單的用QT巨集將他們替換為 Q_SIGNALS, Q_SLOTS,和 Q_EMIT,就可以繼續使用訊號和槽了。