Qt 里面的信号(Signal)和槽(Slot)虽然看着像事件,但它实际上是用来在两个对象之间进行通信的。既然是通信,就会有发送者和接收者。

1、信号是发送者,触发时通过特有的关键字“emit”来发出信号。

2、槽是信号的接收者,它实则是一个方法(函数 )成员,当收到信号后会被调用。

为了让C++类能够使用信号和槽机制,必须从 QObject 类派生。QObject 类是 Qt 对象的公共基类。它的第一个作用是让 Qt 对象之形成一株“对象树”。当某个 Qt 对象发生析构时,它的子级对象都会发生析构。比如,窗口中包含两个按钮,当窗口类析构时,里面的两个按钮也会跟着发生析构。所以,在 Qt 的窗口应用程序里面,一般不用手动去 delete 指针类型的对象。位于对象树上的各个对象会自动清理。

QObject 类的另一个关键作用是实现信号和槽的功能。

1、从 QObject 类派生的类,在类内部要使用 Q_OBJECT 宏。

2、跟在 signals 关键字后面的函数被视为信号。这个关键字实际上是 Q_SIGNALS 宏,是 Qt 项目专用的,并不是 C++ 的标准关键字。

3、跟在 slots 或 public slots 后面的成员函数(方法)被认为是槽,当接收到信号时会自动调用。

信号和槽之间相互不认识,需要找个“媒婆”让它们走到一起。因此,在发出信号前要调用 QObject :: connect 方法在信号与槽之间建立连接。

老周不喜欢说得太复杂,上面的介绍应该算比较简洁了,接下来咱们来个示例,就好理解了。

这里老周定义了两个类:DemoObject 类里面包含了一个 QStack<int> 对象,是个栈集合,这个应该都懂,后进先出。两个公共方法,AddOne 用来向 Stack 对象压入元素,TakeOne 方法从 Stack 对象中弹出一个元素。不过,弹出的元素不是经 TakeOne 方法返回,而是发出 GetItem 信号,用这个信号将弹出的元素发送给接收者(槽在 TestRecver 类中)。第二个类是 TestRecver,对,上面 DemoObject 类发出的 GetItem 信号可以在 TestRecver 类中接收,槽函数是 setItem。

#include <iostream>
#include <qobject.h>
#include <qstack.h> class DemoObject : public QObject
{
// 这个是宏
Q_OBJECT private:
QStack<int> _inner; public:
void AddOne(int val)
{
_inner.push(val);
}
void TakeOne()
{
if(_inner.empty()){
return;
}
int x = _inner.pop();
// 发出信号
emit GetItem(x);
}
// 信号
signals:
void GetItem(int n);
}; class TestRecver : public QObject
{
// 记得用这个宏
Q_OBJECT // 槽
public slots:
void setItem(int n)
{
std::cout << "取出项:" << n << std::endl;
}
};

在 main 函数中,先创建 DemoObject 实例,用 AddOne 方法压入三个元素。然后创建 TestRecver 实例,用 connect 方法建立信号和槽的连接。

int main(int argc, char **argv)
{
DemoObject a;
a.AddOne(50);
a.AddOne(74);
a.AddOne(80); TestRecver r;
// 信号与槽连接
QObject::connect(&a, &DemoObject::GetItem, &r, &TestRecver::setItem); // 下面这三行会发送GetItem信号
a.TakeOne();
a.TakeOne();
a.TakeOne(); return 0;
}

下面是 CMakeLists.txt 文件:

cmake_minimum_required(VERSION 3.0.0)
project(myapp LANGUAGES CXX)
find_package(Qt6 REQUIRED COMPONENTS Core)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON) add_executable(myapp main.cpp) target_link_libraries(myapp PRIVATE Qt6::Core)

注意,这里一定要把 CMAKE_AUTOMOC 选项设置为 ON,1,或者 YES。因为我们用到了 Q_OBJECT 宏,它需要 MOC 生成一些特定C++代码和元数据。这个示例只用到 QtCore 模块的类,所以 find_package 和 target_link_libraries 中只要引入这个就行。

当你兴奋异常地编译和运行本程序时,会发生错误:

