第五次实训工作报告

设计题目

华南理工大学:植此青绿

本周工作小结:

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

  1. 对之前的部分代码进行注释和优化。
  2. 思考并实现自由模式随机地图的生成,如何让用户尽可能体会到随机的乐趣
  3. 思考自主创建模式代码的实现,并考虑判断题目是否有解,不过经过实践,随机算法和搜索都无法解决,最终选择放弃,默认用户输入的地图是正确的。

环境与工具

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

前置工作

  1. 研究C++的各类随机数,最终决定选择使用更先进的随机数生成器:C++11引入了更先进的随机数生成器,如std::mt19937。这些生成器通常比旧的rand()函数提供更好的随机性。
1
2
3
4
5
6
#include <random>
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> distr(1, 100);
int random_number = distr(gen);
std::mt19937 gen(42); // 使用种子42
  1. QT写文件的操作和纯C++略有差异,需要提前了解,并区分,还有qrc资源文件的使用.
  2. 自输入模式用户的输入,由于暂无思路因此减去了判断解的过程。
  3. 对一开始的关卡一,关卡二,关卡三进行复用,通过点击按钮引发的槽函数更换为只有一个关卡类。
  4. 设置了一个按钮拖拽类,方便用户实现按钮自动拖拽来自定义布局.
后期预备开启功能:

后期预备优化随机算法,并对各关卡进行页面美化。

程序功能说明

随机模式的设计

1. 拼图类的变量说明

1
2
3
4
5
6
7
8
// 定义拼图类
struct Puzzle
{
PuzzleType type;
int value; // 用于记录拼图的值(即被拼区域每次加一后的值)
int x, y; // 拼图在地图中的位置
Puzzle(PuzzleType type, int value, int x, int y) : type(type), value(value), x(x), y(y) {}
};

2. 7幅拼图

1
2
3
4
5
6
7
8
9
10
std::vector<Puzzle> puzzles =
{
Puzzle(PT_1x1, 1, 0, 0),
Puzzle(PT_1x1, 1, 0, 0),
Puzzle(PT_2x2, 1, 0, 0),
Puzzle(PT_2x2, 1, 0, 0),
Puzzle(PT_3x3, 1, 0, 0),
Puzzle(PT_3x3, 1, 0, 0),
Puzzle(PT_4x4, 1, 0, 0)
};

3. 大体思路是使用已有的7幅地图,每次随机覆盖地图,并检查是否出界,这样的好处就是可以在得到一副随机地图的同时也可以得到相应的解答,因此也省去了随机模式下解的判断过程.

本随机算法保证了一定会有一块4*4地图,算法运行时长在1-2秒上下,整体还算稳健,但还有改进空间.

算法检查是否越界过程:

1
2
3
4
5
6
7
8
9
if (puzzle.x + puzzle.type+1 > 6 || puzzle.y + puzzle.type+1 > 6)
{
placed = 1;
for (int i = 0; i <= 35; i++)
{
map[i] = 0;
}
break;
}

保证地图格式要求符合比赛模式下的自由模式.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
for (int i = 0; i <= 35; i++)
{
if(map[i]==4)
{
cnt++;
}
if(cnt>1)
{
flag=1;
break;
}
if (map[i] > 4)
{
flag = 1;
break;
}
if(map[i]==0)
{
flag=1;
break;
}
}

将随机生成的地图通关qt的读写文件读入游戏程序的根目录下,起初是采取了绝对路径,但这对于不同电脑的用户没有普适性,因此采用了这种方式.

除此之外,也对文件无法打开进行了稳健性处理.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 写入C盘文件
QFile file("./xxx.txt");
if (file.open(QIODevice::WriteOnly | QIODevice::Text))
{
QTextStream out(&file);
for (int val : map)
{
out << val << '\n';
}
file.close();
}
else
{
qWarning() << "Failed to open file for writing";
}

输入模式的设计

1. 创建按钮并连接到槽函数

1
2
3
4
5
editMapButton = new QPushButton("编辑地图", this);
editMapButton->setStyleSheet(buttonStyle);
editMapButton->setIcon(QIcon(":/new/images/zsr.png"));
editMapButton->setIconSize(QSize(41,41));
connect(editMapButton, &QPushButton::clicked, this, &InputModePage::openMapinputDialog);

2. 更新槽函数

