简介

  • 本书主要介绍如何使用Qt进行C++应用程序开发。
  • Qt实际上是一套应用程序开发类库,Qt类库由许多模块组成,例如核心的GUI组件模块Qt Widget,用于数据库访问的Qt SQL模块,用于二维图表显示的Qt Charts模块,用于数据三位显示的Qt Data Visualization模块,用于网络编程的Qt Network模块等。

第一章 认识Qt

1.1 Qt简介

  • C++是一种通用的标准编程语言,使用任何编辑器都可以编写C++源程序,然后利用C++编译器对程序进行编译,就可以生成可执行的程序。
  • 为了方便进行C++程序的编写和编译,有各种综合开发环境(Integrated Developing Environment, IDE),例如Visual Studio就是Windows平台上常见的编写C++程序的IDE。一个IDE不仅提供程序的编辑和编译,一般还提供一套基本类库,用于提供支持平台应用程序开发的各种基本类,例如Visual Studio使用MFC进行Windows平台的应用程序开发。
  • Qt是一套应用程序开发类库,但是与MFC不同,Qt是跨平台的开发类库。Qt支持PC和服务器的平台,包括Windows,Linux,macOS等,还支持移动和嵌入式操作系统,例如IOS,Embedded Linux,Andriod,WinRT等。跨平台意味着只需要编写一次程序,在不同平台上无需改动或只需要少许改动后在编译,就可以形成不同平台上运行的版本。

1.2 Qt的获取和安装

1.2.1 Qt的许可类型

  • Qt的许可类型分为商业许可和开源许可,开源许可又分为LGPLV3和GPLV2/GPLV3.

1.2.2 Qt的版本

  • Qt的版本更新比较快,且版本更新时会新增一些类或者停止维护一些以前版本的类。如果不是为了维护用旧版本编写的程序,一定要选用最新版本的Qt进行程序开发。

1.2.3 Qt的下载和安装

  • 从Qt官网可以下载最新版本的Qt软件。根据开发项目的不同,Qt分为桌面和移动设备应用开发,嵌入式设备开发两大类不同的安装包。
    • 桌面和移动设备应用开发就是开发在PC,服务器,手机,平板电脑等设备上运行的程序,操作系统平台可以是Windows,Linux,macOS,Andriod等。
    • 嵌入式设备开发是针对具体的嵌入式设备来开发应用程序,例如物联网设备,汽车电子设备,医疗设备等特定的嵌入式设备。
  • Qt的安装包分为在线安装包和离线安装包,为便于重复安装,最好下载离线安装包。
  • 在安装过程中会出现安装选项设置页面,在这个页面里选择需要安装的模块。这些模块包括内容如下
    • MinGW 编译器模块。MinGW是Minimalist GNU For Windows的缩写,MinGW是Windows平台上使用的GNU工具集导入库的集合。
    • 用于UWP编译的模块。UWP是Windows 10中Universal Windows Platform的简称,有不同编译器类型的UWP
    • 用于Windows平台上的MSVC编译器模块。要安装MSVC编译器的模块,需要计算机上已经安装相应版本的Visual Studio
    • 用于Andriod平台的模块,例如 Andriod x86和Android ARMv7
    • Sources是Qt的源程序类
    • Qt Charts是二维图标模块,用于绘制柱状图,饼图,曲线图等常用二维图表
    • Qt Data Visualization是三维数据图表模块,用于数据的三维显示,例如散点的三维空间分布,三维曲面等
    • Qt Purchsing,Qt WebEngine, Qt Network Auth(TP)等其他模块,括号里的TP表示技术预览(Technology Preview)
    • Qt Scritp(Deprecated)是脚本模块,括号里的”Deprecated”表示这是个已经过时的模块
  • “Tools”节点下面是一些工具软件,包括内容如下
    • Qt Creator 是用于Qt程序开发的IDE
    • MinGW 是MinGW编译工具链
    • StrawBerry Perl是一个Perl语言工具
  • 工具软件有
    • Assistant 是一个独立的查看Qt帮助文件的程序,集成在了Qt Creator中
    • Designer 是一个独立的进行窗口,对话框等界面可视化设计的程序。Designer也集成在了Qt Creator中,在Qt Creator中编辑或创建界面文件时,就可以自动打开并进行界面设计。
    • Linguist是一个编辑语言资源文件的程序,在开发多语言界面的应用程序时会用到。
  • 这三个工具软件可独立使用,前两个集成到了Qt Creator里,可在Qt Creator打开。所以Qt的主要工具是Qt Creator。

1.3 Qt Creator初步使用

1.4 编写一个Hello World程序

1.4.1 新建一个项目

  • Qt Creator可以创建多种项目,各类应用程序如下
    • Qt Widgets Application,支持桌面平台的有图形用户界面(Graphic User Interface,GUI)界面的应用程序。GUI的设计完全基于C++语言,采用Qt提供的一套C++类库。
    • Qt Console Application,控制台应用程序,无GUI界面,一般用于学习C/C++语言。
    • Qt Quick Application, 创建可部署的Qt Quick 2应用程序。Qt Quick是Qt支持的一套GUI开发架构,其界面设计采用QML语言,程序架构采用C++语言。利用Qt Quick可以设计非常炫的用户界面,一般用于移动设备或嵌入式设备上无边框的应用程序的设计。
    • Qt Quick Controls 2 Application,创建基于Qt Quick Controls 2组件的可部署的Qt Quick 2应用程序。
    • Qt Canvas 3D Application,创建Qt Canvas 3D QML项目,也是基于QML语言的界面设计,支持3D画布。
  • 界面的基类(base class),有3中基类可以选择
    • QMainWindow 是主窗口类,主窗口具有主菜单栏,工具栏和状态栏,类似于一般的应用程序的主窗口;
    • QWidget 是所有具有可视界面类的基类,选择QWidget创建的界面对各种界面组件都可以支持
    • QDialog 是对话框类,可建立一个基于对话框的界面

第二章 GUI应用程序设计基础

  • 本章深入的介绍Qt Creator设计GUI应用程序的基本方法,包括Qt创建的应用程序项目的 基本组织结构,可视化设计的UI界面文件的原理和运行机制,信号与槽的使用方法,窗体可视化设计的底层原理,应用程序的窗体,组件布局,菜单,工具栏,Actions等常见设计元素的使用方法。

