将 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 
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 
#include 
#include 
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 
#include 

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 

void Receiver::qtSlot( const std::string& message )
{
    std::cout << message << std::endl;
}

下面,我们来测试一下:

// main.cpp
#include 
#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 的系统都会自然而然的使用信号槽。你可以从中获取很大的好处。任何大型的系统,如果希望能够降低组件之间的耦合程度,都应该借鉴这种思想。正如其他的机制和技术一样,最重要的是把握一个度。在正确的地方使用信号槽,可以让你的系统更易于理解、更灵活、高度可重用,并且你的工作也会完成得更快。