Skip to content

Qt概述

qt 系列资料大多数都是来自于 爱编程的大丙 老师,原文地址 https://subingwen.cn/qt/

更多内容可参考 qt 官方文档

从CLI走向GUI

在之前的课程中,我们所编写的程序基本上都属于是控制台应用,又称为命令行交互界面(CLI,Command-Line Interface),应用的输入和输出都是在控制台终端上进行的(即所谓的"黑框框"程序)。对于资深用户来说,通过CLI的方式去使用应用程序可能是非常便利的,但是对于普通用户而言,CLI程序的使用门槛就太高了。因此,随着计算机硬件性能提升以及大分辨率显示器的出现,能够在显示器上面简单直观地显示功能和操作的GUI程序(Graphic User Interface)就流行起来了。

接下来我们的课程就要涉及到GUI程序的编写和创建。相比于CLI,GUI程序会更加复杂:一个典型的CLI程序只有一个输入设备和一个输出设备,通常执行的流程是这样的:先等待用户从输入设备中输入,再执行指定生成数据,最后输出到输出设备上——这种阻塞式的执行流程就意味着在设计CLI客户端程序时,程序员的思路一般是串行的,下面是一个简单的示例:

cpp
int main(){
    while(true){
        cout << ">>>\n";
        cin >> cmd;
        switch(cmd){
                 ...
        }
    }
}

而当设计GUI程序时,输入设备和输出设备数量情况就不一样了。在某一个时刻,用户可以选择在界面上的很多位置执行不同的操作,应用程序要同时根据这些不同位置的操作执行不同的业务逻辑导致输出不一样的结果。比如在下面的图形界面当中,用户的鼠标点击不同的区域,应用程序可能会弹出新菜单,或者是改变窗口大小,甚至还有可能退出程序;此外,用户也可以不点击鼠标,还可以执行鼠标拖动、键盘按下释放等等操作。

相较于CLI程序一般采用阻塞式的输入输出模型,GUI应用程序中会同时存在多个元素等待用户的不同操作。这就意味着程序在设计GUI程序的时候,应该要拥有并行的思维——GUI程序应该像我们之前写过的服务端程序一样使用事件机制或者是多线程来处理等待。出于性能方面的考虑,目前主流的GUI设计方案都是采用事件驱动模型。

当用户执行各种比如移动鼠标,点击按钮等等操作的时候,计算机会产生对应的硬件中断,一般计算机会在执行中断处理程序的过程中,将应用正在等待的资源准备就绪,最终使得应用程序能够根据用户需求顺利执行。我们把用户执行的各种操作称为事件。事件由用户产生,由操作系统统一收集和管理——操作系统会将事件合理地分配给各个应用程序。各个应用程序在收到事件之后,会根据事件的具体情况来决定后续要执行的代码。由于这些后续的代码不是无条件顺序执行的,而是需要根据事件的情况按需执行,所以程序编写的时候,这些代码往往是以回调函数的形式存在的。

Qt概述

什么是Qt

不论我们学习什么样的知识点首先第一步都需要搞明白它是什么,这样才能明确当前学习的方向是否正确,下面给大家介绍一下什么是Qt。

Qt是一个跨平台的C++应用程序开发框架

  • 具有短平快的优秀特质:投资少、周期短、见效快、效益高
  • 几乎支持所有的平台,可用于桌面程序开发以及嵌入式开发
  • 有属于自己的事件处理机制
  • 可以搞效率的开发基于窗口的应用程序。

Qt是标准C++的扩展,C++的语法在Qt中都是支持的

  • 良好封装机制使得Qt的模块化程度非常高,可重用性较好,可以快速上手。
  • Qt提供了一种称为信号槽(signals/slots)的安全类型来替代回调函数(Callback function),这使得各个元件之间的协同工作变得十分简单。

Qt的特点

知道了Qt是什么之后,给大家介绍一下Qt这个框架的一些优点,就是因为具有了这些优秀的特质才使得现在很多企业都首选Qt进行基于窗口的应用程序开发,并且近年来市场对Qt程序猿的需求也在不断攀升。

  • 广泛用于开发GUI程序,也可用于开发非GUI程序。(GUI = Graphical User Interface,开发GUI程序也就是基于窗口的应用程序开发。)
  • 有丰富的API:Qt包括多达250个以上的C++类,可以处理正则表达式。
  • 支持2D/3D图形渲染,支持OpenGL
  • Qt给程序猿提供了非常详细的官方文档
  • 支持XML,Json
  • 框架底层模块化,使用者可以根据需求选择相应的模块来使用
  • 可以轻松跨平台:和Java的跨平台方式不同,Qt在不同的平台使用的是相同的上层接口,但是在底层封装了不同平台对应的API(暗度陈仓)。

