#include <windows.h> #include <stdio.h> LRESULT CALLBACK WinSunProc( HWND hwnd, // handle to window UINT uMsg, // message identifier WPARAM wParam, // first message parameter LPARAM lParam // second message parameter ); int WINAPI WinMain( HINSTANCE hInstance, // handle to current instance HINSTANCE hPrevInstance, // handle to previous instance LPSTR lpCmdLine, // command line int nCmdShow // show state ) { WNDCLASS wndcls; wndcls.cbClsExtra=0; wndcls.cbWndExtra=0; wndcls.hbrBackground=(HBRUSH)GetStockObject(BLACK_BRUSH); wndcls.hCursor=LoadCursor(NULL,IDC_CROSS); wndcls.hIcon=LoadIcon(NULL,IDI_ERROR); wndcls.hInstance=hInstance; wndcls.lpfnWndProc=WinSunProc; wndcls.lpszClassName="lqkwudizhiwang"; wndcls.lpszMenuName=NULL; wndcls.style=CS_HREDRAW | CS_VREDRAW; RegisterClass(&wndcls); HWND hwnd; hwnd=CreateWindow("lqkwudizhiwang","lqk wudi",WS_OVERLAPPEDWINDOW, 100,100,600,400,NULL,NULL,hInstance,NULL); ShowWindow(hwnd,SW_SHOWNORMAL); UpdateWindow(hwnd); MSG msg; while(GetMessage(&msg,NULL,0,0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; } LRESULT CALLBACK WinSunProc( HWND hwnd, // handle to window UINT uMsg, // message identifier WPARAM wParam, // first message parameter LPARAM lParam // second message parameter ) { switch (uMsg) { case WM_CHAR: char szChar[20]; sprintf(szChar, "bieluananle %d", wParam); if (wParam == 49) { MessageBox(hwnd, "我们无敌?", "李奇坤的标题", MB_OK); } else{ MessageBox(hwnd, szChar, "这是框框", 0); } break; case WM_LBUTTONDOWN: MessageBox(hwnd,"mouse clicked","message",0); HDC hdc; hdc=GetDC(hwnd); TextOut(hdc,0,50,"wode程序员之家",strlen("wode程序员之家")); //ReleaseDC(hwnd,hdc); break; case WM_PAINT: HDC hDC; PAINTSTRUCT ps; hDC=BeginPaint(hwnd,&ps); TextOut(hDC,0,0,"lqkkkkkkk新的",strlen("lqkkkkkk新的")); EndPaint(hwnd,&ps); break; case WM_CLOSE: if(IDYES==MessageBox(hwnd,"nitama是否真的结束?","message",MB_YESNO)) { DestroyWindow(hwnd); } break; case WM_DESTROY: PostQuitMessage(0); break; default: return DefWindowProc(hwnd,uMsg,wParam,lParam); } return 0; }
函数指针(Function Pointer)
指针变量存储的内容是一个地址信息,而指针的类型确定了指向内容的类型。
指针指向函数:
//定义函数 cm_to_inches double cm_to_inches(double cm) { return cm / 2.54; } //将函数变量 cm_to_inches 赋值给 func1 变量 double (*func1)(double) = cm_to_inches; //输出结果 printf("%fn", func1(15.0));
上面的代码中 func1 就是一个函数指针,cm_to_inches 这个函数的声明和函数变量 *func1 是一致的,所以二者是可以赋值的,就像两个整形赋值一样,可以类比下面的代码:
//定义整形 int int cm_to_inches = 15; //将整形变量 cm_to_inches 赋值给 func1 变量 int func1 = cm_to_inches; //输出结果 printf("%dn", func1);
double (*func1)(double) = cm_to_inches; 等价于 typedef double (*FUNC1)(double); //定义函数 cm_to_inches double cm_to_inches(double cm) { return cm / 2.54; } //将函数变量 cm_to_inches 赋值给 func1 变量 FUNC1 func1 = &cm_to_inches; //输出结果 printf("%fn", func1(15.0));
如果你对 typedef 不是很熟悉,请立刻回去翻一翻语法书,并感受下面的三条语句:
typedef int myinteger; typedef char *mystring; typedef void (*myfunc)();
等价于:
myinteger i; // is equivalent to int i; mystring s; // is the same as char *s; myfunc f; // compile equally as void (*f)();
回调函数(Callback Function)
如果说 函数指针 是语言相关的话**,回调函数 就是一个语言无关的概念了。回调函数这个名字起的很好,可以明显感受到它有点 “返过来调用的意思”,它还有一个被大众熟悉的称号:“好莱坞法则”。** don’t call us, we’ll call you.
其实回调函数以及不是单纯的手段了,它已经上升到了一种架构的层次,这个回调手法其实被多种设计模式所使用,特别在异步编程中,函数本身是一阶公民的语言更是如此。JavaScript 就是重灾区,甚至产生了 “回调地狱” 这种神奇的 “意大利恶魔” !
回调函数===窗口过程函数
回调函数首先是一个你需要自己实现内部逻辑的一个 函数,函数内部可以处理不同状态下的多种逻辑策略,最后将函数的调用权交给第三方(操作系统、程序插件等等),当第三方检测到某些状态发生的时候,会通过执行该函数通知你,这个通知的过程叫做 回调。
第一种在程序中用 轮询 来实现,第二种程序中用 回调 来实现。
回调是替代轮询的一种策略方法。之所以叫做回调函数,是因为回调策略一般和函数本身是绑定关系,而C语言中,函数指针就是实现回调策略的一种技巧,这种技巧常被称为 回调函数。
在 Windows 编程中,操作系统通过 回调函数 告诉你发生了什么事件,例如鼠标移动、键盘响应、窗口最大化、程序退出、计算机休眠等等,你只需要定义一个回调函数,并将这个回调函数的指针交给操作系统即可,
按照这个回调函数的功能,该函数也被称为 窗口过程函数,表示窗口在运行过程中 Windows 不断调用的函数。
注册窗口类
每次注册窗口类都需要先填充一个叫做 WNDCLASSEX 的结构体。
下面是填充窗口类的代码:
// Register the window class. const wchar_t CLASS_NAME[] = L"Sample Window Class"; WNDCLASSEX wcex; wcex.cbSize = sizeof(WNDCLASSEX); wcex.style = CS_HREDRAW | CS_VREDRAW; wcex.lpfnWndProc = WindowProc; wcex.cbClsExtra = 0; wcex.cbWndExtra = 0; wcex.hInstance = hInstance; wcex.hIcon = LoadIcon(NULL, IDI_APPLICATION); wcex.hCursor = LoadCursor(NULL, IDC_ARROW); wcex.hbrBackground = (HBRUSH) GetStockObject(WHITE_BRUSH); wcex.lpszMenuName = NULL; wcex.lpszClassName = CLASS_NAME; wcex.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
WNDCLASSEX 成员比较多,这里简单的做一下介绍,具体信息可以查看相关 MSDN 文档。
cbSize 用来指定结构体的大小,直接使用 sizeof(WNDCLASSEX) 赋值。
style 用来指定窗口类的样式,具体可以查看这篇文章 Class Styles 了解,这里使用 CS_HREDRAW | CS_VREDRAW 组合样式,代表当窗口改变大小时进行重绘操作。
lpfnWndProc 用来指定 窗口过程函数 指针。该函数定义了窗口大多数的行为,具体可以查看 WindowProc。
cbClsExtra 用来定义窗口类结果体的扩展数据大小,一般填充0。
cbWndExtra 用来定义窗口实例的扩展数据大小,一般填充0。
hInstance 代表应用程序的实例句柄。该值就是 WinMain 函数 的 hInstance 参数。
hIcon 代表窗口类的图标句柄,这里使用默认的应用程序图标。
hCursor 代表窗口类的光标句柄,这里使用默认的箭头图标。
hbrBackground 代表窗口类背景颜色的画刷句柄,这里使用纯色的白色画刷。
lpszMenuName 代表窗口类的菜单句柄,这里没有菜单,填 NULL。
lpszClassName 是一个字符串,用来标识一个窗口类。
hIconSm 代表窗口类的小图标句柄,这里和 hIcon 指定相同的图标。
窗口类的名称(lpszClassName)在进程内必须唯一,不可以重名。需要注意 Windows 标准控件一样具有类名,如果你是用了这些控件,请避免与其重名,否则会导致窗口类注册失败的情况。
上述结构体中,主要的成员其实只有四个:cbSize、lpfnWndProc、hInstance 和 lpszClassName,其它的值都可以临时设置为 0。
填充 WNDCLASSEX 结构体后,需要将其注册通知操作系统,具体使用下面的函数:
ATOM WINAPI RegisterClassEx( _In_ const WNDCLASSEX *lpwcx );
函数接收一个窗口类的指针,如果成功会返回一个窗口类的句柄,如果失败则会返回0。
LRESULT & CALLBACK详解
LRESULT 是一个整形变量,应用程序在执行完窗口过程函数后通过该值将结果返回给 Windows。这个值包含了应用程序对具体消息的处理结果,不同的消息该值可能不同。
CALLBACK 是函数调用约定。窗口过程函数本质上是一个回调函数,调用者是操作系统。一个典型的窗口过程函数内部是一个巨大的选择/分支语句,根据不同的消息类型执行不同的代码逻辑。
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
对于 WM_SIZE 消息处理可以这样:
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_SIZE: { int width = LOWORD(lParam); // Macro to get the low-order word. int height = HIWORD(lParam); // Macro to get the high-order word. // Respond to the message: OnSize(hwnd, (UINT)wParam, width, height); } break; } } void OnSize(HWND hwnd, UINT flag, int width, int height) { // Handle resizing }
消息循环
常见的游戏循环逻辑如下:
MSG msg = {0}; while (msg.message != WM_QUIT) { if (PeekMessage(&msg, 0, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessage(&msg); } else { processInput(); update(); render(); } }
PostMessage 和 SendMessage
消息会保存到消息队列中,但是有的时候,操作系统会跳过队列,直接将消息传递到窗口过程函数中。
Posting a message。投递一个消息的含义是将消息放到队列中,然后应用程序会在消息循环中调用 GetMessage 和 DispathMessage 函数获取分发消息。
Sending a message。发送一消息的含义是跳过消息队列,操作系统直接将其传递到窗口过程函数。
前者对应 API 中的 PostMessage 函数,该函数调用后会立即返回。通过调用它可以确保你将消息投放到消息队列,但是无法保证该消息响应(执行)的时间,可以将其看做是异步的。
后者对应 API 中的 SendMessage,该函数调用后并不会插入队列而是直接传递到窗口过程函数进行处理执行,直到消息处理结束返回,可以将其看做同步。
一般在使用中 SendMessage 会导致线程堵塞,所以在处理耗时的任务时不推荐使用,会导致界面假死,常用的场景是一些同步通知且处理迅速的场景。如果不是很在意消息响应的时间和处理顺序,推荐始终用 PostMessage 替代 SendMessage。
GetMessage 和 PeekMessage
前面讲到 GetMessage 会堵塞执行直到消息队列中有新的消息插入。普通的应用程序使用没有任何问题,但是如果是游戏应用就会存在游戏逻辑不能及时更新的情况。
一般游戏中都会存在游戏循环,其会一直调用游戏的处理逻辑,每一帧都会调用,而大多数的游戏循环都是和消息循环合并到一起。
如果在游戏循环中调用 GetMessage 的时候正好消息队列为空就会导致下面的游戏逻辑不能及时执行。而游戏程序恰好对实时性要求极高,这就会造成游戏运行时期画面卡顿的现象。
既然如此,是否可以不执行 GetMessage 分发消息,直接抛弃消息循环?显然不可行,这会导致键盘鼠标不能及时响应,消息队列中消息积累很多确无法及时处理,整个应用处于卡顿假死状态。
为了解决这个问题,游戏程序中一般使用 PeekMessage 函数替代 GetMessage 函数,二者的功能几乎一致,唯一的差别是 PeekMessage 不管消息队列中有没有消息都会立刻返回,也就解决了刚刚提到的更新不及时和不更新卡顿假死问题。
PeekMessage 函数和 GetMessage 函数的唯一差别是多了一个控制消息处理方式的参数 wRemoveMsg:
BOOL WINAPI PeekMessage( _Out_ LPMSG lpMsg, _In_opt_ HWND hWnd, _In_ UINT wMsgFilterMin, _In_ UINT wMsgFilterMax, _In_ UINT wRemoveMsg );
wRemoveMsg 有三种类型:
PM_NOREMOVE,该值会导致 PeekMessage 获取消息后不会将该消息从消息队列中移除。
PM_REMOVE,该值会导致调用 PeekMessage 后将消息从消息队列中移除。
PM_NOYIELD,该值使系统不释放等待调用程序空闲的线程。可以和前两个值组合使用。
窗口绘制消息
窗口第一次显示的时候,客户区必须被绘制。因此当应用程序被显示的时候,你至少会收到一次 WM_PAINT 消息。
当完成客户区的绘制工作,清除更新区域,这会告诉操作系统在发生某些变化之前不需要再次发送 WM_PAINT 消息了。
现在假设用户移动窗口遮挡了你程序的一部分。当遮挡部分再次可见的时候,这部分区域会加入到更新区域,并通过 WM_PAINT 消息通知你的程序。
用户在伸缩窗口的时候也会触发窗口重绘。如下图所示,用户向右拉伸窗口,这个右侧新的扩展区域也会加入到更新区域中。
在案例中的代码逻辑非常简单,它只是使用纯色填充整个更新客户区域,但是用来说明问题足够了。
switch(uMsg) { case WM_PAINT: { PAINTSTRUCT ps; HDC hdc=BeginPaint(hwnd,&ps); // All painting occurs here, between BeginPaint and EndPaint. FillRect(hdc,&ps.rcPaint,(HBRUSH)(COLOR_WINDOW+1)); EndPaint(hwnd,&ps); } return 0; }
调用 BeginPaint 函数开始绘制操作。这个函数会将重绘信息填充到 PAINTSTRUCT 结构体中,结构体中的 rcPaint 成员就是当前需要重绘的区域。这个更新区域是相对于客户区来定义的:
更新区域
在应用程序的重绘代码中,有两个常见的策略:
一种策略是绘制整个客户区,不管操作系统传过来的更新区域的大小。任何在更新区域之外的内容都会被裁剪掉,也就是说操作系统会忽略它们。
另一种是只绘更新区域的内容。
如果选择第一种策略,代码会很简单,反之选择第二种会让程序的效率更高,对于复杂的绘制逻辑优化效果会非常明显。
下面的代码会使用单一的颜色填充整个更新区域,使用的颜色是系统默认的窗口背景颜色(COLOR_WINDOW)。实际的颜色依赖于当前用户的配色方案。
FillRect(hdc,&ps.rcPaint,(HBRUSH)(COLOR_WINDOW+1));
FillRect 函数的内部细节对于例子来说不是很重要,但是该函数的第二个参数就是要填充的矩形坐标。在代码中,我们传入了整个更新区域。在窗口第一次收到 WM_PAINT 消息的时候,整个客户区都需要被重绘,所以 rcPaint 将包含整个客户区,而随后的 WM_PAINT 消息,rcPaint 参数内部包含的区域可能会小一些。
FillRect 函数是图形设备接口(GDI)的一部分,这套接口已经非常古老,在 Windows 7 以后的系统,微软推出了一个新的 2D 图形引擎,名字叫做 Direct2D。该引擎支持硬件加速等高性能的图形操作。
在绘制结束后,需要调用 EndPaint 函数。该函数会清除更新区域,并向 Windows 发送信号,通知它程序已经完成了窗口的绘制,在下次发送变换之前无需再次发送 WM_PAINT 消息。
窗口关闭消息
用户可以随时点击右上角的关闭按钮或者使用键盘上的 ALT+F4 组合键关闭一个应用程序,这两种方式都会触发 WM_CLOSE 消息。
WM_CLOSE 消息可以在用户关闭窗口之前给出一个友好的提示信息。如果你确认想要关闭窗口,则可以直接调用 DestoryWindows 函数,否则,只需要简单的返回0即可,操作系统会忽略这条消息而不会关闭销毁窗口。
WM_CLOSE 消息:
case WM_CLOSE: if (MessageBox(hwnd, L"Really quit?", L"My application", MB_OKCANCEL) == IDOK) { DestroyWindow(hwnd); } // Else: User canceled. Do nothing. return 0;
代码中,MessageBox 函数会展示一个包含确定和取消按钮的模态对话框,如果用户点击确定,程序就会调用 DestoryWindows 销毁窗口。如果用户点击取消,则会跳过 DestoryWindows,不会做任何改变。任何情况下,返回0 代表着你已经处理了该消息。
如果你想直接关闭窗口而不显示任何提示信息,你只要简单的调用 DestoryWindows 即可。
当一个窗口销毁之后,会收到 WM_DESTORY 消息。这个条消息是在窗口从屏幕中移除之后,真正销毁窗口之前发送的。
在你的主应用程序中,典型的响应 WM_DESTORY 消息的代码是调用 PostQuitMessage 函数。
case WM_DESTROY: PostQuitMessage(0); return 0;
PostQuitMessage 函数内部会将 WM_QUIT 消息放到消息队列,在消息循环中读取到 WM_QUIT 消息,会直接退出消息循环,关闭程序。
应用程序的状态管理
窗口过程是一个函数,该函数包含了每条消息对应的业务逻辑。它本身是无状态的。然而有些时候你需要跟踪应用程序中每次函数调用的状态信息。
最简单的方法就是将所有东西都放到一个全局变量中,对于简单的程序来说这种方法足够了,许多 SDK 的案例都是用这种方法。
对于复杂程序来说,这种方法会导致全局变量的骤增
你的应用程序可能不止一个窗口,每个窗口都有自己的窗口过程,区分哪个窗口使用哪个全局变量有时候并不是一个简单的事情,过多相似属性的全局变量会让程序可读性非常差,书写过程中非常容易造成错误。
CreateWindowEx 函数提供一种方式可以将任何一种数据结构以指针的方式传递给窗口。当这个函数被调用的时候,它会向窗口过程发送下面两条消息:WM_NCCREATE WM_CREATE
消息的顺序和列表中的一致,CreateWindowEx 函数不仅仅发送这两条消息,但是其它的消息暂时被忽略。
WM_NCCREATE 和 WM_CREATE 消息会在窗口显示之前发送,初始化 UI 的逻辑可以放到二者的消息处理函数中。例如,可以在处理函数中处理窗口布局初始化的代码。
CreateWindowEx 函数最后一个参数是一个 void* 类型的指针变量。你可以通过该参数传递任何你想传递的值。当窗口过程在处理 WM_NCCREATE 和 WM_CREATE 消息的时候,它能从消息的附加数据中拿到该值。
展示一下这个功能,首先你需要定义一个结构体保存状态信息。
// Define a structure to hold some state information. struct StateInfo { // ... (struct members not shown) }; 当你调用 CreateWindowEx 函数的时候,将这个结构体的指针传入。 StateInfo *pState = new (std::nothrow) StateInfo; if (pState == NULL) { return 0; } // Initialize the structure members (not shown). HWND hwnd = CreateWindowEx( 0, // Optional window styles. CLASS_NAME, // Window class L"Learn to Program Windows", // Window text WS_OVERLAPPEDWINDOW, // Window style // Size and position CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, // Parent window NULL, // Menu hInstance, // Instance handle pState // Additional application data );
虚函数
BaseWindow 类中有一个纯虚函数,用来实现自定义的窗口过程函数。例如:
LRESULT MainWindow::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_DESTROY: PostQuitMessage(0); return 0; case WM_PAINT: { PAINTSTRUCT ps; HDC hdc = BeginPaint(m_hwnd, &ps); FillRect(hdc, &ps.rcPaint, (HBRUSH) (COLOR_WINDOW+1)); EndPaint(m_hwnd, &ps); } return 0; default: return DefWindowProc(m_hwnd, uMsg, wParam, lParam); } return TRUE; }
上面虚函数内部的第一个参数不是窗口句柄(HWND),原因是窗口句柄已经成为了 MainWindow 的成员变量(m_hwnd),并不需要传递就可以直接在函数中获取到。
很多 Windows 程序框架都是使用类似的方法,如 MFC、ATL等等,当然它们是比较完善通用的框架,所以代码要比上边展示的要复杂很多。
代码
#include <windows.h> #include <stdio.h> LRESULT CALLBACK WinSunProc( HWND hwnd, // handle to window UINT uMsg, // message identifier WPARAM wParam, // first message parameter LPARAM lParam // second message parameter ); int WINAPI WinMain( HINSTANCE hInstance, // handle to current instance HINSTANCE hPrevInstance, // handle to previous instance LPSTR lpCmdLine, // command line int nCmdShow // show state ) { WNDCLASS wndcls; wndcls.cbClsExtra=0; wndcls.cbWndExtra=0; wndcls.hbrBackground=(HBRUSH)GetStockObject(BLACK_BRUSH); wndcls.hCursor=LoadCursor(NULL,IDC_CROSS); wndcls.hIcon=LoadIcon(NULL,IDI_ERROR); wndcls.hInstance=hInstance; wndcls.lpfnWndProc=WinSunProc; wndcls.lpszClassName="sunxin2006"; wndcls.lpszMenuName=NULL; wndcls.style=CS_HREDRAW | CS_VREDRAW; RegisterClass(&wndcls); HWND hwnd; hwnd=CreateWindow("sunxin2006","http://www.sunxin.org",WS_OVERLAPPEDWINDOW, 0,0,600,400,NULL,NULL,hInstance,NULL); ShowWindow(hwnd,SW_SHOWNORMAL); UpdateWindow(hwnd); MSG msg; while(GetMessage(&msg,NULL,0,0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; } LRESULT CALLBACK WinSunProc( HWND hwnd, // handle to window UINT uMsg, // message identifier WPARAM wParam, // first message parameter LPARAM lParam // second message parameter ) { switch(uMsg) { case WM_CHAR: char szChar[20]; sprintf(szChar,"char code is %d",wParam); MessageBox(hwnd,szChar,"char",0); break; case WM_LBUTTONDOWN: MessageBox(hwnd,"mouse clicked","message",0); HDC hdc; hdc=GetDC(hwnd); TextOut(hdc,0,50,"程序员之家",strlen("程序员之家")); //ReleaseDC(hwnd,hdc); break; case WM_PAINT: HDC hDC; PAINTSTRUCT ps; hDC=BeginPaint(hwnd,&ps); TextOut(hDC,0,0,"http://www.sunxin.org",strlen("http://www.sunxin.org")); EndPaint(hwnd,&ps); break; case WM_CLOSE: if(IDYES==MessageBox(hwnd,"是否真的结束?","message",MB_YESNO)) { DestroyWindow(hwnd); } break; case WM_DESTROY: PostQuitMessage(0); break; default: return DefWindowProc(hwnd,uMsg,wParam,lParam); } return 0; }