目录
[一.账号+密码 登录注册界面设计(loginwidget.cpp)](#一.账号+密码 登录注册界面设计(loginwidget.cpp))
二.邮箱注册登录界面设计(emailloginwidget.cpp))
[2.2.与 账号+密码 登录界面的切换](#2.2.与 账号+密码 登录界面的切换)
一.账号+密码 登录注册界面设计(loginwidget.cpp)
我们设计界面都是需要有原型图的,那么我们的原型图就如下面所示


我们发现它是有双模态的。也就是注册和登录两种状态。
首先我们就需要先将这个界面搭建出来吧
cpp
// 构造函数,初始化登录/注册窗口
LoginWidget::LoginWidget(QWidget *parent)
: QWidget{parent}
{
// 1. 设置本窗口的基本属性
this->setFixedSize(400, 350); // 固定窗口大小
this->setWindowTitle("登录"); // 窗口标题
this->setWindowIcon(QIcon(":/resource/image/logo.png")); // 窗口图标
this->setStyleSheet("QWidget { background-color: rgb(255, 255, 255); }"); // 白色背景
this->setAttribute(Qt::WA_DeleteOnClose); // 关闭时自动销毁窗口对象
// 2. 创建布局管理器(网格布局)
QGridLayout* layout = new QGridLayout();
layout->setSpacing(0); // 控件间距为0
layout->setContentsMargins(50, 0, 50, 0); // 左右边距50,上下边距0
this->setLayout(layout);
// 3. 创建标题标签
titleLabel = new QLabel();
titleLabel->setText("登录");
titleLabel->setAlignment(Qt::AlignCenter); // 居中显示
titleLabel->setFixedHeight(50); // 固定高度
titleLabel->setStyleSheet("QLabel { font-size: 40px; font-weight: 600; }"); // 字体样式
// 4. 创建用户名输入框
QString editStyle = "QLineEdit { border: none; border-radius: 10px; font-size: 20px; background-color: rgb(240, 240, 240); padding-left:5px; }";
usernameEdit = new QLineEdit();
usernameEdit->setFixedHeight(40);
usernameEdit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); // 水平伸展,垂直固定
usernameEdit->setPlaceholderText("输入用户名");
usernameEdit->setStyleSheet(editStyle);
// 5. 创建密码输入框
passwordEdit = new QLineEdit();
passwordEdit->setFixedHeight(40);
passwordEdit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
passwordEdit->setPlaceholderText("输入密码");
passwordEdit->setStyleSheet(editStyle);
passwordEdit->setEchoMode(QLineEdit::Password); // 密码模式(显示圆点)
// 6. 创建验证码输入框
verifyCodeEdit = new QLineEdit();
verifyCodeEdit->setFixedHeight(40);
verifyCodeEdit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
verifyCodeEdit->setPlaceholderText("输入验证码");
verifyCodeEdit->setStyleSheet(editStyle);
// 7. 创建显示验证码图片的控件
// 使用自定义的 VerifyCodeWidget 来生成和显示验证码图片
verifyCodeWidget = new VerifyCodeWidget(this);
// 8. 创建登录按钮
submitBtn = new QPushButton();
submitBtn->setText("登录");
submitBtn->setFixedHeight(40);
submitBtn->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
QString btnGreenStyle = "QPushButton { border: none; border-radius: 10px; background-color: rgb(44, 182, 61); color: rgb(255, 255, 255); }";
btnGreenStyle += "QPushButton:pressed { background-color: rgb(240, 240, 240); }"; // 按下时颜色变浅
submitBtn->setStyleSheet(btnGreenStyle);
// 9. 创建切换到邮箱号登录按钮
emailModeBtn = new QPushButton();
emailModeBtn->setFixedSize(100, 40);
emailModeBtn->setText("邮箱号登录");
QString btnWhiteStyle = "QPushButton { border: none; border-radius: 10px; background-color: transparent; }";
btnWhiteStyle += "QPushButton:pressed { background-color: rgb(240, 240, 240); }";
emailModeBtn->setStyleSheet(btnWhiteStyle);
// 10. 创建切换模式(登录和注册)按钮
switchModeBtn = new QPushButton();
switchModeBtn->setFixedSize(100, 40);
switchModeBtn->setText("注册");
switchModeBtn->setStyleSheet(btnWhiteStyle);
// 11. 将所有控件添加到网格布局中
// 参数:控件, 行, 列, 行跨度, 列跨度
layout->addWidget(titleLabel, 0, 0, 1, 5); // 标题占5列
layout->addWidget(usernameEdit, 1, 0, 1, 5); // 用户名输入框占5列
layout->addWidget(passwordEdit, 2, 0, 1, 5); // 密码输入框占5列
layout->addWidget(verifyCodeEdit, 3, 0, 1, 4); // 验证码输入框占前4列
layout->addWidget(verifyCodeWidget, 3, 4, 1, 1); // 验证码图片占第5列
layout->addWidget(submitBtn, 4, 0, 1, 5); // 登录/注册按钮占5列
layout->addWidget(emailModeBtn, 5, 0, 1, 1); // 邮箱登录按钮放在左下
layout->addWidget(switchModeBtn, 5, 4, 1, 1); // 切换模式按钮放在右下
}
这样子我们就搭建出来了,但是还有一些问题需要我们来进行解决。