Qt案例

Qt发展至今已经是一个非常成熟的框架,基于这个框架许多公司开发出了很多优秀的软件,下边给大家介绍几款我们常见到的软件:

  • VirtualBox:虚拟机软件。
  • VLC多媒体播放器:一个体积小巧、功能强大的开源媒体播放器。
  • YY语音:又名“歪歪语音”,是一个可以进行在线多人语音聊天和语音会议的免费软件。
  • 咪咕音乐:咪咕音乐是中国移动倾力打造的正版音乐播放器
  • WPS Office:金山公司(Kingsoft)出品的办公软件,与微软Office兼容性良好,个人版免费。
  • Skype:一个使用人数众多的基于P2P的VOIP聊天软件。

Qt的模块化设计

Qt框架采用了一种模块化的设计:Qt 允许开发者按需选择并集成框架中的不同模块,以满足特定项目的需求。这种模块化设计提供了灵活性和可定制性,使得 Qt 能够应用于各种不同的应用场景。Qt的模块可以分为两类:核心模块和附加模块。其中核心模块实现了Qt的主要功能,并且确保在所有的平台下的效果表现一致。

Qt类库里大量的类根据功能分为各种模块,这些模块又分为以下几大类:

  • Qt基本模块(Qt Essentials):提供了Qt在所有平台上的基本功能。
  • Qt附加模块(Qt Add-Ons):实现一些特定功能的提供附加价值的模块。
  • 增值模块(Value-AddModules):单独发布的提供额外价值的模块或工具。
  • 技术预览模块(Technology Preview Modules):一些处于开发阶段,但是可以作为技术预览使用的模块。
  • Qt工具(Qt Tools):帮助应用程序开发的一些工具。

Qt官网或者帮助文档的 All Modules 页面可以查看所有这些模块的信息。以下是官方对Qt基本模块的描述。关于其他模块感兴趣的话可以自行查阅。

模块名称主要功能
QtCore- 提供基础数据结构,如字符串、数组、列表等
- 提供事件处理机制
- 提供文件和 IO 操作
- 提供线程管理和同步机制
- 提供日期和时间功能等
QtGUI- 提供 GUI 组件和工具,如窗口、按钮、标签、文本框等
- 提供绘图和渲染功能
- 提供事件处理和事件过滤器
- 提供剪贴板、拖放、键盘和鼠标事件处理等
QtWidgets- 使用C++继承了QtGUI以实现扩展功能
- 提供标准的桌面应用程序组件和窗口管理器
- 包括 QWidget、QMainWindow、QDialog 等组件
- 提供布局管理器用于界面布局
- 提供图标、样式、字体等可视化设置
- 提供对话框、消息框等常用窗口组件
QtNetwork- 提供网络通信功能,如 TCP 和 UDP 通信
- 提供 HTTP 客户端和服务器实现
- 提供 FTP 客户端和服务器实现
- 提供 SSL 和加密支持等
QtSQL- 提供数据库访问 API,支持多种数据库,如 SQLite、MySQL、PostgreSQL 等
- 提供 SQL 查询、事务管理、模型视图框架等
- 提供与数据库的连接和操作

第一个Qt项目

Qt开发环境安装完成之后,下面教大家如何使用QtCreator创建Qt项目。

创建项目

创建基于窗口的Qt应用程序

img

指定项目的存储路径

img

  • 项目名称根据需求自己指定即可
  • 在指定项目的存储路径的时候,路径中不能包含中文,不能包含中文,不能包含中文

指定默认的窗口类的名字以及窗口的类型

img

选择编译套件,编译套件用于项目文件的编译,如果安装了多个编译套件,在这里选择其中一个就可以了。

img

选择版本控制工具,比如: gitsvn 等,可以不指定。

img

项目文件(.pro)

在创建的Qt项目中自动生成了一个后缀为 .pro 的项目文件,该文件中记录着项目的一些属性信息,具体信息如下:

cmake
# 在项目文件中,注释需要使用 井号(#)
# 项目编译的时候需要加载哪些底层模块
QT       += core gui 

# 如果当前Qt版本大于4,会添加一个额外的模块: widgets
# Qt 5中对gui模块进行了拆分,将 widgets 独立出来了
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
   
# 使用c++11新特性
CONFIG += c++11 

#如果在项目中调用了废弃的函数,项目编译的时候会有警告的提示    
DEFINES += QT_DEPRECATED_WARNINGS

# 项目中的源文件
SOURCES += \
        main.cpp \
        mainwindow.cpp
        
# 项目中的头文件
HEADERS += \
        mainwindow.h
        
# 项目中的窗口界面文件
FORMS += \
        mainwindow.ui

更多关于 qmake 的信息参考附录

main.cpp

