I'm trying to learn how to use Qt (5.11.1) for GUI applications so I've done a simple memory game where 12 tiles are displayed and every time the user clicks on a tile it will show an image, so they have to match them into 6 pairs of images.
There's a countdown of 1 minute. Game ends if the time is up before all 6 pairs have been matched, or if all 6 pairs are matched, only that it will show different messages to the user. There is no next level, saving score or anything, so it's very simple.
I know there's room for adding many more features, but would like to know what can be improved from what I've done so far.
My mainwindow.h file:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QTimer>
#include <QTime>
#include <QString>
#include <QVector>
#include <QHash>
#include <QRandomGenerator>
#include <QPushButton>
#include <QMessageBox>
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow();
QTimer *timer=new QTimer();
QTime time;
QVector<QString> tiles{"tile01", "tile02", "tile03", "tile04",
"tile05", "tile06", "tile07", "tile08",
"tile09", "tile10", "tile11", "tile12"};
QHash<QString, QString> tile_image;
int score=0;
bool isTurnStarted;
QPushButton* previousTile;
QPushButton* currentTile;
int matchesLeft;
QMessageBox msgBox;
private slots:
void updateCountdown();
void tileCliked();
void randomize(QVector<QString> &tiles);
void bindTileImage(QVector<QString> &tiles, QHash<QString, QString> &tile_image);
void findTurnResult();
void restartTiles();
void showImage();
void findFinalResult();
void updateState();
void initalizeGame();
private:
Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H
My mainwindow.cpp file:
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow){
ui->setupUi(this);
//Connect timer to the slot that will handle the timer
connect(timer, SIGNAL(timeout()), this, SLOT(updateState()));
//Connect each button to the same slot, which will figure out which button was pressed and show its associated image file accordingly
connect(ui->tiles01, SIGNAL(clicked()), this, SLOT(tileCliked()));
connect(ui->tiles02, SIGNAL(clicked()), this, SLOT(tileCliked()));
connect(ui->tiles03, SIGNAL(clicked()), this, SLOT(tileCliked()));
connect(ui->tiles04, SIGNAL(clicked()), this, SLOT(tileCliked()));
connect(ui->tiles05, SIGNAL(clicked()), this, SLOT(tileCliked()));
connect(ui->tiles06, SIGNAL(clicked()), this, SLOT(tileCliked()));
connect(ui->tiles07, SIGNAL(clicked()), this, SLOT(tileCliked()));
connect(ui->tiles08, SIGNAL(clicked()), this, SLOT(tileCliked()));
connect(ui->tiles09, SIGNAL(clicked()), this, SLOT(tileCliked()));
connect(ui->tiles10, SIGNAL(clicked()), this, SLOT(tileCliked()));
connect(ui->tiles11, SIGNAL(clicked()), this, SLOT(tileCliked()));
connect(ui->tiles12, SIGNAL(clicked()), this, SLOT(tileCliked()));
initalizeGame();
}
void MainWindow::tileCliked(){
//get the tile that was clicked
currentTile=qobject_cast<QPushButton*>(sender());
//get the image linked to that tile in the map and set tile background to it
showImage();
//disable current tile so it can't be clicked again (unless there is no match, in which case it will be re-enabled)
currentTile->setEnabled(false);
//do something depending on whether the revealed tile is the first or the second tile in the turn
if (!isTurnStarted){
previousTile=currentTile;
isTurnStarted=true;
}
else{
//change score and display it
findTurnResult();
ui->lblScore->setText(QString::number(score));
//reset turn
isTurnStarted=false;
}
}
void MainWindow::showImage(){
QString tile_name=currentTile->objectName();
QString img=tile_image[tile_name];
currentTile->setStyleSheet("#" + tile_name + "{ background-image: url(://" + img + ") }");
}
void MainWindow::restartTiles(){
//return tiles from current turn to the default state (remove backgrounds)
previousTile->setStyleSheet("#" + previousTile->objectName() + "{ }");
currentTile->setStyleSheet("#" + currentTile->objectName() + "{ }");
//re-enable both tiles so they can be used on another turn
currentTile->setEnabled(true);
previousTile->setEnabled(true);
//re-enable the whole tile section
ui->frame->setEnabled(true);
}
void MainWindow::findFinalResult(){
msgBox.setWindowTitle("Game has ended");
msgBox.setIcon(QMessageBox::Information);
msgBox.setStandardButtons(QMessageBox::Yes);
msgBox.addButton(QMessageBox::No);
msgBox.setDefaultButton(QMessageBox::Yes);
msgBox.setEscapeButton(QMessageBox::No);
if (matchesLeft==0){
timer->stop();
msgBox.setText("Good job! Final score: " + QString::number(score) + "\nPlay again?");
if (QMessageBox::Yes == msgBox.exec()){
initalizeGame();
}
else{
QCoreApplication::quit();
}
}
else{
if (time.toString()=="00:00:00"){
timer->stop();
ui->frame->setEnabled(false);
msgBox.setText("Game over.\nPlay again?");
if (QMessageBox::Yes == msgBox.exec()){
initalizeGame();
}
else{
QCoreApplication::quit();
}
}
}
}
void MainWindow::findTurnResult(){
//check if there is a match (the current tile matches the previous tile in the turn)
if (tile_image[currentTile->objectName()]==tile_image[previousTile->objectName()]){
score+=15;
matchesLeft--;
//if there is a match, find out if all tiles have been matched.
findFinalResult();
}
else{
score-=5;
//disable the whole tile section so no tiles can be turned during the 1-second "memorizing period"
ui->frame->setEnabled(false);
//if there is no match, let user memorize tiles and after 1 second hide tiles from current turn so they can be used on another turn
QTimer::singleShot(1000, this, SLOT(restartTiles()));
}
}
void MainWindow::initalizeGame(){
//start turn
isTurnStarted=false;
//Set score
score=0;
ui->lblScore->setText(QString::number(score));;
//Set matches counter
matchesLeft=6;
//Set clock for countdown
time.setHMS(0,1,0);
//Initialize countdown
ui->countdown->setText(time.toString("m:ss"));
// Start timer with a value of 1000 milliseconds, indicating that it will time out every second.
timer->start(1000);
//Randomly sort tiles in container
randomize(tiles);
//Grab pairs of tiles and bind the name of an image file to each pair
bindTileImage(tiles, tile_image);
//enable tiles frame
ui->frame->setEnabled(true);
//enable every tile and reset its image
QList<QPushButton *> btns = ui->centralWidget->findChildren<QPushButton*>();
foreach (QPushButton* b, btns) {
b->setEnabled(true);
b->setStyleSheet("#" + b->objectName() + "{ }");
}
}
void MainWindow::updateCountdown(){
time=time.addSecs(-1);
ui->countdown->setText(time.toString("m:ss"));
}
void MainWindow::updateState(){
updateCountdown();
findFinalResult();
}
void MainWindow::randomize(QVector<QString> &tiles){
int a,b,min,max;
min = 0;
max = tiles.size()-1;
for(int i=0; i<tiles.size(); i++){
a=QRandomGenerator::global()->generate() % ((max + 1) - min) + min;
b=QRandomGenerator::global()->generate() % ((max + 1) - min) + min;
std::swap(tiles[a],tiles[b]);
}
}
void MainWindow::bindTileImage(QVector<QString> &tiles, QHash<QString, QString> &tile_image){
auto iter=tiles.begin();
for (int i=1; i<=6; i++){
QString file_name="0"+QString::number(i)+".png";
tile_image[(*iter)]=file_name;
iter++;
tile_image[(*iter)]=file_name;
iter++;
}
}
MainWindow::~MainWindow(){
delete ui;
}
And the mainwindow.ui file:
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>Memory game</string>
</property>
<property name="styleSheet">
<string notr="true">#centralWidget {
background-image: url(://background.png);
}
#howToPlay {
color: white;
}
#countdown {
color: white;
}
#scoring {
color: white;
}
#lblScore {
qproperty-alignment: AlignCenter;
color: white;
background: teal;
border: 3px solid silver;
border-radius: 7px;
}</string>
</property>
<widget class="QWidget" name="centralWidget">
<widget class="QLabel" name="howToPlay">
<property name="geometry">
<rect>
<x>160</x>
<y>40</y>
<width>471</width>
<height>31</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>14</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Click on two tiles and try to match the images</string>
</property>
</widget>
<widget class="QLabel" name="countdown">
<property name="geometry">
<rect>
<x>690</x>
<y>20</y>
<width>81</width>
<height>20</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>10</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>cronómetro</string>
</property>
</widget>
<widget class="QLabel" name="scoring">
<property name="geometry">
<rect>
<x>330</x>
<y>520</y>
<width>71</width>
<height>21</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Puntos:</string>
</property>
</widget>
<widget class="QLabel" name="lblScore">
<property name="geometry">
<rect>
<x>410</x>
<y>510</y>
<width>41</width>
<height>31</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>14</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>0</string>
</property>
</widget>
<widget class="QFrame" name="frame">
<property name="geometry">
<rect>
<x>70</x>
<y>80</y>
<width>661</width>
<height>431</height>
</rect>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<widget class="QPushButton" name="tile10">
<property name="geometry">
<rect>
<x>180</x>
<y>300</y>
<width>131</width>
<height>111</height>
</rect>
</property>
<property name="text">
<string/>
</property>
</widget>
<widget class="QPushButton" name="tile05">
<property name="geometry">
<rect>
<x>20</x>
<y>160</y>
<width>131</width>
<height>111</height>
</rect>
</property>
<property name="text">
<string/>
</property>
</widget>
<widget class="QPushButton" name="tile06">
<property name="geometry">
<rect>
<x>180</x>
<y>160</y>
<width>131</width>
<height>111</height>
</rect>
</property>
<property name="text">
<string/>
</property>
</widget>
<widget class="QPushButton" name="tile09">
<property name="geometry">
<rect>
<x>20</x>
<y>300</y>
<width>131</width>
<height>111</height>
</rect>
</property>
<property name="text">
<string/>
</property>
</widget>
<widget class="QPushButton" name="tile07">
<property name="geometry">
<rect>
<x>340</x>
<y>160</y>
<width>131</width>
<height>111</height>
</rect>
</property>
<property name="text">
<string/>
</property>
</widget>
<widget class="QPushButton" name="tile03">
<property name="geometry">
<rect>
<x>340</x>
<y>20</y>
<width>131</width>
<height>111</height>
</rect>
</property>
<property name="text">
<string/>
</property>
</widget>
<widget class="QPushButton" name="tile11">
<property name="geometry">
<rect>
<x>340</x>
<y>300</y>
<width>131</width>
<height>111</height>
</rect>
</property>
<property name="text">
<string/>
</property>
</widget>
<widget class="QPushButton" name="tile01">
<property name="geometry">
<rect>
<x>20</x>
<y>20</y>
<width>130</width>
<height>110</height>
</rect>
</property>
<property name="text">
<string/>
</property>
</widget>
<widget class="QPushButton" name="tile04">
<property name="geometry">
<rect>
<x>500</x>
<y>20</y>
<width>131</width>
<height>111</height>
</rect>
</property>
<property name="text">
<string/>
</property>
</widget>
<widget class="QPushButton" name="tile12">
<property name="geometry">
<rect>
<x>500</x>
<y>300</y>
<width>131</width>
<height>111</height>
</rect>
</property>
<property name="text">
<string/>
</property>
</widget>
<widget class="QPushButton" name="tile02">
<property name="geometry">
<rect>
<x>180</x>
<y>20</y>
<width>131</width>
<height>111</height>
</rect>
</property>
<property name="text">
<string/>
</property>
</widget>
<widget class="QPushButton" name="tile08">
<property name="geometry">
<rect>
<x>500</x>
<y>160</y>
<width>131</width>
<height>111</height>
</rect>
</property>
<property name="text">
<string/>
</property>
</widget>
</widget>
<zorder>frame</zorder>
<zorder>howToPlay</zorder>
<zorder>countdown</zorder>
<zorder>score</zorder>
<zorder>lblScore</zorder>
</widget>
<widget class="QToolBar" name="mainToolBar">
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
</widget>
<widget class="QStatusBar" name="statusBar"/>
</widget>
<layoutdefault spacing="6" margin="11"/>
<resources/>
<connections/>
</ui>
Please note I translated variable and slot names into English for better understanding. I could have missed something.
Thanks!!