nbsp;
第2章 Windows编程进阶
“我总忍不住要表达我不喜欢那些认为理解会破坏他们的经验的人们……他们怎么会知道呢?”
Marvin Minsky
在前一章里已经学过了如何创建窗口——现在开始介绍如何利用Windows的绘图及文字工具。当熟悉了这些内容之后,将确切地说明什么是资源,怎样利用它们来创建自己的菜单,图标,鼠标光标等内容。学完本章后,将具备足够的Windows编程基本知识来理解本书其余章节所有源代码范例,然后就可以进入遗传算法和神经网络的学习了。其中偶尔会涉及一些必须让读者了解的额外知识,但一定是很容易解释清楚的。
Windows负责屏幕上绘制图形的部分称为图形设备接口(Graphics Device Interface,GDI),可在窗口中显示的图形大体上可以被归为4类:
q 文本(text)。GDI中关于文本的部分显然非常重要。GDI中提供了大量的格式化文本和输出工具,用户几乎可以得到想得到的任何一种方式在屏幕上创建和输出文字。
q 直线(line),形状(shape),曲线(curve)。GDI对绘制直线,曲线(Bezier曲线),基本形(如矩形和椭圆)和多边形提供了充分的支持。多边形就是由一系列相互连接的顶点组成的shape,最后一点和第一点相连,形成闭合图形。绘图时,首先需要创建一支用来绘图的画笔(Pen),然后用该画笔绘制需要的shape。
q 位图(bitmap)。GDI提供了许多处理位图的函数。可以加载位图,缩放位图,保存位图或从一处复制位图到另一处。位图复制通常被游戏程序员称为blitting(图像位块传送)。
q 填充区域(filled area)。绘图时除了画笔,还可以创建用户自己的画刷(Brush)。画刷可用来填充屏幕上的区域(region)和shape。
注意:在游戏界里,GDI是以慢出名的。所谓慢,是与其他API,如OpenGL或微软的DirectX相比较而言的。在例子中选用GDI,是因为它简单好用,也好理解,使用它也足够快,而且更重要的是,在代码中不会充斥混淆视听的复杂API调用。
设备描述表——又称DC——在使用GDI的图形文字绘制中扮演很重要的角色。在朝任何图形输出设备——屏幕,打印机,甚至是内存中的一块位图——开始绘图之前,都必须获得该设备的设备描述表的句柄。画刷,画笔,光标,桌面,窗口实例(hInstance),图标,位图……都有相应的句柄。句柄就象执照。要开车需要执照,要经营酒店也需要执照。类似地,也需要取得对特定类型的对象或设备的句柄,才能进行想要的操作。如果向Window发出请求“是否可以在这个窗口上绘图?”,则Windows就会赋予用户一个执照——窗口句柄——这样就可以绘图了。
2.1.1.1 如何得到句柄(Handle)?
可以有几种方法用来取得设备描述表句柄,为了简单,将用缩写HDC来称呼设备描述表句柄。
在第一章中处理WM_PAINT消息的内容时,首先创建的是PAINTSTRUCT结构,Windows将其填充,以描述关于窗口的详细信息。PAINTSTRUCT结构定义如下:
注意:从PAINTSTRUCT中获得的句柄只对在RECT结构rcPaint定义的区域中绘图有效。如果想要在此区域外绘图,就需要从其他途径获得HDC。
当调用BeginPaint时,它不但返回一个HDC,同时还填充了PAINTSTRUCT结构。因此,一个获得HDC的方法如下:
HDC hdc=GetDC(hWnd);
hWnd为你要获取HDC的窗口的句柄。无论何时用这个方法创建HDC,一定不要忘记在使用完毕之后释放它。可以使用ReleaseDC函数来释放它:
ReleaseDC(hWnd,hdc);
没有在WM_PAINT中调用ReleaseDC是因为EndPaint函数自动释放了DC。但如果创建了HDC而事后没有释放,就会出现资源泄漏问题,慢慢地程序也可能出现许多不曾听说的故障。甚至可能导致系统崩溃。记住这一点很重要。
如果需要的话,也可以获取一个应用于整个窗口(包括系统菜单和标题栏域)的HDC,而不仅仅是客户区。为此可以用GetWindowDC(hWnd)来实现。
HDC hdc=GetDC(NULL);
也可以获得对整个屏幕的HDC,只要在调用GetDC的时候用NULL作为参数即可。
HDC hdc=GetDC(NULL);
提示:许多初学者会忘记的事情是Windows并不监视窗口的重画。因此,如果在WindowProc函数的WM_PAINT部分以外进行绘图,一定要确保你画出来的东西会在屏幕需要更新的时候(例如,用户拖动一个其他窗口经过了窗口的上空,或用户把窗口最小化,或最大化)被重画。否则,很快地屏幕看起来就会一团糟了。
这里有一个绘制线条的程序(默然:只需要更换第1章的main.cpp就可以了,define.h文件不变):
//-----------------------------------------------------------------------
//
// Name: GDI_Lines1
//
// Author: Mat Buckland 2002
//
// Desc: 基本线程绘制示范
//------------------------------------------------------------------------
#include <windows.h>
#include "defines.h"
//--------------------------------- 公共声明 ------------------------------
//
//------------------------------------------------------------------------
char* g_szApplicationName = "The Groovy GDI - Lines";
char* g_szWindowClassName = "MyWindowClass";
//---------------------------- WindowProc ---------------------------------
//
// 这是处理所有Windows消息的回调函数
//-------------------------------------------------------------------------
LRESULT CALLBACK WindowProc (HWND hwnd,
UINT msg,
WPARAM wParam,
LPARAM lParam)

