【微服务即时通讯】登录注册界面设计

目录

[一.账号+密码 登录注册界面设计(loginwidget.cpp)](#一.账号+密码 登录注册界面设计(loginwidget.cpp))

1.1.验证码模块

1.2.登录/注册双模态的转换

1.3.与邮箱注册/登录界面的切换

1.4.注册/登录按钮

二.邮箱注册登录界面设计(emailloginwidget.cpp))

2.1.登录/注册双模态的转换

[2.2.与 账号+密码 登录界面的切换](#2.2.与 账号+密码 登录界面的切换)

2.3.获取验证码

2.4.登录/注册按钮


一.账号+密码 登录注册界面设计(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 事件,它在以下几种典型情况下会被触发,从而引起重绘:

  1. 控件首次创建完成并显示时

    比如在 QWidget 上绘制内容,必须在控件构造完成并显示之后才会生效。paintEvent 会在首次显示时被调用,确保绘制内容正确呈现。

  2. 控件被遮挡后重新显露时

    如果控件之前被其他窗口或元素遮挡,当遮挡物移开时,系统会触发重绘,以保证之前被遮挡部分的内容能够正确恢复显示。

  3. 窗口从最小化状态还原时

    窗口最小化后再恢复显示,其内容需要重新绘制,此时也会触发 paintEvent

  4. 控件大小发生变化时

    当控件尺寸改变(如用户拖拽调整窗口大小),通常需要重新绘制以适应新的尺寸,此时也会自动调用 paintEvent

  5. 主动在代码中调用 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 秒的定时器,用于实现发送验证码按钮的倒计时功能。

具体意义如下:

  1. 防止用户频繁请求验证码

当用户点击"发送验证码"按钮后,程序会:

  • 立即请求服务器发送验证码。
  • 同时启动这个定时器,让按钮进入 30 秒倒计时(倒计时期间按钮变灰不可点击)。
  • 倒计时结束后按钮才恢复可用,从而避免用户在短时间内反复点击,造成服务器压力和体验问题。
  1. 每 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;
}
相关推荐
400分2 小时前
git踩坑之修改仓库首次提交(根提交)的commit信息
架构
倔强的石头1062 小时前
告别昂贵的ETL——大数据架构下的时序选型指南
大数据·架构·etl
非情剑2 小时前
Tlog实现微服务日志追踪
微服务·云原生·架构
小小仙。2 小时前
IT自学第四十一天(微服务)
微服务·云原生·架构
GIOTTO情2 小时前
Infoseek 字节探索媒介投放技术架构解析:AI 驱动的全链路自动化实现
人工智能·架构·自动化
志栋智能2 小时前
超自动化巡检:敏捷运维体系中的重要一环
运维·服务器·网络·云原生·容器·kubernetes·自动化
一切皆是因缘际会3 小时前
结构安全革命:下一代 AI 从 “不可控” 到 “绝对可控” 的范式跃迁
人工智能·安全·ai·架构
2501_933329553 小时前
Infoseek数字公关AI中台技术解析:基于DeepSeek+NLP的全网舆情监测与智能处置系统
人工智能·架构·数据库开发
丷丩3 小时前
策略模式实战:GeoAI-UP中MVT发布器的可扩展架构设计
人工智能·架构·gis·策略模式·空间分析·geoai