2.1 UI文件设计与运行机制

2.1.1 项目文件组成

  • 一个Widget Application项目包含一下一些文件
    • 项目组织文件 samp2_1.pro,存储项目设置的文件
    • 主程序入口文件main.cpp,实现main()函数的程序文件
    • 窗体界面文件widget.ui,一个XML格式存储的窗体上的元件及其布局的文件
    • widget.h是所设计的窗体类的头文件,widget.cpp是widget.h里定义类的实现文件。在C++里,任何窗体或界面组件都是用类封装的,一个类一般有一个头文件和一个源文件。

2.1.2 项目管理文件

  • 后缀为.pro的文件是项目的管理文件,文件名就是项目的名称

2.1.3 界面文件

  • 后缀为 .ui 的文件是可视化设计的窗体的定义文件,例如 widget.ui
  • 本书后面将成这个集成在Qt Creator中的Qt Designer为 UI设计器,以便与独立运行的Qt Designer区别开来

2.1.4 主函数文件

  • main函数是应用程序的入口。它的主要功能是创建应用程序,创建窗口,显示窗口,并运行从应用程序,开始应用程序的消息循环和事件处理。

2.1.5 窗体相关的文件

  • 为了搞清楚窗体类的定义,以及界面功能的实现原理,香茗居编译后会自动生成一个文件 ui_widget.h,这样对于一个窗体,就有四个文件
    • widget.h 定义窗体类的头文件定义了类Widget
    • widget.cpp Widget类的功能实现源程序文件
    • widget.ui 窗体界面文件,由UI设计器自动生成,存储了窗体上各个组件的属性设置和布局
    • ui_widget.h 编译后,根据窗体上的组件及其属性,信号与槽的关联自动生成的一个类的定义文件,类的名称是Ui_Widget

2.2

2.2.3 信号与槽

  • 信号与槽(Signal & Slot)是Qt编程的基础,也是Qt的一大创新。因为有了信号与槽的编程机制,在Qt中处理界面各个组件的交互操作时变得更加直观和简单。
  • 信号(Signal)就是在特定情况下被发射的事件,例如PushButton最常见的信号就是鼠标单击时发射的clicked()信号,一个ComboBox最常见的信号是选择的列表项变化时发射的CurrentIndexChanged()信号。GUI程序设计的主要内容就是对界面上各组件的信号的相应,只需要知道什么情况下发射哪些信号,合理的去响应和处理这些信号就可以了。
  • 槽(Slot)就是对信号响应的函数。槽就是一个函数,与一般的C++函数是一样的,可以定义在类的任何部分(public, private或protected),可以具有任何参数,也可以被直接调用。槽函数与一般函数不同的是: 槽函数可以与一个信号关联,当信号被发射时,关联的槽函数被自动执行
  • 信号与槽关联是用QObject::connect()函数实现的,其基本格式是:
    1
    
    QObject::connect(sender, SIGNAL(signal()), reciver, SLOT(slot()));
    
  • connect()是QObject类的一个静态函数,而QObject是所有Qt类的基类,在实际调用时可以忽略前面的限定符,所以可以直接写为:
    1
    
    connect(sender, SIGNAL(signal()), reciver, SLOT(slot()));
    
    • 其中,sender是发射信号的对象的名称,signal()是信号名称。信号可以看作是特殊的函数,需要带括号,有参数时还需要指明参数。receiver是接收信号的对象名称,slot()是槽函数的名称,需要带括号,有参数时还需要指明参数。
    • SIGNAL和SLOT是Qt的宏,用于指明信号和槽,并将它们的参数转换为相应的字符串。例如
      1
      
      QObject::connect(btnClose, SIGNAL(clicked()), Widget, SLOT(close()));
      
    • 其作用就是将btnClose按钮的clicked()信号与窗体(Widget)的槽函数close()相关联,这样,当单击btnClose按钮时,就会执行Widget的Close()槽函数。
  • 关于信号与槽的使用,有以下一些规则需要注意
    • 一个信号可以连接多个槽,例如
      1
      2
      
      connect(spinNum, SIGNAL(valueChanged(int)), this, SLOT(addFun(int)));
      connect(spinNum, SIGNAL(valueChanged(int)), this, SLOT(updateStatus(int)));
      
      • 这是当一个对象spinNUm的数值发生变化时,所在窗体有两个槽进行响应,一个addFun()用于计算,一个updateStatus()用于更新状态。
      • 当一个信号与多个槽函数关联时,槽函数按照建立连接时的顺序依次执行。
      • 当信号和槽函数带有参数时,在connect()函数里,要写明参数的类型,但可以不写参数名称。
    • 多个信号可以连接同一个槽,例如
      1
      2
      3
      
      connect(ui->rBtnBlue, SIGNAL(clicked()), this, SLOT(setTextFontColor()));
      connect(ui->rBtnRed, SIGNAL(clicked()), this, SLOG(setTextFontColor()));
      connect(ui->rBtnBlack, SIGNAL(clicked()), this, SLOG(setTextFontColor()));
      
      • 这样,当任何一个RadioButton被单击时,都会执行setTextFontColor()函数。
    • 一个信号可以连接另外一个信号,例如
      1
      
      connect(spinNum, SIGNAL(valueChanged(int)), this, SIGNAL(refreshInfo(int)));
      
      • 这样,当一个信号发射时,也会发射另外一个信号,实现某些特殊的功能。
    • 严格情况下,信号与槽的参数个数和类型需要一致,至少信号的参数不能少于槽的参数。如果不匹配,会出现编译错误或运行错误。
    • 在使用信号与槽的类中,必须在类的定义中加入宏Q_OBJECT
    • 当一个信号被发射时,与其关联的槽函数通常被立即执行,就像正常调用一个函数一样。只有当信号关联的所有槽函数执行完毕后,才会执行发射信号处后面的代码。
  • 信号与槽机制是Qt GUI编程的基础,使用信号与槽机制可以比较容易的将信号与响应代码关联起来。