...{
//记录客户window区域的坐标
static int cxClient, cyClient;
switch (msg)

...{
//当你的应用程序窗口第一次显示时,
//一个WM_CREATE消息被送出
case WM_CREATE:

...{
//为了获得客户端窗口的尺寸,首先我们需要创建一个RECT结构
//然后请求Windows用客户端窗口的尺寸填充这个结构
//从而使用RECT对cxClient 与cyClient赋值
RECT rect;
GetClientRect(hwnd, &rect);
cxClient = rect.right;
cyClient = rect.bottom;
}
break;
case WM_PAINT:

...{
PAINTSTRUCT ps;
BeginPaint (hwnd, &ps);
//决定我们在每一边画多少条线。
const int NumLinesPerSide = 10;
//根据窗口的X与Y来计算窗口每一条线的间隔
int yStep = cyClient/NumLinesPerSide;
int xStep = cxClient/NumLinesPerSide;
//现在,开画!
for (int mult=1; mult<NumLinesPerSide; ++mult)

...{
//移到线开始的位置
MoveToEx(ps.hdc, xStep*mult, 0, 0);
//画线
LineTo(ps.hdc, 0, cyClient-yStep*mult);
//移到线开始的位置
MoveToEx(ps.hdc, xStep*mult, cyClient, 0);
//画线
LineTo(ps.hdc, cxClient, cyClient-yStep*mult);
//移到线开始的位置
MoveToEx(ps.hdc, xStep*mult, 0, 0);
//画线
LineTo(ps.hdc, cxClient, yStep*mult);
//移到线开始的位置
MoveToEx(ps.hdc, xStep*mult, cyClient, 0);
//画线
LineTo(ps.hdc, 0, yStep*mult);
}
EndPaint (hwnd, &ps);
}
break;
//用户是否改变了窗口的尺寸?
case WM_SIZE:

...{
//如果改变了窗口的尺寸,我们必须更新我们的变量以便重绘
//我们更改cxClient和cyClient,以便缩放我们的线条
cxClient = LOWORD(lParam);
cyClient = HIWORD(lParam);
}
break;
case WM_DESTROY:

...{
//销毁应用程序,这需要一个WM_QUIT消息
PostQuitMessage (0);
}
break;
}//switch结尾
//那些我们没处理的消息
//使用Windows的默认处理函数去处理它们
return DefWindowProc (hwnd, msg, wParam, lParam);
}
//-------------------------------- WinMain -------------------------------
//
// Windows程序的入口
//------------------------------------------------------------------------
int WINAPI WinMain (HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR szCmdLine,
int iCmdShow)

