第3章 启程——Windows编程基础. 学习笔记

(1)Windows编程体系与游戏编程


● 首先我们需要知道,游戏类的应用程序通常可以被分为图形用户接口(GUI)和引擎,这两个部分。

● Windows 平台游戏编写套装为—— C++语言、Windows API(能直接与操作系统打交道的高效特性)与图形库(DirectX 或者 OpenGL)【能直接与显卡打交道的图形库】

● 游戏编程更加强调计算机图形学的知识和计算机资源的高效利用。而不是那些我们在普通的软件编程中习以为常的单线程的、事件驱动的或者顺序逻辑执行的那些软件

如果在Visual Studio 中用C++语言进行开发的话, 通常有3种基本的创建交互式Windows 应用程序的方式:——Windows 程序的体系

(1) 使用Windows API来进行开发, Windows API是Windows 操作系统与应用程序之间的通信的基本接口

(2)使用MFC来进行开发, 是微软的基础类库,也就是一组封装了Windows API的C++类而已`

(3) 使用Windows Forms,相对于传统的Windows API与MFC开发方式,这是一种基于窗口的新的开发机制,Windows Forms是微软的.NET开发框架的图形用户界面部分,该组件通过现有的Windows API封装为托管代码提供了对Windows本地组件的访问方式, 我们通常可以把这套组件看做是MFC的替代品

● 在Windows平台上进行游戏开发,通常都是采用C++编程语言搭上 Windows API这套方案,再配合上某种图形库,来完成游戏程序的编写,就算是游戏引擎,也基本上是用的C++、Windows API 加上图形库这套体系

● 游戏编程中MFC的使用: MFC还得依靠 Windows API, 而且比Windows API 高层,也就是说MFC跑起来必定会比Windows API多走一段路, 这就注定了MFC的效率不如 Windows API。由于MFC有底层代码的隐蔽性等特点, 且MFC毕竟封装了很多实际上没用到的东西, 不利于游戏开发的效率和游戏的运行速度, 不适合做游戏开发, MFC做游戏地图编辑器之类的工具软件倒是比较适合。


(2)理解两个术语——API与SDK

何为API:

● 微软公司为了方便我们开发基于Windows的应用程序,为我们提供了各种各样的函数,这些函数是Windows操作系统提供给应用程序编程的接口 ,称为API, API可以理解为预先定义好的函数, **

● 我们在编写Windows程序中所说的API函数,就是指系统提供的函数, 主要的Windows函数都在【Windows.h】头文件中进行声明了

● 任何Windows应用程序与Windows本身之间的所有通信,都要使用Windows应用程序编程接口,也就是Windows API, 它是Windows操作系统提供的标准函数, 可以提供应用程序与Windows互相进行通信的方法, 在Windows和应用程序之间传递数据的是结构体而不是类 。

什么是SDK : ● 中文译为软件开发包, 说白了就是一个开发所需资源的一个集合, 所以 DirectX SDK 就是用 DirectX 进行开发的一个资源的打包集合, 这里需要注意的是 API和SDK 都是IT界广泛使用的术语, 并不是专指特定的API或者SDK


(3)WinMain函数

** ● WinMain函数——Windows程序的入口点函数 : 这个函数是所有Windows应用程序的心脏, 是所有Windows 程序的入口点函数, 当Windows 操作系统启动一个程序时, 他最先调用的就是这个程序的WinMain函数(实际上是由插入到可执行文件中的启动代码调用的) , 当WinMain 函数结束或返回时, Windows 应用程序也就结束了**

** ● WinMain函数的原型为: **

int WINAPI WinMain
(
  _In_HINSTANCE hInstance,
  _In_HINSTANCE hPrevInstance,
  _In_LPSTR lpCmdLine,
  _In_int nCmdShow
);

说明: 其中函数类型int之后的WINAPI 字样基本上可以忽略, 它只是WinDef.h 头文件中定义的一个宏,我们可以利用转到定义功能, 查看到它在WinDef.h 头文件中的定义如下:
#define WINAPI _stdcall

也就是说WINAPI 其实就是_stdcall,只是为了让我们清楚知道这里的_stdcall 表示的是一种调用约定, 它让编译器知道了应当以Windows 兼容的方式来产生机器指令, 如果我们在写WinMain 函数的时候去掉这句WINAPI 而直接写 intWinMain(), 仍然是可以编译运行的, 只不过在编译期间会得到一条警告,大概说的是—— 也就是建议我们要确保WinMain 函数具有WINAPI 前辍。

注意: 这里的WINAPI 有时也会写作CALLBACK, 这个CALLBACK和WINAPI 是完全等效的, 因为他们具有相似的宏定义:
** #define CALLBACK _stdcall
#define WINAPI _stdcall

● 在这些 WinMain函数中的 参数之前都有一个 _In 字样, 这里的 _In 可以理解为一个宏, 表示需要我们进行自行输入的一个参数, 与其对应的是 _Out, 它表示指定的这个参数是函数本身向外输出的一个参数。

● 下面我们分别对WinMain 函数的每一个成员进行详细的介绍。
这里的 4个参数, 都是需要在系统调用WinMain 函数的时候, 传递给应用程序的

(1) 第一个参数, HINSTANCE类型的 hInstance, 它表示该程序当前运行的实例句柄, hInstance 其实是一个数值, 当一个程序在Windows下运行时, 它唯一对应一个运行中的实例, 也只有运行中的程序实例, 才有资格分配到实力句柄。 一个应用程序可以运行多个实例, 每运行一个实例, 系统都会给该实例分配一个句柄值, 并且通过hInstance 参数传递给程序的入口点WinMain 函数

(2) 第二个参数, _In_HINSTANCE 类型的 hPrevInstance, 它表示当前实例的前一个实例的句柄, 对于这个参数的用法, MSDN中明确表示在Win32 环境下,该参数总是取NULL, 这就是说,在Win32 环境下, 这个参数没有存在感,不起任何作用, 只是在进行WinMain 函数书写时需要将它作为一个参数表示出来而已。

(3) 第三个参数, LPSTR 类型的lpCmdLine,它是一个以空终止的字符串,指定传递给运用程序的命令行参数,

(4) 第四个参数, int类型的 nCmdShow, 指定程序窗口应该如何显示,是最大化,最小化,还是隐藏等等。

这个参数可以有如下取值:

这里写图片描述

通过对 nCmdShow 这个值进行个性化的设置, 可以为游戏窗口的创建注入很多新鲜的血液。

(4) MessageBox 函数


先写一个在第二章的源代码如下:

#include<Windows.h>
int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nCmdShow)

{
  MessageBox(NULL,L"你好,Visual Studio!",L"消息窗口",0);
  return 0;
}

下面我们来看看这个示例程序中用于显示出“你好,Visual Studip!”消息窗口的那句代码:

 MessageBox(NULL,L"你好,Visual Studio!",L"消息窗口",0);
 
 **说明:它用于显示一个消息框**

我们在MSDN中查到这个函数有如下原型:

int WINAPI MessageBox
( 
  _In_opt_ HWND hWnd,
  _In_opt_ LPCTSTR lpText,
  _In_opt_ LPCTSTR lpCaption,
  _In_ UINT uType
  
);  

说明:这里的 _In_opt 类似于之前提到过的 _in, 只不过后面多了一个 _opt, 表示可选的, 两个词组合在一起就表示为“可选的输入参数”了, 就是说这个参数我们可以自己填内容,不填具体内容的话直接填NULL也是可以的, 这个选择权在于我们。

● 第一个参数, HWND 类型的hWnd, 表示我们显示的消息框所属窗口的句柄,在后面会介绍句柄的含义, 在此程序示例中,这个参数设置为了NULL, 表示消息框是从属桌面的。

● 第二个参数,LPCTSTR类型的 lpText , 它是一个以NULL结尾的字符串, 表示所要显示的消息的内容。

● 第三个参数, LPCTSTR类型的 lpCaption, 它也是一个以NULL结尾的字符串, 在其中填写我们要显示的消息框的标题的内容。

● 第四个参数, UINT 类型的 uType , 表示我们消息窗口需要什么样的样式, 微软已经为我们定义好了许多可供选择的样式和消息对应的图标, 一些常用的样式列表如下:

这里写图片描述

常用的图标我们也是通过一些标识名来指定, 有如下几种:

这里写图片描述

这里我们只是列出了一些常用的标识, 完整版可以自行查阅MSDN

注意: 如果想要多个标识一起使用的话, 我们可以使用逻辑或 把不同的标识连接起来, 具体的符号是 那条斜杠 “ | ”, 注意不是两条斜杠, 比如要创建一个具有Yes和No按钮, 并带有“问号”图标的消息框, 我们就把uType 填成这样 MB_YESNO | MB_ICONQUESTION ,非常直观。 当需要多个标识同时使用的场合, 用一下这条斜杠 “ | ” 连接一下就好了 。

(5) PlaySound 函数


实现音效播放的核心API函数PlaySound 函数的定义如下:

BOOL PlaySound
(
  LPCTSTR pszSound,
  HMODULE hmod,
  DWORD fdwSound
);

说明: 如果要使用PlaySound函数的话, 必须在编译之前连接 winmm.lib 库文件。

● 第一个参数, LPCTSTR类型的 pszSound, 可以看到作为LPCTSTR类型的此参数为一个字符串, 指定了要播放的声音文件, 这个值设为NULL的话, 就会把所有当前播放的声音全部停掉

● 第二个参数, HMODULE类型的 hmod, 包含了我们在第一个参数中指定的声音文件作为资源的可执行文件的句柄, 这里我们把这个参数设为NULL就可以了。

● 第三个参数, DWORD 类型的fdwSound, 是一个用来控制声音播放的一个标志, 下面是一些常用标识的列表, 我们可以使用一个或者多个标识, 标识之间用 “ | ”连接。
这里写图片描述

在下一小节的FirstBlood 示例程序中, 用的 是 SND_FILENAME 和 SND_ASYNC 这两个标识, 这样PlaySound 函数就会知道第一个参数 pazSound 中填的是文件名, 而且声音是伊布播放的了, 异步播放的意思是 PlaySound 函数会在播放后立即返回调用点。

(6) 示例程序 Firstblood !


● Firstblood 示例程序的代码:

#include<Windows.h>
#progma comment(lib,"winmm.lib")
int WINAPI WinMain(HINSTANCE hInstancem, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)

{
   PlaySound(l"FirstBlood.wav",NULL,SND_FILENAME | SND_ASYNC); //播放音效

  MessageBox(NULL,L"First blood! 你好,游戏开发的世界,我们来征服你了!", L"First blood! 消息窗口",0);  //显示一个消息框

   return 0;
}

接下来我们来逐行详细来讲解一下这8段代码的含义:

● 第一行代码剖析: 包含了一下Windows.h 头文件, 如果我们要编写基于Windows API 的程序,这个头文件一般都要 include 一下,

● 第二段代码剖析, 后面会用到 PlaySound 函数来播放音效, 这里就必须在程序中链接使用 PlaySound 函数所需的 winmm.lib 库文件, 我们经常在编译源代码时, 会出现“无法解析的外部符号”系列的错误, 这一般都是 lib库文件没有添加或者添加不正确造成的。

注意: 在工程中包含库文件有两种方式, 任选一种就可以了:

● 第一种方式, 就是用我们这里使用的 #progma comment(lib,“winmm.lib”) 语句, 在双引号 “ ” 之间填我们需要链接的库文件。 一般我们把#progma comment 语句放在包含头文件的 include语句之后。

● 第3~4行代码解析: 也就是写一下 WinMain 函数的标准原型。

● 第5行代码解析:调用一下PlaySound 函数来播放音效, 其中第一个参数我们这样写 L"FirstBlood.wav", 其中的L 表示我们要把字符串 “FirstBlood.wav” 转化为宽字符串。 因为在Visual Studio 中默认使用的是 Unicode 字符集, 如果我们不写L, 而直接写 “FirstBlood.wav”的话, 就会报出 “不能将参数1 从const char [15] 转换为 LPCWSTR”系列的错误。

因为我们把 FirstBlood.wav 这个音频文件放在了工程文件夹中与程序源文件相同的路径下了, 所以直接写 L “FirstBlood.wav”, 程序就会在程序源文件所在的目录找到 FirstBlood.wav 音频文件并载入进来。

在这里的第三个参数我们填的是 SHD_FILENAME | SND_ASYNC, 这表示从文件名读取音频文件, 并且为异步播放模式。

● 第6行代码解析: 调用一下MessageBox 函数来显示一个对话框, 其中第一个参数填NULL 就行了, 第二个参数填对话框的内容, 第三个参数填对话框的标题, 注意也都要用L 将字符转化为 宽字符版。

● 第7~8行代码解析: 给上 WinMain 函数的返回值, 因为WinMain函数的返回值类型为int, 这里返回0就可以了,最后以大括号结尾, 表示WinMain 函数的内容到这里就结束了。

(7)Windows 程序的“外貌”——窗口


● 窗口就是Windows 应用程序的外貌,我们通过这个窗口来实现用户与计算机之间的交互操作

● 那每一个Windows 应用程序至少要有一个窗口, 称之为主窗口。 窗口一般为屏幕上呈现的一块矩形区域, 是Windows 运用程序与用户进行交互的接口。 利用窗口, 运用程序可以接受用户的输入以及对输出进行显示

● 那一个应用程序通常都包含标题栏、菜单栏、系统菜单、最小化框、最大化框,有的应用程序还含有滚动条。

这里写图片描述

● 窗口又可以分为客户区和非客户区。 其中客户区为窗口的一部分, 游戏软件通常都在客户区完成游戏中各种画面、功能与效果的实现。 标题栏、菜单栏、系统菜单、最小化、最大化框 统称为窗口的非客户区, 他们主要由Windows 系统来管理, 而应用程序则主要管理客户区的外观及其操作

(8)Winodows资源的“身份证”——句柄


● 在Windows 应用程序中, 窗口都是通过窗口句柄(HWND)来标识的, 我们要对某个窗口进行操作的话, 首先就是要得到这个窗口的句柄。

● 在Windows 程序中, 有各种各样的资源, 比如窗口、图标、光标等。 系统创建这些资源时会为它们分配内存, 并返回标识这些资源的标识号, 这些标识号就是句柄

(9) Windows 程序的“邮局”—— 消息与消息队列


● 首先我们要牢记一个概念, Windows 程序设计是一种基于事件驱动方式的程序设计模式, Windows 程序与操作系统间的通信主要是基于消息的。

举个栗子: 假如我们用鼠标左键点击某某软件的桌面快捷方式图标, 那么此时的操作系统会感知到这一事件, 于是将事件包装成一个消息, 投递到消息队列当中, 然后会从消息队列中取出消息并进行响应。

也就是说,我们每单击一次鼠标,每按键盘上的一个键,就会有这样的一个过程进行, 操作系统感知事件, 将事件包装成消息, 投递到消息队列中,然后程序从消息队列中取出消息并响应。 其实在这个处理的过程中, 操作系统也会给游戏程序“发送消息”。 所谓发送消息, 就是操作系统调用程序代码中专门负责处理消息的函数, 就是这个 “宽口过程函数”

(10) 消息的表示形式——MSG结构体


● 在Windows 程序中,消息是由MSG结构体来表示的。 MSG的结构体的定义如下:

typedef struct tagMSG   //msg
{
  HWND hwnd;  //指定消息所属的窗口
  UINT  message;  //指定消息的标识符
  WPARAM wParam; //指定此msg 的附加消息
  LPARAM  lParam;//指定此msg 的附加消息
  DWORD  time;  //指定投递到消息队列的时间
  POINT pt; // 指定投递到消息队列中时鼠标的当前位置
  
 }MSG;

● 第一个参数 , HWND 类型的hwnd, 指定了消息所属的窗口, 通常一个消息都是与某个窗口相关联的, 举个栗子, 我们在某个活动窗口按下鼠标左键, 产生的按键消息就是发给这个窗口的, 在Windows 程序中, 我们通常用 HWND 类型的变量来标识窗口。

● 第二个参数, UINT 类型的message, 他指定了消息的标识符。 在Windows中, 消息是由一个数值来表示的 不同的消息对应着不同的数值。 但是在Windows 中将消息对应的数值定义为 WM_XXX 宏的形式, XXX 对应某种消息的英文拼写的大写形式, 比如说 鼠标右键按下的消息是 WM_KEYDOWN , 鼠标左键双击的消息是 WM_LBUTTONDBLCLK, 所以说, 我们在程序中通常都是以 WM_XXX 宏的形式来使用消息的。

● 第三个参数以及第四个参数, WPARAM 类型的wParam 和 LPARAM 类型的 lParam ,它们都用于指定消息的附加消息 ,

wParam 和 lParam 表示的信息会随着消息的不同而不同, 这两种类型分别微 unsigned int 和 long 型。

● 第五个参数, DWORD 类型的 time , 表示投递到消息队列中的时间

● 第六个参数, POINT 类型的pt , 表示投递到消息队列中时鼠标的当前位置

(11) 关于消息队列


● 每个Windows 应用程序开始执行后, 系统都会为该程序创建一个消息队列。 这个消息队列用于存放改程序创建的窗口的消息。 比如我们按下鼠标左键的时候, 将会产生一个 WM_LBUTTONDOWN 消息, 系统就会将这个消息放到窗口所属的应用程序消息队列中, 等待应用程序的处理, Windows 将产生的消息依次放到 消息队列中, 而应用程序则通过一个消息循环不断地从消息队列中取出消息并进行响应。 这种消息机制, 就是Windows 程序运行的机制


(12) 步步为营——窗口创建四部曲


● 创建一个完整的窗口,一般都需要经过下面四个步骤:

窗口类的设计
窗口类的注册
窗口的正式创建
窗口的显示与更新

**(1) 窗口类的设计 **

● 一个完整的窗口具有许多特征, 包括光标(鼠标悬停在该窗口中时的形状)、 图标、 背景颜色等等。

● 在创建一个Windows 应用程序的窗口前, 也必须对该类型的窗口进行设计,指定窗口的一些特征。

● 在Windows 中控制窗口的特征的结构体有两个: WNDCLASS 和 WNDCLASSEX , 前者已经面临着被废弃的命运, 我们应当使用WNDCLASSEX , 两者的结构体非常之相似, 我们来看看WNDCLASSEX 结构体的原型为:

typedef struct tagWNDCLASSEX  
{
  UINT cbSize;
  UINT style;
  WNDPROC  lpfnwndProc;
  int cbClsExtra;
  int cbWndExtra;
  HINSTANCE hInstance;
  HICON hlcon;
  HCURSOR  hCursor;
  HBRUSH hbrBackground;
  LPCTSTR lpszMenuName;
  LPCTSTR lpszClassName;
  HICON hIconSm;
} WNDCLASSEX, *PWNDCLASSEX;

首先,为了讲解方便, 我们先表示在WinMain 函数中定义一个 WNDCLASSEX

WNDCLASSEX wndClass={0};  // 用WNDCLASSEX定义了一个窗口类, 即用wndClass  实例化了WNDCLASSEX,  用于之后窗口的各项初始化

● 第一个参数,UINT 类型的cbSize, 表示该结构体的字节数大小,一般我们取为 sizeof( WNDCLASSEX ) 就好了, 即代码这样写:

wndClass.cbSize = sizeof( WNDCLASSEX ) ;//设置结构体的字节数大小

● 第二个参数, UINT类型的 style, 指定这一类型窗口的风格样式, 常用的样式取值在下面表格中都列举出来了, 其中要取多个的话用逻辑与“ | ” 来进行连接,

这里写图片描述

如果我们定义窗口风格为可以水平重绘,垂直重绘, 那么代码方面就是这样写:

wndClass.style = CS_HREDRAW | CS_VREDRAW;	//设置窗口的样式

● 第三个参数, WNDPROC 类型的 lpfnwndProc , 它是一个函数指针, 指向窗口过程函数, 而窗口过程函数是一个回调函数, 其中, 回调函数人如其名, 并不是由该函数的实现方直接调用的, 而是在特定的事件或条件发生时由另外一方调用的, 用于对该事件或条件进行响应。

这里写图片描述

需要注意的是, 一个Windows 程序可以包含多个窗口过程函数, 一个窗口过程总是与某一个特定的窗口类相关联 (通过WNDCLASS 结构体中的 lpfnwndProc 成员变量来指定), 而基于该窗口类创建的窗口使用的是同一个窗口过程

这里写图片描述

同样地, 如果我们定义的窗口过程函数名称 WndProc, 那么对这个参数赋值的代码就是这样写:

wndClass.lpfnWndProc = WndProc;	//设置指向窗口过程函数的指针

● 第四个参数, int 类型的cbClsExtra , 表示窗口类附加内存, 一般我们将这个参数设置为0就可以了, 代码可以这样写:

wndClass.cbClsExtra	= 0;//窗口类的附加内存,取0就可以了

● 第五个参数, int 类型的cbWndExtra, 表示的是窗口的附加内存, 一般我们也将这个参数设置为0, 代码可以这样写:

wndClass.cbWndExtra	= 0;//窗口的附加内存,依然取0就行了

● 第六个参数, HINSTANCE 类型的 hInstance , 指定包含窗口过程的程序的实例句柄。 我们需要做的就是把 WinMain 函数的第一个参数, 也就是改程序当前运行的实例句柄传给它, 代码可以这样写:

wndClass.hInstance = hInstance;	//指定包含窗口过程的程序的实例句柄。

● 第七个参数, HICON 类型的 hlcon , 用于指定窗口类的图标句柄, 这个成员变量必须是一个图标资源的句柄, 如果这个成员为NULL, 那么系统将提供一个默认的图标。

在为 hIcon 变量赋值时, 我们可以调用 LoadIcon 函数来加载一个图标资源, 并将 LoadIcon 的返回值赋给 hIcon , LoadIcon 的原型如下:

HICON WINAPI LoadIcon
{
  _In_opt_ HINSTANCE hInstance,
  _In_ LPCTSTR lpIconName
};

LoadIcon 函数不仅可以加载Windows 系统提供的标准图标到内存中, 还可以加载由用户自己制作的图标资源到内存中, 并返回系统分配给该图标的句柄, 但要注意的是, 如果加载的是系统的标准图标, 那么第一个参数必须设置为NULL。

不过我们通常会使用 LoadImage 函数来从文件直接加载一个图标资源
如果要从工程目录下加载一个名为 icon.ico 的图标作为我们的程序图标, 那么代码可以这样写:

wndClass.hIcon=(HICON)::LoadImage(NULL,L"icon.ico",IMAGE_ICON,0,0,LR_DEFAULTSIZE|LR_LOADFROMFILE);  //本地加载自定义ico图标

其中的_T(“icon.ico”)中的T("") 会根据当前的字符环境智能地在宽字符和普通字符间转换, 与Unicode 字符集环境下的L“icon.ico”是一个用法

● 第八个参数, HCURSOR 类型的 hCursor , 它表示的是窗口类的光标句柄, 同样, 我们用LoadCursor 来为其加载一个光标资源, 返回系统分配给该光标的句柄, 这个函数的原型如下:

HCURSOR WINAPI LoadCorsor
{
  _In_opt_ HINSTANCE hInstance,
  _In_ LPCTSTR lpCursorName
}

一般地, 可以把这个参数设置为默认的箭头光标,就是这样写:

wndClass.hCursor = LoadCursor( NULL, IDC_ARROW ); //指定窗口类的光标句柄。

● 第九个参数, HBRUSH类型的 hbrBackground, 指定窗口类的背景画刷句柄, 它的前辍 hbr 表示它是一个画刷句柄。 当窗口发生重绘时, 系统使用这里指定的画刷来擦除窗口的背景, 我们既可以为 hbrBackground 指定一个画刷句柄, 也可以把它取为一个标准的系统颜色。 我们可以调用 GetStockObject 函数来得到系统的标准画刷,这个函数的原型如下:

HGDIOBJ GetStockObject 
{
  _in int fnObject
};

说明: 其中唯一的一个参数fnObject 指定要获取的对象的类型,
这个函数不仅可以获取画刷的句柄, 还可以获取画笔、字体和调色板的句柄, 比如我们要为背景指定一个灰色画刷的话, 代码可以这样写:

wndClass.hbrBackground=(HBRUSH)GetStockObject(GRAY_BRUSH);  //为hbrBackground成员指定一个灰色画刷句柄	```



