目录
1.什么是QT
2.环境搭建
QT SDK的下载
QT的使用
QT构建项目
快捷指令
QT的简单编写
对象树
编码问题
组件
初识信号槽
窗口的释放
窗口坐标体系
1.什么是QT
QT 是一个跨平台的 C++ 图形用户界面库,支持多个系统,用于开发具有图形界面的应用程序。它由挪威公司 TrollTech(现为 Digia 的一部分)出品,后来更名为 The Qt Company。QT 不仅仅是一个图形用户界面库,它还包含用于网络通信、线程、数据库、正则表达式处理、XML 处理、JSON、国际化、音频和视频处理等的库。
对于GUI的开发方式,QT只是其中一种,由于GUI也大多应用在客户端,而不是服务器,因此主要是在windows下环境的开发,当然也有许多GUI的开放方式:
基于c/c++的:
1.首先就是比较古老的,windows提供的原生API, Windows API,开发较为繁琐。
2.利用面向对象思想封装成类--MFC就是其中之一,由微软公司提供的GUI开发库。
3.同时代也诞生出了QT,也是利用了面向对象的思想,但是QT一直在推成出新,一直在更新,但是MFC已经很久不更新了。
后来微软自己又推出了新的体系例如c# (.net)下的 Windows Forms,之后又升级成了WPF,UWP.
2.环境搭建
无论是哪一种开发工具,我们都需要准备三件事:
1.下载c/c++的编译器(gcc,cl.exe)。
2.QT SDK(qt软件开发工具包的安装),不过一般SDK会内置一个C++的编译器(mingw下的gcc)。
3.集成开发环境VS (比较重量级,功能更多,但需要额外配置,初学不建议),QTcreater(有些许bug,不过上手方便好用,适合初学),Eclipse等(生态有限)。
不过现在对于这三个东西,你安装QTcreator就已经足够了。
QT SDK的下载
首先我们去官方下载qt sdk,官网点击这里:Index of /archive/qt,选择一个较新的版本进行安装例如5.14进入文件夹内,2.6G的这个大小的就是我们qt的sdk,选择合适的系统安装。
国外网站下载比较慢,所以下载是比较慢的。安装之后就点击一路next基本上就可以了。
其中选择安装的组件,windos下我们就用Mingw--与QT creater就可以,如果你使用VS进行开发,那么可以安装Msvc.最后配置一下环境变量,后面的操作就比较方便。
打开文件会发现关于QT的又许多个文件,其中QT Assistance是官方文档,QT designer是支持拖拽式的图形界面开发,linguist QT语言家,支持国际化。我们主要使用的是QT creater集成开发环境。
QT的使用
使用方法和VS类似,我们首先创建项目,在项目里可以看见左侧有许多,开发GUI我们就使用application,选择了左边,右边也有许多开发选项,选择wigets applicatiion(图形化界面模板)。
此时还要进行构建项目的配置,第一个选择路径,其次还有配置构建工具这里就是用qmake,这也是老牌的实用的功能工具了。
之后就是关于文件的设置。
首先使用QT创建项目会创建一些代码,这些代码会包含一个类,而这里的Base class就是选择该类的父类,包括有三个类(QMianWindow 完整的应用程序窗口,Qwidgets表示一个控件,即窗口上的一个部件 Qdialog表示一个对话框的开发)。当前学习我们就使用第二个Qwidgets即可。
然后选择翻译,这个我们不用关注,之后就是构建套件,选择哪一个编译器,如果你之前电脑还安装了VS这样的,你会发现,这里有许多编译器,这里我们就使用当前的MingW即可。最后汇总选择NULL即可。之后就为我们生成了整个项目(包括头文件,源文件,form文件等)。
对于左下角我们可以进行运行调试。构建的项目中的文件,以及调用的类等QT creator都会帮我们创建好.
QT构建项目
简单的helloworld标签的编写,两种方式实现:
第一种方式,直接通过图形化组件的拖拽实现,双击widget.ui进入面板设计,选择Display widget中的lable标签,此时QT Deigner右上角中会出现一个树形的你的组件,此时的ui文件会多出一部分代码代表这个组件,进一步的qmake在编译项目的时候,基于该部分生成一段c++代码。
此时我们重新运行就可以看到发生了变化。在我们的项目路径下的buildXXXX这个文件中的ui_widget.h,也就是我们的父类文件,在这里面生成了对应的代码:
第二种方式写代码的方式
首先重新创建一个项目,一般我们在添加组件的代码放在.cpp的父类中的构造函数中
在此时前,我们需要直到每创建了一个组件就需要你去包含该组件名对应的头文件,创建组件时使用堆栈都可以,建议用堆,同时参数为this,指定父对象(因为对象树)。
对象树:QT中时使用对象树将不同组件连接在一起的
由于QT开发的比较早,对于一些基础类型,用那时候的c/c++的感觉都不好,因此QT自己搞了一套轮子,自己重新的将基本数据类型重新封装了支持QT的顺畅开发如(Qstring ,Qlist,Qvector,Qdict...)。不过后期c++发展的比较好,都比较完善了,但QT也不可能把自己的删了,所以其实这两套用法作用其实差不多。
因此开发过程中这两种容器都可以,但QT的一些原生接口使用的是QT自己的类型。
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
//创建标签对象
QLabel * lable=new QLabel(this);
//设置文本内容
lable->setText("hello world");
}
根据这个简单代码我们基本了解的QT。
快捷指令
Qt Creator 中的快捷键• 注释:ctrl + /• 运⾏:ctrl + R• 编译:ctrl + B• 字体缩放:ctrl + ⿏标滑轮• 查找:ctrl + F• 整⾏移动:ctrl + shift + ⬆/⬇• 帮助⽂档:F1• ⾃动对⻬:ctrl + i;• 同名之间的 .h 和 .cpp 的切换:F4• ⽣成函数声明的对应定义: alt + enter使⽤帮助⽂档打开帮助⽂档有三种⽅式. 实际编程中使⽤哪种都可以.1、光标放到要查询的类名/⽅法名上, 直接按 F12、Qt Creator 左侧边栏中直接⽤⿏标单击 "帮助" 按钮
QT的简单编写
对象树
对于以上代码,有的同学可能会有疑问,为什么没有释放,这是因为在创建的时候,我们将它挂到了对象树上,调用了析构释放掉了。
QT通过对象树将各个组件关联起来,以N叉树的结构连接起来,对象树也会将这些组件统一释放掉。例如假想一下,如果我们项目的父类是widget,那么在其他组件创建时,都需要传widget的指针给这些对象连接,此时就是将widgt作为对象树root,其他组件为子节点,即将这些组件添加到主窗口上,当窗口关闭,调用窗口析构时在调用组件析构,而不需要我们手动释放。
其次因为我们是界面开发,因此不能存在窗口还在,但是组件已经被释放掉了,而是当窗口关闭时,统一释放所有组件,随意在栈上创建的,也就存在提前释放的情况。我们可以通过自己创建一个标签类,析构中打印信息,此时再去创建,关闭窗口就会自动调用析构:
#include "mylable.h"
#include<iostream>
myLable::myLable(QWidget * parent)
:QLabel(parent)
{
}
myLable::~myLable()
{
std::cout<<" 你好"<<std::endl;
}
//widget.cpp的构造
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
myLable * lable=new myLable(this);
lable->setText(" hello world");
}
编码问题
由于QT的控制台默认编码格式是gbk,而QT creator编写代码的格式为utf-8,所以会出现编码不一致的问问题,因此我们可以在高级里修改QT的编码格式且按照当前编码设置为gbk可以解决该问题。除此之外,QT提供了Qstring可以进行编码格式的修改,且也提供了专门打印日志的工具 Qdebug可以用来打印日志。
组件
除了基本父类组件 Qwidget(继承的子类Widget) 一个简单窗口,QMainWindow(MainWindow) 包含菜单栏,工具栏的多个组建的窗口,QDialog(QDialog)--对话框。
剩下的就是与窗口齐平的组件,这些组件一般需要传入父类指针实现组件的绑定其中包括:
初识信号槽
信号槽(Signal-Slot mechanism)是一种在软件开发中常用的设计模式,特别是在Qt框架中,它用于实现对象间的通信和事件处理。以下是关于信号槽的详细解释:
基本概念
- 信号(Signal):对象发出的一种特定事件,类似于广播,没有特定的接收者。例如,当按钮被点击时,按钮对象会发出一个“clicked”信号。
- 槽(Slot):用于响应信号的特定函数或方法。槽可以是任何对象的成员函数,也可以是全局函数或静态成员函数。当信号被发出时,与之相关联的槽会被自动调用。
实现原理
- 解耦:信号和槽机制实现了对象间的解耦,即发送者和接收者之间不需要知道对方的具体实现细节。
- 连接:通过
connect
函数,可以将信号和槽进行连接。当信号被发出时,与之连接的槽会被自动调用。- 语法和宏:在Qt中,信号和槽通过特定的语法和宏进行声明和连接,例如
SIGNAL(signal)
和SLOT(slot)
。特性
- 类型安全:需要关联的信号和槽的签名必须是一致的,这保证了类型的安全性。
- 松散耦合:信号和槽降低了Qt对象之间的耦合度,使得代码更加灵活和可维护。
- 多对多关系:多个信号可以与单个槽进行连接,单个信号也可以与多个槽进行连接。
用途
- 窗口间通信:在Qt中,信号槽是实现不同窗口间通信的主要方式。
- 事件处理:当某个事件发生时,通过发出信号并调用相应的槽函数来处理该事件。
示例
以按钮点击事件为例,当按钮被点击时,按钮对象会发出一个“clicked”信号。如果有对象对这个信号感兴趣,它就可以使用
connect
函数将“clicked”信号与自己的一个槽函数进行连接。当按钮被点击时,“clicked”信号被发出,与之连接的槽函数会被自动调用,从而实现了对按钮点击事件的处理。优缺点
- 优点:降低了对象间的耦合度,提高了代码的可维护性和灵活性;实现了类型安全。
- 缺点:与回调函数相比,信号和槽的运行速度可能稍慢,因为需要遍历信号和槽的关联关系并执行相应的槽函数。
总之,信号槽是一种强大的对象间通信机制,在Qt框架中得到了广泛应用。通过合理地使用信号槽,可以编写出更加灵活、可维护和可扩展的代码。
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
QPushButton *button=new QPushButton(this);
button->setText("按钮");
connect(button,&QPushButton::clicked,this,&Widget::HandleClick);
}
例如以上代码在我们创建一个新的按钮组件后,利用信号槽进行了信号的捕捉,这个信号对于组件本事有封装的信号产生函数,当收到该信号,就会去让处理信号的对象的组件做出对应的操作HandleClick--例如我这里是打开一个新的窗口:
如果是使用图形化创建窗口,此时并不需要new一个QPushButton,也不需要放在QWidget中,而是在生成项目的时候,就已经在内部的ui中构建了一个成员变量,可以通过ui进行访问,也不想需要释放。
窗口的释放
由于主窗口关闭时,会自动执行析构函数,而子窗口关闭时,却不会调用析构函数,因此著窗口关闭时调用析构需要我们来设计:
方法一构造添加内置窗口析构函数
直接在构造函数添加如下代码,然后就可以调用子窗口析构函数了。
setAttribute(Qt::WA_DeleteOnClose);
第二种方法是 重写虚函数
当窗口关闭瞬间,会发射一个关闭信号,调用槽函数void closeEvent(QCloseEvent* event),只需要在需要关闭的窗口重写虚函数closeEvent(),在关闭该窗口瞬间,会自动调用该虚函数。该方法不会受其它窗口影响,也即是其它窗口关闭,不会调用这个窗口的closeEvent()虚函数。只会在该窗口类起作用。
void Widget::closeEvent(QCloseEvent *event)
{
Q_UNUSED(event);
//event->accept();
//delete this;
qDebug("关闭了窗口");
}
Q_UNUSED()函数在程序中的作用,就如它所代表的英文一样,unused,即无用的意思。即Q_UNUSED()函数在程序中没有实质性的作用,用来避免编译器警告。
第三种方法自定义信号槽
连接close信号到一个自定义槽函数,以便在窗口关闭之前执行特定的操作。接下来我们主要学习QT的信号槽机制。
窗口坐标体系
在计算机中使用的是左手坐标系
当我们创建组件,如果没有设置坐标系的位置,默认是(0,0),可以通过组件的接口move移动组建的位置,单位是像素,例如我们笔记本电脑的显示的像素就是(水平)1920 *1080 (垂直),即1080p。其实就是小灯泡的个数。
槽与信号
说到信号大家肯定不陌生,因为我们在学习Linux时就已经知道了,Linux中是有一个信号机制的,且包含大量的信号,通过发送特定的信号,系统捕捉到信号就回去内核态执行对应的信号处理函数,如果是用户重写了信号处理函数,那么就存在用户态到内核态到用户态的过程。
在QT中为了实现组件之间的通信,也引入了信号机制,虽然两者并没什么关系,但还是具有许多相似性。
对于一个信号就要涉及到三个要素:
信号源-----由哪一个组件发出的。
信号的类型---------这是什么样的信号? 点击按钮信号,关闭窗口信号,移动光标信号,下拉框选择信号.......等等各种组件产生的信号。
信号处理的方式:slot(槽) ------->一个函数
而在QT中就可以使用一个connect函数将一个信号和一个槽关联起来。这有收到该信号就去调用槽函数处理,这个槽函数其实就是一个回调函数。那么对于该信号槽机制比较重要的就是connect了,那我们来看看connect的原型:
第一个参数为一个部件类的指针,表示信号源,第二个参数表示的是信号的类型,第三个信号表示负责信号的组件,第四个参数表示如何处理信号,第五个参数有初始值一般不关注。
对于信号分两种:一种是对象自己本身就已经封装有的,一种是自定义的信号。
像这种载体是的时候有一个这样的信号的图标的就是信号,代表点击的信号。
而这种就是槽函数,这是去做对应操作的,例如这里就是去关闭组件。
其次QT规定了在使用connect的时候,参数一是啥组件,参数二的类型就是在这个组件(我的父类也可以)内定义好的信号类型,即信号类型与信号源相匹配。
对于槽函数也分两种:一种的已经内置的,一种是自定义的。
参数三与参数四就分别是处理的组件,以及用来处理信号的槽函数的地址。
问题:为什么参数是char*,而我们传的是函数指针?
实际上这个connect函数声明是以前旧版本的声明,首先两个指针肯定不是同类型的,不能直接转,而现在的版本实际上提供了两个宏Signal和SLOT,这两个宏可以将对应的槽函数与信号函数指针转会为char*,QT5之后对于参数进行了优化,即使用函数模板,使用泛型参数进行转化,不需要写宏。
自定义信号与自定义槽
无论是信号还是槽都是组件的成员函数,所以除了内置的,我们可以自己在类中编写我们需要的成员函数/
自定义槽
例如按钮的点击信号对应处理的槽:
void Widget::HandleClick()
{
this->close();//点击按钮,关闭窗口
}
但是在类中声明时需要signal: 或者slot:修饰,在以前时是private/public/protected signals/slots :修饰的,除了这种方式外,我们还可以以图形化界面的方式自定义信号与槽(底层自动代码,但相对比较固定)。
当我们选择后QT会帮我们自动创建好对应的槽函数,生成的槽函数并没有显示connect,QT其实还可以通过函数名字自动连接信号,不需要我们再写connect,每一个组件都有Objectname,如按钮的On_PushButton_clicked()就说明了处理按钮点击事件,但是Objectname一定是按照名字规则
才可以去触发信号槽处理,QMetaObject::connectSlotsByName(Widget),PushButton->setObjectName(),这些函数我们在使用ui构建部件时,编译器会自动帮我们创建的。
所以通过ui创建可以根据命名来自动捕获处理信号,但是手动写的信号,还是需要手动connect,因为我们这里的代码是没有QMetaObject::connectSlotsByName(Widget)来解析函数名的。
自定义信号
除了槽,信号也可以自定义,也是一个成员函数,由private/public/protected signals修饰,但是相对于自定义槽,实际开发中自定义信号不是很用的上,一般外设的信号已经内置有对应的信号函数,因此自定义信号基本上很少用到。
对于自定义信号,是比较特殊的:
1.在QT中,自定义信号只需要声明,在编译过程中编译器会自动生成还信号函数,程序员无法干涉。
2.返回参数类型只能为void,参数有没有都无所谓,支持重载。
3.内置信号会由于你的某个操作,自动发送信号,是封装好的,而自定义信号,不仅需要定义,还要由你来决定在哪里发信号,QT提供了一个关键则emit,用来发送信号。(实际上emit没什么大的作用,我们直接调函数也是可以发送信号的)
注意:先绑定信号与槽之后,在发送信号。
例如:
//先定义一个信号
signals:
void mysignal();
//再定义一个槽
public slots:
void HandleMySignal();
//构造中连接信号于槽
connect(this,&Widget::mysignal,this,&Widget::HandleMySignal);//绑定信号与槽
//发送信号
emit mysignal();//mysignal();
void Widget::HandleMySignal()
{
this->setWindowTitle("自定义处理信号");
}
除了以上不带参数的信号,也可一定有有参数的信号,此时信号的参数与槽的参数必须保持一致(可以参数一样,也可以信号参数比槽函数多)。这样的设定也是可以去让小于等于槽函数的参数个数的信号都可以来绑定。
//声明
signals:
void mysignal(const QString &str);
//再定义一个槽
public slots:
void HandleMySignal(const QString &str);//会将信号的参数传递给槽
//槽的实现
void Widget::HandleMySignal(const QString &str)
{
this->setWindowTitle(str);
}
//在点击按钮时发送信号,同时传参数
void Widget::on_pushButton_clicked()
{
//发送信号
emit mysignal("自定义处理信号");
}
当然内置信号也是有参数的,不过不用我们来传。
最后一点就是在使用信号与槽机制时一定一定要添加上Q_OBJECT这个宏,有了它才能使用信号槽。
connect(this,&Widget::mysignal,this,([this](const QString &str){ this->setWindowTitle(str);}));//不需要声明,定义,直接一次性写
因为信号与槽这恶机制,开发者是想要实现除了信号处理的功能,更重要的是实现一对一,一对多,多对多复用信号和槽的这种机制,这种关系与数据库中的关联关系时非常类似的,对于这种关系,QT引出了一张关联表来记录信号与槽之间的关系。
不过这个表格不是真表格,而是connect所绑定的一个个关系。
但是理想很丰满,显示很骨干,实际开发需要中更多的就是一对一,稍微少一点的一对多,多对多基本上是一个伪需求,用不到。
补充:
1.信号与槽的断开:使用disconnect断开信号与槽的连接,用法类似。用的也比较少,一般不会断开,如果是想将之前的连接断开,建立新的连接,不是一对多的绑定,可以使用该函数。
2.槽函数的定义也可以使用lanmda表达式。在c++的时候我们就已经直到,对于函数指针,仿函数,lanmda表达式,这三者之间其实是可以相互替换,三者作用相同,还可以通过包装器作为参数传递。