扫雷 MFC实现

来源:互联网 发布:中国创新能力知乎 编辑:程序博客网 时间:2024/06/02 15:44


1.   1.   游戏实现

 

扫雷,是附带在Window里面的游戏,是个简单的游戏。因此我们就从扫雷开始我们的游戏旅程。很多人都玩过这个游戏,只是不知道怎么用程序实现。不过还有人不知道怎么玩,下面就先说说游戏的规则:

 

●      开始:按左键开始游戏,按按钮或菜单重新开始。

●       左键:按下时,是雷则结束,非雷则显示数字。

●       数字:代表此数字周围一圈八格中雷的个数。

●       右键:奇次按下表示雷,偶数按下表示对上次的否定。

●       结束:左键按到雷结束,找出全部雷结束。

 

接下来就该介绍游戏的编程过程了。不过要先交代一下一些内容。

 

●       添加位图。

●        添加全局变量。

●       画初始界面。

●       添加函数。

 

为什么要按这种次序呢?因为我们在画初始界面时,可能要用到位图或变量,而变量的定义又可能要对位图进行定义。这样的步骤的好处还有:在做一步之后都可以运行,有错就改,无错就做下一步。

上图是扫雷的一个画面。

下面就一步一步地演示,以编程的思路进行,当然,由于编程过程中有一些函数中的代码是分成两三次写的,我们就不重复,全部代码在第一次讲到时列出,而后面讲到时就只是提一下。

 

新建单文档工程2_1。

 

2.   2.   资源编辑

 

添加位图:

 

前十二幅是在雷区的,后四幅是按钮。为了便于加载,必须各自保证其连续性。另外,为什么不添加一个按钮而用位图呢?是因为即使我们添加了按钮也要添加四幅位图!

 

位图的ID号:

按扭位图:    30*30     IDB_ANNIU1、IDB_ANNIU 2、IDB_ANNIU3、 IDB_ANNIU4

雷区位图:    14*14     ID号按下图依次为:IDB_BITMAP14。。。。。。IDB_BITMAP25

 

位图:下图(图2-1)。

 

                             图2-1

 

3.   3.   变量函数

 

定义新类:

 

对于雷,我们是单独定义一个类,这样有利于程序的操作。

       class Lei

{

public:

    //显示哪一个位图

       int weitu;

    //这个位置相应的值

       int shumu;

};

并有如下规定(图2-2):

 

                         图2-2

                                                                                 

视图类变量:

 

接着是在View类添加变量和函数:

 

//剩下雷数

intleftnum;

//雷数

       int leinum;     

//结束    

       int jieshu;

//计时

       short second;

//开始计时

       int secondstart;

//位图数组

       CBitmap m_Bitmap[12];

//按扭位图数组

       CBitmap m_anniu[4];

//雷区行数

       int m_RowCount;

//雷区列数

       int m_ColCount;

//最大雷区

              Lei lei[50][50];

 

    //这个位置周围雷数为0

       void leizero();

    //计时器函数

       afx_msg void OnTimer(UINT nIDEvent);

    //鼠标按下左键

       afx_msg void OnLButtonDown(UINT nFlags,CPoint point);

//鼠标按下右键

       afx_msg void OnRButtonDown(UINT nFlags,CPoint point);

    //初始化函数

       afx_msg int OnCreate(LPCREATESTRUCTlpCreateStruct);

    //鼠标左键松开

       afx_msg void OnLButtonUp(UINT nFlags,CPoint point);

 

4.   4.   具体实现

 

删去状态栏和工具栏:

 

开始执行程序,就能见到一个有状态栏和工具栏的大的单文档,与上图不同,所以我们第一步就是整理框架:

打开下面函数,把里面的一些语句去掉。如下所示:

    int CMainFrame::OnCreate(LPCREATESTRUCTlpCreateStruct)

