QT 学习笔记摘要(三)
第一节 事件
1. 概念
事件:是用户和应用软件间产生的一个 交互 操作,由用户操作产生或者系统内部产生,通过 事件循环 对事件进行处理,事件也可以用来在对象间进行信息交互
信号槽 : 用户进行的各项操作,就可能会产生出信号,可以给某个信号指定槽函数,当信号触发时,就能够自动的执行到对应的槽函数
事件: 用户进行的各种操作,也会产生事件,程序员同样可以给事件关联上处理函数(处理的逻辑),当事件触发的时候,就能够执行到对应的代码
总结: 信号槽就是对于事件的进一步封装,事件是信号槽的底层机制
在Qt平台中会将系统产生的消息转换为Qt事件 .事件本质上就是一个QEvent的对象
2. 为什么会出现事件
在实际Qt开发程序的过程中,绝大部分和用户之间进行的交互都是通过"信号槽"来完成的
但是在有些特殊情况下,信号槽不一定能搞定(某个用户的动作行为,Qt没有提供对应的信号)
此时就需要通过重写事件处理函数的形式,来手动处理事件的响应逻辑
让当前的类,重写某个事件处理函数,这里用到的是"多态"机制,创建子类,继承自Qt中已有的类
再在子类中重写父类的事件处理函数,后续事件触发的过程中,就会通过多态这样的机制,执行到我们自己写的子类函数中
3. 事件处理流程(重要)
3.1 事件产生
用户操作的各种操作都会产生事件,比如鼠标点击,键盘输入,拖动窗口,而在qt中会将所有的事件包装成一个QEvent对象,并将对象放入事件队列中
3.2 事件分发/事件过滤器
Qt 的事件循环将事件分发给目标对象前,会检查目标对象是否安装了事件过滤器,
当安装了:这个事件就需要先经过事件过滤器eventFilter(),事件能否向下进行,是需要通过eventFilter()返回值判断的
当返回值为true:表明事件已经被处理完毕了,不会向下传递了
当返回值为false:表明事件还没有处理完毕,会向下执行,并将事件分发给目标对象
当没有安装:这个事件就直接交给目标对象,通常是窗口控件等
3.3 事件处理
走到这一步,表明该事件没有被过滤器拦截,它将被传递给目标对象的 event() 函数 当事件已经交给目标对象后,还需要判读目标对象是否存在处理这个事件的函数
存在时:直接调用对应的事件处理函数处理
不存在时:该事件就会沿着继承链继续向上传递,交给目标对象的父对象去处理,以此类推
4. 代码演示
4.1 演示一: 绑定了信号与槽的控件 又重写了相同的事件函数
首先新建一个mainwindow窗口的函数,并在ui界面中添加一个按钮,在转到槽,实现点击信号的槽函数,这个槽函数中实现打印一句话就行了
之后再在项目中新建一个mypushbutton类继承QPushbutton
再将ui界面中的按钮类型提升为我们自定义的mypushbutton类型
运行项目得到:
说明一下:
- 这个按钮此时是绑定了信号与槽的,而打印结果和我们预期一样
接下来我们在子类中重写鼠标按下事件
然后再运行程序:
通过打印结果我们发现,这个按钮本来绑定的信号槽没有触发了,而是执行的重写鼠标按下事件的函数
结论/总结
当目标对象重写了事件处理函数以后,原本的槽函数没有被触发,这是因为在我们当前这个事件处理函数中,根本就没有发送信号
而之前被触发,是因为父类的QPushButton中的event()在发送信号,而在这里如果想要触发槽函数,可以通过:
方式一:自己手动发送信号(这里是emit clicked())
方式二(推荐):调用父类的事件处理函数(这里是QPushButton::mousePressEvent(event);)则父类就会帮我们发送信号
4.2 演示二:事件过滤器使用
补:事件过滤器,就是对指定事件进行过滤,增强事件处理函数
和上面一样还是先创建一个自定义的类MyEvenFilter
说明一下:
- 在我们自定义的事件过滤器对象中,必须重写eventFilter()事件过滤函数
- watched参数表示:目标对象
- event参数表示:事件对象
接下来就是在主窗口中安装这个事件过滤器了,不过这个需要先在主窗口中创建一个我们自定义类的对象(就是添加一个我们自定义类型的成员函数),方便调用
说明一下:
- 安装事件过滤器的函数是installEventFilter
运行演示
说明一下:
- 此时事件过滤器,没有过滤到鼠标按下的事件,然后就直接返回true,自然连按钮也不会出现了,同理信号槽也没有用了,按钮中重写的事件也没有用了
- 但是当我直接return false;就会继续向下执行事件
- 上面代码表示捕捉鼠标按下事件,[进行增强处理], return ture;不再先下执行
- 同理如果改成return fasle;就会向下处理
推荐返回父类的事件过滤器
推荐返回值返回时:
- 直接调用父类的事件过滤器
5. 事件VS信号
项目 | 信号/槽 | 事件 |
触发 | 手动 emit | Qt 自动分发(系统或用户触发) |
响应 | connect 后自动调用槽函数 | 重写事件函数(如 mousePressEvent ) |
用途 | 对象通信,逻辑处理 | 用户输入、窗口变化等底层操作 |
6. QEventLoop
QEventLoop是Qt框架中的一个核心组件,用于实现局部或临时的事件循环。Qt中,主要的事件循环是由QCoreApplication
或QGuiApplication
提供的
6.1 在某个函数中,期望5s后在继续执行后面逻辑
方式一:QThread::sleep
说明一下:
- sleep()这种方式会卡死整个页面,它会将程序阻塞到这里,导致其他操作都无法执行
方式二:QEventLoop事件循环
说明一下:
- 可以配合定时器,当到一定时间后就发送信号,并关联时的发送退出事件循环信号
- 使用QEventLoop事件循环机制将不会阻塞其他程序执行
6.2 实现一个模态对话框
模态:当弹出新窗口时,无法对父窗口进行任何操作,
--- 首先新建一个自定义类,然后让它继承自QDialog类
---- 再在页面中新建按钮,并关联点击信号,打开对话框
说明一下:
- 对应时间循环可以简单的理解为一个死循环,不过里面不仅仅只是死循环
7. 常见事件
7.1 QMouseEvent(鼠标事件)
#ifndef LABEL_H
#define LABEL_H#include <QWidget>
#include <QLabel>
#include <QMouseEvent>
class Label : public QLabel
{Q_OBJECT
public:Label(QWidget* parent);void mousePressEvent(QMouseEvent *event);// 按下void mouseReleaseEvent(QMouseEvent *event);// 释放void mouseDoubleClickEvent(QMouseEvent *event);// 双击
};#endif // LABEL_H
#include "label.h"
#include <QDebug>
Label::Label(QWidget* parent) : QLabel(parent)
{;
}void Label::mousePressEvent(QMouseEvent *event)
{if(event->button() == Qt::LeftButton){//qDebug() << "按下左键";this->setText("按下左键");}else if(event->button() == Qt::RightButton){qDebug() << "按下右键";}
}void Label::mouseReleaseEvent(QMouseEvent *event)
{if(event->button() == Qt::LeftButton){qDebug() << "释放左键";}else if(event->button() == Qt::RightButton){qDebug() << "释放右键";}
}void Label::mouseDoubleClickEvent(QMouseEvent *event)
{if(event->button() == Qt::LeftButton){qDebug() << "双击左键";}else if(event->button() == Qt::RightButton){qDebug() << "双击右键";}
}
这里需要再ui界面中把这个控件提升为我们自己写的类,具体操作是: 选中原来的控件,鼠标右键,提升,选择新的类型
说明一下:
- 这里是针对QLabel重写的事件,自然也只能在label控件中看到效果
说明一下:
- 这里是在widget中重写的事件,则这个大窗口都会有效果
- Qt为了保证程序的流畅性,默认情况下不会对鼠标移动进行追踪,鼠标移动的时候不会调用
mouseMoveEvent,除非显示告诉Qt要追踪鼠标位置
7.2 QWheelEvent(鼠标滚轮事件)
7.3 QKeyEvent(键盘事件)
7.4 QTimerEvent(时间事件)
说明一下:
- 此处的timerId类似于linux中的文件描述符
- QTimer的背后是QTimerEvent定时器事件进行支持的,所以要实现定时器,通常使用QTimer
7.5 QMoveEvent(窗口移动事件)
7.6 QResizeEvent(窗口尺寸事件)
第二节 进程 && 线程
1. 进程
- QProcess类:额外执行新的程序,执行程序就是一个新的进程执行
- QProcess类:进程管理类,使用QProcess类可以操作进程
- start(程序的路径):启动进程
1.1 QProcess入门
#include "mainwindow.h"#include <QApplication>
#include <QProcess>
#include <QDebug>
#include <QTextCodec>int main(int argc, char *argv[])
{QApplication a(argc, argv);MainWindow w;w.show();// 打开一个记事本进程// 实例化进程对象QProcess process;// 执行进程命令//process.start("notepad");// 执行ping 进程操作
// process.start("ping www.baidu.com");process.start("ping",QStringList() <<"www.baidu.com");// 获取进程返回值// process.waitForFinished(5000) = 如果进程在5s之内没有执行结束,没有获取返回值,就说明该进程执行失败if(process.waitForFinished()){QByteArray data = process.readAllStandardOutput();QByteArray error = process.readAllStandardError();// 获取系统默认字符编码QTextCodec *codec = QTextCodec::codecForName("System");QString result = codec->toUnicode(data);qDebug() << result;}return a.exec();
}
1.2 进程间通信
本地:1. 共享内存 2. 共享文件
非本地:1.网络 2. 数据库
1.3 共享内存
新建读内存项目
说明一下:
- 读共享内存时,只需要关联相同的共享内存就行了,即名字要相同
新建写内存项目
mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H#include <QMainWindow>
#include <QSharedMemory>QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACEclass MainWindow : public QMainWindow
{Q_OBJECTpublic:MainWindow(QWidget *parent = nullptr);~MainWindow();private slots:void on_btn_write_clicked();private:Ui::MainWindow *ui;QSharedMemory *memory;
};
#endif // MAINWINDOW_H
mainwindow.cpp
说明一下:
- 写共享内存时,需要开辟共享内存的大小
程序运行
注意运行时:
- 需要先运行写内存项目(会创建共享内存),之后在运行读内存项目(这样才能关联共享内存)
2. 线程
2.1 线程介绍
在Qt中提供了一个QThread类来描述线程,QThread提供了一个与平台无关的管理线程的方法,即一个QThread对象管理一个线程
2.2 为什么需要线程
原因一:进行耗时操作时,如果在UI线程(主线程) 里面进行耗时操作,界面不会响应用户操作,则会产生卡界面的现象,由于线程是异步的,则可以把这个耗时操作转移给子线程执行
原因二:为了提升性能,现在的电脑一般都是多核CPU,多线程的并行处理事务,将会大大提高程序性能
2.3 注意事项
- Qt的默认线程为UI线程(主线程)︰负责窗口事件处理或窗口控件数据的更新;
- 子线程负责后台的业务逻辑,子线程不能对窗口对象做任何操作,这些事需要交给UI线程;
- 主线程和子线程之间如果进行数据的传递,需要使用信号槽机制
2.4 四种使用方式
- 继承QThread类,重写run()方法,实例化QThread子类实例对象,调用start()
- 使用moveToThread将一个继承QObject的子类对象移至线程,内部槽函数均在线程中执行
- 使用QThreadPool,搭配QRunnable (线程池)
- 使用QtConcurrent(线程池)
2.5 继承QThread类 && 重写run函数
先创建一个自定义的类,然后让它继承自QThread
再在子线程中重写run函数,并将主线程中耗时的操作交个子线程中运行
之后就是在主线程中创建实例对象,并start开始线程
说明一下:
- MyThread* thread = new MyThread;只是实例化了一个对象,并没有创建子线程
- thread->start();这段代码将会新建一个线程,并调用里面重写的run函数
既然本质上还是要调用重写的run函数,为什么不直接调用run函数,而是用thread->start()?
-- 等价与:qt线程中Start()和run()的区别?
- start()会另开一个线程,去异步执行业务逻辑,run()只是一个普通的成员函数,它不会另开线程,只会在当前线程中去同步执行业务逻辑
运行程序
说明一下:
- 从输出结果来看,线程是异步执行的,而不是同步/顺序执行的
QThread实例存在于实例化它的旧线程中,而不是调用run()的新线程中。这意味着QThread的所有绑定的信号槽和调用方法都将在旧线程中执行。
说明一下:
- 由此可以证明,原来QThread实例化对象中所绑定的信号与槽,将只能在旧线程中调用
2.6 moveToThread函数 && 迁移子类对象到线程中
默认情况下我们在代码中创建的对象都属于主线程,这个对象的槽函数在调用的时候,占用的都是主线程的时间,
我们也可以将一个QObject类型的对象或子类对象通过moveToThread移动到子线程中去,这样当这个对象的槽函数被信号触发调用的时候,槽函数占用的就是子线程的时间。
更改此对象及其子对象的线程关联性。如果对象有父对象,则不能移动该对象。事件处理将在TargetThread中继续。
新建文件
代码编写
首先子定义的这个work是继承自QObject的,里面实现一个槽函数打印线程ID
而在Mainwindow2中,我们绑定信号与槽,并执行创建子线程,
注:我发起这个信号是通过ui界面中按钮的点击
运行程序:
-
此时因为worker对象属于当前主线程,因此它的槽函数是占主线程时间的,如果想让槽函数占子线程时间就可以使用moveToThread()将对象Worker移动到子线程中去
2.7 QThreadPool && QRunnable (线程池)
QThreadpool管理多个线程的集合。QThreadpool管理和回收单个QThread对象,以帮助降低使用线程的程序中的线程创建成本。
每个Qt应用程序都有一个全局QThreadpool对象,可以通过调用globallnstance()来访问。要使用QThreadpool线程之一,子类QRunnable并实现run()虚函数。然后创建该类的一个对象并将其传递给QThreadpool:start().
新建文件
编写代码
myrunnable.h
#ifndef MYRUNNABLE_H
#define MYRUNNABLE_H
#include <QRunnable>
/*** @brief The MyRunnable class* MyRunnable需要去继承QRunnable这个类,这个类就是去告诉线程池,我获取线程是要去做什么任务的*/
class MyRunnable : public QRunnable
{
public:MyRunnable();void run();
};#endif // MYRUNNABLE_H
myrunnable.cpp
#include "myrunnable.h"
#include <QDebug>
#include <QThread>MyRunnable::MyRunnable()
{}// 子线程执行业务逻辑的
void MyRunnable::run()
{qDebug() << "线程池的任务,执行业务逻辑 " <<QThread::currentThreadId();
}
mainwindow3.h
#ifndef MAINWINDOW3_H
#define MAINWINDOW3_H#include <QMainWindow>namespace Ui {
class MainWindow3;
}class MainWindow3 : public QMainWindow
{Q_OBJECTpublic:explicit MainWindow3(QWidget *parent = nullptr);~MainWindow3();private:Ui::MainWindow3 *ui;
};#endif // MAINWINDOW3_H
mainwindow3.cpp
#include "mainwindow3.h"
#include "ui_mainwindow3.h"
#include <QThreadPool>
#include "myrunnable.h"
#include <QDebug>MainWindow3::MainWindow3(QWidget *parent) :QMainWindow(parent),ui(new Ui::MainWindow3)
{ui->setupUi(this);qDebug() << "主线程开始执行:" << QThread::currentThreadId();// 线程的使用方式三: QThreadPool// 1 实例化QThreadPool实例化对象// 实例化QThreadPool方式有两种://QThreadPool *pool = new QThreadPool;// 方式二: qt中会提供一个全局的线程池对象QThreadPool *pool = QThreadPool::globalInstance();//pool->setMaxThreadCount();// 2 通过线程池执行任务MyRunnable *task = new MyRunnable;for(int i=0;i<10;i++){// 从线程池中拿一个线程出来执行任务pool->start(task);}qDebug() << "主线程执行结束:" << QThread::currentThreadId();
}MainWindow3::~MainWindow3()
{delete ui;
}
线程池对象创建方法:
- 方式一:new QThreadPool
- 方式二:QThreadPool::globalInstance(全局静态函数)
程序运行
说明一下:
- 为了更好的观察到现象,可以把任务数增多
- 此时就可以发现有相同的线程ID被重复使用了
2.8 [最简单]QtConcurrent(线程池)
通过QtConcurrent命名空间提供高级APl,可以在不使用互斥锁、读写锁、等待条件或信号量等低级线程原语的情况下编写多线程程序。使用QtConcurrent编写的程序会根据可用*处理器内核的数量自动调整所使用的线程数。
说简单点,就是我们使用QtConcurrent实现多线程时,可以不用考虑对共享数据的保护问题。而且,它可以根据CPU的能力,自动优化线程数。
和QThreadPool区别:
- QThreadPool自己计算和设置最佳的线程池个数,而QtConcurrent会自动去优化线程池个数
- QThreadPool是不能接收子线程返回结果的,但是QtConcurrent可以接收子线程执行结果的
使用时需要加入模板
```c++
QT += core gui concurrent
实际开发时,会遇到两种类型:
1. cpu密集型:做大量计算的,它的线程池个数=电脑核数
2. IO密集型:输入输出(数据库操作比较多的) ,线程池个数 = 核数*2
新建文件
代码编写
#include "mainwindow4.h"
#include "ui_mainwindow4.h"
#include <QDebug>
#include <QThread>
#include <QtConcurrent>QString fun1()
{qDebug() << "无参函数,线程执行任务" <<QThread::currentThreadId();return "fun1..............";
}QString fun2(QString name,int age)
{qDebug() << "无参函数,线程执行任务name=" << name <<" age=" <<age <<QThread::currentThreadId();return "fun2.................";
}MainWindow4::MainWindow4(QWidget *parent) :QMainWindow(parent),ui(new Ui::MainWindow4)
{ui->setupUi(this);qDebug() << "主线程开始执行:" << QThread::currentThreadId();// 线程使用方式四: QtConcurrent// 将fun1和fun2任务加入到线程池中QFuture<QString> f1 = QtConcurrent::run(fun1);QFuture<QString> f2 = QtConcurrent::run(fun2,QString("zhangsan"),30);// 执行任务,result()的返回值,就是fun1()和fun2()这两个函数所返回来的内容f1.result();f2.result();qDebug() << "主线程执行结束:" << QThread::currentThreadId();}MainWindow4::~MainWindow4()
{delete ui;
}
程序运行
通过观察发现它好像是同步的?
但其实是因为result执行线程任务,是需要等待并拿到返回结果的,所以看上去好像是同步执行
说明一下:
-
通过测试,上面的代码还是会在子线程任务之后执行,说明QtConcurrent还是异步操作
-
只是说如果在返回值之后做操作的话,必须要等返回值拿到结果以后才能执行
-
如果以后我的主线程耗费5s,子线程任务都分别要消耗5s
3.加锁
3.1 QMutex入门
- 这里创建了2个线程,一个线程对num循环5k次,由于没加锁导致结果不是1w
- 这里把锁加上就不会出现各个线程相互竞争的问题了
3.2 QMutexLocker自动解锁
- 因为上面的锁很容易忘记释放,忘记unlock,在逻辑复杂的情况下
- Qt中也有一个对互斥锁进行封装的类->QMutexLocker,类似与std::mutex智能指针
- C++11 也引入了std::lock_guard
3.3 其他补充说明
条件变量:QWaitCondition
信号量:QSemaphore
读写锁:QReadLocker、QWriteLocker、QReadWriteLock
-
这里就暂不介绍了,这里类和API用法和Linux中的类似,就是对系统函数/接口的封装,
-
要用到的时候,再查查文档