用户自输入36个数字,暂时默认用户输入是正确的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void InputModePage::openMapinputDialog()
{
bool ok;
std::vector<int> newHiddenMap(36);
for (int i = 0; i < 36; ++i) {
QString text = QInputDialog::getText(this, tr("编辑地图"), tr("请输入第%1个数字:").arg(i + 1), QLineEdit::Normal, QString(), &ok);
if (!ok || text.isEmpty())
{
return;
}
bool conversionOk;
int value = text.toInt(&conversionOk);
if (!conversionOk || value < 0 || value > 4)
{
QMessageBox::warning(this, tr("错误"), tr("请输入一个有效的数字(0-4)"));
return;
}
newHiddenMap[i] = value;
}
hiddenMap = newHiddenMap;
updatePuzzleGridSymbols();

}

image-20240521204001579 ### 对练习页面的美化,将原先的水平布局改成了垂直布局,更改了按钮样式,并添加了图标。

1
2
3
4
5
levelOneButton->setStyleSheet(buttonStyle);
levelOneButton->setIcon(QIcon(":/new/images/jian.png"));
levelOneButton->setIconSize(QSize(35, 35));
levelOneButton->setFixedSize(250, 100);
hlayout->addWidget(levelOneButton);

样式如上。

练习页面的复用

1. 通过点击按钮来连接槽函数,实时更新关卡地图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    connect(levelOneButton, &QPushButton::clicked, this, &PracticePage::readlevelone);
connect(levelTwoButton, &QPushButton::clicked, this, &PracticePage::readleveltwo);
connect(levelThreeButton, &QPushButton::clicked, this, &PracticePage::readlevelthree);

//复用地图的三个函数
void PracticePage::readlevelone()
{
levelOnePa->_nowstate="关卡一";
levelOnePa->update(":/new/images/qqzl.txt");
}

void PracticePage::readleveltwo()
{
levelOnePa->_nowstate="关卡二";
levelOnePa->update(":/new/images/qqzlx.txt");
}

void PracticePage::readlevelthree()
{
levelOnePa->_nowstate="关卡三";
levelOnePa->update(":/new/images/qqzlt.txt");
}

2. 在关卡类内创建更新信息的函数update(),避免了代码大量重复的冗余。

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
void LevelOnepa::update(const QString &filePath)
{
readHiddenMapFromFile(filePath,hiddenMap);
for (int row = 0; row < puzzleSize; ++row)
{
for (int col = 0; col < puzzleSize; ++col)
{
// 创建特殊符号图片标签
QLabel *symbolCell = new QLabel(this);
symbolCell->setFixedSize(60, 60);
symbolCell->setAlignment(Qt::AlignCenter);
// 设置初始特殊符号
setSpecialSymbolForCell(symbolCell, hiddenMap[row * puzzleSize + col]);
// 用一个布局将文本标签和特殊符号图片标签组合在一起
QVBoxLayout *cellLayout = new QVBoxLayout();
cellLayout->setContentsMargins(0, 0, 0, 80);
symbolCell->setAttribute(Qt::WA_TransparentForMouseEvents, true);
cellLayout->insertWidget(0,symbolCell);
// 将组合后的布局添加到拼图网格中
QWidget *cellWrapper = new QWidget(this);
cellWrapper->setLayout(cellLayout);
puzzleGrid->addWidget(cellWrapper, row, col);
}
}
}

设置页面再谈

由于先前的设置页面我对于布局了解并不深刻,并不知道各个布局如何正确使用,但经过一段时间的qt使用后,有了深入的了解,便来对设置页面(帮助文档进行设计),并将之记录,进一步加深对布局的理解。

大体分析:

需要一个图像垂直布局:放置5个图标在左侧。

图像和文字需要在同一行,因此需要添加一个水平布局,添加对应的图片和文字。

然后将水平布局添加到上述水平布局。

在此之前要先添加返回按钮,因为放置是从高到低的。

1. 返回按钮设置和图标

与上关卡的按钮样式设置一致,加上了图标,使之更加美观。

1
2
3
4
5
//创建中心内容的布局
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->addWidget(returnButton, 0, Qt::AlignTop | Qt::AlignLeft);
// 设置主布局
setLayout(mainLayout);

2. 设置图像垂直布局

1
QVBoxLayout *imageLayout = new QVBoxLayout;