{

       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("Failed to createtoolbar\n");

              return -1;      // fail to create

       }

 

       if (!m_wndStatusBar.Create(this) ||

              !m_wndStatusBar.SetIndicators(indicators,

               sizeof(indicators)/sizeof(UINT)))

       {

              TRACE0("Failed to createstatus bar\n");

              return -1;      // fail to create

       }

 

       // TODO: Delete these three lines if youdon't want the toolbar to

       // be dockable

       m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY);

       EnableDocking(CBRS_ALIGN_ANY);

       DockControlBar(&m_wndToolBar);

*/

return0;

}

 

设置窗口大小:

 

运行附加的代码,还能看到扫雷游戏的框架是不能调大小的,而且总是显示在最前面,这又是怎么实现的呢?      

在下面函数里添加语句,你能说出前三句是什么意思吗?注释已经被我去掉了,如果不知道,不如按一下F1。

                              

 BOOLCMainFrame::PreCreateWindow(CREATESTRUCT& cs)

{

       if( !CFrameWnd::PreCreateWindow(cs) )

              return FALSE;

       // TODO: Modify the Window class orstyles here by modifying

       // the CREATESTRUCT cs

       cs.dwExStyle=cs.dwExStyle|WS_EX_TOPMOST;      //

       cs.style=WS_SYSMENU|WS_OVERLAPPED|WS_MINIMIZEBOX;//;

       //设置窗口大小:400*340

       cs.cx=400;

       cs.cy=340;  

       return TRUE;

}

 

构造函数:

 

由于构造函数是程序运行时就执行的,所以,除了对变量赋值之外,我们还可以把游戏的核心结构即内部数组赋值:先是把全部格子的位图和雷数赋值为0,然后调用随机函数按指定雷数赋值为-1,最后把不是雷的格子的雷数赋值为相应的值。

 

CMy2_1View::CMy2_1View()

{

     // TODO: add construction code here

     for(int ii=0;ii<16;ii++)

            m_Bitmap[ii].LoadBitmap(IDB_BITMAP14+ii);

    for(int jj=0;jj<4;jj++)

            m_anniu[jj].LoadBitmap(IDB_ANNIU1+jj);

    //计时

second=0;

      //1时开始计时

secondstart=0;

    //行数

m_RowCount=25;

      //列数

m_ColCount=16;

//雷数

    leinum=80;

      //剩余雷数

leftnum=leinum;

      //jieshu=1时停止

jieshu=0;

 

      intaa=0;

 

     //初始化为0

     for(int i=0;i<m_RowCount;i++)

     {

            for(int j=0;j<m_ColCount;j++)

            {

                   lei[i][j].shumu=0;

                   lei[i][j].weitu=0;

            }

     }

     //获取当前时间

     CTime time=GetCurrentTime();

     int s;

     //获取秒数

     s=time.GetSecond();

     //设置40个雷

     do

     {

            //以当前秒数为产生随机算法

            int k=(rand()*s)%m_RowCount;

            int l=(rand()*s)%m_ColCount;

       //为了避免一个位置同时算两个雷

       //只允许当前位置不是雷时赋值为雷

            if(lei[k][l].shumu!=-1)

            {

                   lei[k][l].shumu=-1;

               aa++;

            }

      

     }while(aa!=leinum);  

    //给方格赋值,计算雷数

     for(int a=0;a<m_RowCount;a++)

            for(int b=0;b<m_ColCount;b++)

                   if(lei[a][b].shumu==0)

                   {

                          for(intc=a-1;c<a+2;c++)

                                 for(intd=b-1;d<b+2;d++)

                                        if(c>=0&&c<m_RowCount&&d>=0&&d<m_ColCount)

                                               if(lei[c][d].shumu==-1)

                                                      lei[a][b].shumu++;       

                   }

}

 

界面函数:

 

现在,可以开始画界面了。如下函数:

很明显,前面部分是用画的方法画出整个界面,但是,后面for循环显示的位图并不是现在画界面的内容,为什么要写呢?

