扫雷游戏的实现C++

来源:互联网 发布:ori什么软件 编辑:程序博客网 时间:2024/04/30 22:46

扫雷游戏是Windows操作系统自带的一个小游戏,几乎每个电脑使用者都接触过它。它同时也是一款比较经典的小游戏,实现它的方法很多,也可以用不同算法和语言实现。近期用了两个周末(各一天)和大半个月的空余时间终于实现了一个比较完整的扫雷程序。现通过C++来呈现这款小游戏的实现方法。 

关于扫雷:

游戏的规则:在不掀开任何藏有地雷的方块情况下,以最快的速度找出所有的地雷。如果在掀开方块的过程中,不小心翻开(踩到)藏有地雷的方块,则宣告失败(游戏结束),游戏进行的过程中没有踩到地雷且找到所有的地雷,游戏才算成功。 

左键 表示翻开方块,是雷则结束,非雷则显示数字;

右键 表示标示或疑似地雷(不管是否真的是雷,奇数次按下表示表示此区域有雷,偶数次按下表示疑问);当反复按下右键,则方块会以标示→疑问不断循环。

操作者可以通过地雷区内的数字提示了解以数字为中心的周边八个方格内所含的地雷数,如果翻开的方块显示数字“3”,则表示以其为中心的周边方块内藏有3个地雷。当按下的方块不是地雷,且周边八个方块也都没有地雷时,方块会以被翻开方块的八个方向将空白方块翻开。成功地找出全部的地雷,游戏结束。 

说明:由于程序的代码有1K行左右,这里只介绍实现的设计思路和要点,尽量不去贴代码。 

设计实现:

(1)扫雷界面

新建一个MFC单文档应用程序。工程名MineSweepingView ,初始状态下可以直接运行程序。初始界面如图1-1-1所示:

图1-1-1MFC SDI初始化界面示意图