这3个都是QPushButton,都是按钮,那么按下这些按键会有啥效果呢?
我们这个时候就需要给这些按钮设置槽函数了。
1.1.验证码模块
我们仔细看一下上面这段代码,我们发现验证码模块是我们自己写的。
那么我们现在就来写一下吧。
首先我们的验证码都是四个随机的大写英文字母,因此首先我们就需要先将这4个英文字母给写出来。
cpp
QString VerifyCodeWidget::generateVerifyCode()
{
QString code;
// 循环生成4个字符
for (int i = 0; i < 4; ++i) {
// 每次循环生成一个字符,从 'A' 开始
int init = 'A';
// 随机偏移 0-25,生成 'A' 到 'Z'
init += randomGenerator.generate() % 26;
code += static_cast<QChar>(init);
}
return code;
}
我们这里是使用绘图功能来画出这个验证码的,具体情况如下,那么这里其实是使用到了Qt绘图的paintEvent事件.
一个关键的注意事项是:与绘制相关的操作通常不建议放在 QWidget 的构造函数中执行。
这是因为在构造阶段,控件尚未完成初始化和显示,此时进行绘制是无效的。
Qt 为此专门提供了一个 paintEvent 事件处理函数,我们应在此函数中执行所有绘制逻辑。
与之对应的是 QPaintEvent 事件,它在以下几种典型情况下会被触发,从而引起重绘:
-
控件首次创建完成并显示时
比如在
QWidget上绘制内容,必须在控件构造完成并显示之后才会生效。paintEvent会在首次显示时被调用,确保绘制内容正确呈现。 -
控件被遮挡后重新显露时
如果控件之前被其他窗口或元素遮挡,当遮挡物移开时,系统会触发重绘,以保证之前被遮挡部分的内容能够正确恢复显示。
-
窗口从最小化状态还原时
窗口最小化后再恢复显示,其内容需要重新绘制,此时也会触发
paintEvent。 -
控件大小发生变化时
当控件尺寸改变(如用户拖拽调整窗口大小),通常需要重新绘制以适应新的尺寸,此时也会自动调用
paintEvent。 -
主动在代码中调用 repaint() 或 update() 时
我们可以在代码中主动调用
repaint()(立即重绘)或update()(异步调度重绘)来触发paintEvent,从而实现手动刷新界面。
因此,将绘制代码统一放置在 paintEvent 中,能够确保界面在各种情况下都能正确、及时地更新显示内容,避免出现内容丢失或显示异常的问题。
两位"重绘助手":repaint() 和 update()
它们都是 QWidget 的成员函数,作用都是最终触发 paintEvent,但脾气完全不同:
- repaint() - "急性子"
行为:它一被调用,立即、强制地产生一个绘制事件,并且会阻塞当前正在执行的代码,直到 paintEvent 函数执行完毕,画面画好了,它才返回。
优点:响应快,立竿见影。
缺点:因为会阻塞,如果重绘很复杂,可能会让界面卡顿一下。频繁调用它效率很低。
什么时候用:很少用。除非在某些对实时性要求极高、不能有丝毫延迟的场合(比如动画)。
- update() - "聪明人"(最常用!)
行为:它一被调用,并不是立刻去画,而是给 Qt 系统"发个消息":"喂,我这儿需要重画一下,你方便的时候处理一下。" 然后它就立刻返回了,不会阻塞你的代码。
优点:Qt 会把多个连续的 update() 请求合并成一次 paintEvent 调用,效率非常高,避免了不必要的重复绘制,不会造成界面卡顿。
缺点:有极短的延迟,不是立刻执行。
什么时候用:几乎任何时候你需要主动触发重绘,都应该用它。比如你改变了一个数据(比如进度值),然后调用 update(),请求界面更新来反映这个新数据。
那么我们就知道刷新验证码的代码怎么写了
cpp
// 刷新验证码(重新生成并重绘)
void VerifyCodeWidget::refreshVerifyCode()
{
// 重新生成验证码
verifyCode = generateVerifyCode();
// 通过 update 就可以起到 "刷新界面", 本身就会触发 paintEvent 进行重绘
this->update();
}
那么我们现在就来编写paintEvent 事件处理函数,在这里我们将完成验证码的绘画
cpp
// 绘制事件,负责绘制验证码图片(包括噪点、干扰线和验证码字符)
void VerifyCodeWidget::paintEvent(QPaintEvent *event)
{
(void) event; // 避免未使用参数的编译警告
const int width = 180; // 控件宽度
const int height = 80; // 控件高度
QPainter painter(this); // 创建绘图对象
QPen pen; // 画笔
QFont font("楷体", 25, QFont::Bold, true); // 设置字体:楷体,25号,粗体,斜体
painter.setFont(font);
// 画点: 添加随机噪点,使验证码难以被机器识别
for(int i = 0; i < 100; i++)
{
// 随机生成颜色
pen = QPen(QColor(randomGenerator.generate() % 256,
randomGenerator.generate() % 256,
randomGenerator.generate() % 256));
painter.setPen(pen);
// 在随机位置画点
painter.drawPoint(randomGenerator.generate() % width,
randomGenerator.generate() % height);
}
// 画线: 添加随机干扰线,进一步增加识别难度
for(int i = 0; i < 5; i++)
{
// 随机生成颜色
pen = QPen(QColor(randomGenerator.generate() % 256,
randomGenerator.generate() % 256,
randomGenerator.generate() % 256));
painter.setPen(pen);
// 绘制随机直线
painter.drawLine(randomGenerator.generate() % width,
randomGenerator.generate() % height,
randomGenerator.generate() % width,
randomGenerator.generate() % height);
}
// 绘制验证码字符(4个大写字母)
for(int i = 0; i < verifyCode.size(); i++)
{
// 每个字符使用随机颜色
pen = QPen(QColor(randomGenerator.generate() % 255,
randomGenerator.generate() % 255,
randomGenerator.generate() % 255));
painter.setPen(pen);
// 绘制单个字符,位置水平方向间隔20像素,垂直方向随机偏移0-9像素
painter.drawText(5 + 20 * i, randomGenerator.generate() % 10,
30, 30, Qt::AlignCenter, QString(verifyCode[i]));
}
}
此外,我们点击这个验证码还需要实现刷新验证码的功能,这个时候我们就会用到鼠标的按下事件,我们重写一下鼠标按下事件处理函数
cpp
// 鼠标点击事件,点击验证码图片时刷新验证码
void VerifyCodeWidget::mousePressEvent(QMouseEvent *event)
{
(void) event; // 避免未使用参数的编译警告
// 用户点击验证码区域,刷新验证码
this->refreshVerifyCode();
}
此外,我们还需要验证用户生成的验证码和我们真实的验证码是否相同,我们就需要下面这个函数来进行处理。
cpp
// 检查用户输入的验证码是否正确
// verifyCode: 用户输入的验证码字符串
// 返回值: true表示正确,false表示错误(忽略大小写比较)
bool VerifyCodeWidget::checkVerifyCode(const QString &verifyCode)
{
// 此处比较验证码的时候, 需要忽略大小写.
return this->verifyCode.compare(verifyCode, Qt::CaseInsensitive) == 0;
}
1.2.登录/注册双模态的转换
我们先看一个按钮来

