Qt扫雷游戏
前言
本人目前正在学习Qt的开发,仅通过此游戏作为练手项目,在此记录,代码中仍有许多不足之处敬请谅解。
此项目主要使用到Qt中的Model/View架构。先展示一下最终效果图:

开发思路
由于表格天然对应扫雷的格子,所以采用QAbstractTableModel和QTableView来实现,并通过代理QStyledItemDelegate来自定义格子样式。
项目结构

代码展示
MinesweeperModel.h
#pragma once
#include <QAbstractTableModel>
class MinesweeperModel : public QAbstractTableModel
{
Q_OBJECT
public:
enum CellState {
Hidden,
Revealed,
Flagged
};
struct Cell {
bool hasMine; // 是否有地雷
int adjacentMines; // 周围地雷数
CellState state;
};
explicit MinesweeperModel(int rows, int columns, int mines = 10, QObject *parent = nullptr);
void configModel(int rows, int columns, int mines);
void init();
void revealCell(const QModelIndex& index); // 展开单元格
void toggleFlag(const QModelIndex& index); // 切换旗帜状态
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
int columnCount(const QModelIndex& parent = QModelIndex()) const override;
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
Qt::ItemFlags flags(const QModelIndex& index) const override;
signals:
void gameWin();
void gameOver();
void startGame();
private:
QVector<QVector<Cell>> m_grid;
int m_rows; // 行数
int m_columns; // 列数
int m_mines; // 地雷数
int m_revealedCount; // 排雷数
void placeMines();
};
MinesweeperModel.cpp
#include "MinesweeperModel.h"
#include <QRandomGenerator>
#include <algorithm>
MinesweeperModel::MinesweeperModel(int rows, int columns, int mines, QObject* parent)
:QAbstractTableModel(parent), m_rows(rows), m_columns(columns), m_mines(mines)
{
init();
}
void MinesweeperModel::configModel(int rows, int columns, int mines)
{
beginResetModel();
m_rows = rows;
m_columns = columns;
m_mines = mines;
init();
endResetModel();
}
void MinesweeperModel::init()
{
m_revealedCount = 0;
// 初始化网格大小
m_grid.resize(m_rows);
for (QVector<Cell>& row : m_grid)
{
row.resize(m_columns);
}
// 初始化所有格子
for (QVector<Cell>& row : m_grid)
{
for (Cell& cell : row)
{
cell.hasMine = false;
cell.adjacentMines = 0;
cell.state = Hidden;
}
}
// 随机放置地雷
placeMines();
//计算周围雷数
for (int r = 0; r < m_rows; ++r)
{
for (int c = 0; c < m_columns; ++c)
{
if (m_grid[r][c].hasMine) continue;
int count = 0;
for (int dr = -1; dr <= 1; ++dr)
{
for (int dc = -1; dc <= 1; ++dc)
{
if (dr == 0 && dc == 0) continue;
int nr = r + dr;
int nc = c + dc;
if (nr >= 0 && nr < m_rows &&
nc >= 0 && nc < m_columns &&
m_grid[nr][nc].hasMine)
{
++count;
}
}
}
m_grid[r][c].adjacentMines = count;
}
}
emit dataChanged(createIndex(0, 0), createIndex(m_rows - 1, m_columns - 1));
emit startGame();
}
void MinesweeperModel::revealCell(const QModelIndex& index)
{
if (!index.isValid()) return;
Cell &cell = m_grid[index.row()][index.column()];
if (cell.state == Revealed || cell.state == Flagged)
{
return;
}
cell.state = Revealed;
emit dataChanged(index, index);
if (cell.hasMine)
{
emit gameOver();
return;
}
else
{
m_revealedCount += 1;
}
// 没有数字,递归展开8邻域
if (cell.adjacentMines == 0)
{
int r = index.row();
int c = index.column();
for (int dr = -1; dr <= 1; ++dr)
{
for (int dc = -1; dc <= 1; ++dc)
{
if (dr == 0 && dc == 0) continue;
int nr = r + dr;
int nc = c + dc;
if (nr >= 0 && nr < m_rows &&
nc >= 0 && nc < m_columns)
{
QModelIndex neighbor = createIndex(r + dr, c + dc);
revealCell(neighbor);
}
}
}
}
// 判断是否胜利
if (m_revealedCount + m_mines == m_rows * m_columns)
{
emit gameWin();
// 将剩余未展开的Cell插上旗帜
for (int r = 0; r < m_rows; ++r)
{
for (int c = 0; c < m_columns; ++c)
{
if (m_grid[r][c].state == Hidden)
{
m_grid[r][c].state = Flagged;
}
}
}
emit dataChanged(createIndex(0, 0), createIndex(m_rows - 1, m_columns - 1));
}
}
void MinesweeperModel::toggleFlag(const QModelIndex& index)
{
if (!index.isValid()) return;
Cell &cell = m_grid[index.row()][index.column()];
if (cell.state == Revealed) return;
if (cell.state == Hidden)
{
cell.state = Flagged;
}
else if (cell.state == Flagged)
{
cell.state = Hidden;
}
emit dataChanged(index, index);
}
int MinesweeperModel::rowCount(const QModelIndex& parent) const
{
return m_rows;
}
int MinesweeperModel::columnCount(const QModelIndex& parent) const
{
return m_columns;
}
QVariant MinesweeperModel::data(const QModelIndex& index, int role) const
{
if (!index.isValid()) return QVariant();
const Cell &cell = m_grid[index.row()][index.column()];
if (role == Qt::UserRole)
{
return cell.state;
}
else if (role == Qt::UserRole + 1)
{
return cell.hasMine;
}
else if (role == Qt::UserRole + 2)
{
return cell.adjacentMines;
}
return QVariant();
}
Qt::ItemFlags MinesweeperModel::flags(const QModelIndex& index) const
{
if (!index.isValid()) return Qt::NoItemFlags;
// 可选中、可响应鼠标事件,但不支持编辑
return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
}
void MinesweeperModel::placeMines()
{
QVector<QPair<int, int>> cells;
for (int r = 0; r < m_rows; ++r)
{
for (int c = 0; c < m_columns; ++c)
{
cells.append({r, c});
}
}
// 随机排列
std::shuffle(cells.begin(), cells.end(), *QRandomGenerator::global());
// 放置地雷
for (int i = 0; i < m_mines; ++i)
{
auto [r,c] = cells[i];
m_grid[r][c].hasMine = true;
}
}
MinesweeperView.h
#pragma once
#include <QTableView>
class MinesweeperView : public QTableView
{
Q_OBJECT
public:
explicit MinesweeperView(QWidget* parent = nullptr);
protected:
void mousePressEvent(QMouseEvent* ev) override;
};
MinesweeperView.cpp
#include "MinesweeperView.h"
#include "MinesweeperModel.h"
#include <QMouseEvent>
#include <QHeaderView>
MinesweeperView::MinesweeperView(QWidget* parent)
:QTableView(parent)
{
setSelectionMode(NoSelection);
setEditTriggers(NoEditTriggers);
horizontalHeader()->hide();
verticalHeader()->hide();
horizontalHeader()->setSectionResizeMode(QHeaderView::Fixed);
verticalHeader()->setSectionResizeMode(QHeaderView::Fixed);
horizontalHeader()->setDefaultSectionSize(20);
verticalHeader()->setDefaultSectionSize(20);
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setShowGrid(false);
setIconSize(QSize(20, 20));
}
void MinesweeperView::mousePressEvent(QMouseEvent* ev)
{
QModelIndex index = indexAt(ev->pos());
if (!index.isValid()) return;
MinesweeperModel* m = qobject_cast<MinesweeperModel*>(model());
if (!m) return;
if (ev->button() == Qt::LeftButton)
{
m->revealCell(index);
}
else if (ev->button() == Qt::RightButton)
{
m->toggleFlag(index);
}
}
MinesweeperDelegate.h
#pragma once
#include <QStyledItemDelegate>
class MinesweeperDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
explicit MinesweeperDelegate(QObject* parent = nullptr);
void paint(QPainter* painter,
const QStyleOptionViewItem& option,
const QModelIndex& index) const override;
};
MinesweeperDelegate.cpp
#include "MinesweeperDelegate.h"
#include "MinesweeperModel.h"
#include <QPainter>
MinesweeperDelegate::MinesweeperDelegate(QObject* parent)
:QStyledItemDelegate(parent)
{
}
void MinesweeperDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const
{
MinesweeperModel::CellState state = (MinesweeperModel::CellState)index.data(Qt::UserRole).toInt();
bool hasMine = index.data(Qt::UserRole + 1).toBool();
int adj = index.data(Qt::UserRole + 2).toInt();
QColor hiddenColor(200, 200, 200);
QColor revealedColor(235, 235, 235);
QColor borderDark(120, 120, 120);
QColor borderLight(255, 255, 255);
QRect r = option.rect;
painter->setRenderHint(QPainter::Antialiasing);
painter->save();
if (state == MinesweeperModel::Hidden)
{
painter->fillRect(r, hiddenColor);
painter->setPen(borderLight);
painter->drawLine(r.topLeft(), r.topRight());
painter->drawLine(r.topLeft(), r.bottomLeft());
painter->setPen(borderDark);
painter->drawLine(r.topRight(), r.bottomRight());
painter->drawLine(r.bottomLeft(), r.bottomRight());
}
else if (state == MinesweeperModel::Revealed)
{
painter->fillRect(r, revealedColor);
painter->setPen(Qt::gray);
painter->drawRect(r.adjusted(0, 0, -1, -1));
}
else if (state == MinesweeperModel::Flagged)
{
painter->fillRect(r, hiddenColor);
painter->drawPixmap(r, QPixmap(":/icons/flag.png"));
}
painter->restore();
// draw mine
if (state == MinesweeperModel::Revealed && hasMine)
{
painter->drawPixmap(r, QPixmap(":/icons/mine.png"));
}
// draw number
if (state == MinesweeperModel::Revealed && adj > 0)
{
static const QColor numberColor[9] = {
Qt::transparent,
QColor(0,0,255), //1
QColor(0,128,0), //2
QColor(255,0,0), //3
QColor(0,0,128), //4
QColor(128,0,0), //5
QColor(0,128,128), //6
QColor(0,0,0), //7
QColor(128,128,128)//8
};
painter->setPen(numberColor[adj]);
painter->drawText(r, Qt::AlignCenter, QString::number(adj));
}
painter->restore();
}
MainButton.h
#pragma once
#include <QPushButton>
#include <QPixmap>
class MainButton : public QPushButton
{
Q_OBJECT
public:
enum State {
Playing,
Successed,
Failed
};
explicit MainButton(QWidget* parent = nullptr);
void setState(State state);
protected:
void paintEvent(QPaintEvent* ev) override;
private:
State m_state;
QPixmap pix_playing;
QPixmap pix_successed;
QPixmap pix_failed;
};
MainButton.cpp
#include "MainButton.h"
#include <QPainter>
MainButton::MainButton(QWidget* parent)
:QPushButton(parent)
{
resize(20, 20);
pix_playing.load(":/icons/playing.png");
pix_failed.load(":/icons/failed.png");
pix_successed.load(":/icons/successed.png");
setState(Playing);
}
void MainButton::setState(State state)
{
m_state = state;
update();
}
void MainButton::paintEvent(QPaintEvent* ev)
{
Q_UNUSED(ev);
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing);
const QPixmap* pix = nullptr;
switch (m_state)
{
case Playing: pix = &pix_playing; break;
case Failed: pix = &pix_failed; break;
case Successed: pix = &pix_successed; break;
}
p.drawPixmap(rect(), *pix);
}
Minesweeper.h
#pragma once
#include <QtWidgets/QMainWindow>
#include "MinesweeperModel.h"
#include "MinesweeperView.h"
#include "MinesweeperDelegate.h"
#include "MainButton.h"
#include <QTimer>
#include <QLabel>
class Minesweeper : public QMainWindow
{
Q_OBJECT
public:
enum Level {
Simple,
Moderate,
Difficulty
};
Minesweeper(QWidget *parent = nullptr);
~Minesweeper();
private:
QWidget* m_center;
MinesweeperModel* m_model;
MinesweeperView* m_view;
MinesweeperDelegate* m_delegate;
MainButton* m_button;
QLabel* m_timerLabel;
QTimer* m_timer;
int m_elapsedSeconds;
Level m_level;
QAction* act_simple;
QAction* act_moderate;
QAction* act_difficulty;
void initUI();
void setupSignals();
void adjustToFitSize();
};
Minesweeper.cpp
#include "Minesweeper.h"
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QHeaderView>
#include <QMenuBar>
Minesweeper::Minesweeper(QWidget *parent)
: QMainWindow(parent)
{
initUI();
}
Minesweeper::~Minesweeper()
{}
void Minesweeper::initUI()
{
// 设置菜单栏
QMenuBar* bar = menuBar();
QMenu* optionMenu = bar->addMenu("Option");
act_simple = new QAction("simple", this);
act_moderate = new QAction("moderate", this);
act_difficulty = new QAction("difficulty", this);
optionMenu->addAction(act_simple);
optionMenu->addAction(act_moderate);
optionMenu->addAction(act_difficulty);
m_center = new QWidget(this);
setCentralWidget(m_center);
// 主按钮和计时显示
m_button = new MainButton(this);
m_timerLabel = new QLabel("000", this);
m_timerLabel->setAlignment(Qt::AlignRight);
QFont font = m_timerLabel->font();
font.setBold(true);
font.setPointSize(14);
m_timerLabel->setFont(font);
m_timerLabel->setStyleSheet("color: red;");
QHBoxLayout* hLayout = new QHBoxLayout();
hLayout->addStretch(1);
hLayout->addWidget(m_button, 0);
hLayout->addWidget(m_timerLabel, 1);
hLayout->setContentsMargins(0, 0, 0, 0);
hLayout->setSpacing(0);
// 定时器
m_timer = new QTimer(this);
m_elapsedSeconds = 0;
m_timer->start(1000);
// view
m_model = new MinesweeperModel(9, 9, 10, m_center);
m_delegate = new MinesweeperDelegate(this);
m_view = new MinesweeperView(this);
m_view->setModel(m_model);
m_view->setItemDelegate(m_delegate);
QVBoxLayout* vLayout = new QVBoxLayout();
vLayout->addLayout(hLayout);
vLayout->addWidget(m_view);
vLayout->setContentsMargins(0, 0, 0, 0);
vLayout->setSpacing(0);
m_center->setLayout(vLayout);
// 设置信号槽
setupSignals();
// 调整view和窗口大小
adjustToFitSize();
}
void Minesweeper::setupSignals()
{
connect(m_model, &MinesweeperModel::startGame, this, [this]() {
m_button->setState(MainButton::Playing);
m_view->setEnabled(true);
m_elapsedSeconds = 0;
m_timerLabel->setText("000");
m_timer->start(1000);
});
connect(m_model, &MinesweeperModel::gameWin, this, [this]() {
m_button->setState(MainButton::Successed);
m_view->setEnabled(false);
m_timer->stop();
});
connect(m_model, &MinesweeperModel::gameOver, this, [this]() {
m_button->setState(MainButton::Failed);
m_view->setEnabled(false);
m_timer->stop();
});
connect(m_button, &MainButton::clicked, this, [this]() {
m_model->init();
});
connect(act_simple, &QAction::triggered, this, [this]() {
m_model->configModel(9, 9, 10);
adjustToFitSize();
});
connect(act_moderate, &QAction::triggered, this, [this]() {
m_model->configModel(15, 15, 30);
adjustToFitSize();
});
connect(act_difficulty, &QAction::triggered, this, [this]() {
m_model->configModel(30, 30, 100);
adjustToFitSize();
});
connect(m_timer, &QTimer::timeout, this, [this]() {
++m_elapsedSeconds;
m_timerLabel->setText(QString("%1").arg(m_elapsedSeconds, 3, 10, QChar('0')));
});
}
void Minesweeper::adjustToFitSize()
{
int w = m_model->columnCount(QModelIndex()) * 20;
int h = m_model->rowCount(QModelIndex()) * 20 + menuBar()->height();
m_view->resize(w, h);
setFixedSize(m_view->width(), m_view->height() + m_button->height());
}

浙公网安备 33010602011771号