Qt扫雷游戏

前言

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

开发思路

由于表格天然对应扫雷的格子,所以采用QAbstractTableModelQTableView来实现,并通过代理QStyledItemDelegate来自定义格子样式。

项目结构

image

代码展示

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());
}

源码地址

https://github.com/CodeCiWei/Minesweeper.git

posted @ 2026-05-08 11:15  初五二十一  阅读(17)  评论(0)    收藏  举报