改编的 WinMine 源代码,用于教授 Win32 API 编程






4.82/5 (18投票s)
一套理想的源代码包,
引言
作为一名大学教师,我本学期教授“Windows 编程”课程。我选择了 Charles Petzold 的著作《Programming Windows》作为教材。然而,由于我们只有 32 个课时,无法涵盖书中如此多的示例程序,因此我决定采用两到三个具有相当复杂度的示例,并以几个步骤来呈现。第一步只是一个框架,而每一步新都会在前一步的基础上引入新功能,直到最终成为一个完整的应用程序。
我认为,第一个示例程序最理想的选择是 WinMine(或扫雷)游戏。它阐释了许多基本 Windows API 函数的使用,例如 CreateWindow
、SetWindowPos
等,以及许多常用窗口消息的处理,例如 WM_PAINT
消息和鼠标消息。它足够复杂,可以成为一个真正的应用程序,同时又足够简单,不会让学生因接触大量新概念而不知所措。此外,作为一个知名的游戏,它的源代码比其他程序更容易理解。
WinMine 的开源实现已经有很多了,其中我选择了 ReactOS 团队的 Joshua Thielen 编写的 WineMine
项目;它非常简洁和干净。我最终交给学生的软件包是本文附件中的 WinMine4Edu
项目。WinMine4Edu
基于 WineMine
,但我做了许多修改,使其更像微软的 WinMine
,并且我重写了鼠标消息处理部分,因为我未能理解 WineMine
的这部分,而且我认为我的实现可能更简洁一些。
编写代码时的注意事项
在改编代码时,我有一个双重目的。我希望它能尽可能地模仿微软原版的 WinMine
,但同时我必须始终牢记,读者是那些在 Windows 编程方面先验知识很少的学生。考虑到这一点,我决定不实现鼠标中键的处理,因为它对于一个完整的游戏来说并非必需。
为了进一步简化。在处理 WM_RBUTTONUP
消息时,我们还采用了与微软 WinMine
略有不同的规则。微软 WinMine
允许在游戏处于 WAITING
状态时标记地雷,但我们不允许这样做;微软 WinMine
还允许剩余地雷数为负数,而我们不允许。我认为,这种方法完全合理,并且可以节省我们编写代码来在 LED 显示屏上显示负数的工作。
代码架构
WinMine4Edu
包含三个 C 源文件:WinMine.c、MinePaint.c 和 MineGame.c。WinMine.c 包含 WinMain
和主窗口过程,其中包括命令处理和其他杂项任务。MinePaint.c 包含图形部分,而 MineGame.c 实现鼠标消息处理。
继承自 Winemine
,有一个全局结构体变量“board
”,其中包含游戏的所有偏好设置、配置和当前状态。
主要例程如下:
void InitBoard(); // Compute the members of "board" that do not change from game to game
void LoadBoard(); // Load preferences from the registry
void SaveBoard(); // Save preferences to the registry
void DestroyBoard();// Release memory DCs and bitmaps associated with the board
void CheckLevel(); // Check whether the level parameters are sane
void NewBoard(); // Compute the members of "board" that may change from game to game
void NewGame(); // Call NewBoard and refresh the window
void TranslateMouseMsg(UINT* puMsg, WPARAM wParam);
void ProcessMouseMsg(UINT uMsg, LPARAM lParam);
void OnCommand(WPARAM wParam, LPARAM lParam);
void SetDifficulty(DIFFICULTY Difficulty); // Set new difficulty for the game
void DrawBackground(HDC hDC); // Draw the margins and borders of the rect's
void DrawMinesLeft(HDC hdc); // Draw the number of remaining mines in LED digits
void DrawFace(HDC hdc); // Draw the face button
void DrawTime(HDC hdc); // Draw the number of seconds elapsed since the game begins
void DrawGrid(HDC hdc); // Draw the grid of boxes
void DisplayFace(FACE_STATE faceState); // Set new state of the face and show it
void SetAndDispBoxState(int r, int c, BOX_STATE state); // Set new state of the box
// and show it
void DecMinesLeft(); // Decrement the number of remaining mines and show it
void IncMinesLeft(); // Increment the number of remaining mines and show it
void ZeroMinesLeft(); // Set the number of remaining mines to zero and show it
void IncTime(); // Increment the number of seconds elapsed and show it
void LayMines(int row, int col); // Lay the mines, this occurs after the
// first WM_LBUTTON message on a box
void Pt2RowCol(POINT pt, int *prow, int *pcol); // Calculate the current
//mouse point is in what box (row,col)
void GameWon(); // Called when the game is won
void GameLost(); // Called when the game is lost
void PressBox(int row, int col); // Do the pressing of the specified box
void UnPressBox(); // Do the unpressing of the currently pressed box
UINT CountMines(int row, int col); // Count how many mines are there in the
// surrounding boxes
BOOL StepBox(int row, int col); // Steps on a box, a recursive function
注意事项
这里我们先来熟悉一下术语。显示剩余地雷数的矩形称为“CounterRect
”;显示当前时间的矩形称为“TimerRect
”;显示游戏当前状态的按钮称为“Face
”;我们有方块“boxes
”,它们可能包含地雷,也可能不包含。由方块组成的矩形阵列称为“Grid
”。
我加入了一些可能让你觉得不错的功能。
- 当客户区域的任何元素(矩形、脸部或方块)需要重绘时,我们不会向窗口函数发送或发布
WM_PAINT
消息,而是直接使用通过GetDC
获取的hDC
进行绘制。 - 每个方块只有两个信息项,一个指定它是否有地雷,另一个指示方块的当前状态。方块的视觉外观仅取决于此状态(状态唯一确定要显示在地块上的位图的偏移量),这就是为什么你会发现我的
DrawBox
函数如此简单,只有一条语句! - 与
WineMine
一样,我们在方块上按下左鼠标按钮时调用PressBox
,在鼠标离开或释放左键,或发生WM_LBUTTONDOWN
以外的任何其他鼠标事件时调用UnPressBox
。我们的实现是,当方块处于“未点击”和“非最终”状态时,PressBox
才改变方块的状态:BS_INITIAL
或BS_DICEY
。当按下具有BS_INITIAL
状态的方块时,它暂时处于BS_DOWN
状态;当按下具有BS_DICEY
状态的方块时,它暂时处于BS_DICEY_DOWN
状态。释放方块会将方块状态恢复到其原始状态,即BS_INITIAL
或BS_DICEY
。 - 当在方块上释放左鼠标按钮时,该方块称为“被点击”。“被点击”的状态包括
BS_NUM1
到BS_NUM8
、BS_DOWN
和BS_BLAST
。因此,BS_DOWN
既是按下时的临时状态,也是点击后的永久状态,具体取决于上下文。这是通过遵循以下规则来编写代码来实现的 - 任何时候只能有一个方块被按下,或者没有方块被按下。
- 当我们释放一个方块时,它必须是当前被按下的方块。
- 当响应鼠标事件(按下/释放以外的事件)而改变任何方块的状态时,我们首先执行释放操作。这确保了此时没有方块被按下。因此,我们可以保证,如果我们此时遇到
BS_DOWN
状态,它一定是点击后的永久状态,而不是按下的结果。
StepBox
函数实现了方块的点击操作,请注意它是递归的,这意味着当被点击的方块周围没有地雷时,它可能会调用自身。请参阅以下代码片段
// steps on this box, return value indicates if it is safe
BOOL StepBox(int row, int col)
{
UINT cMinesSurround;
if (board.Box[row][col].State != BS_INITIAL &&
board.Box[row][col].State != BS_DICEY)
{
// previously stepped, so safe, no need to step second time,
// or already flagged as a mine
return TRUE;
}
if (board.Box[row][col].fMine)
{
// stepped on a mine!
SetAndDispBoxState(row, col, BS_BLAST);
return FALSE;
}
board.uSteps++;
cMinesSurround = CountMines(row, col);
SetAndDispBoxState(row, col, BS_DOWN - cMinesSurround);
if (cMinesSurround == 0)
{
int r, c;
for (r = row-1; r <= row+1; r++)
for (c = col-1; c <= col+1; c++)
{
if (WITHIN_GRID(r, c) && (r != row || c != col))
{
StepBox(r, c);
}
}
}
return TRUE;
}
结论
我们的最终成果是一个相当完整的 WinMine
程序,其复杂性得到控制,非常适合讲解 Win32 API 编程的细节。在我的教学实践中,我分五个步骤介绍了这个程序。学生们对 Win32 编程的知识循序渐进地积累,他们的信心和对 Windows 编程的兴趣也随之增长。