0. 序言
阔别一年,近日重拾 C++ Qt 做了点有意思的东西,于是把对前一篇教程未涉及的一些知识,以及一些可以改进的方式和一些积累的免费资源获取途径一并形成续作分享于此。
- 本文基于你已经阅读和实操过C++ Qt——从入门到入土 (一)的假设展开叙述。
1. 关于 Qt 安装的补充说明
1.1 安装途径——国内镜像源在线安装
重拾C++ Qt的第一站便是安装Qt,跟着自己上一篇博客的教程走,结果发现速度慢得感人,望着浏览器预估的7小时内完成下载,我果断搜索了国内镜像源。并且找到了清华大学镜像源——清华大学Qt在线安装软件镜像源
如下图所示安装自己系统对应的软件,因为我用的Windows,所以框出了Windows版本的:
1.2 具体安装内容
具体安装过程和需要勾选的内容就不多赘述了,站内已有许多相关优质文章,这里给出其中一篇作为指引:Qt详细安装指南
2. 使用 QtCreator 进行开发
2.1 为什么不用 VS 了?
看过前一篇博客的读者朋友可能会暗暗发问,为什么上一篇贯穿始终的 VS 到这被取代了。原因很简单,我的新设备装的是 VS2022 ,可能是由于版本过新,导致我一使用 VS 建立 Qt 项目就会报一个叫做 system.exception
的错误。因此我直接采用 QtCreator 进行开发。
这里实际上采用 QtCreator 很多方面比 VS 更方便了反而,同时在企业中开发 Qt 项目时也多是用 CMake+QtCreator 的方式构建项目。
因此,从这一篇文章开始编译器一律采用 Qtcreator。
2.2 构建一个新项目
- 首先,根据下图指引找打 QtCreator 并打开
- 按照下图指引建立一个新项目
- 填写项目名称和地址
- 接着一路点下一步直到语言选择
- 编译器选择 可以看到其实还是用 VS2019 来完成代码编译的
- 现在 你便得到了一个基于 CMake 构建的 采用 Qt Creator 开发的项目模板了
2.3 构建 & 运行
可以看到页面左下方有三个按钮 自上而下依次为:
- 编译运行 Run
点击后直接对项目中所有源码进行编译 并运行 Main.cpp
- DeBug
点击之后对项目中所有源码进行编译 并进入调试模式运行 Main.cpp
- 编译和构建 Build
编译所有源码 并通过 CMake 更新项目架构
那么,现在直接点击 Run 运行一下看看吧!
没有任何问题,成功构建了一个项目。
2.4 对主窗口源码的微调
在我实际开发的过程中,遇到了一个堆栈溢出的问题,即关闭窗口后报错说堆栈溢出,具体信息类似于数组越界。这很邪门,因为第一次遇到这个报错的时候我连数组都没调用过。
经过理论分析和考证得知应该是窗口关闭后的析构问题,为了规避这个错误,大家可以将Main.cpp中的倒数两行进行如下修改:
- 修改前
MainWindow w;
w.show();
- 修改后:
MainWindow* w = new MainWindow();
w->show();
P.S. 这两种方式哪个不会引发程序崩溃是需要自己试的!!!如果你在其中一种写法下一关闭软件窗口就报错,那么换成另一种就好了!!!血的教训!!!
3. 更方便的布局方式——QLayout
- P.S. 本文将贯穿一个 购物软件 的设计开发为例进行讲解。
3.1 问题引入
在上一篇文章中,我们对控件的排布摆放都是通过人工计算位置+Move函数移动到预期位置来实现的。诚然,这是一种十分精确的精确到像素级的排布方式,但是计算起来很麻烦,控件一旦多起来会带来很多计算工作,这当然是我们希望避免的。
这个问题在软件的开发中很容易体现出来。回到我们的购物软件设计这条主线任务,首先常常活跃混迹于各种购物网站如 Amazon, Macy 以及各种品牌购物网站的你肯定发现了他们最共同的地方。
以 Burberry 为例,打开其官网,你会发现顶部的这一栏就像成为一个业内默规一样,大伙的购物网站顶部清一色都是这样的导航栏:
解构一下这里的元素你会发现其实构成元素很简单:
- 左侧水平并列着六个类别选择标签
- 中间是 LOGO 标签
- 右侧是经典三元素即:
- 搜索
- 账户
- 购物车
这样的设计美观简洁,功能强大。那么我们便直接以这样的UI设计再简化一下得到我们的需求。
首先我们随机挑选一个设计感不错的商家获取他们的资源图。第一步找到导航栏资源,我们知道在淘宝的PC页面装修页面顶部是有一个 招牌图 的,我们随机以一个PC端的淘宝店铺页面为例:
可以看到这里会有一张950*120格式的招牌背景图,相比于白底而言,这更有视觉冲击力。
因此,现在,假设我们的需求是实现一个在这张背景图上左侧放置店铺LOGO,右侧底部防止经典三元素的导航栏,如下图:
经过上一篇博客的学习,聪明的你肯定已经立刻意识到这里只需要5个控件,即:
- 背景图标签 QLabel
- LOGO图标签 QLabel
- 搜索按钮/标签 QPushButton/QLabel
- 账户按钮/标签 QPushButton/QLabel
- 购物车按钮/标签 QPushButton/QLabel
现在已知上述五个标签 的尺寸分别为:
- QSize(950, 120)
- QSize(60, 60)
- QSize(30, 30)
- QSize(30, 30)
- QSize(30, 30)
现在你可以前文的 Move 方式来计算各个控件坐标位置,如下图所示:
即各控件坐标位置应为:
- 0, 0
- 5, 30
- 825, 85
- 870, 85
- 915, 85
你可能觉得这并不是很大的计算量,但是当控件多起来,排版复杂起来,一切都会非常麻烦。因此我们引入 Qt 中一个非常强大的控件排布方式——QLayout。
3.2 QLayout 原理与类型概述
QLayout顾名思义便是应用在 Qt 开发中的布局,他实际上是一种隐形控件,听起来很高大上,但实际上经过上一篇的学习,你敏锐的洞察到这只不过是一个初始时便把SetVisible设置成false让他看不见的把戏罢了。
在这个隐形控件中的所有子元素都会按照一定规则进行整齐的排布。
至于这一规则是什么,我们来通过简介所有四种 QLayout 的功能来一一体会:
QLayout类型 | 排布规则 |
---|---|
QHBoxLayout | 水平并列排布 |
QVBoxLayout | 垂直并列排布 |
QGridLayout | 网格排布 根据给各个控件指定的坐标以及行列占据数排布 |
QFormLayout | 两列控件 垂直并列排布 左侧为标签 右侧为输入框等 |
现在我们先通过 QtDesigner 来直观地感受一下各个布局内的排布规则
P.S, 均以向对应的布局内加入 4 个标签来展示效果,并设置标签颜色方便直观感受
-
QHBoxLayout
下面的每个颜色块代表一个标签, 最外面的红色虚线代表QLayout
-
QVBoxLayout
下面的每个颜色块代表一个标签, 最外面的红色虚线代表QLayout
-
QGridLayout
下面的每个颜色块代表一个标签, 最外面的红色虚线代表QLayout
P.S. 这里第二个标签设置占满一行,其他占满两行
-
QFormLayout
下面的每个颜色块代表一个标签,右侧白色条为输入框, 最外面的红色虚线代表QLayout
3.3 QLayout的空间构成
通过上面的介绍和展示,聪明的你给 QLayout 取了一个别名叫 ”容器“ ,没错,其实 QLayout 就像是一个看不见外壳的容器,里面按照事先定下的规则摆放各式各样的控件。
细心的你可能注意到了在上述的图例中,布局内各个控件间水平、竖直方向均存在着一定的空隙。恭喜你,成功发现了QLayout的“隐藏设置”,即默认为布局中的每个控件之间设置了一定的间隙。这个间隙可以由函数 setSpacing(int)
来设置所有控件间的间隙大小,也可以通过 addSpacing(int)
来改变特定两个控件中的间隙。
回到我们上面的导航栏例子,可以看到 搜索-账户-购物车 三个图标之间均存在 15px 的间隙,而整体来看最左最右又有着 5px 的间隙。
前者显然可以通过将在对应布局调用 setSpacing(15)
轻松实现,而后者则不是控件与控件间的间距了,这要如何实现呢。
想想现实生活中任何一个容器总是有一定厚度的吧,即实际上存放物品的内层空间与外层在上、下、左、右四个方向都有着一定的间隙。下图可以很好的反应这种关系:
实际上,QLayout正是这样一种默认厚度为0的特殊容器,而他的厚度可以由四个属性来决定:
layoutLeftMargin
: 内部存储空间与外层的左侧间隙layoutRightMargin
: 内部存储空间与外层的右侧间隙layoutTopMargin
: 内部存储空间与外层的顶部间隙layoutBottomMargin
: 内部存储空间与外层的底部间隙
因此上面的需求可以通过将layoutLeftMargin
和layoutRightMargin
设置为 5 来实现。具体而言这四个方向的间隙设置通过 setContentsMargins(left, top, righht, bottom)
函数实现。四个参数分别代表 左上右下 四个方向的间隙大小。
3.4 布局的基本使用方法
现在让我们结合上面介绍的知识,试着将导航栏的布局实现一下。
3.4.1 准备工作——背景标签的图片设置
首先结合上一篇文章的知识将最底部的背景图标签实现出来。我们给主窗口建立一个叫做 Nav_Background_Label 的 QLabe l指针私有成员。随后直接我抄我自己,得到一个给标签设置背景图的主窗口的私有函数SetImageOnLabel
,其中label
参数为一个QLabel指针,path
为背景图片存放路径,label_size
为标签尺寸,则有:
mainwindow.cpp中:
void MainWindow::SetImageOnLabel(QLabel* label, QString path, QSize label_size){// IMG SettingQFile file(path);if (!file.open(QIODevice::Append)){// Set to default colorlabel->setStyleSheet("QLabel {background-color:rgba(110, 189, 221, 0.5);}");}else{label->setText(tr(""));// Create a QPixmap objectQPixmap pix = QPixmap();// Load the imagepix.load(path);// Set the specific sizelabel->setFixedSize(label_size);// Set background image by 'setPixmap' methodlabel->setPixmap(pix.scaled(label->size(),Qt::IgnoreAspectRatio,Qt::SmoothTransformation));// Auto Fill the Background and it's necessarylabel->setAutoFillBackground(true);label->setStyleSheet("QLabel {background-color:rgba(255, 255, 255, 0);}");label->setScaledContents(true);}
}
mainwindow.h中:
// 额外引入头文件
#include <QLabel>
#include <QFile>
#include <QPixmap>public:void SetImageOnLabel(QLabel*, QString, QSize);
P.S. 为了避免出现图片读取失败,请采用绝对路径
此时,为标签添加背景图片就很简单了。我们自定义一个私有函数 nav_label_init
来进行对主窗口导航栏的标签进行初始化 ,此后代码仅展示源文件.cpp部分并补充说明需要引入的头文件以节省篇幅。
则有:
// 注意 这里是基于主窗口设置了(1080, 720)的大小实现的
void MainWindow::nav_label_init(void){Nav_Background_Label = new QLabel(this);SetImageOnLabel(Nav_Background_Label,tr("P:/Qt_Projects/My_Store_App/MyStore/img/navi_back.png"),QSize(950, 120));Nav_Background_Label->move(65, 0);
}
效果如下:
3.4.2 布局使用三部曲
现在,我们将 Logo、Search、Account、Cart 四个标签加入同一个水平布局中,并将该布局放在背景标签上。
首先,需要在头文件中引入 QLayout 如下:
#include <QLayout>
随后为窗口设置一个水平布局私有成员 Nav_HLayout
如下:
private:QHBoxLayout* Nav_Layout = NULL;// 这里四个是要加入的标签声明QLabel* Nav_logo_label = NULL,* Nav_search_label = NULL,* Nav_account_label = NULL,* Nav_cart_label = NULL;
定义好后,在 nav_label_init
中初始化各个控件,然后通过addWidget方法将各个标签加入水平布局中:
Step 1. 各控件初始化
Nav_Layout = new QHBoxLayout();Nav_logo_label = new QLabel(this);SetImageOnLabel(Nav_logo_label,tr("P:/Qt_Projects/My_Store_App/MyStore/img/navi_logo.png"),QSize(60, 60));Nav_search_label = new QLabel(this);SetImageOnLabel(Nav_search_label,tr("P:/Qt_Projects/My_Store_App/MyStore/img/navi_search.png"),QSize(30, 30));Nav_account_label = new QLabel(this);SetImageOnLabel(Nav_account_label,tr("P:/Qt_Projects/My_Store_App/MyStore/img/navi_account.png"),QSize(30, 30));Nav_cart_label = new QLabel(this);SetImageOnLabel(Nav_cart_label,tr("P:/Qt_Projects/My_Store_App/MyStore/img/navi_cart.png"),QSize(30, 30));
Step 2. 利用 addWidget 函数依次将控件加入布局
Nav_Layout->addWidget(Nav_logo_label);Nav_Layout->addWidget(Nav_search_label);Nav_Layout->addWidget(Nav_account_label);Nav_Layout->addWidget(Nav_cart_label);
Step 3. 将布局利用 setLayout 函数绑定到背景标签上
Nav_Background_Label->setLayout(Nav_Layout);
此时效果如下:
可以看到,此时四个标签都很整齐地水平排布在了背景标签上。
由此我们可以得到 QLayout 的使用三部曲:
- 初始化 布局控件 和 要排布的控件
- 通过
addWidget
依次向布局中加入控件 - 将布局通过
setLayout
绑定到指定控件
3.4.3 布局的对齐方式
由我们上述的需求和样例可以看到,Logo标签位于背景的左侧,而三要素位于右下角且贴于底部。这恰巧对应上了布局中的一个很重要的属性——对齐方式。
P.S. 这一属性其实是几乎所有控件都具备的。
首先,我们给出 Qt 中常用的 七种 对齐方式:
对齐属性值 | 效果 |
---|---|
Qt::AlignTop | 贴于顶部 |
Qt::AlignBottom | 贴于底部 |
Qt::AlignLeft | 贴于左侧 |
Qt::AlignRight | 贴于右侧 |
Qt::AlignHCenter | 水平居中 |
Qt::AlignVCenter | 垂直居中 |
Qt::AlignCenter | 水平垂直均居中 |
值得注意的是,这里的对齐方式可以通过或运算 ‘|’ 进行任意组合
如右下角可以表示为:Qt::AlignRight|Qt::AlignBottom
而为一个控件设置对齐方式的方法则是调用 setAlignment
函数。因此我们进行如下尝试:
// 在绑定到 背景标签 之前设置
Nav_Layout->setAlignment(Qt::AlignRight|Qt::AlignBottom);
效果如下:
可以看到所有 Nav_Layout
中的控件都被放到右下角了,这是由于 QLayout 设置的对齐方式是针对整个布局本身这个控件而言的,因此其内所有控件都会遵从。可是 Logo 我们是希望在左侧的,这该如何是好呢?
这就要引出下面的 布局嵌套 了。
3.5 布局嵌套
细心的你可能早就察觉到了,我在讲述 QLayout 的原理时用到的字眼是,其中的所有 子元素 会遵从规则排布。这一子元素当然可以是任何Qt控件,当然也包括布局。
所以我们可以单独把 Logo 加入一个居左对齐的水平布局 Logo_Layout
中,随后再将其与 Nav_Layout
合并放到一个大布局 Nav_Bar_Layout
中。
则有:
Logo_Layout->addWidget(Nav_logo_label);Logo_Layout->setAlignment(Qt::AlignLeft);Nav_Layout->addWidget(Nav_search_label);Nav_Layout->addWidget(Nav_account_label);Nav_Layout->addWidget(Nav_cart_label);Nav_Layout->setAlignment(Qt::AlignRight|Qt::AlignBottom);Nav_Layout->setSpacing(15);Nav_Bar_Layout->addLayout(Logo_Layout);Nav_Bar_Layout->addLayout(Nav_Layout);Nav_Background_Label->setLayout(Nav_Bar_Layout);
此时效果如下:
值得注意的是,向布局中添加布局应当使用函数 addLayout
3.6 网格布局 QGridLayout
有了导航栏之后,已经开了个好头,接下来要想顾客能浏览我们的商品,那自然需要在主界面的导航栏下方添加一个浏览页面用于展示商品信息。这里需要添加一个子窗口来实现浏览效果。(这一部分具体知识内容将在下一篇博客介绍,这段时间大概2天一更直到完善这个购物软件吧。)
3.6.1 建立一个基于栈页面的Qt类
首先类似于 VS 建立 Qt 类的操作,如下图所示新建类:
接着我们需要进入头文件和源文件中将如下两个地方改成 QStackedWidget
3.6.2 确定子窗口大小
由于该窗口需要放到其他窗口中显示,因此需要重写 sizeHint() const
这一虚函数来在每次子窗口显示前 指定一个大小。因此需要在该子窗口的头文件处声明,并在源文件处如下定义:
// 950是为了和顶部等宽
QSize OverViewWindow::sizeHint() const{return QSize(950, 480);
}
3.7 QGridLayout 的使用
3.7.1 主要使用方法
QGridLayout
顾名思义是将其存储空间划分为若干个小块形成若干行与若干列的网格状空间。因此,在向其内添加控件时,除了要使用对应类型的 add
函数并提供控件指针外,还需提供一组数据 (row, col, rowspan, colspan)
分别代表:
- 起始位置是第几行 从零开始
- 起始位置是第几列 从零开始
- 总共占据几行
- 总共占据几列
P.S. 后两个参数为可选参数 不是必须给出的
通过上一篇文章的学习,添加一个浏览界面私有成员到主窗口中并完成初始化对你来说不是一件难事,因此对此不再多做赘述。这里我为其添加了一个叫做 OverviewPage
的私有成员,并通过 QSS 设置成了米色背景方便区别主窗口背景。
现在我们利用 addWidget
函数来将导航栏和这个浏览页面布置到主窗口。首先他们都是要占据所有列的,即等宽的。因此,我们只需要考虑占据行数的差距,这里我们以:
- 导航栏占据一行
- 浏览页面占据五行
- 每行一共 10 列
为例,则可以进行如下设置:
MainLayout->addWidget(Nav_Background_Label, 0, 0, 1, 10);
MainLayout->addWidget(OverviewPage, 1, 0, 5, 10);
MainLayout->setAlignment(Qt::AlignHCenter | Qt::AlignTop);
this->setLayout(MainLayout);
此时运行你会发现如下报错信息:
QWidget::setLayout: Attempting to set QLayout "" on MainWindow "MainWindow", which already has a layout
这是因为主窗口是默认有一个布局方案的,因此需要“替换”成自己的布局,则需要采取下面提供的一种间接方案。
3.7.2 “替换”主窗口的布局方案
由于主窗口是有布局方案的,因此我们采用将需要的布局绑定到一个新窗口,随后将之设置为主窗口的中心页面来实现,具体到代码,则如下所示:
QWidget * widget = new QWidget(this);
widget->setLayout(this->MainLayout);
this->setCentralWidget(widget);
此时可以得到预期效果如下(主窗口大小初始设置为950, 600效果最佳):
3.7.3 行列间的间隔
细心的你肯定发现了 在明明应该紧密相连的导航栏与浏览页面之间存在着一条很细的间隔,这是你无论怎么设置上述的容器间隔都消除不了。这是因为这里的间隔来自 QGridLayout 专属的两个属性:
layoutHorizontalSpace
: 同一行中 各控件水平间距layoutVerticalSpace
: 相邻行中的垂直间距
他们分别可以通过:
setHorizontalSpacing(int)
: 设置水平间距setVerticalSpacing(int)
: 设置垂直间距
来实现调节,例如我们将这两个值都设置为0如下:
MainLayout->setHorizontalSpacing(0);
MainLayout->setVerticalSpacing(0);
此时效果如下:
4. 批量初始化和设置控件外观
4.1 采用 QVector 进行批量设置
在上述的例子中,其实对于 背景图、Logo、三要素 这五个标签的初始化和背景图设置是相当重复的,因此可以采用如下方式进行批量设置:
Step 1. 初始化并加入同一个 Qt 容器
QVector<QLabel*> labels;
Nav_Background_label = new QLabel(this);
labels.append(Nav_Background_label );
Nav_logo_label = new QLabel(this);
labels.append(Nav_logo_label );
Nav_search_label = new QLabel(this);
labels.append(Nav_search_label );
Nav_account_label = new QLabel(this);
labels.append(Nav_account_label );
Nav_cart_label = new QLabel(this);
labels.append(Nav_cart_label );
Step 2. 通过容器存储各个标签对应图片的位置和大小信息
QString Img_Path = "P:/Qt_Projects/My_Store_App/MyStore/img";
QVector<QString> Label_Imgs = {Img_Path+tr("/navi_back.png"),Img_Path+tr("/navi_search.png"),Img_Path+tr("/navi_account.png"),Img_Path+tr("/navi_cart.png"),Img_Path+tr("/navi_logo.png")};
QVector<QSize> Label_Sizes = {QSize(950, 120), QSize(30, 30), QSize(30, 30),QSize(30, 30), QSize(60, 60)};
Step 3. 遍历标签容器进行批量设置
// Set the Labels
for(auto i = 0; i < labels.length(); i++){SetImageOnLabel(labels[i], Label_Imgs[i], Label_Sizes[i]);
}
你可能会觉得这样有些画蛇添足多此一举,但实际上,当需要重复设置的属性很多时,这种批量设置的写法就能体现出它的优越之处。因此,采取这种方式对格式属性类似的控件进行初始化设置时非常良好的习惯。
5. 杂谈
5.1 图标绘制和公开免费资源
这里的 搜索-账户-购物车
这三个图标是我在一个公开图标网白嫖来的,这里的图标种类很全,而且免费,强力推荐:免费图标资源获取
此外的图片和图标都是我利用一个免费的像素画网站PixelArt亲手绘制的,像素画门槛低,简单易学,对于想做出自己风格的软件的独立开发者值得一试。