这个错误是因为 MOC 生成的代码最终要用回到我们的程序中的,但代码文件没有包含这些代码。所以你看上面已经提示你了,解决方法是包含 main.moc。这个文件名和你定义 DemoObject 类的代码文件名相同。我刚刚的代码文件是 main.cpp,所以它生成的代码文件就是 main.moc。

不过,#include 指令一定要写在 DemoObject 和 TestRecver 类的定义之后,这样才能正确放入生成的代码。# include 放在文件头部仍然会报错的,此时,DemoObject 和 TestRecver 类还没有定义,无法将 main.moc 中的源代码插入到 main.cpp 中(会找不到类)。

#include <iostream>
#include <qobject.h>
#include <qstack.h> class DemoObject : public QObject
{
// 这个是宏
Q_OBJECT ……
}; class TestRecver : public QObject
{
// 记得用这个宏
Q_OBJECT ……
}; #include "main.moc" int main(int argc, char **argv)
{
…… return 0;
}

要是你觉得这样麻烦,最省事的做法是把类的定义写在头文件中,实现代码写在cpp文件中。MOC 默认会处理头文件,所以不会报错。

之后再编译运行,就不会报错了。

如果用的是 Windows 系统,cmd 默认编码是 GBK,不是 UTF-8,VS Code 的代码默认是 UTF8 的,控制台可能会打印出来乱码。这里老周不建议改代码文件的编码,因为说不定你还要把这代码放到 Linux 系统中编译的。在 cmd 中用 CHCP 命令改一下控制台的编码,再运行程序就行了。

chcp 65001

其实,信号和槽的函数签名可以不一致。下面我们再来做一例。这个例子咱们用到 QWidget 类的 windowTitleChanged 信号。当窗口标题栏中的文本发生改变时会发出这个信号。它的签名如下:

  void windowTitleChanged(const QString &title);

这个信号有一个 title 参数,表示修改的窗口标题文本(指新的标题)。而咱们这个例子中用于和它连接的槽函数是无参数的。

private slots:  // 这个是槽
void onTitleChanged();

尽管签名不一致,但可以用。

在这个例子中,只要鼠标点一下窗口区域,就会修改窗口标题——显示鼠标指针在窗口中的坐标。窗口标题被修改,就会发出 windowTitleChanged 信号,然后,onTitleChanged 也会被调用。

接下来是实现步骤:

1、准备 CMakeLists.txt 文件。

