一.背景
互联网核心岗位:
- 开发(程序员)、测试、运维(管理机器)、产品经理(非技术岗,提出需求)
- 开发岗位:后端开发、前端开发、算法工程师、游戏开发
- 前端开发:网页前端开发、桌面应用开发、移动端应用开发
qt属于客户端开发,也就是前端开发的一个分支,属于桌面应用开发
- 客户端:直接和用户打交道的这一端的程序(edge浏览器、qq音乐、steam…)
- 服务器:站在客户端背后的男人
虽然大部分的客户端程序,是需要有背后的服务器做支持的,但是确实也有些程序,是不需要服务器的,这样的程序(画图板、计算器…)同样也是直接和客户打交道,也可以称之为 “客户端程序”。
准确来说,qt特指用来进行桌面应用开发(电脑上的程序)设计到的一套技术。
qt无法进行网页开发,也不能进行移动端开发(虽然目前qt官方也支持移动应用的开发了,但是目前还没有听说有啥知名的商用移动应用程序是qt开发的)
- 客户端开发的重要任务:编写和用户交互的界面
和用户交互的界面,两种分格:
- 命令行界面/终端界面(黑框框) —— TUI (专业软件,给程序员用的)
- 图形化界面 —— GUI (普通用户,会用鼠标即可)
qt是用来编写桌面GUI程序的一套框架~~
在Windows上编写GUI程序,也有很多的解决方案 ~~ qt只是其中的一种(学会了一个,其他的都比较容易上手)
1.GUI开发的各种技术方案
Windows下还有那些方案,可以开发GUI?
-
Windows API:windows系统提供的原始API
开发起来非常原始和繁琐,但在windows刚诞生的阶段也没有什么好选的 -
MFC:利用面向对象的思想,将windows API分装起来变为一个个控件,并且提供图形化界面的方式可以拖拽控件。
上世纪90年代到今天,影响力非常深远 -
QT:采取面向对象的方式将系统API进行分装,提供可视化界面
上世纪1991年诞生,和MFC不同,MFC早以不在更新,而qt仍然在不停的退出新的版本,至今仍然非常有生命力。
以上都是基于C/C++搭建出来的一些 GUI 开发的技术体系~~
后来微软自己做了一个编程语言C#,在进行windows界面开发时,微软逐渐偏向于C#
-
Windows Forms 给C#量身定做的一套开发GUI技术体系
-
WPF、UWP:Windows Forms的升级版
-
Electron:本质上是基于HTML网页,打包成一个基于windows上运行的客户端程序~~
这个技术体系,最初是用来开发“atom”文本编辑器的,而“atom”是GitHub官方开发的
后来“atom”不在流行,被微软的VSCode取代
比较大的缺点是开发出来的程序,运行效率低于上面介绍的一些原生开发的技术体系,所以很少有公司去使用它
QT虽然是上述解决方案中的一种,仍然属于是其中非常能打的(商业公司的产品,使用QT非常多,对应的就业岗位相比于其他的技术方案也是很多的)
并且,qt可以跨平台,支持windows、linux、Mac,对于公司来说,更容易降低开发成本。
2.什么是框架
QT是跨平台的C++ 图形用户界面应用程序框架 。它为应用程序开发者提供了建立艺术级图形界面所需要的所有功能。它是完全面向对象的,很容易扩展。QT为开发者提供了一种基于组件的开发模式,开发者可以基于简单的拖拽和组合来实现复杂的应用程序,同时也可以使用C++语言进行高级开发。
一般而言自由、灵活就意味着容易出错!毕竟不能给它自由过了火。
而框架,本质是一群大佬发明出来,方便让咱们能力低的程序员,写出来的东西比较靠谱的东西,也就是说看框架是用来限制程序员的自由,让代码的下限得到保证。
平时我们了解较多的是库,库和框架都属于大佬把一些程序写好,让你去使用。
- 库:被程序员调用(程序员是主体)
- 框架:程序员配合框架,完善填充框架中留出的一些细节(框架是主体)
相对于C++,Java对于框架更加的依赖。
编写C++代码,框架当然也很重要,C++的生态是割裂的,散列的,不像隔壁Java,存在非常大的社区,一统天下。C++不同的开源社区/大厂,各自有各自的框架,各自为政。
相比之下,像QT这种,能够被大家共同认可的框架,在整个C++生态中是不常见的
3.QT支持的系统
- Windows系统
- Linux系统,尤其是Linux中的KDE桌面基于QT构建的
Linux主要是给服务器使用的,服务器不需要图形化界面,是TUI界面,基于命令操作,门槛更高, 效率也更高。Linux需要将资源放在更重要的地方,而不是图形化界面。
Linux桌面环境中存在好几套桌面环境,也有少数的用户,使用Linux做为桌面,但是这些桌面环境都不是很成熟,一起来会很不舒服- GNOME:基于GTK创建的
- KDE:基于QT创建的
- Mac系统
- 嵌入式系统
嵌入式系统也是QT实际开发中的主要系统之一,日常使用的冰箱、洗衣机、路由器…这些设备内部也有计算机,这种设备,里面的计算机,配置就不会那么高。
有先嵌入式系统,也是需要运行图形化界面的程序,这个时候,QT就可以起到作用,尤其是一些工业设备上。
当然在这个领域,QT也受到了来自安卓的挑战
4.QT的版本
目前最新的版本是Qt6,但是相对来说,qt6和qt5之间的核心功能区别不大,并且企业中也仍然有大量的项目在使用qt5.
另外,qt在发布的时候还提供了两种许可证:
- 商业许可:你想开发一个QT程序,就可以向所属公司购买商业许可证
- 开源许可:也就是想要开发一个程序,不需要花钱,直接拿来用
而相比开源序可,商业序可提供的更是一种服务,一种技术支持,当开发程序时若出现问题,可以直接找所属公司,让他们来解决。
5.QT的优点
-
跨平台,几乎支持所有的平台
-
接口简单,容易上手,学习qt框架对学习其他框架有参考意义
-
一定程度上简化了内存回收机制;
C++是一个讲究效率的语言,对于内存回收重视度不够,而qt采取了一定的策略,实现了半自动的垃圾回收,简化内存释放,尽可能小的影响程序的运行效率。 -
开发效率高,能够快速的构建应用程序
-
有很好的社区氛围,市场份额在缓慢上升
-
可以进行嵌入式开发
6.QT的应用常见
-
桌面的应用程序
qt能够创建各种类型的桌面应用程序,包括文件管理器、媒体播放器、绘图程序等。Qt应用程序支持多种操作系统,可以运行在多个桌面操作系统上 -
移动应用程序
支持Android和IOS移动操作系统,为应用程序提供强大的跨平台能力。
Android和IOS系统已经有许多类似的框架,很少有人使用QT去开发 -
嵌入式系统
QT在嵌入式领域应用非常广泛,它可以构建面向各种设备的图像应用程序,在机顶盒、车载娱乐系统、安防监控领域具有广泛的应用。
二.环境搭建
注意:具体如何下载安装QT,已经有大量的资料存在,这里不在赘述
QT的开发环境,需要安装3个部分:
-
C++ 编译器(gcc、cl.exe…)
注意编译器不是ide,不是visual studio,编译器只是IDE调用的一个程序 -
QT SDK
SDK是软件开发工具包,其中包含开发qt程序用到的各种工具,一般SDK中已经内置了一个C++编译器,比如Windows版本的QT SDK里已经内置了C++编译器。(内置的编译器是mingw,widows版本的gcc/g++)
要是想用VS内置的cl.exe做为编译器,也不是不行,需要配置很多东西,容易出错。
具体安装的过程中,需要将对应的C++编译器给勾选上。 -
需要有一个QT的集成开发环境(IDE)
- QT官方提供的QT Creator
使用这个写qt,是最容易上手的方式,不需要任何额外的配置,开箱即用,在初学阶段建议使用,虽然在使用过程中存在不少bug(有先bug非常影响使用体验),但用起来挺方便的,比较适合初学者。 - Visual Studio
功能更强,但需要额外的配置更多,更容易出错,有先公司开发商业QT程序的时候,可能会使用VS。需要给VS安装QT插件,并且需要QT SDK使用VS的编译器重新编译(可以使用编译好的版本) - Eclipse
Eclipse并不只是java IDE,本身是一个IDE本身,可以搭配不同的插件构成不同的IDE,但目前Eclipse市场份额受到很大的冲击。
- QT官方提供的QT Creator
说是安装三个东西,其实只需要安装一个QT SDK,另外两个也就有了。
1.认识QTSDK中的重要工具
- Assistant 5.15.2:Qt自带的离线版本的官方文档,虽然Qt也有中文文档(非官方的),但主流仍然是去看官网的英文文档。
- QT Designer:图形化的设计界面的工具,通过拖拽控件的方式来快速的生产界面,后面经常使用但是搭配QT Creator来使用
- Linguist:QT语言家,作用是对国际化进行支持,有的时候,写的程序,是要和国际接轨,允许你单独创建一个语言配置文件,把界面上需要用到的各种文字,都配置到文件中,并且在文件中提前的把各种语言的翻译都配置进去。
- QT Creator:QT的集成开发工具,学习QT过程中主要使用的工具
2.使用QT Creator创建项目
Application:应用程序,如果使用QT写一个GUI程序,就应该选这个
- QT Widget:传统的开发GUI的方式
- QT Quick:QT搞出的一套新的用来开发GUI的方式
- QT for Python:QT不只支持C++,也支持python和Java
构建系统,通过QT写的程序,涉及到一系列的 “元编程” 技术。通过代码来生成代码。
QT框架会在编译的时候,自动先调用一系列的生成工具,基于你自己写的代码,生成一系列的其他C++代码,最终编译的代码,也就是最后生成的这些代码
- qmake:老牌的构建工具
- CMake:并非是QT专属,很多开源项目都会使用CMake
- Qbs:新一代的QT构建工具,实际上很少人去使用它
-
Base class:使用QT Creator创建项目时,会自动生成一些代码出来,生成的代码就是包含一个类。此处就是要选择这选择这个自动生成的类的父类是谁。
- QMainWindow:完整的应用程序窗口,可以包含菜单栏、工具栏、状态栏…
- QWidget:表示一个控件(窗口上的一个具体的元素,输入框、按钮、下拉框、单选按钮、复选按钮…)
- QDialog:表示一个对话框
QT中内置的类都是以Q开头的
QWidget最简单,初学阶段选QWidget。 -
Class name:自动生成的类的类名,它的父类是Base class选择的类
-
Header File、Source File:类对应的文件
次数生成的文件名和类名关联,关联并不是强制的,推荐大家使用一样的 -
form file:Qt中创建图形化界面的方式有两种
- 直接通过C++代码的方式创建界面
- 通过from file(ui文件),以图形化的方式来生成界面
此时就可使用QT Designed或者QT Creator来编辑这个ui文件,从而以图形化的方式快速的生成图形界面
选择翻译文件(对应语言),此处暂时不关注,和国际化相关
选择一下基于那个编译器的QT SDK来构建后续的代码。
3.项目解释
(1)main.cpp
#include "widget.h"
#include <QApplication>
int main(int argc, char *argv[]) // 命令行参数
{
QApplication a(argc, argv); // 将命令行参数作为参数创建对象
Widget w; // Widget 刚刚创建项目时,填写了生成的类名,也就是创建一个控件对象
w.show(); // 显示该控件对象
return a.exec();
}
- 编写一个QT的图形化界面程序,一定需要有QApplication对象
- Widget的父类是QWidget,都是由QWidget提供(QT是很好的面向对象示例,在学习QT的过程中可以很好的理解面向对象)
- exec在Linux中曾经学过,表示进程中的程序替换,把可执行文件中的代码和数据替换到当前进程中。当然,当前QT中的exec和Linux中的没有如何关系。
(2)widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; } // 命名空间,声明类
QT_END_NAMESPACE
class Widget : public QWidget //QT SDK内置类,要想继承QWidget必须包含头文件,#include <QWidget>
{
Q_OBJECT // QT内置的一个宏,本质是文本替换
public:
// QT中引入了“对象树”机制,创建的QT对象,就可以把这个对象挂在树上,往树上挂的时候需要指定“父节点”(后面讲)
Widget(QWidget *parent = nullptr);
~Widget();
private:
Ui::Widget *ui; // 与 from file 密切相关
};
#endif // WIDGET_H
-
QT设定,使用QT中内置的类,包含的头文件的名字就是和类名一致的
也不是用到的所有的QT的类都需要显示包含头文件。参考C++中,头文件可能是间接包含 -
写代码的原则:一个QT的类,先拿过来,如果直接能用,说明对应的头文件已经被包含过了,无需显示包含。如果这个类提示找不到定义,在手动显示对应的头文件。
-
Q_OBJECT展开之后会生成一大堆代码,当我们使用QT中的
信号
和槽
的时候需要引入这个宏(具体后面介绍)
(3)widget.cpp
#include "widget.h" // 创建项目生成的头文件
#include "ui_widget.h" // form file 被 qmake 生成的头文件
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this); // 6、8行 把 form file 生成的界面和当前 widget 关联起来
}
Widget::~Widget()
{
delete ui;
}
- 关键要点:form file
(4)widget.ui
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Widget</class>
<widget class="QWidget" name="Widget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>Widget</string>
</property>
</widget>
<resources/>
<connections/>
</ui>
-
单击Widget.ui文件,进入上述代码编辑部分,该代码格式为
xml
xml
和html
非常相似,都是使用成对的标签来表示数据
xml
这里的标签,有那些标签,都有什么含义,这个是程序员自定义的。此处看到的这些标签,就是QT开发大佬们自己约定的(这里的标签具体是啥含义不需要关注,只需要直到ui文件本质是一个xml即可)
html
虽然也是通过标签来表示,但是它的标签是固定的,每个标签是啥含义,有一个专门的标签委员会约定,所有的浏览器也是按照同样的规则来解释的。
这里的xml
标签的含义,就类似与Linux网络原理中学过的一个话题,自定义应用层协议。 -
QT中使用xml文件就是去描述程序的界面是啥样的,进一步的qmake会调用相关的工具,依据这个文件生成一些C++代码,从而把完整的界面构造出来。
-
双击Widget.ui文件,QT Creator就会调用QT Designer,打开ui文件,打开图形化界面编辑器
(5)Empty.pro
QT += core gui // 要引入的QT模块,后面学习到一些内容的时候可能会修改这里
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++11
# The following define makes your compiler emit warnings if you use
# any Qt feature that has been marked deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS
# You can also make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
// 下面三项描述了当前项目中,参与构建的文件有啥(编译器要编译那些文件)
// 这个地方不需要手动修改,QT Creator 帮咱们自动维护好
SOURCES += \
main.cpp \
widget.cpp
HEADERS += \
widget.h
FORMS += \
widget.ui
# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target
-
QT项目的工程文件,也是qmake工具构建时候的重要依据
-
.pro
类似于在Linux上的makefile
makefile其实是一个非常古老的技术,是常见的方式,但不是唯一的方式
qmake搭配.pro
起到的作用就和Makefile
是类似的 -
QT Creator将运行过程都分装好了,只需要点击运行即可
(6)临时文件
上面的看到的各个文件都是源代码,如果编译运行QT项目,构建过程中还会生成一些中间文件
打开文件资源管理器,看看项目对应的目录是什么样子
运行一次文件,就会在项目目录并列的地方多出一个 build-xxxx
目录,这个目录里面就是该项目运行过程中,生成的一些临时文件。
- 编译QT程序,还是会用到makefile,只不过这个makefile不需要手动写,而是 qmake 自动生成的
- ui_widget: widget.ui xml生成的 .h文件
三.初始QT
1.Hello World
有两种方式实现Hello World
- 通过图形化方式,在界面上创建出一个控件,显示 Hello World
- 通过重代码的方式,通过编写代码,在界面上创建一个控件,显示Hello World
接下来,我们先分别了解代码和图形化方式,在来看一下不同的控件对应这两种方式的实现过程。
(1)图形化方式
双击Widget.ui文件,进入图形化界面,在控件区的最下方有一个Display Widgets分类,这个分类中的所有控件都是用来显示,使用Label控件(在QT中完成Hello World 可以通过很多种控件实现),拖拽该控件,放入程序窗口,修改内容,得到结果:
点击左下角运行:
刚才往界面上拖拽了一个QLabel控件,此时,ui文件的xml中就多出一段代码(如下:16~28行)
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Widget</class>
<widget class="QWidget" name="Widget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>Widget</string>
</property>
<widget class="QLabel" name="label">
<property name="geometry">
<rect>
<x>310</x>
<y>230</y>
<width>221</width>
<height>51</height>
</rect>
</property>
<property name="text">
<string>Hello World</string>
</property>
</widget>
</widget>
<resources/>
<connections/>
</ui>
进一步的qmake就会在编译项目的时候,基于这个内容生成一段C++代码,通过这个C++代码构建出界面内容。(自动完成)
而在该项目临时文件ui_widget.h中,就会根据Widget.ui配置文件,创建出对应的方法:
void setupUi(QWidget *Widget)
{
if (Widget->objectName().isEmpty())
Widget->setObjectName(QString::fromUtf8("Widget"));
Widget->resize(800, 600);
// 下面三行创建标签
label = new QLabel(Widget);
label->setObjectName(QString::fromUtf8("label"));
label->setGeometry(QRect(310, 230, 221, 51));
retranslateUi(Widget);
QMetaObject::connectSlotsByName(Widget);
} // setupUi
(2)代码方式
一般我们通过代码来构建界面时,一般会将构造界面的代码放在 Widget/MainWindow 的构造函数中。(以Widget为类)而构造函数在main.cpp中的Widget w;
创建对象时调用,调用的是widget.cpp中对应的构造函数。
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget) // ui成员变量是qmake自动生成的
{
ui->setupUi(this);
}
Widget::~Widget()
{
delete ui;
}
使用代码方式要使用Lable标签,那必须要添加头文件,否则报错:
添加QLable头文件时出现了两个文件:
-
qlabel.h:早期QT使用的头文件分格
当时C++的标准还没有确立,直到C++98之后,规定包含头文件,比如统一使用#include 代替原有的#include <stdio.h> -
QLabel:在原来头文件的基础上,重新使用更加规则的头文件名编写该头文件
目前我们用更加规范的也就是QLabel
#include "widget.h"
#include "ui_widget.h"
#include <QLabel> // 在QT当中,每个类都有对应的头文件
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
QLabel* lable = new QLabel(this); // 创建Lable对象,参数this表示给当前lable对象指定父对象(父对象:Widget w对象)(对象树,使各个节点连在一起)
// 创建对象时,可以在堆上创建,也可以在栈上创建,两种写法都行,但是更推荐在堆上创建(原因在对象树中讲)
lable->setText("Hello World!"); // 设置控件中要显示的文件,参数为const QString,QString内部提供构造函数转换C分格字符串,
// QT诞生在没有标准及标准库的年代,在当时如何表示字符串并不统一,而且当时使用的字符串方式都不好用,于是自己建造轮子,
// 做了一系列的基础类,来支持QT的开发,如:字符串:QString、动态数组:Qvector、链表:QList等
// 经过很多年的打磨,C++标准库和QT使用的虽然本质是一个东西,但是两种又是分开的,想要删掉某一方这是不可能的,只能共存。
// 所以,在开发代码时可以使用标准库或QT提供的,但是在QT原生的api中,涉及到的东西,用的都是自己的这一套东西。
// 后续代码中会经常遇到QString,很少遇到std::string,但两种可以相互转换
// QString用起来要比std::string好,因为QString内部已经对于字符编码做了处理(如转码操作),而std::string啥也没干
}
Widget::~Widget()
{
delete ui;
}
- 通过代码创建QLable默认是在左上角,想要换到其他位置也是可以的(后续在下面窗口坐标系中讲)
2.1 内存泄漏问题
在上述代码中我们发现,new出来的对象,并没有释放,难道不会造成内存泄漏吗?
#include "widget.h"
#include "ui_widget.h"
#include <QLabel>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
QLabel* lable = new QLabel(this); // new出来的对象没有释放
lable->setText("Hello World!");
}
Widget::~Widget()
{
delete ui;
}
上述代码中,在QT中不会产生内存显露问题,label对象会在合适的时候被析构释放,虽然没有手动delete,但确实释放了。
之所以能够将该对象释放,主要是因为把这个对象挂在了对象树上。(详细看下面)
2.2 对象树
对象树不只是在QT中用到,前端开发(网页开发)也涉及到类似的对象树(DOM),本质上也是一个树形结构(N叉树),通过树形结构把界面上的各种元素组织起来。
QT中也是类似的,也搞了一个对象树,也是N叉树,把界面上的各种元素组织起来。
使用这些对象树,将这些内容组织起来,最主要的目的就是在合适的时机(窗口关闭),把这些对象统一进行释放。
这里的树上的对象,统一销毁是最好的,如果提前销毁,就会导致对应的控件在界面上不存在了。
所以创建对象时使用new,就是为了把这个对象的生命周期交给QT对象统一管理。若是在栈上创建对象,在运行时就会出现问题(提前释放),生命周期结束后,对应的栈就会销毁,对象也不会长久存在。
为了更好的看到这个现象,我们做一个小demo
-
创建mylabel类
-
打开mylabel.h头文件
#ifndef MYLABEL_H #define MYLABEL_H #include <QLabel> // 使用F4可以切换到对应的.cpp文件 class mylabel : public QLabel { public: // 构造函数使用带QWidget* 版本的,这样才能保证添加到对象树上 mylabel(QWidget* parent); ~mylabel(); }; #endif // MYLABEL_H
注意这里需要手动添加头文件
-
点击F4,切换到mylabel.cpp文件,修改该文件如下:
#include "mylabel.h" #include <iostream> mylabel::mylabel(QWidget* parent) :QLabel(parent) { } mylabel::~mylabel() { std::cout << "mylable 被销毁!" << std::endl; }
-
打开widget.cpp文件,修改文件如下:
#include "widget.h" #include "ui_widget.h" #include "mylabel.h" Widget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget) { ui->setupUi(this); // 使用自己定义的 mylabel 代替原来的 QLabel,所谓的“继承”本质上是扩展,保持原有功能不变的基础上, // 给对象扩展一个析构函数,通过这个析构函数,打印一个自定义的日志,方便咱们观察程序的运行效果 mylabel* label = new mylabel(this); label->setText("hello World!"); } Widget::~Widget() { delete ui; }
-
运行该项目,显示hello world!
-
关闭显示窗口,在version Control模块,查看日志,看到mylabel类执行析构函数时输出内容:
由此,我们可以确定,在QT中我们new创建一个对象,该对象被放入对象树中,当窗口被销毁时,就会自动销毁对象树上所有对象,mylabel的析构函数被执行。
2.3 乱码问题
观察上面的demo,我们看到析构函数所输出的内容与我们想要它输出的存在差异
预期打印的时“被销毁”三个中文,但是实际显示效果出现了乱码。
对于乱码,我想大多数人会经常涉及到,而乱码问题,出现的原因有且只有一个(不限于C++):编码方式不匹配
背景
首先,我们要知道一个汉字,在计算机中,占几个字节?大多数人回答都是两个字节,其实这是不对的。
针对这个问题,只要你回答出一个具体的数字,就一定是错的,这需要结合前提条件,即当前中文编码使用的是那种方式(字符集)。
中文中的常用字大概是4千多个,算上不常见的,总数差不多是6万左右,参考ASCII码表,将这6万格字整理为一个表格,不同的数字对应不同的字,这对于计算机来说不是难事,对于这些表格称之为字符集,但具体这个表格长什么样子,不同的组织给出的结果不同。
表示汉字的字符集,其实有很多种,不同的字符集,表示同一个汉字,使用的数字不同。
随着时间的推移,很多字符集淹没在了历史中,目前,表示汉字的字符集,主要有两种方式:
- GBK(中国大陆):使用2个字节表示一个汉字(Windows简体中文,默认的字符集就是GBK)
- UTF-8/UTF8:边长编码,表示一个符号使用的字节数存在变化(2~4不定),但对于汉字,一般是3个字节(Linux默认就是UTF8)
一个汉字,具体的UTF8/GBK的一个汉字具体对于的编码是多少,可以通过在网上查找。
解决问题
了解了背景,我们在回到问题,编码不匹配就是,你的字符串本身是utf8编码的,但是终端(控制台)是按照gbk的方式来解析显示的,此时就会出现乱码问题。
想要解决这个问题,我们就要了解到,那些环境涉及到编码的问题,这些了解后将其统一自然就解决了。
上述的字符串使用的编码方式,与当前mylabel.cpp文件的编码方式是一致的。
对于如何查看一个文件的编码方式这里教大家一个小技巧:
- 在QT Creator中右击文件,选择在Explorer中显示
- 打开后右击文件,选择打开方式,选择记事本打开
- 在记事本的右下角给出编码方式
显示是UTF-8说明这个文件是UTF-8编码,显示是ANSI说明这个文件是GBK编码
在QT Creator内置的终端中显示出乱码,说明它不是UTF8编码,而且这个终端好像不能设置字符编码。
想要解决这个乱码问题,只能统一编码格式,要么改终端,要么改文件,终端不好改,而对于改文件,在当前表示中文的编码中,主流的方式就是使用UTF-8,这种编码方式适用范围极广,改它缺点也很大。
这里,我们就要使用到QT中提供的一个 qDebug() 这样一个工具,借助这个,就可以完成打印日志这个工作,很好的处理字符编码(不需要程序员关注,QT内部解决)。
修改mylabel.cpp如下:
#include "mylabel.h"
#include <iostream>
#include <QDebug>
mylabel::mylabel(QWidget* parent)
:QLabel(parent)
{
}
mylabel::~mylabel()
{
// std::cout << "mylable 被销毁!" << std::endl;
qDebug() << "mylable 被销毁!";
}
- QDebug是QT中的一个类,我们不会直接使用这个类,而是通过qDebug()这个东西,当做cout来使用。
后续在QT中,如果需要通过打印日志的方式,输出一些调试信息,都优先使用qDebug,虽然cout也行,但是凑团对于编码的处理不好,在Windows上容易出现乱码(如果在Linux上使用QT Creator,一般没事,Linux默认的编码一般都是utf8)。
qDebug还有一个好处,可以统一进行关闭,当我们想要它正常显示时它正常显示,若是不想,可以通过编译开关,来实现一键式的关闭。
2.4 小节
-
认识QLabel类,能够在界面显示字符串。
通过 setText 来设置,参数 QString(QT中把C++里的很多容器类,进行了重新分装,历史原因)
-
内存泄漏
-
对象树,QT中通过对象树来统一释放界面的控件对象。
QT还式推荐使用new的方式在堆上创建对象,通过对象树,统一释放对象
创建对象树的时候,在构造函数中,指定父对象(此时才会挂在对象树上)
如果你的对象没有挂在对象树上,记得手动释放
-
通过继承自 QT 内置的类,可以达到堆现有控件进行功能扩展的效果。
QT内置的 QLabel,没法看到销毁的过程,为了看清,就创建类 mylabel,继承自QLabel,重写了析构函数。
在析构函数中,加上日志,直观的观察到对象释放的过程。
(面向对象继承的本质是对现有代码的扩展,也可以重写控件中任何的功能,达到对功能扩展的目的)
-
乱码问题 和 字符集问题
-
如果 QT 中打印日志,作为调试信息。
使用cout固然可以,但不是上策(字符编码处理的不好,也不方便关闭)
QT推荐使用qDebug()完成日志的打印。
-
我们之前调试程序的时候,都是使用调试器,VS/gdb,这里为什么打印日志调试?
其实调试器有很多的局限性,是无法使用的,比如概率性问题,一个bug调试100次,只出现一次甚至更少,要想调试,使用调试器就不可能了。
而无论使用那种方式,本质上就是观察程序执行的中间过程中间结果,只是在不同的场景下使用哪一个更有效而已。
(3)编辑框实现
编辑框分为如下两种:
- 单行编辑框:QLineEdit
- 多行编辑框:QTextEdit
这里我们使用单行编辑框即可,和上面的图形化方式相同,编辑框也可以使用代码的方式,这里先介绍控件的方式。
如下图,拖拽 Line Edit 控件到面板,输入字符串,运行后即为所需内容:
而对于代码的方式,我们需要修改 widget.cpp 内容如下:
#include "widget.h"
#include "ui_widget.h"
#include <QLineEdit>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
QLineEdit* q = new QLineEdit(this);
q->setText("hello World!");
}
Widget::~Widget()
{
delete ui;
}
(4)按钮方式
4.1 图形化方式
在设计中找到push button按钮(这里主要使用该按钮),拖拽其到面板修改内容即可:
作为一个按钮,可以进行点击,但当我们运行该程序,点击按钮会发现没有什么反映。
这里就涉及到了QT当中的信号槽机制,本质就是给按钮的点击操作,关联上一个处理函数,当用户点击的时候,就会执行该函数。(这里的函数本质就是一个回调函数)
想要管理这样一个函数需要使用到 connect()
函数(在Linux网络编程中,也学到过这样一个函数connect函数,用来给TCP socket建立连接用的,这里的connect与网络中的没有任何关系),该函数是QObject这个类提供的静态函数,这个函数的作用就是 “连接信号” 和 “槽”。
修改Widget.cpp中代码如下:
#include "widget.h"
#include "ui_widget.h"
#include <QObject>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
connect(ui->pushButton, &QPushButton::clicked, this, &Widget::handleClick);
}
Widget::~Widget()
{
delete ui;
}
void Widget::handleClick() // 注意:需要在Widget.h文件中添加该函数声明
{
ui->pushButton->setText("hello QT!");
}
运行结果如下:
接下来我们来简单了解一下connect这个函数:
static QMetaObject::Connection connect(
const QObject *sender,
const QMetaMethod &signal,
const QObject *receiver,
const QMetaMethod &method,
Qt::ConnectionType type = Qt::AutoConnection);
-
第一个参数表示谁发出的信号
这需要访问form file(ui 文件)中创建的控件。
在QT Designer(设计)中创建一个控件,此时会给这个控件分配一个objectName属性,这个属性的值,要求是在界面中是唯一的(不能和别人重复),这个值可以自己生成,也可以手动修改。
如下图,为上面按钮控件对于的值:
qmake在预处理
.ui
文件的时候,就会根据这里的 objectName 生成对于的C++代码。
若我们打开该项目对应的临时文件,找到 ui_widget.h 文件打开后,会发现如下成员变量:
C++代码中该 QPushButton 类型所创建对象的名字就是这里的 objectName对应的值,这个变量就是ui属性中的成员变量。
若是我们手动修改QT Designer中该控件的objectName值,ui_widget.h中对应的名称也会改变。(注意:变的是变量名,QPushButton是类型名,没办法变) -
第二个参数在点击按钮时就会触发这个信号,表示发出了啥信号
有了这个信号就要将它关联到具体的槽函数上 -
第三个参数表示谁来处理这个给信号
指定关联到那个对象的槽函数 -
第四个参数表示具体怎么处理这个信号
指定具体的槽函数
4.2 代码方式
我们新建一个项目,修改widget.cpp文件如下(widget.h文件也需要修改,在代码中有体现):
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
myButton = new QPushButton(this); // 因为需要在处理信号函数中用到myButton变量,将其在Widget类中设置为成员变量
myButton->setText("Hello World!");
connect(myButton, &QPushButton::clicked, this, &Widget::handleClick);
}
Widget::~Widget()
{
delete ui;
}
void Widget::handleClick()
{
if(myButton->text() == QString("Hello World!"))
{
myButton->setText("Hello QT!");
}
else
{
myButton->setText("Hello World!");
}
}
对于纯代码版本,按钮对象是咱们自己new的,为了保证其他函数中也可以访问到这个变量,就需要把按钮对象,设定为Widget类的成员变量。
而对于图形界面,不需要自己new对象操作,这已经被QT自动生成了,而这个按钮对象,已经作为 ui 对象里的一个成员变量。无需作为 Widget 成员
对于这两种方法,在实际开发中使用谁没有主次之分。
如果当前的界面内容比较固定,此时就用图形化的方式来构建界面。
如果当前界面经常要动态变化,此时就以代码的方式来构建界面。
2.QT中的命名规则
在给变量/函数/文件/类 起名字是非常有讲究的:
- 起名要有描述性
- 如果名字有点长,有多个单词构成的,就需要使用适当的方式来区分不同单词
如下是QT中类名、函数名和变量名的命名规则:
- 类名:首字母大写,单词和单词之间首字母大写;
- 函数名及变量名:首字母小写,单词和单词之间首字母小写
3.QT Creator中的常见快捷键
- F4:头文件和源文件之间切换
- Alt + Enter:将光标移动到h文件中的方法声明,按Alt+Enter,再按回车键将在cpp中添加对应的方法实体
- ctrl + / :注释
- ctrl + R :运行
- ctrl + B :编译
- ctrl + 鼠标滚动 : 字体缩放
- ctrl + F : 查找
- ctrl + i :自动对齐
- F1:帮助文件
4.帮助使用文档
打开帮助文件有三种方式(在QT Creator中使用):
- 光标选中想要查找的类或函数,点击F1
- QT Creator右侧栏边框中直接使用鼠标单机 “帮助” 按钮
- 直接在Windows搜素框搜素QT Assistant或是在QT安装目录下的mingw**/bin/assistant下直接点击该应用程序
本质上上述三种方式查找的都是同一份文档。
在实际开发中,一定会用到很多的第三方库和框架,很可能这些库会比较小众,网上很难找到一些相关资料,这就需要我们来查找官方文档,而且这个官方文档的作者很可能来自于其他国家,但只要我们所用到的东西走向世界,那一定会有英文的文档。
5.窗口坐标体系
坐标体系:以左上角为原点(0,0),X向右增加,Y向下增加。
计算机中采用左手坐标系,它的原点(0,0)在屏幕/窗口的左上角。
给QT中某个控件设置位置时,就需要指定坐标,相对于控件来说,坐标系的原点就是相对于它的父窗口/控件。
在前面讲对象树时已经提到,QPushButton的父元素/父控件/父窗口 就是QWidget。
QWidget没有父元素(NULL),所以它的父元素就是整个显示器桌面了。
重新创建一个项目,修改widget.cpp文件并运行如下:
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
myButton = new QPushButton(this);
myButton->setText("Hello World!");
}
Widget::~Widget()
{
delete ui;
}
关于像素:
显示器本质上是由一大堆可以发光的小亮点/小灯泡来构成的
如果将自己的手机摄像头对准显示器,焦距放到最大,距离合适是可以看到的。
也可以在自己windows设置中查看自己显示器上的分辨率,这样我们就可以知道自己电脑横纵坐标最大取值范围。
move函数中参数为1,表示移动1像素。
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
myButton = new QPushButton(this);
myButton->setText("Hello World!");
myButton->move(100, 300);
}
Widget::~Widget()
{
delete ui;
}
同样的,不只是控件可以设置位置,运行出来的窗口也可以设置相对于桌面的位置,这需要使用在qmake创建的widget类对象来调用,也就是在widget构造函数中使用this指针调用,如下(这里就不掩饰结果了):
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
myButton = new QPushButton(this);
myButton->setText("Hello World!");
myButton->move(100, 300);
this->move(100, 0);
}
Widget::~Widget()
{
delete ui;
}