那么我们将它反过来呢?

可以看到,这个就是可以实现一个双模态的转换。因此,我们给这个按钮配置一个槽函数
cpp
connect(switchModeBtn, &QPushButton::clicked, this, &LoginWidget::switchMode);
那么这个槽函数是怎么实现的?
这个就涉及到来注册和登录模式的一种切换了,这个是借助于一个标志位来进行实现的。
cpp
bool isLoginMode=true; // true = 登录模式,false = 注册模式
- 初始值:我们在头文件中初始化为 true(登录模式),因为窗口标题一开始就是"登录",按钮文本也是"登录"。
- 作用:记录当前界面处于登录还是注册状态,后续所有行为都依赖这个标志。
我们来看看
cpp
void LoginWidget::switchMode()
{
if (isLoginMode) {
// 当前是登录模式 → 切换到注册模式
this->setWindowTitle("注册");
titleLabel->setText("注册");
submitBtn->setText("注册");
emailModeBtn->setText("邮箱号注册");
switchModeBtn->setText("登录");
} else {
// 当前是注册模式 → 切换到登录模式
this->setWindowTitle("登录");
titleLabel->setText("登录");
submitBtn->setText("登录");
emailModeBtn->setText("邮箱号登录");
switchModeBtn->setText("注册");
}
isLoginMode = !isLoginMode; // 取反,完成状态切换
}
好像就是一个简单的显示文本的替换而已。
1.3.与邮箱注册/登录界面的切换
我们接下来看看这个


