QT基础
1. 简介
1.1 什么是Qt
Qt是一个1991年由Qt Company开发的跨平台C++图形用户界面应用程序开发框架。它既可以开发GUI程序,也可用于开发非GUI程序,比如控制台工具和服务器。Qt是面向对象的框架,使用特殊的代码生成扩展以及一些宏,Qt很容易扩展,并且允许真正的组件编程。
Qt Creator是一个用于Qt开发的轻量级跨平台集成开发环境。Qt Creator可带来两大关键益处:提供首个专为支持跨平台开发而设计的集成开发环境(IDE),并确保首次接触Qt框架的开发人员能迅速上手和操作。即使不开发Qt应用程序,Qt Creator也是一个简单易用且功能强大的IDE。
Qt是一套应用程序开发库,但与MFC不同,Qt是跨平台的开发类库。跨平台意味着只需要编写一次程序,在不同平台上无需改动或只需要少许改动后在编译,就可以形成在不同平台上运行的版本。
1.2 Qt特征
1.面向对象
Qt具有模块设计和控件或元素的可重用性的特点。一个控件不需要找到它的内容和用途,通过Signal和slot与外界通信、交流。而且Qt的控件都可通过继承。
2.控件间的相互通信
Qt提供signal和slot概念,这是一种安全可靠的方法,它允许回调,并支持对象之间在彼此不知道对方信息的情况下,进行合作,这使Qt非常合适于真正的控件编程。
3.友好的联机帮助
Qt包括大量的联机参考文档,有超文本HTML方式、UNIX帮助页、man手册和补充的指南。对于初学者,指南将一步步地解释Qt编程。
4.用户自定义
其它的工具包在应用时都存在一个普遍的问题,就是经常没有真正适合需求的控件,生成的自定义控件对用户来说,也是一个黑匣子。比如,在Motif手册中就讨论了用户自定义的控件问题。而在Qt中,能够创建控件,具有绝对的优越性,生成自定义控件非常简单,并且容易修改控件。
5.方便性
由于Qt是一种跨平台的GUI工具包,所以它对编程者隐藏了在处理不同窗口系统时的潜在问题。为了将基于Qt程序更加方便,Qt包含了一系列类。该类能够使程序员避免了在文件处理、时间处理等方面存在依赖操作系统方面的细节问题。
2. Qt编译过程
1.编写代码
通过文本文档写入如下代码,后缀改为cpp
1 |
|
2.生成工程文件
cmd进入工程目录(最好是QT自带的)
在过程目录下的cmd输入:
qmake -project
修改工程文件,在工程目录下会生成一个后缀为pro的文件,在该文件的最后一行添加:
QT += widgets gui
3.生成makefile,终端输入qmake
4.输入mingw32-make,执行成功就可以在工程目录下的debug或release目录下生成可执行文件。
注:以上步骤可能存在环境变量文件,如果出现,想要配置
3. Qt Creator
在创建一个Qt的项目时,也就是创建一个主窗口,基类的选择有以下三种:
QmainWindow
:继承QWidget,带有菜单栏、工具栏、状态栏的界面QWidget
:最基础的窗口类,qt里面能看到的东西的基类,不带菜单栏的界面,类似于QQ、微信的登录框Qdialog
:继承QWidget,对话框界面,完成一些业务时,弹出的对话框
Qt里面绝大部分的类都是继承自QObject,它是一个顶层类
下面创建了一个MainWindow窗口类,继承的基类是QWidget
下面是创建窗口后,提供的main()函数:
1 | int main(int argc, char *argv[]) |
下面是窗口类的头文件
1 | class MainWindow : public QWidget |
下面是窗口类的构造函数以及一些程序:
1 | MainWindow::MainWindow(QWidget *parent) |
在上面的程序中,ui
是一个指向UI类对象的指针,该对象通常通过UI设计器生成,并包含了所有UI组件的实例和布局信息。而setupUi
是UI类中的一个成员函数,它的作用是将UI组件添加到主窗口上,并设置它们的位置、大小等属性。this
是一个指向当前对象的指针,通常是主窗口的实例。setupUi
函数会使用这个指针来确定UI组件应该添加到哪个窗口上。
执行后的效果如下:
4. 基本知识1
QWidget
类是所有用户界面对象的基类,它提供了创建和管理窗口部件的功能。另一方面,QObject
类是Qt对象模型的基类,它提供了对象树、信号与槽机制、事件处理等基本功能。
4.1 父子关系
默认情况下按钮是没有认父亲的,也就是一个顶层窗口,想要按钮显示在窗口中,就要跟窗口构造父子关系,方法如下:
- setParent
- 构造函数传参
创建了一个MainWindow窗口类,继承的基类是QWidget
1 | int main(int argc, char *argv[]) |
4.2 基础
1.关于Qt的坐标系是以父窗口的左上角为(0,0)的,以向右的方向为x的正方向,以向下的方向为y的正方向,顶层窗口就是以屏幕左上角为(0,0)的。
2.常用的API:
- move:移动窗口到父窗口某个位置
- resize:重新设置窗口的大小
- setFixedSize():设置窗口的固定大小
- setWindowTitle():设置窗口标题
- setGeometry():同时设置窗口位置和大小,相当于是move和resize的结合体,参数(x轴,y轴,宽,高)
3.对象树
概念:各个窗口对象通过建立父子关系构造的一个关系树
内存管理:父对象释放的时候会自动释放各个子对象
Qt中有内存回收机制,即对象树,但不是所有被new出的对象被自动回收,满足下面两个条件才可以自动回收:
- 创建的对象必须是
QObject
类的子类(间接子类也可以),QObject
类是没有父类的,Qt中有很大一部分类都是从这个类派生出去的 - 创建出的类对象,必须要指定其父对象是谁,一般情况下有两种操作方式:
- 在构造对象时指定父对象
- 通过调用
QWidget
的API指定父窗口对象,就是setParent()方法
下面创建了一个主窗口MainWindow窗口类,继承的基类是QWidget,又创建了一个MyPushButton窗口类,使它继承的基类是QWidget。
以下是主窗口MainWindow类的构造函数:
1 | MainWindow::MainWindow(QWidget *parent) |
5. 信号槽机制
信号:各种事件;槽:相应信号的动作
信号和槽本质都是函数
当某个事件发送后,如某个按钮被点击了一下,它就会发出一个被点击的信号(signal),当某个对象接到这个信号之后,就会做一些相关的处理动作(slot)。但是Qt对象不会无故收到某个信号,要想让一个对象收到另一个对象发出的信号。这时需要建立连接,即connect()。
connect(信号发送者,信号,信号接收者,槽);
1 | connect(btn, &QPushButton::clicked, this, &Widget::close); |
使用connect的时候保留&
符号的原因:
- 提高代码可读性(提示这是一个指针)
- 自动提示(会根据前面写的内容来提示你接下来可能输入的内容)
下面编写了多个类,类与类之间想要在Qt中使用信号槽机制,那么必须要满足如下条件:
- 这个类必须从
QObject
类或者是其子类进行派生 - 在定义类的头文件种加入Q_OBJECT宏
5.1 自定义信号与槽
1自定义信号要求:
- 信号是类的成员函数
- 返回值是void类型
- 信号的名字可以根据实际情况进行指定
- 参数可以随意指定,信号也可以重载
- 信号需要使用signals关键字进行声明,类似于public
- 信号函数只需要声明,不需要定义(没有函数体实现)
- 在程序中发送自定义信号,发送的本质就是调用信号函数
建议:习惯性在信号函数前加关键字emit;其只是显示的声明信号被发送,没有特殊含义。底层emit == #define emit
2.槽函数就是信号的处理动作,自定义槽函数和自定义的普通函数写法是一样的,要求:
- 返回值是void类型
- 槽也是函数,因此也支持重载
- Qt中的槽函数的类型有类的成员函数、全局函数、静态函数和lambda表达式(匿名函数)
- 槽函数可以使用关键字进行声明:public slots、private slots、protected slots
3.信号槽使用扩展:
一个信号可以连接多个槽函数,发送一个信号有多个处理函数
- 需要写多个connect连接
- 槽函数的执行顺序是随机的,和connect函数的调用顺序没有关系
- 信号的接收者可以是一个对象,也可以是多个对象
一个槽函数可以连接多个信号,多个不同的信号,处理动作是相同的
- 写多个connect就可以
信号可以连接信号:信号接收者可以接收信号,继续发出新的信号,即传递了数据,并没有进行处理
信号槽是可以断开的,通过disconnect()函数,其与connect函数的参数一样
4.参数的二义性问题:当某个类具有两个相同名字的信号,一个带参数,一个不带参数,那么接收该信号的类不知道该执行哪个槽函数,所以会报错,需要通过下面两种方法来解决:
- 使用函数指针赋值,让编译器自动挑选符合类型的函数
- 使用static_cast强制转换,让编译器自动挑选符合类型的函数
下面是一个窗口类的构造函数
1 | MainWindow::MainWindow(QWidget *parent) |
在上面程序中也可以信号连接信号,如果有同名但参数不同的信号,还需要进行转换
1 | connect(btn, &QPushButton::clicked, tea, &teacher::hungry); |
5.Qt4中的信号和槽
使用两个宏 SIGNAL和SLOT,它们的原理是将后面的参数转换成字符串,使用方式为:
connect(信号发送者, SIGNAL(函数原型), 信号接收者, SLOT(函数原型));
- 好处:没有重载二义性的问题,有无参数可以直接写在connect里面
- 坏处:写错了,编译期间不报错(在项目中就很难排查错误)
6.QDebug输出QString默认会转义,比如说当给信号传入的参数是emit tea->hungry("kfc\r\n");
,控制台输出的也是"kfc\r\n"
。
解决办法:
- 将QString转成char*:
qDebug()<<"请吃"<<what.toUtf8().data();
- 使用qDebug().noquote():
qDebug().noquote()<<"请吃"<<what;
5.2 定时器程序
下面是一个非自定义的信号与槽,所以就需要先手动在ui界面添加两个按钮和两个标签。然后通过系统提供的按钮信号来触发槽函数。
1 | MainWindow::MainWindow(QWidget *parent) |
6. QMainWindow
QMainWindow是一个为用户提供主窗口程序的类,包含一个菜单栏(menu bar)、多个工具栏(tool bars)、多个停靠部件(dock widgets)、一个状态栏(status bar)及一个中心部件(central widget),是许多应用程序的基础,如文本编辑器,图片编辑器等。具体结果如下:
1.下面创建了一个窗口类MainWindow,继承的基类是QMainWindow,在该窗口中添加各个部件内容:
1 | MainWindow::MainWindow(QWidget *parent) |
2.UI文件的使用
创建项目时保留UI,setupUI函数就是关联UI文件的代码到程序,原理就是qt将ui文件转化成了c++代码。
3.资源文件使用
创建资源文件:新建 —–> Qt —–> Qt Resource File —–> 名称为res
7. QDialog对话框
对话框是GUI程序中不可或缺的组成部分。很多不能或者不适合放入主窗口的功能组件都必须放在对话框中,比如用于完成一次性任务的功能(如登录功能、现在某个文件打开、保存文件)。对话框通常会是一个顶层窗口,出现在程序最上层,用于实习短期任务或者简洁的用户交互。
Qt中使用QDialog类实现对话框,但是声明一个QDialog对象的时候,不管这个对话框对象跟哪个窗口建立了父子关系,当它显示出来的时候都还是一个顶层的窗口。
1.对话框没有最大化、最小化按钮的窗口,其可以分为模态对话框
和非模态对话框
:
- 模态对话框:就是对话框还没有关闭前不能操作同一个进程的其它窗口
- 创建模态:QDialog::exec()函数,是一个阻塞的消息循环函数
- 非模态对话框:就是对话框没有关闭前也能操作同一个进程的其它窗口
- 使用show()函数来直接显示窗口就可以,非阻塞的情况下要使用new的方式来创建对话框对象,不然容易随着所在区域的释放而释放。
- 内存泄漏问题,模态对话框关闭后可能并不会马上释放,所以就得通过设置窗口的属性来让其关闭后自动释放:
dlg->setAttribute(Qt::WA_DeleteOnClose);
下面通过两个按钮的发送信号来触发槽函数,从而实现创建模态和非模态对话框
1 | void MainWindow::on_model_clicked() |
2.系统标准对话框
QMessageBox用来提示用户某条信息,分为以下几种:
- 错误提示框:
QMessageBox::critical();
- 警告提示框:
QMessageBox::warning();
- 信息提示框:
QMessageBox::information();
- 问题提示框:
QMessageBox::question();
1 | //参数依次是:父对象窗口;提示框标题;提示框提示的信息 |
问题提示框相较于其它几个提示框比较特殊,可以指定对话框的按钮,通过返回值来获取用户点击了哪个按钮。
3.文件对话框
使用QFileDialog来打开一个文件对话框,常用的函数是getOpenFileName来选择单一某文件,返回值是用户选择的文件路径。
在下面程序的中,设置的过滤器表示为将打开的目录种类分为PNG、ICO和all类型,默认先显示的是PNG类别的文件。
1 | //默认打开的是当前项目所在的文件 |
4.布局
有如下两类布局:
- 静态:就是位置和大小不会跟着外部窗口变化而变化
- 动态:就是位置和大小会跟着外部窗口变化而变化
常用的动态布局:水平、垂直、栅格、表单布局,推荐使用widget的自带的布局功能
使用弹簧来调整布局的位置(居中),栅格布局可以将空间分为几行几列的表格,方便对齐
大小策略:默认情况下动态布局,子窗口的大小会跟着父窗口的大小变化而变化,调整水平或垂直策略,变为固定
调整子窗口和父窗口之间的间隙,设置父窗口的margin,调整子窗口之间的间隙就调整spacing
调整窗口的固定大小,就是将窗口的最大值和最小值都设为同一个值
5.按钮组
radio button单选按钮:互斥域的问题,如果想将某些单选按钮隔离开,就用容器将它们隔离,一般用Group Box
check box多选按钮:有三态(tristate),每次点击按钮的时候stateChange信号里边传进来
6.QListWidget窗口
往ListWidget窗口中添加内容有两种方式:
- 往ListWidget窗口中,通过addItem添加QListWidgetItem对象(一个一个添加,可以修改item的属性)
- 往ListWidget窗口中,通过addItems添加QStringList对象(一次性添加多个,不可以修改item的属性
1 | //一次性添加一个 |
7.treeWidget窗口
- 设置标题,会根据setHeaderLabels函数里面的成员数生成对应的列数
- 添加根节点通过addTopLevelItem函数
- 根节点下面添加子节点通过addChild函数
1 | //1.设置标题 |
得到的效果图如下
8.tableWidget窗口
- 设置行数通过setRowCount()函数、设置列数通过setColumnCount()函数
- 设置水平的标题通过setHorizontalHeaderLabels()函数
- 设置表格某行某列的数据setItem(row, col, item)
1 | //设置行数、列数 |
得到的效果图如下:
9.容器
stacked Widget 页面切换需要我们自己去实现,一般使用按钮点击的时候切换,通过setCurrentIndex()方式切换到第几页,序号从0开始
10、显示控件
label可以显示静态图,通过pixmap属性即可完成
label也可以显示动态图gif,需要先创建movie对象,再通过setMovie()函数设置电影,最后通过start()函数来播放动画
1 | QMovie *movie = new QMovie(":/Image/mario.gif", QByteArray(), this); |
11.自定义控件
当系统提供的控件不能满足我们生产中的一个功能时,就需要自己创建一些控件。
比如说一个自定义一个按钮控件,需要先创建一个自定义按钮类MyButton,让它继承QPushButton类,在MyButton类的析构函数中实现一些想要的功能,最后将主窗口中的按钮(QPushButton类)提升为MyButton类,这样就可以实现自定义按钮的一些功能了。
8. 事件
事件(event)是由系统或者Qt应用程序本身在不同的时刻发出的。当用户按下鼠标、敲下键盘、或者是窗口需要重新绘制的时候,都会发出一个相应的事件。一些事件在对用户操作做出响应时发出,如键盘事件等;另一些事件则是由系统自动发出,如计时器事件。
在所有组件的父类QWidget中,定义了很多事件处理的函数,如:
- keyPressEvent():键盘按键按下事件
- keyReleaseEvent():键盘按键松开事件
- mouseDoubleClickEvent():鼠标双击事件
- mouseMoveEvent():鼠标移动事件
1.在下面程序中,先创建一个主窗口widget,将一个Label标签拖入其中,再创建一个自定义标签类MyLabel,使它继承QLabel类,然后在该类里面添加对应的事件功能函数,最后将主窗口的Label标签提升为自定义标签类MyLabel。
自定义标签类的头文件:
1 | protected: |
自定义标签对应的事件函数实现:
1 | MyLabel::MyLabel(QWidget *parent) : QLabel(parent) |
2.定时器事件timerEvent
闹钟就是定时器,闹钟响了就是定时器事件
在头文件中需要重写定时器事件:void timerEvent(QTimerEvent* event);
里面实现的功能即为定时器时间到后执行的操作
在需要的位置通过startTimer来启动一个定时器,返回值就是定时器的id,参数是毫秒,每隔相应的时间就会触发一次定时器事件
通过函数killtimer()来杀死一个定时器,参数就是要杀死定时器的id
timerEvent定时器事件处理函数中可以通过event参数获取到当前事件是哪个定时器发出的,如:event->timerId()
3.系统封装好的定时器QTimer
1 | Widget::Widget(QWidget *parent) |
4.绘图事件
什么时候画
- 绘图事件:窗口需要重新显示的时候(大小发生变化、窗口切换等),就会收到一个绘图事件paintEvent,收到绘图事件之后,窗口就要将自己画出来。
怎么画
- 通过定义一个画家QPainter,给出一个画图设备QPaintDevice(窗口)作为参数。
5.创建了一个窗口Widget,在该窗口头文件重写了画图事件,下面为该画图事件函数里面的功能实现
1 | void Widget::paintEvent(QPaintEvent *event) |
6.手动触发绘图事件
在一些应用场景,我们或许希望能手动的触发绘图事件,而不是等到窗口发生变化再触发绘图事件,所以就可以使用下面方法完成。
- 可以使用两个函数
- repaint:会马上触发绘图事件,当某一处函数调用了多次repaint时,会触发多次绘图事件
- update:update做了一些优化,当某一处函数调用了多次update时,只会触发一次绘图事件
注意:不要再绘图事件paintEvent()函数中再触发绘图事件,会导致无线循环
下面程序实现了将本地存在的图片绘制到窗口
1 | //painter绘制已经存在到达图片 |
7.绘图设备
绘图设备是指继承QPainterDevice的子类,Qt一共提供了4个这样的类,分别是QPixmap、QBitmap、QImage和QPicture,其中:
- QPixmap专本为图像在屏幕上的显示做了优化
- QBitmap是QPixmap的一个子类,它的色深限定为1,可以使用QPixmap的isQBitmap()函数来确定这个QPixmap是不是一个QBitmap。
- QImage专门为图像的像素级访问做了优化
- QPicture则可以记录和重现QPainter的各条命令。