QT——信号槽机制

一、概述

    QT 是一个跨平台的 C++ GUI 应用构架,它提供了丰富的窗口部件集,具有面向对象、易于扩展、真正的组件编程等特点,更为引人注目的是目前 Linux 上最为流行的 KDE 桌面环境就是建立在 QT 库的基础之上。QT 支持下列平台:MS/WINDOWS-95、98、NT 和 2000;UNIX/X11-Linux、Sun Solaris、HP-UX、Digital Unix、IBM AIX、SGI IRIX;EMBEDDED- 支持 framebuffer 的 Linux 平台。伴随着 KDE 的快速发展和普及,QT 很可能成为 Linux 窗口平台上进行软件开发时的 GUI 首选。

    信号和槽机制(Signals and Slots)是 QT 的核心机制,要精通 QT 编程就必须对信号和槽有所了解。信号和槽是一种高级接口,应用于对象之间的通信,它是 QT 的核心特性,也是 QT 区别于其它工具包的重要地方。信号和槽是 QT 自行定义的一种通信机制,它独立于标准的 C/C++ 语言,因此要正确的处理信号和槽,必须借助一个称为 moc(Meta Object Compiler)的 QT 工具,该工具是一个 C++ 预处理程序,它为高层次的事件处理自动生成所需要的附加代码。

二、引出    

    Older toolkits使用回调(callbacks)来达到这样的目的。回调是一个指向函数的指针,所以如果你希望一个处理函数通知你某些事件发生了,你可以传递一个指向其他函数的指针(回调)给它。这个处理函数将在适当的时候调用回调函数。回调函数有两个明显的缺点,第一,它们并不是类型安全,我们永远都不能确定调用者是否将通过正确的参数来调用“回调函数”;第二,回调函数与处理函数是紧耦合(strongly coupled)的,因为调用者必须知道应该在什么时候调用哪个回调函数。

    Qt使用了信号和槽来代替回调函数。当一个特定的事件发生时,信号会被发送出去。Qt的窗体部件(widget)拥有众多预先定义好的信号,当然,我们也可以创建窗体部件(widget)的子类来为它们添加我们需要的自定义信号。槽,则是对一个特定的信号进行的反馈。Qt的窗体部件(widget)同样拥有众多预先定义好的槽,但是通常的做法是,创建窗体部件(widget)的子类并添加自定义槽,以便对感兴趣的信号进行处理。

三、特点

    1)QT信号槽机制的引用精简了程序员的代码量 

    2)QT的信号可以对应多个槽(但他们的调用顺序随机),也可以多个槽映射一个信号 ,信号还可以映射信号

    3)QT的信号槽的建立和解除绑定十分自由 

    4)信号槽同真正的回调函数比起来时间的耗损还是很大的,所有在嵌入式实时系统中应当慎用 

    5)信号槽的参数限定很多例如不能携带模板类参数,不能出现宏定义等等

    信号和槽机制是类型安全的(type-safe):一个信号的参数必须和接收槽的参数匹配。(槽的参数可以比它接收的信号的参数短)由于这种参数匹配机制,编译器以帮助我们检查类型不匹配的签名。

    信号与槽是松耦合(loosely coupled)的:一个发出信号的类既不知道也不关心哪一个槽接收到这个信号。Qt的信号和槽机制保证了如果你将一个信号连接到一个槽上,槽会在正确的时间以号的参数被调用。信号与槽可以携带任意个、任意类型的参数。他们是完全的类型安全。

    所有从QObject或者它的一个子类(比如:QWidget)继承的类都可以使用号与槽。对象中以这种方式通信:一个对象的状态发生了改变并发送信号,关心这个改变的另一对像接收到这个信号。发送信号的对象并不知道也不感兴趣什么对象接收它所发出的信号,这是真正的信息封装,保证了对象能被当作软件组件来使用。

    槽能被用来接收信号,除此之外它们也是普通的成员函数。槽不知道是否有信号与它连接起来,正如对象不知道它发出信号是否会被接收一样。这样的机制确保了可以使用Qt创建一个个完全独立的组件。

    你可以把你感兴趣的多个信号与一个个槽进行连接,也可以把一个信号与多个槽进行连接。甚至可以直接把一个信号连接到另一个信号(当第一个信号发送出去的时候,第二个信号紧接着被发送)。

    就这样,信号与插槽建立了强大的组件编程机制。

