目录
学习C++以来,一直编写的都是控制台程序。于是决心要学习GDI编写一个桌面窗口应用程序,于是有了这个中国象棋。本人第一次编写这类程序,难免会有明显的错误,还望大佬轻喷,恳请批评指正。
一、桌面程序及GDI基础
wWinMain和WndProc
关于C++如何建立桌面应用程序,请参见:
https://blog.csdn.net/u011583927/article/details/54896961
在VS 2017 community中,有Windows桌面应用程序的模板,其中包含已经码好的窗口主函数wWinMain和窗口过程函数WndProc。可以简单理解为wWinMain负责接收消息,WndProc负责处理消息。
本程序主要在于WndProc的编写,WndProc具有以下结构
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
PAINTSTRUCT ps;
HDC hdc;
switch (message)
{
case WM_COMMAND:
{
int wmId = LOWORD(wParam);
// 分析菜单选择:
switch (wmId)
{
case IDM_ABOUT: //菜单-关于
DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
break;
case IDM_EXIT: //菜单-退出
DestroyWindow(hWnd);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
}
break;
case WM_PAINT:
{
hdc = BeginPaint(hWnd, &ps);
//绘制图像
EndPaint(hWnd, &ps);
}
break;
case WM_LBUTTONDOWN:
//鼠标点击事件
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
其中message即传入消息的类型,对于不同的消息类型,WndProc进行不同的动作。
关于Windows中的消息种类,请参见:
https://blog.csdn.net/luo_xianming/article/details/40452981
在本程序中,主要为两个事件:WM_PAINT和WM_LBUTTONDOWN
WM_PAINT一般发生在
- 创建窗口时(wWinMain调用UpdateWindow时)
- 改变窗口尺寸时(最大化窗口,最小化后还原窗口时)
此时要求窗口重画自己,需要在此时重新绘制窗口内容。
WM_LBUTTONDOWN 即鼠标左键按下事件,可以用GET_X_LPARAM(lParam)和GET_Y_LPARAM(lParam)获取此时鼠标在窗口中的坐标,从而判断用户点击了屏幕中的什么目标,而做出相应的响应。
GDI与棋盘的绘制
基础部分理解之后,开始程序的编写。首先是绘制棋盘,这里用到一些GDI绘图函数:
- MoveToEx 为指定的设备场景指定一个新的当前画笔位置
- LineTo 用当前画笔画一条线,从当前位置连到一个指定的点
- TextOut 文本绘图函数
- Ellipse 描绘一个椭圆,由指定的矩形围绕
- Rectangle 用当前选定的画笔描绘矩形,并用当前选定的刷子填充
- SetTextColor 设置当前输出文本颜色
更多GDI函数,请参见:
https://blog.csdn.net/gaojinshan/article/details/8277016
棋盘的参数和逻辑坐标如图示定义,左上角为(0,0),右下角为(8,9)。
因此,可以初步定义一个函数DrawAll来在每一次操作之后重新绘制棋盘:
#define PL 60
#define EDGE 50
HFONT hFont = CreateFont(30, 0, 0, 0, 0, 0, 0, 0, GB2312_CHARSET, 0, 0, 0, 0, TEXT("隶书"));
HPEN red = CreatePen(0, 3, RGB(255, 0, 0));
HPEN black = CreatePen(0, 3, RGB(0, 0, 0));
HPEN green = CreatePen(0, 2, RGB(0, 255, 0));
HPEN origin = CreatePen(0, 1, RGB(0, 0, 0));
//全局画笔和字体资源
void DrawLineLogic(HDC hdc, int x1, int y1, int x2, int y2)
//绘制从逻辑坐标(x1,y1)到(x2,y2)的直线
{
MoveToEx(hdc, EDGE + x1 * PL, EDGE + y1 * PL, 0);
LineTo(hdc, EDGE + x2 * PL, EDGE + y2 * PL);
return;
}
void DrawAll(HDC hdc) //hdc为要绘制的窗口句柄,调用时要传入一个窗口句柄以便确认要绘图的窗口
{
SelectObject(hdc, origin); //SelectObject为当前场景选择画笔或字体等资源
Rectangle(hdc, EDGE - 40, EDGE - 40, EDGE + 8 * PL + 40, EDGE + 9 * PL + 40);
Rectangle(hdc, EDGE, EDGE, EDGE + 8 * PL, EDGE + 9 * PL); //绘制棋盘的两个矩形
/* 此处第一句的 Rectangle 大矩形绘制出来后,会以默认白色填充矩形内部,
会覆盖掉矩形内部所有图形,因此相当于先对棋盘区域进行了“擦除” */
for (int i = 1; i <= 8; i++)//绘制横线
DrawLineLogic(hdc, 0, i, 8, i);
for (int i = 1; i <= 7; i++)//绘制纵线
{
DrawLineLogic(hdc, i, 0, i, 4);
DrawLineLogic(hdc, i, 5, i, 9);
}
DrawLineLogic(hdc, 3, 0, 5, 2);
DrawLineLogic(hdc, 5, 0, 3, 2);
DrawLineLogic(hdc, 3, 7, 5, 9);
DrawLineLogic(hdc, 5, 7, 3, 9);//绘制四条斜线
SelectObject(hdc, hFont);//选择字体
SetTextColor(hdc, RGB(0, 0, 0));//设置输出文本的颜色
TextOut(hdc, 180, 303, L"楚 河 汉 界", lstrlen(L"楚 河 汉 界"));
for(int i=0;i<9;i++)
for(int j=0;j<10;j++)
p[i][j].DrawSelf(hdc);//绘制棋子
if (chosen)
chosen->DrawSq(hdc);//绘制选择框
return;
}
DrawSelf和DrawSq分别为绘制棋子和绘制选择框,在后文中将讲述。
因此我们可以把DrawAll放在WM_PAINT事件中,这样就可以在每次恢复和改变窗口时重新绘制棋盘。
case WM_PAINT:
{
hdc = BeginPaint(hWnd, &ps);
DrawAll(hdc);
DrawTurn(hdc, TURN);//绘制回合
EndPaint(hWnd, &ps);
}
break;
二、棋盘点类的定义和棋子的相关绘制
类的定义和初始化
定义一个类Point,用来描述棋盘上的点:
#define RED 1
#define BLACK -1
enum Type {NONE, JU, MA, PAO, XIANG, SHI, BING, SHUAI };//枚举棋子种类
class Point
{
public:
Point();
~Point();
COORD pos; //坐标
Type tp; //棋子种类
short player; //所属玩家
void DrawSelf(HDC hdc); //绘制自己
void DrawSq(HDC hdc); //绘制选择框
void init(int x,int y); //初始化
bool GetThere(int x, int y); //判断能否到达x,y处
Point* king(); //返回己方帅的指针
WCHAR tx[2]; //棋子文本
private:
};
Point::Point()//构造函数
{
player = NULL;
tp = NONE;
}
首先考虑初始化。定义一个初始化函数initialize(),在程序第一次运行,和结束之后重新开始时运行initialize(),对所有Point对象进行重新赋值。即重新摆棋。初始化函数initialize()调用成员函数init(x,y),根据摆棋的逻辑坐标位置(x,y),得到棋子的所属玩家和棋子种类,同时赋值棋子文本:
Point p[9][10]; //全局变量,用来描述全部90个交叉点。
Point *king_RED = NULL; //全局变量,红方帅的指针
Point *king_BLACK = NULL; //全局变量,黑方将的指针
void initialize() //初始化,在程序第一次运行,和结束时重新开始时对p重新赋值。即重新摆棋。
{
memset(p, 0, sizeof(p));
for (int i = 0; i < 9; i++)
for (int j = 0; j < 10; j++)
p[i][j].init(i, j);
return;
}
//成员函数init的定义如下
void Point::init(int x, int y)
{
//赋值逻辑坐标
pos.X = x;
pos.Y = y;
//根据棋盘位置判断所属玩家
if (y == 0 || (y == 2 && (x == 1 || x == 7) || (y == 3 && x % 2 == 0)))
player = BLACK;
else if (y == 9 || (y == 7 && (x == 1 || x == 7) || (y == 6 && x % 2 == 0)))
player = RED;
//根据棋盘位置判断棋子的种类
if ((x == 0 || x == 8) && (y == 0 || y == 9))
{
tp = JU;
wcscpy_s(tx, L"車");
}
else if ((x == 1 || x == 7) && (y == 0 || y == 9))
{
tp = MA;
wcscpy_s(tx, L"馬");
}
else if ((x == 2 || x == 6) && (y == 0 || y == 9))
{
tp = XIANG;
if(player == RED)
wcscpy_s(tx, L"相");
else
wcscpy_s(tx, L"象");
}
else if ((x == 3 || x == 5) && (y == 0 || y == 9))
{
tp = SHI;
if (player == RED)
wcscpy_s(tx, L"仕");
else
wcscpy_s(tx, L"士");
}
else if (x == 4 && (y == 0 || y == 9))
{
tp = SHUAI;
if (player == RED)
{
wcscpy_s(tx, L"帥");
king_RED = this;
}
else
{
wcscpy_s(tx, L"将");
king_BLACK = this; //king_RED和king_BLACK是指向红黑双方“帅”或“将”的指针
}
}
else if ((x == 1 || x == 7) && (y == 2 || y == 7))
{
tp = PAO;
if (player == RED)
wcscpy_s(tx, L"炮");
else
wcscpy_s(tx, L"砲");
}
else if (x % 2 == 0 && (y == 3 || y == 6))
{
tp = BING;
if (player == RED)
wcscpy_s(tx, L"兵");
else
wcscpy_s(tx, L"卒");
}
return;
}
棋子的绘制
接下来为绘制自己的函数DrawSelf()。DrawSelf()通过判断自己的player以获取颜色,根据tx中的文本,将相应的棋子绘制在自己的逻辑坐标pos上。除此之外,DrawSelf()还可以在该处无棋子的情况下,绘制棋盘上的炮兵标记。
#define R 28
void Point::DrawSelf(HDC hdc)
{
if (player == NULL) //构造函数将player默认赋为NUll,如果没有棋子则为NULL
{
if ((pos.X % 2 == 0 && (pos.Y == 3 || pos.Y == 6)) || ((pos.X == 1 || pos.X == 7) && (pos.Y == 2 || pos.Y == 7)))
//判断是否为标记绘制点
{
SelectObject(hdc, origin); //选择画笔origin,在前文已经定义
DrawS(hdc, pos.X, pos.Y); //在逻辑坐标绘制炮兵标记
}
return;
}
else
{
if (player == RED)
{
SetTextColor(hdc, RGB(255, 0, 0)); //改变文本输出为红色
SelectObject(hdc, red); //选择画笔red,在前文已经定义
}
else
{
SetTextColor(hdc, RGB(0, 0, 0)); //改变文本输出为黑色
SelectObject(hdc, black); //选择画笔black,在前文已经定义
}
Ellipse(hdc, EDGE + pos.X * PL - R, EDGE + pos.Y * PL - R, EDGE + pos.X * PL + R, EDGE + pos.Y * PL + R); //在相应位置绘制圆
SelectObject(hdc, hFont); //选择字体,在前文已经定义
TextOut(hdc, EDGE + pos.X * PL - R / 2 - 1, EDGE + pos.Y * PL - R / 2 - 1, tx, 1); //在相应位置输出tx中的文本
return;
}
}
//绘制标记的函数如下
#define l1 4
#define l2 15
void DrawS(HDC hdc, int x, int y)
{
//注意:如果在棋盘两边则只需画一半
if (x != 8) //只要不在最右边,都需要画右侧的一半
{
DrawLine(hdc, EDGE + l1 + x * PL, EDGE + y * PL - l2, EDGE + l1 + x * PL, EDGE + y * PL - l1);
DrawLine(hdc, EDGE + l1 + x * PL, EDGE + y * PL + l1, EDGE + l1 + x * PL, EDGE + y * PL + l2);
DrawLine(hdc, EDGE + l1 + x * PL, EDGE + y * PL - l1, EDGE + l2 + x * PL, EDGE + y * PL - l1);
DrawLine(hdc, EDGE + l1 + x * PL, EDGE + y * PL + l1, EDGE + l2 + x * PL, EDGE + y * PL + l1);
}
if (x != 0) //只要不在最左边,都需要画左侧的一半
{
DrawLine(hdc, EDGE - l1 + x * PL, EDGE + y * PL - l2, EDGE - l1 + x * PL, EDGE + y * PL - l1);
DrawLine(hdc, EDGE - l1 + x * PL, EDGE + y * PL + l1, EDGE - l1 + x * PL, EDGE + y * PL + l2);
DrawLine(hdc, EDGE - l1 + x * PL, EDGE + y * PL - l1, EDGE - l2 + x * PL, EDGE + y * PL - l1);
DrawLine(hdc, EDGE - l1 + x * PL, EDGE + y * PL + l1, EDGE - l2 + x * PL, EDGE + y * PL + l1);
}
return;
}
//此处DrawLine和DrawLineLogic类似,后者是以逻辑坐标为准,前者是以真实坐标为准。
void DrawLine(HDC hdc, int x1, int y1, int x2, int y2)
{
MoveToEx(hdc, x1, y1, 0);
LineTo(hdc, x2, y2);
return;
}
回合的绘制
为了更人性化,我们还需要绘制回合标志。效果如图:
void DrawTurn(HDC hdc,short turn)
{
SelectObject(hdc, hFont);
if (turn == RED)
{
SetTextColor(hdc, RGB(255, 0, 0));
SelectObject(hdc, red);
Ellipse(hdc, 772, 172, 828, 228);
TextOut(hdc, 785, 185, L"帥", 1);
TextOut(hdc, 743, 285, L"红方回合", 4);
}
else if (turn == BLACK)
{
SetTextColor(hdc, RGB(0, 0, 0));
SelectObject(hdc, black);
Ellipse(hdc, 772, 172, 828, 228);
TextOut(hdc, 785, 185, L"将", 1);
TextOut(hdc, 743, 285, L"黑方回合", 4);
}
return;
}
其中短整型turn取值和player类似,只能取RED,BLACK和NULL,NULL则表示游戏结束。每进行一次行动,turn进行一次改变,同时运行一次DrawTurn()。
选择框的逻辑和绘制
首先绘制选择框的函数如下,比较简单。注意此处不能使用Rectangle()绘制矩形,因为绘制矩形会覆盖矩形之内的图形。因此必须绘制四条直线。
void Point::DrawSq(HDC hdc)
{
SelectObject(hdc, green);
int RL = R + 2;
DrawLine(hdc, EDGE + pos.X * PL - RL, EDGE + pos.Y * PL - RL, EDGE + pos.X * PL - RL, EDGE + pos.Y * PL + RL);
DrawLine(hdc, EDGE + pos.X * PL - RL, EDGE + pos.Y * PL + RL, EDGE + pos.X * PL + RL, EDGE + pos.Y * PL + RL);
DrawLine(hdc, EDGE + pos.X * PL + RL, EDGE + pos.Y * PL + RL, EDGE + pos.X * PL + RL, EDGE + pos.Y * PL - RL);
DrawLine(hdc, EDGE + pos.X * PL + RL, EDGE + pos.Y * PL - RL, EDGE + pos.X * PL - RL, EDGE + pos.Y * PL - RL);
}
关键在于选择框的逻辑。
选择框应具有以下逻辑:
- 只有点击棋子才会在相应棋子的位置上显示选择框
- 在红黑回合只能选取相应红黑一方的棋子,若游戏结束则无法选取任何棋子
- 在没有被选取棋子的时候,选取合法的棋子会显示选择框
- 再次点击被选取的棋子,会取消选择框
- 在有已经被选取的棋子情况下,选择另一个友方棋子,则取消前者的选择框,在后者处绘制选择框
- 在有已经被选取的棋子情况下,选择敌方棋子或者无棋子的交点,则进行移动逻辑判断
除了最后一条,按照以上逻辑,在WM_LBUTTONDOWN事件中进行以下判断:
Point *chosen = NULL;
// chosen 为一个 Point 类的全局指针变量,始终指向被选中的棋子,若无被选中的棋子时则为NULL
/**********************************************
注意到 DrawAll 中有这样一句:
if (chosen)
chosen->DrawSq(hdc);
则每次调用 DrawAll 会自动在 chosen 处绘制选择框,
若 chosen 没有值,调用 DrawAll 则会取消显示选择框
**********************************************/
case WM_LBUTTONDOWN:
hdc = GetDC(hWnd); //获取显示设备上下文环境的句柄
//注意:GetDC一定要与ReleaseDC成对出现,否则会造成内存泄露!
//获取鼠标坐标并转化为逻辑坐标
xPos = int(round((GET_X_LPARAM(lParam) - EDGE) / float(PL)));
yPos = int(round((GET_Y_LPARAM(lParam) - EDGE) / float(PL)));
if (xPos >= 0 && xPos <= 8 && yPos >= 0 && yPos <= 9 && TURN != NULL)
//判断是否为合理的棋子区域,并且游戏并未结束
{
if (!chosen)//如果没有被选中的棋子,chosen为空
{
if (p[xPos][yPos].player == TURN)//判断回合是否合法
{
chosen = &p[xPos][yPos];//对 chosen 赋值
chosen->DrawSq(hdc);//绘制选择框
}
}
else //如果有被选中的棋子
{
if (chosen == &p[xPos][yPos])//如果点击的棋子就是已经被选中的棋子
{
chosen = NULL;//按照逻辑,取消选中
DrawAll(hdc);//重画 此时 chosen 为 NULL,则会取消显示选择框
}
else if(chosen->player == p[xPos][yPos].player)
//如果点击的棋子是友方棋子
{
chosen = &p[xPos][yPos];//修改chosen的值
DrawAll(hdc);//重画 会在 chosen 位置绘制选择框
}
else
{
//有已经被选中的棋子,且此时鼠标点击的既不是自身也不是友方棋子,
//即点击的为空点或者敌方棋子。
//需要进行移动逻辑判断,后文将会讲述。
}
}
}
ReleaseDC(hWnd, hdc);//hdc用完之后一定记得ReleaseDC
break;
三、棋子的移动逻辑
Point中有一个返回bool的GetThere的函数,用以判断对象p的棋子能否到达逻辑坐标(x,y)处。根据棋子的种类tp,按照不同棋子的移动规则进行不同的判断。判断并不难,但有些繁琐。我会尽量在注释中解释,读者可自行体会。
int sgn(int n) //需要一个判断正负的函数
{
return n > 0 ? 1 : -1;
}
Point * Point::king() //需要一个返回己方将帅指针的函数
{
if (player == RED)
return king_RED;
else if (player == BLACK)
return king_BLACK;
else
return NULL;
}
bool Point::GetThere(int x, int y)
{
switch (tp)
{
case JU://車只能横竖移动,即目标与自身的逻辑横纵坐标的其一要相等
if (pos.X == x)
{
for (int i = 1; i < abs(y - pos.Y); i++)
{
if (p[x][pos.Y + i * sgn(y - pos.Y)].player != NULL)
//依次检查目标和自身之间的棋子,不为NULL则表示有棋子,无论是什么则不能通过
return false; //有障碍,false
}
return true;//如果运行到这里表示无障碍,true
}
else if (pos.Y == y)//与上同理
{
for (int i = 1; i < abs(x - pos.X); i++)
{
if (p[pos.X + i * sgn(x - pos.X)][y].player != NULL)
return false;
}
return true;
}
else
return false;
break;
case MA:
if ((abs(x - pos.X) == 1 && abs(y - pos.Y) == 2)|| (abs(x - pos.X) == 2 && abs(y - pos.Y) == 1))//第一步,马走日的判断
{
//以下为拐马脚的判断
if (y == pos.Y - 2 && p[pos.X][pos.Y - 1].player != NULL)
return false;
else if (y == pos.Y + 2 && p[pos.X][pos.Y + 1].player != NULL)
return false;
else if (x == pos.X - 2 && p[pos.X - 1][pos.Y].player != NULL)
return false;
else if (x == pos.X + 2 && p[pos.X + 1][pos.Y].player != NULL)
return false;
else
return true;
}
else
return false;
break;
case PAO:
if (p[x][y].player == NULL)
//炮在不吃子的时候,即目标处无棋子时,移动规则和車相同
{
if (pos.X == x)
{
for (int i = 1; i < abs(y - pos.Y); i++)
{
if (p[x][pos.Y + i * sgn(y - pos.Y)].player != NULL)
return false;
}
return true;
}
else if (pos.Y == y)
{
for (int i = 1; i < abs(x - pos.X); i++)
{
if (p[pos.X + i * sgn(x - pos.X)][y].player != NULL)
return false;
}
return true;
}
else
return false;
}
else
//目标处有棋子,则需要隔一个棋子才为true,思路和車的判断类似,此时要求障碍数等于1
{
if (pos.X == x)
{
int n = 0;
for (int i = 1; i < abs(y - pos.Y); i++)
{
if (p[x][pos.Y + i * sgn(y - pos.Y)].player != NULL)
n++;
}
if (n == 1)
return true;//刚好隔一个棋子,true
else
return false;//只要障碍不为1,false
}
else if (pos.Y == y)
{
int n = 0;
for (int i = 1; i < abs(x - pos.X); i++)
{
if (p[pos.X + i * sgn(x - pos.X)][y].player != NULL)
n++;
}
if (n == 1)
return true;
else
return false;
}
else
return false;
}
break;
case XIANG:
if (!((y >= 5) ^ (king()->pos.Y >= 5)))
//相不能过河,即相和己方帅要在同一边,满足在同一边则此处为true
//考虑到可能要增加翻转棋盘的功能,因此不能简单的通过逻辑纵坐标判断过河。
{
if (abs(x - pos.X) == 2 && abs(y - pos.Y) == 2 && p[(x + pos.X) / 2][(y + pos.Y) / 2].player == NULL) //象走田&&“压象心”判断
return true;
else
return false;
}
else
return false;
break;
case SHI:
if (x <= 5 && x >= 3 && (y <= 2 || y >= 7) && abs(x - pos.X) == 1 && abs(y - pos.Y) == 1)//士只能在田字格里移动,并且只能斜着走一格
return true;
else
return false;
break;
case BING:
if ((king()->pos.Y >= 5) ^ (pos.Y >= 5)) //若已经过河则为true
{
if ((!(((y - pos.Y > 0) ^ (pos.Y - king()->pos.Y > 0))) && x == pos.X && abs(y - pos.Y) == 1) || (y == pos.Y && abs(x - pos.X) == 1)) //可以前进和左右移动
return true;
else
return false;
}
else //未过河
{
if (!(((y - pos.Y > 0) ^ (pos.Y - king()->pos.Y > 0))) && x == pos.X && abs(y - pos.Y) == 1) //只能前进
return true;
else
return false;
}
break;
case SHUAI:
if (x <= 5 && x >= 3 && (y <= 2 || y >= 7))//帅只能在田字格中移动
{
if (abs(x - pos.X) + abs(y - pos.Y) == 1)//一般,帅只能横竖移动一格
return true;
else if (p[x][y].tp == SHUAI && x == pos.X)//“对将”规则
{
for (int i = 1; i < abs(y - pos.Y); i++)
//“对将”规则时,将帅在同一竖直线上,且之间无障碍,将可以吃掉对方的帅
{
if (p[x][pos.Y + i * sgn(y - pos.Y)].player != NULL)
return false;
}
return true;
}
else
return false;
}
else
return false;
break;
default:
return false;
break;
}
}
在上文WM_LBUTTONDOWN事件中还有移动逻辑判断未完成,现在可以根据GetThere函数完成移动的判断。将以下代码加入上文的最后一个else中。
if (chosen->GetThere(xPos, yPos))//如果chosen能到到点击的地方
{
Type temp = p[xPos][yPos].tp;//记录下吃掉的棋子类型,用以判断是否游戏结束
p[xPos][yPos].player = chosen->player;
p[xPos][yPos].tp = chosen->tp;
wcscpy_s(p[xPos][yPos].tx , chosen->tx);
//将目标点的player,tp和tx替换为换成chosen所指的数据
chosen->player=NULL;
chosen->tp = NONE;
chosen = NULL;
//再将chosen所指处的这些数据设为空
DrawAll(hdc);//重画,则会按照此时的情况重新显示棋盘和棋局,
//(xPos,yPos)处会显示选中的chosen棋子,原chosen处则不再显示,实现了棋子的移动
if (temp == SHUAI)
//如果(xPos,yPos)处原本是敌方的帅(之前的判断已经排除了是己方棋子的可能性)
{
if (TURN == RED)//此时是红回合则红方胜
MessageBox(hWnd, L"红方胜!", L"结束", 0);//MessageBox用以创建一个对话框
else
MessageBox(hWnd, L"黑方胜!", L"结束", 0);
ReleaseDC(hWnd, hdc);//不要忘记ReleaseDC
TURN = NULL;
break;
}
if (p[xPos][yPos].tp == SHUAI&& p[xPos][yPos].player==RED)
//如果移动的是帅,则要改变将帅指针的值,以便后续的判断
//注意,此处是p[xPos][yPos]换为chosen之后的tp,temp是交换之前的tp
//因此此处是移动的棋子,temp是被吃掉的棋子
king_RED = &p[xPos][yPos];
else if (p[xPos][yPos].tp == SHUAI && p[xPos][yPos].player == BLACK)
king_BLACK = &p[xPos][yPos];
TURN = 0 - TURN;//之前定义RED=1,BLACK=-1,这个操作可以实现RED和BLACK的转换
DrawTurn(hdc, TURN); //绘制回合标志
}
else//如果chosen不能到达目标点
{
chosen = NULL;//会取消选择
DrawAll(hdc);//重画,取消选择框显示
}
四、菜单简介
在WndProc函数中,switch结构里有一项WM_COMMAND的事件,对应的是菜单的选项。
首先在资源视图中找到Menu,在右边点击“请在此处键入”,输入文本,即可添加菜单项:
双击某一项菜单,可以更改它对应的ID。例如双击重新开始(R),在右侧可以看到
在右边菜单编辑器-杂项-ID中,可以输入一个整型以自定义它对应的ID,如此处设置为32772,则在WM_COMMAND中的代码为:
case WM_COMMAND:
{
int wmId = LOWORD(wParam);
// 分析菜单选择:
switch (wmId)
{
case IDM_ABOUT://这是"关于"的ID的宏
DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
break;
case IDM_EXIT://这是"退出"的ID的宏
DestroyWindow(hWnd);
break;
case 32772://重新开始,赋值,重画
initialize();
TURN = RED;
hdc = GetDC(hWnd);
DrawAll(hdc);
DrawTurn(hdc,TURN);
ReleaseDC(hWnd, hdc);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
}
break;
其余的菜单,读者可以自行设计。
五、总结
我用的是VS2017 community,项目下默认有头文件stdafx.h和源文件stdafx.cpp。为了省事,我将类和函数的声明放在了stdafx.h中,函数的实现放在了stdafx.cpp中,(不知道这样会有什么不好的后果,反正目前没有出错)。在ChineseChess中包含stdafx.h,创建主窗口,以及对消息进行判断。
关于stdafx.h和stdafx.cpp,参见:
https://blog.csdn.net/follow_blast/article/details/81704460
文件名 | 说明 |
stdafx.h | 类和函数的声明 |
stdafx.cpp | 各种函数的定义 |
ChineseChess.cpp | wWinMain和WndProc的源文件,显示窗口及消息判断 |
在原有的stdafx.h代码下添加以下代码:
#include <Windowsx.h>
#include <atlstr.h>
#define PL 60
#define EDGE 50
#define l1 4
#define l2 15
#define R 28
#define RED 1
#define BLACK -1
enum Type {NONE, JU, MA, PAO, XIANG, SHI, BING, SHUAI };
void DrawLine(HDC hdc, int x1, int y1, int x2, int y2);
void DrawLineLogic(HDC hdc, int x1, int y1, int x2, int y2);
void DrawS(HDC hdc, int x, int y);
void DrawAll(HDC hdc);
void DrawTurn(HDC hdc,short turn);
void initialize();
class Point
{
public:
Point();
~Point();
COORD pos; //坐标
Type tp; //棋子种类
short player; //所属玩家
void DrawSelf(HDC hdc); //绘制自己
void DrawSq(HDC hdc); //绘制选择框
void init(int x,int y); //初始化
bool GetThere(int x, int y); //判断能否到达x,y处
Point* king(); //返回己方帅的指针
WCHAR tx[2]; //棋子文本
private:
};
stdafx.cpp中原本有一句#include "stdafx.h",在其下添加函数的实现(函数代码略去,已在前文列出):
extern Point p[9][10];
extern Point *king_RED;
extern Point *king_BLACK;
extern Point *chosen;
HFONT hFont = CreateFont(30, 0, 0, 0, 0, 0, 0, 0, GB2312_CHARSET, 0, 0, 0, 0, TEXT("隶书"));;
HPEN red = CreatePen(0, 3, RGB(255, 0, 0));
HPEN black = CreatePen(0, 3, RGB(0, 0, 0));
HPEN green = CreatePen(0, 2, RGB(0, 255, 0));
HPEN origin = CreatePen(0, 1, RGB(0, 0, 0));
//全局画笔和字体资源
int sgn(int n)
{
……
}
void initialize()
{
……
}
void DrawLineLogic(HDC hdc, int x1, int y1, int x2, int y2)
{
……
}
void DrawLine(HDC hdc, int x1, int y1, int x2, int y2)
{
……
}
void DrawS(HDC hdc, int x, int y)
{
……
}
Point::Point()
{
……
}
Point::~Point()
{
}
Point * Point::king()
{
……
}
void Point::DrawSelf(HDC hdc)
{
……
}
void DrawTurn(HDC hdc,short turn)
{
……
}
void Point::DrawSq(HDC hdc)
{
……
}
void Point::init(int x, int y)
{
……
}
bool Point::GetThere(int x, int y)
{
……
}
void DrawAll(HDC hdc)
{
……
}
在ChineseChess.cpp中,首先会有
#include "stdafx.h"
#include "ChineseChess.h"
在这两句之后以及wWinMain函数之前定义全局变量:
Point p[9][10];
Point *chosen = NULL;
Point *king_RED = NULL;
Point *king_BLACK = NULL;
short TURN = RED;
在wWinMain中添加一句initialize();
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
// TODO: 在此放置代码。
initialize();
// 初始化全局字符串
LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadStringW(hInstance, IDC_CHINESECHESS, szWindowClass, MAX_LOADSTRING);
MyRegisterClass(hInstance);
……
……
WndProc的代码如下:(switch结构下代码略去,已在前文中列出)
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
PAINTSTRUCT ps;
HDC hdc;
int xPos, yPos;
switch (message)
{
case WM_COMMAND:
{
……
}
break;
case WM_PAINT:
{
……
}
break;
case WM_LBUTTONDOWN:
{
……
}
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
文件列表图:
最后添加几张运行效果图