第六次实训工作报告

设计题目

华南理工大学:植此青绿

本周工作小结:

本周我主要进行了以下工作:

  1. 设计了登录界面和登录页面的整体逻辑。
  2. 创建并设置了玩家排行榜页面,包括基本布局和样式的设计。
  3. 实现了从文件中读取排行榜数据并根据关卡进行排序和显示。
  4. 添加了动态更新排行榜的功能,根据不同关卡展示相应的排行榜。
  5. 进行了部分代码优化和调试工作,确保程序功能的正常实现。

环境与工具

  • 开发环境:Qt Creator 13.0.0
  • 操作系统:Windows 11

前置工作

  1. 准备了一个txt文件进行登录用户的读入。
  2. 设计了登录页面和相应逻辑。
  3. 设计了排行榜页面的基本布局和样式,包括按钮、标签等UI元素。
  4. 准备了用于存储排行榜数据的文本文件。
  5. 思考排行榜具体滑动的设计和读取逻辑,能否实现实时更新
  6. 完善关卡类的提交按钮
后期预备开启功能:

登录页面登录后可以显示用户的游玩记录(暂时有想法,自认为也不难实现)

程序功能说明

设计登录页面类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//+------------------------------------------------------------+
//| LoginDialog |
//+------------------------------------------------------------+
//| - avatarLabel: QLabel | // 头像标签
//| - placeholderLabel: QLabel | // 占位符标签
//| - usernameEdit: QLineEdit | // 用户名输入框
//| - passwordEdit: QLineEdit | // 密码输入框
//| - loginButton: QPushButton | // 登录按钮
//| - registerButton: QPushButton | // 注册按钮
//| - forgotPasswordButton: QPushButton | // 忘记密码按钮
//| - returnButton: QPushButton | // 返回按钮
//| - uploadAvatarButton: QPushButton | // 上传头像按钮
//| - trayIcon: QSystemTrayIcon | // 系统托盘图标
//| - minimizeAction: QAction | // 最小化动作
//| - restoreAction: QAction | // 还原动作
//| - quitAction: QAction | // 退出动作
//| - _username: QString | // 存储用户名
//| - _password: QString | // 存储密码
//| - imagePath: QString | // 存储头像路径
//+------------------------------------------------------------+
//| + LoginDialog(parent: QWidget) | // 构造函数,初始化登录对话框
//| + ~LoginDialog() | // 析构函数,销毁登录对话框
//| + attemptLogin(): void | // 尝试登录
//| + attemptRegister(): void | // 尝试注册
//| + showError(errorMessage: QString): void | // 显示错误消息
//| + showSuccess(message: QString): void | // 显示成功消息
//| + forgotPassword(): void | // 忘记密码处理
//| + uploadAvatar(): void | // 上传头像处理
//| + on_ReturnButton_clicked(): void | // 返回按钮点击处理
//| + keyPressEvent(event: QKeyEvent): void | // 键盘按键处理
//| + createTrayIcon(): void | // 创建系统托盘图标
//| + getUsername(): QString | // 获取用户名
//| + getPassword(): QString | // 获取密码
//| + getAvatarPath(): QString | // 获取头像路径
//+------------------------------------------------------------+
image-20240521145042467

基本功能:实现了基础的页面和逻辑。

1. 创建了整体的按钮和原先的工作一样,也添加了图标,就不过多赘述了。

2. 各大槽函数的书写

  • void LoginDialog::attemptRegister():先检查用户名和密码是否为空,然后以只读if (!file.open(QIODevice::ReadOnly | QIODevice::Text))的方式打开数据文件,该文件是程序一运行就会创建的,只要该游戏在你的电脑里面那一刻,这个文件就存在,直到将该游戏彻底删除为止,因此数据会一直保留。接下来在文件中查找是否用户名出现过,一旦没有出现过,就以追加的方式打开模式if(!file.open(QIODevice::Append | QIODevice::Text)),并写入新用户信息。

  • void LoginDialog::attemptlogin():登录逻辑也一样,就查找密码和用户名,匹配上用户名就查看密码是否正确,不然就提示用户不存在。

3. 获取用户名和密码

1
2
QString username = usernameEdit->text();
QString password = passwordEdit->text();

这两行代码从用户名输入框和密码输入框中获取用户输入的用户名和密码。