四、实例

      通过调用 QObject 对象的 connect 函数来将某个对象的信号与另外一个对象的槽函数相关联,这样当发射者发射信号时,接收者的槽函数将被调用。该函数的定义如下:

static bool connect(const QObject *sender, const char *signal,  
    const QObject *receiver, const char *member, Qt::ConnectionType =  
        #ifdef qdoc  
            Qt::AutoConnection  
        #else  
            #ifdef QT3_SUPPORT  
                Qt::AutoCompatConnection  
            #else  
                Qt::AutoConnection  
            #endif  
        #endif  
);
    这个函数的作用就是将发射者 sender 对象中的信号 signal 与接收者 receiver 中的 member 槽函数联系起来。当指定信号 signal 时必须使用 QT 的宏 SIGNAL(),当指定槽函数时必须使用宏 SLOT()。如果发射者与接收者属于同一个对象的话,那么在 connect 调用中接收者参数可以省略。
    两个对象连接:标签对象 label 和滚动条对象 scroll,并将 valueChanged() 信号与标签对象的 setNum() 相关联,另外信号还携带了一个整形参数,这样标签总是显示滚动条所处位置的值。
QLabel     *label  = new QLabel; 
QScrollBar *scroll = new QScrollBar; 
QObject::connect( scroll, SIGNAL(valueChanged(int)), label,  SLOT(setNum(int)) );
    信号与信号连接:
class MyWidget : public QWidget 
{ 
    Q_OBJECT    //最好都加上
    public: 
        MyWidget(); 
    ... 
    signals: 
        void aSignal(); 
    ... 
    private: 
    ... 
        QPushButton *aButton; 
}; 
MyWidget::MyWidget() 
{ 
    aButton = new QPushButton( this ); 
    connect( aButton, SIGNAL(clicked()), SIGNAL(aSignal()) ); 
}
    在上面的构造函数中,MyWidget 创建了一个私有的按钮 aButton,按钮的单击事件产生的信号 clicked() 与另外一个信号 aSignal() 进行了关联。这样一来,当信号 clicked() 被发射时,信号 aSignal() 也接着被发射。当然,你也可以直接将单击事件与某个私有的槽函数相关联,然后在槽中发射 aSignal() 信号,这样的话似乎有点多余。
    所有包含信号或槽的类在他们声明的顶端都必须写上Q_OBJECT,即使不使用信号槽机制,最好也在类中加上该宏定义进行申明,其次必须直接或间接继承自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;
};
void Counter::setValue(int value)
{
     if (value != m_value) {
         m_value = value;
         emit valueChanged(value);
     }
}

emit行从对象中以新的value值作为参数,发送信号valueChanged()。

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) 使a发送一个valueChanged(12) 信号,b将利用setValue() 槽来接收这个信号。比如,b.setValue(12)被调用,然后b发送相同的valueChanged()信号,但是因为没有槽与b的 valueChanged() 信号连接,因此这个信号被忽略。
注意,只有当value != m_value时,setValue()函数才可以设定值并且发送信号。这样就避免了循环连接时出现死循环(例如,如果b.valueChanged()与a.setValue()连接)。
    一般情况下,发送一个信号的连接和发送两个信号的重复连接,都可以通过调用QObject::connect连接信号和槽时使用的参数。如果你在连接信号和槽时传递了Qt::UniqueConnection类型,只有它不是一个重复连接,连接才会成功。如果之前已经有了一个链接(相同的信号连接到同一对象的同一个槽上),那么连接将会失败并将返回false。
    这个例子说明对象之间可以在不需要了解彼此信息的情况下相互协作。要实现这样的功能,对象之间仅需要互相连接就可以了,这种连接可以通过简单的调用QObject::connect() 函数或利用uic(User Interface Compiler,UI Designer工具之一,从.ui文件(XML样式)读取描述,然后生成C++代码。)的自动连接特性来完成。
    当信号与槽没有必要继续保持关联时,我们可以使用 disconnect 函数来断开连接。其定义如下:
