C++梁哥笔记day27 | 我的日常分享

C++梁哥笔记day27

Qt开发

一、布局管理器

所谓GUI界面,归根结底,就是一堆组件的叠加。我们创建一个窗口,把按钮放在上面,把图标放上面,这样就成了一个界面。在放置时,组件的位置尤其重要。我们必须要指定组件放在哪里,一遍窗口能够按照我们需要的方式进行渲染。这就涉及到组件定位的机制。

Qt提供了两种徐建定位机制:绝对定位和布局定位。

  • 绝对定位就是一种最原始的定位方法:给出这个组件的坐标和长宽值。这样Qt就知道该把组件放在哪里以及如何设置组件的大小。但是这样做带来的一个问题是,如果用户改变了窗口大小,比如点击最大化按钮或者使用鼠标拖动窗口边缘,采用绝对定位的组件是不会有任何响应的。这也很自然,因为你并没有告诉Qt,在窗口变化时,组件是否要更新自己以及如何更新。或者,还有更简单的方法:禁止用户改变窗口大小。但这总不是长远之计。
  • 布局定位:你只要把组件放入某一种布局,布局由专门的布局管理器进行管理。当需要调整大小或者位置的时候,Qt使用对应的布局管理器进行调整。

布局定位完美的解决了使用绝对定位的缺陷。

Qt提供的布局中以下三种使我们最常用的:

  • QHBoxLayout:按照水平方向从左到右布局
  • QVBoxLayout:按照竖直方向从上到下布局
  • QGridLayout:在一个网格中进行布局,类似HTML中css Grid布局,css内容可以根据阮一峰写的教程回顾一下

1.1 系统提供的布局控件

![截屏2021-02-03 下午8.14.46](截屏2021-02-03 下午8.14.46.png)

这四个为系统为我们提供的布局控件,但是使用起来不是非常灵活,后面有时间再试试。

1.2 利用Widget做布局

没有设置布局

Feb-03-2021 20-17-54

设置了布局

Feb-03-2021 20-28-20

给按钮绑定槽函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QPushButton>
#include <QDebug>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);

connect(ui->btn_login,&QPushButton::clicked,[=](){
//获取用户名
QString name = ui->name->text();
//获取密码
QString password = ui->pwd->text();
qDebug()<<"用户名:"<<name<<"密码:"<<password;
});
}

MainWindow::~MainWindow()
{
delete ui;
}

运行结果:

Feb-03-2021 20-38-54

二、常用控件

Qt为我们应用程序界面开发提供的一系列的控件,下面我们介绍两种最常用的一些控件,所有控件的使用方法我们都可以通过帮助文档获取。

2.1 单选框与组容器

对于单选框如果不添加组容器

Feb-04-2021 14-01-25

添加组容器

QQ20210204-140451 Feb-04-2021 14-05-32

2.2 listwidget列表控件

截屏2021-02-04 下午2.12.54
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QListWidgetItem>
#include <QListWidget>
#include <QDebug>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
//绑定信号槽
connect(ui->listWidget,&QListWidget::itemClicked,[=](QListWidgetItem *item){
qDebug()<<item->text();
});
}

MainWindow::~MainWindow()
{
delete ui;
}

运行结果:

Feb-04-2021 14-21-44

2.3 treewidget树控件

截屏2021-02-04 下午2.34.27 Feb-04-2021 14-40-18

案例:采用代码实现上面效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QTreeWidgetItem>
#include <QList>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);

QStringList list;
list<<"地区"<<"天气";
ui->treeWidget->setHeaderLabels(list);

//方式一
QTreeWidgetItem *item = new QTreeWidgetItem(QStringList()<<"湖北"<<"多云转晴");
QTreeWidgetItem *item1 = new QTreeWidgetItem(QStringList()<<"黄石"<<"多云");
item1->addChild(new QTreeWidgetItem(QStringList()<<"阳新"<<"多云"));
// item->addChild(item1);
QTreeWidgetItem *item2 = new QTreeWidgetItem(QStringList()<<"武汉"<<"晴");
item2->addChild(new QTreeWidgetItem(QStringList()<<"东西湖区"<<"晴"));
// item->addChild(item2);
ui->treeWidget->addTopLevelItem(item);

//方式二
QList<QTreeWidgetItem *> list1;
//list1.push_back(item1);
//list1.push_back(item2);
list1<<item1<<item2;
item->addChildren(list1);
ui->treeWidget->addTopLevelItem(item);



QTreeWidgetItem *item112 = new QTreeWidgetItem(QStringList()<<"浙江"<<"晴");
item112->addChild(new QTreeWidgetItem(QStringList()<<"温州"<<"多云转晴"));
ui->treeWidget->addTopLevelItem(item112);

}

MainWindow::~MainWindow()
{
delete ui;
}

运行结果:

截屏2021-02-04 下午3.16.09

2.4 tablewidget表格控件

截屏2021-02-04 下午3.30.59

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QDebug>
#include <QTableWidget>
#include <QTableWidgetItem>
#include <QVector>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);

ui->tableWidget->setColumnCount(3);
ui->tableWidget->setRowCount(4);

ui->tableWidget->setHorizontalHeaderLabels(QStringList()<<"C++成绩"<<"Java成绩"<<"Html成绩");
ui->tableWidget->setVerticalHeaderLabels(QStringList()<<"Bob"<<"Lucy"<<"Jack"<<"小花");
QVector<QStringList> list1;
list1.push_back(QStringList()<<"78"<<"98"<<"56");
list1.push_back(QStringList()<<"76"<<"57"<<"87");
list1.push_back(QStringList()<<"84"<<"86"<<"94");
list1.push_back(QStringList()<<"74"<<"83"<<"65");
for(int i=0;i<4;i++){
for(int j=0;j<3;j++){
ui->tableWidget->setItem(i,j,new QTableWidgetItem(list1.at(i).at(j)));
}
}
}

MainWindow::~MainWindow()
{
delete ui;
}

运行结果:

截屏2021-02-05 下午2.53.50

2.5 combo box下拉列表

Feb-05-2021 15-02-22

代码实现:信号槽绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);

ui->comboBox->addItem(QIcon("/Users/jiaxiaoyu/Downloads/mmexport1611730292701.jpg"),QString("狗头"));
connect(ui->comboBox,&QComboBox::activated,[=](int idx){
qDebug()<<ui->comboBox->itemText(idx);
});
}

MainWindow::~MainWindow()
{
delete ui;
}

运行结果:

Feb-05-2021 15-17-30

2.6 QLabel控件的使用

QLabel控件是我们最常用的控件之一,其功能很强大,我们可以用来显示文本、图片和动画等。

2.6.1 显示文字(普通文本、html)

通过QLabel类的setText函数设置显示的内容

1
void setText(const QString &);
  • 可以显示普通文本字符串

    1
    2
    QLabel *label = new QLabel;
    label->setText("Hello World!");
  • 可以显示HTML格式的字符串,比如显示一个链接

    1
    2
    3
    QLabel *label = new QLabel(this);
    label->setText("<h1><a href=\"https://www.baidu.com"\></h1>");
    label->setOpenExternalLinks(true);

    其中setOpenExternallinks函数是用来设置用户点击链接之后是否自动打开链接,如果参数指定为true则会自动打开。

2.6.2 显示图片

可以使用QLabel的成员函数setPixmap设置图片

1
void setPixmap(const QPixmap &);

首先定义QPixmap对象

1
QPixmap pixmap;

然后加载图片

1
pixmap.load(":/images/1.png");

最后将图片设置到QLabel中

1
2
QLabel *label = new QLabel;
label->setPixmap(pixmap);

案例:

1
2
3
4
QPixmap pixmap;
pixmap.load("/Users/jiaxiaoyu/Downloads/qidai.jpg");
ui->label->setPixmap(pixmap);
ui->label->resize(360,360);

运行结果:

截屏2021-02-06 下午12.57.24

2.6.3 显示动画

可以使用QLabel的成员函数setMovie加载动画,可以播放gif格式的文件

1
void setMovie(QMovie *movie);

首先定义QMovie对象,并初始化

1
QMovie *movie = new QMovie(":/1.gif");

播放加载的动画

1
movie->start();

将动画设置到QLabel中

1
2
QLabel *label = new QLabel;
label.setMovie(movie);

案例:

1
2
3
4
QMovie *movie = new QMovie("/Users/jiaxiaoyu/Downloads/goutou.jpg");
ui->label->setMovie(movie);
ui->label->resize(360,360);
movie->start();

运行结果:

Feb-06-2021 13-08-09

2.7 QLineEdit

Qt提供的单行文本编辑框

2.7.1 设置/获取内容

  • 获取编辑框的内容,text()

    1
    QString text() const
  • 设置编辑框内容

    1
    void setText(const QString &);

2.7.2 设置显示模式

使用QLineEdit类的setEchoMode()函数设置文本的显示模式

1
void setEchoMode(EchoMode mode);

EchoMode 是一个枚举类型,一共定义了四种显示模式:

  • QLineEdit::Normal 正常显示模式,按照输入的内容显示
  • QLineEdit::NoEcho 不显示任何内容,此模式下无法看到用户的输入(只是看不见,不是不能输入)
  • QLineEdit::Password 密码模式,输入的字符会根据平台转换为特殊字符。*、●等。
  • QLineEdit::PasswordEchoOnEdit 编辑时输入字符显示输入内容,否则用小黑点代替

可以使用setTextMargins函数指定显示文本与输入框上下左右边界间隔的像素数。

1
void setTextMargins(int left,int top,int right,int bottom);

2.8 其他控件

Qt中的控件的使用方法可以参考Qt提供的帮助文档。

2.9 自定义控件

在搭建Qt窗口界面的时候,在一个项目中很多窗口或者是窗口中的某个模块会被经常性的重复使用。一般遇到这种情况我们会将这个窗口或者模块拿出来,做成一个独立的窗口类,以备以后重复使用。

在使用Qt的ui文件搭建界面的时候,工具栏中只为我们提供了标准的窗口控件,如果我们想使用自定义控件怎么办?

例如:我们从QWidget派生出一个类SmallWidget,实现了一个自定义窗口

不是创造一个新的控件,而是使用已有的控件,拼凑出一个控件。

  1. 定义一个自定义控件

    截屏2021-02-05 下午3.37.08 截屏2021-02-05 下午3.37.48 截屏2021-02-05 下午3.39.46 截屏2021-02-05 下午3.40.43
  2. 给自己创建的ui添加控件

    截屏2021-02-06 下午3.14.23
  3. 在其他ui文件中使用自己创建的ui控件

    在其他ui文件中添加一个容器,右键点击提升为

    QQ20210206-151832
QQ20210206-152131 QQ20210206-152212

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include "mywidget.h"
#include "ui_mywidget.h"

mywidget::mywidget(QWidget *parent) :
QWidget(parent),
ui(new Ui::mywidget)
{
ui->setupUi(this);
connect(ui->horizontalSlider,&QSlider::valueChanged,ui->spinBox,&QSpinBox::setValue);
connect(ui->spinBox,&QSpinBox::valueChanged,ui->horizontalSlider,&QSlider::setValue);
}

mywidget::~mywidget()
{
delete ui;
}

运行结果:

Feb-06-2021 15-34-32

2.9.1 自定义控件提供外部接口

![截屏2021-02-06 下午3.35.40](截屏2021-02-06 下午3.35.40.png)

为什么要提供外部接口;下面看一个需求,我们在widget.ui定义了两个按钮,如图,截屏2021-02-06 下午3.41.11将进度条分设置为一半和获取进度条的值,但是在widge.cpp中是获取不到我们定义的horizontalSlider和slideBox的。这时候就需要在mywidget.h中声明外部接口,在mywidget中定义。

在mywidget.h的类中声明

getValue和setValue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#ifndef MYWIDGET_H
#define MYWIDGET_H

#include <QWidget>

namespace Ui {
class mywidget;
}

class mywidget : public QWidget
{
Q_OBJECT

public:
explicit mywidget(QWidget *parent = nullptr);
~mywidget();
int getValue(void);
void setValue(int val);

private:
Ui::mywidget *ui;
};

#endif // MYWIDGET_H

在mywidget.cpp中定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include "mywidget.h"
#include "ui_mywidget.h"

mywidget::mywidget(QWidget *parent) :
QWidget(parent),
ui(new Ui::mywidget)
{
ui->setupUi(this);
connect(ui->horizontalSlider,&QSlider::valueChanged,ui->spinBox,&QSpinBox::setValue);
connect(ui->spinBox,&QSpinBox::valueChanged,ui->horizontalSlider,&QSlider::setValue);
}

mywidget::~mywidget()
{
delete ui;
}

int mywidget::getValue()
{
return ui->horizontalSlider->value();
}

void mywidget::setValue(int val)
{
ui->horizontalSlider->setValue(val);
}

在widget.cpp中调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include "widget.h"
#include "ui_widget.h"
#include "mywidget.h"
#include <QDebug>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);

//connect(ui->pushButton,&QPushButton::clicked,ui->widget,&mywidget::getValue);
connect(ui->pushButton,&QPushButton::clicked,[=](){
qDebug()<<ui->widget->getValue();
});
connect(ui->pushButton_2,&QPushButton::clicked,[=](){
ui->widget->setValue(50);
});

}

Widget::~Widget()
{
delete ui;
}

运行结果:

Feb-06-2021 16-00-49

其实也可以将接口定义为槽函数,其实定义为成员函数也可以,反正还是可以通过函数地址进行调用,反正与C++是融汇贯通的,怎么做都可以。


三、Qt消息机制和事件

3.1 事件

事件(event)是由系统或者Qt本身在不同的时刻发出的。当用户按下鼠标、敲下键盘,或者是窗口需要重新绘制的时候,都会发出一个相应的事件。一些事件在对用户操作做出相应时发出,如键盘事件等;另一些事件则是由系统自动发出,如计时器事件。

在前面我们也曾今简单提到,Qt程序需要在main()函数创建一个QApplication对象,然后调用它的exec()函数。这个函数就是开始Qt的事件循环。在执行exec()函数之后,程序将进入事件循环来监听应用程序的事件。当事件发生时,Qt将创建一个事件对象,Qt中所有事件类都继承与QEvent。当事件对象创建完毕后,Qt将这个事件对象传递给QObject的event()函数。event()函数并不直接处理事件,而是按照事件对象的类型分派给特定的事件处理函数(event handler),关于这一点,会在后面详细说明。

在所有组件的父类QWidget中,定义了很多事件处理的回调函数,如:

  • keyPressEvent()
  • keyReleaseEvent()
  • mouseDoubleClickEvent()
  • mouseMoveEvent()
  • mousePressEvent()
  • mouseReleaseEvent()

这些函数都是protected virtual的,也就是说,我们可以在子类中重新实现这些函数。

下面来看一个例子:

定义一个label控件,设置了边框,重写label控件的鼠标移入和移出事件。但是label是继承QLabel的,这是系统提供的类,我们是无法修改的,所以我们可以定义一个自己的mylabel类,将label控件提升为mylabel,然后在mylabel中重新事件方法。

截屏2021-02-06 下午4.14.37

上面图片中 Base Class 没有QLabel,这时候我们随便选一个,然后在生成的.h和.cpp文件中手动修改为QLabel就可以了。

修改好的.h和.cpp:

mylabel.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#ifndef MYLABEL_H
#define MYLABEL_H

#include <QLabel>
#include <QWidget>
class myLabel : public QLabel
{
Q_OBJECT
public:
explicit myLabel(QWidget *parent = nullptr);

signals:

};

#endif // MYLABEL_H

mylabel.cpp

1
2
3
4
5
6
#include "mylabel.h"

myLabel::myLabel(QWidget *parent) : QLabel(parent)
{

}

下一步对label控件提升:

截屏2021-02-06 下午4.19.57

总结:如果想重写某个类的事件,一般情况,就要自定义一个类,继承于该控件的类型,然后将控件的类,提升为自定义的类,这样我们就可以在自定义类中重写控件类型的事件函数。

下一步进行重写事件:

mylabel.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#ifndef MYLABEL_H
#define MYLABEL_H

#include <QLabel>
#include <QWidget>
class myLabel : public QLabel
{
Q_OBJECT
public:
explicit myLabel(QWidget *parent = nullptr);
//重写鼠标移出事件
virtual void leaveEvent(QEvent *event);
//重写鼠标移入事件
virtual void enterEvent(QEnterEvent *event);



signals:

};

#endif // MYLABEL_H

mylabel.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include "mylabel.h"
#include <QDebug>
myLabel::myLabel(QWidget *parent) : QLabel(parent)
{

}

void myLabel::leaveEvent(QEvent *event)
{
qDebug()<<"鼠标移出了"<<Qt::endl;
}

void myLabel::enterEvent(QEnterEvent *event)
{
qDebug()<<"鼠标移入了"<<Qt::endl;
}

运行结果:

Feb-06-2021 16-56-19

鼠标按下 移动

ev 坐标 左右键

截屏2021-02-06 下午5.09.23

3.2 事件分发器event()

事件对象创建完毕后,Qt将这个事件对象传递给QObject的event()函数。event()函数并不直接处理事件,而是将这些事件对象按照它们不同的类型,分发给不同的事件处理器(event handler)。

如上所述,event()函数主要用于事件的分发。所以如果你希望在事件分发之间做一些操作,就可以重写这个event()函数了。例如,我们希望在QWidget组件总监听tab键的按下,那么就可以继承QWidget,并重写它的event()函数,来达到目的。

截屏2021-02-06 下午5.19.31
1
2
3
4
5
6
7
8
9
bool CustomWidget::event(QEvent *c){
if(e->type() = QEvent::KeyPress){
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(c);
if(keyEvent->key() == Qt::Key_Tab){
qDebug()<<"按下了tab键";
return true;
}
}
}

CustomWidget是一个普通的QWidget子类。我们重写了它的event()函数,这个函数有一个QEvent对象作为参数,也就是需要转发的事件对象。函数返回值是bool类型。

  • 如果传入的事件已被识别并且处理,则需要返回true,否则返回false。如果返回值是true,那么Qt会认为这个事件已经处理反比,不会再将这个事件发送给其它对象,而是会继续处理事件队列中的下一个事件。
  • 在event()函数中,调用事件对象的accept()和ignore()函数是没有作用的,不会影响到事件的传播。

我们可以通过使用QEvent::type()函数可以检查事件的实际类型,其返回值是QEvent::Type类型的枚举。我们处理过自己感兴趣的事件之后,可以直接返回true,表示我们已经对此事件进行了处理;对于其他我们不关心的事件,则需要调用父类的event()函数继续转发,否则这个组件就只能处理我们定义的事件了、

案例:

mylabel.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include "mylabel.h"
#include <QDebug>
#include <QEvent>
myLabel::myLabel(QWidget *parent) : QLabel(parent)
{

}

void myLabel::leaveEvent(QEvent *event)
{
qDebug()<<"鼠标移出了"<<Qt::endl;
}

void myLabel::enterEvent(QEnterEvent *event)
{
qDebug()<<"鼠标移入了"<<Qt::endl;
}

bool myLabel::event(QEvent *e)
{
//我们只关心鼠标移入事件
if(e->type() == QEvent::Enter){
qDebug()<<"事件分发器 鼠标移入了";
return true;//返回true表示当前事件已经处理了,不需要再往下分了
}
return QLabel::event(e);//调用父类QLabel的event处理其他的事件
}

mylabel.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#ifndef MYLABEL_H
#define MYLABEL_H

#include <QLabel>
#include <QWidget>
class myLabel : public QLabel
{
Q_OBJECT
public:
explicit myLabel(QWidget *parent = nullptr);
//重写鼠标移出事件
virtual void leaveEvent(QEvent *event);
//重写鼠标移入事件
virtual void enterEvent(QEnterEvent *event);
//重写事件分发器
virtual bool event(QEvent *e) override;

signals:

};

#endif // MYLABEL_H

运行结果:

Feb-08-2021 16-02-00

3.3 事件过滤器

有时候,对象需要查看、甚至要拦截发送到另外对象的事件。例如,对话框可能想要拦截按键事件,不让别的组件收到;或者要修改回车键的默认处理。

通过前面的内容,我们已经知道,Qt创建了QEvent事件对象之后,会调用QObject的event()函数处理事件的分发。显然,我们可以在event()函数中实现拦截的操作由于event()函数是protected的,因此,需要继承已有类。如果组件很多,就需要重写很多个event()函数。这相当麻烦,更不用说重写event()函数还得小心一堆问题。好在Qt提供了另外一种机制来达到这一目的:事件过滤器。

截屏2021-02-06 下午5.49.22

QObject有一个eventFilter()函数,用于建立事件过滤器。函数原型如下:

1
virtual bool QObject::eventFilter(QObject *watched,QEvent *event);

这个函数正如其名字显示的那样,是一个”事件过滤器“。所谓事件过滤器,可以理解成一种过滤代码。事件过滤器会检查接收到的事件。如果这个事件是我们关心的类型,就进行我们自己的处理;如果不是,就继续转发。这个函数返回一个bool类型,如果你想要将参数event过滤出去,比如,不想让他继续转发,就返回true,否则返回false。事件过滤器的调用时间是目标对象(也就是参数里面的watched对象)接收到事件对象之前。也就是说,如果你在事件过滤器中停止了某个事件,那么watched对象以及以后所有的事件过滤器根本不会知道这么一个事件了。

步骤:

  1. 加载事件过滤器 在构造函数中加载installEventFilter
  2. 重写事件过滤器

案例:

mylabel.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include "mylabel.h"
#include <QDebug>
#include <QEvent>
myLabel::myLabel(QWidget *parent) : QLabel(parent)
{
//1、加载事件过滤器
this->installEventFilter(this);
/*
QObject::installEventFilter(this);//直接使用QObject中的函数
QObject().installEventFilter(this);//使用匿名对象
(new QObject())->installEventFilter(this);//使用匿名对象
*/

}

void myLabel::leaveEvent(QEvent *event)
{
qDebug()<<"鼠标移出了"<<Qt::endl;
}

void myLabel::enterEvent(QEnterEvent *event)
{
qDebug()<<"鼠标移入了"<<Qt::endl;
}

bool myLabel::event(QEvent *e)
{
//我们只关心鼠标移入事件
if(e->type() == QEvent::Enter){
qDebug()<<"事件分发器 鼠标移入了";
return true;//返回true表示当前事件已经处理了,不需要再往下分了
}
return QLabel::event(e);//调用父类QLabel的event处理其他的事件
}

//watched:事件发生的控件 event:控件产生的具体事件
bool myLabel::eventFilter(QObject *watched, QEvent *event)
{
//判断是不是当前控件发生的事件
if(watched == this){
//判断事件类型是不是鼠标移入事件
if(event->type() == QEvent::Enter){
qDebug()<<"事件过滤器 鼠标移入了";
return true;//返回true表示当前事件已经处理了,不需要再往下分了
}
}
//对于其他控件的其他事件交给父类的事件过滤器处理
return QLabel::eventFilter(watched,event);
}

mylabel.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#ifndef MYLABEL_H
#define MYLABEL_H

#include <QLabel>
#include <QWidget>
class myLabel : public QLabel
{
Q_OBJECT
public:
explicit myLabel(QWidget *parent = nullptr);
//重写鼠标移出事件
virtual void leaveEvent(QEvent *event) override;
//重写鼠标移入事件
virtual void enterEvent(QEnterEvent *event) override;
//重写事件分发器
virtual bool event(QEvent *e) override;
//重写事件过滤器
virtual bool eventFilter(QObject *watched, QEvent *event) override;


signals:

};

#endif // MYLABEL_H

mainwindow.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
Q_OBJECT

public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();

private:
Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H

mainwindow.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
}

MainWindow::~MainWindow()
{
delete ui;
}

main.cpp

1
2
3
4
5
6
7
8
9
10
11
#include "mainwindow.h"

#include <QApplication>

int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}

运行结果:

Feb-10-2021 13-11-45

事件过滤器的强大之处在于,我们可以为整个应用程序添加一个事件过滤器。记得,installEventFilter()函数是QObject的函数,QApplication或者QCoreApplication对象都是QObject的子类,因此,我们可以向QApplication或者QCoreApplication添加事件过滤器。这种全局的事件过滤器将会在所有其他特征对象的事件过滤器之前调用。尽管很强大,但这种行为会严重降低整个应用程序的事件分发效率。因此,除非是不得不使用的情况为,否则我们不应该这么做、

注意:

事件过滤器和按照事件过滤器的组件必须在同一个线程,否则,过滤器将不起作用。另外如果在按照过滤器之后,这两个组件到了不同的线程。那么,只用等到两者重新回到同一线程的时候过滤器才会有效。

3.4 总结

Qt的事件是整个Qt框架的核心机制之一,也比较复杂。说它复杂,更多是因为它涉及到的函数众多,而处理方法也很多,有时候让人难以选择。

Qt中有很多种事件:鼠标事件、键盘事件、大小改变事件、位置移动的事件等等。如何处理这些事件,实际有两种选择:

  1. 所有事件对应一个事件处理函数,在这个处理函数中用一个喊打的分支语句进行选择。其代表作就是win32 API的WndProc()函数:

    1
    2
    3
    4
    LRESULT CALLBACK WndProc(HWND hWnd,
    UINT message,
    WPARRAM wParam,
    LPARAM lParam)

    在这个函数中,我们需要使用switch语句,选择message参数的类型进行处理,典型代码是:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    switch(message){
    case WM_PAINT:
    //...
    break;
    case WM_DESTROY:
    //...
    break;
    //....
    }
  2. 每一种事件对应一个事件处理函数。Qt就是使用这么一种机制:

    • mouseEvent()
    • keyPressEvent()
    • ……

Qt具有这么多种事件处理函数,肯定有一个地方对其进行分发,否则,Qt怎么知道哪一种事件调用哪一个事件处理函数呢?这个分发的函数就是event()。显然,当QMouseEvent产生之后,event()函数将其分发给mouseEvent()事件处理器进行处理。

event()函数会有两个问题:

  • event()函数是一个protected函数,这就意味着我们要想重写event(),必须继承一个已有的类。试想,我的程序根本不想到鼠标事件,程序中所有组件都不允许处理鼠标事件,是不是我们得继承所有组件,一一重写其event()函数?protected 函数带来的另外一个问题是,如果我们基于第三方库进行开发,而对方没有提供源代码,只有一个链接库,其它都是封装好的。我们怎么去继承这种库中的组件呢?
  • event()函数的确有一定的控制,不过有时候我们的需求更严格一些,我们希望那些组件根本看不到这种事件。event()函数虽然可以拦截,但其实也是接收到了QMouseEvent对象。如果我们要让它连收都收不到,怎么做呢?这样做的好处是,模拟一种系统根本没有那个事件的效果,所以其它组件根本不会收到这个事件,也就无需修改自己的事件处理函数。这种需求怎么办呢?

这两个问题是event()函数无法处理的。于是,Qt提供了另外一种解决方案:事件过滤器。事件过滤器给我们一种能力,让我们能够完全移出某种事件。事件过滤器可以安装到任意QObject类型上面,并且可以安装多个。如果要实现全局的事件过滤器,则可以安装到QApplication或者QCoreApplication上面。这里需要注意的是,如果使用installEventFilter()函数给一个对象安装事件过滤器,那么该事件过滤器只对该对象有效,只有这个对象的事件需要先传递给事件过滤器的eventFilter()函数进行过滤,其它对象不受影响。如果给QApplication对象安装事件过滤器的话,那么该事件过滤器对程序中的每一个对象都有效,任何对象的事件都是先传给eventFilter()函数。

事件过滤器可以解决刚刚我们提出的event()函数的两点不足:

  1. 首先,事件过滤器不是protected的,因此我们可以向任何QObject子类安装事件过滤器
  2. 其次,事件过滤器在目标对象接收到事件之前进行处理,如果我们将事件过滤掉,目标对象根本不会见到这个事件。

事实上,还有一种方法,我们没有介绍。Qt事件的调用最终个都会追溯到QCoreApplication::notify()函数,因此,最大的控制权实际上是重写QCoreApplication::notify()函数。

1
virtual bool QCoreApplication::notify(QObject *receiver,QEvent *event);

该函数会将event发送给receiver,也就是调用receiver->event(event),其返回值就是来自receiver的事件处理器。注意,这个函数为任意线程的任意对象的任意事件调用,因此,它不存在事件过滤器的线程的问题。不过我们并不推荐这么做,因为notify()函数只有一个,而事件过滤器要灵活得多。

现在我们可以总结一下Qt的事件处理,实际上是有五个层次:

  • 重写painEvent()、mousePressEvent()等事件处理函数。这是最普通、最简单的形式,同时功能也最简单。
  • 重写event()函数。event()函数是所有对象的事件入口,QObject和QWidget中的实现,默认是把事件传递给特定的事件处理函数。
  • 在特定对象上面安装事件过滤器。该过滤器仅过滤该对象接收到的事件。
  • 在QCoreApplication::instance()上面安装事件过滤器。该过滤器将过滤所有事件,因此和notify()函数一样强大,但是它更灵活,因为可以安装多个事件过滤器。全局的事件过滤器可以看到disabled组件上面发出的鼠标事件。全局过滤器有一个问题:只能用在主线程。
  • 重写QCoreApplication::notify()函数。这是最强大的,和全局事件过滤器一样提供完全控制,并且不受线程的限制。但是全局范围内只能有一个被使用(因为QCoreApplication是单例的)。

四、定时器QTimer

定时器的触发方式有三种:

  1. 定时器事件触发

    1
    2
    virtual void timerEvent(QTimerEvent *e);
    int startTimer(int interval,Qt::TimerType timerType = Qt::CoarseTimer);
  2. 定时器对象触发

    创建定时器对象,调用对象成员方法。

    1
    2
    3
    void start(int msec);
    void start();
    void stop();
  1. 静态成员函数触发

    singleShot信号触发。实现延时功能(或者倒计时)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /*Static Public Members*/
    void singleShot(int msec, const QObject *receiver, const char *member)
    void singleShot(int msec, Qt::TimerType timerType, const QObject *receiver, const char *member)
    void singleShot(int msec, const QObject *receiver, PointerToMemberFunction method)
    void singleShot(int msec, Qt::TimerType timerType, const QObject *receiver, PointerToMemberFunction method)
    void singleShot(int msec, Functor functor)
    void singleShot(int msec, Qt::TimerType timerType, Functor functor)
    void singleShot(int msec, const QObject *context, Functor functor)
    void singleShot(int msec, Qt::TimerType timerType, const QObject *context, Functor functor)
    void singleShot(std::chrono::milliseconds msec, const QObject *receiver, const char *member)
    void singleShot(std::chrono::milliseconds msec, Qt::TimerType timerType, const QObject *receiver, const char *member)

4.1 案例:定时器事件触发

widget.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QTimer>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE

class Widget : public QWidget
{
Q_OBJECT

public:
Widget(QWidget *parent = nullptr);
~Widget();
//重写timerEvent函数
virtual void timerEvent(QTimerEvent *e) override;

private:
Ui::Widget *ui;
};
#endif // WIDGET_H

widget.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include "widget.h"
#include "ui_widget.h"

Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
//启动定时器 每1000ms执行一次
this->startTimer(1000);
}

Widget::~Widget()
{
delete ui;
}

void Widget::timerEvent(QTimerEvent *e)
{
static int num = 0;
ui->label->setText(QString::number(num++));

}

运行结果:

Feb-10-2021 14-18-05

4.2 案例:触发多个定时器

startTimer()返回值是定时器id

截屏2021-02-10 下午2.19.39

可以利用timerId()判断获取到定时器id,从而判断是哪一个定时器

widget.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>

QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE

class Widget : public QWidget
{
Q_OBJECT

public:
Widget(QWidget *parent = nullptr);
~Widget();
virtual void timerEvent(QTimerEvent *e);

private:
Ui::Widget *ui;
//定义两个变量存放定时器id
int id1;
int id2;
};
#endif // WIDGET_H

widget.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include "widget.h"
#include "ui_widget.h"

Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
//启动两个定时器
this->id1 = this->startTimer(1000);
this->id2 = this->startTimer(3000);

}

Widget::~Widget()
{
delete ui;
}

void Widget::timerEvent(QTimerEvent *e){
static int num1=1;
static int num2=1;
//判断定时器
if(e->timerId() == this->id1){
ui->label->setText(QString::number(num1++));
}
//不要使用if else_if 虽然好像运行起来没问题
//不知道为啥 我总觉得如果同时的话id1执行了 id2就不会执行
if(e->timerId() == this->id2){
ui->label_2->setText(QString::number(num2++));
}

}

运行结果:

Feb-10-2021 14-45-20

4.3 案例:定时器对象触发

widget.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include "widget.h"
#include "ui_widget.h"
#include <QTimer>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);

QTimer *timer = new QTimer(this);
connect(timer,&QTimer::timeout,[=](){
static int num = 1;
ui->label->setText(QString::number(num++));
});

//给两个按钮绑定槽信号
connect(ui->pushButton,&QPushButton::clicked,[=](){
//开始
timer->start(100);
});
connect(ui->pushButton_2,&QPushButton::clicked,[=](){
//暂停
timer->stop();
});
}

Widget::~Widget()
{
delete ui;
}

运行结果:

Feb-10-2021 14-54-06

4.4 案例:静态成员函数触发

倒计时显示

widget.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include "widget.h"
#include "ui_widget.h"
#include <QTimer>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
QTimer::singleShot(3000,[=]{
ui->label->setText("憨批 憨批 憨批");
});

}

Widget::~Widget()
{
delete ui;
}

运行结果:

Feb-10-2021 15-02-00