4. 打开文件

1
2
3
4
5
6
QFile file("./vvv.txt");
if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
{
// showError("无法打开用户数据文件。");
return;
}

这里尝试以写入模式打开文件vvv.txt

5. 读取文件内容并检查用户名和密码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
QTextStream in(&file);
bool isRegistered = false;

while (!in.atEnd())
{
QString line = in.readLine();
QStringList parts = line.split(' ');
if (parts.size() == 2 && parts[0] == username)
{
isRegistered = true;
if (parts[1] == password)
{
QString loginTime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
emit loggedIn(username, loginTime);
_username = username;
_password = password;
showSuccess("登录成功。");
usernameEdit->clear();
passwordEdit->clear();
file.close();
return;
}
else
{
showError("密码错误,请重试。");
usernameEdit->clear();
passwordEdit->clear();
file.close();
return;
}
}
}
file.close();

  • QTextStream in(&file);:创建一个文本流以读取文件内容。

  • bool isRegistered = false;:初始化一个布尔变量以跟踪用户是否注册。

  • while (!in.atEnd()):循环读取文件的每一行,直到文件结束。

  • QStringList parts = line.split(' ');:将每行按空格分割成两部分:用户名和密码。

  • ```cpp if (parts.size() == 2 && parts[0] == username)

    1
    2
    3
    4
    5
    6
    7

    :检查行的格式是否正确并且用户名是否匹配。

    - `isRegistered = true;`:如果用户名匹配,标记为已注册。

    - ```cpp
    if (parts[1] == password)

    :检查密码是否匹配。

    • 如果匹配,记录当前时间并触发loggedIn信号,同时显示登录成功消息,清除输入框并关闭文件。
    • 如果不匹配,显示密码错误消息,清除输入框并关闭文件。

6. 处理未注册用户

1
2
3
4
5
6
if (!isRegistered)
{
showError("用户名未注册,请先注册。");
}
usernameEdit->clear();
passwordEdit->clear();

如果文件读取完毕后没有找到匹配的用户名,则显示“用户名未注册,请先注册”的错误消息,并清除输入框。

  • void LoginDialog::forgotPassword():用于实现找回密码的功能。

    主要逻辑

    1. 获取用户名:使用 QInputDialog::getText 弹出一个输入框,提示用户输入用户名。如果用户点击了“确定”,且输入的用户名非空,则继续执行下一步。
    2. 获取新密码:再次弹出一个输入框,提示用户输入新密码。如果用户点击了“确定”,且输入的新密码非空,则继续执行下一步。
    3. 读取文件:打开名为 vvv.txt 的文件,该文件存储了用户名和密码。如果文件无法打开,则函数直接返回。
    4. 查找用户名并修改密码:逐行读取文件内容,检查每一行的用户名是否与输入的用户名匹配。如果匹配,将该行的密码更新为新密码;否则,保持原样。用一个 QStringList 存储所有的文件内容,并记录是否找到匹配的用户名。
    5. 写回文件:如果找到了匹配的用户名,则重新打开文件,以截断模式写入更新后的内容。写入更新后的所有行,并关闭文件。更新内部的 _password 变量,并显示成功消息。如果未找到匹配的用户名,显示错误消息。

特色:错误处理

  • 文件无法打开时,直接返回。
  • 用户名未注册时,显示错误消息。
  • 文件写入失败时,显示错误消息。