这是为了用户框重画的需要,当我们的游戏玩了一半后最小化,或是把部分窗口移出屏幕,或是执行了新的应用程序覆盖了原来的程序时,必须重画。我们调用重画函数,它都要重新执行OnDraw(CDC*pDC)函数,那么,此时它就必须把已经显示出来的位图也显示出来。而开始时雷区位图是不可见的,并不影响界面的初始化。

 

    void CMy2_1View::OnDraw(CDC* pDC)

{

       CMy2_1Doc* pDoc = GetDocument();

       ASSERT_VALID(pDoc);

       // TODO: add draw code for native datahere

       //画背景

    CBrush mybrush1;

      mybrush1.CreateSolidBrush(RGB(192,192,192));

      CRectmyrect1(0,0,1200,800);

      pDC->FillRect(myrect1,&mybrush1);

  //画黑框

       CBrush mybrush;

      mybrush.CreateSolidBrush(RGB(0,0,0));

      CRectmyrect(20,10,70,40);

      pDC->FillRect(myrect,&mybrush);

 

       CRect myrect2(325,10,375,40);

      pDC->FillRect(myrect2,&mybrush);

 

       CPen mypen;

       CPen*myoldPen;

       mypen.CreatePen(PS_SOLID,2,RGB(255,255,255));

    myoldPen=pDC->SelectObject(&mypen);

//画黑框的白线

       pDC->MoveTo(20,40);

       pDC->LineTo(70,40);

       pDC->LineTo(70,10);

       pDC->MoveTo(325,40);

       pDC->LineTo(375,40);

       pDC->LineTo(375,10);

//画雷区边线

//左上角是白线,右下角是黑线,以显示立体感

    for(int i=0;i<m_RowCount;i++)

              for(int j=0;j<m_ColCount;j++)

              {

                     pDC->MoveTo(10+i*15,50+j*15+14);

                     pDC->LineTo(10+i*15,50+j*15); 

                     pDC->LineTo(10+i*15+14,50+j*15);

              }

       pDC->SelectObject(myoldPen);

 

       CPen mypen2;

       CPen*myoldPen2;

       mypen2.CreatePen(PS_SOLID,1,RGB(0,0,0));

   myoldPen2=pDC->SelectObject(&mypen2);

    for(int ii=0;ii<m_RowCount;ii++)

              for(intjj=0;jj<m_ColCount;jj++)

              {

                     pDC->MoveTo(10+ii*15,50+jj*15+14);

                     pDC->LineTo(10+ii*15+14,50+jj*15+14);  

                     pDC->LineTo(10+ii*15+14,50+jj*15);

              }

       pDC->SelectObject(myoldPen2);

 

              CDC Dc;

      if(Dc.CreateCompatibleDC(pDC)==FALSE)

              AfxMessageBox("Can't create DC");

        //显示按钮

        Dc.SelectObject(m_anniu[0]);

        pDC->BitBlt(180,10,160,160,&Dc,0,0,SRCCOPY);

     //判断显示什么位图

        //weitu=1已按下的数字区

        //weitu=2显示旗

        //weitu=3显示问号

        for(int a=0;a<m_RowCount;a++)

               for(int b=0;b<m_ColCount;b++)

                     {

                            if(lei[a][b].weitu==1)

                            {

                    Dc.SelectObject(m_Bitmap[lei[a][b].shumu]);

                                   pDC->BitBlt(a*15+10,b*15+50,160,160,&Dc,0,0,SRCCOPY);

                            }

                            if(lei[a][b].weitu==2)

                            {

                                   Dc.SelectObject(m_Bitmap[9]);

                                   pDC->BitBlt(a*15+10,b*15+50,160,160,&Dc,0,0,SRCCOPY);

                            }

                            if(lei[a][b].weitu==3)

                            {

                                   Dc.SelectObject(m_Bitmap[10]);

                                   pDC->BitBlt(a*15+10,b*15+50,160,160,&Dc,0,0,SRCCOPY);

                            }

                            //结束

                            if(jieshu==1&&lei[a][b].shumu==-1)

                            {

                   Dc.SelectObject(m_Bitmap[11]);

                                   pDC->BitBlt(a*15+10,b*15+50,160,160,&Dc,0,0,SRCCOPY);

                                   Dc.SelectObject(m_anniu[3]);

                                   pDC->BitBlt(180,10,160,160,&Dc,0,0,SRCCOPY);

                            }

                           

                     }

 

//显示黑框里的数字

       int nOldDC=pDC->SaveDC();

       pDC->SetTextColor(RGB(255,0,0));

       pDC->SetBkColor(RGB(0,0,0));

       CFont font;                                             

       if(0==font.CreatePointFont(160,"ComicSans MS"))

       {

              AfxMessageBox("Can't CreateFont");

       }

       pDC->SelectObject(&font);

       CString str; 

    //利用判断显示位数,不够三位前面加0

       if(leftnum<10)

              str.Format("00%d",leftnum);

       else

              str.Format("0%d",leftnum);                              

       pDC->TextOut(25,10,str);

       if(second<10)

              str.Format("00%d",second);

       else if(second<100)

                     str.Format("0%d",second);

              else

                     str.Format("%d",second);

       pDC->TextOut(330,10,str);

       pDC->RestoreDC(nOldDC);

                      

}

