1、 第二章 第二章 扫雷 1. 1. 游戏实现 扫雷,是附带在Window里面的游戏,是个简单的游戏。因此我们就从扫雷开始我们的游戏旅程。很多人都玩过这个游戏,只是不知道怎么用程序实现。不过还有人不知道怎么玩,下面就先说说游戏的规则: ● 开始:按左键开始游戏,按按钮或菜单重新开始。 ● 左键:按下时,是雷则结束,非雷则显示数字。 ● 数字:代表此数字周围一圈八格中雷的个数。 ● 右键:奇次按下表示雷,偶数按下表示对上次的否定。 ●
2、 结束:左键按到雷结束,找出全部雷结束。 接下来就该介绍游戏的编程过程了。不过要先交代一下一些内容。 ● 添加位图。 ● 添加全局变量。 ● 画初始界面。 ● 添加函数。 为什么要按这种次序呢?因为我们在画初始界面时,可能要用到位图或变量,而变量的定义又可能要对位图进行定义。这样的步骤的好处还有:在做一步之后都可以运行,有错就改,无错就做下一步。 上图是扫雷的一个画面。 下面就一步一步地演示,以编程的思路进行,当然,由于编程过程中有一些函数中的代码是分成两三次写的,我们就不重复,全部代码在第一次讲到时列出,而后面讲到时就只是
3、提一下。 新建单文档工程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)。
4、 图2-1 3. 3. 变量函数 定义新类: 对于雷,我们是单独定义一个类,这样有利于程序的操作。 class Lei { public: //显示哪一个位图 int weitu; //这个位置相应的值 int shumu; }; 并有如下规定(图2-2): 图2-2 视图类变量:
5、 接着是在View类添加变量和函数: //剩下雷数 int leftnum; //雷数 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]; //
6、这个位置周围雷数为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(LPCREATESTRUCT lpCreateStruct); //鼠标左键松开
7、 afx_msg void OnLButtonUp(UINT nFlags, CPoint point); 4. 4. 具体实现 删去状态栏和工具栏: 开始执行程序,就能见到一个有状态栏和工具栏的大的单文档,与上图不同,所以我们第一步就是整理框架: 打开下面函数,把里面的一些语句去掉。如下所示: int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CFrameWnd::OnCreate(lpCreateStruct) == -1) return -1; /*
8、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 create toolbar\n"); return -1; // fail to create } if (!m_wndSta
9、tusBar.Create(this) || !m_wndStatusBar.SetIndicators(indicators, sizeof(indicators)/sizeof(UINT))) { TRACE0("Failed to create status bar\n"); return -1; // fail to create } // TODO: Delete these three lines if you don't want the toolbar to // be dockable m_wndToolBar
10、EnableDocking(CBRS_ALIGN_ANY); EnableDocking(CBRS_ALIGN_ANY); DockControlBar(&m_wndToolBar); */ return 0; } 设置窗口大小: 运行附加的代码,还能看到扫雷游戏的框架是不能调大小的,而且总是显示在最前面,这又是怎么实现的呢? 在下面函数里添加语句,你能说出前三句是什么意思吗?注释已经被我去掉了,如果不知道,不如按一下F1。 BOOL CMainFrame::PreCreat
11、eWindow(CREATESTRUCT& cs) { if( !CFrameWnd::PreCreateWindow(cs) ) return FALSE; // TODO: Modify the Window class or styles here by modifying // the CREATESTRUCT cs cs.dwExStyle=cs.dwExStyle|WS_EX_TOPMOST; // cs.style=WS_SYSMENU|WS_OVERLAPPED|WS_MINIMIZEBOX;//; //设置窗口大小:400*340 cs
12、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_BITMA
13、P14+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; int aa=0; //初始化为0 for(int
14、 i=0;i 15、nt;
//为了避免一个位置同时算两个雷
//只允许当前位置不是雷时赋值为雷
if(lei[k][l].shumu!=-1)
{
lei[k][l].shumu=-1;
aa++;
}
}while(aa!=leinum);
//给方格赋值,计算雷数
for(int a=0;a 16、c++)
for(int d=b-1;d=0&&c 17、盖了原来的程序时,必须重画。我们调用重画函数,它都要重新执行OnDraw(CDC* pDC)函数,那么,此时它就必须把已经显示出来的位图也显示出来。而开始时雷区位图是不可见的,并不影响界面的初始化。
void CMy2_1View::OnDraw(CDC* pDC)
{
CMy2_1Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// TODO: add draw code for native data here
//画背景
CBrush mybrush1;
mybrush1.CreateSoli 18、dBrush(RGB(192,192,192));
CRect myrect1(0,0,1200,800);
pDC->FillRect(myrect1,&mybrush1);
//画黑框
CBrush mybrush;
mybrush.CreateSolidBrush(RGB(0,0,0));
CRect myrect(20,10,70,40);
pDC->FillRect(myrect,&mybrush);
CRect myrect2(325,10,375,40);
pDC->FillRect(myrect2,&mybrush);
19、
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);
//画雷区边线
//左上角是白线,右下角是黑线,以显示立体感
20、
for(int i=0;i 21、 myoldPen2=pDC->SelectObject(&mypen2);
for(int ii=0;ii 22、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 23、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,S 24、RCCOPY);
}
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,16 25、0,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( 26、160,"Comic Sans MS"))
{
AfxMessageBox("Can't Create Font");
}
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 27、);
else if(second<100)
str.Format("0%d" ,second);
else
str.Format("%d" ,second);
pDC->TextOut(330,10,str);
pDC->RestoreDC(nOldDC);
}
运行一下,外观已经出来了,只是还不能玩。那我们就来添加一些功能函数,使它可以玩。
当然,如果你对程序已经有一定的经验的话,你就会指出上面的函数太长了。这并不太符合我们编程的要求。我们编程有一个讲究,就是尽量使函数的代码少,一般为一页左右,便于查看。那么,我们可以把上面的函数细分为几 28、个小函数,然后在这个函数里面分别调用。
按下鼠标左键:
用if语句判断,如果在按钮上面,则显示按钮按下位图;如果在扫雷区,先把按钮位图改为张口位图,再判断按下的是否是雷,是就结束,重画,以显示所有的雷;否则,重画相应格子以显示数字。
void CMy2_1View::OnLButtonDown(UINT nFlags, CPoint point)
{
// TODO: Add your message handler code here and/or call default
//获取指针pdc
CDC *pDC=GetDC();
29、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)&&(poi 30、nt.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;
//鼠标坐标转换为数组坐标
int a=(point.x-10)/15;
int b=(point.y-50)/15;
if(lei[a][b].we 31、itu==0||lei[a][b].weitu==3)
{
if(lei[a][b].shumu==-1)
{
jieshu=1;
//结束时,释放Timer
KillTimer(1);
//重画,因为这次重画将显示全部的雷,
//不能用部分重画
Invalidate();
}
else
{
lei[a][b].weitu=1;
CRect rect;
rect.left=a*15+10;
rect.right=a*15+25; 32、
rect.top=b*15+50;
rect.bottom=b*15+65;
InvalidateRect(&rect);
}
}
} CView::OnLButtonDown(nFlags, point);
}
如果你现在运行的话,你会发现按下按钮时并不还原,这就涉及到鼠标函数:OnLButtonUp(UINT nFlags, CPoint point)
松开鼠标左键:
松开左键时,显示按钮没有按下的位图;再判断,如果结束,就要显示失败的位图;另外,如果是在按钮上松开按钮,即表示我们已经按下了重新开始的按钮,必须 33、调用重新开始函数OnStart()。
由于OnStart()函数是与菜单里的开始共有的,此处先保留不说,若有必要运行,可以先去掉最后两行。
void CMy2_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 34、 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< 35、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(jies 36、hu==1)
return;
if((point.x>=10)&&(point.x<=385)&&(point.y>=50)&&(point.y<=290))
{
int a=(point.x-10)/15;
int b=(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)
{
le 37、i[a][b].weitu=3;
leftnum++;
}
//重画剩下雷数
CRect rect2;
rect2.left=20;
rect2.right=70;
rect2.top=10;
rect2.bottom=40;
InvalidateRect(&rect2);
//重画打击格子
CRect rect;
rect.left=a*15+10;
rect.right=a*15+25;
rect.top=b*15+50;
rect.bottom=b*15+65;
38、
InvalidateRect(&rect);
}
CView::OnRButtonDown(nFlags, point);
}
显示没有雷的区域:
运行,玩一下,你会发现当按下的是一个周围没有雷的格子是它并不会象Window里面的扫雷游戏一样显示它周围的格子雷数。怎么实现呢?
添加一个如下函数:
//扫描,如果是已经被按下且雷数为0,显示它周围的八个格,并重画
void CMy2_1View::leizero()
{
for(int i=0;i 39、)
if(lei[i][j].shumu==0&&lei[i][j].weitu==1)
{
for(int n=i-1;n=0&&n<25&&m>=0&&m 40、 rect.right=n*15+25;
rect.top=m*15+50;
rect.bottom=m*15+65;
InvalidateRect(&rect);
}
}
}
再运行,效果是有的,只是它只显示一部分,即这个周围的几个。那么我们应该怎样使它显示全部呢?可以利用计时器函数。
计时器函数:
OnTimer(UINT nIDEvent)函数,同时也可以实现计时显示。添加OnCreate(LPCREATESTRUCT lpCreateStruct)和 OnTimer(UI 41、NT nIDEvent):
int CMy2_1View::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CView::OnCreate(lpCreateStruct) == -1)
return -1;
// TODO: Add your specialized creation code here
//20次为一秒
SetTimer(1,50,NULL);
return 0;
}
void CMy2_1View::OnTimer(UINT nIDEvent)
{
// TODO: Add 42、 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;
43、 rect3.top=10;
rect3.bottom=40;
InvalidateRect(&rect3);
}
CView::OnTimer(nIDEvent);
}
扫雷游戏就这样就是了。下面是附加内容,它将说明菜单的添加和重新开始函数的算法。
5. 5. 附加内容
修改菜单:
游戏已经可以玩了,只是点到雷之后就完了,无法重新开始。还有,菜单还没有改。下面就修改菜单并实现重新开始功能:
把菜单改为如下图2-3。
图2-3
并在View()函数中 44、按下图添加OnStart()函数(图2-4):
图2-4
开始函数:
OnStart()函数其实只是构造函数的再版。
void CMy2_1View::OnStart()
{
SetTimer(1,50,NULL);
// TODO: Add your command handler code here
second=0;//计时
secondstart=0;//1时开始计时
// num=0;
leftnum=leinum;//剩余雷数
jieshu=0;//jieshu=1时停止
int aa=0;
// 45、初始化0
for(int i=0;i 46、le(aa!=leinum);
//给方格赋值
for(int a=0;a






