第五次实训工作报告
设计题目
华南理工大学:植此青绿
本周工作小结:
本周我主要进行了以下工作:
对之前的部分代码进行注释和优化。
思考并实现自由模式随机地图的生成,如何让用户尽可能体会到随机的乐趣
思考自主创建模式代码的实现,并考虑判断题目是否有解,不过经过实践,随机算法和搜索都无法解决,最终选择放弃,默认用户输入的地图是正确的。
环境与工具 :
开发环境:Qt Creator 13.0.0
操作系统:Windows 11
前置工作
研究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 ) ;
QT写文件的操作和纯C++略有差异,需要提前了解,并区分,还有qrc资源文件的使用.
自输入模式用户的输入,由于暂无思路因此减去了判断解的过程。
对一开始的关卡一,关卡二,关卡三进行复用,通过点击按钮引发的槽函数更换为只有一个关卡类。
设置了一个按钮拖拽类,方便用户实现按钮自动拖拽来自定义布局.
后期预备开启功能:
后期预备优化随机算法,并对各关卡进行页面美化。
程序功能说明
随机模式的设计
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 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 (); }
###
对练习页面的美化,将原先的水平布局改成了垂直布局,更改了按钮样式,并添加了图标。
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个水平布局,添加图片和文字
这里仅仅放出一行的设计,接下来都是同质化代码,不再赘述。
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 (); int dy = mouseEvent->pos ().y () - lastPoint.y (); int x1 = this ->x () + dx; int y1 = this ->y () + dy; 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 () ; emit toggled (!isChecked) ; isChecked = !isChecked; } else { isMoved = false ; } isPressed = false ; break ; } case QEvent::MouseButtonDblClick: emit doubleClicked () ; break ; default : break ; } return QWidget::eventFilter (watched, event); }
起初是采用了右键拖拽逻辑,但有些搞笑,一开始点击后会可以拖拽也会触发槽函数,然后就改为了左键了。
寄语
希望能在下周搞清楚排行榜和登录的设计,这两部分尚且不完全,道阻且长,心向往之,总可抵达。