QT 学习笔记摘要(三)

发布于:2025-06-26 ⋅ 阅读:(17) ⋅ 点赞:(0)

第一节 事件

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中,主要的事件循环是由QCoreApplicationQGuiApplication提供的

 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_NAMESPACE
 
class MainWindow : public QMainWindow
{
    Q_OBJECT
 
public:
    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_OBJECT

public:
    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区别:

  1. QThreadPool自己计算和设置最佳的线程池个数,而QtConcurrent会自动去优化线程池个数
  2. 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中的类似,就是对系统函数/接口的封装,

  • 要用到的时候,再查查文档


网站公告

今日签到

点亮在社区的每一天
去签到