C++ Qt——从入门到入土 (三)
0. 前置知识 — SQL 在C++ Qt的应用
SQL数据库在C++ Qt中的保姆级应用详解
点赞+关注+收藏 私信获取本文的全部源码哦
1. 登录功能的实现
1.0 登录-注册模板项目
为了方便自己和大家在软件开发中的登录-注册页面构造,我开源了一套自制的模板项目供大家使用C++ Qt 登录-注册模板
1.1 登录窗口的布局
首先,我们想要通过布局和QSS实现如下的登录窗口外观。
Step 1. 首先,我们拆解一下上图中需要用到哪些控件:
- 蓝色代表 QLabel
- 橙色代表 QLineEdit
- 红色代表 QPushButton
Step 2. 接着,我们分析一下窗口的布局:
显而易见的是,这里 一共有 4 行,每一行行内 水平对齐,因此可以通过 4 个水平布局实现。而每一行 居中放置于每一列,因此可以直接 将四行的水平布局依次加入一个居中放置元素的垂直布局 来实现。
1.2 登录窗口类的建立和控件的初始化
首先,现在的你已经可以驾轻就熟的建立一个基于 QMainWindow 的登录窗口了。这里我将其命名为 SignWindow
。为其添加上述需要用到的控件到私有成员中如下:
private:
QVBoxLayout* VLayout = NULL;
QLabel* Background_Label = NULL,
* Area_Label = NULL,
* User_Label = NULL,
* Password_Label = NULL,
* Password_Seen_Label = NULL,
* Register_Label = NULL;
QLineEdit* User_Line = NULL,
* Password_Line = NULL;
QPushButton* Sign_Button = NULL;
这里的命名也很直白了哈哈哈,接着我们通过一个公有函数 Items_Init()
先将他们初始化到登录窗口上。
void SignWindow::Items_Init(void){
VLayout = new QVBoxLayout();
Background_Label = new QLabel(this);
Area_Label = new QLabel(this);
User_Label = new QLabel(this);
Password_Label = new QLabel(this);
Password_Seen_Label = new QLabel(this);
Register_Label = new QLabel(this);
User_Line = new QLineEdit(this);
Password_Line = new QLineEdit(this);
Sign_Button = new QPushButton(this);
}
1.3 小插曲 —— 将图片设置函数封装到一个自定义库中
在上一篇博客,我们在主窗口中封装了一个为标签设置图片的函数。这是一个几乎每个窗口都能用到的功能,因此我们将他单独封装到一个库中。说白了就是 建立一对 头-源文件
,还是点击左侧小锤子添加新文件,依次选择添加 Header
和 Source
文件即可:
utils.h
:
#ifndef UTILS_H
#define UTILS_H
#include <QString>
#include <QLabel>
#include <QPixmap>
#include <QFile>
void Set_Image_On_Label(QLabel*, QString);
#endif // UTILS_H
utils.cpp
:
#include "utils.h"
void Set_Image_On_Label(QLabel* label, QString path){
QFile file(path);
if (!file.open(QIODevice::Append))
{
// Set to default color
label->setStyleSheet("QLabel {background-color:rgba(110, 189, 221, 0.5);}");
}
else
{
label->setText("");
// Create a QPixmap object
QPixmap pix = QPixmap();
// Load the image
pix.load(path);
// Set background image by 'setPixmap' method
label->setPixmap(pix.scaled(label->size(),
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation));
// Auto Fill the Background and it's necessary
label->setAutoFillBackground(true);
label->setStyleSheet("QLabel {background-color:rgba(255, 255, 255, 0);}");
label->setScaledContents(true);
}
}
此时在任何需要为标签添加背景图片的源文件顶部引入 "utils.h"
即可调用这一函数了。
1.4 将控件添加到布局
我们通过一个自定义的公有函数 Layout_Init()
先将他们按上述布局分析的排布放入对应布局控件。
void SignWindow::Layout_Init(void){
// Horizontal Layouts
QVector<QHBoxLayout*> HLayouts;
for(auto i = 0; i < 4; i++){
HLayouts.append(new QHBoxLayout());
}
// First Row
HLayouts[0]->addWidget(User_Label);
HLayouts[0]->addWidget(User_Line);
// Second
HLayouts[1]->addWidget(Password_Label);
HLayouts[1]->addWidget(Password_Line);
HLayouts[1]->addWidget(Password_Seen_Label);
// Third
HLayouts[2]->addWidget(Sign_Button);
// Fourth
HLayouts[3]->addWidget(Register_Label);
// Add to Vertical Layout
for(auto i = 0; i < 4; i++){
VLayout->addLayout(HLayouts[i]);
}
// Set to AreaLabel
Area_Label->setLayout(VLayout);
}
上面用到了通过局部指针容器来批量设置水平布局,这在实际开发中是非常常见的优化写法。
1.5 自定义可点击标签 —— CLabel
接下来我们需要设置各个控件的外观样式了,所以我们希望能先在主窗口中通过 点击账户图标弹出登录窗口,你可能兴冲冲地结合前面两篇的知识直接咣咣咣为其绑定了一个由点击信号触发的窗口弹出槽函数。
但是!!! QLabel根本没有内置点击信号
因此,我们需要自定义一个基于 QLabel
的可点击标签类,步骤也很简单,首先按照建立一个 QWidget 派生类的方法建立一个名叫 CLabel
的类(名字随意,CLabel单纯因为我和我女朋友姓的缩写都是C)
接着将类中表示继承的部分换成 QLabel 再引入 QLabel 即可。那么如何让他能发出点击信号呢?这里就回归 Qt底层逻辑,即任何一个 Qt 控件在被鼠标点击时都会触发一个 鼠标点击事件。这是一个继承自最底层的类的公有槽函数,因此我们只需要覆写他,在其中发射 clicked()
信号即可。
clabel.h
:
#ifndef CLABEL_H
#define CLABEL_H
#include <QLabel>
#include <QMouseEvent>
class CLabel : public QLabel
{
Q_OBJECT
public:
explicit CLabel(QWidget *parent = nullptr);
signals:
void clicked();
protected:
virtual void mousePressEvent(QMouseEvent* event);//重写mousePressEvent事件
};
#endif // CLABEL_H
clabel.cpp
:
#include "clabel.h"
CLabel::CLabel(QWidget *parent)
: QLabel{parent}
{
}
void CLabel::mousePressEvent(QMouseEvent *ev)
{
// emit the 'clicked' signal
if (ev->button() == Qt::LeftButton) {
//触发clicked信号
emit clicked();
}
// Send back to the parent object
QLabel::mousePressEvent(ev);
}
这里挖个坑,后续会穿插详细介绍 鼠标点击事件 的内容,这里仅给出实际使用实例。
接着我们将主窗口中三要素的类别全部替换成 CLabel
并在批量初始化时设置一个聚焦状态将鼠标改为手状态,即 setCursor(QCursor(Qt::PointingHandCursor));
随后我们在主窗口中添加如下两个私有变量和一个公有槽函数:
SignWindow* SignPage
登陆页面
bool Sign
判断当前是否有账号登录
void Open_Sign_Window()
打开登录窗口的槽函数
按照教程一中介绍过的互斥窗口的逻辑实现如下打开槽函数:
void MainWindow::Open_Sign_Window(void){
if(!Sign){
if(SignPage != NULL){
SignPage->close();
}
SignPage = new SignWindow(this);
SignPage->show();
}
}
此时我们将该槽函数绑定到账户标签:
connect(Nav_account_label,
&CLabel::clicked,
this,
&MainWindow::Open_Sign_Window);
随后在主窗口中点击账户标签则会弹出登录窗口:
1.6 控件样式批量设置
关于 QSS 的样式设置详见系列教程的第一篇,此处直接给出我的样式设置代码。随后仅补充说明之前未提到过的重要属性。
void SignWindow::QSS_Init(void){
// Main Area
Area_Label->setFixedSize(400, 240);
Area_Label->setObjectName("Core_Area");
Area_Label->setStyleSheet("QLabel#Core_Area\
{background-color:rgba(160, 160, 160, 0.4);\
border-radius:15px;\
border-width: 1px;\
border-style: solid;\
border-color: black;}");
// Center Position
Area_Label->move((this->width()-Area_Label->width())/2,
(this->height()-Area_Label->height())/2 + 20);
// Item Vectors
QVector<QLabel*> Labels;
Labels.append(User_Label);
Labels.append(Password_Label);
QVector<QLineEdit*> LineEdits;
LineEdits.append(User_Line);
LineEdits.append(Password_Line);
QVector<QString> LineNames = {"User Names: ", "Password: "};
QVector<QString> LineHints = {"请输入帐号", "请输入密码"};
QVector<QString> LineImgs = {"img/Line_Account.png", "img/Line_Password.png"};
// Register
Register_Label->setText("Do not have an account? Register Here!");
Register_Label->setFont(QFont("Timers", 12, QFont::Bold));
Register_Label->setMaximumHeight(60);
Register_Label->setMinimumWidth(280);
Register_Label->setAlignment(Qt::AlignCenter);
Register_Label->setStyleSheet("QLabel {background-color:rgba(0, 0, 0, 0);\
color:white;}\
QLabel:hover {background-color:rgba(0, 0, 0, 0);\
color:rgba(16, 190, 221, 0.4);}");
Register_Label->setCursor(QCursor(Qt::PointingHandCursor));
// Style Settings for Labels
for(auto i = 0; i < Labels.length(); i++){
Labels[i]->setStyleSheet("QLabel {background-color:rgba(0, 0, 0, 0);\
color:white;}");
Labels[i]->setMinimumHeight(32);
Labels[i]->setText(LineNames[i]);
Labels[i]->setFont(QFont("Timers", 16, QFont::Bold));
}
// Style Settings for LineEdits
for(auto i = 0; i < LineEdits.length(); i++){
QString Line_Style = "QLineEdit {background-image: url(";
Line_Style.append(LineImgs[i]);
Line_Style.append(");background-repeat: no-repeat;background-position: left;padding: 2 10 2 30;");
Line_Style.append("border-radius:15px;\
border-width: 1px;\
border-style: solid;\
border-color: black;}");
LineEdits[i]->setStyleSheet(Line_Style);
LineEdits[i]->setFont(QFont("Timers", 16, QFont::Fantasy));
LineEdits[i]->setMinimumHeight(32);
// Initialize the text
LineEdits[i]->setPlaceholderText(LineHints[i]);
// Limitation of the Input Length
LineEdits[i]->setMaxLength(20);
}
// Style Settings for QPushButton
Sign_Button->setStyleSheet("QPushButton {background-color:rgba(255, 255, 255, 1);\
border-radius:15px;\
border-width: 1px;\
border-style: solid;\
border-color: black;}\
QPushButton:hover {background-color:rgba(16, 190, 221, 0.4);\
border-radius:15px;\
border-width: 1px;\
border-style: solid;\
border-color: black;}");
Sign_Button->setCursor(QCursor(Qt::PointingHandCursor));
Sign_Button->setText("Log in");
Sign_Button->setMinimumWidth(80);
Sign_Button->setMinimumHeight(32);
Sign_Button->setFont(QFont("Timers", 14, QFont::Bold));
// Password Seen Label
Password_Seen_Label->setFixedSize(32, 32);
Set_Image_On_Label(Password_Seen_Label, "img/Unseen.png");
}
现在,效果如下:
1.6.1 新属性介绍
background-repeat
当 背景图片小于控件尺寸 时指定 背景图片的重复方式 以填满控件
属性值 | 含义 |
---|---|
repeat-x | 将图片水平方向铺满 比如一张 10*10 的图片会在 100*10 的控件上从左到右重复10次 |
repeat-y | 将图片垂直方向铺满 比如一张 10*10 的图片会在 10*100 的控件上自上而下重复10次 |
repeat | 将图片在水平和垂直方向铺满 比如一张 10*10 的图片会在 100*100 的控件上重复100次 |
no-repeat | 不对图片进行重复填充 |
background-position
当背景图片在控件上排布的 对齐方式 属性值可以通过空格隔开来实现组合复用
属性值 | 含义 |
---|---|
top | 图片 向上对齐 |
bottom | 图片 向下对齐 |
center | 图片 居中对齐 |
left | 图片 居左对齐 |
right | 图片 居左对齐 |
padding
指定 输入行上文字的填充大小,可以理解为将 控件 上所有文字按照这一属性 进行移动
属性值为四个由空格隔开的数字,分别代表:
属性值 | 含义 |
---|---|
第一个数字 | 文字 上方填充大小 相当于向下移动 |
第二个数字 | 文字 右侧填充大小 相当于向左移动 |
第三个数字 | 文字 下方填充大小 相当于向上移动 |
第四个数字 | 文字 左侧填充大小 相当于向右移动 |
1.6.2 让布局灵活对齐的”空白组件“
可以看到由于在水平布局中单个控件会铺满整行,上述的布局中大小便显得参差不齐,那么如何让单个控件不铺满一行呢?聪明的你可能想到了利用QLabel来占坑然后设置成不可见就好了。
但事实上还有更简便的做法 —— QSpacerItem
,可以理解为这是在布局中封装的一个特殊控件,他的唯一作用就是占坑,使用方法如下(不需要额外引入头文件):
Specific_Layout->addItem(new QSpacerItem(width, height));
Specific_Layout
需要添加占位控件的布局指针
width
需要占据的宽度
height
需要占据的高度
综上,回到之前写好的 Layout_Init()
函数中,添加一些占位控件如下:
void SignWindow::Layout_Init(void){
// Horizontal Layouts
QVector<QHBoxLayout*> HLayouts;
for(auto i = 0; i < 5; i++){
HLayouts.append(new QHBoxLayout());
}
// First Row
HLayouts[0]->addWidget(User_Label);
HLayouts[0]->addWidget(User_Line);
HLayouts[0]->addItem(new QSpacerItem(38, 32));
// Margin
HLayouts[1]->addItem(new QSpacerItem(32, 32));
// Second
HLayouts[2]->addWidget(Password_Label);
HLayouts[2]->addItem(new QSpacerItem(24, 32));
HLayouts[2]->addWidget(Password_Line);
HLayouts[2]->addWidget(Password_Seen_Label);
// Third
HLayouts[3]->addItem(new QSpacerItem(130, 60));
HLayouts[3]->addWidget(Sign_Button);
HLayouts[3]->addItem(new QSpacerItem(130, 60));
// Fourth
HLayouts[4]->addWidget(Register_Label);
// Add to Vertical Layout
for(auto i = 0; i < 5; i++){
HLayouts[i]->setAlignment(Qt::AlignCenter);
VLayout->addLayout(HLayouts[i]);
}
// Set to AreaLabel
VLayout->setAlignment(Qt::AlignCenter);
Area_Label->setLayout(VLayout);
}
加入了这些留白后,现在的页面便如下所示:
1.7 Qt 阴影特效实现错误红框提示
在点击登录按钮之后,如果输入的信息不合规范或者压根没有信息输入时,我们希望实现让输入框有一圈红色特效来醒目的提示,这个功能要如何实现呢?先来看效果图:
这一特效是通过 Qt 一个特殊的 阴影特效类 实现的。
1.7.1 头文件的引入
#include <QGraphicsDropShadowEffect>
1.7.2 详细指南参考链接
详情使用方法和参数说明详见 QGraphicsDropShadowEffect详解
1.7.3 红色警报阴影和无阴影
下面我们通过两个函数来得到红色阴影和没有阴影特效的 Qt 阴影特效实例,由于这个功能许多窗口都可能需要,因此我们写入 utils.cpp
中:
- 红色警告阴影
QGraphicsDropShadowEffect* red_shadow(QWidget* parent){
QGraphicsDropShadowEffect* shadow_effect = new QGraphicsDropShadowEffect(parent);
// Offset Settings
shadow_effect->setOffset(0, 0);
// Set the color of the shadow
shadow_effect->setColor(Qt::red);
// radius of the shadow(The larger it is, the more vague it will be)
shadow_effect->setBlurRadius(20);
return shadow_effect;
}
- 没有阴影
QGraphicsDropShadowEffect* none_shadow(QWidget* parent){
QGraphicsDropShadowEffect* shadow_effect = new QGraphicsDropShadowEffect(parent);
// Offset Settings
shadow_effect->setOffset(0, 0);
// Set the color of the shadow
shadow_effect->setColor(Qt::red);
// radius of the shadow(The larger it is, the more vague it will be)
shadow_effect->setBlurRadius(0);
return shadow_effect;
}
1.7.4 输入为空警告+再输入清除
这一步的实现想必对你来说已经是小菜一碟了。你很简明扼要地发现只需要两个内置信号和两个槽函数即可实现:
- 判断输入内容
- 输入内容时清空阴影
很好,于是我们得到如下两个槽函数:
void SignWindow::Input_Check(void){
// There is a blank
if (User_Line->text()==""||Password_Line->text()==""){
User_Line->setGraphicsEffect(red_shadow(this));
Password_Line->setGraphicsEffect(red_shadow(this));
QMessageBox::warning(this, "输入警告", "账号或密码不能为空");
}
}
void SignWindow::Clear_Shadows(void){
User_Line->setGraphicsEffect(none_shadow(this));
Password_Line->setGraphicsEffect(none_shadow(this));
}
接下来我们进行和信号的绑定,显然输入检验是在 按下按钮后需要进行的,因此将其与按钮点击信号进行连接如下:
connect(Sign_Button,
&QPushButton::clicked,
this,
&SignWindow::Input_Check);
接下来,是当任何一个输入框内容改变时则清空阴影特效,因此:
connect(User_Line,
&QLineEdit::textChanged,
this,
&SignWindow::Clear_Shadows);
connect(Password_Line,
&QLineEdit::textChanged,
this,
&SignWindow::Clear_Shadows);
此时效果如下:
1.8 登录检验
接下来让我们进一步完善上述检验输入内容的代码,从而实现登录检验功能。
首先,如果用户名和密码的输入均不为空,我们就可以接入数据库进行对比了。但是,数据库建立在了主窗口中,因此我们不能直接在登录窗口判断,那怎么办呢,很简单,需要什么控件帮忙,给他发信号即可。因此我们在登录窗口类中定义如下信号传递输入的用户名和密码信息:
void Input_Info(QVector<QString>);
可以看到这里我用了一个字符串容器来进行参数传递,接着我们在输入检验槽函数中发射他:
void SignWindow::Input_Check(void){
// There is a blank
if (User_Line->text()==""||Password_Line->text()==""){
User_Line->setGraphicsEffect(red_shadow(this));
Password_Line->setGraphicsEffect(red_shadow(this));
QMessageBox::warning(this, "输入警告", "账号或密码不能为空");
}
else{
emit Input_Info({User_Line->text(),
Password_Line->text()});
}
}
接着为了方便设置登录窗口的错误提示,我们添加一个公有成员函数用于将两个输入框设置红色阴影:
void SignWindow::Wrong_Warning(void){
User_Line->setGraphicsEffect(red_shadow(this));
Password_Line->setGraphicsEffect(red_shadow(this));
}
现在让我们进入主窗口的代码部分,首先为主窗口添加一个响应登录信息的槽函数 Check_Account(QVector<QString>)
,结合上面的 SQL 知识进行检验如下:
void MainWindow::Check_Account(QVector<QString> info){
// Try to find the user_name
QString name = info[0];
QString password = info[1];
QString result = Get_Password(name);
// User Do not Exist
if(result.length()==0 || password!=result){
QMessageBox::critical(SignPage, "错误", "账号或密码错误!");
SignPage->Wrong_Warning();
}
else{
// Update Info
Sign = true;
user_name = name;
user_data.append(result);
if(QMessageBox::information(SignPage, "登录成功", "恭喜你登陆成功!")){
SignPage->close();
}
}
}
由于这是当登录窗口打开才可能触发的槽函数,因此我们在登录窗口的打开槽函数中进行如下连接:
connect(SignPage,
&SignWindow::Input_Info,
this,
&MainWindow::Check_Account);
此时效果演示如下:
1.9 密码的显示与不显示切换
这里主要通过直接调用 QLineEdit 的 setEchoMode
方法来修改显示模式。其显示模式为 **QLineEdit::EchoMode
**这一枚举量 ,共有以下四种:
枚举名称 | 用途说明 |
---|---|
QLineEdit::Normal | 正常显示,输入什么便显示什么 |
QLineEdit::NoEcho | 无论怎么输入什么也不显示 适用于连密码长度都需要保密的场景 |
QLineEdit::Password | 输入的字符会以 “●” 的样式显示 |
QLineEdit::PasswordEchoOnEdit | 输入时当前字符正常显示,随后以 “●” 的样式显示 |
综上,我们的需求即是,点击小眼睛时候将显示模式在 QLineEdit::PasswordEchoOnEdit
与 QLineEdit::Normal
之间切换。这可以轻松的通过如下的槽函数实现(记得将小眼睛的标签类型换成我们自定义的可点击标签):
void SignWindow::Switch_Password_Input_Mode(void){
if(Password_Line->echoMode()==QLineEdit::PasswordEchoOnEdit){
Password_Line->setEchoMode(QLineEdit::Normal);
Set_Image_On_Label(Password_Seen_Label, "img/Seen.png");
}
else{
Password_Line->setEchoMode(QLineEdit::PasswordEchoOnEdit);
Set_Image_On_Label(Password_Seen_Label, "img/Unseen.png");
}
}
接着在初始化小眼睛标签后如下进行槽函数连接:
connect(Password_Seen_Label,
&CLabel::clicked,
this,
&SignWindow::Switch_Password_Input_Mode);
此时效果如下:
2. 杂谈
2.1 局部布局指针可能造成的野指针错误
在进行布局时,如果在某个成员函数中单独构造一个 单独的布局指针,例如:
QVBoxLayout* VLayout = new QVBoxLayout();
这是一种不稳定的写法,有概率造成窗口因为野指针而崩溃。因此推荐采用本文的写法,也即 将这种整体布局的布局控件单独设置成当前窗口的私有成员
2.2 关于 QtCreator 提升开发效率的两个快捷键
F4
在 头/源文件 中快速切换
Shift+F2
选中函数/类名 在 定义/实现 中快速切换