bool QObject::disconnect ( const QObject * sender, const char * signal, 
		 const Object * receiver, const char * member ) [static]
    这个函数断开发射者中的信号与接收者中的槽函数之间的关联。
    有三种情况必须使用 disconnect() 函数:
  • 断开与某个对象相关联的任何对象。这似乎有点不可理解,事实上,当我们在某个对象中定义了一个或者多个信号,这些信号与另外若干个对象中的槽相关联,如果我们要切断这些关联的话,就可以利用这个方法,非常之简洁。
disconnect( myObject, 0, 0, 0 ) 
//或者
myObject->disconnect()
  • 断开与某个特定信号的任何关联
disconnect( myObject, SIGNAL(mySignal()), 0, 0 ) 
//或者
myObject->disconnect( SIGNAL(mySignal()) )
  • 断开两个对象之间的关联
disconnect( myObject, 0, myReceiver, 0 ) 
//或者
myObject->disconnect(  myReceiver )
    在 disconnect 函数中 0 可以用作一个通配符,分别表示任何信号、任何接收对象、接收对象中的任何槽函数。但是发射者 sender 不能为 0,其它三个参数的值可以等于 0。

五、编译过程

    Qt工程编译时,C++预处理器会删改 signals, slots与emit关键字,以便让标准的C++编译器能够识别。因为这些关键字是Qt内部自定义的,而不是C++标准的关键字。
     通过对包含信号与槽的类定义运行 moc 程序,会产生一个相应的C++源文件,这个源文件将会被编译,并参与应用程序的目标文件(Object file)链接过程。如果你直接使用 qmake 命令, 它会把自动调用 moc 程序的makefile规则添加到工程的makefile文件中。

六、信号建立和发射及槽接收过

    信号在对象的内部状态改变的时候以某种方式发送出来,以通知该对象感兴趣的客户端或拥有者。只有在类中定义了信号才能在该类及其子类中发送信号。

    当信号被发送时,一般情况下,与之相连的槽会立即被调用,这个过程与一般的函数调用是一样的。这一切的发生,信号与槽机制完全与任何的GUI事件循环无关。emit语句后面代码在所有的槽代码执行完毕之后将会继续执行。这个情况在使用“队列连接”(Queued Connections)时,会有些不同,这种情况下,emit关键字后面的代码会立即执行,而槽会在之后被执行。

    如果一个信号同时与好几个槽连接,那么在信号产生时,这些槽将按与信号连接的先后顺序,逐个调用。

    信号由moc程序自动产生,并且不能在“.cpp”文件中实现。信号不能有返回值(只能使用void)。

    关于信号函数的注释:经验显示,如果信号与槽的参数不使用一些特殊的类型,那么它们的重用性将更好。如果QScrollBar::valueChanged()信号使用了一个特殊的参数,如:QScrollBar::Range,那么它仅可能被连接到为QScrollBar设计的特殊槽上。这是软件复用思想中最深恶痛绝的。这么做的话,该信号就不能与其他不同的窗口输入部件(Input Widget)中的槽连接了。

      槽与信号连接之后,当信号发送时就会被调用。槽其实就是普通的C++函数,并且可以像普通函数一样调用.它们唯一的特点就是可以与信号相连。

      因为槽是普通的成员函数,所以当它们被直接调用时,它们也遵循C++函数调用规则。然而,它们也可以通过信号-槽之间的连接,由其它组件调用而不管槽的访问级别(如:private级别的槽)。换句话说,从任意的类实例中发送出来的信号都能调用一个不相关类中的private级别的槽。

      槽也可以用virutal定义,实际上,后面我们会发现这样子做很有用。

      与回调函数(callback)相比,信号与槽会稍微慢一些(译者注:大概慢一个数量级左右,但以现在的硬件来说,我们根本感觉不出来。),这是因为信号-槽机制更灵活。当然,实际应用程序上,它们的不同点是忽略不计的。一般来讲,引发一个连接了多个槽的信号,会比直接调用信号的接收者,慢10倍左右(这里不与虚函数调用比较)。这是因为,信号-槽机制需要定位连接对象,以确保安全地遍历所有的连接(例如,检查在信号发送过程中,所有要接收信号的对象未被销毁),以及参数的正反序列化,这些过程都必须占用花费。举个例子说明下,同时调用十个非虚函数听起来好像很多,但它的花费将比任意的new或delete操作少得多。试想下,你执行创建或销毁字符串,向量或列表的操作,它们将引发new或delete操作,与这些操作相比,信号-槽所需要的花费仅仅占整个函数调用花费的一小部分。

      在槽中进行系统调用的花费与上面讲述的类似。在一台i586-500机器上,你每秒可以进行2,000,000次的1对1信号-槽调用,或者1,200,000次的1对2信号-槽调用。信号-槽机制的简单性与灵活性,与它们所占花费比起来,那真是物超所值。当然客户在使用你的程序时,完全不会察觉到这种效率上的微小变化。

      注意,有些第三方库可能定义了一些变量,像signals或slots,这些库在Qt程序中使用时,这可能会引发编译器的警告或错误。要解决这个问题,使用#undef预处理器关闭这些预处理符号。

