
1. 项目概述从经典游戏到编程实践扫雷这个几乎存在于每一台Windows电脑上的经典游戏承载了无数人的童年记忆。它规则简单上手容易但背后隐藏的逻辑却相当精妙充满了策略与运气。今天我们不玩扫雷而是要亲手用C#把它“造”出来。这不仅仅是一个怀旧项目更是一个绝佳的编程练手机会它能让你系统地实践C#在Windows窗体应用开发中的核心技能包括事件驱动编程、二维数组操作、递归算法、自定义控件绘制以及游戏状态管理。对于C#初学者而言实现扫雷是一个里程碑式的挑战。它不像“Hello World”那样简单也不像大型商业软件那样复杂得无从下手。它恰到好处地涵盖了桌面应用开发的多个关键环节你需要设计一个交互界面UI处理鼠标点击事件实现核心的游戏逻辑如翻开格子、计算周围雷数、自动展开空白区域并最终判断游戏的胜负。通过这个项目你能将书本上抽象的“类”、“对象”、“委托”、“事件”等概念转化为一个个看得见、摸得着的功能。而对于有一定经验的开发者这同样是一个优化代码结构、实践设计模式如MVC/MVVM的简化思想的好场景。我们这次的目标就是抛开任何游戏引擎仅使用.NET Framework或.NET Core/5/6/7/8的Windows Forms从零开始构建一个功能完整、逻辑严谨的扫雷游戏。2. 核心设计与架构思路拆解在动手写代码之前理清思路至关重要。一个混乱的设计会让后续开发举步维艰。扫雷游戏的核心可以抽象为两个部分数据模型和视图交互。我们需要清晰地划分这两者的职责。2.1 数据模型游戏状态的灵魂游戏的所有状态都应该由一个纯粹的数据模型来管理它不关心界面如何显示只负责存储数据和提供核心逻辑方法。我们通常会定义一个MineField类来表示整个雷区。首先雷区本质上是一个二维网格。我们可以用一个二维数组Cell[,]来表示其中每个Cell是一个自定义的类或结构体封装一个格子的所有状态。一个格子需要哪些信息呢是否有雷布尔值IsMine。周围雷数整数AdjacentMinesCount。这个值在初始化布雷后就可以计算出来。当前状态枚举类型例如CellState包含Unopened未翻开、Opened已翻开、Flagged已标记为旗子、QuestionMark标记为问号。坐标位置记录其在网格中的行索引和列索引便于逻辑处理。MineField类的职责包括初始化根据用户选择的难度初级9x9/10雷中级16x16/40雷高级16x30/99雷创建二维数组。随机布雷使用Random类在网格中随机放置指定数量的地雷。这里有个关键点确保第一次点击的位置绝对不是雷这是扫雷游戏的通用规则能提升玩家体验。我们可以在玩家第一次点击后再根据点击位置动态生成雷区。计算周围雷数遍历每个非雷格子统计其周围8个格子中地雷的数量并存入AdjacentMinesCount。翻开格子这是核心逻辑。当玩家点击一个格子时如果该格子是雷游戏结束失败。如果该格子周围雷数为0即空白格子则需要自动展开所有相邻的空白格子直到被数字格子包围。这通常通过递归或队列迭代的算法实现。如果该格子是数字1-8则只翻开它自己。标记格子处理右键点击循环切换格子的标记状态未标记 - 旗子 - 问号 - 未标记。判断胜负胜利条件有两个1. 所有非雷格子均被翻开2. 所有地雷均被正确标记为旗子。需要提供方法实时检查。设计心得将数据模型与界面分离是至关重要的。MineField类不应该包含任何与UI绘制相关的代码如Graphics操作。它只提供数据和方法并通过事件如CellStateChanged来通知界面更新。这遵循了关注点分离原则使得代码更易于测试和维护。例如你可以单独为MineField编写单元测试而无需启动窗体。2.2 视图交互WinForms的舞台视图层负责将数据模型可视化并处理用户的输入。在WinForms中我们主要有两种实现方式使用DataGridView控件这是最快速的方式。DataGridView本身就是一个网格控件我们可以将其单元格类型设置为按钮或自定义绘制。通过处理CellClick事件来响应鼠标操作。优点是开发速度快自带网格管理缺点是自定义绘制自由度相对较低性能在极大网格时可能不是最优。自定义控件绘制继承Control或Panel在OnPaint方法中根据MineField的数据使用Graphics对象手动绘制每一个格子。通过重写OnMouseDown/OnMouseUp来处理鼠标事件计算点击的坐标对应哪个格子。这种方式拥有最高的自由度可以完全控制格子的外观、动画效果性能也更好是更专业的选择。在本项目中为了更深入地理解WinForms的绘图和事件机制我推荐并采用第二种方式自定义绘制。我们将创建一个MineFieldControl用户控件。视图层的关键任务渲染在OnPaint中遍历所有格子根据其CellState和AdjacentMinesCount绘制不同的外观未翻开的灰色方块、翻开的白色方块、数字、地雷、旗子、问号。输入处理在OnMouseDown中获取鼠标坐标换算成网格索引(row, col)。区分左键和右键。左键按下可以改变格子按下状态的外观例如凹陷效果在OnMouseUp时调用MineField.OpenCell(row, col)。右键单击调用MineField.ToggleFlag(row, col)。数据绑定让MineFieldControl持有一个MineField实例的引用。当MineField的状态发生变化通过事件通知时调用this.Invalidate()触发重绘更新界面。游戏状态显示在主窗体上还需要添加显示剩余雷数、用时计时器以及一个“重新开始”的笑脸按钮。3. 核心模块实现与关键技术点3.1 数据模型MineField类的详细实现让我们深入MineField类的代码细节。首先定义核心的枚举和结构。// 格子状态枚举 public enum CellState { Unopened, // 未翻开 Opened, // 已翻开 Flagged, // 已插旗 QuestionMark // 问号 } // 格子类 public class Cell { public bool IsMine { get; set; } public int AdjacentMinesCount { get; set; } public CellState State { get; set; } public int Row { get; } public int Col { get; } public Cell(int row, int col) { Row row; Col col; State CellState.Unopened; } } // 游戏难度预设 public class GameDifficulty { public int Rows { get; } public int Columns { get; } public int MineCount { get; } public static readonly GameDifficulty Beginner new GameDifficulty(9, 9, 10); public static readonly GameDifficulty Intermediate new GameDifficulty(16, 16, 40); public static readonly GameDifficulty Expert new GameDifficulty(16, 30, 99); private GameDifficulty(int rows, int columns, int mineCount) { Rows rows; Columns columns; MineCount mineCount; } }接下来是MineField类的骨架和关键方法public class MineField { public Cell[,] Grid { get; private set; } public int Rows { get; private set; } public int Columns { get; private set; } public int TotalMines { get; private set; } public int FlagsPlaced { get; private set; } public bool IsGameOver { get; private set; } public bool IsFirstClick { get; private set; } true; // 事件当格子状态改变时触发用于通知UI更新 public event ActionCell CellStateChanged; // 事件游戏结束时触发参数为是否胜利 public event Actionbool GameOver; public MineField(GameDifficulty difficulty) { Initialize(difficulty); } private void Initialize(GameDifficulty difficulty) { Rows difficulty.Rows; Columns difficulty.Columns; TotalMines difficulty.MineCount; FlagsPlaced 0; IsGameOver false; IsFirstClick true; Grid new Cell[Rows, Columns]; // 初始化所有格子对象 for (int r 0; r Rows; r) { for (int c 0; c Columns; c) { Grid[r, c] new Cell(r, c); } } // 注意此时先不布雷等待第一次点击 } // 在第一次点击的安全位置后布雷 private void PlaceMines(int safeRow, int safeCol) { Random rand new Random(); int minesPlaced 0; while (minesPlaced TotalMines) { int r rand.Next(Rows); int c rand.Next(Columns); // 确保随机位置不是安全点及其周围8格可选提升体验且不是雷 if ((Math.Abs(r - safeRow) 1 Math.Abs(c - safeCol) 1) || Grid[r, c].IsMine) continue; Grid[r, c].IsMine true; minesPlaced; } // 布雷完成后计算所有非雷格子周围的雷数 CalculateAdjacentMines(); } private void CalculateAdjacentMines() { // 定义8个方向向量 int[] dRow { -1, -1, -1, 0, 0, 1, 1, 1 }; int[] dCol { -1, 0, 1, -1, 1, -1, 0, 1 }; for (int r 0; r Rows; r) { for (int c 0; c Columns; c) { if (Grid[r, c].IsMine) continue; int count 0; for (int d 0; d 8; d) { int nr r dRow[d]; int nc c dCol[d]; if (nr 0 nr Rows nc 0 nc Columns Grid[nr, nc].IsMine) { count; } } Grid[r, c].AdjacentMinesCount count; } } } }核心方法翻开格子OpenCell这是游戏逻辑中最复杂也最精彩的部分。public void OpenCell(int row, int col) { // 游戏已结束或格子已翻开不处理 if (IsGameOver || Grid[row, col].State CellState.Opened) return; // 如果是第一次点击先布雷确保点击点不是雷 if (IsFirstClick) { IsFirstClick false; // 临时将点击点设为“安全区”在PlaceMines中排除 PlaceMines(row, col); // 确保点击点不是雷理论上PlaceMines已排除这里再加一道保险 Grid[row, col].IsMine false; // 需要重新计算周围雷数因为布雷后整个雷区变了 CalculateAdjacentMines(); } var cell Grid[row, col]; // 如果点中了雷 if (cell.IsMine) { cell.State CellState.Opened; OnCellStateChanged(cell); IsGameOver true; OnGameOver(false); // 游戏失败 return; } // 使用递归或栈/队列来实现空白区域的自动展开 OpenCellRecursive(row, col); // 每次操作后检查是否胜利 CheckForWin(); } private void OpenCellRecursive(int row, int col) { // 边界检查和状态检查 if (row 0 || row Rows || col 0 || col Columns) return; var cell Grid[row, col]; if (cell.State CellState.Opened || cell.State CellState.Flagged) return; // 翻开当前格子 cell.State CellState.Opened; OnCellStateChanged(cell); // 如果当前格子是空白周围雷数为0则递归翻开周围的8个格子 if (cell.AdjacentMinesCount 0) { // 递归展开周围格子 OpenCellRecursive(row - 1, col - 1); OpenCellRecursive(row - 1, col); OpenCellRecursive(row - 1, col 1); OpenCellRecursive(row, col - 1); OpenCellRecursive(row, col 1); OpenCellRecursive(row 1, col - 1); OpenCellRecursive(row 1, col); OpenCellRecursive(row 1, col 1); } // 如果是数字格子1-8递归到此停止 }踩坑记录递归深度与栈溢出在早期版本中我直接使用了深度递归OpenCellRecursive。这在初级难度9x9下没问题但在高级难度16x30下如果开局点击一个大片的空白区域递归深度可能达到数百层存在栈溢出的风险StackOverflowException。解决方案是使用显式的栈Stack或队列Queue进行迭代。将初始格子压栈然后循环处理栈中的格子将其周围符合条件的格子再压栈。这种方式完全避免了递归深度问题是更健壮的实现。这里为了代码清晰先展示递归版本但生产代码强烈建议使用迭代。标记格子与胜负判断public void ToggleFlag(int row, int col) { if (IsGameOver || Grid[row, col].State CellState.Opened) return; var cell Grid[row, col]; switch (cell.State) { case CellState.Unopened: cell.State CellState.Flagged; FlagsPlaced; break; case CellState.Flagged: cell.State CellState.QuestionMark; FlagsPlaced--; break; case CellState.QuestionMark: cell.State CellState.Unopened; break; } OnCellStateChanged(cell); } private void CheckForWin() { // 胜利条件所有非雷格子都已翻开 for (int r 0; r Rows; r) { for (int c 0; c Columns; c) { var cell Grid[r, c]; if (!cell.IsMine cell.State ! CellState.Opened) { return; // 发现未翻开的非雷格子游戏继续 } } } // 所有非雷格子都已翻开游戏胜利 IsGameOver true; // 可选自动标记所有地雷 for (int r 0; r Rows; r) { for (int c 0; c Columns; c) { var cell Grid[r, c]; if (cell.IsMine cell.State ! CellState.Flagged) { cell.State CellState.Flagged; OnCellStateChanged(cell); } } } OnGameOver(true); }3.2 自定义控件MineFieldControl的绘制与交互数据模型完成后我们需要一个控件来展示它。创建一个新的用户控件MineFieldControl。public partial class MineFieldControl : UserControl { private MineField _mineField; private int _cellSize 30; // 每个格子的像素大小 private Font _numberFont; // 颜色定义 private readonly Color _colorUnopened Color.FromArgb(192, 192, 192); private readonly Color _colorOpened Color.FromArgb(215, 215, 215); private readonly Color[] _numberColors new Color[] { Color.Transparent, // 0 Color.Blue, // 1 Color.Green, // 2 Color.Red, // 3 Color.DarkBlue, // 4 Color.DarkRed, // 5 Color.Teal, // 6 Color.Black, // 7 Color.Gray // 8 }; public MineField MineField { get _mineField; set { if (_mineField ! null) { _mineField.CellStateChanged - OnCellStateChanged; _mineField.GameOver - OnGameOver; } _mineField value; if (_mineField ! null) { _mineField.CellStateChanged OnCellStateChanged; _mineField.GameOver OnGameOver; // 根据雷区大小调整控件尺寸 this.Size new Size( _mineField.Columns * _cellSize, _mineField.Rows * _cellSize ); } this.Invalidate(); // 重绘 } } public MineFieldControl() { InitializeComponent(); DoubleBuffered true; // 启用双缓冲减少绘制闪烁 _numberFont new Font(Arial, 14, FontStyle.Bold); } // 将鼠标坐标转换为网格索引 private (int row, int col) PointToCell(Point point) { int row point.Y / _cellSize; int col point.X / _cellSize; // 确保索引在有效范围内 if (_mineField ! null row 0 row _mineField.Rows col 0 col _mineField.Columns) { return (row, col); } return (-1, -1); // 无效坐标 } }核心绘制方法OnPaint 在控件的OnPaint方法中我们遍历所有格子根据其状态进行绘制。protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); Graphics g e.Graphics; g.SmoothingMode System.Drawing.Drawing2D.SmoothingMode.AntiAlias; g.TextRenderingHint System.Drawing.Text.TextRenderingHint.ClearTypeGridFit; if (_mineField null) return; for (int r 0; r _mineField.Rows; r) { for (int c 0; c _mineField.Columns; c) { var cell _mineField.Grid[r, c]; Rectangle rect new Rectangle(c * _cellSize, r * _cellSize, _cellSize, _cellSize); DrawCell(g, cell, rect); } } // 绘制网格线 using (Pen gridPen new Pen(Color.Gray, 1)) { // 垂直线 for (int x 0; x _mineField.Columns; x) { g.DrawLine(gridPen, x * _cellSize, 0, x * _cellSize, _mineField.Rows * _cellSize); } // 水平线 for (int y 0; y _mineField.Rows; y) { g.DrawLine(gridPen, 0, y * _cellSize, _mineField.Columns * _cellSize, y * _cellSize); } } } private void DrawCell(Graphics g, Cell cell, Rectangle rect) { // 1. 绘制格子背景凸起或凹陷效果 bool isPressed false; // 可以根据需要记录鼠标按下状态 if (cell.State CellState.Unopened || cell.State CellState.QuestionMark) { // 未翻开状态绘制凸起效果 ControlPaint.DrawBorder3D(g, rect, Border3DStyle.Raised); using (Brush brush new SolidBrush(_colorUnopened)) { g.FillRectangle(brush, rect.X 2, rect.Y 2, rect.Width - 4, rect.Height - 4); } } else if (cell.State CellState.Opened) { // 翻开状态平坦背景 using (Brush brush new SolidBrush(_colorOpened)) { g.FillRectangle(brush, rect); } } else if (cell.State CellState.Flagged) { // 旗子状态凸起背景旗子图标 ControlPaint.DrawBorder3D(g, rect, Border3DStyle.Raised); using (Brush brush new SolidBrush(_colorUnopened)) { g.FillRectangle(brush, rect.X 2, rect.Y 2, rect.Width - 4, rect.Height - 4); } DrawFlag(g, rect); return; } // 2. 在已翻开的格子上绘制内容数字或雷 if (cell.State CellState.Opened) { if (cell.IsMine) { DrawMine(g, rect); } else if (cell.AdjacentMinesCount 0) { DrawNumber(g, rect, cell.AdjacentMinesCount); } // 如果AdjacentMinesCount 0就是空白格子不绘制任何内容 } // 3. 绘制问号标记在未翻开状态上叠加 if (cell.State CellState.QuestionMark) { using (StringFormat sf new StringFormat() { Alignment StringAlignment.Center, LineAlignment StringAlignment.Center }) using (Brush textBrush new SolidBrush(Color.Black)) { g.DrawString(?, this.Font, textBrush, rect, sf); } } } private void DrawFlag(Graphics g, Rectangle rect) { // 简化版旗子绘制一个红色矩形加一个杆 int flagSize _cellSize / 2; Point flagTopLeft new Point(rect.X rect.Width / 2 - flagSize / 2, rect.Y rect.Height / 4); Rectangle flagRect new Rectangle(flagTopLeft.X, flagTopLeft.Y, flagSize, flagSize / 2); using (Brush flagBrush new SolidBrush(Color.Red)) { g.FillRectangle(flagBrush, flagRect); } // 旗杆 using (Pen polePen new Pen(Color.Black, 2)) { int poleX rect.X rect.Width / 2; g.DrawLine(polePen, poleX, rect.Y rect.Height / 4, poleX, rect.Y rect.Height * 3 / 4); } } private void DrawMine(Graphics g, Rectangle rect) { // 绘制一个黑色的圆代表地雷 int mineRadius _cellSize / 3; Point center new Point(rect.X rect.Width / 2, rect.Y rect.Height / 2); Rectangle mineRect new Rectangle(center.X - mineRadius, center.Y - mineRadius, mineRadius * 2, mineRadius * 2); using (Brush mineBrush new SolidBrush(Color.Black)) { g.FillEllipse(mineBrush, mineRect); } // 可以添加一些高光或锯齿线让地雷更逼真 } private void DrawNumber(Graphics g, Rectangle rect, int number) { if (number 1 || number 8) return; using (StringFormat sf new StringFormat() { Alignment StringAlignment.Center, LineAlignment StringAlignment.Center }) using (Brush textBrush new SolidBrush(_numberColors[number])) { g.DrawString(number.ToString(), _numberFont, textBrush, rect, sf); } }鼠标事件处理 我们需要处理鼠标的按下和抬起事件以区分点击和实现按下效果。private (int pressedRow, int pressedCol) _mouseDownCell (-1, -1); protected override void OnMouseDown(MouseEventArgs e) { base.OnMouseDown(e); var (row, col) PointToCell(e.Location); if (row -1 || col -1) return; _mouseDownCell (row, col); // 可以在这里触发一个“格子被按下”的视觉反馈例如重绘该格子为凹陷状态 // 为了简化我们先不实现按下效果只记录位置 } protected override void OnMouseUp(MouseEventArgs e) { base.OnMouseUp(e); var (row, col) PointToCell(e.Location); if (row -1 || col -1 || _mouseDownCell ! (row, col)) { // 如果鼠标在格子外抬起或者抬起位置与按下位置不同视为无效点击 _mouseDownCell (-1, -1); return; } if (_mineField null || _mineField.IsGameOver) return; if (e.Button MouseButtons.Left) { _mineField.OpenCell(row, col); } else if (e.Button MouseButtons.Right) { _mineField.ToggleFlag(row, col); } _mouseDownCell (-1, -1); this.Invalidate(); // 操作后请求重绘 } // 响应数据模型的事件 private void OnCellStateChanged(Cell cell) { // 只重绘发生变化的格子优化性能 Rectangle invalidRect new Rectangle( cell.Col * _cellSize, cell.Row * _cellSize, _cellSize, _cellSize ); this.Invalidate(invalidRect); this.Update(); // 立即更新使反馈更及时 } private void OnGameOver(bool isWin) { // 游戏结束时可以显示对话框或改变主窗体按钮状态 // 这里我们触发一个自定义事件让主窗体处理 GameOver?.Invoke(this, new GameOverEventArgs(isWin)); } public event EventHandlerGameOverEventArgs GameOver;3.3 主窗体整合与游戏流程控制最后我们需要一个主窗体MainForm来承载游戏控件、计时器、计数器和新游戏按钮。public partial class MainForm : Form { private MineFieldControl _mineFieldControl; private Label _lblMineCount; private Label _lblTime; private Button _btnNewGame; private System.Windows.Forms.Timer _gameTimer; private int _elapsedSeconds; private GameDifficulty _currentDifficulty GameDifficulty.Beginner; public MainForm() { InitializeComponent(); SetupUI(); StartNewGame(); } private void SetupUI() { // 雷数显示 _lblMineCount new Label { Text 000, Font new Font(LCD, 20), ForeColor Color.Red, BackColor Color.Black, TextAlign ContentAlignment.MiddleCenter, Size new Size(60, 40) }; _lblMineCount.Location new Point(20, 20); this.Controls.Add(_lblMineCount); // 笑脸按钮重新开始 _btnNewGame new Button { Text , Font new Font(Segoe UI Emoji, 16), Size new Size(40, 40) }; _btnNewGame.Location new Point((this.ClientSize.Width - _btnNewGame.Width) / 2, 20); _btnNewGame.Click BtnNewGame_Click; this.Controls.Add(_btnNewGame); // 计时器显示 _lblTime new Label { Text 000, Font new Font(LCD, 20), ForeColor Color.Red, BackColor Color.Black, TextAlign ContentAlignment.MiddleCenter, Size new Size(60, 40) }; _lblTime.Location new Point(this.ClientSize.Width - 80, 20); this.Controls.Add(_lblTime); // 游戏区域 _mineFieldControl new MineFieldControl(); _mineFieldControl.Location new Point(20, 80); this.Controls.Add(_mineFieldControl); // 游戏计时器 _gameTimer new System.Windows.Forms.Timer { Interval 1000 }; // 1秒 _gameTimer.Tick GameTimer_Tick; // 调整窗体大小以适应游戏区域 this.ClientSize new Size( Math.Max(_mineFieldControl.Width 40, 300), _mineFieldControl.Height 120 ); } private void StartNewGame() { // 停止并重置计时器 _gameTimer.Stop(); _elapsedSeconds 0; _lblTime.Text 000; // 创建新的雷区 var newField new MineField(_currentDifficulty); _mineFieldControl.MineField newField; _mineFieldControl.GameOver MineFieldControl_GameOver; // 更新雷数显示 _lblMineCount.Text newField.TotalMines.ToString(D3); // 重置按钮表情 _btnNewGame.Text ; } private void BtnNewGame_Click(object sender, EventArgs e) { StartNewGame(); } private void GameTimer_Tick(object sender, EventArgs e) { _elapsedSeconds; if (_elapsedSeconds 999) _elapsedSeconds 999; // 最大显示999 _lblTime.Text _elapsedSeconds.ToString(D3); } private void MineFieldControl_GameOver(object sender, GameOverEventArgs e) { _gameTimer.Stop(); _btnNewGame.Text e.IsWin ? : ; string message e.IsWin ? 恭喜你扫雷成功 : 很遗憾你触雷了。; MessageBox.Show(message, 游戏结束, MessageBoxButtons.OK, MessageBoxIcon.Information); } // 可以添加菜单栏来选择难度 private void SetDifficulty(GameDifficulty difficulty) { _currentDifficulty difficulty; StartNewGame(); } } public class GameOverEventArgs : EventArgs { public bool IsWin { get; } public GameOverEventArgs(bool isWin) IsWin isWin; }4. 进阶优化与扩展思路一个基础版本的扫雷已经完成了。但要让它更完善、更专业我们还可以进行很多优化和扩展。4.1 性能与体验优化双缓冲与局部重绘我们已经为控件设置了DoubleBuffered true这能有效减少闪烁。更进一步在OnCellStateChanged事件中我们只重绘发生变化的单个格子 (Invalidate(Rectangle))而不是整个控件这能极大提升绘制效率尤其是在快速操作时。递归算法的迭代改造如前所述将OpenCellRecursive改为使用StackCell的迭代版本彻底杜绝栈溢出风险。private void OpenCellIterative(int startRow, int startCol) { StackCell cellsToOpen new StackCell(); cellsToOpen.Push(_grid[startRow, startCol]); while (cellsToOpen.Count 0) { var cell cellsToOpen.Pop(); if (cell.State CellState.Opened || cell.State CellState.Flagged) continue; if (cell.IsMine) continue; // 安全机制理论上不会执行 cell.State CellState.Opened; OnCellStateChanged(cell); // 如果是空白格子将其周围的未翻开非雷格子加入栈中 if (cell.AdjacentMinesCount 0) { for (int dr -1; dr 1; dr) { for (int dc -1; dc 1; dc) { if (dr 0 dc 0) continue; int nr cell.Row dr; int nc cell.Col dc; if (nr 0 nr Rows nc 0 nc Columns) { var neighbor _grid[nr, nc]; if (!neighbor.IsMine neighbor.State CellState.Unopened) { cellsToOpen.Push(neighbor); } } } } } } }音效与动画为点击、标记、游戏胜利/失败添加简单的音效。对于翻开格子可以添加一个简单的缩放或颜色渐变动画提升游戏反馈感。这可以通过Timer控制Graphics绘制的参数来实现。“和弦点击”功能这是官方扫雷的高级功能。当一个已翻开的数字格子其周围已标记的旗子数等于该数字时在此数字格子上同时点击鼠标左右键会自动翻开周围所有未标记的格子。这个功能能极大提升高手玩家的效率。实现方法是在OnMouseUp中判断是否是左右键同时按下e.Button MouseButtons.Left MouseButtons.Right或e.Button MouseButtons.Middle然后检查条件并执行批量翻开。4.2 功能扩展自定义难度除了初级、中级、高级可以增加一个“自定义”选项让用户自由设置行数、列数和雷数并验证其合理性例如雷数不能超过总格子数的一半。游戏记录与排行榜将最快通关时间分难度保存到本地文件或数据库并提供一个排行榜界面。撤销功能允许玩家撤销上一步操作。这需要维护一个操作历史栈记录每一步改变的格子状态。实现起来有一定复杂度但非常实用。换肤功能允许用户选择不同的格子样式、数字颜色和地雷图标。可以将绘制逻辑抽象为“皮肤”接口方便切换。求解器与提示实现一个简单的算法能够分析当前局面给出安全可点击的格子或提示可能埋雷的位置。这涉及到逻辑推理算法的实现是一个更大的挑战。4.3 架构演进引入MVVM模式对于希望练习更高级架构的开发者可以尝试将项目改造为MVVM模式。虽然WinForms不是MVVM的天然土壤但通过一些库如Microsoft.Xaml.Behaviors.WinForms的移植版或自行实现绑定是可以做到的。Model就是我们的MineField类保持不变。ViewModel创建一个GameViewModel它包含MineField实例并将格子的状态通过ObservableCollectionCellViewModel暴露出来。CellViewModel包装了Cell的数据并实现INotifyPropertyChanged。ViewMineFieldControl变为数据驱动通过数据绑定将CellViewModel的属性与UI元素的显示关联起来。命令绑定处理鼠标点击。 这样做的好处是业务逻辑与UI彻底解耦便于单元测试并且为未来迁移到WPF或MAUI等框架打下基础。从零实现一个扫雷游戏远不止是画几个格子那么简单。它贯穿了软件设计的核心思想数据与视图分离、事件驱动、算法应用递归/迭代、状态管理。每一个细节的打磨无论是确保第一次点击安全还是实现流畅的空白展开亦或是优化绘制性能都能让你对C#和桌面开发有更深刻的理解。当你最终看到自己编写的程序成功地运行起来那种亲手创造出一个经典游戏的成就感是任何教程都无法替代的。希望这个详细的实现指南能成为你C#学习路上一次扎实而有趣的实践。