● 第十个参数, LPCTSTR 类型的lpszMenuName, 一个以空终止的字符串,指定菜单资源的名字, 如果不需要下拉菜单(比如 编辑、保存和载入)的话, 就把这项设置为NULL 就好了, 代码可以这样写,

wndClass.lpszMenuName = NULL;//用一个以空终止的字符串,指定菜单资源的名字。

● LPCTSTR lpsz 类型的 lpszClassName, 一个以空终止的字符串, 指定窗口类的名字, 这里的类名称是自定义的, 名字取了之后后面会用到, 用来标识我们正在定义的这个窗口类

比如表示窗口类的名字我们指定为 “ForTheDreamOfGameDevelop”(中文的意识是:致我们的游戏开发的梦想!), 那么代码可以这样写:

wndClass.lpszClassName = L"ForTheDreamOfGameDevelop";//用一个以空终止的字符串,指定窗口类的名字。

● 第十二个参数, HICON类型的hIconSm, 指定窗口类的小图标句柄, 一般我们不用这个参数

我们把上面讲解的每个参数的代码串起来就是如下:

//【1】窗口创建四步曲之一:开始设计一个完整的窗口类

	WNDCLASSEX wndClass = { 0 };//用WINDCLASSEX定义了一个窗口类
	wndClass.cbSize = sizeof( WNDCLASSEX ) ;			//设置结构体的字节数大小
	wndClass.style = CS_HREDRAW | CS_VREDRAW;	//设置窗口的样式
	wndClass.lpfnWndProc = WndProc;					//设置指向窗口过程函数的指针
	wndClass.cbClsExtra		= 0;								//窗口类的附加内存,取0就可以了
	wndClass.cbWndExtra		= 0;							//窗口的附加内存,依然取0就行了
	wndClass.hInstance = hInstance;						//指定包含窗口过程的程序的实例句柄。
	wndClass.hIcon=(HICON)::LoadImage(NULL,L"icon.ico",IMAGE_ICON,0,0,LR_DEFAULTSIZE|LR_LOADFROMFILE);  //本地加载自定义ico图标
	wndClass.hCursor = LoadCursor( NULL, IDC_ARROW );    //指定窗口类的光标句柄。
	wndClass.hbrBackground=(HBRUSH)GetStockObject(GRAY_BRUSH);  //为hbrBackground成员指定一个灰色画刷句柄	
	wndClass.lpszMenuName = NULL;						//用一个以空终止的字符串,指定菜单资源的名字。
	wndClass.lpszClassName = L"ForTheDreamOfGameDevelop";		//用一个以空终止的字符串,指定窗口类的名字。