第三章 Qt类库概述

  • Qt是一个用标准C++编写的跨平台开发类库,它对标准C++进行了扩展,引入了元对象系统,信号与槽,属性等特性,使应用程序的开发变得更高效。本章将介绍Qt的这些核心特点,对于理解和编写高效的Qt C++程序是大有帮助的。
  • 本章还介绍头文件中Qt的一些全局定义,包括数据类型,函数和宏等,介绍Qt的容器类及其响应迭代器的使用方法。这些全局定义和容器类在程序中经常用到,了解其原理便于理解后面遇到的一些实例程序。
  • Qt类库中大量的类是以模块形式分类组织的,包括基本模块和扩展模块等,本章对这些模块做一个总体的介绍。一个模块通常就是一个编程主题,例如数据库,图表,网格等。本书后面的章节一般是每章介绍一个编程主题。

3.1 Qt核心特点

3.1.1 概述

  • Qt本身并不是一种编程语言,它实质上是一个跨平台的C++开发类库,是用标准C++编写的类库,它为开发GUI应用程序和非GUI应用程序提供了各种类。
  • Qt对标准C++进行了扩展,引入了一些新的概念和功能,例如信号与槽,对象属性等。Qt的元对象编译器(Meta-Object Compiler, MOC)是一个预处理器,在源程序被编译前先将这些Qt特性的程序转换为标准C++兼容的形式,然后再由标准C++编译器进行编译。这就是为什么在使用信号与槽机制的类里,必须添加一个Q_OBJECT宏的原因,只有添加了这个宏,moc才能对类里的信号与槽的代码进行预处理。

  • Qt Core模块是Qt类库的核心,所有其他模块都依赖于此模块。Qt 为C++语言增加的特性就是在Qt Core模块里实现的,这些扩展特性由Qt的元对象系统实现,包括信号与槽机制,属性系统,动态类型转换等。

3.1.2 元对象系统

  • Qt的元对象系统(Meta-Object System)提供了对象之间通信的信号与槽机制,运行时类型信息和动态属性系统。
  • 元对象系统由以下三个基础组成
    • QObject类是所有使用元对象系统的类的基类。
    • 在一个类的private部分声明Q_OBJECT宏,使得类可以使用元对象的特性,例如动态属性,信号与槽。
    • MOC(元对象编译器)为每个QObject的子类提供必要的代码来实现元对象系统的特性。
  • 构建项目时,MOC工具读取C++源文件,当它发现类的定义里有Q_OBJECT宏时,它就会为这个类生成另外一个包含有元对象支持代码的C++源文件,这个生成的源文件连同类的实现文件一起被编译和链接。

  • 除了信号与槽机制外,元对象还提供如下一些功能
    • QOBject::metaObject()函数返回类关联的元对象,元对象类QMetaObject包含了访问对象的一些接口函数,例如QMetaObject::className()函数可在运行时返回类的名称字符串。
      1
      2
      
      QObject *obj = new QPushButton;
      obj->metaObject()->className();  // 返回"QPushButton
      
    • QMetaObject::newInstance()函数创建类的一个新的实例
    • QObject::inherits(const char* className)函数判断一个对象实例是否是名称为className的类或QObject的子类的实例。例如
      1
      2
      3
      4
      
      QTimer *timer = new QTimer; // QTimer是QObject的子类
      timer->inherits("QTimer");  // 返回true
      timer->inherits("QObject"); // 返回true
      timer->inherits("QAbstractButton"); // 返回false,不是QAbstractButton的子类
      
    • QObject::tr()和QObject::trUtf8()函数可翻译字符串,用于多语言界面设计。
    • QObject::setProperty()和QObject::property()函数用于通过属性名称动态设置和获取属性值。
  • 对于QObject及其子类,还可以使用qobject_cast()函数进行动态映射(dynamic cast)。
  • 使用动态投射,使得程序可以在运行时对不同的对象做不同的处理。

3.1.3 属性系统

  • Qt提供一个 Q_PROPERTY()宏可以定义属性,它也是基于元对象系统实现的。Qt的属性系统与C++编译器无关,可以用任何标准的C++编译器编译定义了属性的QtC++程序。
  • Q_PROPERTY宏定义一个返回值类型为type,名称为name的属性,用READ,WRITE关键字定义属性的读取,写入函数,还有其他的一些关键字定义属性的一些操作特性。属性的类型可以是QVariant支持的任何类型,也可以用户自定义类型。
  • Q_PROPERTY宏定义属性的一些主要关键字的意义如下
  • READ: 指定一个读取属性值的函数,没有MEMBER关键字时必须设置READ
  • WRITE: 指定一个设定属性值的函数,只读属性没有WRITE设置
  • MEMBER: 指定一个成员变量与属性关联,成为可读可写的属性,无需再设置READ和WRITE
  • RESET: 是可选的,用于指定一个设置属性缺省值的函数
  • NOTIFY: 是可选的,用于设置一个信号,当属性值变化时发射此信号
  • DESIGNABLE: 表示属性是否在Qt Designer里可见,缺省为true
  • CONSTANT: 表示属性值是一个常数,对于一个对象实例,READ指定的函数返回值是常数,但是每个实例的返回值可以不一样。具有CONSTANT关键字的属性不能有WRITE和NOTIFY关键字。
  • FINAL: 表示所定义的属性不能被子类重载

