信號(signals)和槽(slots) 信號和信號槽被用於對象(object)之間的通信。信號和槽機制是QT的重要特徵並且也許是QT與其他框架最不相同的部分。 前言 在GUI程序設計中,通常我們希望當對一個窗口部件(widget)進行改變時能告知另一個對此改變感興趣的窗口部件。更一般的,我們希望任何一類的對象(object)都能和其他對象進行通信。例如,如果用戶單擊一個關閉按鈕,我們可能就希望窗口的 close() 函數被調用。 早期的工具包用回調(backcalls)的方式實現上面所提到的對象間的通信。回調是指一個函數的指針,因此如果你希望一個處理函數通知你一些事情,你可以傳遞另一個函數(回調函數)指針給這個處理函數。這個處理函數就會在適當的時候調用回調函數。回調有兩個重要的缺陷:首先,它們不是類型安全的。我們無法確定處理函數是用正確的參數調用這個回調函數。其次,回調與處理函數緊密的聯繫在一起以致處理函數必須知道調用哪個回調。 消息和槽 在QT中,我們使用一種可替代回調的技術:信號和槽機制。當一個特別的事件產生時則發出一個信號。QT的窗口部件有很多已經預定義好的信號,我們也可以通過繼承,給窗口部件的子類添加他們自己信號。槽就是一個可以被調用處理特定信號的函數。QT的窗口部件有很多預定義好的槽,但是通常的做法是給子類窗口部件添加自己的信號,這樣就可以操縱自己加入的信號了。
上面這個圖一定要好好理解,每個signal和Slot都是一個Object的屬性,不同Object的signal可以對應不用的Object的Slot。
所有繼承至QObject或是其子類(如 QWidget)的類都可包含信號和槽。當對象改變它們自身狀態的時候,信號被髮送,從某種意義上講,它們也許對外面的世界感興趣。這就是所有對象在通訊時所做的一切。它不知道也不關心有沒有其他的東西接受它發出的信號。這就是真正的消息封裝,並且確保對象可用作一個軟件組件。 槽被用於接收信號,但是他們也是正常的成員函數。正如一個對象不知道是否有東西接受了他信號,一個槽也不知道它是否被某個信號連接。這就確保QT能創建真正獨立的組件。 你可以將任意個信號連接到你想連接的信號槽,並且在需要時可將一個信號連接到多個槽。將信號直接連接到另一個信號也是可能的(這樣無論何時當第一個信號被髮出後會立即發出第二個)。 總體來看,信號和槽構成了一個強有力的組件編程機制。 簡單示例 一個極小的 C++ 類 聲明如下: class Counter int value() const {return m_value;}
#include <QObject> class Counter : public QObject public: int value() const {return m_value;} public slots: private: QObject版本的類與前一個C++類有着相同的域,並且提供公有函數接受這個域,但是它還增加了對信號和槽(signals-slots)組件編程的支持。這個類可以通過valueChanged()發送信號告訴外部世界他的域發生了改變,並且它有一個可以接受來自其他對象發出信號的槽。 所有包含信號和槽的類都必須在他們聲明中的最開始提到Q_OBJECT。並且他們必須繼承至(直接或間接)QObject。 槽可以由應用程序的編寫者來實現。這裏是Counter::setVaule()的一個可能的實現: void Counter::setValue(int value) emit所在的這一行從對象發出valueChanged信號,並使用新值做爲參數。 在下面的代碼片段中,我們創建兩個Counter對象並且使用QObject::connect()函數將第一個對象的valueChanged()信號連接到第二個對象的setValue()槽。 Counter a, b; 函數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++規則。然而,對於槽,他們可以被任何組件通過一個信號-槽連接(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")) { 元對象信息的使用也可以是qobject_cast<T>(), 他和QObject::inherits() 相似,但更不容易出錯。 if (QAbstractButton *button = qobject_cast<QAbstractButton *>(widget)) 查看Meta-Object系統可獲取更多信息。 一個實例 這是一個註釋過的簡單的例子(代碼片斷選自qlcdnumber.h)。 #ifndef LCDNUMBER_H #include <QFrame> class LcdNumber : public QFrame LcdNumber通過QFrame和QWiget繼承至QObject,它包含了大部分signal-slot知識。這是有點類似於內置的QLCDNumber部件。 Q_OBJECT宏由預處理器展開,用來聲明由moc實現的機個成員函數;如果你的編譯器出現錯誤如下"undefined reference to vtable for LcdNumber", 你可能忘了運行moc或者沒有用連接命令包含moc輸出。 public: LcdNumber並不明顯的與moc相關,但是如果你繼承了QWidege,那麼可以幾乎肯定在你的構造函數中有父對象的變量,並且希望把它傳給基類的構造函數。 析構函數和一些成員函數在這裏省略;moc會忽視成員函數。 signals: 當LcdNumbe被要求顯示一個不可能的值時,便發出信號。 如果你沒有留意溢出,或者你知道溢出不會出現,你可以忽略overflow()信號,比如不將其連接到任何槽。 如果另一方面,當有數字溢出時你想調用兩個不同的錯誤處理函數,可以將這個信號簡單的連接到兩個不同的槽。QT將調用兩個函數(無序的)。 public slots: #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); connect(taxFileButton, SIGNAL(clicked()), 然後,連接信號 mapped()到 readFile() ,根據被按下的按鈕,就可以打開不同的文件。 connect(signalMapper, SIGNAL(mapped(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,就可以繼續使用信號和槽了。 |
Qt的信號和槽機制是Qt的一大特點,實際上這是和MFC中的消息映射機制相似的東西,要完成的事情也差不多,就是發送一個消息然後讓其它窗口響應,當然,這裏的消息是廣義的 MFC中的消息機制沒有采用C++中的虛函數機制,原因是消息太多,虛函數開銷太大.在Qt中也沒有采用C++中的虛函數機制,原因與此相同.其實這裏還有更深層次上的原因,大體說來, 示例代碼: int main(int argc, char *argv[]) QPushButton quit("Quit"); 這裏主要是看QPushButton的clicked()信號和app的quit()槽如何連接?又是如何響應? 任何從QObject派生的類都包含了自己的元數據模型,一般是通過宏Q_OBJECT定義的 首先聲明瞭一個QMetaObject類型的靜態成員變量,這就是元數據的數據結構 struct Q_CORE_EXPORT QMetaObject 這裏的三個虛函數metaObject,qt_metacast,qt_metacall是在moc文件中定義的 宏QT_TR_FUNCTIONS是和翻譯相關的 好了,看看實際的例子吧: QPushButton的元數據表如下: // content: // slots: signature, parameters, type, tag, flags // properties: name, type, flags 0 // eod static const char qt_meta_stringdata_QPushButton[] = { const QMetaObject QPushButton::staticMetaObject = { 在這裏我們看到了靜態成員staticMetaObject被填充了 首先應該看qt_meta_data_QPushButton,因爲這裏是元數據的主要數據,它被填充爲一個整數數組,正因爲這裏只有整數,不能有任何字符串存在,因此纔有 qt_meta_data_QPushButton實際上是以以下結構開頭的 struct QMetaObjectPrivate 一般使用中是直接使用以下函數做個轉換 這種轉換怎麼看都有些黑客的味道,這確實是十足的C風格 再結合實際的數據看一看 // content: // slots: signature, parameters, type, tag, flags // properties: name, type, flags 0 // eod 元數據的結束標記 static const char qt_meta_stringdata_QPushButton[] = { QPushButton\\showMenu()\popupPressed()\bool\autoDefault\default\flat\ 當然我們還可以看看QPushButton的基類QAbstractButton的元數據 // content: // signals: signature, parameters, type, tag, flags // slots: signature, parameters, type, tag, flags // properties: name, type, flags 0 // eod static const char qt_meta_stringdata_QAbstractButton[] = { QAbstractButton00pressed()0released()0checked0clicked(bool)0clicked()0toggled(bool)0size0setIconSize(QSize)0msec0animateClick(int)0animateClick()0click()0toggle()0setChecked(bool)0b0setOn(bool)0QString0text0QIcon0icon0QSize0iconSize0QKeySequence0shortcut0bool0checkable0autoRepeat0autoExclusive0down0 基本上都是大同小異的 QObject::connect(&quit, SIGNAL(clicked()), &app, SLOT(quit())); // connect的源碼 // 不允許空輸入 #ifndef QT_NO_DEBUG QByteArray tmp_method_name; #ifndef QT_NO_DEBUG // 得到元數據類 if (method_index < 0) { int *types = 0; #ifndef QT_NO_DEBUG 檢查信號標記其實比較簡單,就是用signal的第一個字符和用QSIGNAL_CODE=2的標記比較而已 得到信號的索引實際上要依次找每個基類的元數據,得到的偏移也是所有元數據表加在一起後的一個索引 // 這裏是所有基類的方法偏移量算法,就是累加基類所有的方法數目 // 得到方法的元數據 // 如果找到了,就填充QMetaMethod結構 bool QMetaObject::connect(const QObject *sender, int signal_index, void QConnectionList::addConnection(QObject *sender, int signal, 通過connect函數,我們建立了信號和槽的連接,並且把信號類+信號索引+槽類,槽索引作爲記錄寫到了全局的connect列表中 一旦我們發送了信號,就應該調用相關槽中的方法了,這個過程其實就是查找全局的connect列表的過程,當然還要注意其中要對相關的參數打包和解包 // emit是發送信號的代碼 // 發送信號的真正實現在moc裏面 void QMetaObject::activate(QObject *sender, const QMetaObject *m, int local_signal_index, void QMetaObject::activate(QObject *sender, int from_signal_index, int to_signal_index, void **argv) // 得到全局鏈表 QReadLocker locker(&list->lock); void *empty_argv[] = { 0 }; // 在sender的哈希表中得到sender的連接 if (it == end) { QThread * const currentThread = QThread::currentThread(); // 記錄sender連接的索引 for (int i = 0; i < connections.size(); ++i) { // 判斷是否放到隊列中 // 爲receiver設置當前發送者 if (qt_signal_spy_callback_set.slot_begin_callback != 0) #if defined(QT_NO_EXCEPTIONS) if (qt_signal_spy_callback_set.slot_end_callback != 0) list->lock.lockForRead(); if (qt_signal_spy_callback_set.signal_end_callback != 0) { int Foo::qt_metacall(QMetaObject::Call _c, int _id, void **_a) { // 首先在基類中調用方法,返回的id已經變成當前類的方法id了 _id = QObject::qt_metacall(_c, _id, _a); if (_id < 0) return _id; if (_c == QMetaObject::InvokeMetaMethod) { switch (_id) { // 這裏就是真正的調用方法了,注意參數的解包用法 case 0: valueChanged(*reinterpret_cast< int(*)>(_a[1])); break; case 1: setValue(*reinterpret_cast< int(*)>(_a[1])); break; } _id -= 2; } return _id; } |