编程成长之路

我们都是站在父母的肩上去看他们不曾看到的风景!加油!
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

Qt开发笔记-----基础篇

Posted on 2023-05-21 16:15  来颗维C  阅读(1027)  评论(0编辑  收藏  举报

1.1 为什么要学 Qt

Qt是一个跨平台的 C++ 图形用户界面应用程序框架

Qt 为应用程序开发者提供建立艺术级图形界面所需的所有功能

Qt 是完全面向对象的,很容易扩展,并且允许真正的组件编程

(1)Qt 发展史
在讲解学习 Qt 的必要性之前, 先来了解下 Qt 的发展历史:

1991年,Qt 最早由 奇趣科技(TrollTech)开发

1996年,进入商业领域,它也是目前流行的 Linux 桌面环境 KDE 的基础

2008年,奇趣科技被诺基亚公司收购,Qt 成为诺基亚旗下的编程语言

2012年,Qt 又被 Digia 公司收购

2014年4月,跨平台的集成开发环境 Qt Creator 3.1.0 发布,同年5月20日配发了 Qt5.3 正式版,至此 Qt 实现了对 iOS、Android、WP 等各平台的全面支持

2020年,Qt 团队宣布 6.0 版本发布。

当前 Qt 最新版本为 Qt 6.3

(2)学习 Qt 的必要性
跨平台

用 Qt 编写的程序,可以直接编译运行到不同的平台:windows、linux、mac

主流的应用级软件肯定是要跑在这三个平台的,用 mfc、c# 做的话, 就只能跑在 windows 上,无法轻易移植到 linux , mac 上去

知名软件都用 Qt

很多知名软件都是 Qt 开发的

谷歌地图

Google Earth,是一款谷歌公司开发的虚拟地球软件,通过它可以浏览全球各地的高清晰度卫星图片

Wireshark

一款免费的网络抓包分析软件,它的功能非常强大对于开发者而言,可以用它通过抓包分析,来解决开发过程中的很多问题。

VirtualBox

开源的虚拟机软件,可以在一台电脑上同时运行两个操作系统

典型的用法就是:宿主机安装 windows 系统,然后通过 virtualbox,再安装一个 linux 系统,这样就可以同时运行 windows 和 linux 两个系统,并且在两个系统之间可以很方便地共享文件

virtualbox 对标的是 Vmware,VMware 也是一个虚拟机软件,不过它是收费的

VLC

这是一个开源的多媒体播放器

它体积小巧、功能强大,做音视频开发的小伙伴应该是很熟悉它的

WPS Office

金山公司(Kingsoft)出品的国产办公软件套装,完全兼容微软的 Office 系列

YY 语音

国内第一大游戏语音通讯平台,可以进行在线多人语音聊天和语音会议

百度查一下还有很多,可以不学 mfc ,但是 Qt 一定要学

代码层次更加清晰

用 mfc 的话, 系统自动生成的代码和自己写的代码,是杂糅在一起的,比如,当界面的控件删除之后,代码就报错,需要自己手动去修改源码

而在 Qt 中,系统自动生成的代码和自己写的代码是完全隔离开的

界面美观

Qt 支持 QSS, 类似于 web 端的 css, 修改控件的样式, 很简单。

因此,用 Qt 做出来的界面是非常美观的。

2D/3D开发

支持2D/3D图形渲染,支持 OpenGL,如果做三维应用,首选推荐 Qt

丰富的API、完善的开发文档

Qt 包括多达 250 个以上的 C++ 类,并且提供了非常完善的文档,并且附带有很多例子,对开发者非常友好

嵌入式开发

嵌入式开发,就是指的嵌入式 linux 开发, Qt 是做嵌入式 linux 界面开发最好的,没有之一

在后续的课程中,我也会推出 《嵌入式 QT 开发》的教程,会支持正点原子、野火、韦东山的系列的开发板

最好会写界面

如果学过了 C++,但是连一个简单的界面做不出来,有点说不过去

当然你可以最强的是用 C++ 做后台,但是界面还是要掌握的,因为通常工作中会写一个带有界面的简单的测试工具,此时 Qt 就派上用场了

最重要一点 - 工资待遇好

1.2 安装 Qt Creator

image-20230521143813703

Qt 开发重要有两种开发环境

Qt Creator

它是 Qt 官方提供的开发环境,并且 creator 本身是跨平台的,它可以被安装在 windows、linux、mac 上。

Visual Studio

Visual Studio 是微软的一个集成开发环境,它号称宇宙第一IDE

只要在 Visual Studio 中安装一个 Qt 官方提供的插件,就能进行 Qt 的开发

后面的课程,我们会使用 Qt Creator 为例进行 Qt 课程的讲解 ,毕竟是官方推出的开发环境!

(1)QT 重要版本
QT4

嵌入式设备上的图形界面,很多用的仍是 Qt 4 的版本,Qt 4.8.7 是 Qt4 系列的终结版本,解决了以往的全部 BUG

如果是不得不使用 Qt 4 版本,建议使用 Qt 4.8.7

QT5

5.15 LTS 作为 Qt 5 系列的最后版本,在修复 bug 方面也做了大量工作,是 Qt 5 系列中最好、最稳定的版本。

Qt 6

目前 最新版本为 QT 6.3

(2)Qt Creator 安装方式:离线/在线
Qt 开发的官方开发环境是 Qt Creator,官方下载地址:https://download.qt.io/

在 5.14 版本之前,官方提供离线的安装包,但是从 5.15 版本之后,需要在线安装(这类似于 visual studio 的安装,也是需要下载一个安装器先),如下:

5.14 版本:https://download.qt.io/archive/qt/5.14/5.14.2/

img

5.14 版本之前,提供离线安装包

5.15 版本:https://download.qt.io/archive/qt/5.15/5.15.2/

img

可见,在 5.15 目录下,移除了离线安装包,查看其中的 OFFLINE_README.txt,内容如下:

Due to The Qt Company offering changes, open source offline installers are not available any more since Qt 5.15. Read more about offering changes in the https://www.qt.io/blog/qt-offering-changes-2020 blog.

If you need offline installers, please consider our new Qt for Small Business offering: https://www.qt.io/blog/available-now-qt-for-small-businesses

通过查看链接:www.qt.io/blog/qt-offering-changes-2020,其中得知,离线安装包只对商业付费用户提供,如下:

Starting with Qt 5.15, long term support (LTS) will only be available to commercial customers.
虽然没有提供离线安装包,但是可以通过在线的方式安装

打开如下链接,下载在线安装器:https://download.qt.io/archive/online_installers/4.3/

img

QT官方提供的 - 在线安装器
之后,就可以双击 qt-unified-windows-x86-4.3.0-1-online.exe 开始在线安装了

(3)在线安装
下面列出在线安装的详细步骤

登录 Qt 账户

如果没有 Qt 账户,点击 【注册】按钮,根据提示,注册一个即可

img

​ 登录 Qt 账户
开源义务

同意协议,并勾选个人用户,否则需要输入公司/企业名称

img

禁用信息收集

通常直接选择第二项,禁止向 Qt 官方发送统计信息

img

禁用信息收集
安装文件夹

选择安装文件夹,并选择【Custom installation】自定义安装

img

选择组件

img

这里有必要进行下详细的说明:

右侧的类别中:

Archive

All Supported Release

所有的 Qt 发布版本

LTS

Latest Long-Term Support Releases

长期支持版本

Latest releases

Latest Supported release

最新的正式发布版

Preview

Latest unofficial release previews, including snapshots, alpha, beta, and RC release

预览版,包括alpha预览版,beta测试版,RC(Release Candidate)发行候选版

通常选择 LTS,就像安装 linux 时一样

左侧选择编译器:

Windows 系统下,Qt 主要有两种编译器:

MSVC

MSVC 编译器(Microsoft Visual C++),是微软提供的 VC 编译器,需要 visual studio 环境的支持,由于我安装了 Visual Studio 2019,因此在安装时会列出

MinGW

MinGW 编译器(Minimalist GNU for Windows),也就是 Gcc 编译器,只不过在 Windows 下作了封装而已,这个版本不需要 VS 环境的支持,可以独立生成Windows 平台的应用程序。

左侧其他:

Android

可用于 android 的开发。

不过 android 的开发,通常直接使用谷歌的集成开发环境 Android Studio,使用 kotlin 语言进行开发(以前使用 Java 语言)

Source

Qt 的源码,如果有需要查看源码的需求,可以将这个勾选,不过会多占用至少 3GB 的磁盘控件

Qt charts等

这些时 Qt 一些高级的开发组件,比如 charts 进行图表的开发,WebEngine 进行 Web 相关的开发

说明:

目前来说,直接勾选 MinGW 的两个编译器即可

没有勾选的组件,即使后面用到,可以再次打开这个安装器继续安装!

许可协议

img准备安装

准备安装

img

开始安装

img

安装成功

在所有的 7z 压缩文件提取完毕,并安装之后,就完成最终的安装

img

(4)追加其他组件
如果在安装时,漏掉了某个组件,怎么办?难道要卸载重装?

答案:完全不用,可以追加安装需要的组件

方法如下:

点击系统左下角的【开始】菜单,找到【Qt】,然后选择【Qt Maintenance Tool】或者【Uninstall Qt】

img

打开
登录 Qt 账户:

如果要卸载 Qt,选中【仅卸载】复选框

img

选择组件

img在此,可以追加安装 Qt 源码,如下:

追加组件

img

1.3 新建 Qt 工程

学习任何的编程语言,创建的第一工程都是打印 hello world,学习 Qt 也不例外

只是 Qt 不是一门语言,而是一个 基于 C++ 的 GUI 开发框架,因此这里我们创建第一个界面程序

在创建第一个界面程序之前,首先来配置一下 Qt Creator,比如修改主题样式,深色/浅色主题,代码区字体的大小等

1、配置 Qt Creator
集成开发环境默认的设置,能够满足绝大部分人的需求,但是可能还是有个别选项,不符合我们的需求

因此,第一步都是根据自己的需要,进行简单的设置

常用的集成开发环境:

# 微软出品,宇宙第一 IDE,做 c# 开发就会用到
Visual Studio

# 微软出品并开源的,源代码编辑器,插件丰富强大,可以支持几乎所有开发语言
VS Code

# 谷歌官方,用开发 Android 程序,目前 Android 开发的语言,已经由 Java 变为了 Kotlin
Android Studio

# 用于浏览C/C++代码,韦东山老师习惯使用的工具
Source Insight 

(1)设置主题

可以将主题设置为浅色或者深色,比如在晚上的时候,可以将主题调成深色主题,就像手机的白天模式和夜间模式一样

设置方法:【工具】->【选项】->【环境】

img

(2)设置中英文

【工具】->【选项】->【环境】

img

(3)设置代码区字体大小

【工具】->【选项】->【文本编辑器】->【Fonts & Colors】

img

还可以设置鼠标滚轮,来放大和缩小字体

方法:【工具】->【选项】->【文本编辑器】->【Behavior】

很实用和方便的功能,尤其是你在给同事讲解代码时,可以方便地放大代码区域

img

(4)显示行号、高亮显示当前行

方法:【工具】->【选项】->【文本编辑器】->【Display】

显示行号:方便代码行数的定位

高亮显示当前行:方便快速定位当前光标所在的行。这个默认时未勾选的,建议勾选

显示文件编码:界面上直观地显示文件编码。这个默认时未勾选的,建议勾选,这样文件编码显示在打开文件的右上角。

img

(5)设置文件默认编码

文件的默认编码为 utf-8,建议保持默认,这样中文不会乱码

如果是GB2312或者GBK编码的文件, 使用utf-8编码来打开就会乱码

方法:【工具】->【选项】->【文本编辑器】->【Behavior】

img

6)保存时清理

就是在保存文件时,清除多余的空白字符,使代码简洁,并且占用的文件大小也会缩小