3. 设置图像垂直布局,并创建5个水平布局,添加图片和文字

这里仅仅放出一行的设计,接下来都是同质化代码,不再赘述。

image-20240521204106869

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//创建一个水平布局,然后添加图片和相应的文字
QHBoxLayout *row1 = new QHBoxLayout;
QLabel *imageLabel1 = new QLabel(this);
imageLabel1->setPixmap(QPixmap(":/new/images/px1.png"));
imageLabel1->setScaledContents(true); // 启用自适应大小
imageLabel1->setMaximumSize(50, 50); // 设置最大大小
row1->addWidget(imageLabel1);

QLabel *noTreeLabel1 = new QLabel("代表该方格没有树", this);
noTreeLabel1->setStyleSheet("font-family: Arial; font-size: 30px;");
noTreeLabel1->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
row1->addWidget(noTreeLabel1);

//图像布局添加第一行
imageLayout->addLayout(row1);

比较起第一次的布局也算是相当美观了,后期可以加入一些文字进行粗细的设计,通过加粗一些字体达成突出醒目的效果。

设计可拖拽按钮

由于并未实现地图拖拽,当时游戏逻辑已经成型,改起来势必地动山摇,但不实现拖拽确实是一大遗憾,因此心血来潮实现了可拖拽按钮。

一些成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 初始化成员变量
isPressed = false;
// 鼠标是否被按下
isMoved = false;
// 按钮是否被拖动
lastPoint = QPoint();
// 鼠标按下时的最后一个点
x_left_distance = 0;
// 左边距
x_right_distancce = 0;
// 右边距
y_top_distance = 0;
// 上边距
y_bottom_distance = 0;
// 下边距

时间过滤器,处理鼠标事件,也就是拖拽这一事件的核心

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// 事件过滤器,用于处理鼠标事件
bool YDragButton::eventFilter(QObject *watched, QEvent *event)
{
// 将事件转换为鼠标事件
QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
switch(event->type())
{
case QEvent::MouseButtonPress:
// 鼠标按下事件
{
// 检查是否是右键按下
if (mouseEvent->button() == Qt::RightButton)
{
lastPoint = mouseEvent->pos();
// 记录按下时的点
isPressed = true;
// 标记鼠标被按下
}
break;
}
case QEvent::MouseMove: // 鼠标移动事件
{
if (isPressed)
{
int dx = mouseEvent->pos().x() - lastPoint.x();
// 计算鼠标在 X 方向的移动距离
int dy = mouseEvent->pos().y() - lastPoint.y();
// 计算鼠标在 Y 方向的移动距离
int x1 = this->x() + dx;
// 计算按钮的新 X 坐标
int y1 = this->y() + dy;
// 计算按钮的新 Y 坐标
int right_distance = this->parentWidget()->width() - 2 * x_right_distancce - this->width();
// 计算按钮在父窗口中的最大右边距
int bottom_distance = this->parentWidget()->height() - 2 * y_bottom_distance - this->height();
// 计算按钮在父窗口中的最大下边距
// 检查新位置是否在允许范围内
if (x1 > x_left_distance && x1 < right_distance && y1 > y_top_distance && y1 < bottom_distance)
this->move(x1, y1);
// 移动按钮到新位置
isMoved = true;
// 标记按钮被拖动
}
break;
}
case QEvent::MouseButtonRelease:
// 鼠标释放事件
{
if(isMoved != true)
{
// 如果按钮没有被拖动
emit clicked();
// 触发 clicked 信号
emit toggled(!isChecked);
// 触发 toggled 信号
isChecked = !isChecked;
// 切换按钮的选中状态
}
else
{
isMoved = false;
// 重置拖动标记
}
isPressed = false;
// 重置按下标记
break;
}
case QEvent::MouseButtonDblClick:
// 鼠标双击事件
emit doubleClicked();
// 触发 doubleClicked 信号
break;
default:
break;
}
return QWidget::eventFilter(watched, event);
// 调用基类的事件过滤器
}

起初是采用了右键拖拽逻辑,但有些搞笑,一开始点击后会可以拖拽也会触发槽函数,然后就改为了左键了。

寄语

希望能在下周搞清楚排行榜和登录的设计,这两部分尚且不完全,道阻且长,心向往之,总可抵达。