同时处理了鼠标事件,重写了键盘处理函数,来实现回车注册功能,也算是优化了一点点用户的体验吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void LoginDialog::keyPressEvent(QKeyEvent *event)
{
// 检查按下的键是否是回车键
if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter)
{
// 检查用户名和密码是否都非空
if (!usernameEdit->text().isEmpty() && !passwordEdit->text().isEmpty())
{
// 调用尝试登录函数
attemptLogin();
}
}
else
{
// 其他情况交给父类处理
QWidget::keyPressEvent(event);
}
}
  • void LoginDialog::uploadAvatar():通过打开文件对话框选择图片:检查是否选择了图片,加载图片并截取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void LoginDialog::uploadAvatar()
{
// 打开文件对话框选择图片
imagePath = QFileDialog::getOpenFileName(this, "选择头像", QDir::homePath(), "Images (*.png *.jpg *.jpeg)");
// 检查是否选择了图片
if (!imagePath.isEmpty())
{
// 加载图片并显示在对话框上
QPixmap avatarPixmap(imagePath);
if (!avatarPixmap.isNull())
{
// 截取图片
QPixmap roundedAvatar = avatarPixmap.scaled(250, 250, Qt::KeepAspectRatio, Qt::SmoothTransformation);
roundedAvatar.setMask(roundedAvatar.createHeuristicMask());
// 在对话框上显示截取后的图片
avatarLabel->setPixmap(roundedAvatar);
avatarLabel->move(200,80);
}
}
}
  1. 系统托盘的创建
  • 创建图标
  • 创建托盘菜单
  • 连接QAction对象,用于最小化,还原和退出,并设置为托盘菜单,显示托盘图标。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void LoginDialog::createTrayIcon()
{
// 创建托盘图标
trayIcon = new QSystemTrayIcon(this);
QIcon icon(":/new/images/gdyg2.png"); // 选择一个合适的图标路径
trayIcon->setIcon(icon);
// 创建托盘菜单
QMenu *trayMenu = new QMenu(this);
minimizeAction = new QAction("最小化", this);
restoreAction = new QAction("还原", this);
quitAction = new QAction("退出", this);
connect(minimizeAction, &QAction::triggered, this, &LoginDialog::hide);
connect(restoreAction, &QAction::triggered, this, &LoginDialog::showNormal);
connect(quitAction, &QAction::triggered, qApp, &QApplication::quit);
trayMenu->addAction(minimizeAction);
trayMenu->addAction(restoreAction);
trayMenu->addAction(quitAction);
// 设置托盘菜单
trayIcon->setContextMenu(trayMenu);
// 显示托盘图标
trayIcon->show();
}

设计了用户结构体,和排行榜类。

前置要求,在关卡用户提交部分更改逻辑,直接提交用户头像,用户名和时间到文件,方便排行榜更新,这里不多赘述。

可能会好奇用户名,状态,图片路径从何而来,从登录那一刻就开始通过设计的一系列接口传到关卡一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
	// 重置所有格子数值为初始状态
if (!isGameFinished||!comparePuzzleWithHiddenMap())
{
QMessageBox::warning(this, "警告", "游戏还未结束或您未取得胜利,不能提交!");
return;
}
//用户名和时间的获取
QString userName =_username;
QString elapsedTimeString = QString::number(elapsedTimeSeconds) + "秒";

// 构建提交信息
QString submitInfo = QString("%1 %2 %3 %4").arg(_nowstate).arg(userName).arg(elapsedTimeString).arg(imagePath);
// 写入文件
QString filePath = "./www.txt"; // 文件路径
QFile file(filePath);
if (file.open(QIODevice::Append | QIODevice::Text))
{
QTextStream out(&file);
out << submitInfo << "\n"; // 每次写入一行
file.close();
}
else
{
QMessageBox::warning(this, "错误", "无法打开文件进行写入!");
return;
}

public函数接口:

1
2
3
4
5
6
7
// 更新信息
void PracticePage::updateinformation()
{
levelOnePa->_username=this->_username;
levelOnePa->_password=this->_password;
levelOnePa->imagePath=this->imagePath;
}

1. PlayerRankingPage 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//                    +-------------------------------------+
// | PlayerRankingPage |
// 玩家排行榜页面类
// +-------------------------------------+
// | - mainLayout: QVBoxLayout* |
// 布局管理器,用于管理页面布局
// | - returnButton: QPushButton* |
// 返回按钮,用于返回主页面
// | - rankListLayout: QVBoxLayout* |
// 排行榜布局管理器,用于管理排行榜布局
// +-------------------------------------+
// | + PlayerRankingPage(QWidget*) |
// 构造函数,初始化页面
// | + ~PlayerRankingPage() |
// 析构函数,释放资源
// | + readRankingFromFile(): std::vector<UserRankItem> | // 从文件中读取排行榜数据
// | + showLevel1Ranking() |
// 显示关卡一排行榜
// | + showLevel2Ranking() |
// 显示关卡二排行榜
// | + showLevel3Ranking() |
// 显示关卡三排行榜
// | + updateRanking(const QString&) |
// 更新排行榜数据
// | + clearRankingData() |
// 清空排行榜数据
// +-------------------------------------+

