设备环境
设备环境是Windows内部使用的数据结构,它定义了GDI函数在显示设备特定区域的工作方式。对视频显示器来说,设备环境代表屏幕上的一块区域。要想向某个区域输出文字或绘制图形,必须先取得代表此区域的设备环境句柄。以此句柄为参数调用的GDI函数都是对该区域的操作。例如下面代码将在窗口客户区绘制文本。
case WM_PAINT: // 窗口客户区需要重画
{
char szText[] = "大家好";
HDC hdc;
PAINTSTRUCT ps;
// 使无效的客户区变的有效,并取得设备环境句柄
hdc = ::BeginPaint(hwnd, &ps);
// 显示文字
::TextOut(hdc, 10, 10, szText, strlen(szText));
::EndPaint(hwnd, &ps);
return 0;
}
BeginPaint函数取得窗口客户区无效区域的设备环境句柄,所以在上面的代码中,TextOut函数只能在窗口的客户区输出文本。输出结果如图4.7所示。
如果取得的设备环境句柄对应着整个窗口,而不仅仅是窗口的客户区,就可以在窗口的非客户区绘制文本。请看下面的代码。
case WM_LBUTTONDOWN:
{
char szText[] = "大家好";
// 取得整个窗口的设备环境句柄
HDC hdc = ::GetWindowDC(hwnd);
::TextOut(hdc, 10, 10, szText, strlen(szText));
// 释放设备环境句柄
::ReleaseDC(hwnd, hdc);
return 0;
}
当在窗口的客户区单击鼠标的左键时,Windows会向该窗口的消息处理函数发送WM_LBUTTONDOWN消息,上面就是响应此消息的代码。
GetWindowDC函数能够取得整个窗口的设备环境句柄,而不仅仅是窗口的客户区,所以以这个设备环境句柄为参数的话,GDI函数就可以对整个窗口区域进行操作,图4.8显示了运行上述代码并在窗口客户区单击鼠标左键后程序的输出。
如果不是在处理WM_PAINT消息,可以使用GetDC 函数取得窗口客户区的设备环境句柄,进而进行绘制操作,如下代码所示。
hDC = ::GetDC(hWnd);
//...... // 进行绘制
::ReleaseDC(hWnd, hDC);
这些调用与BeginPaint和 EndPaint 的组合之间的基本区别是,利用从GetDC传回的句柄可以在整个客户区上,不只是在客户区的无效区域绘图。当然,GetDC和 ReleaseDC不使客户区中任何可能的无效区域变成有效。例如,在下面的例子中,程序首先取得记事本程序的窗口句柄,然后通过GetDC函数取得其客户区设备环境句柄,在上面绘制文本。
UseDC.cpp
// 04UseDC 工程下。当然,也可以是一个控制台程序
// UseDC.cpp文件
#include <windows.h>
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
HDC hdc;
HWND hWnd;
char sz[] = "大家好";
// 查找记事本程序的窗口句柄
hWnd = ::FindWindow("Notepad", NULL);
// 如果记事本程序在运行,就向其客户区绘制文本
while (::IsWindow(hWnd)) // IsWindow函数用于判断一个窗口句柄是否有效
{
hdc = ::GetDC(hWnd);
::TextOut(hdc, 10, 10, sz, strlen(sz));
::ReleaseDC(hWnd, NULL);
::Sleep(1000);
}
::MessageBox(NULL, "记事本程序已经退出", "04UseDC", MB_OK);
return 0;
}
打开记事本程序,运行上面的代码,程序就会在记事本的客户区绘制文本,如图4.9所示。
设备环境结构里除了包含它所代表区域的位置和大小信息外,还包含了绘制图形需要的所有其他属性信息,比如,在输出文本时使用的字体、画图时使用的画笔、删除背景时使用的刷子、选用的坐标系统等。Windows并不允许直接存取设备环境结构中成员的值,而是提供了一些API函数来改变里面的默认值。比如可以用SetTextColor 函数改变DC结构中的文本颜色,用SetBkColor 函数改变DC结构中的文本背景颜色等。例如,下面的代码在处理左键单击事件的时候,将输出文本的颜色设为红色,背景色设为蓝色。
HDC hdc;
char sz[] = "大家好";
switch(message)
{
case WM_LBUTTONDOWN:
hdc = ::GetDC(hWnd);
// 设置DC 结构中的文本颜色为红色(下一小节我们再介绍Windows 下的颜色)
::SetTextColor(hdc, RGB(255, 0, 0));
// 设置DC 结构中的文本背景颜色为蓝色
::SetBkColor(hdc, RGB(0, 0, 255));
::TextOut(hdc, 10, 10, sz, strlen(sz));
::ReleaseDC(hWnd, hdc);
break;
case WM_PAINT:
PAINTSTRUCT ps;
hdc = ::BeginPaint(hWnd, &ps);
::TextOut(hdc, 10, 10, sz, strlen(sz));
::EndPaint(hWnd, &ps);
break;
//...... // 处理其它的消息
}
在处理两个消息的时候,在窗口客户区同样的位置输出了同样的字符串。运行程序后,单击鼠标左键前后输出效果的变化如图4.10所示。
响应WM_LBUTTONDOWN消息的时候,通过一些API函数改变了DC结构中成员的值。但是,紧接着如果使另外一个窗口覆盖字串“大家好”,然后再使这个字符串显示,促使窗口接受到WM_PAINT消息,会发现字符串的文本颜色和背景色又都恢复了。这说明DC结构成员的设置并没有被保存下来,下一次使用GDI函数取得的设备环境仍然是Windows默认的。要想让 Windows将每次对DC的设置都保存下来,只要在注册窗口类时,向WNDCLASSEX结构的style成员添加一个CS_OWNDC标志就可以了,即:
wcex.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
Windows 的颜色和像素点
DC上的图形和文本都是由像素点组成的。内存中,用颜色的取值来表示象素点。色深指的是存储每个象素所用的位数。一般现在使用的都是24位色,即用24位表示一个像素,这样可以表示的颜色数目为22方种。每种颜色都可以分为红、绿、蓝三原色,所以可以用红、绿、蓝3分量的组合来表示一种颜色,每个分量占用8位就可以了。
在Win32编程中,统一使用32位的整数(一个COLORREF值)来表示深度为24位的颜色。在这32位中只使用低24位,每一种颜色分量占用8位,其中0~7位为红色,8~15位为绿色,16~23 位为蓝色,如图 4.11 所示。
可以使用 RGB 宏将 3 个分量的值组合在一起。这个宏的定义如下。
#define RGB(r,g,b) ((COLORREF)(((BYTE)(r)|((WORD)((BYTE)(g))<<8))|(((DWORD)(BYTE)(b))<<16)))
r、g、b三个参数分别表示红、绿、蓝的值,例如RGB(255,0,0)表示红色,RGB(0,0,0)表示白色,RGB(255,255,255)表示黑色等。宏GetRValue、GetGValue和 GetBValue可以从COLORREF值中抽出各分量的原色值,这给编程者设置或获取像素的值带来了许多方便。
要想设置一个象素点的值可以用SetPixel函数。
COLORREF SetPixel(
HDC hdc, // 设备环境句柄
int X, // 象素的X 坐标
int Y, // 象素的Y 坐标
COLORREF crColor // 要设置的COLORREF 值
);
SetPixel函数可以在hdc的(X,Y)位置以crColor为颜色画上一个像素点。如果需要获取DC中某个像素点当前的颜色值,可以使用GetPixel函数。
COLORREF GetPixel( HDC hdc, int nXPos, int nYPos);
绘制线条
绘制线条的函数有画单条直线的LineTo,画多条直线的Polyline和 PolylineTo,画贝塞儿曲线的PolyBezier和 PolyBezierTo,画弧线的Arc和ArcTo。
绘制直线仅仅需要指定线的开始坐标,然后以线的另一头的坐标为参数调用LineTo函数就即可。例如,下面的代码绘制了一条坐标从(O,0)到(0,500)的直线。
::MoveToEx(hDC, 0, 0, NULL);
::LineTo(hDC, 0, 500);
DC的数据结构中有一个“当前点”,LineTo函数就是从当前点画一条直线到参数中指定的点,并把这个点设置为新的当前点。画线函数中所有以To结尾的函数都是从当前点开始绘画的,如LineTo、PolylineTo、PolyBezierTo等。而其余的不带To的函数则与当前点没有关系,当然也不会影响当前点的位置。
如果要设置当前点的位置,可以使用MoveToEx函数。
BOOL MoveToEx(
HDC hdc, // 设备环境句柄
int X, // 新位置的X 坐标
int Y, // 新位置的Y 坐标
LPPOINT lpPoint // 用来返回原来的当前点位置,如果不需要的话,这个参数可以被设为NULL
);
上面绘制直线的代码配合使用了MoveToEx和 LineTo函数。首先由MoveToEx 函数设置一个当前点做为起始坐标,然后用LineTo 函数绘画到结束坐标。
下面的程序代码在窗口的客户区绘制了一个网格,各线的间距为50像素,如图4.12所示。
case WM_PAINT:
HDC hdc;
PAINTSTRUCT ps;
RECT rt;
int x, y;
hdc = ::BeginPaint(hWnd, &ps);
// 取得窗口客户区的大小
::GetClientRect(hWnd, &rt);
// 画列
for(x = 0; x < rt.right - rt.left; x += 50)
{
::MoveToEx(hdc, x, 0, NULL);
::LineTo(hdc, x, rt.bottom - rt.top);
}
// 画行
for(y = 0; y < rt.bottom - rt.top; y += 50)
{
::MoveToEx(hdc, 0, y, NULL);
::LineTo(hdc, rt.right - rt.left, y);
}
::EndPaint(hWnd, &ps);
break;
GetClientRect函数用于取得指定窗口的客户区的大小,其返回信息在一个RECT结构里。
如果要绘制的是相连的多条直线,可以使用Polyline和 PolylineTo函数。
BOOL Polyline(HDC hdc, CONST POINT *lpPoint, int cPoints);
BOOL PolylineTo(HDC hdc, CONST POINT *lpPoint, DWORD cPoints );
lpPoint 是指向POINT类型的数组的指针,cPoints参数指出了数组的大小。Polyline 函数只是把 lpPoint 指向的各点连在一起,而 PolylineTo 函数是从当前点开始画线,然后再连接lpPoint指向的各点。所以,如果传递相同的参数,PolylineTo函数画的线比 Polyline函数要多一条。
用Polyline 函数绘制解析函数的图形最方便了。下面是绘制正弦函数y = sin(x)的图形的例子。和用手工画图的步骤一样,先找出此函数上一系列点的坐标,再用“平滑”的曲线将这些坐标连起来。虽然Polyline函数只能用直线连接两个点,但是如果取大量的点的话,最后连成的图形看起来也是平滑的,运行效果如图4.13所示。
SineWave.cpp
//04SineWave 工程下
// SineWave.cpp文件
#include <windows.h>
#include <math.h>
LRESULT __stdcall WndProc(HWND, UINT, WPARAM, LPARAM);
int __stdcall WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
char szWindowClass[] = "SineWave";
// 注册窗口类
WNDCLASSEX wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
wcex.lpfnWndProc = (WNDPROC)WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = NULL;
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)::GetStockObject(WHITE_BRUSH);
wcex.lpszMenuName = NULL;
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = NULL;
::RegisterClassEx(&wcex);
// 创建并线程主窗口
HWND hWnd = ::CreateWindowEx(
WS_EX_CLIENTEDGE, // 扩展样式
szWindowClass, // 类名
"正弦线", // 标题
WS_OVERLAPPEDWINDOW, // 窗口样式
CW_USEDEFAULT, // 初始 X 坐标
CW_USEDEFAULT, // 初始 X 坐标
CW_USEDEFAULT, // 宽度
CW_USEDEFAULT, // 高度
NULL, // 父窗口句柄
NULL, // 菜单句柄
hInstance, // 程序实例句柄
NULL);
::ShowWindow(hWnd, nShowCmd);
::UpdateWindow(hWnd);
// 进入消息循环
MSG msg;
while (::GetMessage(&msg, NULL, 0, 0))
{
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
return 1;
}
// 消息处理函数 // 03SineWave工程下
LRESULT __stdcall WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
#define SEGMENTS 500 // 取的点数(在一个周期内取500个点)
#define PI 3.1415926 // 圆周率
HDC hdc;
PAINTSTRUCT ps;
RECT rt;
int cxClient, cyClient;
POINT pt[SEGMENTS];
int i;
switch (message)
{
case WM_PAINT:
hdc = ::BeginPaint(hWnd, &ps);
::GetClientRect(hWnd, &rt);
cxClient = rt.right - rt.left;
cyClient = rt.bottom - rt.top;
// 画横坐标轴
::MoveToEx(hdc, 0, cyClient / 2, NULL);
::LineTo(hdc, cxClient, cyClient / 2);
// 找出500个点的坐标
for (i = 0; i < SEGMENTS; i++)
{
pt[i].x = cxClient * i / SEGMENTS;
pt[i].y = (int)((cyClient / 2)*(1 - sin(2 * PI*i / SEGMENTS)));
}
// 将各点连在一起
::Polyline(hdc, pt, SEGMENTS);
::EndPaint(hWnd, &ps);
break;
case WM_DESTROY:
::PostQuitMessage(0);
break;
}
return ::DefWindowProc(hWnd, message, wParam, lParam);
}
一个周期内x轴上的取值是0到2n,上面的代码将2T平均分成了500份,所以最后得到了正弦线上500个点。比较麻烦的是y轴的坐标变换过程。必须要明白,在默认的坐标系中,y轴的正方向是向下的,而数学上y轴的正方向是向上的,所以要对计算结果取反。为了将坐标原点上移到窗口的左上角,又要加上cyClient/2,即。
pt[i].y = (int)((cyClient / 2)*(1 - sin(2 * PI*i / SEGMENTS)));
仔细研究上面的代码能够使大家更深刻地了解计算机绘图。
既然是绘画,就要用到画笔,所以在 DC结构中还有一个画笔对象句柄,每次绘画时,GDI函数都会去使用此句柄指示的画笔。画笔对象规定了线条的宽度、颜色和风格。
要想改变DC中默认的画笔对象,可以使用 Windows预定义的画笔对象,也可以创建新的画笔对象。预定义的画笔对象很简单,仅有BLACK_PEN、WHITE_PEN、NULL_PEN三种,分别是黑色画笔、白色画笔和空画笔。在代码中使用它们的一般格式如下。
// 获取预定义画笔的句柄。Stock 的中文含义是常备的、库存的
HPEN hPen = (HPEN)::GetStockObject(BLACK_PEN);
// 将画笔对象选入设备。SelectObject 函数会根据句柄的种类自动替换掉原来的对象,并返回原对象句柄
HPEN hOldPen = (HPEN)::SelectObject(hdc, hPen);
//...... // 开始在DC 中绘图
用GetStockObject得到对象的句柄以后,就可以使用SelectObject将对象选入到DC中了。预定义的画笔对象太“简陋”了,只能是白色或黑色的宽度为1个像素的实线。使用CreatePen函数可以创建自定义的画笔对象。
HPEN CreatePen(
int fnPenStyle, // 画笔的风格,取值有PS_SOLID、PS_DASH、PS_DOT、PS_DASHDOT 等
int nWidth, // 画笔的宽度,单位是DC 坐标映射方法中定义的逻辑单位
COLORREF crColor // 画笔的颜色
);
例如,下面的代码绘制了一条宽度为 3 个像素的红色线条。
HPEN hPen = ::CreatePen(PS_SOLID, 3, RGB(255, 0, 0));
HPEN hOldPen = (HPEN)::SelectObject(hdc, hPen);
::MoveToEx(hdc, 0, 100, NULL);
::LineTo(hdc, 500, 100);
::SelectObject(hdc, hOldPen);
::DeleteObject(hPen); // 一定要删除上面创建的画笔对象,以释放资源
绘制区域
绘制边线的时候要使用画笔,填充区域就要使用画刷了。绘制区域的函数工作的时候以当前画笔绘制边线,并以当前画刷填充中间的区域。这些函数有画矩形的 Rectangle,画椭圆的Ellipse,画多边形的Polygon,画弦的Chord等,其用法如下。
Rectangle(hdc, x1, y1, x2, y2); // 画以(x1, y1)和(x2, y2)为对角坐标的填充矩形
Ellipse(hdc, x1, y1, x2, y2); // 以(x1, y1)和(x2, y2)为对角坐标定义一个矩形,然后画矩形相切的椭
// 圆并填充
Polygon(hdc, lpPoint, 5) // lpPoint 指向存放(x0, y0)到(x4, y4)的内存,函数从(x1, y1)到(x2, y2)...
// 到(x4, y4),再回到(x1, y1),一共画5 条直线并填充
函数的用法相当简单,比如下面是响应WM_PAINT消息的函数,它在窗口客户区中心绘制了100×100像素的矩形,并用红色画刷填充。
void OnPaint(HWND hWnd) // 你要在接受到WM_PAINT 消息的时候调用此函数
{
RECT rt; ::GetClientRect(hWnd, &rt); // 注意,我为了节省空间才写在一起,你不要这样做
int xCenter = rt.right/2;
int yCenter = rt.bottom/2;
PAINTSTRUCT ps;
HDC hdc = ::BeginPaint(hWnd, &ps);
// 创建一个单色(红色)的刷子并选入设备
HBRUSH hBrush = ::CreateSolidBrush(RGB(255, 0, 0));
HBRUSH hOldBrush = (HBRUSH)::SelectObject(hdc, hBrush);
// 以 xCenter、yCenter 为中心,画一个边长为100 的正方形
::Rectangle(hdc, xCenter - 50, yCenter + 50, xCenter + 50, yCenter -50);
::SelectObject(hdc, hOldBrush);
::DeleteObject(hBrush);
::EndPaint(hWnd, &ps);
}
如果对这个矩形的边框不满意,也可以自己创建画笔对象,再选入DC中。上面的代码使用CreateSolidBrush函数创建了新的单色的画刷对象,以取代 DC中默认的画刷对象。
CreateSolidBrush函数要输入的惟一的参数是画刷的颜色,而使用CreateHatchBrush 函数创建的画刷还可以带有特定风格的线条。
HBRUSH CreateHatchBrush(
int fnStyle, // 线条的风格
COLORREF clrref // 图案线条的颜色
);
fnStyle参数指定了不同风格的线条,这些图案线条实际上是由8×8的位图重复铺开组成的。此参数取值可以是HS_BDIAGONAL、HS_FDIAGONAL、HS_CROSS、HS_HORIZONTAL、HS_DIAGCROSS或HS_VERTICAL,其对应的图案如图4.14所示。
当然,Windows也预定义了一些画刷对象,可以被 GetStockObject函数获取,并被选入DC中。这些常用的预定义对象有BLACK_BRUSH(黑色画刷)、DKGRAY_BRUSH(深灰色画刷)、GRAY_BRUSH(灰色画刷)、LTGRAY_BRUSH(浅灰色画刷)、WHITE_BRUSH(白色画刷)、NULL_BRUSH(空画刷)等。
坐标系统
坐标映射方式是设备环境中一个很重要的属性。它的默认值是MM_TEXT,即以左上角为坐标原点,以右方当做x坐标的正方向,以下方当做y坐标的正方向。这种坐标系使用的单位是像素,其好处是窗口中每一点的坐标不会因为窗口的大小而改变。前面的例子使用的都是默认坐标系。
可以用SetMapMode函数改变DC中的坐标映射方式。
int SetMapMode(
HDC hdc, // 设备环境句柄
int fnMapMode // 新的坐标映射方式
);
Windows一共支持8种不同的坐标映射方式,表4.1中对它们各自的属性做了总结。最后两种映射方式提供了更灵活的选择。这个时候可以自己设置逻辑单位,坐标系的原点和坐标的正方向等参数。
必须经过一系列函数的调用才能确定坐标系。首先调用SetWindowExtEx函数设置坐标系的逻辑单位。
BOOL SetWindowExtEx(
HDC hdc, // 设备环境句柄
int nXExtent, // 新的宽度(映射方式为MM_ISOTROPIC 时nXExtent 必须等于nYExtent)
int nYExtent, // 新的高度
LPSIZE lpSize // 用于返回原来的大小,不需要的话可以设它为NULL
);
不管真实的区域大小是多少,仅仅使用这个函数告诉 Windows此区域的逻辑宽度为nXExtent,逻辑高度为nYExtent。Windows会将DC所代表区域的宽度做nXExtent等分,每份的长度就是x轴方向的一个单位长度,将高度做nYExtent 等分就得到了y轴的单位长度。
设置了逻辑单位以后,还要调用SetViewportExtEx函数设置x、y坐标轴的方向和坐标系包含的范围,即定义域和值域。
BOOL SetViewportExtEx(
HDC hdc, // 设备环境句柄
int nXExtent, // 新的宽度(以像素为单位,定义域)
int nYExtent, // 新的高度(以像素为单位,值域)
LPSIZE lpSize // 用于返回原来的大小,不需要的话可以设它为NULL
);
要想坐标系包含整个区域,直接将区域的真实的大小传给此函数就可以了。参数 nXExtent和nYExtent为正则表示x、y轴的正方向和默认的坐标系相同,即向右和向下分别为x、y轴的正方向,为负则表示与默认方向相反。
最后别忘了设置坐标系的原点坐标。
BOOL SetViewportOrgEx(
HDC hdc, // 设备环境句柄
int X, // 原点的横坐标
int Y, // 原点的纵坐标
LPPOINT lpPoint // 用于返回原来的坐标,不需要的话可以设它为NULL
);
当绘制的图形需要随着窗口的大小改变而自动改变的时候,使用MM_ISOTROPIC和MM_ANISOTROPIC坐标映射方式最合适了。下面的代码使用MM_ANISOTROPIC坐标映射方式画了一个与窗口的各边都相切的椭圆。
void OnPaint(HWND hWnd) // 你要在接受到WM_PAINT 消息的时候调用此函数
{
PAINTSTRUCT ps;
HDC hdc = ::BeginPaint(hWnd, &ps);
// 取得客户区的大小
RECT rt; ::GetClientRect(hWnd, &rt); // 注意,我为了节省空间才写在一起,你不要这样做
int cx = rt.right;
int cy = rt.bottom;
// 设置客户区的逻辑大小为500 乘500,原点为(0,0)
::SetMapMode(hdc, MM_ANISOTROPIC);
::SetWindowExtEx(hdc, 500, 500, NULL);
::SetViewportExtEx(hdc, cx, cy, NULL);
::SetViewportOrgEx(hdc, 0, 0, NULL);
// 以整个客户区为边界画一个椭圆
::Ellipse(hdc, 0, 0, 500, 500);
::EndPaint(hWnd, &ps);
}
看看它是怎么工作的?不管真实窗口的大小是多少,程序已经告诉了Windows 该窗口的逻辑大小是500×500,因此,程序运行后一个椭圆总是会环绕在整个窗口中。
MM_ISOTROPIC和MM_ANISOTROPIC映射方式的唯一区别就是,在前面的方式里,X轴和¥轴的逻辑单位的大小是相同的。Isotropic就是“各方向相等”的意思,此映射方式对于绘制圆或正方形来说非常合适。
下面的自定义函数SetIsotropic将坐标系设置成了数学上常用的笛卡儿坐标系,其原点在区域的中心,向右、向上分别为x轴和y轴的正方向。
void SetIsotropic(HDC hdc, int cx, int cy)
{
::SetMapMode(hdc, MM_ISOTROPIC);
::SetWindowExtEx(hdc, 1000, 1000, NULL);
::SetViewportExtEx(hdc, cx, -cy, NULL); // 第 3 个参数为负值,说明了Y 轴的正方向与默认方向相反
::SetViewportOrgEx(hdc, cx/2, cy/2, NULL);
}
使用的时候,应该将参数cx、cy 的值设置为 hdc所代表区域的真实大小。在这个坐标系中进行坐标变换,绘制数学图形就很简单了。例如,本章的实例Clock 里面有一个画时钟外观的函数 DrawClockFace,它将在窗口客户区里画12个黑圆圈。这个函数的实现就是在上面的坐标系下完成的。
// 绘制时钟的外观
void DrawClockFace(HDC hdc)
{
const int SQUARESIZE = 20;
static POINT pt[] =
{
0, 450, // 12 点
225, 390, // 1 点
390, 225, // 2 点
450, 0, // 3 点
390, -225, //... 下面的坐标是上面的点的对称点(关于X 轴、Y 轴或原点对称)
225, -390,
0, -450,
-225, -390,
-390, -225,
-450, 0,
-390, 225,
-225, 390
};
// 选择一个黑色的画刷
::SelectObject(hdc, ::GetStockObject(BLACK_BRUSH));
// 画 12 个圆
for(int i=0; i<12; i++)
{
::Ellipse(hdc, pt[i].x - SQUARESIZE, pt[i].y + SQUARESIZE,
pt[i].x + SQUARESIZE, pt[i].y - SQUARESIZE);
}
}
在处理WM_PAINT消息时使用下面的代码就可以看到程序的运行效果,如图4.15所示。
hdc = ::BeginPaint(hWnd, &ps); // 定义变量的代码我已经省略
::GetClientRect(hWnd, &rt);
SetIsotropic(hdc, rt.right - rt.left, rt.bottom - rt.top);
DrawClockFace(hdc);
::EndPaint(hWnd, &ps);
DrawClockFace函数中最关键的部分是得到各刻度点的中心坐标。在代码中直接给出的pt数组的值很容易通过计算取得。函数在绘制图形时使用的坐标系如图4.16所示。