3.1.4 信号与槽

  • 信号与槽是Qt的一个核心特点,也是它区别于其他框架的重要特性。信号与槽是对象间进行通信的机制,也需要由Qt的元对象系统支持才能实现的。
  • Qt使用信号与槽的机制实现对象间通信,它隐藏了复杂的底层实现,完成信号与槽的关联后,发射信号时并不需要知道Qt是如何找到槽函数的。Qt的信号与槽机制与Delphi和C++ Builder的”事件–响应”比较类似,但是更加灵活。

  • 某些开发架构使用回调函数(callback)实现对象间通信。与回调函数相比,信号与槽的执行速度稍微慢一点,因为需要查找连接的对象和槽函数,但是这种差别在应用程序运行时是感觉不到的,而其提供的灵活性却比回调函数强很多。
  • 对信号与槽的特点和用法做一些补充
    1. connect()函数的不同参数形式
      • QObject::connect()函数有多重参数形式,一种参数形式的函数原型是
        1
        
        QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection);
        
      • 使用这种参数形式的connect()进行信号与槽函数的连接时,一般句法如下
        1
        
        connect(sender, SIGNAL(signal()), receiver, SLOT(slot()));
        
      • 这里使用了宏SIGNAL()和SLOT()指定信号和槽函数,而且如果信号和槽函数带有参数,还需要注明参数类型,例如
        1
        
        connect(spinNum, SIGNAL(valueChanged(int)), this, SLOT(updateStatus(int)));
        
      • 另外一种参数形式的connect()函数的原型是:
        1
        
        QMetaObject::Connection QObject::connect(const QObject *sender, const QMetaMethod &signal, const QObject *receiver, const QMetaMethod &method, Qt::ConnectionType type = Qt::AutoConnection);
        
      • 对于具有默认参数的信号与槽(即信号名称是唯一的,没有参数不同而同名的两个信号),可以使用这种函数指针形式进行关联,例如
        1
        
        connect(lineEdit, &QLineEdit::textChanged, this, &widget::on_textChanged);
        
  • 不管是哪种参数形式的connect()函数,最后一个参数Qt::ConnectionType type,缺省值为Qt::AutoConnection。枚举类型Qt::ConnectionType表示了信号与槽之间的关联方式,有以下几种取值:
    • Qt::AutoConnection(缺省值): 如果信号的接收者与发射者在同一个线程,就使用Qt::DirectConnection方式;否则使用Qt::QueuedConnection方式,在信号发射时自动确定关联方式
    • Qt::DirectConnection: 信号被发射时槽函数立即执行,槽函数与信号在同一个线程
    • Qt::QueuedConnection: 在事件循环回到接收者线程后执行槽函数,槽函数与信号在不同的线程。
    • Qt::BlockingQueuedConnection: 与Qt::QueuedConnection相似,只是信号线程会阻塞直到槽函数执行完毕。当信号与槽函数在同一个线程时绝对不能使用这种方式,否则会造成死锁。 2. 使用sender()获得信号发射者
      • 在槽函数里,使用QObject::sender()可以获取信号发射者的指针。如果知道信号发射者的类型,可以将指针投射为确定的类型,然后使用这个确定类的接口函数。 3. 自定义信号及其使用
      • 在自己设计的类里也可以自定义信号,信号就是在类定义里声明的一个函数,但是这个函数无需实现,只需要发射(emit)
      • 例如,在下面的自定义类QPerson的signals部分定义一个信号ageChanged(int)
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        
        class QPerson : public QObject
        {
        Q_OBJECT
        private:
        int m_age = 10;
        public:
        void incAge();
        signals:
        void ageChanged(int value);
        }
        
    • 信号函数必须是无返回值的函数,但是可以有输入参数。信号函数无需实现,只需在某些条件下发射信号。例如,在incAge()函数中发射信号,其代码如下
      1
      2
      3
      4
      5
      
      void QPerson::incAge()
      {
      m_age++;
      emit ageChanged(m_age); // 发射信号
      }
      
    • 在incAge()函数里,当私有变量m_age变化后,发射信号ageChanged(int),表示年龄发生了变化。至于是否有此信号相关联的槽函数,信号发射者并不管。如果在使用QPerson类对象的程序中为此信号关联了槽函数,在incAge()函数里发射此信号时,就会执行相关联的槽函数。至于是否立即执行槽函数,发射信号的线程是否等待槽函数执行完之后再执行后面的代码,与connect()函数设置信号与槽关联时设置的连接类型以及信号与槽是否在同一个线程有关。

3.2 Qt全局定义

  • 头文件包含了Qt类库的一些全局定义,包括基本数据类型,函数和宏,一般的Qt类的头文件都会包含该文件,所以不用显示包含这个头文件也可以使用其中的定义。

3.2.1 数据类型定义

  • 为了确保在各个平台上各数据类型都有统一确定的长度,Qt为各种常见数据类型定义了类型符号,例如qint8就是signed char的类型定义,即 typedef signed char qint8;

3.2.2 函数

  • 头文件包含一些常用函数的定义,这些函数多以模板类型作为参数,返回相应的模板类型,模板类型可以用任何其他类型替换。若是以double或float类型作为参数的,一般有两个参数版本的额同名函数,例如qFuzzyIsNull(double d)和qFuzzyIsNull(float f)。
  • 还有一些基础的数学运算函数在头文件中定义,例如三角运算函数,弧度与角度之间的转换函数等。

