Tudou

博客园 首页 新随笔 联系 订阅 管理

推荐参考
https://doc.qt.io/qt-5/signalsandslots.html 官方文档-- 最权威的介绍
https://www.devbean.net/2012/08/qt-study-road-2-catelog/ 豆子大神的博客
https://www.cnblogs.com/lifexy/p/8876016.html 简洁明了

0.概述

基础介绍,来自IBM的老文章

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

在我们所熟知的很多 GUI 工具包中,窗口小部件 (widget) 都有一个回调函数用于响应它们能触发的每个动作,这个回调函数通常是一个指向某个函数的指针。但是,在 QT 中信号和槽取代了这些凌乱的函数指针,使得我们编写这些通信程序更为简洁明了。 信号和槽能携带任意数量和任意类型的参数,他们是类型完全安全的,不会像回调函数那样产生 core dumps。

所有从 QObject 或其子类 ( 例如 Qwidget) 派生的类都能够包含信号和槽。当对象改变其状态时,信号就由该对象发射 (emit) 出去,这就是对象所要做的全部事情,它不知道另一端是谁在接收这个信号。这就是真正的信息封装,它确保对象被当作一个真正的软件组件来使用。槽用于接收信号,但它们是普通的对象成员函数。一个槽并不知道是否有任何信号与自己相连接。而且,对象并不了解具体的通信机制。

你可以将很多信号与单个的槽进行连接,也可以将单个的信号与很多的槽进行连接,甚至于将一个信号与另外一个信号相连接也是可能的,这时无论第一个信号什么时候发射系统都将立刻发射第二个信号。总之,信号与槽构造了一个强大的部件编程机制。

1.信号槽机制

信号槽是 Qt 框架引以为豪的机制之一。所谓信号槽,实际就是观察者模式。当某个事件发生之后,比如,按钮检测到自己被点击了一下,它就会发出一个信号(signal)。这种发出是没有目的的,类似广播。如果有对象对这个信号感兴趣,它就会使用连接(connect)函数,意思是,将想要处理的信号和自己的一个函数(称为槽(slot))绑定来处理这个信号。也就是说,当信号发出时,被连接的槽函数会自动被回调。这就类似观察者模式:当发生了感兴趣的事件,某一个操作就会被自动触发。

2.系统自带的信号和槽

下面我们完成一个小功能,我们已经学习了按钮的创建,但是还没有体现出按钮的功能,按钮最大的功能也就是点击后触发一些事情,比如我们点击按钮,就把当前的窗口给关闭掉,那么在Qt中,这样的功能如何实现呢?
其实两行代码就可以搞定了,我们看下面的代码:

QPushButton * quitBtn = new QPushButton("关闭窗口",this);
connect(quitBtn,&QPushButton::clicked,this,&MyWidget::close);

第一行是创建一个关闭按钮,第二行就是核心了,也就是信号槽的使用方式

connect()函数最常用的一般形式:
connect(sender, signal, receiver, slot);

参数解释:

  • sender:发出信号的对象
  • signal:发送对象发出的信号
  • receiver:接收信号的对象
  • slot:接收对象在接收到信号之后所需要调用的函数(槽函数)

那么系统自带的信号和槽通常如何查找呢,这个就需要利用帮助文档了,在帮助文档中比如我们上面的按钮的点击信号,在帮助文档中输入QPushButton,首先我们可以在Contents中寻找关键字 signals,信号的意思,但是我们发现并没有找到,这时候我们应该想到也许这个信号的被父类继承下来的,因此我们去他的父类QAbstractButton中就可以找到该关键字

//  摘抄自文档
Signals

void clicked(bool checked = false)
void pressed()
void released()
void toggled(bool checked)

- 3 signals inherited from QWidget
- 2 signals inherited from QObject 

这里的clicked就是我们要找到,槽函数的寻找方式和信号一样,只不过他的关键字是slot。

3.使用信号槽所需要的条件

很简单:类需要继承自QObject,并且在类的开头声明Q_OBJECT宏. 下面看一下官方文档的例子

#include <QObject>

class Counter : public QObject
{
    Q_OBJECT

public:
    Counter() { m_value = 0; }

    int value() const { return m_value; }

// slots
public slots:
    void setValue(int value);

// signal
signals:
    void valueChanged(int newValue);

private:
    int m_value;
};

4.自定义信号和自定义槽

使用connect()可以让我们连接系统提供的信号和槽。但是,Qt 的信号槽机制并不仅仅是使用系统提供的那部分,还会允许我们自己设计自己的信号和槽。

4.1自定义一个信号

signals:
    void valueChanged(int newValue); // 参数可有可无,看自己需求
    // 信号不需要实现,也不能实现

4.2发送信号

发送信号需要使用Qt 增加的 emit关键字

emit valueChanged(value);