我们发现,无论在注册和登录界面,都是跳转到邮箱号登录注册界面。
这个其实很容易进行实现
至于这个登录按钮,按下去后,客户端会向服务器发送登录请求,如果登录成功,就会切换到主界面。
那么我们现在就能来配置这个槽函数了。
cpp
connect(emailModeBtn, &QPushButton::clicked, this, [=]() {
// 此处还可以把 isLoginMode 这个值传到新的窗口中, 让新的窗口决定自己是登录状态还是注册状态. 大家自行尝试实现.
EmailLoginWidget* emailLoginWidget = new EmailLoginWidget(nullptr);
emailLoginWidget->show();
// 关闭当前窗口
this->close();
});
这个就是比较清晰了吧。
1.4.注册/登录按钮
接下来是一个重头戏。

这两个的槽函数我们需要怎么进行设计?
我们仔细想想就能知道,我们注册和登录给服务器发送的请求不一样。但是它们又是同一个按钮,只能配置一个槽函数。所以我们就需要根据这个注册/登录状态标志位来做出不同的处理。
cpp
// 处理登录/注册按钮的点击事件
void LoginWidget::clickSubmitBtn()
{
// 1. 先从输入框拿到必要的内容
const QString& username = usernameEdit->text();
const QString& password = passwordEdit->text();
const QString& verifyCode = verifyCodeEdit->text();
if (username.isEmpty() || password.isEmpty() || verifyCode.isEmpty()) {
Toast::showMessage("用户名/密码/验证码不能为空!"); // 显示提示信息
return;
}
// 2. 对比验证码是否正确
if (!verifyCodeWidget->checkVerifyCode(verifyCode)) {
Toast::showMessage("验证码不正确!");
return;
}
// 3. 真正去发送网络请求.
DataCenter* dataCenter = DataCenter::getInstance(); // 获取数据中心单例
//根据这个注册/登录状态标志位来做出不同的处理
if (isLoginMode)
{
// 登录模式:连接登录完成信号,并异步发起登录请求
connect(dataCenter, &DataCenter::userLoginDone, this, &LoginWidget::userLoginDone);
//当服务器的响应回来,并且我们获取了里面的数据并更新到数据中心之后,我们设计的是会发送一个DataCenter::userLoginDone信号
dataCenter->userLoginAsync(username, password);//利用这个接口去向服务器发起登录请求,这个是异步接口
}
else
{
// 注册模式:连接注册完成信号,并异步发起注册请求
connect(dataCenter, &DataCenter::userRegisterDone, this, &LoginWidget::userRegisterDone);
//当服务器的响应回来,并且我们获取了里面的数据并更新到数据中心之后,我们设计的是会发送一个DataCenter::userRegisterDone信号
dataCenter->userRegisterAsync(username, password);//利用这个接口去向服务器发起注册请求,这个是异步接口
}
}
那么针对登录成功和注册成功,我们又分别设置了槽函数,大家自己去看看即可。
cpp
// 登录完成的槽函数
void LoginWidget::userLoginDone(bool ok, const QString& reason)
{
// 登录失败,显示失败原因
if (!ok) {
Toast::showMessage("登录失败! " + reason);
return;
}
// 登录成功,跳转到主界面
MainWidget* mainWidget = MainWidget::getInstance();//主界面我们后面再说
mainWidget->show();
this->close(); // 关闭当前登录窗口
}
// 注册完成的槽函数
void LoginWidget::userRegisterDone(bool ok, const QString &reason)
{
if (!ok) {
Toast::showMessage("注册失败! " + reason);
return;
}
Toast::showMessage("注册成功!");
// 切换到登录界面
this->switchMode();
// 输入框清空一下.
// 主要是要清空用户名和密码, 验证码输入框的内容的.
// 但是此处, 只清空一下验证码. 用户名密码这里的情况大概率还是同样的内容.
verifyCodeEdit->clear();
// 更新验证码图片
verifyCodeWidget->refreshVerifyCode();
}
我觉得思路就很简单!!
- 登录成功,我们就进入主界面
- 注册成功,我们就进入登录界面
这个设计思想还是很清晰的。
二.邮箱注册登录界面设计(emailloginwidget.cpp)
这个就有点像上面那个了。