3.2.3 宏定义

  • 头文件中定义了很多宏,以下是一些比较常用的
  • QT_VERSION
    • 这个宏展开为数值形式0xMMNNPP(MM = major, NN = minor, PP = patch)表示Qt编译器版本,例如Qt编译器版本为Qt 5.9.1,则QT_VERSION为0x050901。这个宏常用于条件编译设置,根据Qt版本不同,编译不同的代码段。
      1
      2
      3
      4
      5
      6
      
      #if QT_VERSION >= 0x040100
      QIcon icon = style()->standardIcon(QStyle::SP_TrashIcon);
      #else
      QPixmap pixmap = style()->standardPixmap(QStyle::SP_TrashIcon);
      QIcon icon(pixmap);
      #endif
      
  • QT_VERSION_CHECK
    • 这个宏展开为Qt版本号的一个整数表示,例如
      1
      2
      3
      4
      5
      
      #if (QT_VERSION >= QT_VERSION_CHECK(5, 0, 0))
      #include <QtWidgets>
      #else
      #include <QtGui>
      #endif
      
  • QT_VERSION_STR
    • 这个宏展开为Qt版本号的字符串,例如 “5.9.0”
  • Q_BYTE_ORDER, Q_BIG_ENDIAN 和 Q_LITTLE_ENDIAN
    • Q_BYTE_ORDER 表示系统内存中数据的字节序,
    • Q_BIG_ENDIAN 表示大端字节序
    • Q_LITTLE_ENDIAN 表示小端字节序。
    • 在需要判断系统字节序时会用到,例如
      1
      2
      3
      
      #if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
      ...
      #endif
      
  • Q_DECL_IMPORT, Q_DECL_EXPORT
    • 在使用或设计共享库时,用于导入或到处库的内容
  • Q_DECL_OVERRIDE
    • 在类定义中,用于重载一个虚函数,例如在某个类中重载虚函数paintEvent(),可以定义如下
      1
      
      void paintEvent(QPaintEvent*) Q_DECL_OVERRIDE;
      
    • 使用Q_DECL_OVERRIDE宏后,如果重载的虚函数没有进行任何重载操作,编译器将会报错。
  • Q_DECL_FINAL
    • 这个宏将一个虚函数定义为最终级别,不能再被重载,或定义一个类不能再被继承,实例如下
      1
      2
      3
      
      class QRect Q_DECL_FINAL {  // QRect不能再被继承
      // ...
      };
      
  • Q_UNUSED(name)
    • 这个宏用于在函数中定义不在函数体里使用的参数,示例如下
      1
      2
      3
      4
      5
      
      void MainWindow::on_imageSaved(int id, const QString &filename)
      {
      Q_UNUSED(id);
      LabInfo->setText("图片保存为: " + filename);
      }
      
    • 在这个函数里,id参数没有使用。如果不用Q_UNUSED(id)定义,编译器会出现参数未使用的警告。
  • foreach(variable, container)
    • foreach用于容器类的遍历,例如
      1
      2
      
      foreach (const QString &codecName, recorder->supportedAudioCodecs())
      ui->comboCodec->addItem(codecName);
      
  • forever
    • forever用于构造一个无限循环,例如
      1
      2
      3
      
      forever {
      ...
      }
      
  • qDebug(const char *message, …)
    • 在debugger窗体显示信息,如果编译器设置了 Qt_NO_DEBUG_OUTPUT,则不作任何输出,例如
      1
      
      qDebug("Item in list: %d", myList.size());
      
  • 类似的宏还有 qWarning, qCritical, qFatal, qInfo等,也是用于在debugger窗体显示信息。

3.3 容器类

3.3.1 容器类概述

  • Qt提供了多个基于模板的容器类,这些容器类可以用于存储指定类型的数据项,例如常用的字符串列表类QStringList就是从容器类QList继承的,实现对字符串列表的添加,存储,删除等操作。
  • Qt的容器壁标准模板库(STL)中的容器类更轻巧,安全和易于使用。这些容器类是隐式共享和可重入的,而且他们进行了速度和存储优化,因此可以减少可执行文件的大小。此外,它们还是线程安全的,也就是说它们作为只读容器时可以被多个线程访问

  • 容器类是基于模板的类,如常用的容器类QList,T是一个具体的类型,可以是int, float等简单的类型,也可以是QString, QDate等类,但是不能是QObject或任何其子类。T必须是一个可赋值的类型,即T必须提供一个缺省的构造函数,一个可复制构造函数和一个赋值运算符。
  • Qt的容器类分为顺序容器(sequential containers)和关联容器(associative containers)
  • 容器迭代类用于遍历容器里的数据项,有Java类型的迭代类和STL类型的迭代类。Java类型的迭代类易于使用,提供高级功能,而STL类型的迭代类效率更高一些。
  • Qt还提供了foreach宏用于遍历容器内的所有数据项。

3.3.2 顺序容器类

  • Qt的顺序容器类有 QList, QLinkedList, QVector, QStack和QQueue

  • QList
    • QList是最常用的容器类,虽然它是以数组列表(array-list)的形式实现的,但是在其前或后添加数据非常快。QList以下标索引的方式对数据项进行访问。
  • QLinkedList
    • QLinkedList是链式列表(linked-list),数据项不是用连续的内存存储的,它基于迭代器访问数据项,并且插入和删除数据项的操作时间相同。 除了不提供基于下标索引的数据项访问外,QLinkedList的其他接口函数与QList基本相同。
  • QVector
    • QVector提供动态数组的功能,以下标索引访问数据。
    • QVector的函数接口与QList几乎完全相同,QVector的性能比QList更高,因为QVector的数据项是连续存储的。
  • QStack
    • QStack是提供类似于堆栈的后入先出(LIFO)操作的容器类,push()和pop()是主要的接口函数。
  • QQueue
    • QQueue是提供类似于队列先入先出(FIFO)操作的容器类。enqueue()和dequeue()是主要的操作函数

3.3.3 关联容器类

  • Qt还提供关联容器类 QMap, QMultiMap, QHash, QMultiHash和QSet
  • QMultiMap和QMultiHash支持一个键关联多个值,QHash和QMultiHash类使用散列(Hash)函数进行查找,查找速度较快

  • QSet
    • QSet是基于散列列表的集合模板类,它存储数据的顺序是不定的,查找值的速度非常快。QSet内部就是用QHash实现的。
    • 测试一个值是否包含于这个集合,用contains()函数
  • QMap
    • QMap<Key, T>提供一个字典(关联数组),一个键映射到一个值。QMap存储数据是按照键的顺序,如果不在乎存储顺序,使用QHash会更快
  • QMultiMap
    • QMultiMap是QMap的子类,是用于处理多值映射的便利类
  • QHash
    • QHash是基于散列表来实现字典功能的模板类,QHash<Key, T>存储的键值对具有非常快的查找速度。
  • QHash与QMap的功能和用法相似,区别在于以下几点
    • QHash比QMap的查找速度更快
    • 在QMap上遍历时,数据项是按照键排序的,而QHash的数据项是任意顺序的
    • QMap的键必须提供 “<” 运算符,QHash的键必须提供 “==” 运算符和一个名称为 qHash() 的全局散列函数
  • QMultiHash
    • QMultiHash是QHash的子类,是哟弄个与处理多值映射的便利类,其用法与QMultiMap类似

3.4 容器类的迭代

  • 迭代器(iterator)为访问容器类里的数据项提供了统一的方法,Qt有两种迭代器类: Java类型的迭代器和STL类型的迭代器
  • Java类型的迭代器更易于使用,且提供一些高级功能,而STL类型的迭代器效率更高。

3.4.1 Java类型迭代器