在 VS Code 以及 Source Insight 中都有这种设置

方法:【工具】->【选项】->【文本编辑器】->【Behavior】

img

2、新建第一个Qt工程
环境配置好了,就可以开始新建第一个Qt工程了

(1)打开新建工程窗口

在【欢迎】模式下,点击【Create Project...】或者【文件】菜单,点击【New Project...】,都可以打开如下新建项目的对话框:

在 Qt 中,Widget 类是所有窗口类的基类,因此要创建基于窗口的应用,就要选择【Qt Widgets Application】

img

(2)指定项目名称和保存路径

img

注意:

作为一名专业的程序员,项目名称,项目路径,包括文件明,都不要使用中文,否则可能会报错,或者出现莫名其妙的问题!

(3)选择构建系统

img

(4)选择窗口类

img

(5)国际化

img

6)选择编译套件

img

(7)版本控制 常用的版本控制工具有 svn 和 git,通常我们自己使用 svn 或 git 来进行版本管理即可,这里选择 none

img

3、项目文件说明
上一步,直接点击完成,就可以打开如下:

img

下面对 HelloQt.pro 文件和 main.cpp 文件,进行介绍

img

QT 选项用于指定项目中用到的 Qt 模块

通常一个窗口程序,肯定要用到的三个模块就是:core、gui、widgets

那么,Qt 中还有哪些常用的模块呢?

# QtCore 模块是Qt应用程序的基础,是核心的非图形类。
# 提供了信号与槽的通信机制,并发和多线程,容器,事件系统
Qt Core

# 最重要的GUI模块。图形用户界面 (GUI) 组件的基类。
Qt GUI

# 包含基于GUI应用程序的典型小部件类,比如按钮、文本框、标签等
# 在 Qt5 中, 从 Gui 模块中分离出来。
Qt widgets

# 网络模块。用于支持 TCP, UDP, HTTP 通信
Qt Network

# 多媒体模块。音频、视频、广播和相机功能类。
Qt Multimedia

# Qt Multimedia 的小部件。
Qt Multimedia Widgets

# 数据库模块。用于操作数据库,比如后面会将到的 sqlite、MySQL
Qt SQL

# web引擎模块。用于 web 相关的开发
Qt WebEngine

############################################################
# 以下是 QML 相关的模块
# QML 是什么呢?
# 简单来说,就是使用类似 js 的语法来构建界面,而不是 widget 的方式

# Qt QML模块。用于 QML 和 JavaScript 语言。
Qt QML

# 该模块用于使用 QML2 编写的 GUI 应用程序。用于构建具有自定义用户界面的高度动态应用程序的声明性框架。
Qt Quick

# 提供轻量级 QML 类型,用于为桌面、嵌入式和移动设备创建高性能用户界面。这些类型采用简单的样式架构并且非常高效。
Qt Quick Controls

# 用于从 Qt Quick 应用程序创建系统对话框并与之交互的类型。
Qt Quick Dialogs

# 用于在 Qt Quick 中安排项目的布局。
Qt Quick Layouts 

(2)main.cpp

main.cpp文件是 Qt 程序的入口文件,其中的 main 函数是入口函数

#include "mywindow.h"

#include <QApplication>

int main(int argc, char *argv[])
{
    // 1. QApplication 是 Qt 框架提供的应用程序类
    // 作用:负责 Qt 中事件的处理,比如鼠标的单击事件,键盘的输入事件等
    QApplication a(argc, argv);

    // 2. 创建自己的窗口对象,并调用其 show 方法,将窗口显示出来
    MyWindow w;
    w.show();

    // 3. 调用 QApplication 类的 exec 方法,应用程序就阻塞在这里,并不会退出,而是进入到事件循环的处理, 直到退出程序(比如点击了窗体右上角的关闭按钮)
    return a.exec();
} 

1.4 项目构建流程

第一个 Qt 工程,其中包括 5 个文件:

HelloQt.pro

main.cpp

mywindow.h

mywindow.cpp

mywindow.ui

1、mywindow.h/.cpp/.ui 文件说明
在 main.cpp 中,除了 QApplication 进入事件的循环处理以外,还会创建一个 MyWindow 的对象,并显示出来

int main(int argc, char *argv[])
{
    ...

    // 创建 MyWindow 窗口对象, 并调用其 show 方法,将窗口显示出来
    MyWindow w;
    w.show();

    ...
} 

接下来,我们按住 Ctrl 键的同时,点击 MyWindow 类,跳转到该类的定义处(mywindow.h),如下:

#include <QMainWindow>

 QT_BEGIN_NAMESPACE
 // 在此声明一个MyWindow类,这个类定义在 Ui 命名空间中
 // 因为下面会定义一个 Ui::MyWindow 类型的指针 *ui
 namespace Ui { class MyWindow; }
 QT_END_NAMESPACE

 // 我们自定义的 MyWindow 类,要继承自Qt框架提供的QMainWindow/QDialog/QWidget这三个类其中之一,才可以正常显示出来
 class MyWindow : public QMainWindow
 {
     Q_OBJECT

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

 private:
     // 定义一个 Ui::MyWindow 类型的指针 *ui
     // Ui::MyWindow 这个类定义在 ui_mywindow.h 中(可以 Ctrl+单击 跳转过去)
     // 这个 Ui::MyWindow 类,本身是空实现,但是它继承自 Ui_MyWindow 类

     // Ui_MyWindow 类,是和UI设计界面一一对应的,也就和mywindow.ui这个xml文件一一对应的
     // 至于为什么一个C++代码中类的对象,会和mywindow.ui这个xml文件一一对应,本节课下面就会讲到,暂且知道即可
     Ui::MyWindow *ui;
 }; 

接下来,来到 mywindow.cpp 文件:

MyWindow::MyWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MyWindow) // 在初始化列表中,对ui指针进行初始化赋值
{
    // 并将当前窗口,也就是this指针,设置到ui对象中
    // 只有这样,我们在ui界面中拖拽的控件才会在随MyWindow显示出来。
    ui->setupUi(this);

    // 之后就可以对拖拽的那些控件进行各种操作了,比如下面设置按钮显示的文字。
    ui->btn_start->setText("启动");
    ui->btn_stop->setText("停止");
}

MyWindow::~MyWindow()
{
    // 既然在构造函数中 new 了ui对象,在析构中要delete销毁
    delete ui;
} 

ui_mywindow.h 这个文件是怎么来的?

为什么说 Ui::MyWindow 这个c++ 中的类,是和 xml 格式的 mywindow.ui 文件一一对应的?

为什么上面需要调用 ui->setupUi(this); 才可以将界面上拖拽的按钮显示出来

别着急,下面马上就为你揭秘

在揭秘之前,有必要了解下 Qt 项目的构建流程

上一节中,我们使用向导创建了第一个 Qt 工程,在 Qt Creator 集成开发环境中,一个按钮或者一个快捷键,就可以完成项目的构建和运行。

但是,这些看起来简单的过程,背后到底发生了什么呢?

作为一名专业的程序员,我们有必要探讨一番,这样即使以后出现了问题,编译报错,我们也能很快地定位到问题所在。

2、需要知道的几个概念
在了解项目构建流程之前,有必要先看几个概念:① makefile ② make ③ qmake

作为一名 Qt 开发者,需要有 C/C++ 基础,想必对这几个概念并不陌生,这里做一个总结

(1)Makefile
对于一个源文件 main.cpp,如何编译成可执行文件呢?

g++ -o main main.cpp 

每当 main.cpp 修改之后,就需要重新执行以上编译命令

更简单地,可以新建一个 Makefile 文件,其中内容如下:

main:main.cpp
 g++ -o main main.c 

此时,在命令行直接执行 make 就可以直接执行 Makefile,进而执行 g++ -o main main.c 来编译出可执行文件 main

可见,引入了 Makefile 文件之后,只需执行 make 命令即可

以上只有一个源文件 main.cpp,然而一个实际的工程中,其源文件一般有有很多,并且通常按照功能模块,放在若干个目录中

此时如果每次都手动一条条执行编译命令,很显然是不现实的,因此就有了 Makefile

Makefile 文件用于描述整个工程的编译、链接的规则。比如,工程中的哪些源文件需要编译(根据时间戳,只编译修改过的文件)以及如何编译、编译顺序,最终链接成目标的可执行文件或目标的库文件

Makefile 有自己的书写格式、关键字、函数,像 C 语言有自己的格式、关键字和函数一样。

Makefile 文件编写完毕之后,编译整个工程你所要做的唯一的一件事就是在 shell 窗口下输入 make 命令,就可以将整个工程进行 “自动化编译”,极大提高了效率。

(2)make
make 是一个命令工具,在 shell 窗口下执行 make 命令,它会自动读取并解释 makefile 中指令,来完成整个工程的自动编译,极大的提高了软件开发的效率。

(3)qmake
手写 Makefile 是比较困难而且容易出错,尤其在进行跨平台开发时,必须针对不同平台分别编写 Makefile,会增加跨平台开发复杂性与困难度。

qmake 会根据项目文件(.pro)里面的信息,自动生成 Makefile。

因此在 Qt 开发时,我们只需编写 .pro 文件即可,因为 qmake 会自动根据 .pro 文件生成 Makefile

因此,Qt 项目构建的基本流程是:

qmake

执行 qmake 生成 Makefile

make

执行 make 命令,编译出可执行程序

运行

运行可执行程序

3、Qt 项目构建流程
了解了 Makefile、make、qmake 的基本概念之后,我们知道 Qt 项目的构建流程:

qmake -> make -> run

接下来看下,上一节的 HelloQt 项目是如何编译出可执行文件,并运行呈现出窗口界面的

(1)构建设置
点击左侧的项目模式,可以打开当前项目的构建设置界面,如下:

img

如果想为生成的目标程序,添加命令行参数,那么需要用到项目模式的运行设置

点击左侧的项目模式,可以打开当前项目的运行设置界面,如下:

img

此时可以在 main.cpp 中,添加如下代码来输出命令行参数,如下:

qDebug() << "参数个数:" << argc;
qDebug() << "参1:" << argv[0];
qDebug() << "参2:" << argv[1];
qDebug() << "参3:" << argv[2]; 

执行结果:

参数个数: 3
参1: E:\qt_project\HelloQt\debug\HelloQt.exe
参2: 123
参3: 456 

构建设置和运行设置之后,来看项目构建时,【编译输出】窗口的输出,如下:

# 1、qmake 生成 Makefile
 23:22:27: 正在启动 "C:\Qt\5.15.2\mingw81_32\bin\qmake.exe" E:\qt_project\HelloQt\HelloQt.pro -spec win32-g++ "CONFIG+=debug" "CONFIG+=qml_debug"

 23:22:28: 进程"C:\Qt\5.15.2\mingw81_32\bin\qmake.exe"正常退出。
 23:22:28: 正在启动 "C:\Qt\Tools\mingw810_32\bin\mingw32-make.exe" -f E:/qt_project/HelloQt/Makefile qmake_all

 mingw32-make: Nothing to be done for 'qmake_all'.
 23:22:28: 进程"C:\Qt\Tools\mingw810_32\bin\mingw32-make.exe"正常退出。
 23:22:28: 正在启动 "C:\Qt\Tools\mingw810_32\bin\mingw32-make.exe" -j4

 C:/Qt/Tools/mingw810_32/bin/mingw32-make -f Makefile.Debug
 mingw32-make[1]: Entering directory 'E:/qt_project/HelloQt'

 # 2、执行 uic.exe,将ui文件(xml格式),转换为代码文件(.h)
 C:\Qt\5.15.2\mingw81_32\bin\uic.exe mywindow.ui -o ui_mywindow.h

 # 3、g++ 开始编译,以生成目标文件
 g++ -c -fno-keep-inline-dllexport -g -std=gnu++1z -Wall -Wextra -Wextra -fexceptions -mthreads -DUNICODE -D_UNICODE -DWIN32 -DMINGW_HAS_SECURE_API=1 -DQT_QML_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DQT_NEEDS_QMAIN -I. -IC:\Qt\5.15.2\mingw81_32\include -IC:\Qt\5.15.2\mingw81_32\include\QtWidgets -IC:\Qt\5.15.2\mingw81_32\include\QtGui -IC:\Qt\5.15.2\mingw81_32\include\QtANGLE -IC:\Qt\5.15.2\mingw81_32\include\QtCore -Idebug -I. -IC:\Qt\5.15.2\mingw81_32\mkspecs\win32-g++  -o debug\main.o main.cpp
 g++ -c -fno-keep-inline-dllexport -g -std=gnu++1z -Wall -Wextra -Wextra -fexceptions -mthreads -DUNICODE -D_UNICODE -DWIN32 -DMINGW_HAS_SECURE_API=1 -DQT_QML_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DQT_NEEDS_QMAIN -I. -IC:\Qt\5.15.2\mingw81_32\include -IC:\Qt\5.15.2\mingw81_32\include\QtWidgets -IC:\Qt\5.15.2\mingw81_32\include\QtGui -IC:\Qt\5.15.2\mingw81_32\include\QtANGLE -IC:\Qt\5.15.2\mingw81_32\include\QtCore -Idebug -I. -IC:\Qt\5.15.2\mingw81_32\mkspecs\win32-g++  -o debug\mywindow.o mywindow.cpp
 g++ -fno-keep-inline-dllexport -g -std=gnu++1z -Wall -Wextra -Wextra -dM -E -o debug\moc_predefs.h C:\Qt\5.15.2\mingw81_32\mkspecs\features\data\dummy.cpp
 C:\Qt\5.15.2\mingw81_32\bin\moc.exe -DUNICODE -D_UNICODE -DWIN32 -DMINGW_HAS_SECURE_API=1 -DQT_QML_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DQT_NEEDS_QMAIN --include E:/qt_project/HelloQt/debug/moc_predefs.h -IC:/Qt/5.15.2/mingw81_32/mkspecs/win32-g++ -IE:/qt_project/HelloQt -IC:/Qt/5.15.2/mingw81_32/include -IC:/Qt/5.15.2/mingw81_32/include/QtWidgets -IC:/Qt/5.15.2/mingw81_32/include/QtGui -IC:/Qt/5.15.2/mingw81_32/include/QtANGLE -IC:/Qt/5.15.2/mingw81_32/include/QtCore -IC:/Qt/Tools/mingw810_32/lib/gcc/i686-w64-mingw32/8.1.0/include/c++ -IC:/Qt/Tools/mingw810_32/lib/gcc/i686-w64-mingw32/8.1.0/include/c++/i686-w64-mingw32 -IC:/Qt/Tools/mingw810_32/lib/gcc/i686-w64-mingw32/8.1.0/include/c++/backward -IC:/Qt/Tools/mingw810_32/lib/gcc/i686-w64-mingw32/8.1.0/include -IC:/Qt/Tools/mingw810_32/lib/gcc/i686-w64-mingw32/8.1.0/include-fixed -IC:/Qt/Tools/mingw810_32/i686-w64-mingw32/include mywindow.h -o debug\moc_mywindow.cpp
 g++ -c -fno-keep-inline-dllexport -g -std=gnu++1z -Wall -Wextra -Wextra -fexceptions -mthreads -DUNICODE -D_UNICODE -DWIN32 -DMINGW_HAS_SECURE_API=1 -DQT_QML_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DQT_NEEDS_QMAIN -I. -IC:\Qt\5.15.2\mingw81_32\include -IC:\Qt\5.15.2\mingw81_32\include\QtWidgets -IC:\Qt\5.15.2\mingw81_32\include\QtGui -IC:\Qt\5.15.2\mingw81_32\include\QtANGLE -IC:\Qt\5.15.2\mingw81_32\include\QtCore -Idebug -I. -IC:\Qt\5.15.2\mingw81_32\mkspecs\win32-g++  -o debug\moc_mywindow.o debug\moc_mywindow.cpp
 g++ -Wl,-subsystem,windows -mthreads -o debug\HelloQt.exe debug/main.o debug/mywindow.o debug/moc_mywindow.o  C:\Qt\5.15.2\mingw81_32\lib\libQt5Widgets.a C:\Qt\5.15.2\mingw81_32\lib\libQt5Gui.a C:\Qt\5.15.2\mingw81_32\lib\libQt5Core.a  -lmingw32 C:\Qt\5.15.2\mingw81_32\lib\libqtmain.a -LC:\openssl\lib -LC:\Utils\my_sql\mysql-5.7.25-win32\lib -LC:\Utils\postgresql\pgsql\lib -lshell32

 mingw32-make[1]: Leaving directory 'E:/qt_project/HelloQt'
 23:22:35: 进程"C:\Qt\Tools\mingw810_32\bin\mingw32-make.exe"正常退出。
 23:22:35: Elapsed time: 00:11. 

4、uic 工具

uic 用于将 ui 文件转换为 .h 文件

我们知道在编译项目时,编译器编译的是 cpp/.h 文件,而 ui 文件本身是 xml 格式的,因此需要将 ui 文件转换为编译器可编译的 cpp/.h 文件

根据以上的编译输出可知,uic 工具会将 mywindow.ui 转换为 ui_mywindow.h,如下:

class Ui_MyWindow
{
public:
    QWidget *centralwidget;
    QPushButton *btn_start;
    QPushButton *btn_stop;
    QMenuBar *menubar;
    QStatusBar *statusbar;

    void setupUi(QMainWindow *MyWindow)
    {
        if (MyWindow->objectName().isEmpty())
            MyWindow->setObjectName(QString::fromUtf8("MyWindow"));

        MyWindow->resize(800, 600);

        // 只有将 centralwidget 的父窗口设置为我们自己定义的MyWindow,才能在MyWindow show出来时,才可以将centralwidget也显示出来
        centralwidget = new QWidget(MyWindow);
        centralwidget->setObjectName(QString::fromUtf8("centralwidget"));

  // 同理,只有将 btn 的父窗口设置为centralwidget,才能在centralwidget显示出来时,才可以将btn也显示出来
        btn_start = new QPushButton(centralwidget);
        btn_start->setObjectName(QString::fromUtf8("btn_start"));
        btn_start->setGeometry(QRect(270, 260, 75, 24));
        btn_stop = new QPushButton(centralwidget);
        btn_stop->setObjectName(QString::fromUtf8("btn_stop"));
        btn_stop->setGeometry(QRect(400, 260, 75, 24));

        MyWindow->setCentralWidget(centralwidget);

        // 同理,只有将 menubar 的父窗口设置为我们自己定义的MyWindow,才能在MyWindow show出来时,才可以将menubar也显示出来
        menubar = new QMenuBar(MyWindow);
        menubar->setObjectName(QString::fromUtf8("menubar"));
        menubar->setGeometry(QRect(0, 0, 800, 22));
        MyWindow->setMenuBar(menubar);

        // 同理,只有将 statusbar 的父窗口设置为我们自己定义的MyWindow,才能在MyWindow show出来时,才可以将 statusbar 也显示出来
        statusbar = new QStatusBar(MyWindow);
        statusbar->setObjectName(QString::fromUtf8("statusbar"));
        MyWindow->setStatusBar(statusbar);

        retranslateUi(MyWindow);

        QMetaObject::connectSlotsByName(MyWindow);
    } // setupUi

    void retranslateUi(QMainWindow *MyWindow)
    {
        MyWindow->setWindowTitle(QCoreApplication::translate("MyWindow", "MyWindow", nullptr));
        btn_start->setText(QCoreApplication::translate("MyWindow", "start", nullptr));
        btn_stop->setText(QCoreApplication::translate("MyWindow", "stop", nullptr));
    } // retranslateUi

};

namespace Ui {
    // 这个Ui命名空间中的 MyWindow 类,本身是空实现,但是它继承自 Ui_MyWindow 类
    // Ui_MyWindow 类就是和 mywindow.ui 这个xml文件一一对应的。

    // 比如在在ui设计界面拖放两个按钮,可以在其xml文件中看到两个按钮的属性
    // uic 工具就会根据xml文件中的属性,在 ui_mywindow.h 文件中,生成对应的代码
    // 比如在ui设计界面设置两个按钮的显示文字为“start”和“stop”,那么在生成的这个.h文件中,就会生成对应的代码,如上↑
    class MyWindow: public Ui_MyWindow {};
} // namespace Ui 

至此,我们重新回到上面的三个问题,应该就可以回答了

ui_mywindow.h 这个文件是怎么来的?

答:

通过 Qt 提供的 uic 工具,自动将 .ui 文件,转换为编译器可以编译的 .h 文件

为什么说 Ui::MyWindow 这个 c++ 中的类,是和 xml 格式的 mywindow.ui 文件一一对应的?

答:

uic 工具会去解析 .ui 的 xml 格式文件,依次读取其中的字段,比如 width、height、property 等,来生成对应的 c++ 代码

为什么上面需要调用 ui->setupUi(this); 才可以将界面上拖拽的按钮显示出来

答:

我们自己定义的MyWindow要显示,直接调用其show方法,如果子控件要随我们的窗口一起显示,那么子控件创建时的父窗口就要指定为我们的MyWindow

一句话总结:父窗口显示时,会将其子窗口一起显示出来。

img

1.5 标准信号槽

1、什么是信号槽
(1)信号
首先看一下什么是事件和信号

以 QPushButton 的单击事件为例:

按下按钮,会触发 mousePressEvent 事件,然后 QPushButton 会发射 pressed() 信号;

松开按钮,会触发 mouseReleaseEvent 事件,然后 QPushButton 会发射 released() 信号和 clicked() 信号

常用的事件有很多,比如鼠标的单击和双击事件,鼠标的移动事件,键盘的输入事件等。事件会专门在后边进行讲解

当某个实例化的对象上产生这些事件时,该实例化对象就会发出特定的信号。

信号的本质就是函数,并且是只需声明,无需实现的函数

具体一个类有哪些信号,可以查看 Qt 的帮助文档,以 QPushButton 为例:

首先打开 QPushButton 的帮助说明:

img

接着,跳转到其父类 QAbstractButton,这里就有信号了,如下:

img

点击【signals】可以跳转到信号处,如下:

img

可以点击对应的链接,查看详细说明

这里总结如下:

// 当按钮被点击(按下并抬起)之后,发送该信号,其中带有一个默认参数
// 对于QPushButton 通常不需要传递这个默认参数
// 对于可选中/取消选中的按钮,比如复选框QCheckBox、单选框QRadioButton 可以通过该参数,获取其是否选中
void clicked(bool checked = false);

// 当按钮被按下时,发送该信号
void pressed();

// 当按钮被抬起时,发送该信号
void released();

// 当按钮状态改变时,发送该信号,其中带有一个参数checked
// checked 用于标识复选框QCheckBox、单选框QRadioButton是否被选中
void toggled(bool checked); 

(2)槽
我们通常说的 槽,就是槽函数

当点击了 QPushButton 按钮之后,通常需要执行对应的操作,比如让 QMainWindow 窗口最大/最小/正常化显示,或者关闭窗口

按照以上查看信号的方法,查看 QMainWindow 提供了哪些槽函数,同样跳转到其父类 QWidget 中查看,如下:

img

可以点击对应的链接,查看详细说明,比如:

// 最大化显示
void showMaximized();

// 最小化显示
void showMinimized();

// 正常显示
void showNormal();

// 关闭窗口
bool close(); 

讲解了信号和槽之后,如何实现如下效果呢?

效果:点击按钮后,实现窗口的最大/最小/正常显示,或者关闭窗口。

答:这就需要将信号和槽使用 connect 函数进行连接

