0x00 题记
近一个月没有写wp了,今天正好有空,那就随手记一记
天天搞安全技术也是一件比较烦躁的事情,刚好碰上了网络编程课老师安排做项目,所以趁机换换口味,搞搞开发
知识点
- win32开发,调用各种API
- 窗口类,窗体的属性
- win32消息机制,消息循环
- 按钮的响应事件
- Edit编辑框的内容获取
- 多字节到宽字节
- 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;
在得到鼠标点击的消息后,即可判断鼠标的位置是否在我们设置的“虚拟按钮”的位置内,如果在则响应事件,由此,即可完成一次点击事件的响应。
多线程绘图
为什么会用到多线程绘图?
当你使用单线程完成不了你的需求的时候,就需要使用多线程来完成。我现在面临的情况也一样,在我的绘图窗口,它有可以点击的按钮,还有鼠标移动到特定位置时相关的绘图的要求,以及一张自始至终都在循环的动图。
此时需要解决的问题:
- EasyX是不支持直接放置GIF的,可以加载,但只会显示第一帧
- 鼠标和动图需要分开处理,鼠标的消息不影响动图的循环
第一个问题其实很好解决,动图嘛,无外乎几张图片切换,就直接把放置图片的那段代码循环然后每次加载不同的图片即可。起初我是直接放在的消息循环里面的,和消息是同步的嘛,单线程嘛,在当前函数就不能处理其他函数了(在函数内调用其他函数说白了也在这个大循环里面,也是外函数的一部分了)。代码运行之后,动图就变得及其“诡异”。鼠标移动则动图循环,鼠标无动作则动图停。
折腾了半天才找到原因: 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()
关闭。必须关闭,否则可能会一直在你的进程中,最后结束进程了可能该线程还会保留,占用内存资源