0x00 题记

近一个月没有写wp了,今天正好有空,那就随手记一记

天天搞安全技术也是一件比较烦躁的事情,刚好碰上了网络编程课老师安排做项目,所以趁机换换口味,搞搞开发


知识点

  1. win32开发,调用各种API
    • 窗口类,窗体的属性
    • win32消息机制,消息循环
    • 按钮的响应事件
    • Edit编辑框的内容获取
    • 多字节到宽字节
  2. EasyX图形库的使用
    • 图片的处理
    • 文字的输出
    • 鼠标滞留效果的逻辑原理
    • 虚拟按钮的实现及响应事件
    • 多线程绘图

以项目为例,解析说明上述知识点


0x01 Win32

如果说整个项目中什么最令我头疼,那绝对是win32的这一部分。可能使用现成的MFC组件会比较轻松,但是为了更好的理解这玩意儿,只能恶心自己了。

窗口类、窗体

在说之前先介绍一下窗口类

windows下窗口类分为三种:

  • 系统窗口类

    ​ 由操作系统注册的。一些系统窗口类是所有进程都可以访问的,一些系统窗口类是只能被操作系统内部使用的。对于系统窗口类,应用程序是不能销毁的。

  • 应用程序全局窗口类

    ​ 指由可执行程序或者DLL注册的,可以被当前进程其他模块使用的窗口类。比如你在某个DLL中注册一个全局窗口类,应用程序可以通过加载该DLL之后就可以使用对应的窗口类。

  • 应用程序局部窗口类

    ​ 指由可执行程序或者DLL注册的,仅用于当前模块的窗口类。我们可以注册很多局部窗口类,但推荐的做法是仅注册一个窗口类,用于创建应用程序主窗口。

微软其实是不建议大家做太多的全局窗口类的,具体原因原因在文中解释。

在VS下随手建一个win32项目(区别与控制台项目),一切默认,不选择空项目,这时候就会看到一个已经制作好的窗口程序,有“精美”的窗口,较为完善的消息机制

而事实上,建一个空项目,用 MessageBox()函数就能得到一个最简单的窗体,虽然点了确认键他就关闭了,没什么别的功能。

所以这里用到另一个函数 CreateWindow() ,这个函数的原型是:

HWND CreateWindow(
	LPCTSTR lpClassName,//指向一个空结束的字符串或者整型数atom
	LPCTSTR lpWindowName,//指向一个知道窗口名的空结束符 指针
	DWORD dwStyle,//指定常见窗口的风格
	int x,//窗口的初始水平位置
	int y,//窗口的初始垂直位置
	int nWidth,//以设备单元指明窗口的宽度
	int nHeight,//以设备单元指明窗口的宽度
	HWND hWndParent,//指向被创建窗口的父窗口或所有者窗口的句柄。
	HMENU hMenu,//菜单句柄,或依赖窗口风格指明一个子窗口标识
	HANDLE hlnstance,//与窗口相关联的模块实例的句柄。
	LPVOID lpParam);//指向一个值的指针,该值传递给窗口WM_CREATE消息。
    
//返回值:如果函数成功,返回值为新窗口的句柄:如果函数失败,返回值为NULL。若想获得更多错误信息,需调用GetLastError函数。

像窗口风格,微软是已经给你封装好了的,你只需要查阅文档再根据需求选择就好,不需要从头编写,这点算是比较好的了。其他的参数,具体怎么用的,查查文档或者百度就都明白了。

注册窗口类,在创建窗口之前的操作。当然了,在他之前,还要设计窗口类先。

一览代码先,需要注意的是,CreateWindow函数的第一个参数,窗口类名:

//窗口处理函数
LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

