第四次实训工作报告

设计题目

华南理工大学:植此青绿

本周工作小结:

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

进行重置按钮,开始按钮,提交按钮,

环境与工具

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

前置工作

  1. 研究QT计时器的使用和计时器何时使用的逻辑
  2. 思考用什么数据结构能够最好程度的模拟撤销操作
  3. 其余按钮的设置选择,尽最大可能满足用户需求
后期预备开启功能:

程序功能说明

1. 四个按钮的创建:

1
2
3
4
resetButton = new QPushButton("重置", this);
undoButton = new QPushButton("撤销", this);
submitButton = new QPushButton("提交", this);
startButton = new QPushButton("开始", this);

创建了几个按钮,并为它们设置了样式和点击事件。

计时器

1
2
connect(timer, &QTimer::timeout, this, &LevelOnepa::updateTimer);
timer->setInterval(1000);

这里设置了一个计时器,每秒触发一次updateTimer函数。

开始游戏 (LevelOnepa::startGame())

1
2
3
elapsedTimeSeconds = 0;
timerLabel->setText("时间:0 秒");
timer->start();
  • elapsedTimeSeconds: 重置时间计数。
  • timerLabel->setText(...): 更新时间标签。
  • timer->start(): 启动计时器。

分别是开始游戏的标配设置。

撤销操作 (LevelOnepa::undoLastOperation())

为什么要用栈来实现撤销操作呢?一个奇妙的地方。

原因如下:

  1. 操作的有序性:栈具有“后进先出”(Last In First Out,简称 LIFO)的特性。这意味着最后一个操作(即最近的操作)将首先被撤销,这与用户的预期相符。
  2. 简单性:栈是一种简单而有效的数据结构,它提供了必要的操作,如入栈(push)和出栈(pop),以及检查栈顶元素(top)。
在这里插入图片描述

如何实现用栈实现撤销操作?

我使用了一个QVector<QString>来模拟一个撤销栈。每次用户进行一个操作时(例如,点击一个单元格增加数值),将该操作的描述信息(例如,点击的行、列和使用的格子大小)添加到这个栈中。

1
2
QString operationDescription = QStringLiteral("%1,%2,%3").arg(clickedRow).arg(clickedCol).arg(currentGridSize);
undoStack.push(operationDescription);

当用户点击“撤销”按钮时,你会从栈顶取出最后一个操作的描述信息,并解析这个信息以获取所需的操作参数(行、列和格子大小)。

1
2
3
4
5
QString lastOperation = undoStack.pop();
QStringList operationParts = lastOperation.split(",");
int clickedRow = operationParts[0].toInt();
int clickedCol = operationParts[1].toInt();
int gridSize = operationParts[2].toInt();

根据这些参数撤销相应的操作,将点击区域内的格子数值减一。

1
2
3
4
5
6
7
8
9
10
11
12
13
for (int row = clickedRow; row < std::min(clickedRow + gridSize, puzzleSize); ++row) {
for (int col = clickedCol; col < std::min(clickedCol + gridSize, puzzleSize); ++col) {
targetCells.append(puzzleCells[row * puzzleSize + col]);
}
}
for (auto targetCell : targetCells)
{
int currentValue = targetCell->text().toInt();
if (currentValue > 0)
{
targetCell->setText(QString::number(currentValue - 1));
}
}