这个其实跟上面那个差不多。也是登录/注册双模态。
那么我们现在就先把这个界面给写出来
cpp
// 构造函数,初始化邮箱登录/注册窗口
EmailLoginWidget::EmailLoginWidget(QWidget *parent)
: QWidget{parent}
{
// 1. 设置窗口的基本属性
this->setFixedSize(400, 350); // 固定窗口大小
this->setWindowTitle("登录"); // 窗口标题
this->setWindowIcon(QIcon(":/resource/image/logo.png")); // 窗口图标
this->setStyleSheet("QWidget { background-color: rgb(255, 255, 255); }"); // 白色背景
this->setAttribute(Qt::WA_DeleteOnClose); // 关闭时自动销毁窗口对象
// 2. 创建核心布局管理器(网格布局)
QGridLayout* layout = new QGridLayout();
layout->setSpacing(10); // 控件间距10像素
layout->setContentsMargins(50, 0, 50, 0); // 左右边距50,上下边距0
this->setLayout(layout);
// 3. 创建标题标签
titleLabel = new QLabel();
titleLabel->setText("登录");
titleLabel->setFixedHeight(50);
titleLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); // 水平拉伸,垂直固定
titleLabel->setStyleSheet("QLabel { font-size: 40px; font-weight: 600; }");
titleLabel->setAlignment(Qt::AlignCenter);
// 4. 创建邮箱号输入框
QString editStyle = "QLineEdit { border: none; background-color: rgb(240, 240, 240); font-size: 20px; border-radius: 10px; padding-left: 5px;}";
emailEdit = new QLineEdit();
emailEdit->setPlaceholderText("输入邮箱号");
emailEdit->setFixedHeight(40);
emailEdit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
emailEdit->setStyleSheet(editStyle);
// 5. 创建验证码输入框
verifyCodeEdit = new QLineEdit();
verifyCodeEdit->setPlaceholderText("输入邮箱验证码");
verifyCodeEdit->setFixedHeight(40);
verifyCodeEdit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
verifyCodeEdit->setStyleSheet(editStyle);
// 6. 创建发送验证码按钮
QString btnWhiteStyle = "QPushButton { border: none; border-radius: 10px; background-color: transparent; }";
btnWhiteStyle += "QPushButton:pressed { background-color: rgb(240, 240, 240); }";
sendVerifyCodeBtn = new QPushButton();
sendVerifyCodeBtn->setFixedSize(100, 40);
sendVerifyCodeBtn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
sendVerifyCodeBtn->setText("发送验证码");
sendVerifyCodeBtn->setStyleSheet(btnWhiteStyle);
// 7. 创建提交按钮(登录/注册)
submitBtn = new QPushButton();
submitBtn->setFixedHeight(40);
submitBtn->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
submitBtn->setText("登录");
QString btnGreenStyle = "QPushButton { border: none; border-radius: 10px; background-color: rgb(44, 182, 61); color: rgb(255, 255, 255); }";
btnGreenStyle += "QPushButton:pressed { background-color: rgb(240, 240, 240); }";
submitBtn->setStyleSheet(btnGreenStyle);
// 8. 创建 "切换到用户名" 模式按钮
QPushButton* userModeBtn = new QPushButton();
userModeBtn->setFixedSize(100, 40);
userModeBtn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
userModeBtn->setText("切换到用户名");
userModeBtn->setStyleSheet(btnWhiteStyle);
// 9. 切换登录/注册模式的按钮
switchModeBtn = new QPushButton();
switchModeBtn->setFixedSize(100, 40);
switchModeBtn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
switchModeBtn->setText("注册");
switchModeBtn->setStyleSheet(btnWhiteStyle);
// 10. 将所有控件添加到布局管理器中
layout->addWidget(titleLabel, 0, 0, 1, 5);
layout->addWidget(emailEdit, 1, 0, 1, 5);
layout->addWidget(verifyCodeEdit, 2, 0, 1, 4);
layout->addWidget(sendVerifyCodeBtn, 2, 4, 1, 1);
layout->addWidget(submitBtn, 3, 0, 1, 5);
layout->addWidget(userModeBtn, 4, 0, 1, 1);
layout->addWidget(switchModeBtn, 4, 4, 1, 1);
......
}
在我们这里,有4个按钮。对于每一个按钮,我们都要去设置槽函数。