// demo
void Counter::setValue(int value)
{
    if (value != m_value) {
        m_value = value;
        // emit 信号
        emit valueChanged(value);
    }
}

4.3自定义一个槽

public slots:
    void processValue(int value);

// 自定义槽函数 需要声明也需要实现
void Counter::processValue(int value){
    // do something
}

4.4 同名信号和同名槽函数(允许有重载的信号和槽函数哦)

重载的信号和槽函数 ,需要利用函数指针来指向函数地址来确定究竟是需要哪一个信号和槽函数。(见下文,8.Qt5推荐的信号槽写法)

4.5自定义信号槽需要注意的事项

  • 发送者和接收者都需要是QObject的子类;
  • 信号函数和槽函数返回值是 void。  信号返回值是必须为void,槽是否必须为void虽然都都能编译和运行,但是还是建议返回值为void
  • 信号只需要声明,不需要实现,只有Qt类才能定义信号,且必须在头文件中声明。返回值必须为void类型
  • 发送信号时,只需要通过emit关键字调用信号函数即可
  • 信号函数的属性会被自动设置为protected类型
  • 槽函数需要声明也需要实现
  • 槽函数是普通的成员函数,作为成员函数,会受到 public、private、protected 的影响;
  • 使用connect()函数连接信号和槽。
  • 任何成员函数、static 函数、全局函数和 Lambda 表达式都可以作为槽函数
  • 信号槽要求信号和槽的参数一致,所谓一致,是参数类型一致。
  • 如果信号函数的参数多于槽函数时,多于的参数将被忽略
  • 如果信号和槽的参数不一致,允许的情况是,槽函数的参数可以比信号的少,即便如此,槽函数存在的那些参数的顺序也必须和信号的前面几个一致起来。这是因为,你可以在槽函数中选择忽略信号传来的数据(也就是槽函数的参数可以比信号的少)。

4.6信号与槽扩展知识

  • 一个信号可以和多个槽相连
    如果是这种情况,这些槽会一个接一个的被调用,但是它们的调用顺序是不确定的。

  • 多个信号可以连接到一个槽
    只要任意一个信号发出,这个槽就会被调用。

  • 一个信号可以连接到另外的一个信号
    当第一个信号发出时,第二个信号被发出。除此之外,这种信号-信号的形式和信号-槽的形式没有什么区别。

  • 槽可以被取消连接
    这种情况并不经常出现,因为当一个对象delete之后,Qt自动取消所有连接到这个对象上面的槽。

  • 信号槽可以断开
    利用disconnect关键字是可以断开信号槽

  • 使用Lambda 表达式
    在使用 Qt 5 的时候,能够支持 Qt 5 的编译器都是支持 Lambda 表达式的。 在连接信号和槽的时候,槽函数可以使用Lambda表达式的方式进行处理。后面我们会详细介绍什么是Lambda表达式

5.连接信号和槽QObject::connect函数

参考:
Qt 学习之路 2(4):信号槽

上面介绍过最常用的一种connect,下面我们看一下Qt提供的所有的connect。
这5个函数全是[static]的,返回值这里先不讨论。
我们可以直接使用QObject::connect来使用,一般我们直接使用connect多一点.

// 最常用的
    QMetaObject::Connection QObject::connect(const QObject *sender, 
	const char *signal,
	const QObject *receiver,
	const char *method,
	Qt::ConnectionType type = Qt::AutoConnection)

QMetaObject::Connection connect(const QObject *, const QMetaMethod &,
                                const QObject *, const QMetaMethod &,
                                Qt::ConnectionType);

QMetaObject::Connection connect(const QObject *, const char *,
                                const char *,
                                Qt::ConnectionType) const;

QMetaObject::Connection connect(const QObject *, PointerToMemberFunction,
                                const QObject *, PointerToMemberFunction,
                                Qt::ConnectionType)

QMetaObject::Connection connect(const QObject *, PointerToMemberFunction,
                                Functor);

6.信号与槽的连接方式(connect函数的Qt::ConnectionType的参数)

[static] QMetaObject::Connection QObject::connect(const QObject *sender, 
	const char *signal,
	const QObject *receiver,
	const char *method,
	Qt::ConnectionType type = Qt::AutoConnection)

这个参数 Qt::ConnectionType type = Qt::AutoConnection

6.1 Qt::ConnectionType 介绍

参考:

enum Qt::ConnectionType

This enum describes the types of connection that can be used between signals and slots. In particular, it determines whether a particular signal is delivered to a slot immediately or queued for delivery at a later time.

