《Windows游戏编程大师技巧》二、Windows编程模型
这一章开篇语对我有很大的启迪,一直对Windows下编程有着畏难心理。太庞大的平台和体系,
太多的API和术语,太快的技术换代节奏,而我是个想要把握住本质掌握住基础才会有安全感
和成就感的程序员,所以我只乐于学习Win32 API。而现在受到这本书的启发,对Windows编程
的畏惧少了一些,也有了一些学习计划:
1.窗体界面 -> Win32\MFC (简单了解界面消息处理机制吧,实在是不擅长画UI,MFC也有些难)
2.网络编程 -> Winsock (这是我比较想学的)
3.Web控件 -> ActiveX (不了解,应该学学)
4.动画游戏 -> GDI\DirectX (感兴趣,想写点小游戏自娱自乐)
所以我觉得写个小游戏作为业余项目是很不错的,游戏部分可以学学DirectX,设置界面可以简单
学习下MFC,然后再给游戏增加个网络对战功能就可以学到Winsock了。业余项目关键是不要追求
完美,先把基本功能完成,基础框架搭好。曾经想用Java2D写个塔防游戏,就是太想一下子就写好,
于是就半途而废了。
========================================================
Windows编程就像去见牙医:虽然明明知道对自己是有益处的,可还是没人喜欢总是找牙医。
虽然我不能保证在阅读本章之后你会变得更加喜欢去见牙医,但是我敢保证你会比以往更喜欢
Windows编程。别因为我要解放你的思想而感到害怕(特别是钟情于DOS的顽固分子)。
Windows的起源
Windows 1.0完全建立在DOS基础上,不能执行多任务,运行缓慢。Windows 3.0让开发人员和
商业应用程序逐渐脱离DOS。但游戏编程的人们还在唱着“坚守DOS岗位直到炼狱冻结”的赞歌。
Windows 95推出后,我才真正开始喜欢Windows编程。
Windows 9X/NT是多任务、多线程的,这和DOS有很大不同。DOS下,一旦你的程序开始运行,
就只能运行该程序(中断处理程序除外)。要想多线程就必须自己模拟,这也是游戏程序员多年
来所做的事。
Windows还是一个事件驱动的操作系统。我们不必关心其他正在运行的应用程序,Windows会去处理,
我们只要关心自己的应用程序中信息的处理。这在Windows 3.0/3.1中是不可能的,它们并不是真正的
多任务系统,每一个应用程序都要产生下一个程序。
匈牙利符号表示法
像微软一样的公司,有几千个程序员在干不同的项目,一个名叫Simonyi的人负责创建了一套
编写微软代码的规范。所有微软的API、界面、技术文件等等都采用这些规范。这个规范被称为
匈牙利符号表示法,也许因为他经常加班弄得饥肠辘辘吧(饥饿Hungry和匈牙利Hungary)。
1.变量:根据变量类型采用不同的前缀,之后的每个子名都要以大写字母开头。如char *szFileName;
全局变量还要在开头加上g_或g,如int g_iXPos;
2.函数:与变量相同,但没有前缀。如int PlotPixel(int ix, int iy, int ic);
3.类型和常量:所有字母大写,子名可以用下划线分隔。如#define MAX_CELLS 64
4.类:所有C++的类以大写C为前缀,其余与变量相同。如class CVector { ... };
5.参数:与变量命名相同,如GetPixel(int ix, int iy); 但GetPixel(int x, int y);也是可以的。
学会阅读匈牙利符号表示法但不代表要改变你自己的编程风格。本书在使用Win32 API函数的场合
使用匈牙利表示法,而在其他位置使用自己的风格。
世界上最简单的Windows程序
基于DOS的“Hello World”程序。注意:要用VC创建一个控制台应用程序,
这是类DOS的应用程序,只是它是32位的。将目标.EXE设为控制台应用
程序,而非Win32.EXE!
#include <stdio.h> void main(void) { printf("\nTHERE CAN BE ONLY ONE!!!\n"); }
Windows程序都以WinMain()开始,就像DOS都以main()开始一样。
#define WIN32_LEAN_AND_MEAN #include <windows.h> #include <windowsx.h> int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) { MessageBox(NULL, "THERE CAN BE ONLY ONE!!!", "MY FIRST WINDOWS PROGRAM", MB_OK | MB_ICONEXCLAMATION); return (0); }
现实中的Windows程序
我们所需的只是一个基本的Windows程序,打开一个窗口、处理信息、调用游戏主循环等。
因此只要进行下列工作:
1. 创建Windows类。
2. 创建事件句柄。
3. 注册Windows类。
4. 用之前的Windows类创建一个窗口。
5. 创建一个能够从事件句柄获得或向事件句柄传递信息的主循环。
1.创建Windows类
Windows中的每一个窗口、控件、列表框、对话框和小部件都是一个Windows类。你也可以
自定义Windows类,你编写的每一个应用程序都应该至少有一个Windows类,否则会非常麻
烦。控制Windows类的数据结构是:WNDCLASSEX。创建Windows类就是设置好它的各种属性。
WNDCLASSEX winclass = {
sizeof(WNDCLASSEX),
CS_DBLCLKS | CS_OWNDC | CS_HREDRAW | CS_VREDRAW,
WindowsProc,
0,
0,
hInst,
LoadIcon(NULL, IDI_APPLICATION),
LoadCursor(NULL, IDC_ARROW),
(HBRUSH) GetStockObject(BLACK_BRUSH),
NULL,
"WINCLASS1",
LoadIcon(NULL, IDI_APPLICATION)
};
sizeof(WNDCLASSEX),
CS_DBLCLKS | CS_OWNDC | CS_HREDRAW | CS_VREDRAW,
WindowsProc,
0,
0,
hInst,
LoadIcon(NULL, IDI_APPLICATION),
LoadCursor(NULL, IDC_ARROW),
(HBRUSH) GetStockObject(BLACK_BRUSH),
NULL,
"WINCLASS1",
LoadIcon(NULL, IDI_APPLICATION)
};
2.事件句柄
由上图看到一个重要的属性 - 回调函数,它是通过lpfnWndProc设定。
来看一个事件处理函数的例子,在这里我们只关心WM_CREATE\PAINT\DESTROY三个事件。
LRESULT CALLBACK WindowsProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
HDC hdc ;
PAINTSTRUCT ps ;
RECT rect ;
switch (message)
{
case WM_CREATE:
return 0 ;
case WM_PAINT:
hdc = BeginPaint (hwnd, &ps) ;
EndPaint (hwnd, &ps) ;
return 0 ;
case WM_DESTROY:
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam) ;
}
{
HDC hdc ;
PAINTSTRUCT ps ;
RECT rect ;
switch (message)
{
case WM_CREATE:
return 0 ;
case WM_PAINT:
hdc = BeginPaint (hwnd, &ps) ;
EndPaint (hwnd, &ps) ;
return 0 ;
case WM_DESTROY:
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam) ;
}
3.注册Windows类
很简单只需一行代码:RegisterClassEx(&winclass);
还有一个旧版本的RegisterClass()用于注册基于旧结构WNDCLASS类。
一旦注册就可以任意创建它的窗口了。
4.创建窗口
hwnd = CreateWindowEx(
NULL, // extended style
"WINCLASS1", // class
"Your Basic Window", // title
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
0, 0, 400, 400, // initial x/y/width/height
NULL, // parent window handle
NULL, // window menu handle
hInst, // program instance handle
NULL); // creation parameters
NULL, // extended style
"WINCLASS1", // class
"Your Basic Window", // title
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
0, 0, 400, 400, // initial x/y/width/height
NULL, // parent window handle
NULL, // window menu handle
hInst, // program instance handle
NULL); // creation parameters
如果上面没有设置WS_VISIBLE则需要手动调用ShowWindow来让窗口显示。
调用UpdateWindow触发WM_PAINT事件。
ShowWindow (hwnd, iCmdShow) ;
UpdateWindow (hwnd) ;
5.主事件循环
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
非常简单。GetMessage是关键,用途是从事件队列中获得消息,保存到MSG结构中。
MSG结构包含了WinProc参数列表中的所有参数,之后调用TranslateMessage稍加处理
和转换,并通过DispatchMessage调用WinProc进行进一步处理。
一个给力的图示,包含了所有事件处理流程!
==============================================
2012年4月22日 更新
1.创建WNDCLASSEX时指定了类名为"WINCLASS1",之后注册它,并在创建窗体时指定了类名"WINCLASS1",
所以就会关联到WNDCLASSEX,并创建出它指定样式大小的窗体。而在创建其他预定义控件时,如按钮控件,
"button"类已经预定义并注册了,我们只需在调用CreateWindowEx指定类名为"button"。
2.GetMessage()和PeekMessage()的区别:它们都是从消息队列中取消息。当消息队列中没有消息时,GetMessage()
会阻塞当前线程,而PeekMessage()直接返回不会阻塞。并且PeekMessage()只取消息不从队列中删除,取出并删除
消息需要指定PM_REMOVE参数:PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)。
3.MSG结构:
typedef struct tagMSG {
HWND hwnd; // window where message occurred
UINT message; // message id itself
WPARAM wParam; // sub qualifies message
LPARAM lParam; // sub qualifies message
DWORD time; // time of message event
POINT pt; // position of mouse
} MSG;
MSG结构的前四个成员变量正是WinProc函数的四个参数。验证了WinMain主循环中GetMessage()取到MSG结构后,
调用DispatchMessage()从MSG中取出参数并触发WinProc进行进一步处理。
4.wParam和lParam在不同的消息中会保存不同的值。在按钮控件的消息中,msg为WM_COMMAND,LOWORD(wParam)
为子窗口ID(创建按钮时指定),HIWORD(wParam)为通知码(1 - BN_CLICKED,2 - BN_DOUBLECLICKED),lParam为
子窗口句柄。