TCHAR szAppClassName[] = TEXT("FIRST");//自动适应字符集

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPreInstance, LPSTR lpCmdLine, int nCmdShow)
{
	//1.设计窗口类	Spy++	"FIRST"	
	WNDCLASS wndClass = { 0 };
	wndClass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);	//加载白色画刷
	wndClass.hCursor = LoadCursor(NULL, IDC_ARROW);				//加载光标
	wndClass.hInstance = hInstance;							   //加载应用程序实例句柄
	wndClass.lpfnWndProc = WindowProc;						   //窗口处理函数
	wndClass.lpszClassName = szAppClassName;				    //窗口类型名
	wndClass.style = CS_HREDRAW | CS_VREDRAW;					//窗口类的风格
	
	//2.注册窗口类
	RegisterClass(&wndClass);

	//3.创建窗口
	HWND hWnd = CreateWindow(szAppClassName, TEXT("欢迎"), WS_BORDER | WS_CAPTION | WS_SYSMENU |  WS_MINIMIZEBOX,
							570, 310, 400, 300, NULL, NULL, hInstance, NULL);
	g_hWnd = hWnd;
	//4.显示和更新窗口
	ShowWindow(hWnd, SW_SHOW);
	UpdateWindow(hWnd);
	//5.消息循环
	MSG msg;
	while (GetMessage(&msg, NULL, 0, 0))//WM_QUIT消息,返回0,结束循环
	{
		//将虚拟键消息转换为字符消息,虚拟键值
		TranslateMessage(&msg);
		//将消息分发给窗口处理函数
		DispatchMessage(&msg);
	}
	return 0;
}

WinMain函数是窗口程序的入口点,这这个main里面,就是一个窗口的基本结构

graph LR
ks(开始) --> sj[设计窗口类] 
sj --> zc[注册窗口类] 
zc --> cj[创建窗口]
cj --> xs[显示窗口]
xs --> xh((消息循环))

消息机制

在上述的消息循环中,就是他的消息机制,在设计窗口类时就已经说明了他的窗口处理函数 wndClass.lpfnWndProc = WindowProc。所以所有的消息处理都应该在该函数内完成,直到窗口关闭,退出循环,程序关闭。当然也可以使用其他阻塞函数使程序不关闭。

在这个消息机制中,我在它创建的时候(获取到创建消息),为它创建了许多子窗口,一些按钮和编辑框,用于人机交互。

在得到绘图消息的时候,我为该窗口进行了一系列的绘制,包括显示文字,设置背景(查看详细代码)等等,给交互提供一定的提示。

switch (uMsg)//获取消息
	{
	case WM_CREATE://窗口创建消息
		_ZWnd = CreateWindow(L"Edit",NULL, WS_VISIBLE | WS_CHILD | WS_BORDER,
			135, 89, 150, 22, hWnd, (HMENU)Edit_Z, NULL, NULL);
		/*...创建其他窗口...*/
		break;
	case WM_PAINT://窗口绘图消息
	{
		/* 绘图操作 */
	     TextOut(hdc, 95, 90, L"账户:", 3);
	    /* ...... */
		break;
	}
	case WM_CLOSE://窗口关闭消息
		DestroyWindow(hWnd);//API函数
		break;
	case WM_DESTROY://窗口销毁消息
		PostQuitMessage(0);//API函数,发送WM_QUIT消息,退出
	default:
		break;
	}
	return DefWindowProc(hWnd, uMsg, wParam, lParam);//返回值
}

最后我就得到了这样一个界面

按钮的响应事件

按钮和编辑框都是窗口的一种,按钮是可以点击并且具有返回值的。在创建按钮的初期,是需要给按钮一个菜单句柄1,在获取了按钮按下的消息后,就会返回这个菜单句柄,从而确定是哪个按钮,进而进行下一步的响应。

在这里,我使用了一个不是我定义的窗口类的类名“Button”

switch (uMsg)//获取消息
	{
	case WM_CREATE://窗口创建消息
        CreateWindow(L"Button", L"注册", WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON,
			110, 215, 60, 20, hWnd, (HMENU)Btn_Z, NULL, NULL);
		CreateWindow(L"Button", L"登录", WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON,
				230, 215, 60, 20, hWnd, (HMENU)Btn_D, NULL, NULL);
		break;
	case WM_COMMAND://按键按下
	{
		switch (LOWORD(wParam))
		{
		case Btn_D://点击登录;这里的值是我的宏定义
			//获取输入框内容及其他登录相关操作
			break;
		case Btn_Z://点击注册
			//进入注册界面
             break;
		default:
			break;
		}
        break;
	}
	default:
		break;
	}
	return DefWindowProc(hWnd, uMsg, wParam, lParam);
}

可以直观的看到(事实上这里展示的时候我省略了很多代码,主要展示相关的内容),消息处理函数越来越庞大,越来越繁杂,因为这才是他的核心,他的大脑。