七、信号函数和槽函数的参数

    信号与槽的签名包含参数,且参数可能含有不同的默认值。让我们看下QObject::destroyed():

void destroyed(QObject* = 0);

    当QObject被销毁时,它会发送QObject::destroyed()信号。当我们想对被删除的QObject对象进行一些处理时,例如,做些清理工作,就可以捕捉该信号。一种接收该信号的槽的函数签名可能像下面这样:

void objectDestroyed(QObject* obj = 0);

    为了将信号与-槽相连接,我们会用到QObject::connect(),SIGNAL()和SLOT()宏。在SIGNAL()与SLOT()宏中是否要包含参数,有以下规则:如果信号的参数有默认值,那么传递给SIGNAL()宏的参数个数不能少于SLOT()宏的参数个数。

下面这些代码都会正常工作:

connect(sender, SIGNAL(destroyed(QObject*)), this,

               SLOT(objectDestroyed(Qbject*)));

connect(sender, SIGNAL(destroyed(QObject*)), this,

               SLOT(objectDestroyed()));

connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed()));

但下面的代码就不能正常工作:

connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed(QObject*)));

由于槽需要得到一个QObject作为参数,而信号却不带这个参数,会导致这个信号-槽连接报一个运行时错误,这种错误往往很难找出来。

八、信号-槽的高级用法

    有时候,你可能想要获取信号发送者的信息,这样子我们就可以判断是谁发出的信号。Qt提供了QObject::sender()函数,它返回一个指向信号发送者的指针。

    有时候,会出现一个槽连接多个信号,而这个槽可能需要对不同的信号执行不同的处理方式,QSignalMapper类就是为这种情况设计的。

    假设你有三个不同的按钮,它们分别用于打开不同的文件,如:传真文件,账号文件与报表文件。

    为了打开正确的文件,你可以使用QSignalMapper::setMapping()将所有按钮的clicked()信号映射到QSignalMapper对象,然后你就可以将QPushButton::clicked()信号与QSignalMapper::map()槽相连接,如下代码所示:

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(QString)), this, SLOT(readFile(QString)));

注:下面的代码也可以编译运行,但由于签名规范化(signature normalization,查了GOOGLE,似乎找不到这词的中文翻译,个人认为应该是函数签名的转换,译者注),这些代码的运行效率会低些

        //由于函数签名运行时的规范化,这样子会慢些

connect(signalMapper, SIGNAL(mapped(const QString &)), this, SLOT(readFile(const QString &)));

参考来源:https://blog.csdn.net/kingle123/article/details/72871731

                https://blog.csdn.net/zx249388847/article/details/80261649

                https://blog.csdn.net/zkl99999/article/details/53787044

猜你喜欢

转载自blog.csdn.net/clarkness/article/details/80474126