运行一下,外观已经出来了,只是还不能玩。那我们就来添加一些功能函数,使它可以玩。

当然,如果你对程序已经有一定的经验的话,你就会指出上面的函数太长了。这并不太符合我们编程的要求。我们编程有一个讲究,就是尽量使函数的代码少,一般为一页左右,便于查看。那么,我们可以把上面的函数细分为几个小函数,然后在这个函数里面分别调用。

 

按下鼠标左键:

 

用if语句判断,如果在按钮上面,则显示按钮按下位图;如果在扫雷区,先把按钮位图改为张口位图,再判断按下的是否是雷,是就结束,重画,以显示所有的雷;否则,重画相应格子以显示数字。

 

void CMy2_1View::OnLButtonDown(UINT nFlags,CPoint point)

{

    //TODO: Add your message handler code here and/or call default    

    //获取指针pdc

     CDC*pDC=GetDC();

     CDC Dc;

    if(Dc.CreateCompatibleDC(pDC)==FALSE)

           AfxMessageBox("Can't create DC");

     //显示按下按钮

     if(point.x>180&&point.x<210&&point.y>10&&point.y<40)

     {

        Dc.SelectObject(m_anniu[3]);

        pDC->BitBlt(180,10,160,160,&Dc,0,0,SRCCOPY);         

     }

   if((point.x>=10)&&(point.x<=385)&&(point.y>=50)&&(point.y<=290))

    {                  

           if(jieshu==1)

                  return;

 

            //显示张口按钮

           Dc.SelectObject(m_anniu[1]);

           pDC->BitBlt(180,10,160,160,&Dc,0,0,SRCCOPY);           

        // secondstart为1时计时有效

           secondstart=1;

      //鼠标坐标转换为数组坐标

           inta=(point.x-10)/15;

           intb=(point.y-50)/15;

           if(lei[a][b].weitu==0||lei[a][b].weitu==3)

        {

               if(lei[a][b].shumu==-1)

                  {

                         jieshu=1;

         //结束时,释放Timer

KillTimer(1);

//重画,因为这次重画将显示全部的雷,

//不能用部分重画

                         Invalidate();

                  }

         else

                  {

                         lei[a][b].weitu=1;

                         CRectrect;

                         rect.left=a*15+10;

                         rect.right=a*15+25;

                         rect.top=b*15+50;

                         rect.bottom=b*15+65;

                         InvalidateRect(&rect);

                  }    

           }

          

    }     CView::OnLButtonDown(nFlags, point);

}

