深入理解信號槽(四)

將 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 的系統都會自然而然的使用信號槽。你可以從中獲取很大的好處。任何大型的系統,如果希望能夠降低組件之間的耦合程度,都應該借鑑這種思想。正如其他的機制和技術一樣,最重要的是把握一個度。在正確的地方使用信號槽,可以讓你的系統更易於理解、更靈活、高度可重用,並且你的工作也會完成得更快。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章