(2) 窗口类的注册

● 我们在设计完窗口类(WNDCLASSEX)后, 需要调用 RegisterClassEx 函数对其进行注册, 注册成功后, 才可以创建该类型的窗口

● 注册函数 RegisterClassEx 的原型声明为:

ATOM WINAPI RegisterClassEx  
{
  _In_const WNDCLASSEX *lpwcx
};

这个函数只有一个参数, 即上一步骤中所设计的窗口类对象的指针, 具体来讲,如果按照“窗口类设计”所写的代码, 一开始我们的窗口类的对象取名是wndClass , 即这句代码:

WNDCLASSEX wndClass={0};

这样我们下面填RegisterClassEx 函数的时候就填 wndClass了, 不过需要注意的是, 这里的变量类型前面有一个 “ * ”, 那么我们对应的在窗口类名前就要加上一个 & , 最终就这样写:

RegisterClassEx( &wndClass );

注意: 由于我们第一步用的是WNDCLASSEX 结构体, 那么这一步对应的就是 RegisterClassEx, 而如果我们第一步用的是WNDCLASS的话, 那么这里对应的就是用 RegisterClass(没有结尾处的Ex 后辍)。


**(3) 窗口的正是创建 **

● 首先可以调用 AdjustWindowRect() 函数来根据我们设定的尺寸和风格来计算窗口的尺寸, 窗口的类型取决于我们需要的真实尺寸,