关于注册界面,因为这个函数越写越大,变得及其不方便处理,所以我就写了一个跳转函数,为注册界面也设计了窗口和消息机制,大体和这个差不多,这样做能够减少我的工程量,至于代码量……嘿嘿,DDDD2

Edit内容获取

编辑框也是窗口的一种,不同于其他窗口,他可以输入,这才是交互的关键,因为如果什么都靠按钮来实现?那怕是要整一个虚拟键盘才能满足需求吧。

用下面的代码即可生成一个编辑框

CreateWindow(L"Edit",NULL, WS_VISIBLE | WS_CHILD | WS_BORDER,135, 89, 150, 22, hWnd, (HMENU)Edit_Z, NULL, NULL);

这里已经是用到的第四个窗口类了(我自己定义的登录和注册页面是两个局部窗口类)。还有两个则是我没有定义就能直接使用的,则是系统窗口类,已经由微软打包好等待你调用就行。

我们自己定义的,通常是局部窗口类,当我们使用CreateWindow函数的时候,这个函数会拿着类名去局部串口类查找,找到相同的了,则返回,没找到接着查找全局窗口类,还没找到最后才是局部窗口类。如果我们把所有的窗口类都定义为全局窗口类,无疑会增加每次查找窗口类名的时间以及内存资源开销。甚至导致崩溃。

回到之前的问题:为什么微软不建议定义全局窗口类?应该是心里有答案了吧。

关于Edit窗口类的内容的获取,又是怎么实现的呢,这需要依靠GetWindowText()函数。该函数有三个参数:

Int GetWindowText(HWND hWnd,//带文本的窗口或控件的句柄。
                  LPTSTR lpString,//指向接收文本的缓冲区的指针。
                  Int nMaxCount);//指定要保存在缓冲区内的字符的最大个数,其中包含NULL字符。如果文本超过界限,它就被截断。

因此,通过此函数,再结合按钮响应事件,即可读取文本到IpString中,完成账号密码的交互功能。

多字节到宽字节

首先介绍什么是多字节。

MBCS,Multi-Byte Character Set:指用多个字节来表示一个字符的字符编码集合。一般英文字母用1Byte,汉语等用2Byte来表示。兼容ASCII 127。

在最初的时候,Internet上只有一种字符集——ANSI的ASCII字符集,它使用7 bits来表示一个 字符,总共表示128个字符,其中包括了 英文字母、数字、标点符号等常用字符。

为了扩充ASCII编码,以用于显示本国的语言,不同的国家和地区制定了不同的标准,由此产生了 GB2312, BIG5, JIS 等各自的编码标准。这些使用 2 个字节来代表一个字符的各种汉字延伸编码方式,称为 ANSI 编码,又称为"MBCS(Multi-Bytes Character Set,多字节字符集)"。

不同 ANSI 编码之间互不兼容,当信息在国际间交流时,无法将属于两种语言的文字,存储在同一段 ANSI 编码的文本中。一个很大的缺点是,同一个编码值,在不同的编码体系里代表着不同的字。这样就容易造成混乱。

在这样混乱的时代,没有统一的编码是一件很麻烦的事,所以就有宽字节的诞生。为了统一所有文字的编码,Unicode应运而生。Unicode把所有语言都统一到一套编码里,这样就不会再有乱码问题了。

宽字节字符集:一般指Unicode编码的字符集

Unicode称为统一码或万国码,统一了不同国家的字符编码。

Unicode通常用两个字节表示一个字符,原有的英文编码从单字节变成双字节,只需要把高字节全部填为0就可以。

但是宽字节也不是完美无缺,因为它的效率并不高,比如UCS-4(Unicode的标准之一)规定用4个字节存储一个符号,那么每个英文字母前都必然有三个字节是0,这对存储和传输来说都很耗资源。为了提高Unicode的编码效率,于是就出现了UTF-8编码。UTF-8可以根据不同的符号自动选择编码的长短。比如英文字母可以只用1个字节就够了。(这里就不深究了。)

刚刚讲了一大堆,但这由有什么意义呢?和我的程序和项目又有什么关系呢?在Edit窗口获取的文本,就是宽字节格式,但是事实上,在我程序中,我将所有的输入字符(账号和密码)都限制在了多字节字符集。因此我还沿用宽字节,无疑就会造成不必要的资源浪费。而且在我后面的Socket通信中,相关的API函数send()recv()则是需要多字节才能传输,若是硬要传输宽字节,又得自己写个协议封装一下,何必呢。