比如将 QPushButton 按钮的 clicked() 信号和 QMainWindow 窗口的 close() 槽函数建立连接之后,当点击了 QPushButton 按钮后,Qt 框架就

会自动调用 QMainWindow 窗口的 close() 槽函数,从而实现窗口的关闭。

connect 方法是 QObject 类的静态方法,它有多个重载的方法,如下:

img

Qt4 和 Qt5 中连接信号和槽的方法略有不同,后面会详细说明。

不过总的来说,connect 函数的一般形式如下:

connect(sender, signal, receiver, slot); 

其中:

sender

发出信号的对象。比如 QPushButton 按钮

signal

发出的信号。比如 clicked()

receiver

接收信号的对象。比如 QMainWindow 窗口

slot

接收到信号之后,调用的函数

一句话总结:信号槽是对象之间的信息通讯的方式

2、标准信号槽的使用
这里以一个实际的案例,来演示信号和槽的使用

案例:点击对应按钮,实现窗口的最大化/最小化/正常显示窗口,和关闭窗口

img

(1)新建工程
按照之前的讲解,一步步新建一个窗口工程

(2)界面布局
img

放置按钮并布局

放置4个按钮;

选中窗口,点击工具栏的【水平布局】,进行布局(这样可以对齐并且自适应窗口大小的变化);

在左右两侧各放一个 Horizontal Spacer(避免按钮太大,影响美观);

见名知意

修改窗体的标题为 “标准信号槽-演示”;

放置4个按钮显示的文字,以及name

(3)连接信号槽

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // 连接信号和槽
    connect(ui->btnMax, SIGNAL(clicked()), this, SLOT(showMaximized()));
    connect(ui->btnNormal, SIGNAL(clicked()), this, SLOT(showNormal()));
    connect(ui->btnMin, SIGNAL(clicked()), this, SLOT(showMinimized()));
    connect(ui->btnClose, SIGNAL(clicked()), this, SLOT(close()));
} 

这样,点击按钮,就实现了预期的功能!

(4)拓展:快捷键
向前复制当前行: ctrl + alt + up (向上箭头)

向后复制当前行:ctrl + alt + down(向下箭头)

在使用这两个快捷键时:按住ctrl + alt +箭头,可能与系统图形的快捷键冲突, 导致在 Qt Creator 中无法使用

解决办法:

大多数人使用的是因特尔的核心显卡,则用如下方法:

右键点击桌面空白处 -> 图形选项 -> 快捷键 ->禁用,就OK了。

或者,点击图形属性 -> 找到 ctrl+alt+up 和 ctrl+alt+down,禁用掉就好了,如下:

img

1.6 自定义信号槽

自定义信号和槽的条件:

自定义的类,要继承自 QObject

自定义的类,其中要声明一个宏 Q_OBJECT

只有满足了这两个条件才可以正常使用信号槽机制(当然,槽函数是全局函数、 Lambda 表达式等无需接收者的时候除外,后边讲解)。

1、自定义信号槽案例
接下来,我们通过一个案例,演示自定义信号槽的使用。

案例:“长官” (Commander)发送一个 “冲” (go) 的信号,然后 “士兵" (Soldier)执行“ 战斗” (fight) 的槽函数

(1)创建 Commander 类
在左侧项目文件名上右键 -> 添加新文件:

img

指定类名和父类

img

点击完成,即可添加cpp和.h文件到项目中

img

创建完成之后的 commander.cpp 和 commander.h 文件,内容如下:

#include <QObject>

// 1.自定义的类,需要继承自 QObject 类
class Commander : public QObject
{
    // 2.并且添加 Q_OBJECT 宏,才能正常使用 Qt 的信号和槽机制
    Q_OBJECT
public:
    explicit Commander(QObject *parent = nullptr);
// 3.在 signals 后面添加自定义的信号即可
signals:

};

#endif // COMMANDER_H

// commander.cpp
#include "commander.h"

Commander::Commander(QObject *parent)
    : QObject{parent}
{

} 

(2)添加自定义信号
在 signals 下面添加自定义的信号即可

class Commander : public QObject
{
    Q_OBJECT
public:
    explicit Commander(QObject *parent = nullptr);

signals:
    // 1.信号只需声明,无需实现
    // 2.信号返回值为 void
    void go();
}; 

3)添加 Soldier 类
按照同样的方法,添加 Soldier 类,创建完成之后的 soldier.h 和 soldier.cpp 文件,内容如下:

// soldier.h
 #ifndef SOLDIER_H
 #define SOLDIER_H

 #include <QObject>

 class Soldier : public QObject
 {
     Q_OBJECT
 public:
     explicit Soldier(QObject *parent = nullptr);

 signals:

 };

 #endif // SOLDIER_H

 // soldier.cpp
 #include "soldier.h"

 Soldier::Soldier(QObject *parent)
     : QObject{parent}
 {

 } 

(4)添加自定义槽
Soldier 士兵类,需要实现一个 fight 的槽函数

class Soldier : public QObject
 {
     Q_OBJECT
 public:
     explicit Soldier(QObject *parent = nullptr);

 signals:

 // 1.通常将槽函数添加到 slots 后面
 // 这个 slots 也可以不写。不过建议写上,以指明这是一个槽函数
 // pulic,表示槽函数既可以在当前类及其子类的成员函数中调用,也可以在类外部的其它函数(比如 main() 函数)中调用
 public slots:
    // 2.槽函数的返回值和参数,要和信号保持一致
    // 由于信号无返回值,因此槽函数也无返回值
    // 由于信号无参数,因此槽函数也无参数
    void fight();
 }; 

在 soldier.h 文件中有了槽函数的声明,还需要在 soldier.cpp 中实现

可以在槽函数声明处,直接按 alt + enter 快捷键,快速生成函数定义

void Soldier::fight()
{
    qDebug() << "fight";
} 

(5)连接自定义的信号和槽
信号和槽都已经定义完毕, 接下来就可以进行连接了

在 mainwindow.cpp 中,定义 Commander 和 Soldier 类的实例,并建立信号和槽的连接,如下:

MainWindow::MainWindow(QWidget *parent)
     : QMainWindow(parent)
     , ui(new Ui::MainWindow)
 {
     ui->setupUi(this);

     // 1. 创建两个类的实例
     Commander commander;
     Soldier soldier;

     // 2. 建立信号和槽的连接
     connect(&commander, SIGNAL(go()), &soldier, SLOT(fight()));

     // 3. 发送信号
     // emit 可省略
     /*emit*/commander.go();
 } 

这样,在程序执行后,就可以在【应用程序输出】窗口看到槽函数执行的结果了:

img

(6)信号和槽的重载
我们知道,信号和槽的本质就是函数,是函数就可以重载,因此,我们可以重载同名的信号,和重载同名的槽函数。

仍然以 Commander 和 Soldier 为例:

在 commander.h 中添加重载的 go 信号

class Commander : public QObject
{
    Q_OBJECT
public:
    explicit Commander(QObject *parent = nullptr);

signals:
    void go();
    void go(QString);
}; 

在 soldier.h 中添加重载的 fight 槽函数:

class Soldier : public QObject
{
    Q_OBJECT
public:
    explicit Soldier(QObject *parent = nullptr);

signals:

public slots:
    void fight();
    void fight(QString);
}; 

在 soldier.cpp 中实现重载的 fight 槽函数:

void Soldier::fight(QString s)
{
    qDebug() << "fight for" << s;
} 

接下来在 mainwindow.cpp 中同时发送重载的两个 go 信号:

MainWindow::MainWindow(QWidget *parent)
     : QMainWindow(parent)
     , ui(new Ui::MainWindow)
 {
     ui->setupUi(this);

     // 1. 创建两个类的实例
     Commander commander;
     Soldier soldier;

     // 2. 建立信号和槽的连接
     connect(&commander, SIGNAL(go()), &soldier, SLOT(fight()));
     connect(&commander, SIGNAL(go(QString)), &soldier, SLOT(fight(QString)));

     // 3. 发送信号
     commander.go();
     commander.go("freedom");
 }
 

此时执行程序,就可以在【应用程序输出】窗口看到两个重载的槽函数执行的结果了:

img

2、信号槽总结
(1)使用信号和槽的条件
如果要使用信号和槽,需要满足如下两个条件

自定义的类,要继承自 QObject

自定义的类,其中要声明一个宏 Q_OBJECT

只有满足了这两个条件才可以正常使用信号槽机制。

(2)信号
无需实现

信号的本质是函数,并且只需要声明,不需要实现;

可以重载

信号本质是函数,因此可以重载;

信号声明在类头文件的 signals 域下;

信号返回值类型为 void,参数的类型和个数不限;

信号可以使用 emit 关键字发射,emit 也可以省略;

(3)槽
需要实现

槽的本质是函数,需要实现;

可以重载

槽本质是函数,因此可以重载;

槽函数用 slots 关键字修饰(其实在 Qt5 中可以省略slots关键字,下一节会讲解);

返回值

槽函数的返回值,要和信号保持一致。由于信号的返回值为 void,因此槽函数的返回值也是 void

参数

槽函数的参数个数要 <= 信号的参数个数

也就是说:可以在槽函数中选择忽略信号传来的数据(也就是槽函数的参数比信号的少),但是不能说信号根本没有这个数据,你就要在槽函数中使用(就是槽函数的参数比信号的多,这是不允许的)

1.7 信号槽-多种连接方式

信号和槽要建立连接,本质上是通过 connect 函数来连接实现的。

但是从写法或者操作上来说,有多种方式,以下总结了 5 种方式:

Signal/SLOT(Qt4)

函数地址(Qt5)

UI设计师界面 - 转到槽

UI设计师界面 - 信号槽编辑器

lambda 表达式

大家可根据自己的喜好自行选择。

接下来通过一个案例,来演示这 5 种使用方法:

img

首先,我们先把这个界面,快速地搭建起来

快速新建一个基于 QMainWindow 的工程:

img

切换到 mainwindow.ui 文件,按照如下方法,设计好界面:

修改窗口标题为 “信号槽的 5 种连接方式”

拖拽 5 个 QPushButton

修改按钮的 name:btnMax、btnNormal、btnMin、btnClose、btnSetWindowTitle

布局窗口:垂直布局,以使得布局自适应窗口大小的变化

修改按钮显示的文字,如上

设置按钮字体大小为 15

经过以上几步,就完成了界面的布局,如下:

img

接下来就开始实现 5 种连接方式。

1、Signal/SLOT(Qt4)
上一节的就是使用的这个方式

通过 Signal/SLOT 这两个宏,将函数名以及对应的参数,转换为字符串,这是 Qt4 中使用的方式,当然在 Qt5 中也是兼容支持它的:

connect(sender, SIGNAL(signal()), receiver, SLOT(slot()));
其中:

sender:信号发送者

Signal(Signal()):发送的信号。Signal宏将信号转换成字符串

receiver:信号接收者

SLOT(slot()):槽函数。SLOT宏将槽函数转换成字符串

这种方式,编译器不会做错误检查,即使函数名或者参数写错了,也可以编译通过,这样就把问题留在了运行阶段。

而我们编程开发的一个原则是尽可能早地发现并规避问题,因此这种方式不被推荐。

(1)实现窗口最大化
下面通过这种方式,实现点击按钮,最大化窗口

在 mainwindow.cpp 的构造函数中,使用如下方式连接信号和槽:

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // 1.使用 SIGNAL/SLOT 的方式连接信号和槽
    connect(ui->btnMax, SIGNAL(clicked()), this, SLOT(showMaximized()));

}

此时,运行程序,点击按钮就可以最大化窗口了!

(2)编译不检查
刚才说过,如果函数的名字写错了,在编译时不会报错,比如将 showMaximized 不小心写成了 showMaximize

点击【构建】菜单->【重新构建】,在【编译输出】窗口并不会报错