如果我们需要 Win32 应用程序, 就会有像标题栏这样的非客户区, 一个环绕着应用程序的边框,等等。 如果我们要创建一个特殊的窗口尺寸, 需要牢记在心的是, 应用程序既有客户区,也有非客户区

● AdjustWindowRect () 函数首先利用一个矩形定义左下角、右下角、左上角、 、右上角 窗口区域的坐标, 左上角的属性代表了窗口的起始位置, 结合右下角则可以反映窗口的宽度和高度, AdjustWindowRect () 函数 中也专门有一个布尔类型的标明窗口类型的变量, 指示窗口是否拥有菜单栏, 而有无菜单栏影响着非客户区,

● 当我们设计好窗口类并且将其成功注册后, 就可以用 CreateWindow 函数来创建设计好的这种类型的窗口, CreateWindow 函数的 原型如下:

HWND WINAPI CreateWindow  
(
  _In_opt_ LPCTSTR lpClassName,
  _In_opt_ LPCTSTR lpWindowNmae,
  _In_opt_ DWORD dwStyle,
  _In_ int x,
  _In_ int y,
  _In_ int nWidth,
  _In_ int nHeight,
  _In_opt_ HWND hWndParent,
  _In_opt_ HMENU hMenu,
  _In_opt_ HINSTANCE hInstance,
  _In_opt_ LPVOID lpParam
  
  );

