信号的本质
信号是由于⽤⼾对窗⼝或控件进⾏了某些操作,导致窗⼝或控件产⽣了某个特定事件,这时 Qt 对应的窗⼝类会发出某个信号,以此对⽤⼾的操作做出反应。因此,信号的本质就是事件。如:
在Qt中我们对哪个窗口进行操作,哪个窗口就可以捕捉到这些被触发的事件。对于使⽤者来说触发了⼀个事件我们就可以得到 Qt 框架给我们发出的某个特定信号。信号的呈现形式就是函数, 也就是说某个事件产⽣了, Qt 框架就会调⽤某个对应的信号函数, 通知使⽤者。在 Qt 中信号的发出者是某个实例化的类对象。
槽的本质
槽(Slot)就是对信号响应的函数。槽本质上就是⼀个回调函数,与⼀般的 C++ 函数是⼀样的,可以定义在类的任何位置( public、protected 或 private ),可以具有任何参数,可以被重载,也可以被直接调⽤(但是不能有默认参数)。槽函数与⼀般的函数不同的是:槽函数可以与⼀个信号关联,当信号被发射时,关联的槽函数被⾃动执⾏。
说明
-(1)信号和槽机制底层是通过函数间的相互调⽤实现的。每个信号都可以⽤函数来表⽰,称为信号函数;每个槽也可以⽤函数表⽰,称为槽函数。例如: “按钮被按下” 这个信号可以⽤ clicked() 函数表⽰,“窗⼝关闭” 这个槽可以⽤ close() 函数表⽰,假如使⽤信号和槽机制实现:“点击按钮会关闭窗⼝” 的功能,其实就是 信号函数clicked() 函数调⽤ 槽函数close() 函数的效果。
信号函数的定义是 Qt ⾃动在编译程序之前⽣成的. 编写 Qt 应⽤程序的程序猿⽆需关注.这种⾃动⽣成代码的机制称为 元编程 (Meta Programming) . 这种操作在很多场景中都能⻅到。
在 Qt 中,QObject 类提供了⼀个静态成员函数 connect() ,该函数专⻔⽤来关联指定的信号函数和槽函数。QObject 是 Qt 内置的⽗类. Qt 中提供的很多类都是直接或者间接继承⾃ QObject。
connect() 函数原型:
connect (const QObject *sender,
const char * signal ,
const QObject * receiver ,
const char * method ,
Qt::ConnectionType type = Qt::AutoConnection )
参数说明:
下面我们来写一个实例。
我们创建一个button,然后将QPushButton类的clicked信号函数和QWidget类的槽函数close进行connect连接,这样当点击按钮时,就会发出一个clicked信号,然后就会执行对应的close槽函数。
通过上面的案例我们初步体验了信号和槽的作用,那么Qt中有那么多控件,我们怎么知道每个控件都有什么信号函数和槽函数呢?这就需要我们翻阅Qt的帮助文档了,并且因为Qt中使用了大量的继承,所以当我们查看一个控件的信号函数和槽函数时,如果没有找到,还可以去其父类中查找,因为子类会将父类的信号和槽函数都继承下来。
我们看到QPushButton类的父类QAbstractButton类就有槽函数和信号。
我们看到clicked函数有一个参数,这个参数我们现在还用不到,这是为复选按钮准备的。我们看到clicked函数的介绍中说当按钮被点击后就会触发一个信号。我们查阅文档中的信号的时候,重点就是关注信号的发送时机和用户进行什么样的操作可以产生这个信号。
我们在Qt文档中查看connect函数看到,该函数的第二个参数和第四个参数为char *类型的指针,但是我们调用connect函数时,传入的是函数指针。我们知道char *类型指针和函数指针虽然都是指针,但是是两个不同类型的指针,在C++中不允许使用两个不同类型的指针相互赋值,那么Qt中为什么是这样的呢?
这是因为我们看到的这个connect函数声明是Qt4版本的connect函数的声明。在Qt4版本中,传参的写法和我们现在使用的不同,旧版本Qt使用connect函数时,第二个参数需要加上SIGNAL宏,第四个参数需要加上SLOT宏。这两个宏为两段代码,会将我们传入的函数指针转成char *类型的指针。
从Qt5开始对上述写法做出了简化,即不需要写SIGNAL和SLOT宏了,因为给connect函数提供了重载版本,重载版本中,第二个参数和第四个参数成了泛型参数,允许传入任意类型的函数指针了。此时connect函数就有了一定的参数检查功能,如果传入的第一个参数和第二个参数不匹配,或者第三个参数和第四个参数不匹配,即2,4参数不是1,3参数的成员函数,那么代码在编译时就会出错。
所谓的槽函数就是一个普通的成员函数,自定义槽函数就和自定义一个普通函数类似。在Qt4中槽函数必须放到public/private/protected slots中,public slots中的slots并不是C++标准中的语法,而是Qt自己扩展的关键字,Qt里广泛使用了元编程技术,即基于自己扩展的语法来生成复合C++标准的语法。qmake在构建Qt项目的时候会调用专门的扫描器,扫描代码中特定的关键字,例如slots这种,然后基于关键字自动生成符合C++标准的语法,因为这样项目才可以编译通过并且运行。
我们还可以可视化⽣成槽函数。
然后我们看到Qt Creator直接给我们生成好了一个函数。我们在这个槽函数里面直接写处理clicked信号的代码即可。
我们看到并没有通过connect函数将按钮和on_pushButton_clicked函数进行连接,但是按钮发出的clicked信号和on_pushButton_clicked函数却可以关联。这是因为在Qt中除了通过connect函数来连接信号和槽函数之外,还可以通过函数名字的方式来自动连接!不过这个槽函数的名称需要符合一定的规则。⾃动⽣成槽函数的名称有⼀定的规则。槽函数的命名规则为:on_XXX_SSS,其中:
如:" on_pushButton_clicked() " ,pushButton 代表的是对象名,clicked 是对应的信号。
这些操作都是在底层Qt自动处理的,可以看到在setupUi函数中调用了QMetaObject::connectSlotsByName(Widget)函数,该函数会触发自动连接信号槽函数的规则。
Qt中也允许自定义信号,但是很少会用到自定义信号的需求,因为Qt内置的信号基本已经覆盖了所有用户可能的操作。自定义信号比较简单,因为自定义信号也是声明一个信号函数,并且这个信号函数不需要自己实现。我们只需要写出信号函数的声明,并且告诉Qt这是一个信号即可,那么我们怎么告诉Qt这是一个信号函数呢?这就需要Qt自己扩展的signals关键字了,在signals后面的函数都会被当作信号函数。作为信号函数,这个函数的返回值必须是void,有没有参数都可以,也可以支持重载。当qmake构建项目的时候,扫描到signals关键字,就会自动把下面的函数声明认为是信号,并且给这些信号函数自动生成函数定义。
我们看到运行程序后并没有反应,这是因为没有触发自定义的信号,Qt内置的信号我们可以通过点击,移动等事件来触发,但是我们自定义的信号需要我们通过代码手动来触发。
我们看到通过emit触发自定义信号后,程序调用了与自定义信号连接的槽函数。
下面我们再可视化创建一个按钮,并且为这个按钮生成槽函数,在该槽函数中通过emit触发我们自定义的信号。其实emit是个空宏,什么也没有做,真正的操作都包含在mySignal内部生成的函数定义了,我们就算不写emit关键字,只要调用了信号函数mySignal,那么信号也是可以触发的。不过实际开发过程中为了代码可读性高,还是加上emit关键字比较好。
Qt 的信号和槽也⽀持带有参数, 同时也可以⽀持重载.此处我们要求, 信号函数的参数列表要和对应连接的槽函数参数列表⼀致.此时信号触发, 调⽤到槽函数的时候, 信号函数中的实参就能够被传递到槽函数的形参当中. 通过这样的机制, 就可以让信号给槽传递数据了.
当信号和槽带参数时,两个函数的参数类型必须一一对应,个数不同是可以的,但是要求信号的参数个数必须大于槽的参数的个数,而且槽的参数要和信号的前面的参数一一对应。
通过传参就可以起到复用代码的效果,当有多个逻辑,逻辑上整体一致,但是涉及到的数据不同时,就可以通过函数-参数来复用代码。Qt中很多内置的信号也是带有参数的,clicked信号就带有一个参数,这个参数表示当前按钮是否处于“选中”状态,这个选中状态对于QPushButton没有意义,但是对于QCheckBox复选框有意义。
下面我们来看信号和槽参数个数不一致的情况。
我们看到信号和槽的参数个数是可以不一致的,但是必须要保证信号的参数个数大于槽的参数个数。如果参数个数不同,槽函数就会按照参数顺序,拿到信号的前N个参数,所以需要保证信号的前N个参数和槽的参数是一一对应的。那么为什么会有信号和槽函数参数个数不同的情况呢?这是因为一个槽函数可能会绑定多个信号,如果严格要求参数个数一致,那么就意味着信号绑定到槽函数的要求变高了。而如果不严格要求参数个数一致,那么就有更多的信号可以绑定到同一个槽函数上了。
在Qt中如果想要让某个类使用信号槽机制,那么就需要在类最开始的地方加上Q_OBJECT宏,这个宏会展开成很多额外的代码,以此来支持信号槽机制。
⼀对⼀
主要有两种形式,分别是:⼀个信号连接⼀个槽 和 ⼀个信号连接⼀个信号。一个信号连接一个槽函数我们在上面已经示范过了,下面我们示范一个信号连接一个信号。
使⽤ disconnect 即可完成断开。disconnect 的⽤法和 connect 基本⼀致。
我们可视化生成一个按钮,并且生成一个槽函数。可以看到点击按钮就会打印点击。
当我们使用disconnect函数后,可以看到此时按钮的clicked信号和槽函数之间断开了连接。
优点: 松散耦合
信号发送者不需要知道发出的信号被哪个对象的槽函数接收,槽函数也不需要知道哪些信号关联了⾃⼰,Qt的信号槽机制保证了信号与槽函数的调⽤。⽀持信号槽机制的类或者⽗类必须继承于 QObject类。
缺点: 效率较低
与回调函数相⽐,信号和槽稍微慢⼀些,因为它们提供了更⾼的灵活性,尽管在实际应⽤程序中差别不⼤。通过信号调⽤的槽函数⽐直接调⽤的速度慢约10倍(这是定位信号的接收对象所需的开销;遍历所有关联;编组/解组传递的参数;多线程时,信号可能需要排队),这种调⽤速度对性能要求不是⾮常⾼的场景是可以忽略的,是可以满⾜绝⼤部分场景。