cmake_minimum_required(VERSION 3.0.0)
project(demo VERSION 0.1.0)
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON) file(GLOB SRC_LIST ./*.h ./*.cpp)
add_executable(demo WIN32 ${SRC_LIST})
target_link_libraries(demo PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets)

这里老周就偷懒一下。add_executable(demo ....) 是添加头文件和源码文件的。老周嫌麻烦,加一个文件又要改一次,于是就用 file 命令搜索项目根目录下的所有头文件和 C++ 代码文件。然后把这些搜到的文件添加到变量 SRC_LIST 中。在 add_executable 命令中引用 SRC_LIST 变量,就可以自动添加文件了。

2、定义一个自定义窗口类,从 QWidget 类派生。

/*    头文件     */
#include <QWidget>
#include <QMessageBox>
#include <QMouseEvent>
#include <QString>
#include <QApplication> class MyWindow : public QWidget
{
Q_OBJECT public:
MyWindow(QWidget* parent = nullptr); private slots: // 这个是槽
void onTitleChanged(); protected:
void mousePressEvent(QMouseEvent *event) override;
};
/*     实现代码      */
#include "MyWindow.h" /****************************************************************/
MyWindow::MyWindow(QWidget *parent)
: QWidget::QWidget(parent)
{
// 窗口大小
resize(300, 275);
connect(this, &MyWindow::windowTitleChanged, this, &MyWindow::onTitleChanged);
} void MyWindow::onTitleChanged()
{
QMessageBox::information(this, "Test", "看,窗口标题变了。", QMessageBox::Ok);
} void MyWindow::mousePressEvent(QMouseEvent *event)
{
auto pt = event->pos();
QString s = QString("鼠标指针位置:%1, %2")
.arg(pt.x())
.arg(pt.y());
setWindowTitle(s);
QWidget::mousePressEvent(event);
}
/*****************************************************************/

重写了 mousePressEvent 方法,当鼠标按钮按下时触发,先通过事件参数的 pos 函数得到鼠标坐标,再用 setWindowTitle 方法修改窗口标题。随即 windowTitleChanged 信号发出,在槽函数 onTitleChanged 中只是用 QMessgeBox 类弹出了一个提示框。运行结果如下图所示。

一个信号可以连接多个槽,一个槽可以与多个信号建立连接。这外交能力是真的强,来者不拒。下面咱们做一个 SaySomething 信号连接三个槽的实验。

#include <QObject>

class SomeObj : public QObject
{
Q_OBJECT public:
SomeObj(QObject *parent = nullptr);
void SpeakOut(); // 用这个方法发信号 signals:
void SaySomething();
}; class SlotsObj : public QObject
{
Q_OBJECT public slots:
// 来几个cao
void slot1();
void slot2();
void slot3();
};

以上是头文件。SomeObj 类负责发出信号,SlotsObj 类负责接收信号,它有三个 cao:slot1、slot2、slot3。

下面是 SomObj 类的实现代码。

SomeObj::SomeObj(QObject *parent)
: QObject::QObject(parent)
{
// 无事干
} void SomeObj::SpeakOut()
{
emit SaySomething();
}

emit 关键字(Qt 特有)发出 SaySomething 信号。

下面是 SlotsObj 类的实现代码。

#include "app.h"
#include <iostream>
using namespace std; void SlotsObj::slot1()
{
cout << "第一个cao触发了" << endl;
}
void SlotsObj::slot2()
{
cout << "第二个cao触发了" << endl;
}
void SlotsObj::slot3()
{
cout << "第三个cao触发了" << endl;
}

来,咱们试一试,分别实例化 SomeObj 和 SlotsObj 类,然后让 SaySomething 信号依次与 slot1、slot2、slot3 建立连接。这是典型的“一号战三槽”。

int main(int argc, char** argv)
{
// 分别实例化
SomeObj sender;
SlotsObj recver;
// 建立连接
QObject::connect(&sender, &SomeObj::SaySomething, &recver, &SlotsObj::slot1);
QObject::connect(&sender, &SomeObj::SaySomething, &recver, &SlotsObj::slot2);
QObject::connect(&sender, &SomeObj::SaySomething, &recver, &SlotsObj::slot3); // 发信号
sender.SpeakOut();
return 0;
}

结果表明:信号一旦发出,三个 slot 都调用了。如下图:

好了,今天的故事就讲到这儿了,欲知后事如何,且待下回分解。

最新文章

  1. WPF系列-CheckBox
  2. jenkins自动部署maven工程到服务器----SSH+shell
  3. wkwebview a target=&quot;_blank&quot; 打不开链接的解决方案
  4. Linux kernel map
  5. 数据引用Data References
  6. CSS3之渐变效果
  7. ASP.NET与SOAP协议使用记录
  8. LeetCode——Valid Palindrome
  9. 时间序列 预测分析 R语言
  10. 使用 pm2 守护你的 .NET Core 应用程序
  11. 不用框架,原生使用python做注册接口/登陆接口/充值接口的测试,做的数据/代码分离
  12. XUnit 依赖注入
  13. nodejs静态web服务
  14. 微信for linux
  15. CentOS下安装Apache
  16. WorldWind源码剖析系列:下载队列类DownloadQueue
  17. 【python3】拷贝U盘文件
  18. linux 下crontab相关定时触发的配置规则
  19. 转:使用python的Flask实现一个RESTful API服务器端
  20. Andorid之Annotation框架初使用(四)

热门文章

  1. Luogu4391 [BOI2009]Radio Transmission 无线传输 (KMP)
  2. (已解决)Adobe Creative Cloud 安装 Acrobat PDF 报错 DW071 DW003
  3. 基于开源方案构建统一的文件在线预览与office协同编辑平台的架构与实现历程
  4. 设计模式——桥接模式(Bridge模式)
  5. kettle通过SSH连接Mysql数据库(SSH隧道)
  6. Exchange如何将邮件转发给外部邮件地址
  7. 如何在Elasticsearch中使用pipeline API来对事件进行处理
  8. flask中验证用户登录的装饰器
  9. DophineSheduler上下游任务之间动态传参案例及易错点总结
  10. aardio + PowerShell 可视化快速开发独立 EXE 桌面程序