Constant Value Description 中文解释
Qt::AutoConnection 0 (Default) If the receiver lives in the thread that emits the signal, Qt::DirectConnection is used. Otherwise, Qt::QueuedConnection is used. The connection type is determined when the signal is emitted. 自动连接:(默认值)如果信号在接收者所依附的线程内发射,则等同于直接连接。如果发射信号的线程和接受者所依附的线程不同,则等同于队列连接
Qt::DirectConnection 1 The slot is invoked immediately when the signal is emitted. The slot is executed in the signalling thread. 直接连接:当信号发射时,槽函数将直接被调用。无论槽函数所属对象在哪个线程,槽函数都在发射信号的线程内执行。[这种方式不能跨线程传递消息]
Qt::QueuedConnection 2 The slot is invoked when control returns to the event loop of the receiver's thread. The slot is executed in the receiver's thread. 队列连接:当控制权回到接受者所依附线程的事件循环时,槽函数被调用。槽函数在接收者所依附线程执行。[这种方式既可以在线程内传递消息,也可以跨线程传递消息]
Qt::BlockingQueuedConnection 3 Same as Qt::QueuedConnection, except that the signalling thread blocks until the slot returns. This connection must not be used if the receiver lives in the signalling thread, or else the application will deadlock. Qt::QueuedConnection类似,但是发送消息后会阻塞,直到等到关联的slot都被执行。[说明它是专门用来多线程间传递消息的,而且是阻塞的]
Qt::UniqueConnection 0x80 This is a flag that can be combined with any one of the above connection types, using a bitwise OR. When Qt::UniqueConnection is set, QObject::connect() will fail if the connection already exists (i.e. if the same signal is already connected to the same slot for the same pair of objects). This flag was introduced in Qt 4.6. 与默认工作方式相同,只是不能重复连接相同的信号和槽,因为如果重复连接就会导致一个信号发出,对应槽函数就会执行多次。这个flag可以通过按位或运算来和以上结合在一起使用。(意思就是:只有它不是一个重复连接,连接才会成功。如果之前已经有了一个连接(相同的信号连接到同一对象的同一个槽上),那么连接将会失败并将返回false。)

With queued connections, the parameters must be of types that are known to Qt's meta-object system, because Qt needs to copy the arguments to store them in an event behind the scenes. If you try to use a queued connection and get the error message:

QObject::connect: Cannot queue arguments of type 'MyType'

Call qRegisterMetaType() to register the data type before you establish the connection.

When using signals and slots with multiple threads, see Signals and Slots Across Threads.

See also Thread Support in Qt, QObject::connect(), qRegisterMetaType(), and Q_DECLARE_METATYPE().

6.2 使用建议

那么如何使用呢?

  • 如果是在同一线程里面的操作(signalslot都在同一个线程),那么用Qt::DirectConnection的效率最高(使用默认值Qt::AutoConnection也OK),主要是Qt::DirectConnectionQt::QueuedConnection都需要储存到队列。
  • 如果是多个线程之间进行消息传递(signalslot都在不同线程),那么就要用到Qt::QueuedConnection或者Qt::BlockingQueuedConnection,不过一个是无阻塞的(Qt::QueuedConnection),一个是阻塞的(Qt::BlockingQueuedConnection,发送消息后会阻塞,直到所有的slot都被执行)。

7.补充:Qt4版本的信号槽写法

QObject::connect(scrollBar, SIGNAL(valueChanged(int)),
                  label,  SLOT(setNum(int)));
                  
 //   SIGNAL(信号函数名(参数类型))
 //  SLOT(槽的函数名(参数类型))
 // Qt4 的写法是不是更加的简单和方便

这里使用了SIGNAL和SLOT这两个宏,将两个函数名转换成了字符串。
注意到connect()函数的 signal 和 slot 都是接受字符串,一旦出现连接不成功的情况,Qt4是没有编译错误的(因为一切都是字符串,编译期是不检查字符串是否匹配),而是在运行时给出错误。这无疑会增加程序的不稳定性。所以尽量避免这种写法。不过这种写法,某些场合也能带来便利。

注意: Qt5在语法上完全兼容Qt4,而反之是不可以的。

8.Qt5推荐的信号槽写法

link

由于函数有重载(注意,信号实际也是一个普通的函数),因此对于有重载的函数不能用一个取址操作符获取其地址。需要使用函数指针。

QObject::connect(&newspaper,
                 (void (Newspaper:: *)(const QString &, const QDate &))&Newspaper::newPaper,
                 &reader,
                 &Reader::receiveNewspaper);

C++11后最好这样写,高端大气上档次,就是有点麻烦,更加安全,麻烦点也是值得的。

QObject::connect(&newspaper,
                 static_cast<void (Newspaper:: *)(const QString &, const QDate &)>(&Newspaper::newPaper),
                 &reader,
                 &Reader::receiveNewspaper);
                 

可以看出虽然Qt5 的信号槽的要求更加的多样化,更加的准确化,同时也加大了写法的难度。 但是写的程序更加的健壮。
Qt4的信号槽写法也可以继续使用哦!

posted on 2021-01-11 16:05  JindouBlog  阅读(1322)  评论(0编辑  收藏  举报