3.4.2 STL类型迭代器

  • STL迭代器与Qt和STL的原生算法兼容,并且进行了速度优化
  • 对于每一个容器类,都有两个STL类型迭代器:一个用于只读访问,一个用于读写访问。无需修改数据时一定使用只读迭代器,因为它们速度更快。

  • STL类型的迭代器是数组的指针,所以 “++” 运算符使迭代器指向下一个数据项, “*“运算符返回数据项内容。

  • 隐式共享(Implicit Sharing)是对象的管理方法,一个对象被隐式共享,只是传递该对象的一个指针给使用者,而不实际复制对象数据,只有在使用者修改数据时,才实质复制共享对象给使用者。
  • 对于STL类型的容器,隐式共享还涉及到另外一个问题,即当有一个迭代器在操作一个容器变量时,不要去复制这个容器变量。

3.4.3 foreach关键字

  • 如果只是想遍历容器中所有的项,可以使用foreach关键字。foreach是头文件中定义的一个宏。
  • foreach关键字遍历一个容器变量是创建了容器的一个副本,所以不能修改原来容器变量的数据项

3.5 Qt类库的模块

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

3.5.1 Qt基本模块

  • Qt基本模块是Qt在所有平台上的基本功能,它们在所有的开发平台和目标平台上都可用,在Qt5所有版本上是源代码和二进制兼容的。具体的基本模块如下
    • Qt Core: 其他模块都用到的核心非图形类
    • Qt GUI: 设计GUI界面的基础类,包括OpenGL
    • Qt Multimedia: 音频,视频,摄像头和广播功能的类
    • Qt Multimedia Widgets: 实现多媒体功能的界面组件类
    • Qt Network: 使网路编程更简单和轻便的类
    • Qt QML: 用于QML和JavaScript语言的类
    • Qt Quick: 用于构建具有定制用户界面的动态应用程序的声明框架
    • Qt Quick Controls: 创建桌面样式用户界面,基于Qt Quick的用户界面控件
    • Qt Quick Dialogs: 用于Qt Quick的系统对话框类型
    • Qt Quick Layouts: 用于Qt Quick 2界面元素的布局项
    • Qt SQL: 使用SQL用于数据库操作的类
    • Qt Test: 用于应用程序和库进行单元测试的类
    • Qt Widgets: 用于构建GUI界面的C++图形组件类

3.5.2 Qt附加模块

3.5.3 增值模块

3.5.4 技术预览模块

4.5.5 Qt 工具

  • Qt工具在所有支持的平台上都可以使用,用于帮助应用程序的开发和设计
    • Qt Designer: 用于扩展Qt Designer的类
    • Qt Help: 在应用程序中集成在线文档的类,实现类似于Qt Assistant的功能
    • Qt UI Tools: 操作Qt Designer生成的窗体的类

第四章 常用界面设计组件

第五章 Model/View结构

  • Model/View(模型/视图)结构是Qt中用界面组件显示与编辑数据结构的一种结构,视图(View)是显示和编辑数据的界面组件,模型(Model)是视图与原始数据之间的接口。Model/View结构的典型应用是在是数据库应用程序中。
  • 主要的视图组件有 QListVew, QTreeView和QTableView

第六章 对话框与多窗体设计

  • 在一个完整的应用程序设计中,不可避免地会涉及多个窗体,对话框的设计和调用,如何设计和调用这些对话框和窗体是搞清楚一个庞大的应用程序设计的基础。本章将介绍对话框和多窗体设计,调用方式,数据传递等问题,主要包括以下几点
    • Qt 提供的标准对话框的使用,例如打开文件对话框,选择颜色对话框,字体对话框,消息提示和确认选择对话框等
    • 自定义对话框的设计和调用,如何获取返回值,在对话框中如何操作主窗体等
    • 在一个应用程序中如何设计多种窗体,基于QDialog,QWidget和QMainWindow创建的窗体的调用方式有哪些,它们之间有什么区别
    • 如何创建一个在多页组件中管理的多窗体应用,类似于现在流行的多页浏览器的界面效果,子窗体如何与主窗体实现交互
    • 如何创建MDI(Multi-document interface)应用程序
    • 如何创建一个带有启动界面(Splash)和登录界面的窗体,如何保存和读取应用程序设置的参数。

第七章 文件系统和文件读写

  • 文件的读写是很多程序具有的功能,甚至某些应用程序就是围绕着某一种格式文件的处理而开发的,所以文件读写是应用程序开发的一个基本功能。
  • 本章介绍Qt中如何实现文本文件,二进制文件的读写,以及文件和目录的管理功能。

第八章 绘图

  • GUI用户界面的优势是通过可视化的界面元素为用户提供遍历的操作,界面上的按钮,编辑框等各种界面组件其实都是通过绘图而得到的。Qt的二维绘图基本功能是使用QPainter在绘图设备上绘图,绘图设备包括QWidget,QPixmap等,通过绘制一些基本的点,线,圆等基本形状组成自己需要的图形,得到的图形是不可交互操作的图形。
  • Qt还提供了Graphics View架构,使用GraphicsView, QGraphicsScene和各种QGraphicsItem类绘图,在一个场景中可以绘制大量图件,且每个图件是可选择,可交互的,如同矢量图编辑软件那样可以操作每个图件。Graphics View架构为用户绘制复杂的组件化图形提供了便利。

第九章 Qt Charts

  • Qt Charts是Qt提供的图表模块,在Qt 5.7以前只有商业版才有Qt Charts,但是从Qt 5.7开始,社区版本也包含了Qt Charts。Qt Charts可以很方便的绘制常见的折线图,柱状图,饼图等图表。

第十章 Data Visualization

  • Data Visualization是Qt提供的用于数据三维显示的模块。在Qt 5.7以前只有商业版才有Qt Visualization,但是从Qt 5.7开始,社区版本也包含了Qt Visualization
  • Data Visualization用于数据的三维显示,包括三维柱状图,三维空间散点,三维曲面等。

第十一章 数据库

  • Qt SQL模块提供数据库编程的支持,Qt支持多种常见的数据库,例如 MySQL, Oracle, MS SQL Server, SQlite等。Qt SQL模块包括多个类,可以实现数据库连接,SQL语句执行,数据获取与界面显示等功能,数据与界面之间使用Model/View架构,从而可以方便的实现数据的界面显示和操作。