在这个源文件中有程序的入口函数 main(),下面给大家介绍下这个文件中自动生成的几行代码:

cpp
#include "mainwindow.h"     // 生成的窗口类头文件
#include <QApplication>     // 应用程序类头文件

int main(int argc, char *argv[])
{
    // 创建应用程序对象,在一个Qt项目中实例对象有且仅有一个
    // 类的作用: 检测触发的事件,进行事件循环并处理
    QApplication a(argc, argv);
    // 创建窗口类对象
    MainWindow w;
    // 显示窗口
    w.show();
    // 应用程序对象开始事件循环,保证应用程序不退出
    return a.exec();
}

mainwindow.ui

在Qt中每一个窗口都对应一个可编辑的可视化界面(*.ui),这个界面对应的是一个xml格式的文件,一般情况下不需要在xml格式下对这个文件进行编辑,关于这个文件结构了解即可。

xml
<!-- 双击这个文件看到的是一个窗口界面,如果使用文本编辑器打开看到的是一个XML格式的文件 -->
<!-- 看不懂这种格式没关系,我们不需要在这种模式下操作这个文件。 -->
<!-- 这里只是给大家介绍这个文件的本质 -->
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>800</width>
    <height>600</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>MainWindow</string>
  </property>
  <widget class="QWidget" name="centralwidget"/>
  <widget class="QMenuBar" name="menubar"/>
  <widget class="QStatusBar" name="statusbar"/>
 </widget>
 <resources/>
 <connections/>
</ui>

mainwindow.h

这个文件是窗口界面对应的类的头文件。

cpp
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>      // Qt标准窗口类头文件

QT_BEGIN_NAMESPACE
// mainwindow.ui 文件中也有一个类叫 MainWindow,将这个类放到命名空间 Ui 中
namespace Ui { class MainWindow; }  
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT    // 这个宏是为了能够使用Qt中的信号槽机制

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

private:
    Ui::MainWindow *ui;     // 定义指针指向窗口的 UI 对象
};
#endif // MAINWINDOW_H

mainwindow.cpp

这个文件是窗口界面对应的类的源文件。

cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)    // 基于mainwindow.ui创建一个实例对象
{
    // 将 mainwindow.ui 的实例对象和 当前类的对象进行关联
    // 这样同名的连个类对象就产生了关联,合二为一了
    ui->setupUi(this);
}

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

Qt中的窗口类

我们在通过Qt向导窗口基于窗口的应用程序的项目过程中倒数第二步让我们选择跟随项目创建的第一个窗口的基类,下拉菜单中有三个选项,分别为:QMainWindowQDialogQWidget如下图:

img

基础窗口类

窗口类继承关系

常用的窗口类有3个:QWidget、QDialog、QMainWindow。在创建Qt窗口的时候,需要让自己的窗口类继承上述三个窗口类的其中一个

QWidget

  • 所有窗口类的基类
  • Qt中的控件(按钮、输入框、单选框…)也属于窗口,基类都是QWidget
  • 可以内嵌到其他窗口中:没有边框
  • 可以不内嵌单独显示:独立的窗口,有边框

QDialog

  • 对话框类,后边的章节会具体介绍这个窗口
  • 不能内嵌到其他窗口中

QMainWindow

  • 有工具栏、状态栏、菜单栏,后边的章节会具体介绍这个窗口
  • 不能内嵌到其他窗口中

窗口的显示

内嵌窗口:依附于某一个大的窗口,作为了大窗口的一部分;大窗口就是这个内嵌窗口的父窗口;父窗口显示的时候,内嵌的窗口也就被显示出来了。

不内嵌窗口:这类窗口有边框,有标题栏;需要调用函数才可以显示。

cpp
// QWidget是所有窗口类的基类,调用这个提供的 show() 方法就可以显示将任何窗口显示出来
// 非模态显示
void QWidget::show();   // 显示当前窗口和它的子窗口

// 对话框窗口的非模态显示: 还是调用show() 方法
// 对话框窗口的模态显示
[virtual slot] int QDialog::exec();

坐标体系

在Qt关于窗口的显示是需要指定位置的,这个位置是通过坐标来确定的,所有坐标的选取又都是基于坐标原点来确定的。

窗口的坐标原点

所有坐标的确定都需要先找到坐标原点,Qt的坐标原点在窗口的左上角

x轴向右递增,y轴向下递增

坐标原点

窗口的相对坐标

在一个Qt窗口中一般都有很多子窗口内嵌到这个父窗口中,其中每个窗口都有自己的坐标原点,子窗口的位置也就是其使用的坐标点就是它的父窗口坐标体系中的坐标点。