● 第一个参数, LPCTSTR 类型的 lpClassName, 指定对应窗口类的名称, 就是我们在第一步“窗口类的设计”过程中自己设定的窗口类名, 比如“窗口类的设计” 过程中我们写的是
wndClass.lpszClassName = _T(“ForTheDreamOfGameDevelop”), 那么我们这里也填 L “ForTheDreamOfGameDevelop”。

● 第二个参数, LPCTSTR 类型的 lpWindowNmae,用于指定创建的窗口名字, 也就显示在标题栏上的程序名字

● 第三个参数,DWORD 类型的 dwStyle, 用于指定创建的窗口样式与外观, 在这里要注意区分 WNDCLASSEX 中的 style 成员与 CreateWindow 函数的dwStyle 参数, 前者指定窗口类的样式, 基于该窗口类创建出的窗口都具有这些样式, 而后者指定某个具体的窗口样式。 显然,Windows 为我们预设好了很多不同风格的窗口样式, 我们只要在调用 CreateWindow 函数创建窗口时选定style 参数对应的窗口类型就可以了, 比如,有一个预设好的集各家所长的综合型的样式供我们选择, 它就是 WS_OVERLAPPEDWINDOW 类型, 这一类型的原型为:

#define WS_OVERLAPPEDWINDOW (WS_OVERLAPPED | \
WS_ CAPTION | \
WS_ SYSMENU | \
WS_ THICKFRAME | \
WS_ MINIMIZEBOX | \
WS_ MAXIMIZEBOX

)

这里写图片描述


● 第四个参数, int 类型的 x , 用于指定窗口的水平位置, 一般我们喜欢取它为 CW_USEDEFAULT, 表示默认的位置

● 第五个参数, int 类型的 y, 用于指定窗口的竖直位置,同样的喜欢取它为CW_USEDEFAULT, 表示默认的位置

● 第六个参数, int 类型的 nWidth, 用于指定窗口宽度

● 第七个参数, int 类型的 hHeight, 用于指定窗口的高度,

● 第八个参数, HWND 类型的 hWndParent, 用于指定被创建窗口的父窗口句柄,这个参数为可选可不选的,不过一般我们把它设置为NULL,

● 第九个参数, HMENU 类型的hMenu, 用于指定窗口菜单的资源句柄,也为可选可不选的一个参数, 同样我们把它设置为NULL 就可以了

● 第十个参数, HINSTANCE 类型的 hInstance , 用于指定窗口所属的应用程序实例的句柄, 也就是应用程序的实例ID, 即 WinMain 的第一个参数, 这个参数依然是可选可不选的, 一般还是把它取作和WinMain 函数的第一个参数一致, 对于我们之前写的WinMain 函数, 这个参数取hInstance

● 第十一个参数, lpParam 作为WM_CREATE 消息的附加参数 lParam 传入的数据指针, 此参数也为可选择的, 在MFC 程序中创建多文档界面时会用到, 而大多数情况下, 我们将它设置为NULL


● 如果窗口创建成功, 即CreateWindows 函数调用成功, CreateWindows 函数将返回系统为窗口分配的句柄。 如果调用失败, 即窗口创建失败, 则会返回NULL。

● 注意: 要在窗口创建之前先定义一个窗口句柄变量, 来接收创建窗口之后返回的句柄值

● 这一步整体来看, 就是调用CreateWindows 函数, 代码可以这样写:

	HWND hwnd = CreateWindow( L"ForTheDreamOfGameDevelop",//喜闻乐见的创建窗口函数CreateWindow,就是窗口的标题
	
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, WINDOW_WIDTH(800),WINDOW_HEIGHT(600), NULL, NULL, hInstance, NULL );


***(4) 窗口的显示与更新 **

● 这一步里面用到三个函数, 首先是用于设定窗口显示位置的MoveWindows 函数,然后是用于显示窗口的 ShowWindows 函数, 最后是用于更新窗口的 UpdateWindow 函数, 下面分别来介绍


改变窗口位置与大小


● 设定窗口位置主要就是MoveWindow 这个函数, 它可以改变指定窗口的位置和大小, 其中的窗口位置是以屏幕的左上角为原点 (0,0)的, 这个函数的函数原型为:

BOLL WINAPI MoveWindow
(
  _In_ HWND hWnd,
  _In_ int X,
  _In_ int Y,
  _In_ int nWidth,
  _In_ int nHeight,
  _In_ BOOL bRepaint
);

● 第一个参数, HWND类型的 hWnd, 很显然它就是我们之前用 CreateWindow 创建的窗口的句柄, 这里填我们在 “窗口创建四部曲之三: 窗口的正式创建” 时定义的窗口句柄名称, 表示MoveWindow 是在对那个窗口进行操作。

● 第二个参数, int 类型的 X, 很显然用于指定窗口左方相对于屏幕左上角的新位置

● 第三个参数, int 类型的Y, 用于指定窗口上方相对于屏幕左上角的新位置

● 第四个参数, int 类型的 nWidth, 用于指定窗口的新宽度

● 第五个参数, int类型的 nHeight , 用于指定窗口的新高度

● 第六个参数, BOOL 类型的 bRepaint, 指定了是否要重画窗口, 如果他设置为TRUE 的话, 则窗口会像通常那样在OnPaint 消息处理函数中接收到一条WM_PAINT 消息, 如果这个参数设置为FALSE 的话, 则不会发生任何类型的重画操作

● 来看一个调用实例, 下面代码MoveWindow 就是改变一下窗口的显示位置, 大小并不改变, 还是 800*600

MoveWindow(hwnd,200,50,WINDOW_WIDTH(800), WINDOW_HEIGHT(600),true);//调整窗口显示时的位置,使窗口左上角位于(250,80)处

**● 显示窗口 **


● 窗口创建之后, 就需要将它显示出来, 我们一般调用ShowWindow 来进行窗口的显示, 这个函数的函数原型如下:

BOOL WINAPI ShowWindow
(
  _In_ HWND hWnd,
  _In_ int nCmdShow
);

● 第一个参数, HWND 类型的 hWnd, 填写之前创建的那个窗口的句柄

● 第二个参数, int 类型的 nCmdShow, 用于指定窗口的显示状态, 常用的状态有如下几种:

这里写图片描述
这里写图片描述


● 对于这个参数, 我们可以直接填 nCmdShow, 因为这个函数是WinMain 内部调用的, 直接取WinMain 函数的参数就可以了

我们来看一个函数的调用实例:

ShowWindow( hwnd, nShowCmd );//调用ShowWindow函数来显示窗口

**● 更新窗口 **


● 在调用ShowWindow 函数之后, 紧接着要调用UpdateWindow 来刷新窗口, 这个函数的函数原型为:

BOOL UpdateWindow 
(
  _in HWND hWnd
);

说明: 它的参数 hWnd 指的是创建成功后的窗口的句柄,UpdateWindow 函数通过发送一个WM_PAINT 消息来刷新窗口, UpdateWindow 将 WM_PAINT 消息直接发送给了窗口过程函数进行处理, 而没有放到我们前面所说的消息队列里,

这个函数的调用实例为:

UpdateWindow(hwnd);//对窗口进行更新,就像我们买了新房子要装修一样