2. UserRankItem 类

1
2
3
4
5
6
7
8
9
10
//                    |             UserRankItem            |    
// 用户排名条目类
// +-------------------------------------+
// | - username: QString |
// 用户名
// | - avatarIcon: QIcon |
// 用户头像图标
// | - elapsedTime: QString |
// 完成时间
// +-------------------------------------+

具体功能和逻辑

  • 创建关卡一,关卡二,关卡三,返回按钮,与标题栏
1
2
3
4
5
6
7
8
9
    QPushButton *level1Button = new QPushButton("关卡一", this);
QPushButton *level2Button = new QPushButton("关卡二", this);
QPushButton *level3Button = new QPushButton("关卡三", this);
//标题栏
QHBoxLayout *titleLayout = new QHBoxLayout();
QLabel *avatarTitle = new QLabel("头像", this);
QLabel *usernameTitle = new QLabel("用户名", this);
QLabel *timeTitle = new QLabel("完成时间", this);

  • 创建滚动区域和排行榜布局器
1
2
 QScrollArea *scrollArea = new QScrollArea(this);
QWidget *scrollWidget = new QWidget();
  • 一些槽函数的设计:这里主要是用来识别关卡一,关卡二,关卡三各自的信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//分别用于显示关卡一、关卡二和关卡三的排行榜
void PlayerRankingPage::showLevel1Ranking()
{
updateRanking("关卡一");
}

void PlayerRankingPage::showLevel2Ranking()
{
updateRanking("关卡二");
}

void PlayerRankingPage::showLevel3Ranking()
{
updateRanking("关卡三");
}
  • 排行榜的更新策略

1. 清空排行榜

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
QLayoutItem *item;
while ((item = rankListLayout->takeAt(2)) != nullptr) // 从第3个索引开始,因为前面有标题栏等控件
{
if (QLayout *layout = item->layout())
{
QLayoutItem *subItem;
while ((subItem = layout->takeAt(0)) != nullptr)
{
if (QWidget *widget = subItem->widget())
{
widget->deleteLater();
}
delete subItem;
}
}
delete item;
}

2. 读取并排序新的排行榜数据

1
2
3
4
5
6
7
8
std::vector<UserRankItem> userRankList = readRankingFromFile(level);
std::sort(userRankList.begin(), userRankList.end(), [](const UserRankItem &a, const UserRankItem &b)
{
int timeA = a.elapsedTime.split(" ")[0].toInt();
int timeB = b.elapsedTime.split(" ")[0].toInt();
return timeA < timeB;
});

3. 放入列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
for (const auto &rankItem : userRankList)
{
QHBoxLayout *rankItemLayout = new QHBoxLayout();
rankItemLayout->setSpacing(100);

QLabel *avatarLabel = new QLabel(this);
avatarLabel->setScaledContents(true);
avatarLabel->setPixmap(rankItem.avatarIcon.pixmap(QSize(80, 80))); // 设置头像大小
avatarLabel->setFixedSize(80, 80); // 固定头像大小
avatarLabel->setStyleSheet("border: 2px solid black; border-radius: 10px;");
rankItemLayout->addWidget(avatarLabel);

rankItemLayout->setSpacing(40);
QLabel *usernameLabel = new QLabel(rankItem.username, this);
usernameLabel->setFont(QFont("Microsoft YaHei", 16));
usernameLabel->setStyleSheet("color: black;");
usernameLabel->setAlignment(Qt::AlignCenter);
rankItemLayout->addWidget(usernameLabel);

rankItemLayout->setSpacing(20);
QLabel *timeLabel = new QLabel(rankItem.elapsedTime, this);
timeLabel->setFont(QFont("Microsoft YaHei", 16));
timeLabel->setStyleSheet("color: black;");
timeLabel->setAlignment(Qt::AlignCenter);
rankItemLayout->addWidget(timeLabel);

rankListLayout->addLayout(rankItemLayout);
}

具体效果展示:

image-20240521151544044

寄语

感觉可以来一点花哨的设计,因为大体的游戏逻辑和界面已经做完了,比如联网和音乐,正在逐步考虑中,同时也要进行页面的进一步美化。

春风得意马蹄疾,一日看尽长安花!