那么我们一个一个来说明。
2.1.登录/注册双模态的转换


我们这里跟上面一样,还是借助了这个标志位来判断当前是登录还是注册模态。
cpp
bool isLoginMode = true; // 当前是否为登录模式(true=登录,false=注册)
默认情况下就是登录状态。
那么这个按钮的槽函数就很好安排了,
cpp
// 切换登录/注册模式按钮
connect(switchModeBtn, &QPushButton::clicked, this, &EmailLoginWidget::switchMode);
可以说跟上面的是一模一样的
cpp
// 切换登录模式和注册模式的界面及状态
void EmailLoginWidget::switchMode()
{
if (isLoginMode) {
// 当前是登录模式,切换到注册模式
this->setWindowTitle("注册");
titleLabel->setText("注册");
submitBtn->setText("注册");
switchModeBtn->setText("登录");
} else {
// 当前是注册模式,切换到登录模式
this->setWindowTitle("登录");
titleLabel->setText("登录");
submitBtn->setText("登录");
switchModeBtn->setText("注册");
}
isLoginMode = !isLoginMode; // 反转模式标志
}
2.2.与 账号+密码 登录界面的切换

这个其实就很简单,没有别的啥技巧
cpp
// 切换到用户名登录窗口,并关闭当前窗口
connect(userModeBtn, &QPushButton::clicked, this, [=]() {
LoginWidget* loginWidget = new LoginWidget(nullptr);
loginWidget->show();
this->close();
});
2.3.获取验证码
接下来就来到一个重头戏了。