了解了多字节和宽字节,互转则是目前需要做的首要工作

//TCHAR转换为char
void TCHARtoChar(TCHAR* tbuffer, char* buffer)
{
	int len = WideCharToMultiByte(CP_ACP, 0, tbuffer, -1, NULL, 0, NULL, NULL);
	WideCharToMultiByte(CP_ACP, NULL, tbuffer, -1, buffer, len, NULL, NULL);
	return;
}

//char转换为TCHAR
void ChartoTCHAR(char* buffer, TCHAR* tbuffer)
{
	int len = MultiByteToWideChar(CP_ACP, 0, buffer, strlen(buffer) + 1, NULL, 0);
	MultiByteToWideChar(CP_ACP, NULL, buffer, strlen(buffer) + 1, tbuffer, len);
	return;
}

0x02 EasyX

为什么要用的EasyX?因为他是个图形库,能够极大的缩减图形化界面的工程量。另一方面则是这玩意我比较熟,相比于使用WinAPI绘图,我更喜欢这个相对简单的操作。

在使用WinAPI绘图时,我尝试给登录界面增加背景图片然后放弃了,就调了一个颜色作为背景,其次就是输出了几个字也是及其的麻烦。具体有多麻烦,看看代码吧(如果你是跳转过来的,点击这里返回)

case WM_PAINT://窗口绘图消息
	{
		PAINTSTRUCT ps;     //首先要定义一个paint结构体
		HDC hdc = BeginPaint(hWnd, &ps);   //初始化
		OnPaint(hdc);		//设置背景颜色
		SetTextColor(hdc, RGB(255, 255, 255)); //设置文本颜色
		SetBkColor(hdc, RGB(56, 56, 56));   //设置背景颜色
		TextOut(hdc, 95, 90, L"账户:", 3);		//指定坐标打印文本
		TextOut(hdc, 95, 130, L"密码:", 3);
		TextOut(hdc, 80, 170, L"服务器:", 4);
		HFONT font;  //定义一个hfont变量,用于自定义"请登录"三个大字
		font = CreateFont(50,0, 0,0, FW_NORMAL, FALSE, FALSE, 0, ANSI_CHARSET, OUT_DEFAULT_PRECIS,
						CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_QUALITY | FF_SWISS, _T("宋体"));
        				//给font赋值
		SelectObject(hdc, font);//改变将新的font格式传给hdc
		SetBkMode(hdc, TRANSPARENT);
		TextOut(hdc, 100, 20, L"请 登 录", 5);
		EndPaint(hWnd, &ps);	//结束绘图
		break;
	}
//设置登录界面背景颜色
void OnPaint(HDC hdc)
{
	HBRUSH hBrush;
	RECT rect;
	SetRect(&rect, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN));
	hBrush = CreateSolidBrush(RGB(56, 56, 56));
	FillRect(hdc, &rect, hBrush);
	hBrush = NULL;
	return;
}

EasyX不用那么花里胡哨,只需要只用initgraph(width,height)创建一个绘图窗口,即可开始绘图了。

需要注意的是,如果已经创建了一个绘图窗口了,再次使用该函数绘图时,则会关闭之前的窗口。

图片的处理

在登录界面未完成的图片处理的遗憾,可以在这样弥补。EasyX对于绘图还是很有一套的,就算你是小白,也能很好的处理这些事情,因为它有中文的文档

此时图片则是很简单了,直接参照文档就行。先定义一个IMAGE类,然后将图片用loadimage()函数加载到IMAGE类里面,然后再使用putimage()打印图片即可。举个例子

IMAGE img;
loadimage(&img, _T("IMAGE"), MAKEINTRESOURCE(IDB_PLAYER));
putimage(100, 100, &img);

当然,loadimage()函数可以不用指定img,如果该参数为NULL,那么就直接将图片打印在屏幕上(以左上角为坐标原点)。它还可以将图片压缩到指定长宽。

putimage()函数可以有坐标参数,这意味着你可以在屏幕的任何地方打印任意大小(使用loadimage())图片。

文字的输出