而在运行时,在【应用程序输出】窗口会看到报错,如下:

22:24:45: Starting E:\qt_project\build-HowToConnectSignalAndSlot-Desktop_Qt_5_15_2_MinGW_32_bit-Debug\debug\HowToConnectSignalAndSlot.exe...
QObject::connect: No such slot MainWindow::showMaximize() in ..\HowToConnectSignalAndSlot\mainwindow.cpp:10
QObject::connect:  (sender name:   'btnMax')
QObject::connect:  (receiver name: 'MainWindow')

7.2 函数地址(Qt5)
这种方式中,信号和槽都使用函数的地址,如下:

connect(sender, &Sender::signal, receiver, &Receiver::slot);

其中:

sender:信号发送者

&Sender::Signal:发送的信号

receiver:信号接收者

&Receiver::slot:槽函数

这种方式,编译时就会对函数类型,参数个数做检查。

(1)实现窗口正常显示
下面通过这种方式,实现点击按钮,正常化显示窗口

在 mainwindow.cpp 的构造函数中,使用如下方式连接信号和槽:

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // 1.使用 SIGNAL/SLOT 的方式连接信号和槽
    connect(ui->btnMax, SIGNAL(clicked()), this, SLOT(showMaximized()));
    
    // 2.使用函数地址的方式连接信号和槽
    connect(ui->btnNormal, &QPushButton::clicked, this, &QMainWindow::showNormal);

}

此时,运行程序,点击按钮就可以正常化显示窗口了!

(2)编译检查
如果函数的名字写错了,在编译时就会报错,比如将 showNormal 不小心写成了 showNorma,少写一个字母 L,

在编辑器中就会飘红报错,如下:

mainwindow.cpp:14:58: No member named 'showNorma' in 'QMainWindow'; did you mean 'QMainWindow::showNormal'? (fix available)
 qwidget.h:486:10: 'QMainWindow::showNormal' declared here

当然了,点击【构建】菜单->【重新构建】,在【编译输出】窗口也会会报错,如下:

 ..\HowToConnectSignalAndSlot\mainwindow.cpp: In constructor 'MainWindow::MainWindow(QWidget*)':
 ..\HowToConnectSignalAndSlot\mainwindow.cpp:14:71: error: 'showNorma' is not a member of 'QMainWindow'
      connect(ui->btnNormal, &QPushButton::clicked, this, &QMainWindow::showNorma);
                                                                        ^~~~~~~~~
 mingw32-make[1]: *** [Makefile.Debug:469: debug/mainwindow.o] Error 1

可见,使用这种方式,错误在代码编辑,程序编译阶段就会暴露出来,及早解决掉它。

这是 Qt 推荐的一种连接信号槽的方式!

3、UI设计师界面-转到槽
下面使用这种方式,实现点击 btnMin 按钮,最小化显示窗口

在UI设计师界面,右键单击 btnMin,然后选择【转到槽...】,弹出如下窗口:

转到槽
选择 clicked(),即可生成并跳转到槽函数,即可在 mainwindow.h 和 mainwindow.cpp 中生成对应的代码,如下:

// mainwindow.h
class MainWindow : public QMainWindow
{
    Q_OBJECT

 ...

private slots:
    void on_btnMin_clicked();
};

// mainwindow.cpp
void MainWindow::on_btnMin_clicked()
{
    this->showMinimized();
}

此时会根据按钮的 name 自动生成对应的槽函数,对应关系为:

按钮的名字:btn1

槽函数的名字为:on_btn1_clicked

注意:如果修改了按钮的 name,那么槽函数的名字也要随之修改。

7.4 UI设计师界面-信号槽编辑器
下面使用这种方式,实现点击 btnClose 按钮,关闭窗口

进入到 UI 设计师界面,【View】菜单 ->【视图】->【Signal & Slots Editor】,在打开的信号槽编辑器中,点击绿色的加号+,

就可以连接信号和槽了:

img

此时,我们的代码文件并没有修改,而是修改了 mainwindow.ui 文件,如下:

img

5、Lambda 表达式
槽函数还可以直接写成 lambda 表达式的形式。

C++11 引入了 lambda 表达式,用于定义并创建匿名的函数对象,可以使代码更加的简洁

Qt 是基于 C++ 的一个 GUI 框架,它完全支持 C++ 的语法,因此在 Qt 中也是可以使用 lambda 表达式的。

学习 Qt 多多少少都需要了解一些 C++ 的语法

不过如果你对 C++ 中的 lambda 表达式不了解,也不用担心,这里我们先复习一下 C++ lambda 表达式的使用

lamda 表达式在现代语言中,非常的普遍。lamda表达式简化了我们的一些语法,使得代码的表达更加的清晰,便于书写与阅读。

一些现代语言,比如 java、python、kotlin 等语言都是支持 lambda 表达式的

(1)复习 lambda 表达式
C++ 中的 lambda 表达式,其实就是匿名函数,语法如下:

[capture](parameters) option -> return-type { body }

其中包含 5 个部分:

capture:捕获列表,可选

捕捉列表总是出现在 lambda 表达式的开始。实际上,是 lambda 引出符,编译器根据该引出符判断接下来的代码是否是 lambda 表达式。

捕捉列表能够捕获上下文中的变量,以在 lambda 表达式内使用,主要有如下几种情况:

# 不捕获任何变量

[]

# 按引用捕获外部作用域中所有变量,在 lambda 表达式内使用

[&]

# 按值捕获外部作用域中所有变量,在 lambda 表达式内使用

# 按值捕获的变量,在 lambda 表达式内是只读的,不能修改赋值

[=]

# 捕获外部作用域中所有变量,其中foo变量按引用捕获,其他的按值捕获

[=, &foo]

# 按值捕获bar变量,同时不捕获其他变量

[bar]

# 捕获当前类中的 this 指针

# 捕获了 this 就可以在 lambda 中使用当前类的成员变量和成员函数。

# 如果已经使用了 & 或者 =,就默认添加此选项。

[this]

parameters:参数列表,可选

option:函数选项,可选

return-type:返回值类型,可选。没有返回值的时候也可以连同符号一起省略

body:函数体

接下来,在 mainwindow.cpp 的构造函数中,逐一演示这 5 个部分的使用

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

 ...

    // 演示lambda表达式
    
    // 3.1 匿名函数的定义

#if 0
    []() {
        qDebug() << "lambda...";
    };
#endif

    // 3.2 匿名函数的调用

#if 0
    []() {
        qDebug() << "lambda...";
    }();
#endif

    int a =10;
    
    // 3.3 不捕获任何变量
    // Variable 'a' cannot be implicitly captured in a lambda with no capture-default specified

#if 0
    []() {
        qDebug() << a;
    }();
#endif

    // 3.4 按引用捕获

#if 0
    [&]() {
        qDebug() << a++;    // 10
    }();
    qDebug() << a;          // 11
#endif

    // 3.5 按值捕获
    // 按值捕获的变量,在 lambda 表达式内是只读的,不能修改赋值

#if 0
    [=]() {
        // Cannot assign to a variable captured by copy in a non-mutable lambda
        qDebug() << a++;
    }();
#endif

    // 3.6 按值捕获 + mutalbe 选项
    // 添加 mutable 选项,就可以在 lambda 内修改捕获的变量了
    // 并且=这种方式,是按值传递的,里面的修改,不会影响外边。

#if 0
    [=]() mutable {
        qDebug() << a++;    // 10
    }();
    qDebug() << a;          // 10
#endif

    // 3.7 参数
    // 添加 mutable 选项,就可以在 lambda 内修改捕获的变量了
    // 并且=这种方式,是按值传递的,里面的修改,不会影响外边。

#if 0
    [](int x, int y) {
        qDebug() << x + y;    // 3
    }(1, 2);
#endif

    // 3.8 返回值
    // 返回值可以省略,编译器会自动推断 lambda 表达式的返回值类型
    // 返回值省略时,也可以连同符号`->`一起省略

#if 0
    int sum = [](int x, int y) -> int {
        return x + y;
    }(1, 2);
    qDebug() << sum;    // 3
#endif

#if 1
    int sum = [](int x, int y) {
        return x + y;
    }(1, 2);
    qDebug() << sum;    // 3
#endif
}

(2)槽函数使用 lambda 表达式
有了以上 lambda 表达式的基本知识,将它作为槽函数,就水到渠成了,如下:

MainWindow::MainWindow(QWidget *parent)
     : QMainWindow(parent)
     , ui(new Ui::MainWindow)
 {
     ui->setupUi(this);

     // 1.使用 SIGNAL/SLOT 的方式连接信号和槽
     connect(ui->btnMax, SIGNAL(clicked()), this, SLOT(showMaximized()));
    
     // 2.使用函数地址的方式连接信号和槽
     connect(ui->btnNormal, &QPushButton::clicked, this, &::QMainWindow::showNormal);
    
     // 3.UI设计师界面-转到槽
     // 直接在UI界面,鼠标点击完成信号槽的连接
    
     // 4.UI设计师界面-信号槽编辑器
     // 直接在UI界面,鼠标点击完成信号槽的连接
    
     // 5. 使用lambda 表达式做槽函数
     connect(ui->btnSetWindowTitle, &QPushButton::clicked, this, [this]() {
         this->setWindowTitle("[连接信号槽的 5 种方式]");
     });

 }

此时,运行程序,点击 btnSetWindowTitle 按钮,就可以修改窗口的标题了!

1.8 信号槽-扩展

1、如何连接重载的信号和槽
在信号和槽存在重载时,Qt4 和 Qt5 的写法是有区别的:

Qt4 方式

可以在 SIGNAL/SLOT 中指定函数参数类型,因此写法比较简单;

Qt5 方式

指定信号和槽时,只能指定函数名,无法向 Qt4 那样指定函数参数类型,需要单独定义函数指针,写法上稍显麻烦。

接下来,以《6 自定义信号槽》中 “长官和士兵” 的例子为例,来看下信号槽重载时 Qt4 和 Qt5 写法的不同

// 1、Qt4信号槽的连接:SIGNAL/SLOT
#if 0
    Commander commander;
    Soldier soldier;

    connect(&commander, SIGNAL(go()), &soldier, SLOT(fight()));
    connect(&commander, SIGNAL(go(QString)), &soldier, SLOT(fight(QString)));

    emit commander.go();
    emit commander.go("freedom");
#endif

    // 2、Qt5信号槽的连接:函数地址
#if 0
    Commander commander;
    Soldier soldier;

    // 没有同名的信号和槽时,可以直接这样写。因为不存在二义性
    // connect(&commander, &Commander::go, &soldier, &Soldier::fight);

    // 有同名的信号和槽时,需要向下面这样定义函数指针。因为存在二义性
    // 编译器自动推断:将无参的信号go和无参的槽,赋值给函数指针(ctrl+鼠标点击可以智能跳转过去)
    void (Commander::*pGo)() = &Commander::go;
    void (Soldier::*pFight)() = &Soldier::fight;
    connect(&commander, pGo, &soldier, pFight);

    // 编译器自动推断:将有参的信号go和有参的槽,赋值给函数指针(ctrl+鼠标点击可以智能跳转过去)
    void (Commander::*pGoForFreedom)(QString) = &Commander::go;
    void (Soldier::*pFightForFreedom)(QString) = &Soldier::fight;
    connect(&commander, pGoForFreedom, &soldier, pFightForFreedom);

    emit commander.go();
    emit commander.go("freedom");
#endif
 

8.2 一个信号连接多个槽
一个信号可以连接多个槽函数,如下:

connect(sender, SIGNAL(signal), receiver1, SLOT(fun1()));
connect(sender, SIGNAL(signal), receiver2, SLOT(fun2())); 

这样,当 SIGNAL 这个信号发出时,它连接的 2 个槽函数 fun1,fun2 都会被执行,并且:

Qt4

信号发射时,与之相连接的槽函数的执行顺序是随机的。

Qt5+

信号发射时,这些槽函数的执行顺序与建立连接的顺序相同。

接下来,以《6 自定义信号槽》中 “长官和士兵” 的例子为例:

// 士兵1很勇敢,收到冲锋的信号后,开始战斗
connect(&commander, SIGNAL(go()), &soldier1, SLOT(fight()));

// 士兵2很怕死,收到冲锋的信号后,开始逃跑
connect(&commander, SIGNAL(go()), &soldier2, SLOT(escape())); 

接下来一步步实现这个需求:

首先,在 Soldier 类中添加槽函数的声明和定义,如下:

// Soldier.h
class Soldier : public QObject
{
    ...

public slots:
    void fight();
    void fight(QString);

    // 添加一个“逃跑”的槽函数
    void escape();
};

// Soldier.cpp
void Soldier::escape()
{
    qDebug() << "i'm afraid of death, escape...";
} 

然后,连接信号槽并发送信号,如下:

// 3、一个信号连接多个槽函数
#if 1
    Commander commander;
    Soldier soldier1;
    Soldier soldier2;

    // 士兵1很勇敢,收到冲锋的信号后,开始战斗
    connect(&commander, SIGNAL(go()), &soldier1, SLOT(fight()));

    // 士兵2很怕死,收到冲锋的信号后,开始逃跑
    connect(&commander, SIGNAL(go()), &soldier2, SLOT(escape()));

    emit commander.go();
#endif 

8.3 多个信号连接一个槽
可以将多个信号连接到同一个槽函数,如下:

connect(sender, SIGNAL(signal1), receiver, SLOT(fun()));
connect(sender, SIGNAL(signal2), receiver, SLOT(fun())); 

这样,当 SIGNAL1 和 singnal2 这 2 个信号发出时,都会执行槽函数 fun

接下来,以《6 自定义信号槽》中 “长官和士兵” 的例子为例:

// 当 commander 发射 go 信号和 move 信号时,都会执行士兵的 fight 槽函数,开始战斗
connect(&commander, SIGNAL(go()), &soldier, SLOT(fight()));
connect(&commander, SIGNAL(move()), &soldier, SLOT(fight())); 

接下来一步步实现这个需求:

首先,在 Commander 类中新添加一个 move 的信号,如下:

class Commander : public QObject
 {
     ...

 signals:
     void go();
     void go(QString);

     // 新添加一个 move 信号
     void move();
 }; 

然后,连接信号槽并发送信号,如下:

// 4、多个信号连接一个槽函数
#if 1
    Commander commander;
    Soldier soldier;

    // 当 commander 发射 go 信号和 move 信号时,都会执行士兵的 fight 槽函数,开始战斗
    connect(&commander, SIGNAL(go()), &soldier, SLOT(fight()));
    connect(&commander, SIGNAL(move()), &soldier, SLOT(fight()));

    emit commander.go();
    emit commander.move();
#endif
 

8.4 信号连接信号
信号不仅可以连接槽, 还可以和连接信号,如下:

connect(obj1, SIGNAL(signal1), obj2, SIGNAL(signal2));

这样,当 obj1 发送 SIGNAL1 信号时,就会触发 obj2 发送 SIGNAL2 信号。

接下来,同样以《6 自定义信号槽》中 “长官和士兵” 的例子为例:

// 按钮的点击会发射clicked信号 => commander发射move信号 => soldier执行escapse槽函数
connect(ui->btnAction, &QPushButton::clicked, commander, &Commander::move);
connect(commander, &Commander::move, soldier, &Soldier::escape);

接下来一步步实现这个需求:

首先,在 Commander 类中新添加一个 move 的信号,如下:

class MainWindow : public QMainWindow
{
 ...

    // 在 MainWindow 中,添加 commander 和 soldier 两个指针类型的成员变量
    Commander *commander;
    Soldier *soldier;

};

然后,实例化 commander 和 soldier 两个对象,并连接信号槽,如下:

// 5、信号连接信号
#if 1
    // 首先,成员变量初始化
    commander = new Commander();
    soldier = new Soldier();

    // 然后,信号连接信号 + 信号连接槽
    connect(ui->btnAction, &QPushButton::clicked, commander, &Commander::move);
    connect(commander, &Commander::move, soldier, &Soldier::escape);

#endif

此时,点击按钮,按钮会发射 clicked 信号, 接着 commander 发射 move 信号,move 信号的发射,会去执行 soldier 的 escape 槽函数

注意:

此时的 commander 和 soldier 要定义为类的成员变量。

因为如果把 commander 和 soldier 定义为局部变量,MainWindow 构造执行完毕后,这两个变量就已经释放了

8.5 断开连接 - disconnect
disconnect 用于断开信号和槽之间已经建立的连接。

这种情况并不常用,因为当一个对象 delete 之后, Qt 自动取消所有连接到这个对象上面的槽。

// 6、断开信号的连接
#if 1
    Commander commander;
    Soldier soldier;

    connect(&commander, SIGNAL(go()), &soldier, SLOT(fight()));
    connect(&commander, SIGNAL(go(QString)), &soldier, SLOT(fight(QString)));
    
    emit commander.go();
    
    // 断开所有连接到 commander 信号上的槽函数
    commander.disconnect();
    
    emit commander.go("freedom");  // 对应的槽函数不会执行

#endif

当然了,disconnect 有多个重载的函数,具体参考 Qt 帮助文档即可。

disconnect 函数并不常用,因为当一个对象 delete 之后, Qt 自动取消所有连接到这个对象上面的槽。

2.1 窗口基类 QWidget

所有窗口类的基类

Qt中有 3 个窗口的基类:QWidget、QMainWindow、QDialog

在创建Qt工程时,会让我们选择继承自哪一个窗口类

其中,QMainWindow、QDialog 都是继承自 QWidge

所有控件类的基类

Qt中的控件类(按钮、输入框、单选框等)也属于窗口类

它们的基类也是 QWidget;

可以内嵌到其他窗口的内部,此时需要给其指定父窗口;

可以作为独立的窗口显示,此时不能给其指定父窗口;

1、如何显示 QWidget 窗口
在之前的《2 新建 Qt 工程》中,显示了一个基于QMainWindow 的窗口

本节从零新建一个基于 QWidget 的主窗口,并且再添加一个基于 QWidget 的窗口,并显示它

由于这是第一次,手动添加一个窗口类,所以详细列出步骤

(1)新建基于 QWidget 的窗口类
和《2 新建 Qt 工程》中基本一致,新建一个工程 QWidgetTest,只是这里让主窗口继承自 QWidget

(2)再添加一个 QWidget 窗口类
首先,右键单击【QWidgetTest】,点击【添加新文件...】

img

选择【Qt】->【Qt 设计师界面类】

img

选择模板

前三个对应 QDialog,后面的两个分别对应 QMainWindow 和 QWidget

选择【Widget】,然后下一步

img

输入类名,然后点击下一步

img

汇总显示

img

点击完成之后,就可以将三个文件添加到项目中,并且自动在 .pro 项目文件中,添加了对应的行,如下:

img

(3)显示新添加的 QWidget 窗口
为了便于显示,打开 subwidget.ui 设计师界面, 拖放一个按钮,如下:

img

然后来到 widget.cpp 文件中,添加如下代码,来显示 SubWidget 窗口:

Widget::Widget(QWidget *parent)
     : QWidget(parent)
     , ui(new Ui::Widget)
 {
     ui->setupUi(this);

 #if 0
     // 未指定父窗口,那么这个窗口就是独立的窗口
     // 需要调用其show方法来显示
     SubWidget* subWidget = new SubWidget();
     subWidget->setWindowTitle("SubWidget");
     subWidget->show();
 #endif

 #if 1
     // 如果指定了父窗口,那么就不用调用其show方法了
     // 因为:父窗口显示时,会将其子窗口一起显示出来。
     SubWidget* subWidget = new SubWidget(this);
     subWidget->setWindowTitle("SubWidget");
     // myWidget->show();
 #endif

 } 

2、常用的属性和方法
在上面,调用了 setWindowTitle 方法设置了窗口的标题,那么它还有哪些常用的方法呢?

可以点击 Qt Creator 左侧的【帮助】,切换到【索引】,输入QWidget,就可以查看 QWidget 的详细帮助文档,如下:

img

(1)窗口位置

// 1、设置窗体的几何信息
// 获取相对于当前窗口父窗口的几何信息:宽高、坐标点信息
const QRect &geometry() const;
void setGeometry(int x, int y, int w, int h);
void setGeometry(const QRect &);

// 2、移动窗口。
// 重新设置窗口的位置
void move(int x, int y);
void move(const QPoint &); 

(2)窗口大小
通常,窗口可以拖动其右下角,进行放大和缩小。我们可以设置窗口的最小大小和最大大小;

另外,窗口可以设置为固定大小,这样窗口就不能放大和缩小;

常用的设置大小的函数如下:

// 1、设置窗口尺寸
 QSize size() const
 void resize(int w, int h);
 void resize(const QSize &);

 // 2、设置最大尺寸
 QSize maximumSize() const;
 void setMaximumSize(const QSize &);
 void setMaximumSize(int maxw, int maxh);

 // 3、设置最小尺寸
 QSize minimumSize() const;
 void setMinimumSize(const QSize &);
 void setMinimumSize(int minw, int minh);

 // 4、设置固定尺寸
 void QWidget::setFixedSize(const QSize &s);
 void QWidget::setFixedSize(int w, int h);

 // 5、单独设置窗口的高度
 int height() const;
 int minimumHeight() const;
 int maximumHeight() const;

 void setFixedHeight(int h);
 void setMaximumHeight(int maxh);
 void setMinimumHeight(int minh);

 // 6、单独设置窗口的宽度
 int width() const;
 int minimumWidth() const;
 int maximumWidth() const;

 void setFixedWidth(int w);
 void setMaximumWidth(int maxw);
 void setMinimumWidth(int minw); 

(3)窗口标题、图标、资源文件
设置和获取窗口的标题,直接使用如下两个函数即可:

// 获取和设置窗口的标题
QString windowTitle() const;
void setWindowTitle(const QString &); 

设置窗口的图标,需要一个 QIcon 对象,如下:

// 获取和设置窗口的图标
QIcon windowIcon() const;
void setWindowIcon(const QIcon &icon);

// 构造 QIcon 图标对象
// 有 6 个重载的构造方法,通常我们使用最后一个
// 参数为图标文件的路径
QIcon::QIcon(const QString &fileName);

首先将图标文件 clock.ico,复制到项目目录 QWidgetTest 下的 icon 目录中,如下:

img

在项目上右键,选择【添加新文件...】

img

选择【Qt Resource File】,这样就会在项目中,添加一个 .qrc 的文件

img

资源文件命名为 main.qrc

img

点击完成就会在项目中,添加一个 main.qrc 的资源文件:

img

接下来打开 “资源文件编辑器”,如下:

img

注意:如果没有打开,直接右键 main.qrc,然后选择 “Open in Editor” 即可

然后,点击 “Add Prefix”,添加前缀:

img

再点击 “Add Files”,定位到拷贝到项目目录下的图标文件clock.ico,最终效果如下

img

接下来就可以拷贝这个 clock.ico 的资源路径,在程序中使用了

img

另外,QWidget 还有许多的槽函数:

img

2.2 按钮 QPushButton

2、属性和方法
按钮除了可以设置显示文本之外,还可以设置图标

(1)文本
可以获取和设置按钮上显示的文本

// 获取和设置按钮的文本
QString text() const
void setText(const QString &text)

该属性,既可以在 Qt 设计师右侧的属性窗口中修改,也可以在代码中动态地获取和修改

(2)图标
可以获取和设置按钮上显示的图标

// 获取和设置按钮的图标
QIcon icon() const
void setIcon(const QIcon &icon)