1.1 删除工具栏  界面含有菜单和工具栏,这些对于扫雷游戏界面是多余的选项,需要删去和修改。具体方法:在MainFrm.cpp中,找到函数OnCreate,处理如下:

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct){if (CFrameWnd::OnCreate(lpCreateStruct) == -1){return -1;}//if (!m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP//| CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC) ||//!m_wndToolBar.LoadToolBar(IDR_MAINFRAME))//{//TRACE0("未能创建工具栏\n");//return -1;      // 未能创建//}//if (!m_wndStatusBar.Create(this) ||//!m_wndStatusBar.SetIndicators(indicators,//  sizeof(indicators)/sizeof(UINT)))//{//TRACE0("未能创建状态栏\n");//return -1;      // 未能创建//}//// TODO: 如果不需要可停靠工具栏,则删除这三行//m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY);//EnableDocking(CBRS_ALIGN_ANY);//DockControlBar(&m_wndToolBar);return 0;}

1.2 修改菜单栏 修改结果是仅保留“帮助”,其他的都删除,添加“游戏(G)”以及子菜单“开始”,并且“游戏”置于帮助之前。首先,打开 资源视图——Menu —— IDR_MAINFRAME 在菜单栏的空白处,添加“游戏(G)”,子菜单添加“开始”,在属性栏中编辑“开始”对应的ID:IDM_GAME_START。然后,在MineSweeping.rc 中进行下面的操作:

IDR_MAINFRAMEMENUBEGIN   /* POPUP "文件t(&F)"BEGIN。。。。。。   中间的内容全部注释END*///将游戏置于帮助之前,显示时游戏将显示在左侧       POPUP"游戏(G)"   BEGIN        MENUITEM "开始",                          IDM_GAME_START   END   POPUP "帮助(&H)"   BEGIN        MENUITEM "关于MineSweeping(&A)...",      ID_APP_ABOUT   END  END

说明:这里的“游戏”和“帮助”对应界面中菜单项的第一项和第二项,有顺序。写在前面的置于左侧,写在后面的置于右侧。在BEGINEND之间的是POPUP对应菜单项的子菜单项,这里同样有顺序要求,这里的顺序(上下)和界面中所呈现的顺序是一致。 游戏选项下的子菜单也是一样的,就不在重述。

1.3 界面size MianFrm.cpp中找到函数

BOOL CMainFrame::PreCreateWindow(CREATESTRUCT&cs){       if(!CFrameWndEx::PreCreateWindow(cs) )       {              returnFALSE;       }           //  CREATESTRUCT cs 来修改窗口类或样式       //  the CREATESTRUCT cs    //添加如下代码 改变窗口的样式           cs.style=WS_SYSMENU|WS_OVERLAPPED|WS_MINIMIZEBOX;       //设置窗口大小:480*360       cs.cx=480;       cs.cy=360;      cs.lpszName=_T("扫雷");    //添加界面标题       return TRUE;}

说明:上述修改设置窗口的大小方法仅在VS2008以及更低的VS版本设置有效,在VS2010中设置无效。在VS2010中,改变窗口的大小在 CxxxApp中的InitInstance中添加代码:

BOOL CSaoLei2App::InitInstance(){/*           中间的内容全部注释掉               。。。。。。*///shufac 添加代码改变扫雷界面窗口大小VS2010 MFC窗体的大小设置m_pMainWnd->SetWindowPos(NULL,0,0,480,360,SWP_NOMOVE); // 唯一的一个窗口已初始化,因此显示它并对其进行更新m_pMainWnd->ShowWindow(SW_SHOW);m_pMainWnd->UpdateWindow();return TRUE;} 

至此,扫雷界面显示为:菜单项只有游戏和帮助的菜单项如图1-3-1所示

图1-3-1 扫雷窗口界面

1.4 窗口的背景 窗口的绘制背景操作在View类的OnDraw函数中进行,下面这四行代码控制整个界面的背景,RGB的三个参数从配置文件中获取。

CBrush mybrush; mybrush.CreateSolidBrush(RGB(Param1,Param2,Param3));           CRect myrect(0,0,1200,800); //添加背景的区域pDC->FillRect(myrect,&mybrush);

1.5 绘制雷区以及边线  雷区格子的绘制同样是在View类的OnDraw函数中进行,雷区的左上角顶点左边(10,50),绘制添加的的代码如下:

//画雷区边线//左上角是白线,右下角是黑线,以显示立体感CPen mypen;CPen*myoldPen; mypen.CreatePen(PS_SOLID,2,RGB(255,255,255));myoldPen=pDC->SelectObject(&mypen);   for(inti=0;i<m_ColCount;i++)        {              for(intj=0;j<m_RowCount;j++)              {                      pDC->MoveTo(10+i*15,50+j*15+15);                      pDC->LineTo(10+i*15,50+j*15);                       pDC->LineTo(10+i*15+15,50+j*15);               }        }pDC->SelectObject(myoldPen);//右下角黑线CPen mypen2;CPen*myoldPen2; mypen2.CreatePen(PS_SOLID,1,RGB(0,0,0));myoldPen2=pDC->SelectObject(&mypen2);for(intii=0;ii<m_ColCount;ii++) {       for(intjj=0;jj<m_RowCount;jj++)       {               pDC->MoveTo(10+ii*15,50+jj*15+15);               pDC->LineTo(10+ii*15+15,50+jj*15+15);                   pDC->LineTo(10+ii*15+15,50+jj*15);       } }pDC->SelectObject(myoldPen2);

说明:OnDraw函数同样是在程序运行时就开始调用,执行绘制操作时界面就会显示添加的背景以及绘制的格子(雷区的边线),这里的MoveTo和LineTo函数功能分别为移到某一点和连接至某一点,函数中两个参数对应坐标,客户区的左上顶点坐标为(0,0),向左为x正向向右延伸,向下为y正向向下延伸,单位均为像素。 

1.6 剩余雷数和表情按钮以及游戏进行时间的位置 

级别\类型

剩余雷数

表情按钮

时间

雷区

雷数

界面size

初级

(5,10)

(65,10)

(105,10)

(10,50)

10

10+9*15+10

中级

(20,10)

(115,10)

(190,10)

(10,50)

40

10+16*15+10

高级

(20,10)

(215,10)

(400,10)

(10,50)

99

10+30*15+10

自定义

(20,10)

((10+列数*15+10)/2-15,10)

(10+列数*15+10-70,10)

(10,50)

设定值

10+列数*15+10

注:单位pix

1 前4列是对应边框的左上顶点的坐标

2 剩余雷数和时间框大小均50*30  框内的数字显示距离左边框的距离均为10个像素点

3 表情按钮的大小 30*30 雷区每一个小格子大小均为15*15

4 剩余雷数边框 表情按钮 时间边框距离客户区的上端均为10个像素点

根据以上信息,自定义界面的size可以表示为:

长:10+15*列数+10  

宽:10+30+10+15*行数+10

为了保持界面的对称,不同级别的模式下,表情按钮应该居中,剩余雷数边框和时间边框距离界面左右边界均应该为10个像素点。

时间边框的左边界x坐标即为:界面宽度—10—50

表情按钮左边界x坐标应该为中轴线x坐标减去表情按钮宽度的一半即:(10+15*列数+10 )/2-15

确定了不同界面的size,调用函数

CWnd *CWnd=AfxGetMainWnd();//扫雷的窗口

CWnd->MoveWindow( 300, 200, width,height,TRUE );

(300,200)表示显示界面的位置,width和height表示的是对应级别界面的大小

注:在初级,中级和高级模式情况下,这4个数值固定,给一组参考数据

级别\位置参数

x

y

width

height

初级

300

200

170

250

中级

300

200

270

355

高级

300

200

480

360

在点击自定义后,在弹出的对话框中输入行、列以及雷数点击确定后,界面会发生相应的变化,在编程的时候出现了一个问题,界面的边界会出现对不齐的问题。重复检查上面的计算边框位置后,确定计算方法没有问题,再调查其他可能的原因,最后发现自定义只是改变了客户区的大小,主对话框并没有相应地改变。因此,在自定义处理中width,heigh这两个参数需要做处理,在自定义消息相应函数中添加下面的代码即可控制界面边界对齐。

CWnd *CWnd=AfxGetMainWnd();//扫雷的窗口CRect View_Client_Rect;CRect MainWnd_Rect;int MainWnd_Border_x=0;int MainWnd_Border_y=0; GetClientRect(&View_Client_Rect);CWnd->GetWindowRect(&MainWnd_Rect); MainWnd_Border_x=MainWnd_Rect.Width()-View_Client_Rect.Width();MainWnd_Border_y=MainWnd_Rect.Height()-View_Client_Rect.Height(); if(dlg.m_uiMineAreaColCount<15){       CWnd->MoveWindow(300, 200, MainWnd_Border_x+10+m_ColCount*15+10,MainWnd_Border_y+50+m_RowCount*15+10, TRUE );   }else{       CWnd->MoveWindow(300, 200, MainWnd_Border_x+10+m_ColCount*15+10,MainWnd_Border_y+50+m_RowCount*15+10, TRUE );}
说明:第二个版本中资源大小做过调整,但是思路是一致的。 

1.7 剩余雷数和表情按钮以及游戏进行时间的显示

1.7.1 剩余雷数的显示 初始界面显示相应级别的地雷个数,每发现一个地雷以及右键点击一次雷区,数目减1,另外还要注意在定时器中刷新这个区域

1.7.2 时间的显示 以鼠标左键点击界面开始计时,另外这个区域必须时时刷新,否则时间不会变化,知道结束才显示游戏进行的时间。这涉及到定时器的控制。定时器的控制逻辑是在鼠标左键点击雷区开始的,游戏结束时释放的。 

1.7.3 表情按钮和雷区格子的显示

控制逻辑如下:

//判断显示哪一幅位图//Mine.iBitMap=1    已按下的数字区 取值0 1 2 3 4 5 6 7 8 显示按钮0//Mine.iBitMap=2    显示旗 显示按钮1//Mine.iBitMap=3    显示问号显示按2//Mine.iBitMap=-1 结束时显示中雷  显示按钮3

1.8 显示效果的处理 有了1.6中的各个控件的位置坐标信息,通过前面介绍的MoveTo和LineTo函数可以给显示剩余雷数和时间边框以及表情按钮边沿绘制白色线条,增强立体显示效果,实现起来也很简单。

以上即为界面上需要编辑的全部内容。 

(2)添加资源

扫雷程序需要添加的资源包括图片和声音资源以及配置文件。图片资源包括界面中显示的图片、数字、表情等,声音资源是两个wave格式的音频文件,点击时没有触雷发出的声音,点击后触雷发出的爆炸声。

2.1 图片资源  图2-1所示的是扫雷游戏中的所有的图片资源,通过PhotoShop将他们截取为单个的位图资源,将所有的图片资源存放于文件夹Mine中,然后将这个文件夹置于系统目录:\MineSweeping\res中。前12个是在雷区的,后4个是表情按钮。为了便于加载,须各自保证其连续性。位图ID号:

按钮位图:30*30(size ),ID号依次为:IDB_ANNIU1,IDB_ANNIU2,IDB_ANNIU3,IDB_ANNIU4 ;

雷区位图:15*15(size),ID号依次为:IDB_BITMAP0,IDB_BITMAP1,。。。 。。。IDB_BITMAP11;

添加图片资源:打开工程的资源视图  右键点击Icon(Bitmap)——添加资源——资源类型选择Bitmap——导入——在文件夹\MineSweeping\res\Mine中选择制作好的上述位图资源,编辑对应的ID

图2-1 扫雷游戏中的图片资源

加载图片资源:图片资源添加到项目后在引用的时候需要通过变量来存放,

CBitmap m_Bitmap[12]; //位图数组CBitmap m_Anniu[4];  //表情按钮位图数组

说明:给位图数组赋值并加载相应的位图,数组m_Bitmap的第一个元素加载IDIDB_BITMAP0的位图,第二个元素加载IDIDB_BITMAP1的位图依次类推,第12个元素加载IDIDB_BITMAP11的位图,这样数组m_Bitmap包含了一个雷区对应位图的所有显示的状态。同理,给表情按钮数组赋值并加载相应的位图,数组m_Anniu的第一个元素加载IDIDB_ANNIU1的位图,第二个元素加载IDIDB_ANNIU2的位图,第三个元素加载IDIDB_ANNIU3的位图,第四个元素加载IDIDB_ANNIU4的位图,这样数组m_Anniu包含了一个表情按钮对应4种位图的所有显示的状态

这里的CBitmap是MFC提供的一个位图类,里面封装了关于位图的所有操作方法,具体的参见msdn。加载图片的时机应该是在程序运行开始的时候,因此在构造函数中添加一个函数命名为LoadBitmap(),这个函数功能就是加载位图。具体方法:以加载第一幅位图和第一个表情按钮为例:

m_Bitmap[0].LoadBitmap(IDB_BITMAP0);m_Anniu[0].LoadBitmap(IDB_ANNIU1);

需要注意的是函数的参数是对应位图的ID,其他的依此类推。 

2.2声音资源 在网上下载两个wave格式的音频文件,爆炸声和另外一种你觉得合适的声音(点击不触雷)。爆炸声命名为BOMB.WAV,另外一个可以命名为OTHERS.WAV。新建一个文件夹命名为Sound,置于资源文件目录Res文件夹下。将这两个声音文件存放于Sound文件夹中。然后在资源视图中添加这两个声音资源,具体方法见后续博文MFC关于声音的处理,这里篇幅原因不做细究。添加后ID号分别命名为IDR_BOMB和IDR_OTHERS。这里顺便提一下关于这两种声音的播放时机以及处理。点击鼠标左键,如果触雷,调用

PlaySound(MAKEINTRESOURCE(IDR_BOMB),AfxGetResourceHandle(),SND_ASYNC|SND_RESOURCE|SND_NODEFAULT);

如果没有触雷,调用

PlaySound(MAKEINTRESOURCE(IDR_OTHERS),AfxGetResourceHandle(),SND_ASYNC|SND_RESOURCE|SND_NODEFAULT);

这两个WindowsAPI具体含义参见:MFC声音的播放和录音的实现(一)

另外,播放声音还需要添加头文件和引用播放声音的库,在MineSweepingView.cpp中添加

#include <windows.h>#include <mmsystem.h>#pragma comment(lib, "WINMM.LIB")

2.3配置文件 程序中的扫雷排行榜需要配置文件的支持,另外将背景颜色也设为配置项,避免界面单一,这样个人可以根据自己的情况设定不同的背景。配置文件中的具体操作在前面的博做过详细的说明,这里不再重复。需要说明的是,每一个配置项在程序中需要一个对应的成员变量来存储和表示。配置文件中的配置项如下:

[Primary]TimePrimary=999NamePrimary=佚名[Middle]TimeMiddle=999NameMiddle=佚名[High]TimeHigh=999NameHigh=佚名 [ChooseColor]Param1 = 0Param2 = 0Param3 = 255[NoChooseColor]Param1 = 192Param2 = 192Param3 = 192

具体含义:前三项是不同级别最高记录的操作者个时间,后两项分别是有背景和无背景的颜色配置参数。关于后两项,用户可以根据自己的喜好来配置不同的背景颜色,只需要配置文件中改变这三个参数值即可。更多颜色信息,可参考:RGB颜色对照表

以上即为界面上需要编辑的全部内容。 

(3)数据结构

3-1 级别 根据扫雷不同的难度设定不同的等级,定义一个枚举MINE_LEVEL 枚举的定义置于头文件 MineSweepingView.h 中。代码如下:

enum MINE_LEVEL{       MINE_PRIMARY_LEVEL,//初级       MINE_MIDDLE_LEVEL,//中级       MINE_HIGH_LEVEL,//高级       MINE_CUSTOM_LEVEL //自定义};

3-2 雷 对于雷单独定义一个结构体 Mine。结构体的定义置于头文件 MineSweepingView.h 中。代码如下:

struct  Mine{       //显示哪一个位图取值       int iBitMap;       //这个位置周围8个位置相应的地雷数       int iArondMineNum;};

然后再在View类(MineSweepingView)中添加变量和函数。

3-3 头文件 在头文件 MineSweepingView.h 中 添加变量和函数的声明

public:int m_iLeftMineNum;  //剩余地雷的数目int m_iMineNum;      //地雷数目int m_iEnd;          //结束short m_sTime;       //计时int m_iTimeStart;    //开始计时CBitmap m_Bitmap[12];//雷区位图数组CBitmap m_anniu[4];  //表情按钮位图数组int m_RowCount;//雷区行数int m_ColCount;//雷区列数  Mine m_Mine[50][50];  //最大雷区 50行50列Minem_Mine[50][50];  //最大雷区MINE_LEVELm_MineLevel;//表示游戏等级的成员变量intm_iBQPosition_X;//表情按钮在不同的模式(等级下显示的位置,仅改变x坐标)intm_iTextPosition_LeftMine;//显示剩余雷数框左侧x坐标boolm_bIsPlayVoice;//是否播放声音intm_iGameType;//记录游戏类型 1初级2中级 3高级boolm_bIsSucced;//是否扫雷成功int m_iMineZeroNum;//表示已经发现的m_Mine[a][b].iArondMineNum==0和1的个数 用于判断成功扫雷int m_iFlagNum;//旗帜的数目boolm_bIsChangeColor;//是否改变界面显示颜色bool m_bIsPlayMusic;//是否播放音乐//播放背景音乐相关unsignedlong int m_ulCount;//配置文件 start//扫雷排行榜intm_iPrimaryRecord;intm_iMiddleRecord;intm_iHighRecord;CStringm_strPrimaryName;CStringm_strMiddleName;CStringm_strHighName; //背景颜色的配置 设置项//有颜色的背景int m_iHaveColor_RGBParam1;intm_iHaveColor_RGBParam2;intm_iHaveColor_RGBParam3;//无颜色背景intm_iNoColor_RGBParam1;intm_iNoColor_RGBParam2;intm_iNoColor_RGBParam3;//配置文件 end添加鼠标消息响应函数:protected:afx_msg voidOnTimer(UINT nIDEvent); //计时器函数afx_msg voidOnLButtonDown(UINT nFlags, CPoint        point);//鼠标按下左键函数afx_msg voidOnLButtonUp(UINT nFlags, CPoint point);  //鼠标左键弹起消息响应函数afx_msg voidOnRButtonDown(UINT nFlags, CPoint       point);//鼠标按下右键函数说明:关于鼠标左键 右键 按下弹起的消息响应函数,在这个工程中有三处需要添加代码。函数原型,函数实现,以及用来关联消息和消息响应的宏。在后续将会针对鼠标消息响应做总结,这里不再多做叙述。public:void Minezero();//这个位置周围雷数为0         void OnStart(); void LoadBitmap();void OnInitializeMine(MINE_LEVELminelevel, bool bIsSelfDefine,int rownum,int colnum,int minenum);//布雷并对雷区初始化注:当第二个参数为false时,后三个参数无效void DrawBoard(CDC*pDC);//绘制棋盘void DrawAndShowBitmap(CDC*pDC);//绘制并显示对应的位图 雷区格子和表情按钮   void ShowLeftMineNum(CDC*pDC);void ShowTime(CDC*pDC);void LoadConfig();voidOnHeroProcess();//成功扫雷后 英雄榜的实现处理boolIsSucced();//扫雷是否成功voidOnColorProcess(CDC* pDC);//颜色对话框处理 DWORDgetinfo(DWORD item);//播放背景音乐的辅助函数//菜单消息响应应函数afx_msg voidOnGameStart();afx_msg voidOnGamePrimary();afx_msg voidOnGameMiddle();afx_msg voidOnGameHigh();afx_msg voidOnGameVoice();afx_msg voidOnGameSelfdefine();afx_msg voidOnGameHenro();afx_msg voidOnGameExit();afx_msg voidOnGameColour();afx_msg voidOnGameMusic();afx_msg voidOnGameInstruction();

说明:3-3是程序中完整的头文件,包含了程序中所有的成员变量以及消息响应函数和功能函数,它们的定义就不在本文中叙述了,只讲主要的实现思路。

(4)功能实现

4-1构造函数成员变量的赋值,除此之外,构造函数是在程序运行时就开始执行的,因此构造函数中还包含了位图的加载函数,配置文件加载函数以及布雷并对雷区初始化函数。

位图的加载函数和配置文件加载函数已经做过介绍,这里简单介绍一下布雷并对雷区初始化函数voidOnInitializeMine(MINE_LEVEL minelevel, bool bIsSelfDefine,int rownum,intcolnum,int minenum)

第一个参数对应不同的级别,各个级别函数和列数不同在这个函数中对相应的行数,列数以及雷数赋值。这里面有个比较核心的问题:布雷。需要用到随机数,最开始试过以系统作为随机种子,调用CetCurrentTime(),然后获取描述,这样每一次获取的随机数都不同,但是这个在不同等级切换的时候会导致很卡,放弃了这种做法。以下面的方式处理:

srand((unsigned)time(0));//设置m_iMineNum个雷do {  //以当前秒数为产生随机算法int k=(rand())%m_ColCount; int l=(rand())%m_RowCount; //为了避免一个位置同时算两个雷//只允许当前位置不是雷时赋值为雷if(m_Mine[k][l].iArondMineNum != -1) {  //这个位置有雷m_Mine[k][l].iArondMineNum = -1;  aa++;  } }while(aa!=m_iMineNum); //给方格赋值,计算雷数for(int a=0;a<m_ColCount;a++){for(int b=0;b<m_RowCount;b++) {if(m_Mine[a][b].iArondMineNum==0)  {  for(int c=a-1;c<a+2;c++) {for(int d = b-1;d < b+2; d++){if(c>=0 && c< m_ColCount && d >= 0 && d < m_RowCount) {if(m_Mine[c][d].iArondMineNum == -1){m_Mine[a][b].iArondMineNum++;}}}}} }}

通过以上步骤即完成了随机布雷,它是扫雷中核心的一块。

4-2 Minezero 函数功能扫描,如果是已经被按下且雷数为0,显示它周围的八个格,并重画。扫雷中会出现这样的一种情况:地雷分布较分散,有一片一个地雷也没有,需要实现点击某一个格子而消失一片。要实现这种效果还需要添加函数Minezero(), 函数代码如下,它是在定时器中调用,每时每刻都在判断能扫实现扫雷扫一片的效果。

for(int i=0;i<m_ColCount;i++) {for(int j=0;j<m_RowCount;j++)  {//点击后是空白的情况if(m_Mine[i][j].iArondMineNum==0 && m_Mine[i][j].iBitMap==1)  {  for(int n=i-1;n<i+2;n++) {for(intm=j-1;m<j+2;m++){if(n >= 0 && n < m_ColCount/*25*/ && m >= 0 && m < m_RowCount)  {if(m_Mine[n][m].iArondMineNum !=-1 && m_Mine[n][m].iBitMap == 0)  { m_Mine[n][m].iBitMap=1; //不必扫雷且没有雷的格子的数目m_iMineZeroNum++;CRect rect;  rect.left = n*15+10;  rect.right = n*15+25;rect.top = m*15+50;  rect.bottom = m*15+65;  InvalidateRect(&rect); }}}}}}}

4-3 IsSucced:判断是否扫雷成功函数。判断逻辑相应级别的格子数(行*列是否等于被标记的雷数(用一个变量表示)加上成功打开的格子数),成功打开的格子数即为成员变量m_iMineZeroNum,被标记的雷数即为 m_iFlagNum;//旗帜的数目
在鼠标左键按下时,不是地雷,就执行 m_iMineZeroNum++,另外一处就是在函数Minezero中,自动扫除的格子数。这个函数在实现扫雷排行榜至关重要,后面会进一步提到。m_iFlagNum在点击右键的时候完成m_iFlagNum++,再点一次完成m_iFlagNum--。这里还要注意的时候,在开始函数中不要忘了将他们初始化为0,否则会出这样的一种情况,在从菜单中选择不同的等级的时候,可以正常操作,但是从点击表情按钮开始的时候,这两个值会从上一次的基础上(通常不为0)继续++ 或--,这样判断成功或失效。原因是从菜单项开始的时候会调用OnInitializeMine,这个函数中已经将他们置为0,所以是正常的,点击表情按钮是调用OnStart函数,因此在这个函数中也必须将他们置为0.当然我的程序是这样做的,你可以有其他的方法,只是不要忘了初始化。另外判断成功的方法还有其他的,像只要右键点击正确的雷的位置就算成功,也是可以的,好像这种方法更好,也可以是不同等级的雷数加上打开雷区格子的数目等于雷区格子的总数,总之不唯一,根据自己的设计思路即可。

4-4 OnTimer 计时器函数,实现部分如下:

//结束,返回if(m_iEnd==1){       return; }//     显示个数为0的方格        Minezero();        OnHeroProcess();       if(m_iTimeStart>=20)        {              m_iTimeStart=1;               m_sTime++;        /*           。。。。        重画时间框 对应不同的级别前面已做过介绍           */       注意再次刷新这个边框else{       m_iTimeStart++;}

说明:关于定时器的详细介绍见:MFC中定时器的使用

4-5 鼠标消息响应函数

4.5.1 OnLButtonDown控制逻辑:点击到雷区(函数参数的坐标点会自动识别),是地雷,结束(其他如时间,表情按钮,剩余雷数等做相应的处理),不是雷区,界面上点击有反应的地方只有雷区和表情按钮,是表情按钮,重新开始。

4.5.2 OnLButtonUp控制逻辑:判断按下的是不是雷,是,显示踩雷标志位图,表情按钮显示张口那一幅;不是,表情按钮显示笑脸,位图显示相应的字数或空白

4.5.3 OnRButtonDown控制逻辑:

m_Mine[a][b].iBitMap=2;//显示旗帜 3显示问号  两个之间切换

注意1:重画点击的格子以及剩余雷数的方框

说明:关于鼠标左键 右键 按下弹起的消息响应函数,在这个工程中有三处需要添加代码。函数原型,函数实现,以及用来关联消息和消息响应的宏。在后续将会针对鼠标消息响应做总结,这里不再多做叙述。

头文件中在两个AFX_MSG注释宏之间是消息响应函数原型的声明。源文件中有两处:一处是在两个AFX_MSG_MAP注释宏之间的消息映射宏,通过这两个宏把消息与消息响应函数关联起来;另一处是源文件中的消息响应函数的实现代码。本例中,以鼠标左键按下消息响应为例,CMineSweepingView类对 WM_LBUTTONDOWN消息响应的三处信息分别如下所示:

1)在头文件(CSaoLei2View.h)中声明消息响应函数原型

//{{AFX_MSG(CSaoLei2View)   //注释宏

afx_msg voidOnLButtonDown(UINT nFlags, CPoint point);

//}}AFX_MSG   //注释宏

说明:

在注释宏之间的声明在VC中灰色显示。afx_msg宏表示声明的是一个消息响应函数。

2)在源文件(CSaoLei2View.cpp)中进行消息映射

BEGIN_MESSAGE_MAP(CDrawView,CView)

//{{AFX_MSG_MAP(CDrawView)

ON_WM_LBUTTONDOWN()

//}}AFX_MSG_MAP

// Standardprinting commands

ON_COMMAND(ID_FILE_PRINT,CView::OnFilePrint)

ON_COMMAND(ID_FILE_PRINT_DIRECT,CView::OnFilePrint)

ON_COMMAND(ID_FILE_PRINT_PREVIEW,CView::OnFilePrintPreview)

END_MESSAGE_MAP()

说明:

在宏BEGIN_MESSAGE_MAP()与END_MESSAGE_MAP()之间进行消息映射。

宏ON_WM_LBUTTONDOWN()把消息WM_LBUTTONDOWN与它的响应函数OnLButtonDown()相关联。这样一旦有消息的产生,就会自动调用相关联的消息响应函数去处理。

宏ON_WM_LBUTTONDOWN()定义如下:

#defineON_WM_LBUTTONDOWN() \

{WM_LBUTTONDOWN, 0, 0, 0, AfxSig_vwp, \

(AFX_PMSG)(AFX_PMSGW)(void(AFX_MSG_CALL CWnd::*)(UINT, CPoint))&OnLButtonDown }

3)源文件中进行消息响应函数处理。(CSaoLei2View.cpp中自动生成OnLButtonDown函数轮廓,如下)

voidCDrawView::OnLButtonDown(UINT nFlags, CPoint point)

{

// TODO: Addyour message handler code here and/or call default

CView::OnLButtonDown(nFlags,point);

}

其他的 鼠标左键弹起、鼠标右键按下、鼠标右键弹起和鼠标左键按下消息响应的处理步骤完全一致,不再重述。

以上即为鼠标消息响应函数的全部内容。

4-6菜单消息响应函数

4.6.1开始:函数中重新对雷区初始化并布雷,对界面不操作,点击即可进行扫雷

4.6.2 初级、中级和高级 函数处理基本一致,除了一个成员变量m_MineLevel,不同的级别给这个变量赋相应的值。再改变窗口位置大小(前面已介绍)调用初始化并布雷函数,在调用OnDraw函数和OnStart函数

4.6.3自定义:自定义需要创建一个对话框。步骤:切换到资源视图——Dialog——添加Dialog,属性栏将这个对话框的ID设为IDD_DIALOG_SELFDEFINE,Caption设为自定义雷区,双击这个对话框,根据类导向,创建一个类,类名为SelfDefineMineArea 继承自Cdialog,然后添加三个静态文本控件、三个文本编辑框,静态文本框的属性中Caption自上而下写行数,列数和雷数。对应的三个文本编辑框中添加3个对应的成员变量

m_uiMineAreaRowCount,m_uiMineAreaColCount,m_uiMineNum

然后再在MineSweepingView.cpp中先添加头文件的引用

//自定义扫雷窗口

#include"SelfDefineMineArea.h"

在自定义消息响应函数OnGameSelfdefine()中添加代码:

if(IDOK == dlg.DoModal()){m_ColCount=dlg.m_uiMineAreaRowCount;m_RowCount=dlg.m_uiMineAreaColCount;m_iLeftMineNum=dlg.m_uiMineNum;}

注:以上只是说明两个界面中数值传递的一个过程,并不说明实际就是这样的。其实实际程序还需要

添加边界处理,当输入的行数或者列数都小于9时,都按照初级处理。但这三行代码是必须位置和时机不定,以实际为准。后面的步骤和4.6.2一致。关于主窗口和客户区在这种情况下可能出现的对齐会问题在前面已做过介绍,不再重复。

注:这四个菜单项在当前界面只有一个被选中,这种实现方式涉及到菜单的操作,后续再做介绍,下面的颜色和声音也是一样的。

4.6.4 颜色 通过一个布尔型的成员变量来控制是否选中背景色,在Ondraw函数中添加对这个变量的两种处理方式即可。

4.6.5 声音 播放声音的函数已经做过介绍,播放时机是在鼠标按下后,判断是否是点击的雷区,是,播放爆炸声;不是,播放另一种声音。

4.6.6 音乐 子菜单中的开始和停止为背景音乐控制项。特点选择本地的音乐。具体的实现代码可以参考:MFC声音的播放和录音的实现(一)

4.6.7 扫雷英雄榜  实现这一功能,需要再添加两个对话框并创建相应的类。具体的操作方式参照4.6.3。


图4-6-1保存姓名的界面


图4-6-2扫雷不同等级最高纪录信息

图4-6-1 所示的是类SettingName 对应的对话框,当成功扫雷时,会判断当前扫雷用时和这项纪录保持者所费时间(存储在配置文件,后读出来保存在了相应的成员变量中)进行比较,只有时间比这项纪录保持者用的时间小,才会弹出这个界面,操作者输入自己的名字,点击OK后,会将名字和当前成功扫雷的时间同时写入配置文件中的对应项中,下次进入到图4-6-2界面时,就会显示这项纪录和创作者。 

图4-6-2是由 DlgHero类对应的 对话框,负责显示出各个级别的最高纪录和这项纪录的保持者。这一块逻辑有一点绕,将完整的代码贴在下面,仅供参考。

在CSettingName对话框中添加一个文本编辑控件,并添加成员变量 CStringm_strName;

只保留确定按钮,删除掉取消按钮。

在CDlgHero对话框中添加9个静态文本控件:左侧的三个显示用,只需要改变对应属性的Caption,自上而下依次为初级、中级、高级;中间三个分别对应初级、中级、高级下的最高纪录(时间),先编辑对应ID,依次(自上而下)添加成员变量  

UINT m_sTimePrimaryRecord;UINT m_sTimeMiddleRecord;UINT m_sTimeHighRecord;

右边三个分别对应初级、中级、高级下的最高纪录的保持者的名字,编辑对应的ID,依次(自上而下)添加成员变量  

CString m_sPrimaryHolder;CString m_sMiddleHolder;CString m_sHighHolder;

然后再改变取消按钮的名称和ID,在它的属性栏中编辑,caption改为重置,ID改为IDR_RESET

然后对这个控件添加消息处理函数,函数代码为:

void CDlgHero::OnBnClickedReset(){m_sTimePrimaryRecord=0;m_sTimeMiddleRecord=0;m_sTimeHighRecord=0;m_sPrimaryHolder=_T("佚名"); m_sMiddleHolder =_T("佚名"); m_sHighHolder=_T("佚名"); PROGRAM_PATH 配置文件在本地中的绝对路径名称(路径问题前面的博文已经做了详细介绍)//写入初级成功扫雷破纪录的人和所用时间CString strTemp;strTemp.Format(_T("%d"),m_sTimePrimaryRecord);::WritePrivateProfileString(_T("Primary"), _T("NamePrimary"),m_sPrimaryHolder,PROGRAM_PATH);::WritePrivateProfileString(_T("Primary"), _T("TimePrimary"),strTemp,PROGRAM_PATH);//写入初级成功扫雷破纪录的人和所用时间strTemp.Format(_T("%d"),m_sTimeMiddleRecord);::WritePrivateProfileString(_T("Middle"), _T("NameMiddle"),m_sMiddleHolder,PROGRAM_PATH);::WritePrivateProfileString(_T("Middle"), _T("TimeMiddle"),strTemp,PROGRAM_PATH);//写入初级成功扫雷破纪录的人和所用时间strTemp.Format(_T("%d"),m_sTimeHighRecord);::WritePrivateProfileString(_T("High"), _T("NameHigh"),m_sHighHolder,PROGRAM_PATH);::WritePrivateProfileString(_T("High"), _T("TimeHigh"),strTemp,PROGRAM_PATH);}排行榜处理函数中添加如下代码:void CMineSweepingView::OnHeroProcess(){//扫雷成功后 扫雷英雄榜的处理PROGRAM_PATH 配置文件在本地中的绝对路径名称(路径问题前面的博文已经做了详细介绍)//初级if (m_iLeftMineNum == 0 && IsSucced()){KillTimer(1);AfxMessageBox(_T("扫雷成功!"));//如果是初级if (m_iGameType == 1){//比较当前用时和从配置文件中读取的初级用时比较if (m_sTime < m_iPrimaryRecord)//等于还不算打破记录{CSettingName dlg;if (IDOK == dlg.DoModal()){if(dlg.m_strName == _T("")){m_strPrimaryName=_T("佚名");}else{m_strPrimaryName = dlg.m_strName;}}m_iPrimaryRecord = m_sTime;}//写入初级成功扫雷破纪录的人和所用时间CString strTemp;strTemp.Format(_T("%d"),m_iPrimaryRecord);::WritePrivateProfileString(_T("Primary"), _T("NamePrimary"),m_strPrimaryName,PROGRAM_PATH);::WritePrivateProfileString(_T("Primary"), _T("TimePrimary"),strTemp,PROGRAM_PATH);}//如果是中级if (m_iGameType == 2&& IsSucced()){//比较当前用时和从配置文件中读取的初级用时比较if (m_sTime < m_iMiddleRecord)//等于还不算打破记录{CSettingName dlg;if (IDOK == dlg.DoModal()){if(dlg.m_strName == _T("")){m_strPrimaryName=_T("佚名");}else{m_strMiddleName = dlg.m_strName;}}m_iMiddleRecord = m_sTime;}//写入初级成功扫雷破纪录的人和所用时间CString strTemp;strTemp.Format(_T("%d"),m_iMiddleRecord);::WritePrivateProfileString(_T("Middle"), _T("NameMiddle"),m_strMiddleName,PROGRAM_PATH);::WritePrivateProfileString(_T("Middle"), _T("TimeMiddle"),strTemp,PROGRAM_PATH);}//如果是高级if (m_iGameType == 3&& IsSucced()){//比较当前用时和从配置文件中读取的初级用时比较if (m_sTime < m_iHighRecord)//等于还不算打破记录{CSettingName dlg;if (IDOK == dlg.DoModal()){if(dlg.m_strName == _T("")){m_strHighName=_T("佚名");}else{m_strHighName = dlg.m_strName;}}m_iHighRecord = m_sTime;}//写入初级成功扫雷破纪录的人和所用时间CString strTemp;strTemp.Format(_T("%d"),m_iHighRecord);::WritePrivateProfileString(_T("High"), _T("NameHigh"),m_strHighName,PROGRAM_PATH);::WritePrivateProfileString(_T("High"), _T("TimeHigh"),strTemp,PROGRAM_PATH);}}}
关于MFC中获取系统当前路径参考:http://blog.csdn.net/shufac/article/details/19937753

4-6-8 退出 调用函数 AfxGetMainWnd()->PostMessage(WM_CLOSE,0, 0)

4-6-9  关于 修改CAboutDlg即可,在这个界面中添加一个超链接。具体的实现方法参考:MFC 中创建简单超链接   

4-6-10  显示雷区地图  图4-6-3所示的是当前雷区的布局情况,-1表示雷的位置,其他的数字表示以这个格子为中心,周围8个格子所包含的雷数。


图4-6-3当前雷区地雷分布示意图

注:这个需要再添加一个界面,步骤和前面已经详细介绍的扫雷排行榜步骤是一致的。在响应的菜单消息响应函数中添加下面的代码:

CCheatMap dlg;CString map_str=_T("");CString tmp_str=_T("");for(int i=0;i<m_ColCount;i++){for(int j=0;j<m_RowCount ;j++){tmp_str=_T("");//指定格式化宽度tmp_str.Format(_T("%-4d  "),m_Mine[j][i].iArondMineNum);map_str+=tmp_str;}map_str+=_T("\r\n");}dlg.m_strtemp=map_str;dlg.DoModal();
关于数值在编辑框中的显示,可以做一下总结,最开始的时候想用列表框控件来实现,但是没有实现,后续再关于控件做一个总结。

4-6-11 说明  打开游戏的说明。介绍配置项含义,以及程序的操作功能。
调用函数ShellExecute(NULL,_T("open"), PROGRAM_PATH, NULL, NULL, SW_SHOW)
PROGRAM_PATH即为需要打开的文件路径,路径问题见4-6-7中的链接介绍。

根据以上的设计实现思路,已完成了一个功能比价完整的扫雷程序。有兴趣的可以下载试玩。
下载地址:http://download.csdn.net/download/shufac/6983547
(待完善)

1 0
原创粉丝点击