如果你现在运行的话,你会发现按下按钮时并不还原,这就涉及到鼠标函数:OnLButtonUp(UINTnFlags, CPoint point)

 

松开鼠标左键:

 

松开左键时,显示按钮没有按下的位图;再判断,如果结束,就要显示失败的位图;另外,如果是在按钮上松开按钮,即表示我们已经按下了重新开始的按钮,必须调用重新开始函数OnStart()。

由于OnStart()函数是与菜单里的开始共有的,此处先保留不说,若有必要运行,可以先去掉最后两行。

 

 voidCMy2_1View::OnLButtonUp(UINT nFlags, CPoint point)

{

    //TODO: Add your message handler code here and/or call default

  CDC*pDC=GetDC();

   CDC Dc;

   if(Dc.CreateCompatibleDC(pDC)==FALSE)

    AfxMessageBox("Can't create DC");

    //显示按钮

    Dc.SelectObject(m_anniu[0]);

    pDC->BitBlt(180,10,160,160,&Dc,0,0,SRCCOPY);

   

    if(jieshu==1)

    {

      //显示按扭位图

           Dc.SelectObject(m_anniu[2]);

           pDC->BitBlt(180,10,160,160,&Dc,0,0,SRCCOPY);

    }    

 

  //如果按下的是按扭,重新开始

    if(point.x>180&&point.x<210&&point.y>10&&point.y<40)

        OnStart();

 

    CView::OnLButtonUp(nFlags,point);

}

 

按下鼠标右键:

 

如果是雷,我们按右键,显示旗子,并减少一个剩下雷数;如果我们认为那旗子的格子不是雷,我们按右键,显示问号,并在剩下雷数加上1。有关函数:

 

void CMy2_1View::OnRButtonDown(UINT nFlags,CPoint point)

{

    //TODO: Add your message handler code here and/or call default

    //结束,返回

    if(jieshu==1)

           return;

    if((point.x>=10)&&(point.x<=385)&&(point.y>=50)&&(point.y<=290))

    {           

           inta=(point.x-10)/15;

           intb=(point.y-50)/15;

 

           if(lei[a][b].weitu==0||lei[a][b].weitu==3)   

           {

                  lei[a][b].weitu=2;                

                  leftnum--;      

          

           }

        else

                  if(lei[a][b].weitu==2)

                  {

                         lei[a][b].weitu=3;

                         leftnum++;

                  }

 

      //重画剩下雷数

           CRectrect2;

           rect2.left=20;

           rect2.right=70;

           rect2.top=10;

           rect2.bottom=40;

           InvalidateRect(&rect2);        

   

           //重画打击格子

           CRectrect;

        rect.left=a*15+10;

           rect.right=a*15+25;

           rect.top=b*15+50;

           rect.bottom=b*15+65;

           InvalidateRect(&rect);         

    }

    CView::OnRButtonDown(nFlags,point);

}

 

显示没有雷的区域:

 

运行,玩一下,你会发现当按下的是一个周围没有雷的格子是它并不会象Window里面的扫雷游戏一样显示它周围的格子雷数。怎么实现呢?

添加一个如下函数:

 

//扫描,如果是已经被按下且雷数为0,显示它周围的八个格,并重画

void CMy2_1View::leizero()

{

    for(inti=0;i<m_RowCount;i++)

           for(intj=0;j<m_ColCount;j++)

                  if(lei[i][j].shumu==0&&lei[i][j].weitu==1)

                  {

                         for(intn=i-1;n<i+2;n++)

                                for(intm=j-1;m<j+2;m++)

                                       if(n>=0&&n<25&&m>=0&&m<m_ColCount)

                                              if(lei[n][m].shumu!=-1&&lei[n][m].weitu==0)

                                              {

                                                     lei[n][m].weitu=1;

                                                     CRectrect;

                                                     rect.left=n*15+10;

                                                     rect.right=n*15+25;

                                                     rect.top=m*15+50;

                                                     rect.bottom=m*15+65;

                                                     InvalidateRect(&rect);         

                                              }

                  }

}