该属性,既可以在 Qt 设计师右侧的属性窗口中修改,也可以在代码中动态地获取和修改

(3)样式表
样式表可以设置包括文本颜色、背景色、边框、字体等很多样式

// 获取和设置样式表
// 这是继承自QWidget类的属性和方法
// 只要继承自QWidget类的控件,都有该属性
QString styleSheet() const
void setStyleSheet(const QString &styleSheet)

样式表既可以在 Qt 设计师右侧的属性窗口中修改,也可以在代码中动态地获取和修改

(4)信号
按钮在按下和抬起的过程中,会发射多个信号。

// 当按钮被点击(按下并抬起)时,发送该信号,其中带有一个默认参数
// 对于QPushButton 通常不需要传递这个默认参数
// 对于可选中/取消选中的按钮,比如复选框QCheckBox、单选框QRadioButton 可以通过该参数,获取其是否选中
void clicked(bool checked = false);

// 当按钮被按下时,发送该信号
void pressed();

// 当按钮被抬起时,发送该信号
void released();

3、案例
该案例演示,如何设置按钮的文本和图标,以及信号的使用,如下:

img

实现步骤:

(1)布局
在 UI 设计师界面,拖拽对应的控件,修改显示的文字、控件的 name,然后完成布局

(2)添加图标
首先添加资源文件,并添加两个图标文件(方法在上一节已经进行了详细的介绍)

img

然后为按钮添加图标,如下:

img

(3)添加样式表
在右侧的属性窗口中,或者在按钮上右键->【改变样式表...】,都可以打开编辑样式表的对话框

如下,设置了按钮的字体颜色和背景渐变:

img

添加样式表

(4)代码实现

// 1、设置文本和图标
 void Widget::on_btnStart_clicked()
 {
     QString s = ui->btnStart->text();

     if(s == "启动") {
         // 设置文本和字体
         ui->btnStart->setText("停止");
         ui->btnStart->setFont(QFont("黑体", 16));
    
         // 设置图标和图标大小
         ui->btnStart->setIcon(QIcon(":/icon/stop.ico"));
         ui->btnStart->setIconSize(QSize(48, 48));
     } else {
         ui->btnStart->setText("启动");
         ui->btnStart->setFont(QFont("黑体", 12));
    
         ui->btnStart->setIcon(QIcon(":/icon/start.ico"));
         ui->btnStart->setIconSize(QSize(32, 32));
     }

 }

 // 2、使能禁能
 void Widget::on_btnConnect_clicked()
 {
     QString s = ui->btnConnect->text();
     if(s == "连接") {
         ui->btnConnect->setText("断开连接");
         ui->btnSend->setEnabled(true);
     } else {
         ui->btnConnect->setText("连接");
         ui->btnSend->setEnabled(false);
     }
 }

 // 3、信号演示
 // 按钮抬起时,会发射clicked信号
 void Widget::on_btnSignal_clicked()
 {
     qDebug() << "clicked...";
 }

 // 按钮按下时,会发射pressed信号
 void Widget::on_btnSignal_pressed()
 {
     qDebug() << "pressed...";
 }

 // 按钮抬起时,会发射released信号
 void Widget::on_btnSignal_released()
 {
     qDebug() << "released...";
 } 

2.3 标签 QLabel

QLabel 是 QT 中的标签类,通常用于显示提示性的文本,也可以显示图像

1、属性和方法
QLabel 有很多属性,完整的可查看帮助文档,这里列出常用的属性和方法。

(1)文本
可以获取和设置按钮上显示的文本

// 获取和设置显示的文本
QString text() const;
void setText(const QString &text); 

该属性,既可以在 Qt 设计师右侧的属性窗口中修改,也可以在代码中动态地获取和修改

(2)对齐方式
用于设置标签中的内容在水平和垂直两个方向上的对齐方式,比如左对齐、右对齐、上对齐、下对齐、水平居中、垂直居中等。

// 获取和设置文本的对齐方式
Qt::Alignment alignment() const;
void setAlignment(Qt::Alignment); 

其中,Qt::Alignment 是一个宏,常用取值如下:

Qt::AlignLeft(0x0001) 水平方向-左对齐

Qt::AlignRight(0x0002) 水平方向-右对齐

Qt::AlignHCenter(0x0004) 水平方向-居中对齐

Qt::AlignTop(0x0020)垂直方向-上对齐

Qt::AlignBottom(0x0040)垂直方向-下对齐

Qt::AlignVCenter(0x0080)垂直方向-居中对齐

Qt::AlignCenter(AlignVCenter | AlignHCenter) 垂直方向和水平方向-居中对齐

上面的每一个宏,都代表16进制中的一位,可以进行或(|)操作,来同时设置多个对齐方式。

该属性,既可以在 Qt 设计师右侧的属性窗口中修改,也可以在代码中动态地获取和修改

(3)换行
使能换行时,那么标签在缩放时,如果文字可以显示在下一行,就会自动换行

// 获取和设置文本是否允许换行
// 换行时:在 word-breaks处,不会将一个完整的单词显示在两行
bool wordWrap() const; // 判断是否允许换行
void setWordWrap(bool on); // 设置是否允许换行 

该属性,既可以在 Qt 设计师右侧的属性窗口中修改,也可以在代码中动态地获取和修改

(4)图像
可以获取和设置标签上显示的图像

// 获取和设置显示的图像
const QPixmap *pixmap() const;
void setPixmap(const QPixmap &pixmap); 

该属性,既可以在 Qt 设计师右侧的属性窗口中修改,也可以在代码中动态地获取和修改

3.2 案例
该案例演示,如何设置和获取标签文本,文本对齐方式,文本换行,以及显示图片

img

实现步骤:

(1)布局
在UI设计师界面,拖拽对应的控件,修改显示的文字、控件的 name,然后完成布局

(2)为标签添加背景色
在标签右侧的属性窗口中,点击styleSheet属性右侧的 "...",即可打开如下窗口,可以设置标签的各种样式:

img

修改样式

(3)为标签添加图片
首先添加资源文件,并添加两个图标文件(方法在之前已经进行了详细的介绍)

然后在标签右侧的属性窗口中,点击pixmap属性右侧的小三角,选择 “选择资源”,即可打开如下窗口,选择图片资源:

img

添加图片

(4)代码实现

void Widget::on_btnGetText_clicked()
{
    QMessageBox::information(this, "标签演示", ui->lbTime->text());
}

void Widget::on_btnSetText_clicked()
{
    ui->lbTime->setText(QTime::currentTime().toString());
}

void Widget::on_btnAlignLeft_clicked()
{
    ui->lbAlign->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
}

void Widget::on_btnAlignCenter_clicked()
{
    ui->lbAlign->setAlignment(Qt::AlignCenter);
}

void Widget::on_btnAlignRight_clicked()
{
    ui->lbAlign->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
}

void Widget::on_btnDisableWrap_clicked()
{
    ui->lbWrap->setWordWrap(false);
}

void Widget::on_btnEnableWrap_clicked()
{
    ui->lbWrap->setWordWrap(true);
}

int index = 0;
void Widget::on_btnToggleImage_clicked()
{
    QPixmap px;

    // 将图标文件加载到 QPixmap
    if(index == 0) {
        px.load(QString(":/icon/strawberry.ico"));
        index++;
    } else {
        px.load(QString(":/icon/apple.ico"));
        index = 0;
    }

    // 把图片设置到标签上
    ui->lbImage->setPixmap(px);

    // 不要缩放,否则拉伸导致失真
    ui->lbImage->setScaledContents(false);

    // 图片在标签中居中显示(水平和垂直方向均居中)
    ui->lbImage->setAlignment(Qt::AlignCenter);
} 

2.4 文本框 QLineEdit

QLineEdit 是 QT 中的文本框,准确地说是单行文本框,通常用于接受用户的输入

比如用户输入用户名、密码等,都可以使用该控件。

1、属性和方法
QLineEdit 有很多属性,完整的可查看帮助文档。这里列出常用的属性和方法:

(1)占位字符串
占位字符,就是当文本框中输入内容为空时,显示的字符,用于提示用户文本框中应该输入什么内容

// 获取和设置占位字符串
QString placeholderText() const
void setPlaceholderText(const QString &)

如下,是 QQ 登录界面的用户名和密码文本框,在输入内容为空时,提示如下:

(2)对齐方式

用于设置文本框中的内容在水平和垂直两个方向上的对齐方式,比如左对齐、右对齐、上对齐、下对齐、水平居中、垂直居中等。

// 获取和设置文本的对齐方式
Qt::Alignment alignment() const
void setAlignment(Qt::Alignment flag)

其中,Qt::Alignment 是一个宏,常用取值如下:

Qt::AlignLeft(0x0001) 水平方向-左对齐

Qt::AlignRight(0x0002) 水平方向-右对齐

Qt::AlignHCenter(0x0004) 水平方向-居中对齐

Qt::AlignTop(0x0020)垂直方向-上对齐

Qt::AlignBottom(0x0040)垂直方向-下对齐

Qt::AlignVCenter(0x0080)垂直方向-居中对齐

Qt::AlignCenter(AlignVCenter | AlignHCenter) 垂直方向和水平方向-居中对齐

上面的每一个宏,都代表16进制中的一位,可以进行或(|)操作,来同时设置多个对齐方式。

(3)回显模式
所谓回显模式(EchoMode),就是输入的内容如何显示

该属性既可以在 Qt 设计师右侧的属性窗口中修改,也可以在代码中动态地获取和修改

// 获取和设置回显模式
QLineEdit::EchoMode echoMode() const
void setEchoMode(QLineEdit::EchoMode)

其中,QLineEdit::EchoMode 是一个宏,有如下 4 个取值:

QLineEdit::Normal

正常模式。输入什么就显示什么,默认就是这种方式

QLineEdit::Password

密码模式。不显示实际输入的字符,而是以小圆圈代替,这样别人就无法看到输入的字符。

Do not display anything. This may be appropriate for passwords where even the length of the password should be kept secret.

QLineEdit::NoEcho

无回显模式。无论输入什么内容,在文本框中都不会显示,这样别人既无法看到输入的内容,也无法知道输入字符的长度

这对于输入密码非常有用,在linux下输入密码时,就是这种模式

QLineEdit::PasswordEchoOnEdit

正在输入时显示正常模式显示,当失去焦点时以密码模式显示,也就是显示小圆圈

(4)读写控制
用于设置文本框是否可编辑、是否使能

// 获取和设置文本框的只读属性
bool isReadOnly() const
void setReadOnly(bool)

// 获取和设置文本框的是否使能
bool isEnabled() const
void setEnabled(bool)

注意:设置为只读和设置为禁能,都无法对文本框进行编辑

区别在于,只读时文本框不会置灰,而禁能时文本框会置灰

(5)格式控制
用于指定文本框输入特定格式的内容。比如输入电话号码格式的文本,或者输入IP地址格式的文本等。

// 设置和获取格式控制
QString inputMask() const
void setInputMask(const QString &inputMask)

接收一个 QString 类型的掩码,常用格式如下:

无格式

// 接受任何字符的输入
setInputMask("");

电话号码

// 9 - 表示要求输入0-9的数字
// ;# - 表示格式字符串结束,并且未输入时显示#
ui->leMask->setInputMask("99999999999;#");

密钥

// 9 - 表示要求输入0-9的数字
// ;# - 表示格式字符串结束,并且未输入时显示*
ui->leMask->setInputMask(">AAAAA-AAAAA-AAAAA-AAAAA-AAAAA;*");

其他更多格式使用方法,直接查看QLineEdit帮助文档即可,以上列出的两个,仅作为抛砖引玉

(6)信号槽
QLineEdit 有多个信号和槽函数,详情可参考官方帮助文档,这里仅演示两个常用的,作为抛砖引玉

textChanged 信号