重置操作(on_ResetButton_clicked

这里主要是有很多的细节需要考虑,容易落下一些东西导致程序无法正常运行,不过经过数次调试后也算是全部加齐全了。

一个个介绍一下:

1. 重置格子数值:

遍历所有的格子,并将它们的文本设置为 "0",这样就可以重置所有的格子值。

1
2
3
for (int i = 0; i < 36; ++i) {
puzzleCells[i]->setText("0");
}

2. 清空按钮使用记录:

1
2
3
for (int i = 0; i < useCounts.size(); ++i) {
useCounts[i] = 0;
}

遍历 useCounts 数组,并将其所有元素设置为0。这样可以重置所有按钮的使用次数。

3. 清空撤销栈:

1
undoStack.clear();

这行代码清空了 undoStack,即撤销栈,以便从头开始记录新的操作。

4. 重置相关状态变量:

1
isButtonClicked = false;

这行代码将 isButtonClicked 变量设置为 false,这表示没有按钮被点击。

5. 重置计时器:

1
2
3
elapsedTimeSeconds = 0;
timerLabel->setText("时间:0 秒");
timer->start(); // 重新启动计时器

这部分代码将 elapsedTimeSeconds 设置为0,将计时器标签的文本设置为 "时间:0 秒",然后重新启动计时器。

提交按钮(on_SubmitButton_clicked

提交按钮的逻辑也应该设计好,首先我要能判断是否游戏结束,如果游戏无法结束,那便不能提交,会弹出窗口:

image-20240425005847773

如果用户输入用户名为空,也会弹出警告窗口:

image-20240425005943149

如果满足上述条件,就代表提交成功,则会弹出下列窗口:

image-20240425010505874

处理好一些相应的边界情况之后就应该进行提交这一逻辑的设计了,结合上述步骤如下:

  1. 检查游戏状态: 在用户点击提交按钮之前,首先检查游戏是否已经结束。如果游戏尚未结束,弹出一个警告对话框,提醒用户游戏还未结束或者还未获胜,不能提交。
  2. 获取用户名: 使用QInputDialog::getText方法弹出一个输入框,让用户输入用户名。如果用户名为空,弹出警告对话框。
  3. 格式化游戏时间: 使用elapsedTimeSeconds变量的值并格式化为字符串,表示玩家完成游戏所用的时间。
  4. 构建提交信息: 创建一个字符串,包含关卡名称、用户名和玩家完成游戏所用的时间。
  5. 写入文件: 打开指定路径的文件submitRecord.txt,并将提交的信息追加到文件末尾。如果文件打开失败,弹出错误对话框。
  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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
void LevelOnepa::on_SubmitButton_clicked()
{
// 重置所有格子数值为初始状态
if (!isGameFinished)
{
QMessageBox::warning(this, "警告", "游戏还未结束或您未取得胜利,不能提交!");
return;
}
// 弹出对话框让用户输入用户名
QString userName = QInputDialog::getText(this, "输入用户名", "请输入您的用户名:");
if (userName.isEmpty())
{
QMessageBox::warning(this, "警告", "用户名不能为空!");
return;
}
// 获取时间
QString elapsedTimeString = QString::number(elapsedTimeSeconds) + " 秒";
// 构建提交信息
QString submitInfo = QString("关卡一 %1 %2").arg(userName).arg(elapsedTimeString);
// 写入文件
QString filePath = "C:/cat/submitRecord.txt"; // 文件路径
QFile file(filePath);
if (file.open(QIODevice::Append | QIODevice::Text))
{
QTextStream out(&file);
out << submitInfo << "\n"; // 每次写入一行
file.close();
}
else
{
QMessageBox::warning(this, "错误", "无法打开文件进行写入!");
return;
}
for (int i = 0; i < 36; ++i) {
puzzleCells[i]->setText("0");
}
// 清空所有按钮使用记录
for (int i = 0; i < useCounts.size(); ++i) {
useCounts[i] = 0;
}
// 清空撤销栈
undoStack.clear();
// 根据需要,可重置其他相关状态变量(如isButtonClicked等)
isButtonClicked = false;
// 重置计时器
elapsedTimeSeconds = 0;
timerLabel->setText("时间:0 秒");
timer->start(); // 重新启动计时器
QMessageBox::information(this, "提交成功", "您已成功提交!");
}

最终整个关卡窗口如下:

image-20240425010601693

麻烦的问题

最麻烦的问题莫过于撤销操作的字符串格式化和栈这个数据结构的应用,主要是平常没怎么应用到栈的思想,导致一开始想用数组直接模拟,后来灵光一现才发觉栈的妙用。

其次的问题就是失败窗口和成功窗口大小的设置,一开始是在两个窗口各自的构造函数进行设置的,后来发现怎么改都没用,无论是setBaseSize( 800, 600 );还是this->resize( QSize( 800, 600 ));,又或者是setMinimumSize ( 450, 600 );,都没有任何变化。就这样一筹莫展了好几节课后,点开创建窗口的页面,发现那里有个大小的设置,我说怎么回事呢,改了之后1A了。

其余都是不太大的问题,小调一下都还ok。

寄语

没什么好说的,放慢速度,一次只做一件事,多睡一会。