...{
//我们窗口的句柄
HWND hWnd;
//我们的窗口类结构
WNDCLASSEX winclass;
//第一次填充窗口类结构
winclass.cbSize = sizeof(WNDCLASSEX);
winclass.style = CS_HREDRAW | CS_VREDRAW;
winclass.lpfnWndProc = WindowProc;
winclass.cbClsExtra = 0;
winclass.cbWndExtra = 0;
winclass.hInstance = hInstance;
winclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
winclass.hCursor = LoadCursor(NULL, IDC_ARROW);
winclass.hbrBackground = (HBRUSH)GetStockObject (WHITE_BRUSH);
winclass.lpszMenuName = NULL;
winclass.lpszClassName = g_szWindowClassName;
winclass.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
//注册窗口类
if (!RegisterClassEx(&winclass))

...{
MessageBox(NULL, "注册失败!", "错误", 0);
//退出应用程序
return 0;
}
//创建窗口并将ID赋值给hwnd
hWnd = CreateWindowEx (NULL, //扩长样式(extended style)
g_szWindowClassName, //窗口类名(window class name)
g_szApplicationName, //窗口标题(window caption)
WS_OVERLAPPEDWINDOW, //窗口样式(window style)
0, //初始x坐标(initial x position)
0, //初始y坐标(initial y position)
WINDOW_WIDTH, //初始x尺寸(initial x size)
WINDOW_HEIGHT, //初始y尺寸(initial y size)
NULL, //初窗口句柄(parent window handle)
NULL, //窗口菜单句柄(window menu handle)
hInstance, //程序对象句柄(program instance handle)
NULL); //创造参数(creation parameters)
//确定窗口创建成功
if(!hWnd)

...{
MessageBox(NULL, "创建窗口失败!", "错误!", 0);
}
//让窗口可见
ShowWindow (hWnd, iCmdShow);
UpdateWindow (hWnd);
//这个将用来保存所有的窗口消息
MSG msg;
//我们的消息处理器的入口
while (GetMessage (&msg, NULL, 0, 0))

...{
TranslateMessage (&msg);
DispatchMessage (&msg);
}
UnregisterClass( g_szWindowClassName, winclass.hInstance );
return msg.wParam;
}
为了创建这些美妙的线条,只需要对第一章中你看到的窗口过程做一些修改即可。下面是修改过后的WindowProc的片断:
下面进行绘图。先请读者浏览一下WM_PAINT处理段,接着将逐个讲解相关的部分。
case WM_PAINT:

...{
PAINTSTRUCT ps;
BeginPaint (hwnd, &ps);
//决定我们在每一边画多少条线。
const int NumLinesPerSide = 10;
//根据窗口的X与Y来计算窗口每一条线的间隔
int yStep = cyClient/NumLinesPerSide;
int xStep = cxClient/NumLinesPerSide;
//现在,开画!
for (int mult=1; mult<NumLinesPerSide; ++mult)

...{
//移到线开始的位置
MoveToEx(ps.hdc, xStep*mult, 0, 0);
//画线
LineTo(ps.hdc, 0, cyClient-yStep*mult);
//移到线开始的位置
MoveToEx(ps.hdc, xStep*mult, cyClient, 0);
//画线
LineTo(ps.hdc, cxClient, cyClient-yStep*mult);
//移到线开始的位置
MoveToEx(ps.hdc, xStep*mult, 0, 0);
//画线
LineTo(ps.hdc, cxClient, yStep*mult);
//移到线开始的位置
MoveToEx(ps.hdc, xStep*mult, cyClient, 0);
//画线
LineTo(ps.hdc, 0, yStep*mult);
}
EndPaint (hwnd, &ps);
}
break;
可以看到,绘图代码还是相当易懂的。注意这里绘画所用到的点都是根据cxClient和cyClient计算出来的。这是适应窗口缩放所必须的。有关这一点,后面马上就会具体谈到。现在,解释一下MoveToEx函数和LineTo函数。
如果有一条从点A到点B的线段,MoveToEx可以用来先将画笔所在位置移动到点A,然后再利用LineTo以当前画笔绘制一条线段到B点。MoveToEx的函数原型如下 :
一旦把画笔定位于线段的起点后,就可用LineTo来绘制线段:
技巧:有几种方法可以用来禁止用户改变窗口的大不,但最简单的方法就是在调用CreateWindowEx的时候不使用WS_THICKFRAME标志。如果在文档里查找WS_OVERLAPPEDWINDOW,会发现使用它能节省一些时间,它是一些标志的组合,其中就WS_THICKFRAME。因此,要达到和前面同样的结果,但除了WS_THICKFRAME,应使用如下这些标志:
WS_OVERLAPPED|WS_VISIBLE|WS_CAPTION|WS_SYSMENU
试一试这个方法,看这样做以后在试图改变窗口大小的时候会发生什么。
2.1.2.1 创建自定义画笔
前面的例子用默认的黑色来画线,但如果希望用其他颜色和不同的线宽绘图应该怎么做?为了达到这些目的,必须用CreatePen函数创建你自定义的画笔。下面看一下这个函数: