Qt学习笔记


注意:本网站所有内容为自己学习期间记录的笔记,很多都是原作者那边摘抄过来的,不作商业用途也不插播广告。(如有侵权,请联系我删除,15909440083)

如果有不理解的推荐去看他们的视频,讲的特别详细。参考:

零基础入门六天学会QT完整版
爱编程的大丙

1 Qt概述

1.1 什么是Qt

Qt是一个跨平台的C++图形用户界面应用程序框架。它为应用程序开发者提供建立艺术级图形界面所需的所有功能。它是完全面向对象的,很容易扩展,并且允许真正的组件编程。

1.2 Qt的发展史

1991年 Qt最早由奇趣科技开发

1996年 进入商业领域,它也是目前流行的Linux桌面环境KDE的基础

2008年 奇趣科技被诺基亚公司收购,Qt称为诺基亚旗下的编程语言

2012年 Qt又被Digia公司收购

2014年4月 跨平台的集成开发环境Qt Creator3.1.0发布,同年5月20日配发了Qt5.3正式版,至此Qt实现了对iOS、Android、WP等各平台的全面支持。

当前Qt最新版本为 5.9.0

1.3 支持的平台

  • Windows – XP、Vista、Win7、Win8、Win2008、Win10
  • Uinux/X11 – Linux、Sun Solaris、HP-UX、Compaq Tru64 UNIX、IBM AIX、SGI IRIX、FreeBSD、BSD/OS、和其他很多X11平台
  • Macintosh – Mac OS X
  • Embedded – 有帧缓冲支持的嵌入式Linux平台,Windows CE

1.4 Qt版本

Qt按照不同的版本发行,分为商业版和开源版

  • 商业版
  • 为商业软件提供开发,他们提供传统商业软件发行版,并且提供在商业有效期内的免费升级和技术支持服务。
  • 开源的LGPL版本:
  • 为了开发自有而设计的开放源码软件,它提供了和商业版本同样的功能,在GNU通用公共许可下,它是免费的。

1.5 Qt的下载与安装

下载地址:

Linux Host

OS X Host

Windows Host

安装

默认安装(建议组件全部选中)

· Qt对不同的平台提供了不同版本的安装包,可根据实际情况自行下载安装,本文档使用qt-opensource-windows-x86-mingw482_opengl-5.3.1 版本进行讲解

1.6 Qt的优点

  • 跨平台,几乎支持所有的平台
  • 接口简单,容易上手,学习QT框架对学习其他框架有参考意义。
  • 一定程度上简化了内存回收机制
  • 开发效率高,能够快速的构建应用程序。
  • 有很好的社区氛围,市场份额在缓慢上升。
  • 可以进行嵌入式开发。

img

1.7 成功案例

  • Linux桌面环境KDE
  • WPS Office 办公软件
  • Skype 网络电话
  • Google Earth 谷歌地图
  • VLC多媒体播放器
  • VirtualBox虚拟机软件

2 创建Qt项目

2.1 使用向导创建

打开Qt Creator 界面选择 New Project或者选择菜单栏 【文件】-【新建文件或项目】菜单项

img

弹出New Project对话框,选择Qt Widgets Application,

img

img

设置项目名称和路径,按照向导进行下一步,

img

选择编译套件

img

向导会默认添加一个继承自CMainWindow的类,可以在此修改类的名字和基类。默认的基类有QMainWindow(子类)、QWidget(父类)以及QDialog(子类-对话框)三个,我们可以选择QWidget(类似于空窗口),这里我们可以先创建一个不带UI的界面,继续下一步

img

系统会默认给我们添加main.cpp、mywidget.cpp、 mywidget.h和一个.pro项目文件,点击完成,即可创建出一个Qt桌面程序。

2.2 手动创建

添加一个空项目

img

选择【choose】进行下一步。设置项目名称和路径 —> 选择编译套件 –> 修改类信息 –> 完成(步骤同上),生成一个空项目。在空项目中添加文件:在项目名称上单击鼠标右键弹出右键菜单,选择【添加新文件】

img

img

在此对话框中选择要添加的类或者文件,根据向导完成文件的添加。

2.3 .pro文件

在使用Qt向导生成的应用程序.pro文件格式如下:

QT       += core gui  //包含的模块
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets 	//大于Qt4版本 才包含widget模块
TARGET = QtFirst  //应用程序名  生成的.exe程序名称
TEMPLATE = app    //模板类型    应用程序模板
SOURCES += main.cpp\   //源文件
        mywidget.cpp
HEADERS  += mywidget.h   //头文件

.pro就是工程文件(project),它是qmake自动生成的用于生产makefile的配置文件。.pro文件的写法如下:

  • 注释

    • 从“#”开始,到这一行结束。
  • 模板变量告诉qmake为这个应用程序生成哪种makefile。下面是可供使用的选择:TEMPLATE = app

    • app -建立一个应用程序的makefile。这是默认值,所以如果模板没有被指定,这个将被使用。
    • lib - 建立一个库的makefile。
    • vcapp - 建立一个应用程序的VisualStudio项目文件。
    • vclib - 建立一个库的VisualStudio项目文件。
    • subdirs -这是一个特殊的模板,它可以创建一个能够进入特定目录并且为一个项目文件生成makefile并且为它调用make的makefile。
  • #指定生成的应用程序名:

    • TARGET = QtDemo
  • #工程中包含的头文件

    • HEADERS += include/painter.h
  • #工程中包含的.ui设计文件

    • FORMS += forms/painter.ui
  • #工程中包含的源文件

    • SOURCES += sources/main.cpp sources
  • #工程中包含的资源文件

    • RESOURCES += qrc/painter.qrc
  • greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

    • 这条语句的含义是,如果QT_MAJOR_VERSION大于4(也就是当前使用的Qt5及更高版本)需要增加widgets模块。如果项目仅需支持Qt5,也可以直接添加“QT += widgets”一句。不过为了保持代码兼容,最好还是按照QtCreator生成的语句编写。
  • #配置信息

    • CONFIG用来告诉qmake关于应用程序的配置信息。
    • CONFIG += c++11 //使用c++11的特性

在这里使用“+=”,是因为我们添加我们的配置选项到任何一个已经存在中。这样做比使用“=”那样替换已经指定的所有选项更安全。

2.4 一个最简单的Qt应用程序

main入口函数中

#include "widget.h"
#include <QApplication>     // QApplication应用程序类

//程序入口  argc命令行变量数量   *argv[]命令行变量数组
int main(int argc, char *argv[]){
    //a 应用程序对象   在Qt中 应用程序对象 有且仅有一个
    QApplication a(argc, argv);
    //通过  窗口类  实例化对象   w
    Widget w;
    //窗口是不会默认弹出的,需要调用show方法进行显示
    w.show();
    //a.exec(); 进入消息循环机制   阻塞功能
    return a.exec();
//    while (1){        //等价于这个
//        if(点击叉子){
//            break;
//        }
//    }
}

解释:

  • Qt系统提供的标准类名声明头文件没有.h后缀

  • Qt一个类对应一个头文件,类名就是头文件名

  • QApplication应用程序类

    • 管理图形用户界面应用程序的控制流和主要设置。
    • 是Qt的整个后台管理的命脉它包含主事件循环,在其中来自窗口系统和其它资源的所有事件处理和调度。它也处理应用程序的初始化和结束,并且提供对话管理
    • 对于任何一个使用Qt的图形用户界面应用程序,都正好存在一个QApplication 对象,而不论这个应用程序在同一时间内是不是有0、1、2或更多个窗口。
  • a.exec()

    • 程序进入消息循环,等待对用户输入进行响应。这里main()把控制权转交给Qt,Qt完成事件处理工作,当应用程序退出的时候exec()的值就会返回。在exec()中,Qt接受并处理用户和系统的事件并且把它们传递给适当的窗口部件。

初始创建的mywidget.h文件

#ifndef MYWIDGET_H
#define MYWIDGET_H

#include <QWidget>

class myWidget : public QWidget{
    Q_OBJECT	//Q_OBJECT宏  支持信号和槽
public:
    myWidget(QWidget *parent = 0);
    ~myWidget();
};

#endif // MYWIDGET_H

初始的mywidget.cpp

#include "mywidget.h"

myWidget::myWidget(QWidget *parent)
    : QWidget(parent)
{
}

myWidget::~myWidget()
{

}

3 第一个Qt小程序

3.1 按钮的创建

在Qt程序中,最常用的控件之一就是按钮了,首先我们来看下如何创建一个按钮,加在mywidget.cpp文件中

QPushButton * btn = new QPushButton;   //头文件 #include <QPushButton>
//设置父亲
btn->setParent(this);
//设置文字
btn->setText("德玛西亚");
//移动位置
btn->move(100,100);

//第二种创建
QPushButton * btn2 = new QPushButton("孙悟空",this);
//重新指定窗口大小
this->resize(600,400);

//设置窗口标题
this->setWindowTitle("第一个项目");

//限制窗口大小
this->setFixedSize(600,400);

上面代码中,一个按钮其实就是一个QPushButton类下的对象,如果只是创建出对象,是无法显示到窗口中的,所以我们需要依赖一个父窗口,也就是指定一个父亲利用setParent函数即可,如果想设置按钮上显示的文字利用setText,移动按钮位置用move

对于窗口而言,我们可以修改左上角窗口的标题setWindowTitle,重新指定窗口大小:resize,或者设置固定的窗口大小setFixedSize

image-20230511214500211

3.2 对象模型(对象树)

在Qt中创建对象的时候会提供一个Parent对象指针,下面来解释这个parent到底是干什么的。

  • QObject是以对象树的形式组织起来的。

    • 当你创建一个QObject对象时,会看到QObject的构造函数接收一个QObject指针作为参数,这个参数就是 parent,也就是父对象指针。这相当于,在创建QObject对象时,可以提供一个其父对象,我们创建的这个QObject对象会自动添加到其父对象的children()列表。
    • 当父对象析构的时候,这个列表中的所有对象也会被析构。(注意,这里的父对象并不是继承意义上的父类!)这种机制在 GUI 程序设计中相当有用。例如,一个按钮有一个QShortcut(快捷键)对象作为其子对象。当我们删除按钮的时候,这个快捷键理应被删除。这是合理的。
  • QWidget是能够在屏幕上显示的一切组件的父类。

    • QWidget继承自QObject,因此也继承了这种对象树关系。一个孩子自动地成为父组件的一个子组件。因此,它会显示在父组件的坐标系统中,被父组件的边界剪裁。例如,当用户关闭一个对话框的时候,应用程序将其删除,那么,我们希望属于这个对话框的按钮、图标等应该一起被删除。事实就是如此,因为这些都是对话框的子组件。
    • 当然,我们也可以自己删除子对象,它们会自动从其父对象列表中删除。比如,当我们删除了一个工具栏时,其所在的主窗口会自动将该工具栏从其子对象列表中删除,并且自动调整屏幕显示。

Qt 引入对象树的概念,在一定程度上解决了内存问题。

  • 当一个QObject对象在堆上创建的时候,Qt 会同时为其创建一个对象树。不过,对象树中对象的顺序是没有定义的。这意味着,销毁这些对象的顺序也是未定义的。
  • 任何对象树中的 QObject对象 delete 的时候,如果这个对象有 parent,则自动将其从 parent 的children()列表中删除;如果有孩子,则自动 delete 每一个孩子。Qt 保证没有QObject会被 delete 两次,这是由析构顺序决定的。

如果QObject在栈上创建,Qt 保持同样的行为。正常情况下,这也不会发生什么问题。来看下下面的代码片段:

{
    QWidget window;
    QPushButton quit("Quit", &window);
}

作为父组件的 window 和作为子组件的 quit 都是QObject的子类(事实上,它们都是QWidget的子类,而QWidget是QObject的子类)。这段代码是正确的,quit 的析构函数不会被调用两次,因为标准 C++要求,局部对象的析构顺序应该按照其创建顺序的相反过程。因此,这段代码在超出作用域时,会先调用 quit 的析构函数,将其从父对象 window 的子对象列表中删除,然后才会再调用 window 的析构函数。

但是,如果我们使用下面的代码:

{
    QPushButton quit("Quit");
    QWidget window;
    quit.setParent(&window);
}

情况又有所不同,析构顺序就有了问题。我们看到,在上面的代码中,作为父对象的 window 会首先被析构,因为它是最后一个创建的对象。在析构过程中,它会调用子对象列表中每一个对象的析构函数,也就是说, quit 此时就被析构了。然后,代码继续执行,在 window 析构之后,quit 也会被析构,因为 quit 也是一个局部变量,在超出作用域的时候当然也需要析构。但是,这时候已经是第二次调用 quit 的析构函数了,C++ 不允许调用两次析构函数,因此,程序崩溃了。

由此我们看到,Qt 的对象树机制虽然帮助我们在一定程度上解决了内存问题,但是也引入了一些值得注意的事情。这些细节在今后的开发过程中很可能时不时跳出来烦扰一下,所以,我们最好从开始就养成良好习惯,在 Qt 中,尽量在构造的时候就指定 parent 对象,并且大胆在堆上创建。

3.3 Qt窗口坐标体系

坐标体系:

以左上角为原点(0,0),X向右增加,Y向下增加。

img

3.4 综合的一个小程序

#include "mywidget.h"
#include <QPushButton>
#include "mypushbutton.h"
#include <QDebug>
//命名规范
// 类名 首字母 大小  单词和单词之间 首字母 大写
// 变量、函数名 首字母小写 单词和单词之间 首字母 大写

//快捷键
// 运行 ctrl + R
// 编译 ctrl + B
// 查询 ctrl + F
// 注释 ctrl + /
// 帮助 F1
// 字体缩放  ctrl + 鼠标滚轮
// 整行代码移动  ctrl + shift + ↑ ↓
// 自动对齐  ctrl + i
// 同名之间的.h .cpp切换  F4

// 帮助文档 F1    左侧列表中按钮    C:\Qt\Qt5.6.0\5.6\mingw49_32\bin


MyWidget::MyWidget(QWidget *parent): QWidget(parent){
    //按钮
    QPushButton * btn = new QPushButton;

    //    btn->show(); //show用顶层方式弹出

    //如果想显示到当前窗口中 ,需要做依赖
    btn->setParent(this);
    //显示文本
    btn->setText("德玛西亚");
    //按钮 可以重置大小吗? 可以
    //    btn->resize(300,200);
    
    //按钮2
    QPushButton * btn2 = new QPushButton("第二个",this);
    //移动btn2
    btn2->move(100,100);
    
    //重置窗口大小(显示的)
    resize(600,400);
    //指定窗口标题
    setWindowTitle("第一个窗口");
    //设置窗口固定大小
	//    setFixedSize(600,400);


    //创建自定义的按钮
    MyPushButton * myBtn  = new MyPushButton;
    myBtn->setParent(this);
    myBtn->setText("我的按钮");
    myBtn->move( 300,200);


    //点击按钮  关闭窗口
    //connect(  信号发送者,发送的信号,信号的接受者,处理的槽函数)
    //信号和槽 优点: 松散耦合
    // connect(myBtn, &QPushButton::clicked , this, &QWidget::close );
    // connect( myBtn , &MyPushButton::clicked ,myBtn, &MyPushButton::close);//也可以关闭自身
    connect( myBtn , &MyPushButton::clicked ,this, &MyWidget::close);
}

MyWidget::~MyWidget(){	//他会先走这个代码,但是不会释放这个资源,会检测他的儿子,然后再走儿子的析构代码,释放儿子的资源再释放他自己
    qDebug() << "MyWidget析构调用";
}

image-20230511222548498

image-20230511222618759

4 信号和槽机制

信号槽是 Qt 框架引以为豪的机制之一。所谓信号槽,实际就是观察者模式。当某个事件发生之后,比如,按钮检测到自己被点击了一下,它就会发出一个信号(signal)。这种发出是没有目的的,类似广播。如果有对象对这个信号感兴趣,它就会使用连接(connect)函数,意思是,将想要处理的信号和自己的一个函数(称为槽(slot))绑定来处理这个信号。也就是说,当信号发出时,被连接的槽函数会自动被回调。这就类似观察者模式:当发生了感兴趣的事件,某一个操作就会被自动触发。

4.1 系统自带的信号和槽

下面我们完成一个小功能,上面我们已经学习了按钮的创建,但是还没有体现出按钮的功能,按钮最大的功能也就是点击后触发一些事情,比如我们点击按钮,就把当前的窗口给关闭掉,那么在Qt中,这样的功能如何实现呢?

其实无法两行代码就可以搞定了,我们看下面的代码

QPushButton * quitBtn = new QPushButton("关闭窗口",this);
connect(quitBtn,&QPushButton::clicked,this,&MyWidget::close);

第一行是创建一个关闭按钮,这个之前已经学过,第二行就是核心了,也就是信号槽的使用方式

connect()函数最常用的一般形式:

connect(sender, signal, receiver, slot);
参数解释:
    sender:发出信号的对象
    signal:发送对象发出的信号
    receiver:接收信号的对象
    slot:接收对象在接收到信号之后所需要调用的函数(槽函数)

那么系统自带的信号和槽通常如何查找呢,这个就需要利用帮助文档了,在帮助文档中比如我们上面的按钮的点击信号,在帮助文档中输入QPushButton,首先我们可以在Contents中寻找关键字 signals,信号的意思,但是我们发现并没有找到,这时候我们应该想到也许这个信号的被父类继承下来的,因此我们去他的父类QAbstractButton中就可以找到该关键字,点击signals索引到系统自带的信号有如下几个

QAbstractButton Class   //QAbstractButton是QPushButton的父类
void clicked(bool checked = false)				//点击
void pressed()								//按下
void released()								//抬起
void toggled(bool checked)

这里的clicked就是我们要找到,槽函数的寻找方式和信号一样,只不过他的关键字是slot。

4.2 自定义信号和槽

使用connect()可以让我们连接系统提供的信号和槽。但是,Qt 的信号槽机制并不仅仅是使用系统提供的那部分,还会允许我们自己设计自己的信号和槽。

下面我们看看使用 Qt 的信号槽:

首先定义一个学生类和老师类:
	老师类中声明信号 饿了 hungry
signals:
       void hungury();

	学生类中声明槽   请客 treat
	public slots:
       void treat();
	
在窗口中声明一个公共方法下课,这个方法的调用会触发老师饿了这个信号,而响应槽函数学生请客
	void MyWidget::ClassIsOver()
{
    //发送信号
    emit teacher->hungury();
}
	学生响应了槽函数,并且打印信息
//自定义槽函数 实现
void Student::eat(){
       qDebug() << "该吃饭了!";
}
在窗口中连接信号槽
teacher = new Teacher(this);
student = new Student(this);

connect(teacher,&Teacher::hungury,student,&Student::treat);
//并且调用下课函数,测试打印出 “该吃饭了”

//自定义的信号 hungry带参数,需要提供重载的自定义信号和 自定义槽
void hungury(QString name);  自定义信号
void treat(QString name );    自定义槽
    
//但是由于有两个重名的自定义信号和自定义的槽,直接连接会报错,所以需要利用函数指针来指向函数地址, 然后在做连接
void (Teacher:: * teacherSingal)(QString) = &Teacher::hungury;
void (Student:: * studentSlot)(QString) = &Student::treat;
connect(teacher,teacherSingal,student,studentSlot);

自定义信号槽需要注意的事项:

  • 发送者和接收者都需要是QObject的子类(当然,槽函数是全局函数、Lambda 表达式等无需接收者的时候除外);
  • 信号和槽函数返回值是 void
  • 信号只需要声明,不需要实现
  • 槽函数需要声明也需要实现
  • 槽函数是普通的成员函数,作为成员函数,会受到 public、private、protected 的影响;
  • 使用 emit 在恰当的位置发送信号;
  • 使用connect()函数连接信号和槽。
  • 任何成员函数、static 函数、全局函数和 Lambda 表达式都可以作为槽函数
  • 信号槽要求信号和槽的参数一致,所谓一致,是参数类型一致。
  • 如果信号和槽的参数不一致,允许的情况是,槽函数的参数可以比信号的少,即便如此,槽函数存在的那些参数的顺序也必须和信号的前面几个一致起来。这是因为,你可以在槽函数中选择忽略信号传来的数据(也就是槽函数的参数比信号的少)。

4.3 信号槽的拓展

  • 一个信号可以和多个槽相连

    • 如果是这种情况,这些槽会一个接一个的被调用,但是它们的调用顺序是不确定的。
  • 多个信号可以连接到一个槽

    • 只要任意一个信号发出,这个槽就会被调用。
  • 一个信号可以连接到另外的一个信号

    • 当第一个信号发出时,第二个信号被发出。除此之外,这种信号-信号的形式和信号-槽的形式没有什么区别。
  • 槽可以被取消链接

    • 这种情况并不经常出现,因为当一个对象delete之后,Qt自动取消所有连接到这个对象上面的槽。
  • 信号槽可以断开

    • 利用disconnect关键字是可以断开信号槽的
  • 使用Lambda 表达式

    • 在使用 Qt 5 的时候,能够支持 Qt 5 的编译器都是支持 Lambda 表达式的。
    • 在连接信号和槽的时候,槽函数可以使用Lambda表达式的方式进行处理。后面我们会详细介绍什么是Lambda表达式

4.4 Qt4版本的信号槽写法

connect(zt,SIGNAL(hungry(QString)),st,SLOT(treat(QString)));

这里使用了SIGNAL和SLOT这两个宏,将两个函数名转换成了字符串。注意到connect()函数的 signal 和 slot 都是接受字符串,一旦出现连接不成功的情况,Qt4是没有编译错误的(因为一切都是字符串,编译期是不检查字符串是否匹配),而是在运行时给出错误。这无疑会增加程序的不稳定性。

Qt5在语法上完全兼容Qt4,而反之是不可以的。

4.5 Lambda表达式

C++11中的Lambda表达式用于定义并创建匿名的函数对象,以简化编程工作。首先看一下Lambda表达式的基本构成:

[capture](parameters) mutable ->return-type{
statement
}//[函数对象参数](操作符重载函数参数)mutable ->返回值{函数体}

① 函数对象参数;

[],标识一个Lambda的开始,这部分必须存在,不能省略。函数对象参数是传递给编译器自动生成的函数对象类的构造函数的。函数对象参数只能使用那些到定义Lambda为止时Lambda所在作用范围内可见的局部变量(包括Lambda所在类的this)。函数对象参数有以下形式:

  • 空。没有使用任何函数对象参数。
  • =。函数体内可以使用Lambda所在作用范围内所有可见的局部变量(包括Lambda所在类的this),并且是值传递方式(相当于编译器自动为我们按值传递了所有局部变量)。(常用)
  • &。函数体内可以使用Lambda所在作用范围内所有可见的局部变量(包括Lambda所在类的this),并且是引用传递方式(相当于编译器自动为我们按引用传递了所有局部变量)。
  • this。函数体内可以使用Lambda所在类中的成员变量。
  • a。将a按值进行传递。按值进行传递时,函数体内不能修改传递进来的a的拷贝,因为默认情况下函数是const的。要修改传递进来的a的拷贝,可以添加mutable修饰符。
  • &a。将a按引用进行传递。
  • a, &b。将a按值进行传递,b按引用进行传递。
  • =,&a, &b。除a和b按引用进行传递外,其他参数都按值进行传递。
  • &, a, b。除a和b按值进行传递外,其他参数都按引用进行传递。

② 操作符重载函数参数;

标识重载的()操作符的参数,没有参数时,这部分可以省略。参数可以通过按值(如:(a,b))和按引用(如:(&a,&b))两种方式进行传递。

③ 可修改标示符;

mutable声明,这部分可以省略。按值传递函数对象参数时,加上mutable修饰符后,可以修改按值传递进来的拷贝(注意是能修改拷贝,而不是值本身)。

QPushButton * myBtn = new QPushButton (this);
QPushButton * myBtn2 = new QPushButton (this);
myBtn2->move(100,100);
int m = 10;
connect(myBtn,&QPushButton::clicked,this,[m] ()mutable { m = 20; qDebug() << m; });
connect(myBtn2,&QPushButton::clicked,this,[=] ()  { qDebug() << m; });
qDebug() << m;

④ 函数返回值;

  • ->返回值类型,标识函数返回值的类型,当返回值为void,或者函数体中只有一处return的地方(此时编译器可以自动推断出返回值类型)时,这部分可以省略。

⑤ 是函数体;

  • {},标识函数的实现,这部分不能省略,但函数体可以为空。

4.6 案列

//main.cpp主要内容
QApplication a(argc, argv);
Widget w;
w.show();
return a.exec();
//widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include "student.h"
#include "teacher.h"

class Widget : public QWidget{
    Q_OBJECT
public:
    Widget(QWidget *parent = 0);
    ~Widget();
    Teacher * zt;
    Student * st;
    //下课
    void classIsOver();
};
#endif // WIDGET_H



//widget.cpp
#include "widget.h"
#include <QPushButton>
#include <QDebug>

// Teacher老师类
// Student学生类
// 下课后  老师会触发一个 饿了 的信号
// 学生响应这个信号 并且 请老师吃饭
Widget::Widget(QWidget *parent): QWidget(parent){
    this->zt = new Teacher(this);
    this->st = new Student(this);

    //连接信号和槽(没有函数重载时)
    //connect(zt,&Teacher::hungry,st,&Student::treat);
    //classIsOver();	//内部定义了一个触发自定义信号

    //连接有参信号和槽(有无函数重载都可以用这个)
    //函数指针 可以指向 函数地址
    //void(Teacher:: *teacherSignal)(QString) = &Teacher::hungry;
    //void(Student:: *studentSlot)(QString ) = &Student::treat;
    //connect(zt,teacherSignal, st,studentSlot);
    //classIsOver();	//内部定义了一个触发自定义信号

    //创建按钮
    QPushButton * btn = new QPushButton("下课" , this);
    resize(600,400);
    void(Teacher:: *teacherSignal2)() = &Teacher::hungry;
    void(Student:: *studentSlot2)() = &Student::treat;
    connect(zt,teacherSignal2, st,studentSlot2);				//老师饿了触发学生请客的信号

    //1、信号是可以连接信号
    connect(btn,&QPushButton::clicked,zt,teacherSignal2);		//按钮点击触发老师饿了的信号
    //2、可以断开信号和槽
    disconnect(zt,teacherSignal2, st,studentSlot2);
    //3、一个信号可以响应多个槽函数
    //4、多个信号可以连接同一个槽函数
    //5、信号和槽函数的参数类型 必须一一对应
    //信号的参数个数 可以多余槽函数的参数个数,反之不可以 , 参数类型要一一对应


    //Qt4版本信号和槽写法
    //利用Qt4版本连接有参信号和槽
    //优势 :参数直观
    //劣势 :参数类型不做匹配检测
    //Qt4本质   SIGNAL("hungry(int)")SLOT("treat(QString)")
    //connect(zt, SIGNAL(hungry(QString)) , st , SLOT(treat(QString)));
    //classIsOver();		//内部定义了一个触发自定义信号

    
    //[=] 函数体内可以使用Lambda所在作用范围内所有可见的局部变量
    QPushButton * btn2 = new QPushButton("aaa",this);
    QPushButton * btn3 = new QPushButton("aaa",this);
    [=](){
        btn2->setText("bbb");
        btn3->setText("bbb");
    }();		//()是表示调用

    //最常用lambda使用 [=](){}
    QPushButton * btn4 = new QPushButton("aaa",this);
    btn4->move( 100, 0);

    //当进行信号和槽连接时候,控件内会进入一个锁的状态
    connect(btn4,&QPushButton::clicked,this,[=](){
        btn4->setText("bbb");		//可能会出问题
    });

   //加上mutable修饰符后,可以修改按值传递进来的拷贝
   QPushButton * myBtn = new QPushButton (this);
   QPushButton * myBtn2 = new QPushButton (this);
   myBtn2->move(100,100);
   int m = 10;
   connect(myBtn,&QPushButton::clicked,this,[m] () mutable { m = 20; qDebug() << m; });
   connect(myBtn2,&QPushButton::clicked,this,[=] ()  { qDebug() << m; });
   qDebug() << m;

   //-> 返回值类型
   int num = [=]()->int{
        return 1000;
   }();
   qDebug() << "num = " << num ;

   //点击按钮 关闭窗口
   connect(btn4,&QPushButton::clicked,[=](){
        //this->close();
        st->treat("宫保鸡丁");
   });
}

void Widget::classIsOver(){
    //触发自定义信号
	//emit this->zt->hungry();
   	emit this->zt->hungry("宫保鸡丁");
}

Widget::~Widget(){
}

//student.h
#ifndef TEACHER_H
#define TEACHER_H
#include <QObject>

class Teacher : public QObject{
    Q_OBJECT
public:
    explicit Teacher(QObject *parent = 0);

//自定义信号  写到signals下
signals:
    //返回值是void
    //只需要声明 不需要实现
    //可以有参数  可以发生重载
    void hungry();
    void hungry( QString foodName);
public slots:
};

#endif // TEACHER_H



//teacher.cpp
#include "teacher.h"
Teacher::Teacher(QObject *parent) : QObject(parent){
}
//student.h
#ifndef STUDENT_H
#define STUDENT_H
#include <QObject>

class Student : public QObject{
    Q_OBJECT
public:
    explicit Student(QObject *parent = 0);

signals:
    //自定义槽函数 写到public slots  Qt 5.0版本以上 可以写成全局函数或者public作用域下 或者 lambda表达式
public slots:
    //返回值是void
    //需要声明 也需要有实现
    //可以有参数  可以发生重载
    void treat();
    void treat(QString foodName);
};
#endif // STUDENT_H


//student.cpp
#include "student.h"
#include <QDebug>
Student::Student(QObject *parent) : QObject(parent){
}

void Student::treat(){
    qDebug() << "请老师吃饭";
}

void Student::treat(QString foodName){
    //QString 转 char *    通过.toUtf8转为 QByteArray 类型  通过 .data()转为 char *
     qDebug() << "请老师吃饭 , 老师要吃: " << foodName.toUtf8().data();
}

5 QMainWindow

QMainWindow是一个为用户提供主窗口程序的类,包含一个菜单栏(menu bar)、多个工具栏(tool bars)、多个锚接部件(dock widgets)、一个状态栏(status bar)及一个中心部件(central widget),是许多应用程序的基础,如文本编辑器,图片编辑器等。

img

简单版:直接创建一个带UI界面的东西吧,直接在里面点点。状态栏还是建议用代码的方式

image-20230817102922105

5.1 菜单栏

一个主窗口最多只有一个菜单栏。位于主窗口顶部、主窗口标题栏下面。

image-20230513163911244

  • 创建菜单栏,通过QMainWindow类的menubar()函数获取主窗口菜单栏指针
QMenuBar* menuBar() const
  • 创建菜单,调用QMenu的成员函数addMenu来添加菜单
QAction* addMenu(QMenu * menu)
QMenu* addMenu(const QString & title)
QMenu* addMenu(const QIcon & icon, const QString & title)
  • 创建菜单项,调用QMenu的成员函数addAction来添加菜单项
QAction* activeAction() const
QAction* addAction(const QString & text)
QAction* addAction(const QIcon & icon, const QString & text)
QAction* addAction(const QString & text, const QObject * receiver,const char * member, const QKeySequence & shortcut = 0)
QAction* addAction(const QIcon & icon, const QString & text, const QObject * receiver, const char * member, const QKeySequence & shortcut = 0)

Qt 并没有专门的菜单项类,只是使用一个QAction类,抽象出公共的动作。当我们把QAction对象添加到菜单,就显示成一个菜单项,添加到工具栏,就显示成一个工具按钮。用户可以通过点击菜单项、点击工具栏按钮、点击快捷键来激活这个动作。

resize(600,400);
//1、菜单栏 只能有一个(不是用new的)
QMenuBar * bar = menuBar();     //创建一个菜单栏
setMenuBar(bar);                //把菜单栏放到我们的窗口中

//创建菜单
QMenu * fileMenu = bar->addMenu("文件");
QMenu * editMenu = bar->addMenu("编辑");

//创建菜单项
QAction * newAction = fileMenu->addAction("新建");

//添加分割线
fileMenu->addSeparator();

QAction * openAction = fileMenu->addAction("打开");

image-20230513164957686

5.2 工具栏

主窗口的工具栏上可以有多个工具条,通常采用一个菜单对应一个工具条的的方式,也可根据需要进行工具条的划分。

  • 直接调用QMainWindow类的addToolBar()函数获取主窗口的工具条对象,每增加一个工具条都需要调用一次该函数。
  • 插入属于工具条的动作,即在工具条上添加操作。
    • 通过QToolBar类的addAction函数添加。
  • 工具条是一个可移动的窗口,它的停靠区域由QToolBarallowAreas决定,包括:
    • Qt::LeftToolBarArea 停靠在左侧
    • Qt::RightToolBarArea 停靠在右侧
    • Qt::TopToolBarArea 停靠在顶部
    • Qt::BottomToolBarArea 停靠在底部
    • Qt::AllToolBarAreas 以上四个位置都可停靠

使用setAllowedAreas()函数指定停靠区域:

setAllowedAreas(Qt::LeftToolBarArea | Qt::RightToolBarArea)

使用setMoveable()函数设定工具栏的可移动性:

setMoveable(false//工具条不可移动, 只能停靠在初始化的位置上
//2、工具栏 可以有多个
QToolBar * toolBar = new QToolBar(this);
addToolBar(Qt::LeftToolBarArea,toolBar);//默认停靠范围 Qt::LeftToolBarArea,  添加到窗口中

//设置只允许左右停靠
toolBar->setAllowedAreas(Qt::LeftToolBarArea | Qt::RightToolBarArea);

//设置浮动
toolBar->setFloatable(false);

//设置移动 (总开关-设置为false就拖拽不了)
toolBar->setMovable(false);

//工具栏中放入小部件
toolBar->addAction(newAction);
//添加分割线
toolBar->addSeparator();
toolBar->addAction(openAction);

image-20230513165443593

5.3 状态栏

  • 派生自QWidget类,使用方法与QWidget类似,QStatusBar类常用成员函数:
  • 状态栏也只能最多有一个
//添加小部件
void addWidget(QWidget * widget, int stretch = 0)
//插入小部件
int	insertWidget(int index, QWidget * widget, int stretch = 0)
//删除小部件
void removeWidget(QWidget * widget)
//状态栏  只能有一个(不是new出来的)
QStatusBar * stBar = statusBar();
setStatusBar(stBar);

QLabel * label1 = new QLabel("左侧提示信息",this);
stBar->addWidget(label1);		//左侧信息添加

QLabel * label2 = new QLabel("右侧提示信息",this);
stBar->addPermanentWidget(label2);

5.4 铆接部件

铆接部件 QDockWidget,也称浮动窗口,可以有多个。

QDockWidget * dock = new QDockWidget("标题",this);
addDockWidget(Qt::LeftDockWidgetArea,dock);//添加到窗口中
dock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea | Qt::TopDockWidgetArea);  //设置后期停靠范围
//铆接部件 (浮动窗口)  可以有多个
QDockWidget * dock = new QDockWidget("aaa" ,this);
addDockWidget(Qt::BottomDockWidgetArea,dock);

//设置这个浮动窗口只允许左右停靠
dock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);

5.5 核心部件(中心部件)

除了以上几个部件,中心显示的部件都可以作为核心部件,例如一个记事本文件,可以利用QTextEdit做核心部件

QTextEdit * edit = new QTextEdit(this);
setCentralWidget(edit);
//核心部件  只能有一个
QTextEdit * edit = new QTextEdit(this);
setCentralWidget(edit);

image-20230513165802576

5.6 资源文件

Qt 资源系统是一个跨平台的资源机制,用于将程序运行时所需要的资源以二进制的形式存储于可执行文件内部。如果你的程序需要加载特定的资源(图标、文本翻译等),那么,将其放置在资源文件中,就再也不需要担心这些文件的丢失。也就是说,如果你将资源以资源文件形式存储,它是会编译到可执行文件内部。右键项目->添加新文件 -> Qt ->Qt Recourse File

1. 将资源文件放入到项目下

afebca1eefc24417a408f5a5813dfe93-1

使用 Qt Creator 可以很方便地创建资源文件。我们可以在工程上点右键,选择“添加新文件…”,可以在 Qt 分类下找到“Qt 资源文件”:

2. 右键项目->添加新文件 -> Qt ->Qt Recourse File

7e1b0197369e4c25bfcafdba0c4242a3

点击“选择…”按钮,打开“新建 Qt 资源文件”对话框。在这里我们输入资源文件的名字和路径:

3. 给资源文件起名 res 生成 res.qrc

70ce638003f547b9aa06dbf887722d4c

点击下一步,选择所需要的版本控制系统,然后直接选择完成。我们可以在 Qt Creator 的左侧文件列表中看到“资源文件”一项,也就是我们新创建的资源文件:

78833e82ee274056b05221d629f1dcd7

4. 用编辑的方式打开 res.qrc

596e50ed922f4867aad84b1e6a6414ec

5. 添加前置 - 添加文件

右侧的编辑区有个“添加”,我们首先需要添加前缀,比如我们将前缀取名为 images。然后选中这个前缀,继续点击添加文件,可以找到我们所需添加的文件。这里,我们选择 document-open.png 文件。当我们完成操作之后,Qt Creator 应该是这样子的:

86d1070bb5404641852cdf24fad1a442

6. 使用 “ : + 前缀名 + 文件名 ”

接下来,我们还可以添加另外的前缀或者另外的文件。这取决于你的需要。当我们添加完成之后,我们可以像前面一章讲解的那样,通过使用 : 开头的路径来找到这个文件。比如,我们的前缀是 /images,文件是 document-open.png,那么就可以使用:/images/document-open.png找到这个文件。

这么做带来的一个问题是,如果以后我们要更改文件名,比如将 docuemnt-open.png 改成 docopen.png,那么,所有使用了这个名字的路径都需要修改。所以,更好的办法是,我们给这个文件去一个“别名”,以后就以这个别名来引用这个文件。具体做法是,选中这个文件,添加别名信息

9da36dc10ad24693a7b4e1f253424a61

这样,我们可以直接使用:/images/hudie引用到这个资源,无需关心图片的真实文件名

如果我们使用文本编辑器打开 res.qrc 文件,就会看到一下内容:

<RCC>
    <qresource prefix="/">
        <file alias="hudie">Image/butterfly.png</file>		//取的别名
        <file>Image/butterfly1.png</file>
        <file>Image/down.png</file>
        <file>Image/Frame.jpg</file>
        <file>Image/Luffy.png</file>
        <file>Image/LuffyQ.png</file>
        <file>Image/mario.gif</file>
        <file>Image/OnePiece.png</file>
        <file>Image/Sunny.jpg</file>
        <file>Image/sunny.png</file>
        <file>Image/up.png</file>
    </qresource>
</RCC>

我们可以对比一下,看看 Qt Creator 帮我们生成的是怎样的 qrc 文件。当我们编译工程之后,我们可以在构建目录中找到 qrc_res.cpp 文件,这就是 Qt 将我们的资源编译成了 C++ 代码。

8c02ffbdb2034c52bec176105925ddb5

//给新建添加小图标
//ui->actionNew->setIcon(QIcon("E:/Image/Luffy.png"));

//资源文件添加   语法:   ": + 前缀名  + 文件名称"
ui->actionNew->setIcon(QIcon(":/Image/Luffy.png"));
ui->actionOpen->setIcon(QIcon(":/Image/LuffyQ.png"));

6 对话框QDialog

6.1 基本概念

对话框是 GUI 程序中不可或缺的组成部分。很多不能或者不适合放入主窗口的功能组件都必须放在对话框中设置。对话框通常会是一个顶层窗口,出现在程序最上层,用于实现短期任务或者简洁的用户交互。

Qt 中使用QDialog类实现对话框。就像主窗口一样,我们通常会设计一个类继承QDialog。QDialog(及其子类,以及所有Qt::Dialog类型的类)的对于其 parent 指针都有额外的解释:如果 parent 为 NULL,则该对话框会作为一个顶层窗口,否则则作为其父组件的子对话框(此时,其默认出现的位置是 parent 的中心)。顶层窗口与非顶层窗口的区别在于,顶层窗口在任务栏会有自己的位置,而非顶层窗口则会共享其父组件的位置。

对话框分为模态对话框和非模态对话框。

  • 模态对话框,就是会阻塞同一应用程序中其它窗口的输入
    • 模态对话框很常见,比如“打开文件”功能。你可以尝试一下记事本的打开文件,当打开文件对话框出现时,我们是不能对除此对话框之外的窗口部分进行操作的。
  • 与此相反的是非模态对话框,例如查找对话框,我们可以在显示着查找对话框的同时,继续对记事本的内容进行编辑。

6.2 标准对话框QMessageBox

利用静态成员函数可以弹出 警告、询问、信息、错误对话框

所谓标准对话框,是 Qt 内置的一系列对话框,用于简化开发。事实上,有很多对话框都是通用的,比如打开文件、设置颜色、打印设置等。这些对话框在所有程序中几乎相同,因此没有必要在每一个程序中都自己实现这么一个对话框。

// Qt 的内置对话框大致分为以下几类:
QColorDialog:			选择颜色;
QFileDialog:			选择文件或者目录;
QFontDialog:			选择字体;
QInputDialog:			允许用户输入一个值,并将其值返回;
QMessageBox:			模态对话框,用于显示信息、询问问题等;
QPageSetupDialog:		为打印机提供纸张相关的选项;
QPrintDialog:			打印机配置;
QPrintPreviewDialog:	打印预览;
QProgressDialog:		显示操作过程。

演示前提:已经创建了文件,添加了菜单的这几个功能键,

image-20230513195239680

//点击新建 创建对话框
    connect(ui->actionNew,&QAction::triggered,this,[=](){	//触发新建按钮会触发的事件在下面写
		//对话框分类  模态对话框   非模态对话框,
		//如:模态对话框创建
        QDialog dlg(this);
        dlg.resize(120,30);
        dlg.exec();
    });

常用Qt 的内置对话框

  • QColorDialog: 选择颜色;

    QColor color = QColorDialog::getColor(Qt::red);
    
    //简单使用(模态的)
    QColor color = QColorDialog::getColor(Qt::red);
    qDebug() << color.red() << color.green() << color.blue() ;

    image-20230513205144045

  • QFileDialog: 选择文件或者目录;

    QString fileName = QFileDialog::getOpenFileName(this,"打开文件","C:\\Users\\zhangtao\\Desktop","(*.doc)");
    qDebug () <<fileName;

    image-20230513205527508

  • QFontDialog: 选择字体;

    QFont font = QFontDialog::getFont(&flag,QFont("华文彩云",36));
    qDebug() << "字体" << font.family().toUtf8().data() << "字号"<< font.pointSize()
        	<< "是否加粗"<<font.bold() << "是否倾斜" << font.italic();

    image-20230513205616867

  • QInputDialog: 允许用户输入一个值,并将其值返回;

  • QMessageBox: 模态对话框,用于显示信息、询问问题等;

  • QPageSetupDialog: 为打印机提供纸张相关的选项;

  • QPrintDialog: 打印机配置;

  • QPrintPreviewDialog: 打印预览;

  • QProgressDialog: 显示操作过程。

6.3 自定义消息框

Qt 支持模态对话框和非模态对话框。

模态与非模态的实现:

  • 使用QDialog::exec()实现应用程序级别的模态对话框
  • 使用QDialog::open()实现窗口级别的模态对话框
  • 使用QDialog::show()实现非模态对话框。

模态对话框

  • Qt 有两种级别的模态对话框:

    • 应用程序级别的模态

      当该种模态的对话框出现时,用户必须首先对对话框进行交互,直到关闭对话框,然后才能访问程序中其他的窗口。

    • 窗口级别的模态

      该模态仅仅阻塞与对话框关联的窗口,但是依然允许用户与程序中其它窗口交互。窗口级别的模态尤其适用于多窗口模式。

一般默认是应用程序级别的模态。

在下面的示例中,我们调用了exec()将对话框显示出来,因此这就是一个模态对话框。当对话框出现时,我们不能与主窗口进行任何交互,直到我们关闭了该对话框。

QDialog dialog(this);
dialog.resize(300,50);							//做大一点防止警告
dialog.setWindowTitle(tr("Hello, dialog!"));	  //这个对话框的标题
dialog.exec();

image-20230513200956311

非模态对话框

下面我们试着将exec()修改为show(),看看非模态对话框:

QDialog dialog(this);
dialog.setWindowTitle(tr("Hello, dialog!"));
dialog.show();

是不是事与愿违?对话框竟然一闪而过!这是因为,show()函数不会阻塞当前线程,对话框会显示出来,然后函数立即返回,代码继续执行。注意,dialog 是建立在栈上的,show()函数返回,MainWindow::open()函数结束,dialog 超出作用域被析构,因此对话框消失了。知道了原因就好改了,我们将 dialog 改成堆上建立,当然就没有这个问题了:

 QDialog *dialog = new QDialog;
dialog->setWindowTitle(tr("Hello, dialog!"));
dialog->show();

如果你足够细心,应该发现上面的代码是有问题的:dialog 存在内存泄露!dialog 使用 new 在堆上分配空间,却一直没有 delete。解决方案也很简单:将 MainWindow 的指针赋给 dialog 即可。还记得我们前面说过的 Qt 的对象系统吗?

不过,这样做有一个问题:如果我们的对话框不是在一个界面类中出现呢?由于QWidgetparent 必须是QWidget指针,那就限制了我们不能将一个普通的 C++ 类指针传给 Qt 对话框。另外,如果对内存占用有严格限制的话,当我们将主窗口作为 parent 时,主窗口不关闭,对话框就不会被销毁,所以会一直占用内存。在这种情景下,我们可以设置 dialogWindowAttribute

//非模态对话框创建
QDialog *dialog2 = new QDialog(this);    //如果这个创建在栈上QDialog *dlg2 = QDialog(this);就一闪而过了
dialog2->resize(300,50);
dialog2->setWindowTitle(tr("Hello, dialog!"));
dialog2->show();
//设置 55号属性
dialog2->setAttribute(Qt::WA_DeleteOnClose);

setAttribute()函数设置对话框关闭时,自动销毁对话框。

image-20230513203239612

6.4 消息对话框

QMessageBox用于显示消息提示。我们一般会使用其提供的几个 static 函数:

  • 显示关于对话框。
void about(QWidget * parent, const QString & title, const QString & text)

QMessageBox::about(this, "傻逼", "你TM才傻逼");   //自己做了个小测试,这个是模态的

这是一个最简单的对话框,其标题是 title,内容是 text,父窗口是 parent。对话框只有一个 OK 按钮。

  • 显示关于 Qt 对话框。该对话框用于显示有关 Qt 的信息。
void aboutQt(QWidget * parent, const QString & title = QString());
QMessageBox::aboutQt(this, "傻逼");		//自己做了个小测试,这个是模态的
  • 显示严重错误对话框。

    这个对话框将显示一个红色的错误符号。我们可以通过 buttons 参数指明其显示的按钮。默认情况下只有一个 Ok 按钮,我们可以使用StandardButtons类型指定多种按钮。

StandardButton critical(QWidget * parent, const QString & title, const QString & text, 
                        StandardButtons buttons = Ok,StandardButton defaultButton = NoButton)//错误提示对话框
QMessageBox::critical(this,"错误","critical");

image-20230513203612604

  • 与QMessageBox::critical()类似,不同之处在于这个对话框提供一个普通信息图标。
StandardButton information(QWidget * parent, const QString & title, const QString & text, 
                           StandardButtons buttons = Ok, StandardButton defaultButton = NoButton)
//信息提示对话框(简单使用)
QMessageBox::information(this,"信息","info");//自己做了个小测试,这个是模态的

image-20230513203756240

  • 与QMessageBox::critical ()类似,不同之处在于这个对话框提供一个问号图标,并且其显示的按钮是“是”和“否”
StandardButton question(QWidget * parent,const QString & title, const QString & text, 
             StandardButtons buttons = StandardButtons( Yes | No ), StandardButton defaultButton = NoButton) ;

//询问提示对话框
// 参数1   父窗口  参数2  标题  参数3  提示信息  参数4  按键类型  参数5  默认关联回车按键
if( QMessageBox::Save ==  QMessageBox::question(this,"询问","question" , QMessageBox::Save | QMessageBox::Cancel ,QMessageBox::Cancel))
{
    qDebug()<<"点击的是保存";
}
else{
    qDebug()<<"点击的是取消";
}   

image-20230513204057978

  • 与QMessageBox::critical()类似,不同之处在于这个对话框提供一个黄色叹号图标
StandardButton warning(QWidget * parent, const QString & title, const QString & text, 
                       StandardButtons buttons = Ok,StandardButton defaultButton = NoButton)

//警告提示对话框
QMessageBox::warning(this,"警告","warning");    

image-20230513204331313

我们可以通过下面的代码来演示下如何使用QMessageBox。

if (QMessageBox::Yes == QMessageBox::question(this, tr("Question"), tr("Are you OK?"), 
                                              QMessageBox::Yes | MessageBox::No,QMessageBox::Yes)) 
{
    QMessageBox::information(this, tr("Hmmm..."), tr("I'm glad to hear that!"));
} 
else {
    QMessageBox::information(this, tr("Hmmm..."), tr("I'm sorry!"));
}

image-20230513204559007image-20230513204609321image-20230513204622019

我们使用QMessageBox::question()来询问一个问题。

  • 这个对话框的父窗口是 this。

    QMessageBox是QDialog的子类,它的初始显示位置是在 parent 窗口的中央(这边是在MainWindow的中间,我拖拽过来的)。

  • 第二个参数是对话框的标题。

  • 第三个参数是我们想要显示的内容。

  • 第四个参数是关联的按键类型,我们可以使用或运算符(|)指定对话框应该出现的按钮。比如我们希望是一个 Yes 和一个 No。

  • 最后一个参数指定默认选择的按钮。

  • 这个函数有一个返回值,用于确定用户点击的是哪一个按钮。按照我们的写法,应该很容易的看出,这是一个模态对话框,因此我们可以直接获取其返回值。

自定义消息对话框

QMessageBox类的 static 函数优点是方便使用,缺点也很明显:非常不灵活。我们只能使用简单的几种形式。为了能够定制QMessageBox细节,我们必须使用QMessageBox的属性设置 API。如果我们希望制作一个询问是否保存的对话框,我们可以使用如下的代码:

QMessageBox msgBox;

msgBox.setText(tr("The document has been modified."));
msgBox.setInformativeText(tr("Do you want to save your changes?"));
msgBox.setDetailedText(tr("Differences here..."));
msgBox.setStandardButtons(QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel);
msgBox.setDefaultButton(QMessageBox::Save);
int ret = msgBox.exec();
switch (ret) {
    case QMessageBox::Save:
        qDebug() << "Save document!";
        break;
    case QMessageBox::Discard:
        qDebug() << "Discard changes!";
        break;
    case QMessageBox::Cancel:
        qDebug() << "Close document!";
        break;
}

msgBox 是一个建立在栈上的QMessageBox实例。我们设置其主要文本信息为“The document has been modified.”,informativeText 则是会在对话框中显示的简单说明文字。下面我们使用了一个detailedText,也就是详细信息,当我们点击了详细信息按钮时,对话框可以自动显示更多信息。我们自己定义的对话框的按钮有三个:保存、丢弃和取消。然后我们使用了exec()是其成为一个模态对话框,根据其返回值进行相应的操作。

image-20240415211249088

6.5 标准文件对话框

//文件对话框
QString fileName = QFileDialog::getOpenFileName(this,"打开文件","C:\\Users\\zhangtao\\Desktop","(*.*)");
qDebug () <<fileName;

2acfa67320884daa8834b41a35430bca

QFileDialog,也就是文件对话框。在本节中,我们将尝试编写一个简单的文本文件编辑器,我们将使用QFileDialog来打开一个文本文件,并将修改过的文件保存到硬盘。

首先,我们需要创建一个带有文本编辑功能的窗口。借用我们前面的程序代码,应该可以很方便地完成:

openAction = new QAction(QIcon(":/images/file-open"),tr("&Open..."), this);
openAction->setStatusTip(tr("Open an existing file"));

saveAction = new QAction(QIcon(":/images/file-save"), tr("&Save..."), this);
saveAction->setStatusTip(tr("Save a new file"));

QMenu *file = menuBar()->addMenu(tr("&File"));
file->addAction(openAction);
file->addAction(saveAction);

QToolBar *toolBar = addToolBar(tr("&File"));
toolBar->addAction(openAction);
toolBar->addAction(saveAction);

textEdit = new QTextEdit(this);
setCentralWidget(textEdit);

我们在菜单和工具栏添加了两个动作:打开和保存。接下来是一个QTextEdit类,这个类用于显示富文本文件。也就是说,它不仅仅用于显示文本,还可以显示图片、表格等等。不过,我们现在只用它显示纯文本文件。QMainWindow有一个setCentralWidget()函数,可以将一个组件作为窗口的中心组件,放在窗口中央显示区。显然,在一个文本编辑器中,文本编辑区就是这个中心组件,因此我们将QTextEdit作为这种组件。

我们使用connect()函数,为这两个QAction对象添加响应的动作:

connect(openAction, &QAction::triggered, this, &MainWindow::openFile);
connect(saveAction, &QAction::triggered, this, &MainWindow::saveFile);

下面是最主要的openFile()和saveFile()这两个函数的代码:

//打开文件
void MainWindow::openFile()
{
    QString path = QFileDialog::getOpenFileName(this, tr("Open File"), ".", tr("Text Files(*.txt)"));
    if(!path.isEmpty()) {
        QFile file(path);
        if (!file.open(QIODevice::ReadOnly | QIODevice::Text)){
            QMessageBox::warning(this, tr("Read File"), tr("Cannot open file:\n%1").arg(path));
            return;
        }
        QTextStream in(&file);
        textEdit->setText(in.readAll());
        file.close();
    } 
    else {
        QMessageBox::warning(this, tr("Path"), tr("You did not select any file."));
    }
}

//保存文件
void MainWindow::saveFile(){
    QString path = QFileDialog::getSaveFileName(this, tr("Open File"), ".", tr("Text Files(*.txt)"));
    if(!path.isEmpty()) {
        QFile file(path);
        if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
            QMessageBox::warning(this, tr("Write File"),tr("Cannot open file:\n%1").arg(path));
            return;
        }
        QTextStream out(&file);
        out << textEdit->toPlainText();
        file.close();
    } 
    else {
        QMessageBox::warning(this, tr("Path"),tr("You did not select any file."));
    }
}

openFile()函数中,我们使用QFileDialog::getOpenFileName()来获取需要打开的文件的路径。这个函数原型如下:

QString getOpenFileName(QWidget * parent = 0, const QString & caption = QString(), const QString & dir = QString(),
                        const QString & filter = QString(), QString * selectedFilter = 0,Options options = 0)

不过注意,它的所有参数都是可选的,因此在一定程度上说,这个函数也是简单的。这六个参数分别是:

  • parent:父窗口。

  • Qt 的标准对话框提供静态函数,用于返回一个模态对话框;

  • caption:对话框标题;

  • dir:对话框打开时的默认目录

    • “.” 代表程序运行目录
    • “/” 代表当前盘符的根目录(特指 Windows 平台;Linux 平台当然就是根目录),这个参数也可以是平台相关的,比如“C:\”等;
  • filter:过滤器。

    我们使用文件对话框可以浏览很多类型的文件,但是,很多时候我们仅希望打开特定类型的文件。比如,文本编辑器希望打开文本文件,图片浏览器希望打开图片文件。过滤器就是用于过滤特定的后缀名。如果我们使用“Image Files(.jpg .png)”,则只能显示后缀名是 jpg 或者 png 的文件。如果需要多个过滤器,使用“;;”分割,比如“JPEG Files(.jpg);;PNG Files(.png)”;

  • selectedFilter:默认选择的过滤器;

  • options:对话框的一些参数设定

    比如只显示文件夹等等,它的取值是enum QFileDialog::Option,每个选项可以使用 | 运算组合起来。

QFileDialog::getOpenFileName()返回值是选择的文件路径。我们将其赋值给 path。通过判断 path 是否为空,可以确定用户是否选择了某一文件。只有当用户选择了一个文件时,我们才执行下面的操作。

saveFile()中使用的QFileDialog::getSaveFileName()也是类似的。使用这种静态函数,在 Windows、Mac OS 上面都是直接调用本地对话框,但是 Linux 上则是QFileDialog自己的模拟。这暗示了,如果你不使用这些静态函数,而是直接使用QFileDialog进行设置,那么得到的对话框很可能与系统对话框的外观不一致。这一点是需要注意的。

7 布局管理器

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

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

  • 绝对定位就是一种最原始的定位方法:给出这个组件的坐标和长宽值。

    这样,Qt 就知道该把组件放在哪里以及如何设置组件的大小。但是这样做带来的一个问题是,如果用户改变了窗口大小,比如点击最大化按钮或者使用鼠标拖动窗口边缘,采用绝对定位的组件是不会有任何响应的。这也很自然,因为你并没有告诉 Qt,在窗口变化时,组件是否要更新自己以及如何更新。或者,还有更简单的方法:禁止用户改变窗口大小。但这总不是长远之计

  • 布局定位:你只要把组件放入某一种布局,布局由专门的布局管理器进行管理。当需要调整大小或者位置的时候,Qt 使用对应的布局管理器进行调整

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

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

  • QHBoxLayout:按照水平方向从左到右布局;
  • QVBoxLayout:按照竖直方向从上到下布局;
  • QGridLayout:在一个网格中进行布局,类似于 HTML 的 table;

7.1 系统提供的布局控件

img

这4个为系统给我们提供的布局的控件,但是使用起来不是非常的灵活,这里就不详细介绍了。

7.2 利用widget做布局

第二种布局方式是利用控件里的widget来做布局,在Containers中

img

在widget中的控件可以进行水平、垂直、栅格布局等操作,比较灵活。

再布局的同时我们需要灵活运用弹簧的特性让我们的布局更加的美观,下面是一个登陆窗口,利用widget可以搭建出如下登陆界面:

img

  • 设计登陆窗口
  • 有用户名 密码、编辑框、登陆和退出按钮
  • 灵活运用弹簧进行界面布局

弹簧的操作

image-20230513215542054image-20230513215554755image-20230513215612011

组件的操作(这个操作可以把高度固定到和组件差不多大小)

image-20230513215959780image-20230513220024792

image-20230513220211130

框和空间的距离也可以调整,按右边的图片调整

image-20230513220311718image-20230513220400473

这个可以设置密码格式

image-20230513220504296image-20230513220641058

image-20230513223400242

8 常用控件

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

8.1 QLabel控件使用

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

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

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

void setText(const QString &)
  • 可以显示普通文本字符串
QLabel * label = new QLabel(this);
label ->setText("Hello, World");
  • 可以显示HTML格式的字符串

    比如显示一个链接:

QLabel * label = new QLabel(this);
label ->setText("Hello, World");
label ->setText("<h1><a href=\"https://www.baidu.com\">百度一下</a></h1>");
label ->setOpenExternalLinks(true);

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

显示图片

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

void setPixmap(const QPixmap &)

首先定义QPixmap对象

QPixmap pixmap;

然后加载图片

pixmap.load(":/Image/boat.jpg");

最后将图片设置到QLabel中

QLabel *label = new QLabel;
label.setPixmap(pixmap);

显示动画

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

void setMovie(QMovie * movie)
//首先定义QMovied对象,并初始化:
QMovie *movie = new QMovie(":/Mario.gif");
//播放加载的动画:
movie->start();
//将动画设置到QLabel中:
QLabel *label = new QLabel;
label->setMovie(movie);

8.2 QLineEdit

Qt提供的单行文本编辑框。

设置/获取内容

  • 获取编辑框内容使用text(),函数声明如下:
QString	text() const
  • 设置编辑框内容
void setText(const QString &)

设置显示模式

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

void setEchoMode(EchoMode mode)

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

  • QLineEdit::Normal 模式显示方式,按照输入的内容显示。
  • QLineEdit::NoEcho 不显示任何内容,此模式下无法看到用户的输入。
  • QLineEdit::Password 密码模式,输入的字符会根据平台转换为特殊字符。
  • QLineEdit::PasswordEchoOnEdit 编辑时显示字符否则显示字符作为密码。

另外,我们再使用QLineEdit显示文本的时候,希望在左侧留出一段空白的区域,那么,就可以使用QLineEdit给我们提供的setTextMargins函数:

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

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

8.3 其他控件

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

8.3.1 按钮组

  • QPushButton 最常用按钮,里面可以加图片
  • QToolButton 工具按钮 用于显示图片
    toolButtonStyle 设置显示图片和文件出现与否
    autoRaise透明凸起效果
  • QRadioButton 单选按钮
    默认选中 setChecked(true)
  • QCheckBox 复选按钮
  • stateChanged 信号
    0 未选中
    1 半选中
    2 选中

Push Button简单按钮

b1dc8ec9f6ca4313a36e24b65b570189

Tool Button工具按钮

image-20240416171032942

上面的autoRaise 表示鼠标放上去能有一个突起的效果

5687bb7bdcc64879b26a9d356557a64a

复选框的半选中(全选中是2,半选中是1)

ui->setupUi(this);
//单选按钮 默认选中 男
ui->rbtn_Man->setChecked(true);		//rbtn_Man这个是对应的那个框的类名,可以自己改的

//点击女单选按钮 打印输出
connect(ui->radioButton_2, &QRadioButton::clicked,[=](){
    qDebug() << "选中了女的";
});

//复选按钮 点击环境好之后 监听
connect(ui->checkBox,&QCheckBox::stateChanged,[=](int state){
    qDebug() << state;
});

8.3.2 列表、树和表控件—Item Widgets(Item_based)

List Widget 列表控件

创建项 QListWidgetItem * item = new QListWidgetItem("锄禾日当午");
//将项加载到 控件中
ui->listWidget->addItem(item);
设置对齐方式 item->setTextAlignment(Qt::AlignHCenter);
ui->listWidget->addItems(list); 可以一次性放入所有内容

//listWidget
QListWidgetItem * item = new QListWidgetItem("锄禾日当午");	//创建项
ui->listWidget->addItem(item);								//将项加载到 控件中 
item->setTextAlignment(Qt::AlignHCenter);					//设置对齐方式 

5ccfd1ca04e245faa800cacff7de6b15

//QStringList   QList<QString>  list<String>
QStringList list;
list << "锄禾日当午"<< "汗滴禾下土"<< "谁知盘中餐"<< "粒粒皆辛苦";
ui->listWidget->addItems(list);

6444d3df77a7453bae5b8079e466fb9f

效果图 缺点就是无法设置对齐方式

tree Widget 树控件使用

设置头

ui->treeWidget->setHeaderLabels(QStringList()<<"英雄"<<"英雄简介");

创建根项目

QTreeWidgetItem * liItem = new QTreeWidgetItem(QStringList()<<"力量");

将根项目添加到树控件中

ui->treeWidget->addTopLevelItem(liItem);

挂载子项目

QTreeWidgetItem * l1 = new QTreeWidgetItem(heroL1);
liItem->addChild(l1);
//treeWidget树控件使用
//设置头
ui->treeWidget->setHeaderLabels(QStringList()<<"英雄"<<"英雄简介");       //QStringList()是匿名对象

//创建出力量根
QTreeWidgetItem * liItem = new QTreeWidgetItem(QStringList()<<"力量");
QTreeWidgetItem * minItem = new QTreeWidgetItem(QStringList()<<"敏捷");
QTreeWidgetItem * zhiItem = new QTreeWidgetItem(QStringList()<<"智力");
//将根放入到树中
ui->treeWidget->addTopLevelItem(liItem);
ui->treeWidget->addTopLevelItem(minItem);
ui->treeWidget->addTopLevelItem(zhiItem);

QStringList heroL1,heroL2;
heroL1 << "刚被猪" << "前排坦克,能在吸收伤害的同时造成可观的范围输出";
heroL2 << "船长" << "前排坦克,能肉能输出能控场的全能英雄";
QTreeWidgetItem * l1 = new QTreeWidgetItem(heroL1);
QTreeWidgetItem * l2 = new QTreeWidgetItem(heroL2);
liItem->addChild(l1);
liItem->addChild(l2);

QStringList heroM1,heroM2;
heroM1 << "月骑" << "中排物理输出,可以使用分裂利刃攻击多个目标";
heroM2 << "小鱼人" << "前排战士,擅长偷取敌人的属性来增强自身战力";
QTreeWidgetItem * m1 = new QTreeWidgetItem(heroM1);
QTreeWidgetItem * m2 = new QTreeWidgetItem(heroM2);
minItem->addChild(m1);
minItem->addChild(m2);

QStringList heroZ1,heroZ2;
heroZ1 << "死灵法师" << "前排法师坦克,魔法抗性较高,拥有治疗技能";
heroZ2 << "巫医" << "后排辅助法师,可以使用奇特的巫术诅咒敌人与治疗队友";
QTreeWidgetItem * z1 = new QTreeWidgetItem(heroZ1);
QTreeWidgetItem * z2 = new QTreeWidgetItem(heroZ2);
zhiItem->addChild(z1);
zhiItem->addChild(z2);

cdc640844d424e5da84c4fecb935b314

Table Widget 表格控件使用

设置列 ui->tableWidget->setColumnCount(3);
设置水平表头 ui->tableWidget->setHorizontalHeaderLabels(QStringList()<<"姓名"<<"性别"<<"年龄");
设置行数 ui->tableWidget->setRowCount(5);
设置正文 ui->tableWidget->setItem(行,列 , new QTableWidgetItem(“字符串”));

//tableWidget表格控件使用

//设置列数
ui->tableWidget->setColumnCount(3);

//设置水平表头
ui->tableWidget->setHorizontalHeaderLabels(QStringList()<<"姓名"<<"性别"<<"年龄");

//设置行数
ui->tableWidget->setRowCount(5);

QList<QString> nameList;
nameList << "亚瑟"<< "安琪拉"<<"妲己"<<"赵云"<< "孙悟空";
QStringList sexList;
sexList << "男"<< "女"<< "女"<< "男"<< "中立";


for(int i  = 0 ; i < 5;i++)    {
    int col = 0;
    ui->tableWidget->setItem(i,col++, new QTableWidgetItem(nameList[i]));
    ui->tableWidget->setItem(i,col++, new QTableWidgetItem(sexList.at(i)));

    //int 转 QString   QString::number(int)
    ui->tableWidget->setItem(i,col++, new QTableWidgetItem( QString::number(i+18)));

}

a5bdb4c058ba4b6da52be08675ae1760

8.3.3 containers

scrollArea
滚动条的功能,可以放一些按钮或者其他东西进去

bada696f714c4a42925a516c15fb99ec

toolBox
工具盒子,相当于每一个栏目里面可以放置,官方的话就是家人、朋友、黑名单页。每一页(可以打开)下面可以放很多东西。

a323870563a04df997f35d685f4ffa85

image-20240424204541245

tabWidget
在里面就像网页浏览中切换页面的情景。

image-20240424204657693

image-20240424204729858

stackedWidget
做页面切换的,一般与按钮配合使用,通过点击不同的工具按钮可以在栈中切换我们想要的页面

image-20240424204853004

//点击按钮 切换 stacked容器
ui->stackedWidget->setCurrentIndex(0);//默认使用第一个栈

connect(ui->btn_ScrollArea,&QPushButton::clicked,[=](){
       ui->stackedWidget->setCurrentIndex(0);
});

connect(ui->btn_ToolBox,&QPushButton::clicked,[=](){
       ui->stackedWidget->setCurrentIndex(1);
});

connect(ui->btn_TabWidget,&QPushButton::clicked,[=](){
       ui->stackedWidget->setCurrentIndex(2);
});

容器中就上面几个常用,其他的自己自己玩一玩

8.3.4 Input Widgets

comboBox

下拉框的操作,下面有个按钮可以点击选中拖拉机的选项。

//下拉框使用
ui->comboBox->addItem("奔驰");
ui->comboBox->addItem("宝马");
ui->comboBox->addItem("拖拉机");

//点击按钮 选中拖拉机下拉框内容
connect(ui->btn_select,&QPushButton::clicked,[=](){
//        ui->comboBox->setCurrentIndex(2);
        ui->comboBox->setCurrentText("拖拉机");
    });

ddbea25fac7a4893a6222ca96ff24113

fontComboBox

e32aa34059aa47a29aeb1a64928f4dc8

lineEdit
textEdit
plainTextEdit

a0786e748d9949acbde6d977d1d3eba2

spinBox
doubleSpinBox
timeEdit
dateEdit
dateTimeEdit
dial

f147e1103ac3479db44d30f92a370e19

8.3.5 Display Widgets

Label
Qlabel显示图片

ui->label_img->setPixmap(QPixmap(":/Image/butterfly.png"));

QLabel显示gif图片

QMovie * movie =new QMovie(":/Image/mario.gif");
ui->label_movie->setMovie(movie);
movie->start();

c90bd75b4d0d4a36a32321d430ed1047

8.4 自定义控件

  • 自定义控件封装

    • 右键项目- 添加新文件
    • Qt - Qt设计师界面类
    • 在ui文件中设计自定义控件
  • 在主窗口中 拖拽 Widget做提升的操作

  • 在自定义控件中 封装对外接口

    • SpinBox和 slider控件交互
    • getData获取显示数字
    • setData设置数字

33569a467ef144db95f74ec4bc65aff9

05894e4eab374bb08b59a7af6aca2199

5a357f8cf39043e3a70897630af7090a

8c23cf6c0bdb485c916871074db9a13a

060a0d552f624a078246d59ac9f174d3

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

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

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

// smallwidget.h
class SmallWidget : public QWidget{
    Q_OBJECT
public:
    explicit SmallWidget(QWidget *parent = 0);
signals:
    
public slots:
    
private:
    QSpinBox* spin;
    QSlider* slider;
};


// smallwidget.cpp
SmallWidget::SmallWidget(QWidget *parent) : QWidget(parent){
    spin = new QSpinBox(this);						//计数模块
    slider = new QSlider(Qt::Horizontal, this);		//水平的滑块

    // 创建布局对象
    QHBoxLayout* layout = new QHBoxLayout;
    // 将控件添加到布局中
    layout->addWidget(spin);
    layout->addWidget(slider);
    // 将布局设置到窗口中
    setLayout(layout);

    // 添加消息响应
    connect(spin, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), slider, &QSlider::setValue);
    connect(slider, &QSlider::valueChanged, spin, &QSpinBox::setValue);
}

img

那么这个SmallWidget可以作为独立的窗口显示,也可以作为一个控件来使用:

打开Qt的.ui文件,因为SmallWidget是派生自Qwidget类,所以需要在ui文件中先放入一个QWidget控件, 然后再上边鼠标右键

img

弹出提升窗口部件对话框

img

添加要提升的类的名字,然后选择 添加

img

添加之后,类名会显示到上边的列表框中,然后单击提升按钮,完成操作。我们可以看到, 这个窗口对应的类从原来的QWidget变成了SmallWidgetimg

再次运行程序,这个widget_3中就能显示出我们自定义的窗口了.

// smallwidget.h
#ifndef SMALLWIDGET_H
#define SMALLWIDGET_H

#include <QWidget>

namespace Ui {
	class SmallWidget;
}

class SmallWidget : public QWidget{
    Q_OBJECT

public:
    explicit SmallWidget(QWidget *parent = 0);
    ~SmallWidget();

    //设置值
    void setData(int val);
    //获取值
    int getData();

private:
    Ui::SmallWidget *ui;
};
#endif // SMALLWIDGET_H
// smallwidget.cpp
#include "smallwidget.h"
#include "ui_smallwidget.h"

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

    //修改SpinBox  右侧滚动条 跟着移动
    void(QSpinBox:: *spinBoxP)(int) = &QSpinBox::valueChanged;   //函数指针表示指定某个函数,防止有重载版本函数的干扰
    connect(ui->spinBox,spinBoxP , [=](int val){ui->horizontalSlider->setValue(val);});

    //右侧滚动条移动,左侧跟着改变数字
    connect(ui->horizontalSlider,&QSlider::valueChanged , [=](int val){ui->spinBox->setValue(val);});
}

//设置值
void SmallWidget::setData(int val){
    ui->horizontalSlider->setValue(val);
}

//获取值
int SmallWidget::getData(){
    return ui->horizontalSlider->value();
}

SmallWidget::~SmallWidget(){
    delete ui;
}
// widget.cpp
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>
Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);

    //点击设置按钮,将小控件设置到一半位置
    connect(ui->btn_set,&QPushButton::clicked,[=](){ui->widget->setData(50);});

    //点击获取按钮,获取当前显示数字
    connect(ui->btn_get,&QPushButton::clicked,[=](){qDebug() << ui->widget->getData();});
}

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

9 Qt消息机制和事件

9.1 事件

Qt鼠标常用事件

//鼠标进入事件
void enterEvent(QEvent *);
//鼠标离开事件
void leaveEvent(QEvent *);

//鼠标按下事件
void mousePressEvent(QMouseEvent *ev);
//鼠标释放事件
void mouseReleaseEvent(QMouseEvent *ev);
//鼠标移动事件
void mouseMoveEvent(QMouseEvent *ev);

获取 x y 坐标 ev->x() ev->y()
判断如果是左键 按下

ev->button()   移动  ev->buttons() &   //组合键 做判断

设置鼠标追踪状态

setMouseTracking(true);

image-20230817104152427

image-20230817104209143

image-20230817104219176

再修改mylabel.h中的东西。让他继承Qlabel中的东西

image-20230817104240445

再修改mylabel.cpp中的东西。让他继承Qlabel中的东西05eb649da8a94048b4da0e09b0353341

image-20230817104305972

事件(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 的,也就是说,我们可以在子类中重新实现这些函数。下面来看一个例子:

class EventLabel : public QLabel{
protected:
    void mouseMoveEvent(QMouseEvent *event);
    void mousePressEvent(QMouseEvent *event);
    void mouseReleaseEvent(QMouseEvent *event);
};

void EventLabel::mouseMoveEvent(QMouseEvent *event){
	this->setText(QString("<center><h1>Move: (%1, %2)</h1></center>").arg(QString::number(event->x()),
            QString::number(event->y())));
}

void EventLabel::mousePressEvent(QMouseEvent *event)
{
    this->setText(QString("<center><h1>Press:(%1, %2)</h1></center>").arg(QString::number(event->x()),
                QString::number(event->y())));
}

void EventLabel::mouseReleaseEvent(QMouseEvent *event){
    QString msg;
    msg.sprintf("<center><h1>Release: (%d, %d)</h1></center>",event->x(), event->y());
    this->setText(msg);
}

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    EventLabel *label = new EventLabel;
    label->setWindowTitle("MouseEvent Demo");
    label->resize(300, 200);
    label->show();
    return a.exec();
}
  • EventLabel继承了QLabel,覆盖了mousePressEvent()mouseMoveEvent()MouseReleaseEvent()三个函数。我们并没有添加什么功能,只是在鼠标按下(press)、鼠标移动(move)和鼠标释放(release)的时候,把当前鼠标的坐标值显示在这个Label上面。由于QLabel是支持 HTML 代码的,因此我们直接使用了 HTML 代码来格式化文字。
  • QString的arg()函数可以自动替换掉QString中出现的占位符。其占位符以 % 开始,后面是占位符的位置,例如 %1,%2 这种。
QString("[%1, %2]").arg(x).arg(y);  //语句将会使用x替换 %1,y替换 %2,因此,生成的QString为[x, y]。
  • mouseReleaseEvent()函数中,我们使用了另外一种QString的构造方法。我们使用类似 C 风格的格式化函数sprintf()来构造QString
    运行上面的代码,当我们点击了一下鼠标之后,label 上将显示鼠标当前坐标值。

为什么要点击鼠标之后才能在mouseMoveEvent()函数中显示鼠标坐标值?
这是因为QWidget中有一个mouseTracking属性,该属性用于设置是否追踪鼠标。只有鼠标被追踪时,mouseMoveEvent()才会发出。如果mouseTrackingfalse(默认即是),组件在至少一次鼠标点击之后,才能够被追踪,也就是能够发出mouseMoveEvent()事件。如果mouseTrackingtrue,则mouseMoveEvent()直接可以被发出。
知道了这一点,我们就可以在main()函数中添加如下代码:

label->setMouseTracking(true);

在运行程序就没有这个问题了。

9.2 event()事件分发器

image-20230817105527167

事件对象创建完毕后,Qt 将这个事件对象传递给QObjectevent()函数。event()函数并不直接处理事件,而是将这些事件对象按照它们不同的类型,分发给不同的事件处理器(event handler)。
如上所述,**event()函数主要用于事件的分发。所以,如果你希望在事件分发之前做一些操作,就可以重写这个event()函数了。例如,我们希望在一个QWidget组件中监听 tab 键的按下,那么就可以继承QWidget,并重写它的event()函数**,来达到这个目的:

bool CustomWidget::event(QEvent *e){
    if (e->type() == QEvent::KeyPress) {
        QKeyEvent *keyEvent = static_cast<QKeyEvent *>(e);
        if (keyEvent->key() == Qt::Key_Tab) {
            qDebug() << "You press tab.";
            return true;
        }
    }
    return QWidget::event(e);
}
  • CustomWidget是一个普通的QWidget子类。我们重写了它的event()函数,这个函数有一个QEvent对象作为参数,也就是需要转发的事件对象。函数返回值是 bool 类型。
  • 如果传入的事件已被识别并且处理,则需要返回 true,否则返回 false。如果返回值是 true,那么 Qt 会认为这个事件已经处理完毕,不会再将这个事件发送给其它对象,而是会继续处理事件队列中的下一事件。
  • event()函数中,调用事件对象的accept()ignore()函数是没有作用的,不会影响到事件的传播。

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

bool CustomTextEdit::event(QEvent *e){
   if (e->type() == QEvent::KeyPress) {
        QKeyEvent *keyEvent = static_cast<QKeyEvent *>(e);
       if (keyEvent->key() == Qt::Key_Tab) {
            qDebug() << "You press tab.";
            return true;
       }
    }
    return false;
}

CustomTextEditQTextEdit的一个子类。我们重写了其event()函数,却没有调用父类的同名函数。这样,我们的组件就只能处理 Tab 键,再也无法输入任何文本,也不能响应其它事件,比如鼠标点击之后也不会有光标出现。这是因为我们只处理的KeyPress类型的事件,并且如果不是KeyPress事件,则直接返回 false,鼠标事件根本不会被转发,也就没有了鼠标事件。
通过查看QObject::event()的实现,我们可以理解,event()函数同前面的章节中我们所说的事件处理器有什么联系:

//!!! Qt5
bool QObject::event(QEvent *e){
    switch (e->type()) {
    	case QEvent::Timer:
        	timerEvent((QTimerEvent*)e);
        	break;
        case QEvent::ChildAdded:
        case QEvent::ChildPolished:
        case QEvent::ChildRemoved:
            childEvent((QChildEvent*)e);
            break;
            // ...
        default:
            if (e->type() >= QEvent::User) {
                customEvent(e);
                break;
            }
            return false;
    }
    return true;
}

这是 Qt 5QObject::event()函数的源代码(Qt 4 的版本也是类似的)。我们可以看到,同前面我们所说的一样,Qt 也是使用QEvent::type()判断事件类型,然后调用了特定的事件处理器。比如,如果event->type()返回值是QEvent::Timer,则调用timerEvent()函数。可以想象,QWidget::event()中一定会有如下的代码:

switch (event->type()) {
    case QEvent::MouseMove:
        mouseMoveEvent((QMouseEvent*)event);
        break;
    // ...
}

事实也的确如此。timerEvent()mouseMoveEvent()这样的函数,就是我们前面章节所说的事件处理器 event handler。也就是说event()函数中实际是通过事件处理器来响应一个具体的事件。这相当于event()函数将具体事件的处理“委托”给具体的事件处理器。而这些事件处理器是 protected virtual 的,因此,我们重写了某一个事件处理器,即可让 Qt 调用我们自己实现的版本。

由此可以见,event()是一个集中处理不同类型的事件的地方。如果你不想重写一大堆事件处理器,就可以重写这个event()函数,通过QEvent::type()判断不同的事件。鉴于重写event()函数需要十分小心注意父类的同名函数的调用,一不留神就可能出现问题,所以一般还是建议只重写事件处理器(当然,也必须记得是不是应该调用父类的同名处理器)。这其实暗示了event()函数的另外一个作用:屏蔽掉某些不需要的事件处理器。正如我们前面的CustomTextEdit例子看到的那样,我们创建了一个只能响应 tab 键的组件。这种作用是重写事件处理器所不能实现的。

9.3 事件过滤器

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

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

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

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

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

我们来看一段简单的代码:

class MainWindow : public QMainWindow {
 public:
     MainWindow();
 protected:
     bool eventFilter(QObject *obj, QEvent *event);
 private:
     QTextEdit *textEdit;
 };

 MainWindow::MainWindow() {
     textEdit = new QTextEdit;
     setCentralWidget(textEdit);
     textEdit->installEventFilter(this);
 }

 bool MainWindow::eventFilter(QObject *obj, QEvent *event){
     if (obj == textEdit) {
         if (event->type() == QEvent::KeyPress) {
             QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
             qDebug() << "Ate key press" << keyEvent->key();
             return true;
         } else {
             return false;
         }
     } else {
         // pass the event on to the parent class
         return QMainWindow::eventFilter(obj, event);
     }
 }
  • MainWindow是我们定义的一个类。我们重写了它的eventFilter()函数。为了过滤特定组件上的事件,首先需要判断这个对象是不是我们感兴趣的组件,然后判断这个事件的类型。在上面的代码中,我们不想让textEdit组件处理键盘按下的事件。所以,首先我们找到这个组件,如果这个事件是键盘事件,则直接返回 true,也就是过滤掉了这个事件,其他事件还是要继续处理,所以返回 false。对于其它的组件,我们并不保证是不是还有过滤器,于是最保险的办法是调用父类的函数。
  • eventFilter()函数相当于创建了过滤器,然后我们需要安装这个过滤器。安装过滤器需要调用QObject::installEventFilter()函数。函数的原型如下:
void QObject::installEventFilter ( QObject * filterObj )

这个函数接受一个QObject *类型的参数。记得刚刚我们说的,eventFilter()函数是QObject的一个成员函数,因此,任意QObject都可以作为事件过滤器(问题在于,如果你没有重写eventFilter()函数,这个事件过滤器是没有任何作用的,因为默认什么都不会过滤)。已经存在的过滤器则可以通过QObject::removeEventFilter()函数移除。

  • 我们可以向一个对象上面安装多个事件处理器,只要调用多次installEventFilter()函数。如果一个对象存在多个事件过滤器,那么,最后一个安装的会第一个执行,也就是后进先执行的顺序。

还记得我们前面的那个例子吗?我们使用event()函数处理了 Tab 键:

bool CustomWidget::event(QEvent *e){
    if (e->type() == QEvent::KeyPress) {
        QKeyEvent *keyEvent = static_cast<QKeyEvent *>(e);
        if (keyEvent->key() == Qt::Key_Tab) {
            qDebug() << "You press tab.";
            return true;
        }
    }
    return QWidget::event(e);
}

现在,我们可以给出一个使用事件过滤器的版本:

bool FilterObject::eventFilter(QObject *object, QEvent *event){
    if (object == target && event->type() == QEvent::KeyPress) {
        QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
        if (keyEvent->key() == Qt::Key_Tab) {
            qDebug() << "You press tab.";
            return true;
        } else {
            return false;
        }
    }
    return false;
}

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

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

9.4 总结

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

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

  • 所有事件对应一个事件处理函数,在这个事件处理函数中用一个很大的分支语句进行选择,其代表作就是 win32 API 的WndProc()函数:
LRESULT CALLBACK WndProc(HWND hWnd,
                         UINT message,
                         WPARAM wParam,
                         LPARAM lParam)

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

switch(message)
{
    case WM_PAINT:
        // ...
        break;
    case WM_DESTROY:
        // ...
        break;
    ...
}

每一种事件对应一个事件处理函数。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()函数的两点不足:

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

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

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

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

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

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

9.5 案列源码

9.5.1 Qt鼠标常用事件

//鼠标进入事件
void enterEvent(QEvent *);
//鼠标离开事件
void leaveEvent(QEvent *);

//鼠标按下事件
void mousePressEvent(QMouseEvent *ev);
//鼠标释放事件
void mouseReleaseEvent(QMouseEvent *ev);
//鼠标移动事件
void mouseMoveEvent(QMouseEvent *ev);

获取 x y 坐标 ev->x() ev->y()

QString str =  QString("鼠标移动了 x =  %1  y = %2 " ).arg(ev->x()).arg(ev->y());
ev->button()   移动  ev->buttons() &   //组合键 做判断
if(ev->buttons() & Qt::LeftButton)    //判断如果是左键 按下

设置鼠标追踪状态

setMouseTracking(true);

9.5.2 定时器

定时器1

定时器事件 void timerEvent( QTimerEvent * e)
启动定时器 id1 = startTimer(毫秒)
判断具体定时器标准 e->timerId() == id1

定时器2

通过定时器类实现 QTimer
创建定时器对象 QTimer * timer1 = new QTimer(this);
开启定时器 timer1->start(x毫秒)
每隔x毫秒 会抛出一个信号timeout出来
监听信号处理逻辑
暂停定时器 timer1->stop();

9.5.3 事件分发器

用途:用于分发事件
原型 bool event(QEvent * e);
返回值如果是 true代表用户自己处理事件,不在向下分发

9.5.4 事件过滤器

  • 步骤1 给控件安装过滤器 ui->label->installEventFilter(this);
  • 步骤2 重写过滤器事件 bool Widget::eventFilter(QObject * obj , QEvent * e)
// mylabel.h
#ifndef MYLABEL_H
#define MYLABEL_H
#include <QLabel>

class MyLabel : public QLabel{
    Q_OBJECT
public:
    explicit MyLabel(QWidget *parent = 0);

    //鼠标进入事件
    void enterEvent(QEvent *);
    //鼠标离开事件
    void leaveEvent(QEvent *);
    
    //鼠标按下事件
    void mousePressEvent(QMouseEvent *ev);
    //鼠标释放事件
    void mouseReleaseEvent(QMouseEvent *ev);
    //鼠标移动事件
    void mouseMoveEvent(QMouseEvent *ev);
    
    //事件分发器
    bool event(QEvent *e);

signals:

public slots:
};

#endif // MYLABEL_H
// widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
namespace Ui {class Widget;}

class Widget : public QWidget{
    Q_OBJECT
public:
    explicit Widget(QWidget *parent = 0);
    ~Widget();

    //添加定时器事件
    void timerEvent(QTimerEvent *);
    
    //第一个定时器 标志
    int id1;
    //第二个定时器 标志
    int id2;
    
    //重写过滤器事件
    bool eventFilter(QObject *, QEvent *);

private:
    Ui::Widget *ui;
};

#endif // WIDGET_H
// mylabel.cpp
#include "mylabel.h"
#include <QDebug>
#include <QMouseEvent>
MyLabel::MyLabel(QWidget *parent) : QLabel(parent){
    //设置鼠标追踪
   // setMouseTracking(true);
}

//鼠标进入事件
void MyLabel::enterEvent(QEvent *){
//    qDebug() << "鼠标进入了";
}
//鼠标离开事件
void MyLabel::leaveEvent(QEvent *){
//     qDebug() << "鼠标离开了";
}

//鼠标按下事件
void MyLabel::mousePressEvent(QMouseEvent *ev){     //鼠标中的所有的参数都放在ev里 
    //鼠标左键按下  打印信息
//    if(ev->button() == Qt::LeftButton){
        QString str =  QString("鼠标按下了 x =  %1  y = %2 " ).arg(ev->x()).arg(ev->y());        //字符串拼接
        qDebug() << str;
//    }
}
//鼠标释放事件
void MyLabel::mouseReleaseEvent(QMouseEvent *ev){
    if(ev->button() == Qt::LeftButton){
        QString str =  QString("鼠标释放了 x =  %1  y = %2 " ).arg(ev->x()).arg(ev->y());
        qDebug() << str;
    }

}
//鼠标移动事件
void MyLabel::mouseMoveEvent(QMouseEvent *ev){
//    if(ev->buttons() & Qt::LeftButton){
        QString str =  QString("鼠标移动了 x =  %1  y = %2 " ).arg(ev->x()).arg(ev->y());
        qDebug() <<str;
//    }
}

//事件分发器 :用途,分发事件
bool MyLabel::event(QEvent *e){
    if( e->type() == QEvent::MouseButtonPress)    {
        //如果是鼠标按下  拦截事件,不向下分发
        QMouseEvent *ev = static_cast<QMouseEvent *>(e);

        QString str =  QString("event中  鼠标按下了 x =  %1  y = %2 " ).arg(ev->x()).arg(ev->y());
        qDebug() << str;
    
        return true;
    }
    
    //其他事件  抛给父类去处理
    return QLabel::event(e);
}
// widget.cpp
#include "widget.h"
#include "ui_widget.h"
#include <QTimer> //定时器类
#include <QMouseEvent>
#include <QDebug>

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

    //开启定时器方法1,不推荐
    id1 = startTimer(1000); //单位 毫秒
    id2 = startTimer(2000); //单位 毫秒
    
    //创建定时器对象方法2,推荐
    QTimer * timer1 = new QTimer(this);
    timer1->start(500);
    
    //每隔0.5秒 会抛出一个信号timeout出来
    connect(timer1,&QTimer::timeout , [=](){
        //每隔0.5秒中 让label_4数字++
        static int num = 1;
        ui->label_4->setText( QString::number(num++) );
    });
    
    //点击暂停按钮  停止定时器
    connect(ui->pushButton,&QPushButton::clicked,[=](){timer1->stop();});
    
    //给label添加事件过滤器 ,做更高级的拦截操作
    //1、给控件安装过滤器
    ui->label->installEventFilter(this);
}

//重写过滤器事件
bool Widget::eventFilter(QObject * obj , QEvent * e){
    if(obj == ui->label){
        if( e->type() == QEvent::MouseButtonPress){
            //如果是鼠标按下  拦截事件,不向下分发
            QMouseEvent *ev = static_cast<QMouseEvent *>(e);

            QString str =  QString("事件过滤器  鼠标按下了 x =  %1  y = %2 " ).arg(ev->x()).arg(ev->y());
            qDebug() << str;
    
            return true;
        }
    }
    
    //其他事件抛给父类
    return QWidget::eventFilter(obj,e);

}


//添加定时器事件
void Widget::timerEvent(QTimerEvent * e){
    if(e->timerId() == id1){
        //每隔1秒中 让label_2数字++
        static int num = 1;
        ui->label_2->setText( QString::number(num++) );
    }
    if(e->timerId() == id2){
        //每隔2秒让label_3数字++
        static int num2 = 1;
        ui->label_3->setText( QString::number(num2++) );
    }
}

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

image-20230817113521284

image-20230817113534494

10 绘图和绘图设备

10.1 QPainter

Qt 的绘图系统允许使用相同的 API 在屏幕和其它打印设备上进行绘制。整个绘图系统基于QPainterQPainterDeviceQPaintEngine三个类。

  • QPainter用来执行绘制的操作;
  • QPaintDevice是一个二维空间的抽象,这个二维空间允许QPainter在其上面进行绘制,也就是QPainter工作的空间;
  • QPaintEngine提供了画笔(QPainter)在不同的设备上进行绘制的统一的接口。QPaintEngine类应用于QPainter和QPaintDevice之间,通常对开发人员是透明的。除非你需要自定义一个设备,否则你是不需要关心QPaintEngine这个类的。我们可以把QPainter理解成画笔;把QPaintDevice理解成使用画笔的地方,比如纸张、屏幕等;而对于纸张、屏幕而言,肯定要使用不同的画笔绘制,为了统一使用一种画笔,我们设计了QPaintEngine类,这个类让不同的纸张、屏幕都能使用一种画笔。
class Widget : public QWidget{
    Q_OBJECT
public:
    explicit Widget(QWidget *parent = 0);
    ~Widget();

    //绘图事件
    void paintEvent(QPaintEvent *);
    //x坐标
    int posX;

private:
    Ui::Widget *ui;
};

//绘图事件
void Widget::paintEvent(QPaintEvent *){
    QPainter painter(this);     //这个this表示往这上面画东西
    //画线
    painter.drawLine(QPoint(0,0),QPoint(100,100));
    //画圆
    painter.drawEllipse( QPoint(100,100) , 50,50);    //两个50表示长半轴和短半轴
    //画矩形
    painter.drawRect(QRect(20,20,50,50));
    //写字
    painter.drawText(QRect(10,200,150,100),"好好学习,天天向上");
}

0d4788c0c8604362bbea747f792d043f

 //设置画笔
QPen pen(QColor(255,0,0));
//设置画笔宽度
pen.setWidth(3);
//设置风格
pen.setStyle(Qt::DotLine);
painter.setPen(pen);      //拿起这个画笔

//画刷  可以使封闭图形填充颜色(Qt::cyan表示青色)
QBrush brush(Qt::cyan);
//设置风格
brush.setStyle(Qt::Dense4Pattern);
painter.setBrush(brush);  //设置填充色

//画线
painter.drawLine(QPoint(0,0),QPoint(100,100));
//画圆
painter.drawEllipse( QPoint(100,100) , 50,50);    //两个50表示长半轴和短半轴
//画矩形
painter.drawRect(QRect(20,20,50,50));
//写字
painter.drawText(QRect(10,200,150,100),"好好学习,天天向上");

816bfa7b0d754c96b63070ca08e64e10

painter.drawEllipse(QPoint(100,100) , 50,50);

//设置抗锯齿能力   效率会稍微比低
painter.setRenderHint(QPainter::Antialiasing);

painter.drawEllipse(QPoint(250,100) , 50,50);

35a5f0ec63d44a3c979639d9e60a83ea

painter.drawRect(QRect(20,20,50,50));

//移动画家
painter.translate(100,0);

//保存画家状态
painter.save();

painter.drawRect(QRect(20,20,50,50));

painter.translate(100,0);

//取出画家状态
painter.restore();

painter.drawRect(QRect(20,20,50,50));

image-20240424212345421

#include <QPainter>
#include <QTimer>
Widget::Widget(QWidget *parent):QWidget(parent),ui(new Ui::Widget){
    ui->setupUi(this);

    //点击移动按钮  让图片移动
    posX = 0;
    connect(ui->pushButton,&QPushButton::clicked,[=](){
        posX += 20;
        update();//手动调用绘图事件
    });

    QTimer * timer = new QTimer(this);
    timer->start(10);

    connect(timer,&QTimer::timeout,[=](){
         posX ++;
         update();
    });
}


//绘图事件
void Widget::paintEvent(QPaintEvent *){
    QPainter painter(this);     //这个this表示往这上面画东西

    //画资源图片
    QPixmap pix(":/Image/Luffy.png");

    //如果超出屏幕,强制变为 图片的负位置
    if(posX > this->width()){
        posX = -pix.width();
    }
    painter.drawPixmap(posX,0,pix);    //指定一个位置画出来什么东西


    //如果超出屏幕,强制变为0位置
//    if(posX > this->width()){
//        posX = 0;
//    }
//    painter.drawPixmap(posX,0,QPixmap(":/Image/Luffy.png"));
}

d7bf3f5eeb4241a592eb2ec1718f1201

下图给出了这三个类之间的层次结构:

c655d000518c42c88e1709ac4e359fd2

上面的示意图告诉我们,Qt 的绘图系统实际上是,使用QPainterQPainterDevice上进行绘制,它们之间使用QPaintEngine进行通讯(也就是翻译QPainter的指令)。
下面我们通过一个实例来介绍QPainter的使用:

class PaintedWidget : public QWidget{
    Q_OBJECT
public:
    PaintedWidget(QWidget *parent = 0);
protected:
    void paintEvent(QPaintEvent *);
}

注意我们重写了QWidget的paintEvent()函数。接下来就是PaintedWidget的源代码:

PaintedWidget::PaintedWidget(QWidget *parent) : QWidget(parent){
    resize(800, 600);
    setWindowTitle(tr("Paint Demo"));
}

void PaintedWidget::paintEvent(QPaintEvent *){
    QPainter painter(this);
    painter.drawLine(80, 100, 650, 500);
    painter.setPen(Qt::red);
    painter.drawRect(10, 10, 100, 400);
    painter.setPen(QPen(Qt::green, 5));
    painter.setBrush(Qt::blue);
    painter.drawEllipse(50, 150, 400, 200);
}

在构造函数中,我们仅仅设置了窗口的大小和标题。而paintEvent()函数则是绘制的代码。

首先,我们在栈上创建了一个QPainter对象,也就是说,每次运行paintEvent()函数的时候,都会重建这个QPainter对象。注意,这一点可能会引发某些细节问题:由于我们每次重建QPainter,因此第一次运行时所设置的画笔颜色、状态等,第二次再进入这个函数时就会全部丢失。有时候我们希望保存画笔状态,就必须自己保存数据,否则的话则需要将QPainter作为类的成员变量。

QPainter接收一个QPaintDevice指针作为参数。QPaintDevice有很多子类,比如QImage,以及QWidget。注意回忆一下,QPaintDevice可以理解成要在哪里去绘制,而现在我们希望画在这个组件,因此传入的是 this 指针。

QPainter有很多以 draw 开头的函数,用于各种图形的绘制,比如这里的drawLine()drawRect()以及drawEllipse()等。当绘制轮廓线时,使用QPainterpen()属性。
比如,我们调用了painter.setPen(Qt::red)pen 设置为红色,则下面绘制的矩形具有红色的轮廓线。接下来,我们将 pen 修改为绿色,5 像素宽(painter.setPen(QPen(Qt::green, 5))),又设置了画刷为蓝色。这时候再调用 draw 函数,则是具有绿色 5 像素宽轮廓线、蓝色填充的椭圆。

10.2 绘图设备

绘图设备是指继承QPainterDevice的子类。Qt提供了很多这样的类,例如QPixmapQBitmapQImageQPicture。其中,

  • QPixmap专门为图像在屏幕上的显示做了优化
  • QBitmap是QPixmap的一个子类,它的色深限定为1,可以使用 QPixmap的isQBitmap()函数来确定这个QPixmap是不是一个QBitmap。
  • QImage专门为图像的像素级访问做了优化。
  • QPicture则可以记录和重现QPainter的各条命令。

10.2.1 QPixmap、QBitmap、QImage

QPixmap继承了QPaintDevice,因此,你可以使用QPainter直接在上面绘制图形。QPixmap也可以接受一个字符串作为一个文件的路径来显示这个文件,比如你想在程序之中打开png、jpeg之类的文件,就可以使用 QPixmap使用QPainterdrawPixmap()函数可以把这个文件绘制到一个QLabelQPushButton或者其他的设备上面。QPixmap是针对屏幕进行特殊优化的,因此,它与实际的底层显示设备息息相关。注意,这里说的显示设备并不是硬件,而是操作系统提供的原生的绘图引擎。所以,在不同的操作系统平台下,QPixmap的显示可能会有所差别

QBitmap继承自QPixmap,因此具有QPixmap的所有特性,提供单色图像QBitmap的色深始终为1. 色深这个概念来自计算机图形学,是指用于表现颜色的二进制的位数。我们知道,计算机里面的数据都是使用二进制表示的。为了表示一种颜色,我们也会使用二进制。比如我们要表示8种颜色,需要用3个二进制位,这时我们就说色深是3. 因此,所谓色深为1,也就是使用1个二进制位表示颜色。1个位只有两种状态:0和1,因此它所表示的颜色就有两种,黑和白。所以说,**QBitmap实际上是只有黑白两色的图像数据。**

由于QBitmap色深小,因此只占用很少的存储空间,所以适合做光标文件和笔刷。
下面我们来看同一个图像文件在QPixmapQBitmap下的不同表现:

void PaintWidget::paintEvent(QPaintEvent *)
{
    QPixmap pixmap(":/Image/butterfly.png");
    QPixmap pixmap1(":/Image/butterfly1.png");
    QBitmap bitmap(":/Image/butterfly.png");
    QBitmap bitmap1(":/Image/butterfly1.png");

    QPainter painter(this);
    painter.drawPixmap(0, 0, pixmap);
    painter.drawPixmap(200, 0, pixmap1);
    painter.drawPixmap(0, 130, bitmap);
    painter.drawPixmap(200, 130, bitmap1);
}

bbee1ea68b31423ea899f56a197f81e7

这里我们给出了两张png图片。butterfly1.png是没有透明色的纯白背景,而butterfly.png是具有透明色的背景。我们分别使用QPixmap和QBitmap来加载它们。注意看它们的区别:白色的背景在QBitmap中消失了,而透明色在QBitmap中转换成了黑色;其他颜色则是使用点的疏密程度来体现的。

//QPixmap做绘图设备  对不同平台做了显示的优化
QPixmap pix(300,300);
pix.fill(Qt::white);      //填充白色,不写这个的话就是默认填充黑色

QPainter painter(&pix);                           //创建一个画家,他的画板就是QPixmap的一个对象

painter.setPen( QPen(Qt::red) );                  //设置画笔为红色
painter.drawEllipse(QPoint(150,150) , 100,100);   //画个圆
pix.save("D:\\pix.png");                          //保存图片

e815957ad91642d3845c5efb47021654

//QImage做绘图设备   不同平台下显示效果一样,可以对像素的访问做了优化
QImage img(300,300,QImage::Format_RGB32);
img.fill(Qt::white);      //填充白色,不写这个的话就是默认填充黑色

QPainter painter(&img);
painter.setPen( QPen(Qt::green) );
painter.drawEllipse(QPoint(150,150) , 100,100);
img.save("D:\\img.png");

3e9d9d2bf5fb48209008ef8902f5fc24

void Widget::paintEvent(QPaintEvent *){
    QImage img;
    img.load(":/Image/Luffy.png");

    QPainter painter(this);
    //修改像素点
    for(int i  = 100;i< 150;i++){
        for(int j = 100;j< 150;j++){
           QRgb value  = qRgb(255,0,0);
           img.setPixel(i,j,value);
        }
    }
    painter.drawImage(0,0,img);

}

dda423ba778e488aad883ae03f57b26f

QPixmap使用底层平台的绘制系统进行绘制,无法提供像素级别的操作,而QImage则是使用独立于硬件的绘制系统,实际上是自己绘制自己,因此提供了像素级别的操作,并且能够在不同系统之上提供一个一致的显示形式。

我们声明了一个QImage对象,大小是300 x 300,颜色模式是RGB32,即使用32位数值表示一个颜色的RGB值,也就是说每种颜色使用8位。然后我们对每个像素进行颜色赋值,从而构成了这个图像。我们可以把QImage想象成一个RGB颜色的二维数组,记录了每一像素的颜色。

void PaintWidget::paintEvent(QPaintEvent *){
    QPainter painter(this);
    QImage image(300, 300, QImage::Format_RGB32);
    QRgb value;

    //将图片背景填充为白色
    image.fill(Qt::white);
    
    //改变指定区域的像素点的值
    for(int i=50; i<100; ++i){
        for(int j=50; j<100; ++j){
            value = qRgb(255, 0, 0); // 红色
            image.setPixel(i, j, value);
        }
    }
    
    //将图片绘制到窗口中
    painter.drawImage(QPoint(0, 0), image);

}

72461f9eeab74d6eb331a325a536dd7b

QImageQPixmap的区别

  • QPixmap主要是用于绘图,针对屏幕显示而最佳化设计,**QImage主要是为图像I/O、图片访问和像素修改而设计的**
  • QPixmap依赖于所在的平台的绘图引擎,故例如反锯齿等一些效果在不同的平台上可能会有不同的显示效果,QImage使用Qt自身的绘图引擎,可在不同平台上具有相同的显示效果
  • 由于QImage是独立于硬件的,也是一种QPaintDevice因此我们可以在另一个线程中对其进行绘制,而不需要在GUI线程中处理,使用这一方式可以很大幅度提高UI响应速度。
  • QImage可通过setPixpel()pixel()等方法直接存取指定的像素。
  • QImage与QPixmap之间的转换:
    • QImage转QPixmap
// 使用QPixmap的静态成员函数: fromImage()
QPixmap	fromImage(const QImage & image, Qt::ImageConversionFlags flags = Qt::AutoColor)
    • QPixmap转QImage:
使用QPixmap类的成员函数: toImage()
QImage toImage() const

10.2.2 QPicture(重现命令)

最后一个需要说明的是QPicture这是一个可以记录和重现QPainter命令的绘图设备QPictureQPainter的命令序列化到一个IO设备,保存为一个平台独立的文件格式。这种格式有时候会是“元文件(meta- files)”。Qt的这种格式是二进制的,不同于某些本地的元文件,Qt的pictures文件没有内容上的限制,只要是能够被QPainter绘制的元素,不论是字体还是pixmap,或者是变换,都可以保存进一个picture中。

**QPicture是平台无关的**,因此它可以使用在多种设备之上,比如svg、pdf、ps、打印机或者屏幕。回忆下我们这里所说的QPaintDevice,实际上是说可以有QPainter绘制的对象。QPicture使用系统的分辨率,并且可以调整 QPainter来消除不同设备之间的显示差异。
如果我们要记录下QPainter的命令,首先要使用QPainter::begin()函数,将QPicture实例作为参数传递进去,以便告诉系统开始记录,记录完毕后使用QPainter::end()命令终止。代码示例如下:

Widget::Widget(QWidget *parent):QWidget(parent),ui(new Ui::Widget){
    ui->setupUi(this);
    
    //QPicture  记录和重现绘图指令
    QPicture pic;
    QPainter painter;
    
    painter.begin(&pic);
    painter.setPen( QPen(Qt::blue) );
    painter.drawEllipse(QPoint(150,150) , 100,100);
    painter.end();
    
    pic.save("E:\\pic.zt");

}

void Widget::paintEvent(QPaintEvent *){
    QImage img;
    img.load(":/Image/Luffy.png");
    QPainter painter(this);
    //重现QPicture命令
    QPicture pic;
    pic.load("E:\\pic.zt");
    painter.drawPicture(0,0,pic);
}

11 文件系统

文件操作是应用程序必不可少的部分。Qt 作为一个通用开发库,提供了跨平台的文件操作能力。Qt 通过QIODevice提供了对 I/O 设备的抽象,这些设备具有读写字节块的能力。下面是 I/O 设备的类图(Qt5):

7500b4ebddb24da0b9d3b7921df6775d

  • QIODevice:所有 I/O 设备类的父类,提供了字节块读写的通用操作以及基本接口;
  • QFileDevice:Qt5新增加的类,提供了有关文件操作的通用实现。
  • QFlie:访问本地文件或者嵌入资源;
  • QTemporaryFile:创建和访问本地文件系统的临时文件;
  • QBuffer:读写QbyteArray, 内存文件;
  • QProcess:运行外部程序,处理进程间通讯;
  • QAbstractSocket:所有套接字类的父类;
  • QTcpSocket:TCP协议网络数据传输;
  • QUdpSocket:传输 UDP 报文;
  • QSslSocket:使用 SSL/TLS 传输数据;

11.1文件系统分类:

  • 顺序访问设备:
    是指它们的数据只能访问一遍:从头走到尾,从第一个字节开始访问,直到最后一个字节,中途不能返回去读取上一个字节,这其中,QProcessQTcpSocketQUdpSoctetQSslSocket是顺序访问设备。
  • 随机访问设备:
    可以访问任意位置任意次数,还可以使用QIODevice::seek()函数来重新定位文件访问位置指针,QFileQTemporaryFileQBuffer是随机访问设备,

11.1 基本文件操作

文件操作是应用程序必不可少的部分。Qt 作为一个通用开发库,提供了跨平台的文件操作能力。在所有的 I/O 设备中,文件 I/O 是最重要的部分之一。因为我们大多数的程序依旧需要首先访问本地文件(当然,在云计算大行其道的将来,这一观点可能改变)。QFile提供了从文件中读取和写入数据的能力。

我们通常会将文件路径作为参数传给QFile的构造函数。不过也可以在创建好对象最后,使用setFileName()来修改QFile需要使用 / 作为文件分隔符,不过,它会自动将其转换成操作系统所需要的形式。例如 C:/windows 这样的路径在 Windows 平台下同样是可以的。

QFile主要提供了有关文件的各种操作,比如打开文件、关闭文件、刷新文件等。我们可以使用QDataStreamQTextStream类来读写文件,也可以使用QIODevice类提供的read()readLine()readAll()以及write()这样的函数。值得注意的是,有关文件本身的信息,比如文件名、文件所在目录的名字等,则是通过QFileInfo获取,而不是自己分析文件路径字符串。
下面我们使用一段代码来看看QFile的有关操作:

int main(int argc, char *argv[]){
    QApplication app(argc, argv);

    QFile file("in.txt");
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        qDebug() << "Open file failed.";
        return -1;
    } else {
        while (!file.atEnd()) {
            qDebug() << file.readLine();
        }
    }
     
    QFileInfo info(file);
    qDebug() << info.isDir();
    qDebug() << info.isExecutable();
    qDebug() << info.baseName();
    qDebug() << info.completeBaseName();
    qDebug() << info.suffix();
    qDebug() << info.completeSuffix(); 
    return app.exec();
}

我们首先使用QFile创建了一个文件对象。
这个文件名字是 in.txt。如果你不知道应该把它放在哪里,可以使用QDir::currentPath()来获得应用程序执行时的当前路径。只要将这个文件放在与当前路径一致的目录下即可。
使用open()函数打开这个文件,打开形式是只读方式,文本格式。
这个类似于fopen()r 这样的参数。open()函数返回一个 bool 类型,如果打开失败,我们在控制台输出一段提示然后程序退出。否则,我们利用 while 循环,将每一行读到的内容输出。
可以使用QFileInfo获取有关该文件的信息。
QFileInfo有很多类型的函数,我们只举出一些例子。比如:

  • isDir()检查该文件是否是目录;

  • isExecutable() 检查该文件是否是可执行文件等。

  • baseName() 可以直接获得文件名;

  • completeBaseName() 获取完整的文件名

  • suffix() 则直接获取文件后缀名。

  • completeSuffix() 获取完整的文件后缀

    我们可以由下面的示例看到,baseName()和completeBaseName(),以及suffix()和completeSuffix()的区别:

QFileInfo fi("/tmp/archive.tar.gz");
QString base  = fi.baseName();  // base = "archive"
QString base  = fi.completeBaseName();  // base = "archive.tar"
QString ext   = fi.suffix();  // ext = "gz"
QString ext   = fi.completeSuffix();  // ext = "tar.gz"

11.2 二进制文件读写

QDataStream提供了基于QIODevice的二进制数据的序列化。数据流是一种二进制流,这种流完全不依赖于底层操作系统、CPU 或者字节顺序(大端或小端)。例如,在安装了 Windows 平台的 PC 上面写入的一个数据流,可以不经过任何处理,直接拿到运行了 SolarisSPARC 机器上读取。由于数据流就是二进制流,因此我们也可以直接读写没有编码的二进制数据,例如图像、视频、音频等
**QDataStream既能够存取 C++ 基本类型,如 int、char、short 等,也可以存取复杂的数据类型,例如自定义的类。实际上,QDataStream对于类的存储,是将复杂的类分割为很多基本单元实现的**。
结合QIODeviceQDataStream可以很方便地对文件、网络套接字等进行读写操作。我们从代码开始看起:

QFile file("file.dat");
file.open(QIODevice::WriteOnly);
QDataStream out(&file);
out << QString("the answer is");
out << (qint32)42;

在这段代码中,我们首先打开一个名为 file.dat 的文件(注意,我们为简单起见,并没有检查文件打开是否成功,这在正式程序中是不允许的)。然后,我们将刚刚创建的file对象的指针传递给一个QDataStream实例out。类似于std::cout标准输出流,QDataStream也重载了输出重定向<<运算符。后面的代码就很简单了:将“the answer is”和数字 42 输出到数据流。由于我们的 out 对象建立在file之上,因此相当于将问题和答案写入file。
需要指出一点:最好使用 Qt 整型来进行读写,比如程序中的qint32。这保证了在任意平台和任意编译器都能够有相同的行为。

如果你直接运行这段代码,你会得到一个空白的 file.dat,并没有写入任何数据。这是因为我们的file没有正常关闭。为性能起见,数据只有在文件关闭时才会真正写入。因此,我们必须在最后添加一行代码:

file.close(); // 如果不想关闭文件,可以使用 file.flush();

接下来我们将存储到文件中的答案取出来

QFile file("file.dat");
file.open(QIODevice::ReadOnly);
QDataStream in(&file);
QString str;
qint32 a;
in >> str >> a;

唯一需要注意的是,你必须按照写入的顺序,将数据读取出来。顺序颠倒的话,程序行为是不确定的,严重时会直接造成程序崩溃
那么,既然QIODevice提供了read()readLine()之类的函数,为什么还要有QDataStream呢?QDataStreamQIODevice有什么区别?区别在于,**QDataStream提供流的形式,性能上一般比直接调用原始 API 更好一些**。我们通过下面一段代码看看什么是流的形式:

QFile file("file.dat");
file.open(QIODevice::ReadWrite);

QDataStream stream(&file);
QString str = "the answer is 42";

stream << str;

11.3 文本文件读写

上一节我们介绍了有关二进制文件的读写。二进制文件比较小巧,却不是人可读的格式。而文本文件是一种人可读的文件。为了操作这种文件,我们需要使用QTextStream类。QTextStreamQDataStream的使用类似,只不过它是操作纯文本文件的。

QTextStream会自动将 Unicode 编码同操作系统的编码进行转换,这一操作对开发人员是透明的。它也会将换行符进行转换,同样不需要自己处理。**QTextStream使用 16 位的QChar作为基础的数据存储单位,同样,它也支持 C++ 标准类型,如 int 等。实际上,这是将这种标准类型与字符串进行了相互转换。**
QTextStreamQDataStream的使用基本一致,例如下面的代码将把“The answer is 42”写入到 file.txt 文件中:

QFile data("file.txt");
if (data.open(QFile::WriteOnly | QIODevice::Truncate)) 
{
    QTextStream out(&data);
    out << "The answer is " << 42;
}

这里,我们在open()函数中增加了QIODevice::Truncate打开方式。我们可以从下表中看到这些打开方式的区别:

枚举值 描述
QIODevice::NotOpen 未打开
QIODevice::ReadOnly 以只读方式打开
QIODevice::WriteOnly 以只写方式打开
QIODevice::ReadWrite 以读写方式打开
QIODevice::Append 以追加的方式打开,新增加的内容将被追加到文件末尾
QIODevice::Truncate 以重写的方式打开,在写入新的数据时会将原有数据全部清除,游标设置在文件开头。
QIODevice::Text 在读取时,将行结束符转换成 \n;在写入时,将行结束符转换成本地格式,例如 Win32 平台上是 \r\n
QIODevice::Unbuffered 忽略缓存

我们在这里使用了QFile::WriteOnly | QIODevice::Truncate,也就是以只写并且覆盖已有内容的形式操作文件。注意,QIODevice::Truncate会直接将文件内容清空。

虽然QTextStream的写入内容与QDataStream一致,但是读取时却会有些困难:

QFile data("file.txt");
if (data.open(QFile::ReadOnly)) {
    QTextStream in(&data);
    QString str;
    int ans = 0;
    in >> str >> ans;
}

在使用QDataStream的时候,这样的代码很方便,但是使用了QTextStream时却有所不同:读出的时候,str 里面将是 The answer is 42,ans 是 0。这是因为当使用QDataStream写入的时候,实际上会在要写入的内容前面,额外添加一个这段内容的长度值。而以文本形式写入数据,是没有数据之间的分隔的。因此,使用文本文件时,很少会将其分割开来读取,而是使用诸如使用:

  • QTextStream::readLine() 读取一行
  • QTextStream::readAll()读取所有文本

这种函数之后再对获得的QString对象进行处理。
默认情况下,QTextStream的编码格式是 Unicode,如果我们需要使用另外的编码,可以使用:

stream.setCodec("UTF-8");

这样的函数进行设置。

11.4 例子

92df0446a99f438bad20a3c6f16c1996

#include "widget.h"
#include "ui_widget.h"
#include <QFileDialog>
#include <QDebug>
#include <QMessageBox>
#include <QFile>
#include <QTextCodec>
#include <QFileInfo>
#include <QDateTime>

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

    //点击按钮 弹出文件对话框,选择文件
    connect(ui->pushButton,&QPushButton::clicked,[=](){
        
        //获取文件的路径
         QString filePath = QFileDialog::getOpenFileName(this,"打开文件","C:\\Users\\zhangtao\\Desktop");
         qDebug() << filePath;
         if(filePath.isEmpty()){
             QMessageBox::warning(this,"警告","文件路径不能为空");
             return;
         }
         else{
             //将文件路径放入到 lineEdit中打印出来看看
             ui->lineEdit->setText(filePath);


             //读文件
             //文本编码格式类,Qt默认支持的格式是utf-8
             QTextCodec * codec = QTextCodec::codecForName("gbk");

             
             QFile file(filePath);

             //指定打开方式
             file.open(QIODevice::ReadOnly);

             //Qt默认支持的格式是utf-8
             QByteArray array;
//             array = file.readAll();

             while (!file.atEnd()) {
                  array += file.readLine();
             }

			//将文件内容 读取到 textEdit中
             ui->textEdit->setText(array);
             //ui->textEdit->setText( codec->toUnicode(array)  );       //把读取的文件转换为你想要的格式

             file.close();


             //写文件
//             file.open(QIODevice::Append); //QIODevice::Append追加的方式写文件
//             file.write("啊啊啊啊!");
//             file.close();


             //读取文件信息
             QFileInfo  info(filePath);

             qDebug() << "文件后缀名:" <<info.suffix() << "文件大小:" <<info.size() <<
                         "文件名:"<< info.fileName() << "文件路径:"<<info.filePath();

             qDebug() << "创建日期:" << info.created().toString("yyyy-MM-dd hh:mm:ss")<<
                         "最后修改日期:"<<info.lastModified().toString("yyyy/MM/dd hh:mm:ss");
         }
    });
}

12 基于 TCP 的 Qt 网络通信

在标准 C++ 没有提供专门用于套接字通信的类,所以只能使用操作系统提供的基于 C 的 API 函数,基于这些 C 的 API 函数我们也可以封装自己的 C++ 类 C++ 套接字类的封装。但是 Qt 就不一样了,它是 C++ 的一个框架并且里边提供了用于套接字通信的类(TCP、UDP)这样就使得我们的操作变得更加简单了(当然,在Qt中使用标准C的API进行套接字通信也是完全没有问题的)。下面,给大家讲一下如果使用相关类的进行 TCP 通信。

使用 Qt 提供的类进行基于 TCP 的套接字通信需要用到两个类:

  • QTcpServer:服务器类,用于监听客户端连接以及和客户端建立连接。
  • QTcpSocket:通信的套接字类,客户端、服务器端都需要使用。

这两个套接字通信类都属于网络模块 network

12.1 QTcpServer

QTcpServer 类用于监听客户端连接以及和客户端建立连接,在使用之前先介绍一下这个类提供的一些常用 API 函数:

12.1.1 公共成员函数

构造函数

QTcpServer::QTcpServer(QObject *parent = Q_NULLPTR);
// 给监听的套接字设置监听
bool QTcpServer::listen(const QHostAddress &address = QHostAddress::Any, quint16 port = 0);

// 判断当前对象是否在监听, 是返回true,没有监听返回false
bool QTcpServer::isListening() const;

// 如果当前对象正在监听返回监听的服务器地址信息, 否则返回 QHostAddress::Null
QHostAddress QTcpServer::serverAddress() const;

// 如果服务器正在侦听连接,则返回服务器的端口; 否则返回0
quint16 QTcpServer::serverPort() const

参数

  • address:通过类 QHostAddress 可以封装 IPv4、IPv6 格式的 IP 地址,QHostAddress::Any 表示自动绑定
  • port:如果指定为 0 表示随机绑定一个可用端口。

返回值:绑定成功返回 true,失败返回 false

得到和客户端建立连接之后用于通信的 QTcpSocket 套接字对象,它是 QTcpServer 的一个子对象,当 QTcpServer 对象析构的时候会自动析构这个子对象,当然也可自己手动析构,建议用完之后自己手动析构这个通信的 QTcpSocket 对象。

QTcpSocket *QTcpServer::nextPendingConnection();

阻塞等待客户端发起的连接请求,不推荐在单线程程序中使用,建议使用非阻塞方式处理新连接,即使用信号 newConnection()

bool QTcpServer::waitForNewConnection(int msec = 0, bool *timedOut = Q_NULLPTR);

参数

  • msec:指定阻塞的最大时长,单位为毫秒(ms)
  • timeout:传出参数,如果操作超时 timeout 为 true,没有超时 timeout 为 false

12.1.2 信号

当接受新连接导致错误时,将发射如下信号。socketError 参数描述了发生的错误相关的信息。

[signal] void QTcpServer::acceptError(QAbstractSocket::SocketError socketError);

每次有新连接可用时都会发出 newConnection () 信号。

[signal] void QTcpServer::newConnection();

12.2 QTcpSocket

**QTcpSocket 是一个套接字通信类,不管是客户端还是服务器端都需要使用**。在 Qt 中发送和接收数据也属于 IO 操作(网络 IO),先来看一下这个类的继承关系:

12.2.1 公共成员函数

构造函数

QTcpSocket::QTcpSocket(QObject *parent = Q_NULLPTR);

连接服务器,需要指定服务器端绑定的IP和端口信息。

[virtual] void QAbstractSocket::connectToHost(const QString &hostName, quint16 port, OpenMode openMode = ReadWrite, NetworkLayerProtocol protocol = AnyIPProtocol);

[virtual] void QAbstractSocket::connectToHost(const QHostAddress &address, quint16 port, OpenMode openMode = ReadWrite);

在 Qt 中不管调用读操作函数接收数据,还是调用写函数发送数据,操作的对象都是本地的由 Qt 框架维护的一块内存。因此,调用了发送函数数据不一定会马上被发送到网络中,调用了接收函数也不是直接从网络中接收数据,关于底层的相关操作是不需要使用者来维护的。

接收数据

// 指定可接收的最大字节数 maxSize 的数据到指针 data 指向的内存中
qint64 QIODevice::read(char *data, qint64 maxSize);
// 指定可接收的最大字节数 maxSize,返回接收的字符串
QByteArray QIODevice::read(qint64 maxSize);
// 将当前可用操作数据全部读出,通过返回值返回读出的字符串
QByteArray QIODevice::readAll();

发送数据

// 发送指针 data 指向的内存中的 maxSize 个字节的数据
qint64 QIODevice::write(const char *data, qint64 maxSize);
// 发送指针 data 指向的内存中的数据,字符串以 \0 作为结束标记
qint64 QIODevice::write(const char *data);
// 发送参数指定的字符串
qint64 QIODevice::write(const QByteArray &byteArray);

12.2.2 信号

在使用 QTcpSocket 进行套接字通信的过程中,如果该类对象发射出 readyRead() 信号,说明对端发送的数据达到了,之后就可以调用 read 函数接收数据了。

[signal] void QIODevice::readyRead();

调用 connectToHost() 函数并成功建立连接之后发出 connected() 信号。

[signal] void QAbstractSocket::connected();

在套接字断开连接时发出 disconnected() 信号。

[signal] void QAbstractSocket::disconnected();

12.3 通信流程

使用 Qt 提供的类进行套接字通信比使用标准 C API 进行网络通信要简单(因为在内部进行了封装)原始的 TCP 通信流程 Qt 中的套接字通信流程如下:

12.3.1 服务器端

通信流程

  1. 创建套接字服务器 QTcpServer 对象
  2. 通过 QTcpServer 对象设置监听,即:QTcpServer::listen()
  3. 基于 QTcpServer::newConnection() 信号检测是否有新的客户端连接
  4. 如果有新的客户端连接调用 QTcpSocket *QTcpServer::nextPendingConnection() 得到通信的套接字对象
  5. 使用通信的套接字对象 QTcpSocket 和客户端进行通信

3.1.2 代码片段
服务器端的窗口界面如下图所示:

image-20240425111335507

头文件

class MainWindow : public QMainWindow{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

private slots:
    void on_startServer_clicked();
    void on_sendMsg_clicked();

private:
    Ui::MainWindow *ui;
    QTcpServer* m_server;
    QTcpSocket* m_tcp;
};

源文件

MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow){
    ui->setupUi(this);
    setWindowTitle("TCP - 服务器");
    // 创建 QTcpServer 对象
    m_server = new QTcpServer(this);
    // 检测是否有新的客户端连接
    connect(m_server, &QTcpServer::newConnection, this, [=](){
        m_tcp = m_server->nextPendingConnection();
        ui->record->append("成功和客户端建立了新的连接...");
        m_status->setPixmap(QPixmap(":/connect.png").scaled(20, 20));
        // 检测是否有客户端数据
        connect(m_tcp, &QTcpSocket::readyRead, this, [=](){
            // 接收数据
            QString recvMsg = m_tcp->readAll();
            ui->record->append("客户端Say: " + recvMsg);
        });
        // 客户端断开了连接
        connect(m_tcp, &QTcpSocket::disconnected, this, [=](){
            ui->record->append("客户端已经断开了连接...");
            m_tcp->deleteLater();
            m_status->setPixmap(QPixmap(":/disconnect.png").scaled(20, 20));
        });
    });
}

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

// 启动服务器端的服务按钮
void MainWindow::on_startServer_clicked(){
    unsigned short port = ui->port->text().toInt();
    // 设置服务器监听
    m_server->listen(QHostAddress::Any, port);
    ui->startServer->setEnabled(false);
}

// 点击发送数据按钮
void MainWindow::on_sendMsg_clicked(){
    QString sendMsg = ui->msg->toPlainText();
    m_tcp->write(sendMsg.toUtf8());
    ui->record->append("服务器Say: " + sendMsg);
    ui->msg->clear();
}

13.3.2 客户端

通信流程

  1. 创建通信的套接字类 QTcpSocket 对象
  2. 使用服务器端绑定的 IP 和端口连接服务器 QAbstractSocket::connectToHost()
  3. 使用 QTcpSocket 对象和服务器进行通信

代码片段
客户端的窗口界面如下图所示:

a686305ecd1b42f39e1ca8215e238277

头文件

class MainWindow : public QMainWindow{
    Q_OBJECT
public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

private slots:
    void on_connectServer_clicked();
    void on_sendMsg_clicked();
    void on_disconnect_clicked();
private:
    Ui::MainWindow *ui;
    QTcpSocket* m_tcp;
};

源文件

MainWindow::MainWindow(QWidget *parent):QMainWindow(parent),ui(new Ui::MainWindow){
    ui->setupUi(this);
    setWindowTitle("TCP - 客户端");

    // 创建通信的套接字对象
    m_tcp = new QTcpSocket(this);
    // 检测服务器是否回复了数据
    connect(m_tcp, &QTcpSocket::readyRead, [=](){
        // 接收服务器发送的数据
        QByteArray recvMsg = m_tcp->readAll();
        ui->record->append("服务器Say: " + recvMsg);
    });
        
    // 检测是否和服务器是否连接成功了
    connect(m_tcp, &QTcpSocket::connected, this, [=](){
        ui->record->append("恭喜, 连接服务器成功!!!");
        m_status->setPixmap(QPixmap(":/connect.png").scaled(20, 20));
    });
        
    // 检测服务器是否和客户端断开了连接
    connect(m_tcp, &QTcpSocket::disconnected, this, [=](){
        ui->record->append("服务器已经断开了连接, ...");
        ui->connectServer->setEnabled(true);
        ui->disconnect->setEnabled(false);
    });

}

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

// 连接服务器按钮按下之后的处理动作
void MainWindow::on_connectServer_clicked(){
    QString ip = ui->ip->text();
    unsigned short port = ui->port->text().toInt();
    // 连接服务器
    m_tcp->connectToHost(QHostAddress(ip), port);
    ui->connectServer->setEnabled(false);
    ui->disconnect->setEnabled(true);
}

// 发送数据按钮按下之后的处理动作
void MainWindow::on_sendMsg_clicked(){
    QString sendMsg = ui->msg->toPlainText();
    m_tcp->write(sendMsg.toUtf8());
    ui->record->append("客户端Say: " + sendMsg);
    ui->msg->clear();
}

// 断开连接按钮被按下之后的处理动作
void MainWindow::on_disconnect_clicked(){
    m_tcp->close();
    ui->connectServer->setEnabled(true);
    ui->disconnect->setEnabled(false);
}

13 Qt 程序的发布

当 Qt 程序编写完成通过 IDE 编译就可以得到对应的可执行程序,这个可执行程序在本地运行是完全没有问题的(因为在本地有 Qt 环境,程序运行过程中可以加载到相关的动态库),但是如果我们想把这个 Qt 程序给到其他小伙伴使用可能就会出问题了,原因如下:

  • 这个小伙伴本地根本没有Qt开发环境
  • 这个小伙伴本地有Qt开发环境,但是和我们使用的版本不一致
  • 这个小伙伴本地有Qt开发环境并且使用的版本与我们一致,但是没有配置环境变量

以上几种情况都会导致我们的小伙伴拿到可执行程序之后无法运行,下面来给大家讲一下解决方案。

13.1 生成 Release 版程序

在编写 Qt 程序的时候,不管我们使用的什么样的 IDE 都可以进行编译版本的切换,如果要发布程序需要切换为 Release 版本(Debug 为调试版本),编译器会对生成的 Release 版可执行程序进行优化,生成的可执行程序会更小。这里以 QtCreator 为例,截图如下:

dcb62ca62dfe48198eeac9e47383290f

模式选择完毕之后开始构建当前项目,最后找到生成的带 Release 后缀的构建目录,如下图所示:
进图到 release 目录中,在里面就能找到我们要的可执行程序了
1fa3b9cc6f80414b902f723cc5fddc34

13.2 发布

生成的可执行程序在运行的时候需要加载相关的 Qt 库文件,因此需要将这些动态库一并发布给使用者,Qt 官方给我们提供了相关的发布工具,通过这个工具就可以非常轻松的找出这些动态库文件了,这个工具叫做 windeployqt.exe,该文件位于 Qt 安装目录的编译套件目录的 bin 目录中,以我本地为例:D:\Qt\Qt5.14.2\5.14.2\msvc2017_64\bin

  • C:\Qt 是 Qt 的安装目录
  • 5.14.2 是 Qt 的版本
  • msvc2017_64 是编译套件目录(编译套件有很多时,记得选release 版本的那个)
  • bin 存储 windeployqt.exe 文件的目录

如果已经将这个路径设置到环境变量中了,那么在当前操作系统的任意目录下都可以访问 windeployqt.exe

知道 Qt 提供的这个工具之后就可以继续向下进行了,首先将生成的 Release版本的可执行程序放到一个新建的空目录中

c5af8a0bb5934aeeb9227a7f973db93d

进入到这个目录,按住键盘 shift键然后鼠标右键就可以弹出一个右键菜单

0543ffb7838d429d997e15a32156adff

打开 Powershell窗口执行命令:

LordCard.exe 是可执行程序的名字
windeployqt.exe 的后缀 .exe 可以省略不写
windeployqt.exe .\paddleOCRDemo.exe		# 这是我的
windeployqt.exe LordCard.exe

这样 LordCard.exe 需要的动态库会被全部拷贝到当前的目录中,如下图:

46fffd11029e4b87a5556efeee509314

使用这种方式 Qt 会将一些用不到的动态库也拷贝到当前的目录中,如果确定用不到可以手动将其删除,如果不在意这些,完全可以不用理会,我选择后者。(注意!!!! 有很多以来的库他不会自己拷贝进来)
这个大佬可能没有使用一些自己下载的库,我这就一一补充一下(这个库主要是在我们编译的地方取保险起见可以把自己的,有图片等源文件也有放进来)。

6b95dc7eb2514aab91ebc134613cc4e3

现在一个绿色免安装版的程序就得到了,可以将这个目录打个压缩包发送给自己的小伙伴,但是这种方式终究比较 low,我们可以将这个目录中的文件制作成一个安装包,这样档次一下就上去了。

13.3 环境变量配置

在桌面找到我的电脑(此电脑)图标,鼠标右键,打开属性窗口

6880442c6b124d47b637092c918034b4

在属性窗口中选择 “高级系统设置”

557f80495db04ef0a83d53edeffe1f9d

描述打开环境变量窗口

027f61c1d07240a682940db0b15084e5

新建环境变量

32313b91e5e34037bae7d09bfe198784

我自己的配置成这样了。反正也可以用。

e3ddd1d432f34651a192f6444de45b92

环境变量配置完毕之后,不会马上生效,需要注销或者重启计算机。

将 Qt 的相关目录添加到系统环境变量中

环境变量说明:

在安装目录中找到 Qt 库的 bin 目录: C:\Qt\Qt5.14.2\5.14.2\mingw73_32\bin

在安装目录中找到编译套件的 bin 目录: C:\Qt\Qt5.14.2\Tools\mingw730_32\bin

以上目录为安装过程中的演示目录,各位小伙伴需要根据自己的实际情况,找到对应的本地路径。

e9316d620fbb4a128f7e1193a0a55d2c

14 Qt 程序打包

将应用程序和相关的动态库打包成安装包的工具有很多,大佬用过两个一个是 NIS Edit,一个是 Inno Setup 这是一个免费的 Windows 安装程序制作软件,小巧、简便、精美。

官方下载地址:传送门

其实这两个工具的使用方法是几乎一样的,下面拿 Inno Setup 使用举例。

第一步:创建一个带向导的脚本文件

0e4f15dfb8db4a50ab38a0d1ad720108

第二步:直接 Next,不要创建空的脚本文件

fae42bf969f84adfa6d814444ef93041

第三步:填写相关的应用程序信息

0e6dc5368f664b35ae7acb864d048678

第四步:指定应用程序的安装目录相关的信息

45c5e65a96854671ae5ad96f88e33031

第五步:选择可执行程序和相关的动态库,此处参考的是前边的 1.2 章节中的目录

e426e7cdaf264f1cb3d538d1a611aada

基于这个目录选择相关的文件和目录:

80af9f730e3a47078dc397b2ed328ffa

由于可执行程序关系的动态库有很多,所以可以直接添加动态库的目录,选中对应的目录之后,如果里边还有子目录会弹出如下对话框,选择是即可,需要包含这些子目录。

f465172269d34f599a876bcaf2957157

第六步:给可执行程序关联本地的某种格式的磁盘文件(比如记事本程序会自动关联本地的 .txt 文件),对于我的可执行程序来说无需关联,因此没有做任何设置,直接下一步

da2c2775db7945de80bc908f8e7707cb

第七步:给应用程序创建快捷方式,此处没有进行任何设置,使用的默认选项

50918b328b0641f386c62cbdf5ed23aa

第八步:指定许可文件,文件中的内容会显示到安装向导的相关窗口中,可以选择不指定,直接跳过。

b923ecb2c71348dc9a81c048112756ab

第九步:选择安装模式(给系统的当前用户安装还是给所有用户安装),根据自己喜好指定即可

e074e63f00624bcc93f664f896977b45

第十步:选择安装语言(这个工具没有提供中文,因此只能选择英文)

4297c1da54f14590abcafa51e77e0f92

第十一步:指定安装包文件的相关信息

0b212176a34343d38162ce7b62fe7db4

第十二步:向导结束,终于结束了。。。

93412786f3ad4300b0b15d97f22b5edf

第十三步:提示是否要编译生成的脚本文件,脚本编译完成之后,安装包就生成了。

e307a2ce4e88480ca96f7c3756990cbd

之后弹出第二个对话框,建议通过向导生成的这个脚本文件,这样以后就可以直接基于这个脚本打包程序生成安装包了。

d90a3418082b4085865eab4b19b557e8

编译完成之后,就可以去保存脚本文件的目录找生成的安装文件了

174eee762a324442b11d244e3a404483

14.1 安装

双击生成的安装包文件

04d028d955214c04aa4d3eb691ce8b8d

输入安装密码

f6c2cf031d074046b7513b35aae00ca6

指定安装路径

ea169313083e4dbfaf589d26613deee3

指定是否创建快捷方式

cfc1a3aaaed04fbd9dbadfde28a27c19

开始安装应用程序

548b4e70de474c1b9822acc0a8b3580d

安装完成,可以指定直接启动安装的应用程序

5a4f9d6a497541c180311ec271cdb8e1

最后启动游戏测试下是否可以运行

d9cef7f8faff47b6a1c9839bc8b8e0ad

注意:本网站所有内容为自己学习期间记录的笔记,很多都是原作者那边摘抄过来的,不作商业用途也不插播广告。(如有侵权,请联系我删除,15909440083)

如果有不理解的推荐去看他们的视频,讲的特别详细。参考:

零基础入门六天学会QT完整版
爱编程的大丙


文章作者: 葛杨文
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 葛杨文 !
评论
  目录