● 把这三步综合来看, 就是对前面使用 CreateWindow 函数创建的窗口进行一系列的移动(MoveWindow)、显示(ShowWindow)和更新(UpdateWindow)操作, 当然, 我们是通过 hWnd 这根窗口的纽带来指定对哪个窗口进行操作

总之, 窗口的显示和更新代码整体来看, 就是调用下面三个函数:

MoveWindow(hwnd,200,50,WINDOW_WIDTH(800), WINDOW_HEIGHT(600),true);//调整窗口显示时的位置,使窗口左上角位于(250,80)处

ShowWindow( hwnd, nShowCmd );//调用ShowWindow函数来显示窗口

UpdateWindow(hwnd);//对窗口进行更新,就像我们买了新房子要装修一样

**(13)两套消息循环体系 **


● 在经过窗口创建四部曲的洗礼后, 我们还需要编写一个消息循环, 不断地从消息队列中取出消息, 并且进行响应。 要从消息队列中获取消息, 有两个函数供我们选择, 它们分别是 GetMessage 与 PeekMessage, 两套消息循环体系分别围绕着这两个函数来展开


(13.1) 以GetMessage 为核心的消息循环体系


● 首先讲解一下 GetMessage 函数, 它的作用是从消息队列中获取消息。 如果队列里一条消息也没有, 它就会一直等待,直到消息的出现, 那么这个函数的函数原型为:

BOOL WINAPI GetMessage  
(
  _Out_ LPMSG lpMsg,
  _In_opt_ HWND hWnd,
  _In_ UINT wMsgFilterMin,
  _In_ UINT wMsgFilterMax

);
  

● 第一个参数,LPMSG类型的 lpMsg, 它指向一个消息(MSG)结构体, GetMessage 从线程的消息队列中取出的消息信息将保存在该结构体当中

● 第二个参数, HWND类型的 hWnd, 指定接收属于哪一个窗口的消息, 一般将这个参数设置为NULL就可以了, 表示用于接收属于调用线程的所有窗口的窗口消息

● 第三个参数, UINT 类型的 wMsgFilterMin, 指定要获取消息的最小值, 通常设置为0就可以了

● 第四个参数, UINT 类型的 wMsgFilterMax, 指定要获取消息的最大值。 如果wMsgFilterMin和wMsgFilterMax 都设置为0的话, 就表示接受所有消息, 来者不拒

注意: GetMessage 函数如果收到 除 WM_QUIT 外的消息, 都会返回非零值。 而对于WM_QUIT 消息, GetMessage 函数则会返回零, 如果出现了错误, 就会返回-1, 列如: 当参数hWnd 是无效的窗口句柄时, GetMessage 函数就会返回-1。

● 通常我们采用 GetMessage 为核心来写一个消息循环, 它的核心代码为:

MSG msg={0}; //定义初始化消息
while (GetMessage(&msg,NULL,0,0))  //不断地从消息队列中取出消息
{
   TranslateMessage(&msg); // 将虚拟键消息转化为字符消息
   DispatchMessage(&msg); // 分发一个消息给窗口程序
}

● 第一行代码解析: 定义一个MSG结构体, 注意要在这里给它赋上初值, 否者Visual Studio 会报出警告的。

● 第二行代码解析: 一个while 循环的开始, while循环的判断条件是GetMessage 的返回值, 前面我们说过, GetMessage 函数只有在接受到 WM_QUIT 消息时, 才返回 0, 这个时候while语句判断的条件就为假, 循环就退出了, 这样程序才有可能结束运行,

而在没有接收到 WM_QUIT 消息时, Windows 应用程序就通过这个while循环来保证程序始终处于运行状态。 另外, GetMessage 的作用是从消息队列中取出消息, 如果队列里一条消息也没有的话, 它就会一直等待, 直到出现一条消息。

● 第3行~4行代码解析: while 循环体开始, 首先调用了一个TranslateMessage 函数, TranslateMessage 函数用于将虚拟键消息转化为字符消息。 字符消息被投递到调用线程的消息队列中, 当下一次调用 GetMessage 函数时被取出

注意的是: TranslateMessage 函数并不会修改原有的消息, 它只是产生新的消息并投递到消息队列中

● 第5行~6行代码解析: 接着我们调用 DispatchMessage函数并打上while循环体的回括号, DispatchMessage 函数分配一个消息到窗口过程函数, 由窗口过程函数对消息进行处理, DispatchMessage实际上是将消息回传给操作系统, 由操作系统调用窗口过程函数对消息进行处理(响应)。

这里写图片描述


上图的详细说明如下:

过程《1》: 操作系统接收到应用程序的窗口消息 , 并将消息投递到该应用程序的消息队列中

过程《2》: 应用程序在消息循环中调用GetMessage 函数从消息队列中取出一条一条的消息。 取出消息后, 应用程序可以对消息进行一些预处理, 例如, 放弃对某些消息的响应, 或者调用TranslateMessage 产生新的消息。

过程《3》: 应用程序调用 DispatchMessage , 将消息回传给操作系统。 消息是由MSG结构体对象来表示的, 其中就包含了接收消息的窗口的句柄。 因此, DispatchMessage 函数总能进行正确的传递。

过程《4》: 系统利用 WNDCLASSEX 结构体的 lpfnWndProc 成员保存的窗口过程函数的指针调用窗口过程, 对消息进行处理(即“系统给应用程序发送了消息”)。


(13.2)以PeekMessage为核心的消息循环体系


● 在游戏编写过程中, 相对于GetMessage 函数,用的更多的是 PeekMessage 函数, 那么这个函数的函数原型为:

BOOL WINAPI PeekMessage 
(
   _Out_ LPMSG lpMsg,
  _In_opt_ HWND hWnd,
  _In_ UINT wMsgFilterMin,
  _In_ UINT wMsgFilterMax,
  _In_ UNIT wRemoveMsg
);

● 第一个参数,LPMSG类型的 lpMsg, 它指向一个消息(MSG)结构体, PeekMessage 从线程的消息队列中取出的消息信息将保存在该结构体当中

● 第二个参数, HWND类型的 hWnd, 指定接收属于哪一个窗口的消息, 一般将这个参数设置为NULL就可以了, 表示用于接收属于调用线程的所有窗口的窗口消息

● 第三个参数, UINT 类型的 wMsgFilterMin, 指定要获取消息的最小值, 通常设置为0就可以了

● 第四个参数, UINT 类型的 wMsgFilterMax, 指定要获取消息的最大值。 如果wMsgFilterMin和wMsgFilterMax 都设置为0的话, 就表示接受所有消息, 来者不拒

● 第五个参数, UNIT 类型的wRemoveMsg, PeekMessage 相对于GetMessage 多了这第五个参数, 它用于指定消息的获取方式, 一般这个参数可以在 PM_NOREMOVE 和 PM_REMOVE 中取值

说明: 如果取PM_NOREMOVE 的话, 那么PeekMessage 函数取出某条消息后, 这条消息将不会从消息队列中被移除(就像这条消息被偷看了一样)

而如果取PM_REMOVE 的话, 那么PeekMessage 函数取出某条消息后,这条消息将从消息队列中被移除, 一般我们都把这个参数取为 PM_REMOVE, 这样就是和 GetMessage一样的 “取消息” 操作。

注意: 关于PeekMessage 函数的返回值与GetMessage 不一样, 如果PeekMessage 能在消息队列中取出消息, 那么返回值为非0, 如果PeekMessage 不能在消息队列中取到消息, 返回值为0

