將 Qt 的信號槽系統與 Boost.Signals 結合使用
實際上,將 Qt 的信號槽系統與 Boost.Signals 結合在一起使用並非不可能。通過前面的闡述,我們都知道了二者的不同,至於爲什麼要將這二者結合使用,則是見仁見智的了。這裏,我們給出一種結合使用的解決方案,但是並不是說我們暗示應該將它們結合使用。這應該是具體問題具體分析的。
將 Qt 的信號槽系統與 Boost.Signals 結合使用,最大的障礙是,Qt 使用預處理器定義了關鍵字 signals,slots 以及 emit。這些可以看做是 Qt 對 C++ 語言的擴展。同時,Qt 也提供了另外一種方式,即使用宏來實現這些關鍵字。爲了屏蔽掉這些擴展的關鍵字,Qt 4.1 的 pro 文件引入了 no_keywords 選項,以便使用標準 C++ 的方式,方便 Qt 與其他 C++ 同時使用。你可以通過打開 no_keywords 選項,來屏蔽掉這些關鍵字。下面是一個簡單的實現:
# TestSignals.pro (platform independent project file, input to qmake) # showing how to mix Qt Signals and Slots with Boost.Signals # # Things you'll have in your .pro when you try this... # CONFIG += no_keywords # so Qt won't #define any non-all-caps `keywords' INCLUDEPATH += . /usr/local/include/boost-1_33_1/ # so we can #include <boost/someheader.hpp> macx:LIBS += /usr/local/lib/libboost_signals-1_33_1.a # ...and we need to link with the Boost.Signals library. # This is where it lives on my Mac, # other platforms would have to add a line here # # Things specific to my demo # CONFIG -= app_bundle # so I'll build a command-line tool instead of a Mac OS X app bundle HEADERS += Sender.h Receiver.h SOURCES += Receiver.cpp main.cpp
請注意,我們已經在 pro 文件中打開了 no_keywords 選項,那麼,類似 signals 這樣的關鍵字已經不起作用了。所以,我們必須將這些關鍵字修改成相應的宏的版本。例如,我們需要將 signals 改爲 Q_SIGNALS,將 slots 改爲 Q_SLOTS 等等。請看下面的代碼:
// Sender.h #include <QObject> #include <string> #include <boost/signal.hpp> class Sender : public QObject { Q_OBJECT Q_SIGNALS: // a Qt signal void qtSignal( const std::string& ); // connect with // QObject::connect(sender, SIGNAL(qtSignal(const std::string&)), ... public: // a Boost signal for the same signature boost::signal< void ( const std::string& ) > boostSignal; // connect with // sender->boostSignal.connect(... public: // an interface to make Sender emit its signals void sendBoostSignal( const std::string& message ) { boostSignal(message); } void sendQtSignal( const std::string& message ) { qtSignal(message); } };
現在我們有了一個發送者,下面來看看接收者:
// Receiver.h #include <QObject> #include <string> class Receiver : public QObject { Q_OBJECT public Q_SLOTS: void qtSlot( const std::string& message ); // a Qt slot is a specially marked member function // a Boost slot is any callable signature }; // Receiver.cpp #include "Receiver.h" #include <iostream> void Receiver::qtSlot( const std::string& message ) { std::cout << message << std::endl; }
下面,我們來測試一下:
// main.cpp #include <boost/bind.hpp> #include "Sender.h" #include "Receiver.h" int main( int /*argc*/, char* /*argv*/[] ) { Sender* sender = new Sender; Receiver* receiver = new Receiver; // connect the boost style signal sender->boostSignal.connect(boost::bind(&Receiver::qtSlot, receiver, _1)); // connect the qt style signal QObject::connect(sender, SIGNAL(qtSignal(const std::string&)), receiver, SLOT(qtSlot(const std::string&))); sender->sendBoostSignal("Boost says 'Hello, World!'"); sender->sendQtSignal("Qt says 'Hello, World!'"); return 0; }
這段代碼將會有類似下面的輸出:
[506]TestSignals$ ./TestSignals Boost says 'Hello, World!' Qt says 'Hello, World!'
我們可以看到,這兩種實現的不同之處在於,Boost.Signals 的信號,boostSignal,是 public 的,任何對象都可以直接發出這個信號。也就是說,我們可以使用如下的代碼:
sender->boostSignal("Boost says 'Hello, World!', directly");
從而繞過我們設置的 sendBoostSignal() 這個觸發函數。另外,我們可以看到,boostSignal 完全可以是一個全局對象,這樣,任何對象都可以使用這個信號。而對於 Qt 來說,signal 必須是一個成員變量,在這裏,只有 Sender 可以使用我們定義的信號。
這個例子雖然簡單,然而已經很清楚地爲我們展示了,如何通過 Qt 發出信號來獲取 Boost 的行爲。在這裏,我們使用一個公共的 sendQtSignal() 函數發出 Qt 的信號。然而, 爲了從 Boost 的信號獲取 Qt 的行爲,我們需要多做一些工作:隱藏信號,但是需要提供獲取連接的函數。這樣看上去有些麻煩:
class Sender : public QObject { // just the changes... private: // our new public connect function will be much easier to understand // if we simplify some of the types typedef boost::signal< void ( const std::string& ) > signal_type; typedef signal_type::slot_type slot_type; signal_type boostSignal; // our signal object is now hidden public: boost::signals::connection connectBoostSignal( const slot_type& slot, boost::signals::connect_position pos = boost::signals::at_back ) { return boostSignal.connect(slot, pos); } };
應該說,這樣的實現相當醜陋。實際上,我們將 Boost 的信號與連接分割開了。我們希望能夠有如下的實現:
// WARNING: no such thing as a connect_proxy class Sender { public: connect_proxy< boost::signal< void ( const std::string& ) > > someSignal() { return someSignal_; // ...automatically wrapped in the proxy } private: boost::signal< void ( const std::string& ) > someSignal_; }; sender->someSignal().connect(someSlot);
注意,這只是我的希望,並沒有做出實現。如果你有興趣,不妨嘗試一下。
總結
前面囉嗦了這麼多,現在總結一下。
信號和槽的機制實際上是觀察者模式的一種變形。它是面向組件編程的一種很強大的工具。現在,信號槽機制已經成爲計算機科學的一種術語,也有很多種不同的實現。
Qt 信號槽是 Qt 整個架構的基礎之一,因此它同 Qt 提供的組件、線程、反射機制、腳本、元對象機制以及可視化 IDE 等等緊密地集成在一起。Qt 的信號是對象的成員函數,所以,只有擁有信號的對象才能發出信號。Qt 的組件和連接可以由非代碼形式的資源文件給出,並且能夠在運行時動態建立這種連接。Qt 的信號槽實現建立在 Qt 元對象機制之上。Qt 元對象機制由 Qt 提供的 moc 工具實現。moc 也就是元對象編譯器,它能夠將用戶指定的具有 Q_OBJECT 宏的類進行一定程度的預處理,給這個增加元對象能力。
Boost.Signals 是具有靜態的類型安全檢查的,基於模板的信號槽系統的實現。所有的信號都是模板類 boost::signal 的一個特化;所有的槽函數都具有相匹配的可調用的簽名。Boost.Signals 是獨立的,不需要內省、元對象系統,或者其他外部工具的支持。然而,Boost.Signals 沒有從資源文件動態建立連接的能力。
這兩種實現都非常漂亮,並且都具有工業強度。將它們結合在一起使用也不是不可能的,Qt 4.1 即提供了這種可能性。
任何基於 Qt GUI 的系統都會自然而然的使用信號槽。你可以從中獲取很大的好處。任何大型的系統,如果希望能夠降低組件之間的耦合程度,都應該借鑑這種思想。正如其他的機制和技術一樣,最重要的是把握一個度。在正確的地方使用信號槽,可以讓你的系統更易於理解、更靈活、高度可重用,並且你的工作也會完成得更快。