第十二章 自定义插件和库

  • 当UI设计器提供的界面组件不满足实际需求时,可以从QWidget继承自定义界面组件。有两种方法使用自定义界面组件,一种是提升发(promotion);另一种是为UI设计器设计自定义界面组件的Widget插件,直接安装到UI设计器的组件面板里。

第十三章 多线程

  • Qt为多线程提供了完整的支持。QThread是线程类,是实现多线程操作的核心类,一般从QThread继承定义自己的线程类。线程之间的同步是其交互的主要问题,Qt提供了QMutex,QMutexLocker,QReadWriteLock,QwaitCondition,QSemaphore等多种类用于实现线程之间的同步。Qt还有Qt Concurrent模块,提供一些高级的API实现多线程编程而无需使用QMutex, QwaitCondition和QSemaphore等基础操作。使用Qt Concurrent实现的多线程程序可以自动根据处理器内核个数调整线程个数。
  • 本章主要介绍用QThread实现多线程编程的方法,以及用QMutex,QWaitCondition,QSemaphore等实现线程同步的方法。

13.1 QThread创建多线程程序

13.1.1 QThread类功能简介

  • QThread类提供不依赖于平台的管理线程的方法。一个QThread类的对象管理一个线程,一般从QThread继承一个自定义类,并重定义虚函数run(),在run()函数里实现线程需要完成的任务。
  • 将应用程序的线程称为主线程,额外创建的线程称为工作线程。一般在主线程里创建工作线程,并调用start()开始执行工作线程的任务。start()会在内部调用run()函数,进入工作线程的事件循环,在run()函数里调用exit()或quit()可以结束线程的事件循环,或者在主线程里调用terminate()强制结束线程。

  • QThread类的主要接口函数,信号和槽函数如下
    • 公共函数
      • bool isFinished(): 线程是否结束
      • bool isRunning(): 线程是否正在运行
      • Priority priority(): 返回线程的优先级
      • void setPriority(Priority priority): 设置线程的优先级
      • void exit(int returnCode = 0): 退出线程的事件循环,退出码为 returnCode,0表示成功退出,否则表示有错误
      • bool wait(unsigned long time): 阻止线程执行,直到线程结束(从run()函数返回),或等待时间超过time(毫秒)
    • 公共槽函数
      • void quit(): 退出线程的事件循环,并返回代码0,等效于exit(0)
      • void start(Priority priority): 内部调用run()开始执行线程,操作系统根据priority参数进行调度
      • void terminate(): 终止线程的运行,但不是立即结束线程,而是等待操作系统结束线程。使用terminate()之后应使用wait()
    • 信号
      • void finished(): 在线程就要结束时发射此信号
      • void started(): 在线程开始执行,run()函数被调用之前发射此信号
    • 静态公共成员
      • int idelThreadCount(): 返回系统上能够运行的线程的理想个数
      • void msleep(unsigned long msecs): 强制当前线程休眠msecs毫秒
      • void sleep(unsigned long secs): 强制当前线程休眠secs秒
      • void usleep(unsigned long usecs): 强制当前线程休眠usecs微妙
    • 保护函数
      • virtual void run(): start()调用run()函数开始线程任务的执行,所以在run()函数里实现线程的任务功能
      • int exec(): 由run()函数调用,进入此线程的事件循环,等待exit()退出。
  • QThread是QObject的子类,所以可以使用信号和槽机制。QThread自身定义了started()和finished()两个信号,started()信号在线程开始执行之前发射,也就是在run()函数被调用之前,finished()信号在线程就要结束时发射。

13.2 线程同步

13.2.1 线程同步的概念

  • 在多线程应用程序中,由于多个线程的存在,线程之间可能需要访问同一个变量,或一个线程需要等待另外一个线程完成某个操作后才产生相应的动作。

13.2.2 基于互斥量的线程同步

  • QMutex和QMutexLocker是基于互斥量的线程同步类,QMutex定义的实例是一个互斥量,QMutex主要提供3个函数
    • lock(): 锁定互斥量,如果另外一个线程锁定了这个互斥量,它将阻塞执行直到其他线程解锁这个互斥量。
    • unlock(): 解锁一个互斥量,需要与lock()配对使用
    • tryLock(): 试图锁定一个互斥量,如果成功锁定就返回true;如果其他线程已经锁定了这个互斥量,就返回false,但不阻塞程序执行。
  • 定义的互斥量mutex相当于一个标牌,可以这样来理解互斥量: 列车上的卫生间一次智能进一个人,当一个人尝试进入卫生间就是lock(),如果有人占用,它就只能等待;等里面的人出来,腾出了卫生间是unlock(),这个等待的人才可以进入并且锁住卫生间的门,就是lock(),使用完卫生间之后他再出来时就是unlock()

  • QMutex需要配对使用lock()和unlock()来实现代码段的保护,在一些逻辑复杂的代码段或可能发生一场的代码中,配对就可能出错。
  • QMutexLocker是另外一个简化了互斥量处理的类。QMutexLocker的构造函数接受一个互斥量作为参数并将其锁定,QMutexLocker的析构函数则将此互斥量解锁,所以在QMutexLocker实例变量的生存期内的代码段得到保护,自动进行互斥量的锁定和解锁。(与C++的自动锁原理一样,构造函数锁住互斥量,析构函数释放互斥量)

13.2.3 基于QReadWriteLock的线程同步

  • 使用互斥量时存在一个问题: 每次只能有一个线程获得互斥量的权限。如果在一个程序中有多个线程读取某个变量,使用互斥量时也需要排队。而实际上若只是读取一个变量,是可以让多个线程同时访问的,这样互斥量就会降低程序的性能。
  • Qt提供了QReadWriteLock类,它是基于读或写的模式进行代码段锁定的,在多个线程读写一个共享数据时,可以解决上面所说的互斥量存在的问题。

  • QReadWriteLock以读或写锁定的同步方法允许以读或写的方式保护一段代码,它可以允许多个线程以只读方式同步访问资源,但是只要有一个线程以写方式访问资源,其他线程必须等待直到写操作结束。
  • QReadWriteLock提供一下几个主要的函数
    • lockForRead(): 以只读方式锁定资源,如果有其他线程以写入方式锁定,这个函数会阻塞
    • lockForWrite(): 以写入方式锁定资源,如果本线程或其他线程以读或写模式锁定资源,这个函数就会阻塞。
    • unlock(): 解锁
    • tryLockForRead(): 是lockForRead()的非阻塞版本
    • tryLockForWrite(): 是lockForRead()的非阻塞版本
  • QReadLocker和QWriteLocker是QReadWriteLock的简便形式,如同QMutexLocker是QMutex的简便版本一样,无需与unlock()配对使用。使用QReadLocker和QWriterLocker。