● PeekMessage 和GetMessage 两个函数的比较:

相同点: 两个函数都用于查看应用程序消息队列, 有消息时将队列中的消息派发出去

下面就是关键点了, 无论应用程序的消息队列是否有消息, PeekMessage 函数都立即返回, 程序得以继续执行后面的语句(无消息则执行其他指令,有消息时一般要将消息派发出去, 在执行其他指令)

因为在游戏程序中,时时刻刻都要调用用于绘制的函数进行画面的绘制, 也就是说没有收到消息的时候, 我们的程序还得继续往下运行, PeekMessage 就恰恰符合了我们游戏程序的运行要求

而GetMessage 函数只有在消息队列中有消息时才返回, 队列中无消息就会一直等待, 直至下一个消息出现时才返回, 在等的这段时间, 应用程序不能执行任何指令, 这个时候应用程序就像在休眠一样。 笼统的说, 不按键盘, 不按鼠标的话, 画面就不动, 这样显然是不妥的, 所以 GetMessage 函数的这套机制不符合我们游戏程序的运行要求

这里写图片描述

另外, 在这里先给大家看一下后面我们一直使用的游戏demo 示例程序中关于使用 PeekMessage 函数来实现消息循环的代码:

//【5】消息循环过程
	MSG msg = { 0 }; //定义并初始化msg
	while( msg.message != WM_QUIT )	//使用while循环,如果消息不是WM_QUIT消息,就继续循环
	{
		if( PeekMessage( &msg, 0, 0, 0, PM_REMOVE ) )   //查看应用程序消息队列,有消息时将队列中的消息派发出去,没有消息时依然继续往下执行
		{
			TranslateMessage( &msg );//将虚拟键消息转换为字符消息
			DispatchMessage( &msg );//分发一个消息给窗口程序。
		}
		else
		{
		   Direct3D_Update(hwnd); //调用更新函数, 进行画面的更新
		   Direct3D_Render(hwnd); //调用渲染函数, 进行画面的渲染
	}


上面这段代码的理解:

● 首先依然是一个while 循环主持大局, 判断条件是msg.message != WM_QUIT, 即如果不是程序退出WM_QUIT 消息的话, 循环就一直在进行着, while 循环的循环体中是一个 if…else… 语句, 首先我们来看前半部分:

根据上面我们讲过的PeekMessage 函数的返回值, 有消息返回非零, 无消息返回0, 那么就是说, 在有消息的时候, 与之前讲GetMessage 消息处理的机制相似, 先用PeekMessage 函数取出消息, 并用TranslateMessage 函数将虚拟键消息装换为字符消息, 接着用 DispatchMessage 函数分发一个消息给窗口程序, 而在没有消息的时候,就pass 掉, 继续往下执行else 部分, else部分的大括号内是我们后面讲解 Direct3D 的时候一直会用到的自定义更新函数与绘制函数, 在这里我们理解他们为绘图函数就行了

上面这整段代码合起来就是, 有消息的时候处理消息, 没消息的时候就进行绘图操作, 如果遇到WM_QUIT 就退出循环, 程序结束

最后提醒一下: GetMessage 与PeekMessage 函数的第二个参数通常不要填窗口句柄, 最好填 0 , 因为有可能某一时间这个窗口句柄失效了, 而消息循环仍在进行, 这样就会导致错误


(13)窗口类的注销


● 在WinMain 结束之前, 最好把在窗口创建四部曲中创建的那个窗口类进行注销。 其实, 不进行注销往往也不会出现什么问题, 但是为了我们程序的稳定性着想, 还是需要在消息循环之后, WinMain 函数结束之前, 对之前自定义的那个窗口类进行注销。

● 我们进行窗口注销要用到的是 UnregisterClass 这个函数, 与之前的 RegisterClassEx 函数对应, 不过这里用的不是UnregisterClassEx, 而是去掉Ex的UnregisterClass , 这里需要注意。

● 那么这个函数的函数原型为:

BOOL WINAPI UnregisterClass 
(
  _In_ LPCTSTR lpClassName,
  _In_opt_ HINSTANCE hInstance

);

● 第一个参数填写我们需要注销的类名称

● 第二个参数填写创建这个类的应用程序的实例句柄, 也就是填写WinMain 函数的 hInstance, 或者是类的实例句柄 wndClass.hInstance, 其实填写这两者是等价的, 因为之前在设计窗口类的时候我们写了这句代码:

wndClass.hInstance=hInstance;//指定包含窗口过程的程序的实例句柄

使用UnregisterClass 来注销窗口类的代码就这样写:

UnregisterClass(L"ForTheDreamOfGameDevelop", wndClass.hInstance);  //程序准备结束,注销窗口类



(14) 窗口过程函数


● 这个窗口过程函数,主要用于处理发送给窗口的消息。 一般的 Windows 应用程序的主要代码部分就集中在窗口过程函数中, 当然这里我们说的是一般的Windows 程序, 对于游戏程序而言, 在窗口过程函数中很少去写大量的代码的

● 窗口过程函数的声明原型如下:

LRESULT CALLBACK WindowProc
(
  _In_ HWND hwnd,
  _In_ UINT uMsg,
  _In_ WPARAM wParam,
  _In_ LPARAM lParam
);

注意: 窗口过程函数比较特殊, 它的名字在实际编写程序过程中可以随便取的, 不一定非要叫WindowProc, 但是函数的定义形式必须和上面的声明格式一致

注意: 系统通过窗口过程函数的地址(指针)来调用窗口过程函数, 而不是通过函数的名字来调用的。

● 一开头的LRESULT 是窗口过程函数的返回值, 一般情况下是个非零值。
紧跟其后的CALLBACK 用来告诉Windows 这个函数是一个回调函数, 每当Windows 参数遇到了需要处理的事件时, 就调用这个函数

● 第一个参数, HWND类型的 hwnd, 就是需要处理消息的那个窗口的句柄了。

第二个参数, UINT 类型的uMsg, 表示待处理消息的ID, 即消息的类型。

● 第三、四个参数, WPARAM 类型的 wParam 与 LPARAM类型的 lParam, 用于表示消息的附加信息, 这个附加信息会随着消息类型的不同而不同。

另外, 因为一个程序可以有多个窗口, 而窗口过程函数的第一个参数就用于指定了接收消息的那个特定窗口。 我们可以同时打开几个窗口, 各自窗口具有不同的句柄和分开定义的窗口过程函数来处理各自的消息。

● 我们常常在窗口过程函数中使用 switch/case 语句来确定窗口过程接收的是什么消息, 以及如何对这个消息进行处理。

● 下面来看代码, 这段代码勾勒出了通常的窗口过程函数共同的样子, 那么这段代码也是后面我们学习 Direct3D 游戏编程时常用的窗口过程函数的代码 , 其中的 Direct3D Render(hwnd) 为 进行渲染(绘图)的自定义函数, 而 Direct3D_CleanUp() 是用于程序结束之前清理资源的自定义函数, 代码如下:

LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam ) //窗口过程函数wndProc    
{
	switch( message )	//switch语句开始
	{
	case WM_PAINT:		// 若是客户区重绘消息
		ValidateRect(hwnd, NULL);// 更新客户区的显示
		break;			//跳出该switch语句

	case WM_KEYDOWN:               // 若是键盘按下消息
		if (wParam == VK_ESCAPE)   // 如果被按下的键是ESC
			DestroyWindow(hwnd); // 销毁窗口, 并发送一条WM_DESTROY消息
		break;			//跳出该switch语句

	case WM_DESTROY:			//若是窗口销毁消息
		PostQuitMessage( 0 );//向系统表明有个线程有终止请求。用来响应WM_DESTROY消息
		break;				//跳出该switch语句

	default://若上述case条件都不符合,则执行该default语句
		return DefWindowProc( hwnd, message, wParam, lParam );	//调用缺省的窗口过程
	}

	return 0;	//正常退出
}

