65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (18投票s)

2011年4月19日

CPOL

5分钟阅读

viewsIcon

36496

downloadIcon

1251

一套理想的源代码包,用于向学生介绍 Win32 编程的基础知识

引言

作为一名大学教师,我本学期教授“Windows 编程”课程。我选择了 Charles Petzold 的著作《Programming Windows》作为教材。然而,由于我们只有 32 个课时,无法涵盖书中如此多的示例程序,因此我决定采用两到三个具有相当复杂度的示例,并以几个步骤来呈现。第一步只是一个框架,而每一步新都会在前一步的基础上引入新功能,直到最终成为一个完整的应用程序。

我认为,第一个示例程序最理想的选择是 WinMine(或扫雷)游戏。它阐释了许多基本 Windows API 函数的使用,例如 CreateWindowSetWindowPos 等,以及许多常用窗口消息的处理,例如 WM_PAINT 消息和鼠标消息。它足够复杂,可以成为一个真正的应用程序,同时又足够简单,不会让学生因接触大量新概念而不知所措。此外,作为一个知名的游戏,它的源代码比其他程序更容易理解。

WinMine 的开源实现已经有很多了,其中我选择了 ReactOS 团队的 Joshua Thielen 编写的 WineMine 项目;它非常简洁和干净。我最终交给学生的软件包是本文附件中的 WinMine4Edu 项目。WinMine4Edu 基于 WineMine,但我做了许多修改,使其更像微软的 WinMine,并且我重写了鼠标消息处理部分,因为我未能理解 WineMine 的这部分,而且我认为我的实现可能更简洁一些。

编写代码时的注意事项

在改编代码时,我有一个双重目的。我希望它能尽可能地模仿微软原版的 WinMine,但同时我必须始终牢记,读者是那些在 Windows 编程方面先验知识很少的学生。考虑到这一点,我决定不实现鼠标中键的处理,因为它对于一个完整的游戏来说并非必需。

为了进一步简化。在处理 WM_RBUTTONUP 消息时,我们还采用了与微软 WinMine 略有不同的规则。微软 WinMine 允许在游戏处于 WAITING 状态时标记地雷,但我们不允许这样做;微软 WinMine 还允许剩余地雷数为负数,而我们不允许。我认为,这种方法完全合理,并且可以节省我们编写代码来在 LED 显示屏上显示负数的工作。

代码架构

WinMine4Edu 包含三个 C 源文件:WinMine.cMinePaint.cMineGame.cWinMine.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”。

我加入了一些可能让你觉得不错的功能。

  1. 当客户区域的任何元素(矩形、脸部或方块)需要重绘时,我们不会向窗口函数发送或发布 WM_PAINT 消息,而是直接使用通过 GetDC 获取的 hDC 进行绘制。
  2. 每个方块只有两个信息项,一个指定它是否有地雷,另一个指示方块的当前状态。方块的视觉外观仅取决于此状态(状态唯一确定要显示在地块上的位图的偏移量),这就是为什么你会发现我的 DrawBox 函数如此简单,只有一条语句!
  3. WineMine 一样,我们在方块上按下左鼠标按钮时调用 PressBox,在鼠标离开或释放左键,或发生 WM_LBUTTONDOWN 以外的任何其他鼠标事件时调用 UnPressBox。我们的实现是,当方块处于“未点击”和“非最终”状态时,PressBox 才改变方块的状态:BS_INITIALBS_DICEY。当按下具有 BS_INITIAL 状态的方块时,它暂时处于 BS_DOWN 状态;当按下具有 BS_DICEY 状态的方块时,它暂时处于 BS_DICEY_DOWN 状态。释放方块会将方块状态恢复到其原始状态,即 BS_INITIALBS_DICEY
  4. 当在方块上释放左鼠标按钮时,该方块称为“被点击”。“被点击”的状态包括 BS_NUM1BS_NUM8BS_DOWNBS_BLAST。因此,BS_DOWN 既是按下时的临时状态,也是点击后的永久状态,具体取决于上下文。这是通过遵循以下规则来编写代码来实现的
    1. 任何时候只能有一个方块被按下,或者没有方块被按下。
    2. 当我们释放一个方块时,它必须是当前被按下的方块。
    3. 当响应鼠标事件(按下/释放以外的事件)而改变任何方块的状态时,我们首先执行释放操作。这确保了此时没有方块被按下。因此,我们可以保证,如果我们此时遇到 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 编程的兴趣也随之增长。

© . All rights reserved.