13.2.4 基于QWaitCondition的线程同步

  • 在多线程的程序中,多个线程之间的同步实际上就是他们之间的协调问题。前面采用的互斥量和基于QReadWriteLock的方法都是对资源的锁定和解锁,避免同时访问资源时发生冲突。在一个线程解锁资源后,不能及时通知其他线程。
  • QWaitCondition提供了另外一种改进的线程同步方法,QWaitCondition与QMutex结合,可以使一个线程在满足一定条件时通知其他多个线程,使他们及时做出响应,这样比只使用互斥量效率更高一些。

  • QWaitCondition提供如下一些函数
    • wait(Qmutex *lockedMutex): 解锁互斥量lockedMutex,并阻塞等待唤醒条件,被唤醒后锁定lockedMutex并退出函数
    • wakeAll(): 唤醒所有处于等待状态的线程,线程唤醒的顺序不确定,由操作系统的调度策略决定
    • wakeOne(): 唤醒一个处于等待状态的线程,唤醒哪个线程不确定,由操作系统的调度策略决定
  • QWaitCondition一般用于 “生产者/消费者(producer/consumer)”模型中。

13.2.5 基于信号量的线程同步

  • 信号量(Semaphore)是另一种限制对共享资源进行访问的线程同步机制,它与互斥量(Mutex)相似,但是有区别。一个互斥量只能被锁定依次,而信号量可以多次使用。信号量通常用来保护一定数量的相同的资源,例如数据采集时的双缓冲区。

  • QSemaphore是实现信号量功能的类,它提供以下几个基本的函数
    • acquire(int n): 尝试获得n个资源。如果没有这么多资源,线程将阻塞直到有n个资源可用
    • release(int n): 释放n个资源,如果信号量的资源已全部可用之后再release(),就可以创建更多的资源,增加可用资源的个数
    • int available(): 返回当前信号量可用的资源个数,这个个数永远不可能为负数,如果为0,就说明当前没有资源可用
    • bool tryAcquire(int n = 1): 尝试获取n个资源,不成功时不阻塞线程。
  • 在定义QSemaphore的实例时,可以传递一个数值作为初始可用的资源个数。

第十四章 网络编程

  • Qt网络模块提供了用于编写TCP/IP客户端和服务端程序的各种类,例如用于TCP通信的QTcpSocket和QTcpServer,用于UDP通信的QudpSocket,还有用于实现HTTp,FTP等普通网络协议的高级类,例如QNetworkRequest,QNetworkReply和QNetworkAccessManager。

14.1 主机信息查询

14.1.1 QHostInfo 和 QNetworkInterface 类

  • 查询一个主机的MAC地址或IP地址是网络应用程序中经常用到的功能。

  • QHostInfo的静态函数 localHostName() 可获取本机的主机名,静态函数 fromName() 可以通过主机名获取IP地址,静态函数 lookupHost() 可以通过一个主机名,以异步方式查找这个主机的IP地址。
  • QNetworkInterface 可以获得运行应用程序的主机的所有IP地址和网络接口列表。静态函数 allInterfaces() 返回主机上所有的网络接口的列表,一个网络接口可能包括多个的IP地址,每个IP地址与掩码或广播地址关联。如果无需直到子网掩码和广播的IP地址,使用静态函数 allAddresses() 可以获得主机上的所有IP地址列表。

14.2 TCP通信

14.2.1 TCP通信概述

  • TCP(Transmission Control Protocol)是一种被大多数Internet网络协议(例如HTTP和FTP)用于数据传输的低级网络协议,它是可靠的,面向流,面向连接的传输协议,特别适合用于连续数据传输。

14.3 QUdpSocket 实现 UDP通信

14.3.1 UDP通信概述

  • UDP(User Datagram Protocol,用户数据包协议)是轻量的,不可靠的,面向数据报(datagram),无连接的协议,它可以用于对可靠性要求不高的场合。与TCP通信不同,两个程序之间进行UDP通信无需预先建立持久的socket连接,UDP每次发送数据报都需要指定目标地址和端口。
  • UDP消息传送有单播,广播,组播三种模式
    • 单播(unicast)模式: 一个UDP客户端发出的数据包只发送到另一个指定地址和端口的UDP客户端,是一对一的数据传输
    • 广播(broadcast)模式:一个UDP客户端发出的数据包,在同一个网络范围内其他所有的UDP客户端都可以收到。
    • 组播(multicast)模式: 也称多播。UDP客户端加入到另一个组播IP地址指定的多播组,成员向组播地址发送的数据包组内成员都可以接收到。
  • 使用广播和多播模式,UDP可以实现一些比较灵活的通信功能,而TCP通信只有单播模式,没有广播和多播模式。所以UDP通信虽然不能保证数据传输的准确性,但是具有灵活性,一般的即时通信软件都是基于UDP通信的。

14.4 基于HTTP协议的网络应用程序

14.4.1 实现高层网络操作的类

  • Qt网络模块提供一些类实现OSI七层网络模型中高层的网络协议,例如HTTP,FTP,SNMP等,这些类主要是QNetworkRequest, QNetworkReply和QNetowrkAccessManager

第十五章 多媒体

  • 多媒体功能指的主要是计算机的音频和视频的输入,输出,显示和播放等功能,Qt的多媒体模块为音频和视频播放,录音,摄像头拍照和录像等提供支持,甚至还提供数字收音机的支持。

15.1 Qt多媒体模块功能概述

  • 利用Qt多媒体模块提供的各种类,可以实现一般的音频,视频的输入和输出。

第十六章 应用程序设计辅助功能

  • 本章介绍Qt应用程序设计的一些辅助功能,包括设计多语言界面,使用样式表定制界面和组件的外观,使用QStyle设置界面外观,以及应用程序发布等。