我们先自己想想,按下这个按钮,应该发生什么?
首先,最重要的肯定是
往服务端发起发送验证码请求,然后客户端再
这里我们就需要来完完整整的讲清楚这整个流程了。
cpp
// 发送验证码按钮
connect(sendVerifyCodeBtn, &QPushButton::clicked, this, &EmailLoginWidget::sendVerifyCode);
我们设定的槽函数如下:
cpp
// 发送验证码的槽函数
void EmailLoginWidget::sendVerifyCode()
{
// 1. 获取到邮箱号码
const QString& email = this->emailEdit->text();
if (email.isEmpty()) {
return; // 邮箱为空则不发送
}
this->currentEmail = email; // 保存当前邮箱,防止用户后续修改
// 2. 发送网络请求, 获取验证码
DataCenter* dataCenter = DataCenter::getInstance();
// Qt::UniqueConnection 避免重复连接
connect(dataCenter, &DataCenter::getVerifyCodeDone, this, &EmailLoginWidget::sendVerifyCodeDone, Qt::UniqueConnection);
dataCenter->getVerifyCodeAsync(email);
// 3. 开启定时器, 开始倒计时(每秒触发一次)
timer->start(1000);
}
我们在客户端收到服务器的响应,并更新完数据之后,会发出一个信号,我们给这个信号配置一个槽函数,槽函数非常简单,就是弹出一个弹窗,打印下面这些内容。
cpp
// 验证码发送完成后的处理(网络请求成功返回)
void EmailLoginWidget::sendVerifyCodeDone()
{
Toast::showMessage("验证码已经发送");
}
这里其实涉及到一个全局通知设计(Toast.cpp)
这里的Toast::showMessage其实是我们自己写的一个函数,这个就是专门用于全局通知的
我们就设计了一个全局通知,非常简单的一个弹窗
cpp#ifndef TOAST_H #define TOAST_H #include <QDialog> #include <QWidget> class Toast : public QDialog { Q_OBJECT public: // 此处不需要指定父窗口. 全局通知的父窗口就是 桌面. Toast(const QString& text); // 并不需要手动来 new 这个对象, 而是通过 showMessage 来弹出窗口 static void showMessage(const QString& text); }; #endif // TOAST_H我们看看具体的实现
cpp#include "toast.h" #include <QApplication> #include <QScreen> #include <QVBoxLayout> #include <QLabel> #include <QTimer> Toast::Toast(const QString &text) { // 1. 设置窗口的基本参数 this->setFixedSize(800, 150); this->setWindowTitle("消息通知"); this->setWindowIcon(QIcon(":/resource/image/logo.png")); this->setAttribute(Qt::WA_DeleteOnClose); this->setStyleSheet("QDialog { background-color: rgb(255, 255, 255); }"); // 去掉窗口的标题栏 this->setWindowFlags(Qt::FramelessWindowHint); // 2. 先考虑一下窗口的位置. // 获取到整个屏幕的尺寸, 通过 primaryScreen 来获取. QScreen* screen = QApplication::primaryScreen(); int width = screen->size().width(); int height = screen->size().height(); int x = (width - this->width()) / 2; int y = height - this->height() - 100; // 此处的 100 是窗口底边距离屏幕底边的间隔 this->move(x, y); // 3. 添加一个布局管理器 QVBoxLayout* layout = new QVBoxLayout(); layout->setSpacing(0); layout->setContentsMargins(0, 0, 0, 0); this->setLayout(layout); // 4. 创建显示文本的 Label QLabel* label = new QLabel(); label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); label->setAlignment(Qt::AlignCenter); label->setStyleSheet("QLabel { font-size: 32px; }"); label->setText(text); layout->addWidget(label); // 5. 实现 2s 之后自动关闭. QTimer* timer = new QTimer(this); connect(timer, &QTimer::timeout, this, [=]() { timer->stop(); // 核心代码, 关闭当前窗口 this->close(); }); timer->start(2000); } void Toast::showMessage(const QString &text) { Toast* toast = new Toast(text); toast->show(); }这个就是比较简单的一个设计。
事实上,在我们的槽函数的设计里面,我们还会需要下面这2个成员。
cpp
QTimer* timer; // 倒计时定时器
int leftTime = 30; // 倒计时剩余秒数,默认30秒
我们回到刚刚槽函数的设计,我们思考一下,为什么需要下面这一句?
cpp
// 3. 开启定时器, 开始倒计时(每秒触发一次)
timer->start(1000);
事实上,我们给定时器也配置了一个槽函数
cpp
// 倒计时定时器
timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, &EmailLoginWidget::countDown);
cpp
// 倒计时槽函数,用于控制发送验证码按钮的禁用和文本更新
void EmailLoginWidget::countDown()
{
if (leftTime <= 1)
{
// 倒计时结束:发送按钮设为可用,恢复文本,停止定时器,重置剩余时间
sendVerifyCodeBtn->setEnabled(true);
sendVerifyCodeBtn->setText("发送验证码");
timer->stop();
leftTime = 30; // 重置为默认倒计时秒数
return;
}
// 倒计时未结束:减少剩余秒数,更新按钮文本,禁用按钮
leftTime -= 1;
sendVerifyCodeBtn->setText(QString::number(leftTime) + " s");
if (sendVerifyCodeBtn->isEnabled())
{
sendVerifyCodeBtn->setEnabled(false); // 确保按钮在倒计时期间不可点击
}
}
这样子这个定时器的作用就明显了。
在 EmailLoginWidget::sendVerifyCode() 中,timer->start(1000); 的作用是启动一个间隔为 1 秒的定时器,用于实现发送验证码按钮的倒计时功能。
具体意义如下:
- 防止用户频繁请求验证码
当用户点击"发送验证码"按钮后,程序会:
- 立即请求服务器发送验证码。
- 同时启动这个定时器,让按钮进入 30 秒倒计时(倒计时期间按钮变灰不可点击)。
- 倒计时结束后按钮才恢复可用,从而避免用户在短时间内反复点击,造成服务器压力和体验问题。
- 每 1 秒更新一次按钮上的剩余秒数
定时器每隔 1000 毫秒会触发一次 countDown() 槽函数,该函数会:
- 将 leftTime 减 1。
- 更新按钮的文本,例如从 "30 s" → "29 s" → ... → "1 s"。
- 在倒计时结束前禁用按钮(虽然已经禁用,但再次设置确保状态)。
- 当 leftTime <= 1 时,恢复按钮文本为 "发送验证码",启用按钮,并调用 timer->stop() 停止计时器。
2.4.登录/注册按钮
那么接下来我们就来设计这个按钮的槽函数