// 当文本框内容改变时,发射该信号
void textChanged(const QString &text)

editingFinished

// 当输入完毕时,发射该信号

void editingFinished()

什么叫输入完毕呢?

什么叫输入完毕呢?

答:a、按了enter键 b、文本框失去焦点

2、案例
该案例演示,文本框的回显模式、对齐方式、读写控制、校验、格式控制,如下:

img

(1)布局
在UI设计师界面,拖拽对应的控件,修改显示的文字、控件的 name,然后完成布局

(2)代码实现

Widget::Widget(QWidget *parent)
     : QWidget(parent)
     , ui(new Ui::Widget)
 {
     ui->setupUi(this);

     // 默认正常模式回显
     on_btnEchoNormal_clicked();
     // 默认左对齐
     on_btnAlignLeft_clicked();
     // 默认可读可写
     on_btnAccessReadWrite_clicked();
     // 默认无格式控制
     on_btnMaskNone_clicked();
    
     // 演示-textChanged信号
     ui->leAdd1->setValidator(new QIntValidator(this)); // 用于设置校验,只允许文本框中输入整数
     ui->leAdd1->setPlaceholderText("加数");
     ui->leAdd2->setValidator(new QIntValidator(this));
     ui->leAdd2->setPlaceholderText("被加数");
     ui->leSum->setPlaceholderText("和");
    
     // 演示-textFinished信号
     ui->leSub1->setValidator(new QIntValidator(this));
     ui->leSub1->setPlaceholderText("减数");
     ui->leSub2->setValidator(new QIntValidator(this));
     ui->leSub2->setPlaceholderText("被减数");
     ui->leResult->setPlaceholderText("差");

 }

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

 // 1、回显模式
 // 1.1 正常
 void Widget::on_btnEchoNormal_clicked()
 {
     ui->leEcho->clear();
     ui->leEcho->setEchoMode(QLineEdit::Normal);
     ui->leEcho->setPlaceholderText("Normal");
 }

 // 1.2 密码模式
 void Widget::on_btnEchoPassword_clicked()
 {
     ui->leEcho->clear();
     ui->leEcho->setEchoMode(QLineEdit::Password);
     ui->leEcho->setPlaceholderText("Password");
 }

 // 1.3 不回显
 void Widget::on_btnEchoNoEcho_clicked()
 {
     ui->leEcho->clear();
     ui->leEcho->setEchoMode(QLineEdit::NoEcho);
     ui->leEcho->setPlaceholderText("NoEcho");
 }

 // 1.4 失去焦点时为密码模式
 void Widget::on_btnEchoPasswordEchoOnEdit_clicked()
 {
     ui->leEcho->clear();
     ui->leEcho->setEchoMode(QLineEdit::PasswordEchoOnEdit);
     ui->leEcho->setPlaceholderText("PasswordEchoOnEdit");
 }

 // 打印输处
 void Widget::on_btnEchoPrint_clicked()
 {
     QMessageBox::information(this, "回显模式", ui->leEcho->text());
 }

 // 2、对齐方式
 // 2.1 左对齐
 void Widget::on_btnAlignLeft_clicked()
 {
     ui->leAlignment->setAlignment(Qt::AlignLeft);
     ui->leAlignment->setPlaceholderText("Left");
 }

 // 2.2 居中对齐
 void Widget::on_btnAlignCenter_clicked()
 {
     ui->leAlignment->setAlignment(Qt::AlignCenter);
     ui->leAlignment->setPlaceholderText("Center");
 }

 // 2.3 右对齐
 void Widget::on_btnAlignRight_clicked()
 {
     ui->leAlignment->setAlignment(Qt::AlignRight);
     ui->leAlignment->setPlaceholderText("Right");
 }

 // 3、读写控制
 // 3.1 可读可写
 void Widget::on_btnAccessReadWrite_clicked()
 {
     ui->leAccess->setReadOnly(false);
     ui->leAccess->setPlaceholderText("ReadWrite");
 }

 // 3.2 只读
 void Widget::on_btnAccessReadOnly_clicked()
 {
     ui->leAccess->setReadOnly(true);
     ui->leAccess->setPlaceholderText("ReadOnly");
 }

 // 3.3 禁能
 void Widget::on_btnAccessDisabled_clicked()
 {
     ui->leAccess->setEnabled(false);
     ui->leAccess->setPlaceholderText("Disabled");
 }

 // 3.4 使能
 void Widget::on_btnAccessEnabled_clicked()
 {
     ui->leAccess->setEnabled(true);
     ui->leAccess->setPlaceholderText("Enabled");
 }

 void Widget::on_btnAccessPrint_clicked()
 {
     QMessageBox::information(this, "文本框演示", ui->leAccess->text());
 }

 // 4、格式控制
 // 4.1 无格式
 void Widget::on_btnMaskNone_clicked()
 {
     ui->leMask->setInputMask("");
 }

 // 4.2 电话号码格式
 void Widget::on_btnMaskPhone_clicked()
 {
     // 9 - 表示要求输入0-9的数字
     // ;# - 表示格式字符串结束,并且未输入时显示#
     ui->leMask->setInputMask("99999999999;#");
 }

 // 4.3 密钥格式
 void Widget::on_btnMaskLicense_clicked()
 {
     // 9 - 表示要求输入0-9的数字
     // ;# - 表示格式字符串结束,并且未输入时显示*
     ui->leMask->setInputMask(">AAAAA-AAAAA-AAAAA-AAAAA-AAAAA;*");
 }

 // 5、信号槽演示
 // 5.1 textChanged信号 - 实现自动计算加法结果
 void Widget::on_leAdd1_textChanged(const QString &arg1)
 {
     int sum = arg1.toInt() + ui->leAdd2->text().toInt();
     ui->leSum->setText(QString::number(sum));
 }

 void Widget::on_leAdd2_textChanged(const QString &arg1)
 {
     int sum = arg1.toInt() + ui->leAdd1->text().toInt();
     ui->leSum->setText(QString::number(sum));
 }

 // 5.2 editingFinished信号 - 实现自动计算减法结果
 void Widget::on_leSub1_editingFinished()
 {
     int result = ui->leSub1->text().toInt() - ui->leSub2->text().toInt();
     ui->leResult->setText(QString::number(result));
 }

 void Widget::on_leSub2_editingFinished()
 {
     int result = ui->leSub1->text().toInt() - ui->leSub2->text().toInt();
     ui->leResult->setText(QString::number(result));
 } 

2.5 单选按钮 QRadioButton

单选按钮常用在 “多选一” 的场景,也就是说,在一组单选按钮中,一次只能选中一个单选按钮

比如性别中的 “男女” 二选一,学历中的 “博士/硕士/本科/其他” 四选一,等等。

1、属性和方法
QRadioButton有很多属性,完整的可查看帮助文档。这里列出常用的属性和方法:

(1)文本
这两个是其父类 QAbstractButton 中的属性和方法,因此QPushButton、QRadioButton、QCheckBox 都具有该属性

// 获取和设置显示的文本
QString text() const
void setText(const QString &text)

(2)选中状态

// 获取和设置单选按钮的选中状态
bool isChecked() const
void setChecked(bool)

可见,切换单选按钮的选中状态,有两种方式:

可见,切换单选按钮的选中状态,有两种方式:

通过鼠标点击实现

在代码中使用 setChecked(bool) 来实现

(3)自动排他
我们前面说过,单选按钮实现的是 “多选一”,因此单选按钮的该属性默认是使能的

// 获取和设置自动排他
bool autoExclusive() const
void setAutoExclusive(bool)

而对于多选按钮,也叫复选按钮-QCheckBox,通常的场景是用户选择一组按钮中的多个,因此该属性默认是禁能的。

综合以上,“多选一” 要满足以下两个条件:

把同一组的单选按钮,放在同一个布局中。不同的组的单选按钮,放在不同的布局中

单选按钮的 autoExclusive 属性设置为 true,单选按钮的该属性默认是使能的。可在右侧的属性按钮中看到,如下:

(4)信号槽
按钮在按下和抬起的过程中,会发射多个信号。

// 单选按钮 QRadioButton 被点击时,会发出该信号
void clicked();

// 当单选按钮的选中状态发生改变时,会发射该信号
// 所谓状态改变,是指选中变为非选中,和非选中变为选中
void toggled(bool checked)

2、案例
该案例演示,单选按钮的属性以及信号槽

img

(1)布局
在UI设计师界面,拖拽对应的控件,修改显示的文字、控件的 name,然后完成布局

(2)代码实现
完整的项目

 

Widget::Widget(QWidget *parent)
     : QWidget(parent)
     , ui(new Ui::Widget)
 {
     ui->setupUi(this);

     // 设置第2组中的默认显示
     ui->rbMale2->setChecked(true);
     ui->rbMaster2->setChecked(true);
    
     // 设置第3组中的默认显示
     ui->rbFemale3->setChecked(true);
     ui->rbDoctor3->setChecked(true);
    
     // 使用 QButtonGroup 对单选按钮进行分组
     mBtnGroupGender = new QButtonGroup(this);
     mBtnGroupGender->addButton(ui->rbMale4,0);
     mBtnGroupGender->addButton(ui->rbFemale4,1);
    
     mBtnGroupEdu = new QButtonGroup(this);
     mBtnGroupEdu->addButton(ui->rbDoctor4,10);
     mBtnGroupEdu->addButton(ui->rbMaster4,11);
     mBtnGroupEdu->addButton(ui->rbBachelor4,12);
     mBtnGroupEdu->addButton(ui->rbOther4,13);
    
     connect(ui->rbMale4, &QRadioButton::clicked, this, &Widget::onRadioButtonClicked);
     connect(ui->rbFemale4, &QRadioButton::clicked, this, &Widget::onRadioButtonClicked);
     connect(ui->rbDoctor4, &QRadioButton::clicked, this, &Widget::onRadioButtonClicked);
     connect(ui->rbMaster4, &QRadioButton::clicked, this, &Widget::onRadioButtonClicked);
     connect(ui->rbBachelor4, &QRadioButton::clicked, this, &Widget::onRadioButtonClicked);
     connect(ui->rbOther4, &QRadioButton::clicked, this, &Widget::onRadioButtonClicked);
    
     // 设置第4组中的默认显示
     ui->rbMale4->setChecked(true);
     ui->rbBachelor4->setChecked(true);
     onRadioButtonClicked();

 }

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

 void Widget::on_btnGetSelection_clicked()
 {
     QString s;

     if(ui->rbMale3->isChecked()) {
         s += ui->rbMale3->text();
     } else if(ui->rbFemale3->isChecked()) {
         s += ui->rbFemale3->text();
     }
    
     if(ui->rbDoctor3->isChecked()) {
         s += ui->rbDoctor3->text();
     } else if(ui->rbMaster3->isChecked()) {
         s += ui->rbMaster3->text();
     } else if(ui->rbBachelor3->isChecked()) {
         s += ui->rbBachelor3->text();
     } else if(ui->rbOther3->isChecked()) {
         s += ui->rbOther3->text();
     }
    
     ui->leResult3->setText(s);

 }

 void Widget::onRadioButtonClicked()
 {
     QString s;

     int checkedGenderId = mBtnGroupGender->checkedId();
     if(checkedGenderId == 0) {
         s += "男";
     } else if(checkedGenderId == 1) {
         s += "女";
     }
    
     int checkedEduId = mBtnGroupEdu->checkedId();
     if(checkedEduId == 10) {
         s += "博士";
     } else if(checkedEduId == 11) {
         s += "硕士";
     } else if(checkedEduId == 12) {
         s += "本科";
     } else if(checkedEduId == 13) {
         s += "其他";
     }
    
     ui->leResult4->setText(s);

 }

 void Widget::on_rbMale4_toggled(bool checked)
 {
     qDebug()<< "rbMale4 state changed: " << checked;
 }