用 C++ 和 Win32 从零写一个扫雷:从经典灰色到 Windows 7 蓝色动画
一次完整的「小游戏开发」复盘:零第三方依赖,纯 C++ + Win32 GDI,从能玩,到好看,再到会动。
为什么要写这篇
扫雷看似简单,却是练手 GUI 编程的绝佳项目:它同时涉及游戏逻辑(布雷、洪泛展开、胜负判定)、界面渲染(双缓冲、渐变、七段数码管)和动画系统(翻开波浪、爆炸效果)。
本文记录我用纯 C++(Win32 API + GDI,不依赖 Qt/SDL 等任何第三方库)实现一个扫雷游戏的完整过程,包括架构拆分、两种美术风格的重做,以及几个真实踩过的坑。
一、技术选型:为什么是 Win32
目标很明确:一个可双击运行的 exe,不依赖任何运行库。
几个常见方案对比:
| 方案 | 优点 | 缺点 |
|---|---|---|
| Qt | 组件丰富、跨平台 | 体积大、需带一堆 DLL |
| SDL / SFML | 游戏向、好用 | 仍需额外库 |
| Win32 + GDI | 系统自带、零依赖、单 exe | 代码相对啰嗦,全部手绘 |
最终选 Win32:一个 .cpp 文件、一行 cl 命令就能出 exe。所有界面元素——格子、数字、旗帜、地雷、计数器、笑脸——全部用 GDI 一笔一画地画出来,没有用任何现成控件。
二、整体架构:从单文件到四模块
第一版是一个 700 行的单文件。虽然能跑,但逻辑、渲染、窗口消息全挤在一起,维护困难。于是按职责拆成了四个模块:
minesweeper/
├── game.h / game.cpp # 游戏核心逻辑(纯逻辑,不碰任何窗口/绘图)
├── render.h / render.cpp # 全部 GDI 绘制 + 动画
├── dialogs.h / dialogs.cpp # 「自定义难度」对话框
├── app.cpp # WinMain、窗口、消息循环、菜单、定时器
└── build.bat # 一行 cl 编译
关键设计:逻辑与渲染彻底解耦。
Game类封装了棋盘、行列数、雷数、计时、胜负状态,以及动画时间戳,它不知道「像素」「颜色」「HDC」为何物。render.cpp通过const Game&只读地访问游戏状态来绘制,RenderInit/RenderCleanup负责 GDI 资源生命周期。app.cpp负责粘合:把鼠标消息翻译成game.Reveal()、game.ToggleFlag()等调用,再触发重绘。
// game.h —— 纯逻辑,不 include <windows.h>
class Game {
public:
void NewGame(int rows, int cols, int mines);
void Reveal(int r, int c);
void Chord(int r, int c);
void ToggleFlag(int r, int c);
void Tick();
const std::vector<std::vector<Cell>>& Board() const;
GameState State() const;
// ...
};
三、核心游戏逻辑
1. 首次点击保证安全
经典扫雷的体验底线:第一下不能踩雷。做法是延迟布雷——第一次翻开时才生成地雷,且避开点击处及其周围 8 格:
void Game::PlaceMines(int safeR, int safeC) {
int placed = 0;
while (placed < m_mines) {
int r = rand() % m_rows, c = rand() % m_cols;
if (m_board[r][c].mine) continue;
if (abs(r - safeR) <= 1 && abs(c - safeC) <= 1) continue; // 安全区
m_board[r][c].mine = true;
++placed;
}
// 再遍历一遍,计算每个格子的相邻雷数 adj
}
2. 洪泛展开(Flood Fill)
点开一个「0」格子时,要自动向四周扩散,直到被数字包围。用显式栈代替递归,避免深递归爆栈:
std::vector<std::pair<int,int>> stack;
stack.push_back({r, c});
while (!stack.empty()) {
auto [rr, cc] = stack.back();
stack.pop_back();
Cell& cur = m_board[rr][cc];
if (cur.revealed || cur.flagged) continue;
cur.revealed = true;
if (cur.adj == 0) {
// 把周围 8 格压栈继续展开
for (int dr = -1; dr <= 1; ++dr)
for (int dc = -1; dc <= 1; ++dc) { /* ... */ }
}
}
3. 左右键同按(Chord)
在已翻开的数字上同时按下左右键,如果周围旗子数等于该数字,就自动翻开周围所有未标记的格子——扫雷老玩家必备的提速操作。
4. 胜负判定
- 胜利:所有非地雷格子都被翻开。
- 失败:踩雷后翻开所有地雷,标错的旗子打红叉,踩中的那颗标红。
四、界面渲染:全部手绘
双缓冲,杜绝闪烁
每次重绘先画到内存位图,再一次性贴到屏幕:
void RenderClient(HDC hdc, const Game& game, bool smileDown, const RECT& client) {
int w = client.right - client.left, h = client.bottom - client.top;
HDC mem = CreateCompatibleDC(hdc);
HBITMAP bmp = CreateCompatibleBitmap(hdc, w, h);
HBITMAP oldb = (HBITMAP)SelectObject(mem, bmp);
DrawGame(mem, client, game, smileDown); // 全部画在内存 DC 上
BitBlt(hdc, 0, 0, w, h, mem, 0, 0, SRCCOPY); // 一次性贴出
SelectObject(mem, oldb);
DeleteObject(bmp);
DeleteDC(mem);
}
七段数码管计数器
经典的「剩余雷数 / 计时」是红色 LED 七段数码管。不用字体,直接按位图逐段画:
enum Seg { SEG_A=1<<0, SEG_B=1<<1, SEG_C=1<<2, SEG_D=1<<3,
SEG_E=1<<4, SEG_F=1<<5, SEG_G=1<<6 };
// 0~9 各自的点亮段位
const BYTE g_segDigits[10] = {
0b0111111, // 0
0b0000110, // 1
0b1011011, // 2
// ...
};
void DrawDigit7(HDC hdc, int x, int y, int w, int h, int digit) {
BYTE m = g_segDigits[digit];
if (m & SEG_A) SegRect(hdc, x + t, y, w - 2*t, t); // 上横
if (m & SEG_F) SegRect(hdc, x, y + t, t, mid - t); // 左上竖
// ... 七段各自判断
}
两种美术风格的演进
这其实是我最喜欢的一段经历——同一套逻辑,两套皮肤:
版本 A:经典 Windows 98/XP 风格(灰色 + 3D 凹凸边框)
未翻开格子用四级 3D 立体边框,逐像素验证过层次:
255(白高光) → 223(浅灰) → 192(灰面) → 64(深阴影) → 128(阴影) → 下一格…
版本 B:Windows 7 蓝色 Aero 风格(渐变 + 光泽 + 动画)
改用 GradientFill 画垂直渐变,格子像磨砂玻璃按钮:
void FillGradientV(HDC hdc, const RECT& rc, COLORREF top, COLORREF bottom) {
TRIVERTEX v[2];
v[0].x = rc.left; v[0].y = rc.top;
v[0].Red = GetRValue(top) << 8; // 颜色分量左移 8 位
v[0].Green = GetGValue(top) << 8;
v[0].Blue = GetBValue(top) << 8;
v[1].x = rc.right; v[1].y = rc.bottom;
// ... 同理填 v[1]
GRADIENT_RECT gr = { 0, 1 };
GradientFill(hdc, v, 2, &gr, 1, GRADIENT_FILL_RECT_V);
}
注意:
GradientFill位于msimg32.lib,链接时要带上。
五、动画系统:让格子「活」起来
Windows 7 版最打动人心的,是那些细腻的动画。核心思路很朴素:
给每个格子存一个「动画时间戳」,渲染时拿当前时间相减算出「动画进度」,据此决定画多大、画不画。
struct Cell {
// ...
unsigned revealStamp = 0; // 翻开动画起点
unsigned flagStamp = 0; // 插旗动画起点
unsigned mineStamp = 0; // 地雷爆炸动画起点
};
翻开波浪(扩散波纹)
这是最出彩的效果:点击后,格子从点击点向外逐个弹出,而不是整片瞬间消失。
实现只用了一行数学——曼哈顿距离:
unsigned now = GetTickCount();
// 洪泛展开时,延迟 = 到点击点的曼哈顿距离 × 每格延迟
cur.revealStamp = now + (abs(rr - r) + abs(cc - c)) * 10; // ms
渲染时:
int age = (int)(now - cell.revealStamp);
if (age < 0) { DrawHiddenCell(...); return; } // 波浪还没到,仍显示未翻开
float s = age >= 160 ? 1.0f : 0.55f + 0.45f * age / 160; // 从 55% 放大到 100%
DrawRevealed(..., s); // 按比例缩放绘制
同理:
- 插旗弹出:旗子从 60% 放大到 100%。
- 地雷爆炸:踩雷后所有雷从踩中点依次向外弹出(用同样的曼哈顿距离延迟)。
- 胜利插旗:获胜时逐格插上旗子,级联推进。
16ms 定时器驱动
动画需要持续重绘,但空闲时又不能白烧 CPU。做法是始终挂一个 16ms 定时器,但只在「有动画进行」时才重绘:
case WM_TIMER:
if (wParam == TIMER_ANIM) {
if (RenderHasActiveAnimation(g_game))
InvalidateRect(hwnd, nullptr, FALSE); // 有动画才刷新
}
bool RenderHasActiveAnimation(const Game& game) {
unsigned now = GetTickCount();
for (const auto& row : game.Board())
for (const Cell& cell : row) {
if (cell.revealStamp && (int)(now - cell.revealStamp) < 200) return true;
if (cell.flagStamp && (int)(now - cell.flagStamp) < 200) return true;
if (cell.mineStamp && (int)(now - cell.mineStamp) < 200) return true;
}
return false;
}
六、踩过的坑(真实翻车记录)
1. 闪退:棋盘在第一次绘制前是空的
现象:exe 打开一瞬间就闪退。
原因:全局棋盘 g_board 默认是空 vector,而程序启动时只设置了行列数,从没调用 NewGame() 填充棋盘。ShowWindow + UpdateWindow 立刻触发首次 WM_PAINT,按 9×9 循环访问 g_board[r][c] → 越界访问 → 崩溃。
修复:显示窗口前先初始化棋盘(或让构造函数直接填充)。
教训:GUI 程序的初始化顺序要格外小心——第一帧渲染可能在你想不到的时刻发生。
2. 中文注释导致编译失败
现象:源码里写了中文注释和字符串,编译报一堆 C2001、C2864 错误。
原因:源码是 UTF-8,但 MSVC 默认按本机代码页(中文系统是 GBK/936)解析,多字节被错误拆分,进而引发级联的括号/字符串错乱。
修复:给 cl 加 /utf-8,明确告诉编译器「源码是 UTF-8」。
3. 拆分后 UNICODE 宏不一致
现象:把单文件拆成多文件后,render.cpp/dialogs.cpp 报 DrawTextA 无法接收宽字符串。
原因:#define UNICODE 原本只在单文件顶部,拆开后只有 app.cpp 有,其它文件编译成了 ANSI 版本,导致 DrawTextA(窄字符)和 L"..."(宽字符)不匹配。
修复:把 UNICODE/_UNICODE 放到编译参数统一传(/DUNICODE /D_UNICODE),而不是散落在某个源文件里。多文件工程里,全局宏一定要全局定义。
4. 内存对话框模板的对齐陷阱
「自定义难度」对话框不写 .rc 资源文件,而是在内存里手拼 DLGTEMPLATE 结构。
最阴的坑:sizeof(DLGITEMTEMPLATE) 因为结构体尾部对齐会返回 20 字节,但真正的模板格式只要 18 字节。如果直接 memcpy(sizeof(...)),会多写 2 字节把后面的数据顶坏。必须逐字段写入,不能整体拷贝:
// 正确做法:逐字段写
AppendDword(b, style);
AppendDword(b, 0); // exStyle
AppendWord(b, (WORD)x); // x
AppendWord(b, (WORD)y); // y
// ... 而不是 memcpy(&item, sizeof(item))
七、小结
这个项目虽然小,但把 GUI 编程的「完整闭环」都走了一遍:
| 环节 | 收获 |
|---|---|
| 逻辑设计 | 状态机 + 洪泛展开 + 首次点击安全 |
| 架构拆分 | 逻辑 / 渲染 / 对话框 / 入口 四层解耦 |
| 渲染 | 双缓冲、渐变、七段数码管、纯 GDI 手绘 |
| 动画 | 时间戳 + 曼哈顿距离延迟 + 定时器按需刷新 |
| 工程化 | 编码、宏、内存布局对齐这些「看不见」的坑 |
完整的可运行代码和编译说明都在仓库里(game.cpp / render.cpp / dialogs.cpp / app.cpp + build.bat),一行命令即可编译:
cl /O2 /std:c++17 /EHsc /utf-8 /DUNICODE /D_UNICODE ^
game.cpp render.cpp dialogs.cpp app.cpp ^
/Fe:minesweeper.exe user32.lib gdi32.lib msimg32.lib
如果你也想动手写一个,我的建议是:先把「能玩」做出来,再逐步加皮肤和动画——逻辑稳定后,换皮肤只是替换 render.cpp 的事。这大概也是「关注点分离」最有说服力的一个小例子。
文中所有代码均为项目真实实现,可在源码中对应查看。
附录:完整源码
项目全部源码,按模块列出;编译命令见正文「小结」一节。
game.h
// game.h — 扫雷核心游戏逻辑(与界面无关)
#pragma once
#include <vector>
enum class GameState { PLAYING, WON, LOST };
struct Cell {
bool mine = false;
bool revealed = false;
bool flagged = false;
int adj = 0; // 周围地雷数
// 动画时间戳(毫秒,0 表示无动画;由 GetTickCount 设置)
unsigned revealStamp = 0; // 翻开弹出动画起点
unsigned flagStamp = 0; // 插旗动画起点
unsigned mineStamp = 0; // 地雷弹出动画起点
};
struct Difficulty {
int rows, cols, mines;
const wchar_t* name;
};
class Game {
public:
Game();
// 开局 / 重开
void NewGame(int rows, int cols, int mines);
void Restart();
// 玩家操作
void Reveal(int r, int c);
void Chord(int r, int c);
void ToggleFlag(int r, int c);
void Tick(); // 计时器走一秒
// 状态查询
const std::vector<std::vector<Cell>>& Board() const { return m_board; }
int Rows() const { return m_rows; }
int Cols() const { return m_cols; }
int Mines() const { return m_mines; }
GameState State() const { return m_state; }
int Time() const { return m_time; }
int DeadRow() const { return m_deadR; }
int DeadCol() const { return m_deadC; }
bool IsRevealed(int r, int c) const;
int FlagCount() const;
private:
void PlaceMines(int safeR, int safeC);
void Lose(int r, int c);
void Win();
bool CheckWin() const;
std::vector<std::vector<Cell>> m_board;
int m_rows, m_cols, m_mines;
GameState m_state;
bool m_minesPlaced;
int m_time;
int m_deadR, m_deadC;
};
game.cpp
// game.cpp — 扫雷核心游戏逻辑实现
#include "game.h"
#include <windows.h> // GetTickCount(动画时间戳)
#include <cstdlib>
#include <utility>
namespace {
constexpr unsigned kWaveStagger = 10; // 翻开波浪:每格距离延迟(ms)
constexpr unsigned kMineStagger = 35; // 失败时地雷依次弹出延迟(ms)
constexpr unsigned kWinStagger = 30; // 胜利插旗依次延迟(ms)
}
Game::Game()
: m_rows(9), m_cols(9), m_mines(10),
m_state(GameState::PLAYING), m_minesPlaced(false),
m_time(0), m_deadR(-1), m_deadC(-1) {
m_board.assign(m_rows, std::vector<Cell>(m_cols));
}
void Game::NewGame(int rows, int cols, int mines) {
m_rows = rows;
m_cols = cols;
m_mines = mines;
Restart();
}
void Game::Restart() {
m_board.assign(m_rows, std::vector<Cell>(m_cols));
m_state = GameState::PLAYING;
m_minesPlaced = false;
m_time = 0;
m_deadR = m_deadC = -1;
}
void Game::PlaceMines(int safeR, int safeC) {
int placed = 0;
while (placed < m_mines) {
int r = std::rand() % m_rows;
int c = std::rand() % m_cols;
if (m_board[r][c].mine) continue;
// 首次点击处及其周围 8 格不放雷,保证开局安全
if (std::abs(r - safeR) <= 1 && std::abs(c - safeC) <= 1) continue;
m_board[r][c].mine = true;
++placed;
}
for (int r = 0; r < m_rows; ++r)
for (int c = 0; c < m_cols; ++c) {
int n = 0;
for (int dr = -1; dr <= 1; ++dr)
for (int dc = -1; dc <= 1; ++dc) {
int nr = r + dr, nc = c + dc;
if (nr < 0 || nr >= m_rows || nc < 0 || nc >= m_cols) continue;
if (m_board[nr][nc].mine) ++n;
}
m_board[r][c].adj = n;
}
}
void Game::Lose(int r, int c) {
m_state = GameState::LOST;
m_deadR = r;
m_deadC = c;
unsigned now = GetTickCount();
m_board[r][c].revealed = true;
m_board[r][c].mineStamp = now; // 踩中的雷最先弹出
for (int rr = 0; rr < m_rows; ++rr)
for (int cc = 0; cc < m_cols; ++cc)
if (m_board[rr][cc].mine && !m_board[rr][cc].flagged) {
m_board[rr][cc].revealed = true; // 翻开其余未被标记的雷
m_board[rr][cc].mineStamp = now + (unsigned)((std::abs(rr - r) + std::abs(cc - c)) * kMineStagger);
}
}
bool Game::CheckWin() const {
for (int r = 0; r < m_rows; ++r)
for (int c = 0; c < m_cols; ++c)
if (!m_board[r][c].mine && !m_board[r][c].revealed)
return false;
return true;
}
void Game::Win() {
m_state = GameState::WON;
unsigned now = GetTickCount();
int seq = 0;
for (int r = 0; r < m_rows; ++r)
for (int c = 0; c < m_cols; ++c)
if (m_board[r][c].mine) {
m_board[r][c].flagged = true; // 胜利自动插满旗
m_board[r][c].flagStamp = now + (unsigned)(seq * kWinStagger);
++seq;
}
}
void Game::Reveal(int r, int c) {
if (m_state == GameState::LOST || m_state == GameState::WON) return;
if (r < 0 || r >= m_rows || c < 0 || c >= m_cols) return;
Cell& cell = m_board[r][c];
if (cell.flagged || cell.revealed) return;
if (!m_minesPlaced) {
PlaceMines(r, c);
m_minesPlaced = true;
}
if (cell.mine) { Lose(r, c); return; }
// 洪泛展开(按曼哈顿距离设置波浪延迟,形成从点击点扩散的动画)
unsigned now = GetTickCount();
std::vector<std::pair<int, int>> stack;
stack.push_back({r, c});
while (!stack.empty()) {
auto [rr, cc] = stack.back();
stack.pop_back();
Cell& cur = m_board[rr][cc];
if (cur.revealed || cur.flagged) continue;
cur.revealed = true;
cur.revealStamp = now + (unsigned)((std::abs(rr - r) + std::abs(cc - c)) * kWaveStagger);
if (cur.adj == 0) {
for (int dr = -1; dr <= 1; ++dr)
for (int dc = -1; dc <= 1; ++dc) {
int nr = rr + dr, nc = cc + dc;
if (nr < 0 || nr >= m_rows || nc < 0 || nc >= m_cols) continue;
Cell& nb = m_board[nr][nc];
if (!nb.revealed && !nb.flagged) stack.push_back({nr, nc});
}
}
}
if (CheckWin()) Win();
}
void Game::Chord(int r, int c) {
if (m_state != GameState::PLAYING) return;
if (r < 0 || r >= m_rows || c < 0 || c >= m_cols) return;
if (!m_board[r][c].revealed) return;
int flags = 0;
for (int dr = -1; dr <= 1; ++dr)
for (int dc = -1; dc <= 1; ++dc) {
int nr = r + dr, nc = c + dc;
if (nr < 0 || nr >= m_rows || nc < 0 || nc >= m_cols) continue;
if (m_board[nr][nc].flagged) ++flags;
}
if (flags != m_board[r][c].adj) return;
for (int dr = -1; dr <= 1; ++dr)
for (int dc = -1; dc <= 1; ++dc) {
int nr = r + dr, nc = c + dc;
if (nr < 0 || nr >= m_rows || nc < 0 || nc >= m_cols) continue;
if (!m_board[nr][nc].revealed && !m_board[nr][nc].flagged)
Reveal(nr, nc);
}
}
void Game::ToggleFlag(int r, int c) {
if (m_state != GameState::PLAYING) return;
if (r < 0 || r >= m_rows || c < 0 || c >= m_cols) return;
Cell& cell = m_board[r][c];
if (cell.revealed) return;
cell.flagged = !cell.flagged;
if (cell.flagged) cell.flagStamp = GetTickCount(); // 插旗弹出动画
else cell.flagStamp = 0;
}
void Game::Tick() {
if (m_state == GameState::PLAYING && m_time < 999)
++m_time;
}
bool Game::IsRevealed(int r, int c) const {
return r >= 0 && r < m_rows && c >= 0 && c < m_cols && m_board[r][c].revealed;
}
int Game::FlagCount() const {
int n = 0;
for (int r = 0; r < m_rows; ++r)
for (int c = 0; c < m_cols; ++c)
if (m_board[r][c].flagged) ++n;
return n;
}
render.h
// render.h — 棋盘绘制(GDI)
#pragma once
#include <windows.h>
#include "game.h"
// 布局常量(像素)
constexpr int CELL = 30;
constexpr int HEADER_H = 62;
constexpr int PAD = 10;
constexpr int GAP = 8;
// GDI 资源生命周期(窗口创建/销毁时调用)
void RenderInit();
void RenderCleanup();
// 布局辅助
void ComputeClientSize(int rows, int cols, int& width, int& height);
RECT CellRect(int r, int c);
RECT HeaderRect(int cols);
RECT SmileyRect(int cols);
bool BoardHit(POINT pt, int rows, int cols, int& r, int& c);
// 主绘制入口(含双缓冲)
void RenderClient(HDC hdc, const Game& game, bool smileDown, const RECT& client);
// 是否还有动画进行中(用于驱动动画重绘)
bool RenderHasActiveAnimation(const Game& game);
render.cpp
// render.cpp — 棋盘绘制实现(Windows 7 蓝色 Aero 风格 + 动画)
#include "render.h"
#include <cmath>
namespace {
// ---- 配色(Windows 7 蓝色风格)----
const COLORREF kColBgTop = RGB(206, 228, 248); // 棋盘背景渐变(上)
const COLORREF kColBgBot = RGB(147, 190, 230); // 棋盘背景渐变(下)
const COLORREF kColCellTop = RGB(255, 255, 255); // 未翻开格子(上, 高光)
const COLORREF kColCellBot = RGB(199, 221, 244); // 未翻开格子(下)
const COLORREF kColOpenTop = RGB(219, 233, 250); // 翻开格子(上)
const COLORREF kColOpenBot = RGB(199, 221, 244); // 翻开格子(下)
const COLORREF kColPanelTop = RGB(232, 242, 253); // 顶部面板(上)
const COLORREF kColPanelBot = RGB(181, 210, 238); // 顶部面板(下)
const COLORREF kColBorder = RGB(141, 174, 208); // 格子边框
// ---- 动画时长(ms) ----
constexpr unsigned kPopMs = 160; // 翻开/地雷弹出
constexpr unsigned kFlagPopMs = 120; // 插旗弹出
constexpr unsigned kAnimWin = 200; // 判定动画是否结束的窗口
HBRUSH g_brushBlack, g_brushRed, g_brushYellow, g_brushWhite, g_brushBorder;
HPEN g_penBlack, g_penRed;
HFONT g_fontNum;
// ---- 渐变 / 缩放辅助 ----
void FillGradientV(HDC hdc, const RECT& rc, COLORREF top, COLORREF bottom) {
TRIVERTEX v[2];
v[0].x = rc.left; v[0].y = rc.top;
v[0].Red = GetRValue(top) << 8;
v[0].Green = GetGValue(top) << 8;
v[0].Blue = GetBValue(top) << 8;
v[0].Alpha = 0x0000;
v[1].x = rc.right; v[1].y = rc.bottom;
v[1].Red = GetRValue(bottom) << 8;
v[1].Green = GetGValue(bottom) << 8;
v[1].Blue = GetBValue(bottom) << 8;
v[1].Alpha = 0x0000;
GRADIENT_RECT gr = { 0, 1 };
GradientFill(hdc, v, 2, &gr, 1, GRADIENT_FILL_RECT_V);
}
RECT ScaleRect(const RECT& rc, float s) {
int dw = (int)((rc.right - rc.left) * (1.0f - s) * 0.5f);
int dh = (int)((rc.bottom - rc.top) * (1.0f - s) * 0.5f);
RECT r;
r.left = rc.left + dw;
r.top = rc.top + dh;
r.right = rc.right - dw;
r.bottom = rc.bottom - dh;
return r;
}
// ---- 七段数码管 ----
enum Seg {
SEG_A = 1 << 0, SEG_B = 1 << 1, SEG_C = 1 << 2, SEG_D = 1 << 3,
SEG_E = 1 << 4, SEG_F = 1 << 5, SEG_G = 1 << 6
};
const BYTE g_segDigits[10] = {
SEG_A | SEG_B | SEG_C | SEG_D | SEG_E | SEG_F, // 0
SEG_B | SEG_C, // 1
SEG_A | SEG_B | SEG_G | SEG_E | SEG_D, // 2
SEG_A | SEG_B | SEG_G | SEG_C | SEG_D, // 3
SEG_F | SEG_G | SEG_B | SEG_C, // 4
SEG_A | SEG_F | SEG_G | SEG_C | SEG_D, // 5
SEG_A | SEG_F | SEG_G | SEG_E | SEG_C | SEG_D, // 6
SEG_A | SEG_B | SEG_C, // 7
SEG_A | SEG_B | SEG_C | SEG_D | SEG_E | SEG_F | SEG_G, // 8
SEG_A | SEG_B | SEG_C | SEG_D | SEG_F | SEG_G, // 9
};
void SegRect(HDC hdc, int x, int y, int w, int h) {
RECT r = { x, y, x + w, y + h };
FillRect(hdc, &r, g_brushRed);
}
void DrawDigit7(HDC hdc, int x, int y, int w, int h, int digit) {
int t = w >= 20 ? 4 : 3;
int mid = h / 2;
BYTE m = g_segDigits[digit];
if (m & SEG_A) SegRect(hdc, x + t, y, w - 2 * t, t);
if (m & SEG_F) SegRect(hdc, x, y + t, t, mid - t);
if (m & SEG_B) SegRect(hdc, x + w - t, y + t, t, mid - t);
if (m & SEG_G) SegRect(hdc, x + t, y + mid - t / 2, w - 2 * t, t);
if (m & SEG_E) SegRect(hdc, x, y + mid, t, h - mid - t);
if (m & SEG_C) SegRect(hdc, x + w - t, y + mid, t, h - mid - t);
if (m & SEG_D) SegRect(hdc, x + t, y + h - t, w - 2 * t, t);
}
void DrawMinus7(HDC hdc, int x, int y, int w, int h) {
int t = w >= 20 ? 4 : 3;
SegRect(hdc, x + t, y + h / 2 - t / 2, w - 2 * t, t);
}
// ---- 元素绘制 ----
void DrawHiddenCell(HDC hdc, RECT rc) {
FillGradientV(hdc, rc, kColCellTop, kColCellBot);
FrameRect(hdc, &rc, g_brushBorder);
}
void DrawFlag(HDC hdc, RECT rc, float s) {
RECT r = ScaleRect(rc, s);
int cx = (r.left + r.right) / 2;
int top = r.top + 5, bot = r.bottom - 5;
HGDIOBJ old = SelectObject(hdc, g_penBlack);
MoveToEx(hdc, cx, top, nullptr); LineTo(hdc, cx, bot);
MoveToEx(hdc, cx - 5, bot - 1, nullptr); LineTo(hdc, cx + 5, bot - 1);
SelectObject(hdc, old);
HGDIOBJ ob = SelectObject(hdc, g_brushRed);
HGDIOBJ op = SelectObject(hdc, GetStockObject(NULL_PEN));
POINT tri[3] = { {cx + 1, top}, {cx + 1, top + 8}, {cx - 6, top + 4} };
Polygon(hdc, tri, 3);
SelectObject(hdc, ob);
SelectObject(hdc, op);
}
void DrawWrongFlag(HDC hdc, RECT rc) {
HGDIOBJ old = SelectObject(hdc, g_penRed);
MoveToEx(hdc, rc.left + 6, rc.top + 6, nullptr);
LineTo(hdc, rc.right - 6, rc.bottom - 6);
MoveToEx(hdc, rc.right - 6, rc.top + 6, nullptr);
LineTo(hdc, rc.left + 6, rc.bottom - 6);
SelectObject(hdc, old);
}
void DrawMine(HDC hdc, RECT rc, bool red, float s) {
int cx = (rc.left + rc.right) / 2, cy = (rc.top + rc.bottom) / 2;
int R = (int)(7.0f * s);
if (R < 1) return;
HGDIOBJ ob = SelectObject(hdc, red ? g_brushRed : g_brushBlack);
HGDIOBJ op = SelectObject(hdc, GetStockObject(NULL_PEN));
Ellipse(hdc, cx - R, cy - R, cx + R, cy + R);
SelectObject(hdc, ob);
if (!red) {
// 高光(光泽球体)
int hl = R > 3 ? R / 3 : 1;
HGDIOBJ ow = SelectObject(hdc, g_brushWhite);
Ellipse(hdc, cx - R + hl, cy - R + hl, cx - hl, cy - hl);
SelectObject(hdc, ow);
}
SelectObject(hdc, op);
}
void DrawNumber(HDC hdc, RECT rc, int n) {
static const COLORREF colors[9] = {
0,
RGB(0, 0, 255), // 1 蓝
RGB(0, 128, 0), // 2 绿
RGB(255, 0, 0), // 3 红
RGB(0, 0, 128), // 4 深蓝
RGB(128, 0, 0), // 5 深红
RGB(0, 128, 128), // 6 青
RGB(0, 0, 0), // 7 黑
RGB(128, 128, 128) // 8 灰
};
SetTextColor(hdc, colors[n]);
SetBkMode(hdc, TRANSPARENT);
HGDIOBJ old = SelectObject(hdc, g_fontNum);
wchar_t s[2] = { (wchar_t)(L'0' + n), 0 };
DrawText(hdc, s, 1, &rc, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
SelectObject(hdc, old);
}
void DrawCounter(HDC hdc, RECT rc, int value) {
int v = value < -99 ? -99 : (value > 999 ? 999 : value);
FillRect(hdc, &rc, g_brushBlack);
FrameRect(hdc, &rc, g_brushBorder);
int digitW = 19, digitH = 34;
int totalW = 3 * digitW;
int x0 = rc.left + ((rc.right - rc.left) - totalW) / 2;
int y0 = rc.top + (rc.bottom - rc.top - digitH) / 2;
int absv = v < 0 ? -v : v;
int hundreds = absv / 100 % 10;
int tens = absv / 10 % 10;
int ones = absv % 10;
if (v < 0) {
DrawMinus7(hdc, x0, y0, digitW, digitH);
DrawDigit7(hdc, x0 + digitW, y0, digitW, digitH, tens);
DrawDigit7(hdc, x0 + 2 * digitW, y0, digitW, digitH, ones);
} else {
DrawDigit7(hdc, x0, y0, digitW, digitH, hundreds);
DrawDigit7(hdc, x0 + digitW, y0, digitW, digitH, tens);
DrawDigit7(hdc, x0 + 2 * digitW, y0, digitW, digitH, ones);
}
}
void DrawSmiley(HDC hdc, RECT rc, GameState state, bool down) {
// 蓝色光泽按钮
FillGradientV(hdc, rc, down ? kColCellBot : kColCellTop, down ? kColCellTop : kColCellBot);
FrameRect(hdc, &rc, g_brushBorder);
int cx = (rc.left + rc.right) / 2, cy = (rc.top + rc.bottom) / 2;
HGDIOBJ ob = SelectObject(hdc, g_brushYellow);
HGDIOBJ op = SelectObject(hdc, g_penBlack);
Ellipse(hdc, cx - 11, cy - 11, cx + 11, cy + 11);
// 高光
SelectObject(hdc, g_brushWhite);
SelectObject(hdc, GetStockObject(NULL_PEN));
Ellipse(hdc, cx - 7, cy - 9, cx - 1, cy - 4);
SelectObject(hdc, g_penBlack);
if (state == GameState::LOST) {
MoveToEx(hdc, cx - 7, cy - 5, nullptr); LineTo(hdc, cx - 3, cy - 1);
MoveToEx(hdc, cx - 3, cy - 5, nullptr); LineTo(hdc, cx - 7, cy - 1);
MoveToEx(hdc, cx + 3, cy - 5, nullptr); LineTo(hdc, cx + 7, cy - 1);
MoveToEx(hdc, cx + 7, cy - 5, nullptr); LineTo(hdc, cx + 3, cy - 1);
Arc(hdc, cx - 6, cy + 3, cx + 6, cy + 11, cx - 6, cy + 7, cx + 6, cy + 7);
} else if (state == GameState::WON) {
HGDIOBJ ob2 = SelectObject(hdc, g_brushBlack);
HGDIOBJ op2 = SelectObject(hdc, GetStockObject(NULL_PEN));
Rectangle(hdc, cx - 8, cy - 5, cx - 1, cy + 1);
Rectangle(hdc, cx + 1, cy - 5, cx + 8, cy + 1);
SelectObject(hdc, ob2);
SelectObject(hdc, op2);
MoveToEx(hdc, cx - 1, cy - 2, nullptr); LineTo(hdc, cx + 1, cy - 2);
Arc(hdc, cx - 6, cy + 1, cx + 6, cy + 9, cx + 6, cy + 5, cx - 6, cy + 5);
} else {
HGDIOBJ ob2 = SelectObject(hdc, g_brushBlack);
HGDIOBJ op2 = SelectObject(hdc, GetStockObject(NULL_PEN));
Ellipse(hdc, cx - 6, cy - 4, cx - 3, cy - 1);
Ellipse(hdc, cx + 3, cy - 4, cx + 6, cy - 1);
SelectObject(hdc, ob2);
SelectObject(hdc, op2);
Arc(hdc, cx - 6, cy + 1, cx + 6, cy + 9, cx + 6, cy + 5, cx - 6, cy + 5);
}
SelectObject(hdc, op);
}
void DrawRevealed(HDC hdc, RECT rc, const Cell& cell, const Game& game, int r, int c, float s, unsigned now) {
RECT inner = ScaleRect(rc, s);
FillGradientV(hdc, inner, kColOpenTop, kColOpenBot);
FrameRect(hdc, &inner, g_brushBorder);
if (cell.mine) {
float ms = 1.0f;
if (cell.mineStamp) {
int age = (int)(now - cell.mineStamp);
if (age < 0) ms = 0.0f;
else if (age < (int)kPopMs) ms = 0.4f + 0.6f * age / (float)kPopMs;
}
DrawMine(hdc, inner, (r == game.DeadRow() && c == game.DeadCol()), ms);
} else if (cell.adj > 0) {
if (s >= 0.8f) DrawNumber(hdc, inner, cell.adj); // 弹出接近完成再显示数字
}
}
void DrawCell(HDC hdc, int r, int c, const Game& game) {
RECT rc = CellRect(r, c);
const Cell& cell = game.Board()[r][c];
unsigned now = GetTickCount();
if (!cell.revealed) {
DrawHiddenCell(hdc, rc);
if (cell.flagged) {
float s = 1.0f;
if (cell.flagStamp) {
int age = (int)(now - cell.flagStamp);
if (age < (int)kFlagPopMs) s = 0.6f + 0.4f * (age < 0 ? 0 : age) / (float)kFlagPopMs;
}
DrawFlag(hdc, rc, s);
}
if (game.State() == GameState::LOST && cell.flagged && !cell.mine)
DrawWrongFlag(hdc, rc); // 标错的旗子打叉
} else {
if (cell.revealStamp) {
int age = (int)(now - cell.revealStamp);
if (age < 0) { DrawHiddenCell(hdc, rc); return; } // 波浪尚未到达
float s = (age >= (int)kPopMs) ? 1.0f : (0.55f + 0.45f * age / (float)kPopMs);
DrawRevealed(hdc, rc, cell, game, r, c, s, now);
} else {
DrawRevealed(hdc, rc, cell, game, r, c, 1.0f, now);
}
}
}
void DrawHeader(HDC hdc, const Game& game, bool smileDown) {
RECT panel = HeaderRect(game.Cols());
FillGradientV(hdc, panel, kColPanelTop, kColPanelBot);
FrameRect(hdc, &panel, g_brushBorder);
RECT lc = { panel.left + 6, panel.top + 8, panel.left + 68, panel.top + 50 };
DrawCounter(hdc, lc, game.Mines() - game.FlagCount());
DrawSmiley(hdc, SmileyRect(game.Cols()), game.State(), smileDown);
RECT rc = { panel.right - 68, panel.top + 8, panel.right - 6, panel.top + 50 };
DrawCounter(hdc, rc, game.Time());
}
void DrawGame(HDC hdc, const RECT& client, const Game& game, bool smileDown) {
FillGradientV(hdc, client, kColBgTop, kColBgBot);
DrawHeader(hdc, game, smileDown);
for (int r = 0; r < game.Rows(); ++r)
for (int c = 0; c < game.Cols(); ++c)
DrawCell(hdc, r, c, game);
}
} // namespace
void RenderInit() {
g_brushBlack = CreateSolidBrush(RGB(0, 0, 0));
g_brushRed = CreateSolidBrush(RGB(255, 0, 0));
g_brushYellow = CreateSolidBrush(RGB(255, 255, 0));
g_brushWhite = CreateSolidBrush(RGB(255, 255, 255));
g_brushBorder = CreateSolidBrush(kColBorder);
g_penBlack = CreatePen(PS_SOLID, 1, RGB(0, 0, 0));
g_penRed = CreatePen(PS_SOLID, 2, RGB(255, 0, 0));
g_fontNum = CreateFont(-20, 0, 0, 0, FW_BOLD, FALSE, FALSE, FALSE,
DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
ANTIALIASED_QUALITY, DEFAULT_PITCH, L"Tahoma");
}
void RenderCleanup() {
DeleteObject(g_brushBlack);
DeleteObject(g_brushRed);
DeleteObject(g_brushYellow);
DeleteObject(g_brushWhite);
DeleteObject(g_brushBorder);
DeleteObject(g_penBlack);
DeleteObject(g_penRed);
DeleteObject(g_fontNum);
}
void ComputeClientSize(int rows, int cols, int& width, int& height) {
width = 2 * PAD + cols * CELL;
height = 2 * PAD + HEADER_H + GAP + rows * CELL;
}
RECT CellRect(int r, int c) {
RECT rc;
rc.left = PAD + c * CELL;
rc.top = PAD + HEADER_H + GAP + r * CELL;
rc.right = rc.left + CELL;
rc.bottom = rc.top + CELL;
return rc;
}
RECT HeaderRect(int cols) {
RECT rc = { PAD, PAD, PAD + cols * CELL, PAD + HEADER_H };
return rc;
}
RECT SmileyRect(int cols) {
RECT rc;
int cx = PAD + cols * CELL / 2;
rc.left = cx - 21;
rc.top = PAD + 8;
rc.right = cx + 21;
rc.bottom = PAD + 8 + 42;
return rc;
}
bool BoardHit(POINT pt, int rows, int cols, int& r, int& c) {
int bx = PAD, by = PAD + HEADER_H + GAP;
if (pt.x < bx || pt.y < by) return false;
int cc = (pt.x - bx) / CELL;
int rr = (pt.y - by) / CELL;
if (rr < 0 || rr >= rows || cc < 0 || cc >= cols) return false;
r = rr;
c = cc;
return true;
}
bool RenderHasActiveAnimation(const Game& game) {
unsigned now = GetTickCount();
const auto& board = game.Board();
for (const auto& row : board)
for (const Cell& cell : row) {
if (cell.revealStamp && (int)(now - cell.revealStamp) < (int)kAnimWin) return true;
if (cell.flagStamp && (int)(now - cell.flagStamp) < (int)kAnimWin) return true;
if (cell.mineStamp && (int)(now - cell.mineStamp) < (int)kAnimWin) return true;
}
return false;
}
void RenderClient(HDC hdc, const Game& game, bool smileDown, const RECT& client) {
int w = client.right - client.left;
int h = client.bottom - client.top;
HDC mem = CreateCompatibleDC(hdc);
HBITMAP bmp = CreateCompatibleBitmap(hdc, w, h);
HBITMAP oldb = (HBITMAP)SelectObject(mem, bmp);
DrawGame(mem, client, game, smileDown);
BitBlt(hdc, 0, 0, w, h, mem, 0, 0, SRCCOPY);
SelectObject(mem, oldb);
DeleteObject(bmp);
DeleteDC(mem);
}
dialogs.h
// dialogs.h — 自定义难度对话框
#pragma once
#include <windows.h>
// 显示"自定义难度"对话框;返回 true 表示确定,并回填新的行列雷数
bool ShowCustomDialog(HWND parent, int& rows, int& cols, int& mines);
dialogs.cpp
// dialogs.cpp — 自定义难度对话框实现(内存对话框模板,无需 .rc 资源)
#include "dialogs.h"
#include <vector>
namespace {
constexpr int IDC_ROWS = 1001;
constexpr int IDC_COLS = 1002;
constexpr int IDC_MINES = 1003;
struct DlgData {
int rows, cols, mines;
};
void AppendWord(std::vector<BYTE>& b, WORD w) {
b.push_back((BYTE)(w & 0xFF));
b.push_back((BYTE)(w >> 8));
}
void AppendDword(std::vector<BYTE>& b, DWORD d) {
AppendWord(b, (WORD)(d & 0xFFFF));
AppendWord(b, (WORD)(d >> 16));
}
void AppendWStr(std::vector<BYTE>& b, const wchar_t* s) {
for (; *s; ++s) AppendWord(b, (WORD)*s);
AppendWord(b, 0);
}
void Align4(std::vector<BYTE>& b) {
while (b.size() % 4) b.push_back(0);
}
std::vector<BYTE> BuildTemplate() {
std::vector<BYTE> b;
// DLGTEMPLATE 头部(逐字段写入,避免结构体对齐填充问题)
AppendDword(b, WS_POPUP | WS_CAPTION | WS_SYSMENU | DS_SETFONT | DS_CENTER); // style
AppendDword(b, 0); // dwExtendedStyle
AppendWord(b, 8); // cdit = 3 标签 + 3 编辑框 + 2 按钮
AppendWord(b, 0); // x
AppendWord(b, 0); // y
AppendWord(b, 160); // cx
AppendWord(b, 82); // cy
AppendWord(b, 0); // menu
AppendWord(b, 0); // windowClass
AppendWStr(b, L"自定义难度"); // title
AppendWord(b, 9); // 字号
AppendWStr(b, L"MS Shell Dlg"); // 字体名
auto AddItem = [&](WORD id, WORD cls, const wchar_t* text,
DWORD style, int x, int y, int cx, int cy) {
Align4(b);
AppendDword(b, style);
AppendDword(b, 0); // exStyle
AppendWord(b, (WORD)x);
AppendWord(b, (WORD)y);
AppendWord(b, (WORD)cx);
AppendWord(b, (WORD)cy);
AppendWord(b, id);
AppendWord(b, 0xFFFF);
AppendWord(b, cls); // 控件类(序号)
if (text) AppendWStr(b, text);
else AppendWord(b, 0);
AppendWord(b, 0); // 附加创建数据长度
};
// 标签
AddItem(0, 0x0082, L"行数:", WS_CHILD | WS_VISIBLE, 12, 14, 40, 12);
AddItem(0, 0x0082, L"列数:", WS_CHILD | WS_VISIBLE, 12, 30, 40, 12);
AddItem(0, 0x0082, L"地雷数:", WS_CHILD | WS_VISIBLE, 12, 46, 40, 12);
// 编辑框
AddItem(IDC_ROWS, 0x0081, nullptr, WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP | ES_NUMBER, 56, 12, 65, 12);
AddItem(IDC_COLS, 0x0081, nullptr, WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP | ES_NUMBER, 56, 28, 65, 12);
AddItem(IDC_MINES, 0x0081, nullptr, WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP | ES_NUMBER, 56, 44, 65, 12);
// 按钮
AddItem(IDOK, 0x0080, L"确定", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_DEFPUSHBUTTON, 28, 62, 45, 14);
AddItem(IDCANCEL, 0x0080, L"取消", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_PUSHBUTTON, 88, 62, 45, 14);
return b;
}
INT_PTR CALLBACK DlgProc(HWND h, UINT m, WPARAM w, LPARAM l) {
switch (m) {
case WM_INITDIALOG: {
DlgData* d = reinterpret_cast<DlgData*>(l);
SetWindowLongPtr(h, DWLP_USER, reinterpret_cast<LONG_PTR>(d));
wchar_t buf[16];
wsprintf(buf, L"%d", d->rows); SetDlgItemText(h, IDC_ROWS, buf);
wsprintf(buf, L"%d", d->cols); SetDlgItemText(h, IDC_COLS, buf);
wsprintf(buf, L"%d", d->mines); SetDlgItemText(h, IDC_MINES, buf);
return TRUE;
}
case WM_COMMAND:
switch (LOWORD(w)) {
case IDOK: {
DlgData* d = reinterpret_cast<DlgData*>(GetWindowLongPtr(h, DWLP_USER));
int r = (int)GetDlgItemInt(h, IDC_ROWS, nullptr, FALSE);
int c = (int)GetDlgItemInt(h, IDC_COLS, nullptr, FALSE);
int mn = (int)GetDlgItemInt(h, IDC_MINES, nullptr, FALSE);
if (r < 5 || r > 40 || c < 5 || c > 60) {
MessageBox(h, L"行数范围 5–40,列数范围 5–60。", L"输入有误", MB_ICONWARNING);
return TRUE;
}
int maxm = r * c - 1;
if (mn < 1 || mn > maxm) {
wchar_t msg[128];
wsprintf(msg, L"地雷数量需在 1–%d 之间。", maxm);
MessageBox(h, msg, L"输入有误", MB_ICONWARNING);
return TRUE;
}
d->rows = r;
d->cols = c;
d->mines = mn;
EndDialog(h, IDOK);
return TRUE;
}
case IDCANCEL:
EndDialog(h, IDCANCEL);
return TRUE;
}
}
return FALSE;
}
} // namespace
bool ShowCustomDialog(HWND parent, int& rows, int& cols, int& mines) {
DlgData data = { rows, cols, mines };
std::vector<BYTE> tmpl = BuildTemplate();
INT_PTR res = DialogBoxIndirectParam(
GetModuleHandle(nullptr),
reinterpret_cast<const DLGTEMPLATE*>(tmpl.data()),
parent, DlgProc, reinterpret_cast<LPARAM>(&data));
if (res == IDOK) {
rows = data.rows;
cols = data.cols;
mines = data.mines;
return true;
}
return false;
}
app.cpp
// app.cpp — 程序入口、窗口与消息处理
// 注:UNICODE/_UNICODE 由编译参数统一定义(见 build.bat)
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <windowsx.h>
#include <cstdlib>
#include <ctime>
#include "game.h"
#include "render.h"
#include "dialogs.h"
namespace {
constexpr int TIMER_ID = 1; // 1 秒计时
constexpr int TIMER_ANIM = 2; // 动画驱动(约 60fps)
constexpr int IDM_NEW = 100;
constexpr int IDM_BEGIN = 101;
constexpr int IDM_INTER = 102;
constexpr int IDM_EXPERT = 103;
constexpr int IDM_CUSTOM = 104;
constexpr int IDM_ABOUT = 105;
const DWORD kStyle = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX;
HINSTANCE g_hInst;
HWND g_hwnd;
Game g_game;
bool g_smileDown = false;
int g_customRows = 16;
int g_customCols = 16;
int g_customMines = 40;
const Difficulty g_presets[] = {
{ 9, 9, 10, L"初级" },
{ 16, 16, 40, L"中级" },
{ 16, 30, 99, L"高级" },
};
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
// 依据当前棋盘尺寸调整窗口大小并触发重绘
void ResetWindowAndInvalidate() {
int cw, ch;
ComputeClientSize(g_game.Rows(), g_game.Cols(), cw, ch);
RECT rc = { 0, 0, cw, ch };
AdjustWindowRectEx(&rc, kStyle, TRUE, 0);
SetWindowPos(g_hwnd, nullptr, 0, 0, rc.right - rc.left, rc.bottom - rc.top,
SWP_NOMOVE | SWP_NOZORDER);
InvalidateRect(g_hwnd, nullptr, TRUE);
}
void NewGame() {
g_game.Restart();
ResetWindowAndInvalidate();
}
void SetDifficulty(int idx) {
const Difficulty& d = (idx < 3)
? g_presets[idx]
: Difficulty{ g_customRows, g_customCols, g_customMines, L"自定义" };
g_game.NewGame(d.rows, d.cols, d.mines);
wchar_t title[64];
wsprintf(title, L"扫雷 - %s (%d×%d, %d 雷)", d.name, d.rows, d.cols, d.mines);
SetWindowText(g_hwnd, title);
ResetWindowAndInvalidate();
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
switch (msg) {
case WM_CREATE: {
RenderInit();
// 菜单
HMENU menu = CreateMenu();
HMENU game = CreatePopupMenu();
AppendMenu(game, MF_STRING, IDM_NEW, L"新游戏(&N)\tF2");
AppendMenu(game, MF_SEPARATOR, 0, nullptr);
AppendMenu(game, MF_STRING, IDM_BEGIN, L"初级(&B)");
AppendMenu(game, MF_STRING, IDM_INTER, L"中级(&I)");
AppendMenu(game, MF_STRING, IDM_EXPERT, L"高级(&E)");
AppendMenu(game, MF_STRING, IDM_CUSTOM, L"自定义(&C)...");
AppendMenu(menu, MF_POPUP, (UINT_PTR)game, L"游戏(&G)");
HMENU help = CreatePopupMenu();
AppendMenu(help, MF_STRING, IDM_ABOUT, L"关于(&A)...");
AppendMenu(menu, MF_POPUP, (UINT_PTR)help, L"帮助(&H)");
SetMenu(hwnd, menu);
SetTimer(hwnd, TIMER_ID, 1000, nullptr);
SetTimer(hwnd, TIMER_ANIM, 16, nullptr);
return 0;
}
case WM_TIMER:
if (wParam == TIMER_ID) {
g_game.Tick();
if (g_game.State() == GameState::PLAYING) {
RECT rc = HeaderRect(g_game.Cols());
InvalidateRect(hwnd, &rc, FALSE);
}
} else if (wParam == TIMER_ANIM) {
if (RenderHasActiveAnimation(g_game))
InvalidateRect(hwnd, nullptr, FALSE);
}
return 0;
case WM_PAINT: {
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
RECT client;
GetClientRect(hwnd, &client);
RenderClient(hdc, g_game, g_smileDown, client);
EndPaint(hwnd, &ps);
return 0;
}
case WM_PRINTCLIENT: {
RECT client;
GetClientRect(hwnd, &client);
RenderClient((HDC)wParam, g_game, g_smileDown, client);
return 0;
}
case WM_LBUTTONDOWN: {
SetFocus(hwnd);
POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
RECT sr = SmileyRect(g_game.Cols());
if (PtInRect(&sr, pt)) {
g_smileDown = true;
SetCapture(hwnd);
InvalidateRect(hwnd, &sr, FALSE);
return 0;
}
int r, c;
if (BoardHit(pt, g_game.Rows(), g_game.Cols(), r, c)) {
if ((GetKeyState(VK_RBUTTON) & 0x8000) && g_game.IsRevealed(r, c))
g_game.Chord(r, c);
else
g_game.Reveal(r, c);
InvalidateRect(hwnd, nullptr, FALSE);
}
return 0;
}
case WM_LBUTTONUP: {
if (g_smileDown) {
g_smileDown = false;
ReleaseCapture();
POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
RECT sr = SmileyRect(g_game.Cols());
if (PtInRect(&sr, pt)) NewGame();
else InvalidateRect(hwnd, &sr, FALSE);
}
return 0;
}
case WM_CAPTURECHANGED:
g_smileDown = false;
return 0;
case WM_RBUTTONDOWN: {
POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
int r, c;
if (BoardHit(pt, g_game.Rows(), g_game.Cols(), r, c)) {
if ((GetKeyState(VK_LBUTTON) & 0x8000) && g_game.IsRevealed(r, c))
g_game.Chord(r, c);
else
g_game.ToggleFlag(r, c);
InvalidateRect(hwnd, nullptr, FALSE);
}
return 0;
}
case WM_KEYDOWN:
if (wParam == VK_F2) NewGame();
return 0;
case WM_COMMAND:
switch (LOWORD(wParam)) {
case IDM_NEW: NewGame(); break;
case IDM_BEGIN: SetDifficulty(0); break;
case IDM_INTER: SetDifficulty(1); break;
case IDM_EXPERT: SetDifficulty(2); break;
case IDM_CUSTOM:
if (ShowCustomDialog(hwnd, g_customRows, g_customCols, g_customMines))
SetDifficulty(3);
break;
case IDM_ABOUT:
MessageBox(hwnd,
L"扫雷 Minesweeper\n\nC++ / Win32 实现\n\n"
L"左键翻开,右键插旗,\n"
L"在数字上左右同按可快速翻开。",
L"关于扫雷", MB_OK | MB_ICONINFORMATION);
break;
}
return 0;
case WM_DESTROY:
KillTimer(hwnd, TIMER_ID);
KillTimer(hwnd, TIMER_ANIM);
RenderCleanup();
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
} // namespace
int WINAPI WinMain(HINSTANCE hInst, HINSTANCE, LPSTR, int nCmdShow) {
g_hInst = hInst;
std::srand((unsigned)std::time(nullptr));
WNDCLASSW wc = {};
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WndProc;
wc.hInstance = hInst;
wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_BTNFACE + 1);
wc.lpszClassName = L"MinesweeperWnd";
if (!RegisterClassW(&wc)) return 1;
// 初始大小(初级 9×9)
int cw, ch;
ComputeClientSize(g_game.Rows(), g_game.Cols(), cw, ch);
RECT rc = { 0, 0, cw, ch };
AdjustWindowRectEx(&rc, kStyle, TRUE, 0);
g_hwnd = CreateWindowExW(0, L"MinesweeperWnd", L"扫雷 - 初级 (9×9, 10 雷)",
kStyle, CW_USEDEFAULT, CW_USEDEFAULT,
rc.right - rc.left, rc.bottom - rc.top,
nullptr, nullptr, hInst, nullptr);
if (!g_hwnd) return 1;
ShowWindow(g_hwnd, nCmdShow);
UpdateWindow(g_hwnd);
MSG msg;
while (GetMessage(&msg, nullptr, 0, 0) > 0) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return (int)msg.wParam;
}
build.bat
@echo off
rem Build Minesweeper (MSVC toolchain)
call "D:\VS2026\VC\Auxiliary\Build\vcvars64.bat"
cl /nologo /O2 /std:c++17 /EHsc /W3 /utf-8 /DUNICODE /D_UNICODE game.cpp render.cpp dialogs.cpp app.cpp /Fe:minesweeper.exe user32.lib gdi32.lib msimg32.lib