这里是进行了注册/登录双模态的设计,那么我们就有必要去根据这个登录/注册标志位来判断当前是注册还是登录状态。
cpp
// 点击提交按钮(登录或注册)的槽函数
void EmailLoginWidget::clickSubmitBtn()
{
// 1. 从输入框中拿到必要的内容
// 此处不要从输入框直接拿邮箱. 可能用户故意搞事情, 输入验证码的时候, 输入的是邮箱1, 点击提交的时候, 输入的是邮箱2;
const QString& email = this->currentEmail; // 使用发送验证码时保存的邮箱
const QString& verifyCode = verifyCodeEdit->text();
if (email.isEmpty() || verifyCode.isEmpty()) {
Toast::showMessage("邮箱或者验证码不应该为空");
return;
}
// 2. 根据当前模式发送登录或注册请求
DataCenter* dataCenter = DataCenter::getInstance();
if (isLoginMode)
{
// 登录模式:邮箱登录
connect(dataCenter, &DataCenter::emailLoginDone, this, &EmailLoginWidget::emailLoginDone, Qt::UniqueConnection);
dataCenter->emailLoginAsync(email, verifyCode);
} else
{
// 注册模式:邮箱注册
connect(dataCenter, &DataCenter::emailRegisterDone, this, &EmailLoginWidget::emailRegisterDone, Qt::UniqueConnection);
dataCenter->emailRegisterAsync(email, verifyCode);
}
}
这个可以说跟上面账号+密码登录的完全一样。
cpp
// 邮箱登录完成的槽函数
void EmailLoginWidget::emailLoginDone(bool ok, const QString& reason)
{
if (!ok) {
Toast::showMessage("登录失败! " + reason);
return;
}
// 登录成功,跳转到主窗口
MainWidget* mainWidget = MainWidget::getInstance();
mainWidget->show();
// 关闭当前邮箱登录窗口
this->close();
}
// 邮箱注册完成的槽函数
void EmailLoginWidget::emailRegisterDone(bool ok, const QString &reason)
{
if (!ok) {
Toast::showMessage("注册失败! " + reason);
return;
}
Toast::showMessage("注册成功!");
// 注册成功后自动切换到登录模式
switchMode();
// 清空验证码输入框
verifyCodeEdit->clear();
// 重置倒计时相关变量,使发送验证码按钮恢复可用(leftTime 设为 1 会让倒计时定时器立即重置)
leftTime = 1;
}