《Windows游戏编程大师技巧》学习笔记(六)

《Windows游戏编程大师技巧》学习笔记(六)

WM_PAINT 消息

在之前的代码中,我们对于WM_PAINT消息的处理方式如下:

PAINTSTRUCT	ps;
HDC			hdc;

case WM_PAINT:
	hdc = BeginPaint(hwnd, &ps);
	// 在此处执行绘图操作
	EndPaint(hwnd, &ps);
	return 0;

如下图所示,当一个窗口被移动、改变大小或被其他窗口遮盖时,WN_PAINT消息便被发送了:
WM_PAINT消息.png
在调用BeginPaint函数后,需要重新绘图的区域坐标便被保存在了psrcPaint字段中,rcPaint字段的定义如下:

typedef struct tagRECT
{
    LONG    left;   // 矩形左边缘坐标
    LONG    top;    // 矩形上边缘坐标
    LONG    right;  // 矩形右边缘坐标
    LONG    bottom; // 矩形下边缘坐标
} RECT, *PRECT, NEAR *NPRECT, FAR *LPRECT;

如上述,使用BeginPaint函数只能获取窗口需要重绘的部分,所以如果需要获取窗口的更多信息(如图形设备上下文),则需要将BeginPaint...EndPaint函数对替换为GetDC...ReleaseDC函数对,相关原型如下:

HDC GetDC(HWND hWnd);
int ReleaseDC(HWND hWnd, HDC hdc);

所以之前对于WM_PAINT消息的处理代码就可以变为:

HDC gdc = NULL;

case WM_PAINT:
	gdc = GetDC(hwnd);
	// 在此处执行绘图操作
	ReleaseDC(hwnd, gdc);
	return 0;

如果只是这样简单地替换,会有新的问题:

BeginPaint...EndPaint会向Windows发出一个消息,指示窗口内容已恢复(甚至在没有进行任何图形调用的情况下),所以Windows不会继续发出WM_PAINT消息;如果如上文所述简单替换,那么为了使窗口有效,WM_PAINT会一直不停地传递下去;

要使需要重画的窗口区域有效,并通知Windows已经恢复了该窗口,可以使用专用函数ValidateRect,函数原型如下:

BOOL WINAPI ValidateRect(
    HWND hWnd,          // 窗口句柄
    CONST RECT *lpRect  // 有效矩形坐标
);

在大多数情况下,有效区域是整个窗口,所以上文中的代码段,可以改成如下所示:

PAINTSTRUCT	ps;
HDC			hdc;
RECT        rect;

case WM_PAINT:
	hdc = GetDC(hwnd);
	// 在此处执行绘图操作
	ReleaseDC(hwnd, hdc);
    GetClientRect(hwnd, &rect);
    ValidateRect(hwnd, &rect);
	return 0;

GetClientRect函数可以获取用户矩形区域的坐标,由于窗口可以任意移动,一个窗口拥有两套坐标系:Windows坐标系和用户坐标系;Windows坐标系相对于屏幕,而用户坐标系相对于窗口左上角(0, 0),如下图所示:
Windows坐标系和用户坐标系对比.png
另外,如果想手动地使整个窗口无效,以确保BeginPaint函数设置的rcPaint字段为整个窗口区域,那么可以调用InvalidateRect函数,函数原型如下:

BOOL WINAPI InvalidateRect(
    HWND hWnd,          // 窗口句柄
    CONST RECT *lpRect, // 矩形区域
    BOOL bErase         // 是否使用背景画刷填充
);

函数中第二个参数所设置的窗口无效区域,会和原本的无效区域形成并集,当我们传递NULL时,函数会将整个窗口作为无效区域进行设置;
所以,前述代码也可以继续使用BeginPaint...EndPaint对进行实现,只不过需要修改为如下方式:

PAINTSTRUCT	ps;
HDC			hdc;

case WM_PAINT:
    InvalidateRect(hwnd, NULL, FALSE);
	hdc = BeginPaint(hwnd, &ps);
	// 在此处执行绘图操作
	EndPaint(hwnd, &ps);
	return 0;

视频显示基础和色彩

与图形有关的概念和术语:
相关定义.png
有两种方式用于在显存中表示颜色,分为直接方式和间接方式:直接方式又叫RGB模式,调色板模式以间接模式工作
RGB模式下,用代表红绿蓝三原色的16位、24位或32位数据来表示屏幕上的每个像素颜色,显而易见,16位或32位的数据不能被平均分为三份,所以在这种情况下,每个色彩通道会占据不同的位数,如下图所示:
RGB模式下的色彩编码.png

