C++之 从0开始,用GDI绘图实现中国象棋

 

目录

一、桌面程序及GDI基础

wWinMain和WndProc

GDI与棋盘的绘制

二、棋盘点类的定义和棋子的相关绘制

类的定义和初始化

棋子的绘制

回合的绘制

选择框的逻辑和绘制

三、棋子的移动逻辑

四、菜单简介

五、总结


学习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一般发生在

  1. 创建窗口时(wWinMain调用UpdateWindow时)
  2. 改变窗口尺寸时(最大化窗口,最小化后还原窗口时)

此时要求窗口重画自己,需要在此时重新绘制窗口内容。

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.cppwWinMain和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;
}

文件列表图:

最后添加几张运行效果图

  • 5
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值