再运行,效果是有的,只是它只显示一部分,即这个周围的几个。那么我们应该怎样使它显示全部呢?可以利用计时器函数。

 

计时器函数:

 

OnTimer(UINT nIDEvent)函数,同时也可以实现计时显示。添加OnCreate(LPCREATESTRUCTlpCreateStruct)和OnTimer(UINT nIDEvent):

 

int CMy2_1View::OnCreate(LPCREATESTRUCTlpCreateStruct)

{

    if(CView::OnCreate(lpCreateStruct) == -1)

           return-1;

    //TODO: Add your specialized creation code here

 //20次为一秒

    SetTimer(1,50,NULL);

    return0;

}

 

void CMy2_1View::OnTimer(UINT nIDEvent)

{

    //TODO: Add your message handler code here and/or call default

    //结束,返回

    if(jieshu==1)

           return;

    //显示个数为0的方格

    leizero();

    //计时

    if(secondstart>0)

           secondstart++;

  //二十次为一秒

    if(secondstart==20)

    {

           secondstart=1;

           second++;      

           //重画时间

        CRect rect3;

           rect3.left=325;

           rect3.right=375;

           rect3.top=10;

           rect3.bottom=40;

           InvalidateRect(&rect3); 

    }

    CView::OnTimer(nIDEvent);

}

 

   扫雷游戏就这样就是了。下面是附加内容,它将说明菜单的添加和重新开始函数的算法。

 

 

5.   5.   附加内容

 

修改菜单:

 

游戏已经可以玩了,只是点到雷之后就完了,无法重新开始。还有,菜单还没有改。下面就修改菜单并实现重新开始功能:

把菜单改为如下图2-3。

 

                         图2-3

 

并在View()函数中按下图添加OnStart()函数(图2-4):

 

图2-4

 

开始函数:

 

OnStart()函数其实只是构造函数的再版。

 

    void CMy2_1View::OnStart()

{

SetTimer(1,50,NULL);

       // TODO: Add your command handler codehere

       second=0;//计时

       secondstart=0;//1时开始计时

//     num=0;

    leftnum=leinum;//剩余雷数

       jieshu=0;//jieshu=1时停止

       int aa=0;

       //初始化0

       for(int i=0;i<m_RowCount;i++)

       {

              for(int j=0;j<m_ColCount;j++)

              {

                     lei[i][j].shumu=0;

                     lei[i][j].weitu=0;

              }

       }

       //设置leinum个雷

       do

       {

              int k=rand()%m_RowCount;

              int l=rand()%m_ColCount;

              if(lei[k][l].shumu!=-1)

              {

                     lei[k][l].shumu=-1;

                 aa++;

              }

        

       }while(aa!=leinum);  

    //给方格赋值

       for(int a=0;a<m_RowCount;a++)

              for(int b=0;b<m_ColCount;b++)

                     if(lei[a][b].shumu==0)

                     {

                            for(intc=a-1;c<a+2;c++)

                                   for(intd=b-1;d<b+2;d++)

                                          if(c>=0&&c<m_RowCount&&d>=0&&d<m_ColCount)

                                                 if(lei[c][d].shumu==-1)

                                                        lei[a][b].shumu++;       

                     }

       Invalidate();

 

}

 

这样,整个程序完成了。

 

 

6.   6.   小结

 

 

当然,这个游戏比Window自带的简单。但就目前来说,离它其实也不远。添加菜单项,并相应修改参数值:m_RowCount、       m_ColCount、leinum,并重新初始化界面就行了。   

本书的例子都只是一些最基本的游戏算法,它教你怎样去实现游戏,而至于游戏的扩展,我只是提一些建议,让你自己去实现。

 

原创粉丝点击