调色板是一个有256项的表,每一项都是一个单字节值(0~255),但是实际上每一个输入项都是由三个8位的红绿蓝项构成的,从本质上说它是一个24位RGB全彩描述符;
颜色查找表的工作原理如下:当从8位颜色模式的屏幕上读取到一个像素时,嘉定其值为26,则26就用作颜色表的一个索引;然后将对应于26号索引地址的色彩描述符的24位RGB值取出,显示实际的颜色;
也就是说,通过这种办法,在同一时间屏幕上可以有256种不同的色彩,但是他们是来自于16.7百万色或24位RGB值;
查找过程如下图:
256色调色板模式的工作原理.png

基本文本显示

使用GDI进行文本显示有两个常用函数:TextOutDrawText,在非Unicode环境下二者的定义如下:

BOOL WINAPI TextOutA(
    HDC hdc,            // 设备上下文句柄
    int x, int y,       // 开始输出文本的位置
    LPCSTR lpString,    // 文本内容
    int c               // 文本长度
);

int WINAPI DrawTextA(
    HDC hdc,            // 设备上下文句柄
    LPCSTR lpchText,    // 文本内容
    int cchText,        // 文本长度
    LPRECT lprc,        // 文本边框矩形
    UINT format         // 文本显示样式
);

TextOut函数参数意义明显,此处不再进行解释;
DrawText函数的第四个参数为包裹文本的矩形,这就意味着所有用此函数进行显示的文字都会在显示前被这个矩形进行裁剪;第五个参数为文本显示的样式,如可以使用DT_LEFT调整文本为左对齐,更多的标志见Win32SDK - DrawTextExA
可以通过SetTextColorSetBkColor函数分别设置文本的前景色和背景色,二者函数原型如下:

COLORREF WINAPI SetTextColor(
    HDC hdc,        // 设备上下文句柄
    COLORREF color  // 前景色
);

COLORREF WINAPI SetBkColor(
    HDC hdc,        // 设备上下文句柄
    COLORREF color  // 背景色
);

如上两个函数调用结束后,随后所有的文本显示的前景色和背景色均使用设置的颜色;函数的返回值均为设置前的颜色,所以我们可以保存其返回值,在使用新的颜色渲染结束后恢复原先的颜色;
COLORREF结构体的定义如下(在WinSDK-10.0中此结构体直接被定义为了DWORD,以下定义为作者在书中对旧版本的记录,二者程序意义相同):

typedef struct tagCOLORREF
{
    BYTE bRed;      // 红色分量
    BYTE bGreen;    // 绿色分量
    BYTE bBlue;     // 蓝色分量
    BYTE bDummy;    // 尚未定义使用
};

COLORREF在内存中表示为0x00bbggrr,此处使用小端模式,创建一个有效的颜色定义,可以使用RGB宏,如下所示:

COLORREF red = RGB(255, 0, 0);
COLORREF yellow = RGB(255, 255, 0);

PALETTEENTRYCOLORREF相似,同样可以用来描述颜色,其定义如下:

typedef struct tagPALETTEENTRY {
    BYTE        peRed;
    BYTE        peGreen;
    BYTE        peBlue;
    BYTE        peFlags;
} PALETTEENTRY, *PPALETTEENTRY, FAR *LPPALETTEENTRY;

对于peFlags成员,可以有如下取值:
peFlags取值表.png
由于PALETTEENTRYCOLORREF二者只有最后一个字节的解释不同,所以在多数情况下它们是可以互换的;
默认状态下输出的文本背景色是由SetBkColor进行指定,如果需要设置背景色透明,则可以通过SetBkMode进行设置,函数原型如下:

int WINAPI SetBkMode(
    HDC hdc,    // 设备上下文句柄
    int mode    // 背景模式
);

第二个参数使用TRANSPARENT表示使用透明背景,使用OPAQUE恢复为纯色不透明背景;
与前景色和背景色设置函数相同,SetBkMode的返回值依然是设置前的旧值,可以保存以便恢复;
上述介绍的函数中,TextOut函数的运行速度相对于DrawText来说较快,并且背景不透明的文本渲染速度较快,向淡色背景上输出文本时可以不用设置文本背景色透明;
综上,一个可能的文本输出代码如下:

int old_tmode;
COLORREF old_fcolor, old_bcolor;

HDC hdc = GetDC(hwnd);

old_fcolor = SetTextColor(hdc, RGB(0, 255, 0));
old_bcolor = SetBkColor(hdc, RGB(0, 0, 0));
old_tmode = SetBkMode(hdc, TRANSPARENT);
TextOut(hdc, 20, 30, "Hello World!", strlen("Hello World!"));

// 恢复修改前的颜色,可选
SetTextColor(hdc, old_fcolor);
SetBkColor(hdc, old_bcolor);
SetBkMode(hdc, old_tmode);

ReleaseDC(hwnd, hdc);

于是,很有年代感的文本就出现啦:
程序截图.png