对于使用API打印字符的复杂操作而言,EasyX则是太友好了。只需要先定义一个矩形区域,然后在区域内打印即可。至于为什么需要这个矩形,则是为了使背景是透明的,这样才不会影响到背景图片才会更美观。否则就会看到黑色的字体背景填充,简直就是噩梦。

具体的实现方法示例

settextstyle(40, 0, L"方正舒体");//这里设置格式也是简单许多
RECT r1 = { 0, 0, G_XSIZE, G_YSIZE / 3 };//四个参数表示矩形的四个边(左、上、右、下的顺序)的垂直轴坐标,
									//例如左右的边,则是垂直X轴,所以是X坐标,上下则是Y坐标
drawtext(L"拳王争霸", &r1, DT_CENTER | DT_VCENTER | DT_SINGLELINE);//在矩形区域打印文字

结合刚刚的图片处理,这个时候一个简单的界面就做好了

//游戏主界面绘制
void HomeShow()
{
	cleardevice();	//清除图形屏幕
	BeginBatchDraw();//开始批量绘图
	loadimage(NULL,_T("res/Home.png"),G_XSIZE,G_YSIZE);
	setbkmode(TRANSPARENT);
	settextstyle(40, 0, L"方正舒体");
	RECT r1 = { 0, 0, G_XSIZE, G_YSIZE / 3 };
	drawtext(L"拳王争霸", &r1, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
	settextstyle(20, 0, L"宋体");
	RECT r2 = { G_XSIZE / 2 - 45,G_YSIZE / 3,G_XSIZE / 2 + 45,G_YSIZE / 3 + 30 }; rectangle(G_XSIZE / 2 - 45, G_YSIZE / 3, G_XSIZE / 2 + 45, G_YSIZE / 3 + 30);
	drawtext(L"创建房间", &r2, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
	RECT r3 = { G_XSIZE / 2 - 45,G_YSIZE / 3 + 30,G_XSIZE / 2 + 45,G_YSIZE / 3 + 60 }; rectangle(G_XSIZE / 2 - 45, G_YSIZE / 3 + 30, G_XSIZE / 2 + 45, G_YSIZE / 3 + 60);
	drawtext(L"加入房间", &r3, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
	RECT r4 = { G_XSIZE / 2 - 45,G_YSIZE / 3 + 60,G_XSIZE / 2 + 45,G_YSIZE / 3 + 90 }; rectangle(G_XSIZE / 2 - 45, G_YSIZE / 3 + 60, G_XSIZE / 2 + 45, G_YSIZE / 3 + 90);
	drawtext(L"关于游戏", &r4, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
	RECT r5 = { G_XSIZE / 2 - 45,G_YSIZE / 3 + 90,G_XSIZE / 2 + 45,G_YSIZE / 3 + 120 }; rectangle(G_XSIZE / 2 - 45, G_YSIZE / 3 + 90, G_XSIZE / 2 + 45, G_YSIZE / 3 + 120);
	drawtext(L"退出游戏", &r5, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
	EndBatchDraw();//结束批量绘图
}

这里的G_XSIZE,G_YSIZE是我宏定义的窗口长宽。所以在我登录游戏之后,就得到了一个比登录界面精美的游戏界面

鼠标滞留效果的逻辑原理

一个新的窗口诞生了,但是我们得为这个新的窗口创建消息循环。这里没有键盘的什么事,所以主要看鼠标就好。定义一个鼠标变量,然后获取鼠标消息即可

MOUSEMSG m;
m = GetMouseMsg();

在游戏主菜单界面,有一个选择的菜单列表。为了良好的用户使用体验,我在这里增加了“预选”提示:当用户将光标停选项上面时,该选项的背景改变,不再是透明,而是填充为某种颜色,这样比较的提醒,用户的体验就会增加。

   

看了预览图,现在说说它的逻辑(本来想放动图的……图床不支持)。实现的目标呢:就是根据光标放上去,填充红色。

起初我这样就写了把颜色变红的代码,是这样的:

while (_HOME)//这里的_HOME是判断主菜单界面的关闭与否
	{
		BeginBatchDraw();//开始批量绘图,使用批量绘图可以防止闪烁
		m = GetMouseMsg();
		switch (m.uMsg)//根据鼠标的消息进行相关的操作
		{
		case WM_LBUTTONDOWN://鼠标点击
				break;
		case WM_MOUSEMOVE://鼠标移动
			RECT r;
			for (int i = 0; i<4; i++)
             {
             	if (m.x>G_XSIZE / 2 - 45 && m.x<G_XSIZE / 2 + 45 && 
                    m.y>G_YSIZE / 3 + i * 30 && m.y<G_YSIZE / 3 + 30 + i * 30)//如果光标在区域内
             	{
                    r.left = G_XSIZE / 2 - 45;
                    r.top = G_YSIZE / 3 + i * 30;
                    r.right = G_XSIZE / 2 + 45;
                    r.bottom = G_YSIZE / 3 + 30 + i * 30;
                    POINT points[8] = { { r.left,r.top },{ r.right,r.top },
                                       { r.right,r.bottom },{ r.left,r.bottom } };
                    setfillcolor(RED);//填充颜色
                    fillpolygon(points, 4);
                    setbkmode(TRANSPARENT);
                    /*打印对应的文字*/
             	}
			}
			FlushBatchDraw();//结束批量绘图并在窗口上绘图
			break;
		default:
			break;
		}
	}

这里用了一个小算法:因为四个框是连续且等高的。他们的X坐标是相同的。
此时找到最上面的框的top的Y坐标,我们假设他是 y1.top,同时令框高 h,
我们可以算出框底 :y1.bottom = y1.top+h。
而框与框之间又是相连的,所以 :y2.top = y1.bottom = y1.top
依次内推,得到以下公式:
$$
y_x.top = y_1.top+h*(x-1)
$$
在我的代码里,只是换了一个变量名 i 而已。

但是此时我还没意识到问题的严重性,当我运行测试时才发现,游戏刚打开,是透明的底,没有问题。但是当我把光标移上去再一走的时候,他的背景并没有我想象中的复原。而是一直红在那,甚至最后四个全红。

这事我想了好久,始终没有解决方案,首先,绘图是没有撤销功能的,也就是说我要是改变了那里的颜色,没有办法回去。起初我想到的是把那四个框截图下来,光标移开了再贴上去,就这样覆盖上去就完事了。但是这种办法显然不可取:

  • 其一:若是我换了背景图片又得从头再截图
  • 其二:在加入房间时,有一个房间列表供玩家选择,但是这玩意是动态的,没办法截图

因此我否定了这个方案,最终我找到了解决方案:我在上面的for循环的if语句后面添了几行代码

else
{
    if (getpixel(G_XSIZE / 2 - 45 + 1, G_YSIZE / 3 + i * 30 + 1) == RED)
    {
        HomeShow();//重载
    }
}

就这几行代码,完美的解决了我的问题。首先,我把显示界面的所有绘图操作封装成函数,然后在鼠标移动的过程中,判断是否在选项上方,如果在,则填充红色,重点在于:当鼠标移开了之后,我就判断它是不是填充了红色,如果是,则重新加载这个界面。如此就可以“复原”的效果了。但它的本质是“刷新”。

graph TB
a(登录成功)-->b[主菜单界面]
b-->c[鼠标移动]
b-->c1[鼠标点击]
c-.未在选项上.->d[无变化]
c--在选项上-->e[填充红色] 
e--光标还在-->f[保持红色]
e-.光标移开.->g["刷新"]
d-->h[消息循环结束]
f-->h
g-->h
h==新的循环==>b

虚拟按钮的实现及响应事件

与win32的实体按钮不同,EasyX是不支持这一类的,这一点从它的的图形库中可以看出来,但这并不是意味着我们没有办法实现它,实现的原理也很简单。

在上面的 GetMouseMsg() 函数中,我们得到了不只是鼠标的位置信息,还有各种鼠标消息,例如是否点击,是左键还是右键等等。

这里我们只需要用到左键点击即可,所以在得到 WM_LBUTTONDOWN 的消息时,进行相应的操作即可。例如这样:

case WM_LBUTTONDOWN:
    EndBatchDraw();//结束批量绘图
    if (m.x>G_XSIZE / 2 - 45 && m.x<G_XSIZE / 2 + 45 && m.y>G_YSIZE / 3 && m.y<G_YSIZE / 3 + 30 && _HOME == 1 && _ABOUTGAME == 0)
        //如果选择“创建房间”
    {
        _HOME = false;
        if (playGame(1))//创建房间请求,失败返回非零,成功则进入房间,直到退出房间
            MessageBox(NULL, L"房间数已达上限", L"提示", NULL);
        _HOME = true;
        HomeShow();
        break;
    }
    /*else if(在其他位置)
    {
        对应操作;
    }*/
    else
        break;

在得到鼠标点击的消息后,即可判断鼠标的位置是否在我们设置的“虚拟按钮”的位置内,如果在则响应事件,由此,即可完成一次点击事件的响应。

多线程绘图

为什么会用到多线程绘图?

当你使用单线程完成不了你的需求的时候,就需要使用多线程来完成。我现在面临的情况也一样,在我的绘图窗口,它有可以点击的按钮,还有鼠标移动到特定位置时相关的绘图的要求,以及一张自始至终都在循环的动图。

此时需要解决的问题:

  1. EasyX是不支持直接放置GIF的,可以加载,但只会显示第一帧
  2. 鼠标和动图需要分开处理,鼠标的消息不影响动图的循环

第一个问题其实很好解决,动图嘛,无外乎几张图片切换,就直接把放置图片的那段代码循环然后每次加载不同的图片即可。起初我是直接放在的消息循环里面的,和消息是同步的嘛,单线程嘛,在当前函数就不能处理其他函数了(在函数内调用其他函数说白了也在这个大循环里面,也是外函数的一部分了)。代码运行之后,动图就变得及其“诡异”。鼠标移动则动图循环,鼠标无动作则动图停。

折腾了半天才找到原因: GetMouseMsg() 函数是一个阻塞函数,这就意味着,如果没有消息,程序就会停下来等待消息,所以才造成了刚刚那一幕。所以游戏主页面的实现需要多线程了,随即我就把展示动图那一步拿出来,封装成函数,在游戏进入主界面的时候在一个新的线程启动即可

//主游戏界面
void CreateRoom()
{
	HWND G_hWnd = initgraph(G_XSIZE, G_YSIZE);//创建游戏窗口,同时自动销毁之前的绘图窗口
	MoveWindow(G_hWnd, 500, 230, G_XSIZE + 16, G_YSIZE + 39, FALSE);//移动窗口到指定位置
	_GamePlayS();//绘图操作

	flag_T = 1;//线程结束标识
	hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)my_GIF, 0, 0, 0);//启动线程

	int flag_i = 0;//己方动图flag
	MOUSEMSG m;
	//消息循环
	while (true)
	{
        /*游戏游玩过程的响应函数等等*/
    }
}
//动图
void my_GIF()
{
	int i = 0;
	while (flag_T)
	{
		//循环绘图
	}
}

创建线程之后,也需要关闭线程,所以在检测到玩家点击返回时就会有一下操作

if (m.x > G_XSIZE - 46 && m.x<G_XSIZE - 3 && m.y>G_YSIZE - 26)//点击返回
{
    char Buffer_My[] = { 101,0 };
    _SendMessage(Sock_Play, Buffer_My);//发送“我已退出”消息到服务器
    flag_T = 0;//关闭线程flag
    flag_j = 0;//清空我的选择
    Sleep(100);
    CloseHandle(hThread);//关闭线程
    return;
}

这是在 case WM_LBUTTONDOWN下的操作,可以看到,我退出后还会发送消息给服务器,服务器再将我退出的消息发送给另一位玩家,然后本局游戏完整结束。

**注意:**开启多线程,特别是该线程里的函数还是死循环的时候,就应该将循环改为可以跳出或者结束的非死循环,既添加循环结束标识,这个标识同样可以作为线程结束标识,此时,线程才能使用 CloseHandle() 关闭。必须关闭,否则可能会一直在你的进程中,最后结束进程了可能该线程还会保留,占用内存资源


Footnotes

  1. 这里其实是依据窗口风格指明一个子窗口标识。对于层叠或弹出式窗口,hMenu指定窗口使用的菜单:如果使用了菜单类,则hMenu可以为NULL。对于子窗口,hMenu指定了该子窗口标识(一个整型量),一个对话框使用这个整型值将事件通知父类。应用程序确定子窗口标识,这个值对于相同父窗口的所有子窗口必须是唯一的。

  2. 懂得都懂