子窗口使用父窗口的坐标体系

  • 在Qt的某一个窗口中有可能有若干个控件,这个控件都是嵌套的关系:A窗口包含B窗口,B窗口包含C窗口
  • 每个窗口都有坐标原点,在左上角。子窗口的位置是基于父窗口的坐标体系来确定的,也就是说通过父窗口左上角的坐标点来确定自己的位置
  • Qt中窗口显示的时候使用的相对坐标,相对于自己的父窗口

将子窗口移动到父窗口的某个位置

cpp
// 所有窗口类的基类: QWidget
// QWidget中提供了移动窗口的 API函数
// 参数 x, y是要移动的窗口的左上角的点,窗口的左上角移动到这个坐标点
void QWidget::move(int x, int y);
void QWidget::move(const QPoint &);

内存回收

在Qt中创建对象的时候会提供一个 Parent对象指针(可以查看类的构造函数),下面来解释这个parent到底是干什么的。

QObject是以对象树的形式组织起来的。当你创建一个QObject对象时,会看到QObject的构造函数接收一个QObject指针作为参数,这个参数就是 parent,也就是父对象指针。这相当于,在创建QObject对象时,可以提供一个其父对象,我们创建的这个QObject对象会自动添加到其父对象的children()列表。当父对象析构的时候,这个列表中的所有对象也会被析构。(注意,这里的父对象并不是继承意义上的父类!

QWidget是能够在屏幕上显示的一切组件的父类。QWidget继承自QObject,因此也继承了这种对象树关系。一个孩子自动地成为父组件的一个子组件。因此,它会显示在父组件的坐标系统中,被父组件的边界剪裁。例如,当用户关闭一个对话框的时候,应用程序将其删除,那么,我们希望属于这个对话框的按钮、图标等应该一起被删除。事实就是如此,因为这些都是对话框的子组件。

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

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

对象树

综上所述,我们可以得到一个结论:Qt中有内存回收机制,但是不是所有被new出的对象被自动回收,满足条件才可以回收,如果想要在Qt中实现内存的自动回收,需要满足以下两个条件:

  1. 创建的对象必须是QObject类的子类(间接子类也可以)
    • QObject类是没有父类的,Qt中有很大一部分类都是从这个类派生出去的
      • Qt中使用频率很高的窗口类和控件都是 QObject 的直接或间接的子类
      • 其他的类可以自己查阅Qt帮助文档
  2. 创建出的类对象,必须要指定其父对象是谁,一般情况下有两种操作方式:
cpp
// 方式1: 通过构造函数
// parent: 当前窗口的父对象,找构造函数中的 parent 参数即可
QWidget::QWidget(QWidget *parent = Q_NULLPTR, Qt::WindowFlags f = Qt::WindowFlags());
QTimer::QTimer(QObject *parent = nullptr);

// 方式2: 通过setParent()方法
// 假设这个控件没有在构造的时候指定符对象,可以调用QWidget的api指定父窗口对象
void QWidget::setParent(QWidget *parent);
void QObject::setParent(QObject *parent);

log输出

输出日志到调试窗口

在Qt中进行log输出,一般不使用c中的printf,也不是使用C++中的cout,Qt框架提供了专门用于日志输出的类,头文件名为 QDebug,使用方法如下:

cpp
// 包含了QDebug头文件,直接通过全局函数 qDebug() 就可以进行日志输出了
qDebug() << "Date:" << QDate::currentDate();
qDebug() << "Types:" << QString("String") << QChar('x') << QRect(0, 10, 50, 40);
qDebug() << "Custom coordinate type:" << coordinate;

// 和全局函数 qDebug() 类似的日志函数还有: qWarning(),qInfo(),qCritical()
int number = 666;
float i = 11.11;
qWarning() << "Number:" << number << "Other value:" << i;
qInfo() << "Number:" << number << "Other value:" << i;
qCritical() << "Number:" << number << "Other value:" << i;

qDebug() << "我是要成为海贼王的男人!!!";
qDebug() << "我是隔壁的二柱子...";
qDebug() << "我是鸣人,我擅长嘴遁!!!";

日志信息在IDE的调试窗口输出

输出到调试窗口

输出日志到终端窗口

使用上面的方法只能在项目调试过程中进行日志输出,如果不是通过IDE进行程序调试,而是直接执行可执行程序在这种情况下是没有日志输出窗口的,因此也就看不到任何的日志输出。

默认情况下日志信息是不会打印到终端窗口的,如果想要实现这样的效果,必须在项目文件中添加相关的属性信息

打开项目文件(*.pro)找到配置项 config,添加 console 控制台属性:

cmake
CONFIG += c++11 console

属性信息添加完毕,重新编译项目 日志信息就可以打印到终端窗口了

输出到终端

打印QString时去除引号

方式一

cpp
QString.toUtf8().data()

方式二

cpp
qUtf8Printable(QString);