注意: 在其中调用了 DefWindowProc 函数, 来为应用程序处理没有窗口消息、 来提供默认的处理


(15) 一个完整的窗口程序的诞生


● 注意: 要创建Win32项目, 不要创建成控制台项目,

● 一个完整的窗口程序的书写, 按以下内容的顺序编写:

WinMain 函数——窗口创建四部曲——消息循环——窗口类的注销——窗口过程函数

注意: 除了窗口过程函数是在WinMain函数体之外编写的, 其他的无论是窗口创建四部曲, 消息循环, 还是窗口类的注销, 都是在WinMain 函数体之类进行编写的。

● 现在我们把前面学到的细节知识融合在一起, 适当的做了修改, 就很容易得到如下的这些创建出一个窗口的代码, 这就是我们后面学习 GDI与 DirectX 游戏编程时所用到的基本框架:


//-----------------------------------【程序说明】---------------
//  程序名称::GameCore
//	 2013年3月 Create by 浅墨
//  描述:用代码勾勒出游戏开发所需的程序框架
//------------------------------------------------------------------------------------------------


//-----------------------------------【头文件包含部分】---------------------------------------
//	描述:包含程序所依赖的头文件
//------------------------------------------------------------------------------------------------
#include <windows.h>

//-----------------------------------【宏定义部分】--------------------------------------------
//	描述:定义一些辅助宏
//------------------------------------------------------------------------------------------------
#define WINDOW_WIDTH	800				//为窗口宽度定义的宏,以方便在此处修改窗口宽度
#define WINDOW_HEIGHT	600				//为窗口高度定义的宏,以方便在此处修改窗口高度
#define WINDOW_TITLE	L"【致我们永不熄灭的游戏开发梦想】程序核心框架"		//为窗口标题定义的宏


//-----------------------------------【全局函数声明部分】--------
//	描述:全局函数声明,防止“未声明的标识”系列错误
//-------------------------------------------------------------
LRESULT CALLBACK	WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam );  //窗口过程函数


//-----------------------------------【WinMain( )函数】--------------------------------------
//	描述:Windows应用程序的入口函数,我们的程序从这里开始
//------------------------------------------------------------------------------------------------
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,LPSTR lpCmdLine, int nShowCmd)
{
	//【1】窗口创建四步曲之一:开始设计一个完整的窗口类
	WNDCLASSEX wndClass = { 0 };							//用WINDCLASSEX定义了一个窗口类
	wndClass.cbSize = sizeof( WNDCLASSEX ) ;			//设置结构体的字节数大小
	wndClass.style = CS_HREDRAW | CS_VREDRAW;	//设置窗口的样式
	wndClass.lpfnWndProc = WndProc;					//设置指向窗口过程函数的指针
	wndClass.cbClsExtra		= 0;								//窗口类的附加内存,取0就可以了
	wndClass.cbWndExtra		= 0;							//窗口的附加内存,依然取0就行了
	wndClass.hInstance = hInstance;						//指定包含窗口过程的程序的实例句柄。
	wndClass.hIcon=(HICON)::LoadImage(NULL,L"icon.ico",IMAGE_ICON,0,0,LR_DEFAULTSIZE|LR_LOADFROMFILE);  //本地加载自定义ico图标
	wndClass.hCursor = LoadCursor( NULL, IDC_ARROW );    //指定窗口类的光标句柄。
	wndClass.hbrBackground=(HBRUSH)GetStockObject(GRAY_BRUSH);  //为hbrBackground成员指定一个灰色画刷句柄	
	wndClass.lpszMenuName = NULL;						//用一个以空终止的字符串,指定菜单资源的名字。
	wndClass.lpszClassName = L"ForTheDreamOfGameDevelop";		//用一个以空终止的字符串,指定窗口类的名字。

	//【2】窗口创建四步曲之二:注册窗口类
	if( !RegisterClassEx( &wndClass ) )				//设计完窗口后,需要对窗口类进行注册,这样才能创建该类型的窗口
		return -1;		

	//【3】窗口创建四步曲之三:正式创建窗口
	HWND hwnd = CreateWindow( L"ForTheDreamOfGameDevelop",WINDOW_TITLE,		//喜闻乐见的创建窗口函数CreateWindow
		WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, WINDOW_WIDTH,
		WINDOW_HEIGHT, NULL, NULL, hInstance, NULL );

	//【4】窗口创建四步曲之四:窗口的移动、显示与更新
	MoveWindow(hwnd,250,80,WINDOW_WIDTH,WINDOW_HEIGHT,true);		//调整窗口显示时的位置,使窗口左上角位于(250,80)处
	ShowWindow( hwnd, nShowCmd );    //调用ShowWindow函数来显示窗口
	UpdateWindow(hwnd);						//对窗口进行更新,就像我们买了新房子要装修一样

	//【5】消息循环过程
	MSG msg = { 0 };		//定义并初始化msg
	while( msg.message != WM_QUIT )			//使用while循环,如果消息不是WM_QUIT消息,就继续循环
	{
		if( PeekMessage( &msg, 0, 0, 0, PM_REMOVE ) )   //查看应用程序消息队列,有消息时将队列中的消息派发出去。
		{
			TranslateMessage( &msg );		//将虚拟键消息转换为字符消息
			DispatchMessage( &msg );			//分发一个消息给窗口程序。
		}
	}

	//【6】窗口类的注销
	UnregisterClass(L"ForTheDreamOfGameDevelop", wndClass.hInstance);  //程序准备结束,注销窗口类
	return 0;  
}


//-----------------------------------【WndProc( )函数】--------------------------------------
//	描述:窗口过程函数WndProc,对窗口消息进行处理
//------------------------------------------------------------------------------------------------
LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam )      
{
	switch( message )						//switch语句开始
	{
	case WM_PAINT:						// 若是客户区重绘消息
		ValidateRect(hwnd, NULL);		// 更新客户区的显示
		break;									//跳出该switch语句

	case WM_KEYDOWN:                // 若是键盘按下消息
		if (wParam == VK_ESCAPE)    // 如果被按下的键是ESC
			DestroyWindow(hwnd);		// 销毁窗口, 并发送一条WM_DESTROY消息
		break;									//跳出该switch语句

	case WM_DESTROY:				//若是窗口销毁消息
		PostQuitMessage( 0 );		//向系统表明有个线程有终止请求。用来响应WM_DESTROY消息
		break;								//跳出该switch语句

	default:									//若上述case条件都不符合,则执行该default语句
		return DefWindowProc( hwnd, message, wParam, lParam );		//调用缺省的窗口过程
	}

	return 0;			//正常退出
}

注意: 我们在设计窗口类的时候, 对于窗口的背景是这样写的:

wndClass.hbrBackground=(HBRUSH)GetStockObject(GRAY_BRUSH); //为hbrBackground成员指定一个灰色画刷句柄

说明: 上面的代码中为wndClass.hbrBackground 指定了一个灰色的画刷句柄, 所以我们得到的窗口客户区里就是灰色的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值