Windows包含了几种使用RECT(矩形)结构和「区域」的绘图函数。区域就是屏幕上的一块地方,它是矩形、多边形和椭圆的组合。
矩形函数
下面三个绘图函数需要一个指向矩形结构的指针:
FillRect (hdc, &rect, hBrush) ; FrameRect (hdc, &rect, hBrush) ; InvertRect (hdc, &rect) ;
在这些函数中,rect参数是一个RECT型态的结构,它包含有4个字段:left、top、right和bottom。这个结构中的坐标被当作逻辑坐标。
FillRect用指定画刷来填入矩形(直到但不包含right和bottom坐标),该函数不需要先将画刷选进设备内容。
FrameRect使用画刷画矩形框,但是不填入矩形。使用画刷画矩形看起来有点奇怪,因为对于我们所介绍过的函数(如Rectangle),其边线都是用目前画笔绘制的。FrameRect允许使用者画一个不一定为纯色的矩形框。该边界框为一个逻辑单位元宽。如果逻辑单位大于设备单位,则边界框将会为2个图素宽或者更宽。
InvertRect将矩形中所有图素翻转,1转换成0,0转换为1,该函数将白色区域转变成黑色,黑色区域转变为白色,绿色区域转变成洋红色。
Windows还提供了9个函数,使您可以更容易、更清楚地操作RECT结构。例如,要将RECT结构的四个字段设定为特定值,通常使用如下的程序段:
rect.left = xLeft ; rect.top = xTop ; rect.right = xRight ; rect.bottom = xBottom ;
但是,通过呼叫SetRect函数,只需要一道叙述就可以得到同样的结果:
SetRect (&rect, xLeft, yTop, xRight, yBottom) ;
在您想要做以下事情之一时,可以很方便地选用其它8个函数:
- 将矩形沿x轴和y轴移动几个单元:
OffsetRect (&rect, x, y) ;
- 增减矩形的尺寸:
InflateRect (&rect, x, y) ;
- 矩形各字段设定为0:
SetRectEmpty (&rect) ;
- 将矩形复制给另一个矩形:
CopyRect (&DestRect, &SrcRect) ;
- 取得两个矩形的交集:
IntersectRect (&DestRect, &SrcRect1, &SrcRect2) ;
- 取得两个矩形的联集:
UnionRect (&DestRect, &SrcRect1, &SrcRect2) ;
- 确定矩形是否为空:
bEmpty = IsRectEmpty (&rect) ;
- 确定点是否在矩形内:
bInRect = PtInRect (&rect, point) ;
大多数情况下,与这些函数相同作用的程序代码很简单。例如,您可以用下列叙述来替代CopyRect函数呼叫:
DestRect = SrcRect ;
随机矩形
在图形系统中,有这么一个「永远」有人执行的有趣程序,它简单地使用随机的大小和色彩绘制一系列矩形。您可以在Windows中建立一个这样的程序,但是它并不像乍看起来那样容易编写。我希望您能认识到,您不能简单地在WM_PAINT消息中使用一个while(TRUE)循环。当然,它能够执行,但是程序将停止对其他消息的处理,同时,这个程序不能中止或者最小化。
一种可以接受的方法是设定一个Windows定时器,给窗口程序发送WM_TIMER消息(我将在 第八章中讨论定时器)。对于每条WM_TIMER消息,您使用GetDC取得一个设备内容,画一个随机的矩形,然后用ReleaseDC释放设备内容。但是这样又降低了程序的趣昧性,因为程序不能尽可能快地画随机矩形,它必须等待WM_TIMER消息,而这又依赖于系统时钟的分辨率。
在Windows中一定有很多「闲置时间」,在这个时间内,所有消息队列为空,Windows只停在一个小循环中等待键盘或者鼠标输入。我们能否在闲置时间内获得控制,绘制矩形,并且只在有消息加入程序的消息队列之后才释放控制呢?这就是PeekMessage函数的目的之一。下面是PeekMessage呼叫的一个例子:
PeekMessage (&msg, NULL, 0, 0, PM_REMOVE) ;
前面的四个参数(一个指向MSG结构的指针、一个窗口句柄、两个值指示消息范围)与GetMessage的参数相同。将第二、三、四个参数设定为NULL或0时,表明我们想让PeekMessage传回程序中所有窗口的所有消息。如果要将消息从消息队列中删除,则将PeekMessage的最后一个参数设定为PM_REMOVE。如果您不希望删除消息,那么您可以将这个参数设定为PM_NOREMOVE。这就是为什么Peek_Message是「偷看」而不是「取得」的原因,它使得程序可以检查程序的队列中的下一个消息,而不实际删除它。
GetMessage不将控制传回给程序,直到从程序的消息队列中取得消息,但是PeekMessage总是立刻传回,而不论一个消息是否出现。当消息队列中有一个消息时,PeekMessage的传回值为TRUE(非0),并且将按通常方式处理消息。当队列中没有消息时,PeekMessage传回FALSE(0)。
这使得我们可以改写普通的消息循环。我们可以将如下所示的循环:
while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ;
替换为下面的循环:
while (TRUE) { if (PeekMessage (&msg, NULL, 0, 0, PM_REMOVE)) { if (msg.message == WM_QUIT) break ; TranslateMessage (&msg) ; DispatchMessage (&msg) ; } else { // 完成某些工作的其它行程序 } } return msg.wParam ;
注意,WM_QUIT消息被另外挑出来检查。在普通的消息循环中您不必这么作,因为如果GetMessage接收到一个WM_QUIT消息,它将传回0,但是PeekMessage用它的传回值来指示是否得到一个消息,所以需要对WM_QUIT进行检查。
如果PeekMessage的传回值为TRUE,则消息按通常方式进行处理。如果传回值为FALSE,则在将控制传回给Windows之前,还可以作一点工作(如显示另一个随机矩形)。
(尽管Windows文件上说,您不能用PeekMessage从消息队列中删除WM_PAINT消息,但是这并不是什么大不了的问题。毕竟,GetMessage并不从消息队列中删除WM_PAINT消息。从队列中删除WM_PAINT消息的唯一方法是令窗口显示区域的失效区域变得有效,这可以用ValidateRect和ValidateRgn或者BeginPaint和EndPaint对来完成。如果您在使用PeekMessage从队列中取出WM_PAINT消息后,同平常一样处理它,那么就不会有问题了。所不能作的是使用如下所示的程序代码来清除消息队列中的所有消息:
while (PeekMessage (&msg, NULL, 0, 0, PM_REMOVE)) ;
这行叙述从消息队列中删除WM_PAINT之外的所有消息。如果队列中有一个WM_PAINT消息,程序就会永远地陷在while循环中。)
PeekMessage在Windows的早期版本中比在Windows 98中要重要得多。这是因为Windows的16位版本使用的是非优先权式的多任务(我将在第二十章中讨论这一点)。Windows的Terminal程序在从通讯端口接收输入后,使用一个PeekMessage循环。打印管理器程序使用这个技术来进行打印,其它的Windows打印应用程序通常都会使用一个PeekMessage循环。在Windows 98优先权式的多任务环境下,程序可以建立多个线程,我们将第二十章看到这一点。
不管怎样,有了PeekMessage函数,我们就可以编写一个不停地显示随机矩形的程序。这个RANDRECT如程序5-7中所示。
RANDRECT.C /*---------------------------------------------------------------------- RANDRECT.C -- Displays Random Rectangles (c) Charles Petzold, 1998 -----------------------------------------------------------------------*/ #include <windows.h> #include <stdlib.h>// for the rand function LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; void DrawRectangle (HWND) ; int cxClient, cyClient ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("RandRect") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc= WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground= (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName= NULL ; wndclass.lpszClassName= szAppName ; if (!RegisterClass (&wndclass)) { MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("Random Rectangles"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (TRUE) { if (PeekMessage (&msg, NULL, 0, 0, PM_REMOVE)) { if (msg.message == WM_QUIT) break ; TranslateMessage (&msg) ; DispatchMessage (&msg) ; } else DrawRectangle (hwnd) ; } return msg.wParam ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam) { switch (iMsg) { case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, iMsg, wParam, lParam) ; } void DrawRectangle (HWND hwnd) { HBRUSHhBrush ; HDC hdc ; RECT rect ; if (cxClient == 0 || cyClient == 0) return ; SetRect (&rect, rand () % cxClient, rand () % cyClient, rand () % cxClient, rand () % cyClient) ; hBrush = CreateSolidBrush ( RGB (rand () % 256, rand () % 256, rand () % 256)) ; hdc = GetDC (hwnd) ; FillRect (hdc, &rect, hBrush) ; ReleaseDC (hwnd, hdc) ; DeleteObject (hBrush) ; }
这个程序在现在的计算机上执行得非常快,看起来都不像是一系列随机矩形了。程序使用我在上面讨论过的SetRect和FillRect函数,根据由C的rand函数得到的随机数决定矩形坐标和实心画刷的色彩。我将在第二十章中提供这个程序的多线程版本。
建立和绘制剪裁区域
剪裁区域是对显示器上一个范围的描述,这个范围是矩形、多边形和椭圆的组合。剪裁区域可以用于绘制和剪裁,通过将剪裁区域选进设备内容,就可以用剪裁区域来进行剪裁(就是说,将可以绘图的范围限制为显示区域的一部分)。与画笔、画刷和位图一样,剪裁区域是GDI对象,您应该呼叫DeleteObject来删除您所建立的剪裁区域。
当您建立一个剪裁区域时,Windows传回一个该剪裁区域的句柄,型态为HRGN。最简单的剪裁区域是矩形,有两种建立矩形的方法:
hRgn = CreateRectRgn (xLeft, yTop, xRight, yBottom) ;
或者
hRgn = CreateRectRgnIndirect (&rect) ;
您也可以建立椭圆剪裁区域:
hRgn = CreateEllipticRgn (xLeft, yTop, xRight, yBottom) ;
或者
hRgn = CreateEllipticRgnIndirect (&rect) ;
CreateRoundRectRgn建立圆角的矩形剪裁区域。
建立多边形剪裁区域的函数类似于Polygon函数:
hRgn = CreatePolygonRgn (&point, iCount, iPolyFillMode) ;
point参数是一个POINT型态的结构数组,iCount是点的数目,iPolyFillMode是ALTERNATE或者WINDING。您还可以用CreatePolyPolygonRgn来建立多个多边形剪裁区域。
那么,您会问,剪裁区域究竟有什么特别之处?下面这个函数才真正显示出了剪裁区域的作用:
iRgnType = CombineRgn (hDestRgn, hSrcRgn1, hSrcRgn2, iCombine) ;
这一函数将两个剪裁区域(hSrcRgn1和hSrcRgn2)组合起来并用句柄hDestRgn指向组合成的剪裁区域。这三个剪裁区域句柄都必须是有效的,但是hDestRgn原来所指向的剪裁区域被破坏掉了(当您使用这个函数时,您可能要让hDestRgn在初始时指向一个小的矩形剪裁区域)。
iCombine参数说明hSrcRgn1和hSrcRgn2如何组合,见表5-9。
表5-9 |
iCombine值 | 新剪裁区域 |
RGN_AND | 两个剪裁区域的公共部分 |
RGN_OR | 两个剪裁区域的全部 |
RGN_XOR | 两个剪裁区域的全部除去公共部分 |
RGN_DIFF | hSrcRgn1不在hSrcRgn2中的部分 |
RGN_COPY | hSrcRgn1的全部(忽略hSrcRgn2) |
从CombineRgn传回的iRgnType值是下列之一:NULLREGION,表示得到一个空剪裁区域;SIMPLEREGION,表示得到一个简单的矩形、椭圆或者多边形;COMPLEXREGION,表示多个矩形、椭圆或多边形的组合;ERROR,表示出错了。
剪裁区域的句柄可以用于四个绘图函数:
FillRgn (hdc, hRgn, hBrush) ; FrameRgn (hdc, hRgn, hBrush, xFrame, yFrame) ; InvertRgn (hdc, hRgn) ; PaintRgn (hdc, hRgn) ;
FillRgn、FrameRgn和InvertRgn类似于FillRect、FrameRect和InvertRect。FrameRgn的xFrame和yFrame参数是画在区域周围的边框的宽度和高度。PaintRgn函数用设备内容中目前画刷填入所指定的区域。所有这些函数都假定区域是用逻辑坐标定义的。
在您用完一个区域后,可以像删除其它GDI对象那样删除它:
DeleteObject (hRgn) ;
矩形与区域的剪裁
区域也在剪裁中扮演了一个角色。InvalidateRect函数使显示的一个矩形区域失效,并产生一个WM_PAINT消息。例如,您可以使用InvalidateRect函数来清除显示区域并产生一个WM_PAINT消息:
InvalidateRect (hwnd, NULL, TRUE) ;
您可以通过呼叫GetUpdateRect来取得失效矩形的坐标,并且可以使用ValidateRect函数使显示区域的矩形有效。当您接收到一个WM_PAINT消息时,无效矩形的坐标可以从PAINTSTRUCT结构中得到,该结构是用BeginPaint函数填入的。这个无效矩形还定义了一个「剪裁区域」,您不能在剪裁区域外绘图。
Windows有两个作用于剪裁区域而不是矩形的函数,它们类似于InvalidateRect和ValidateRect:
InvalidateRgn (hwnd, hRgn, bErase) ;
和
ValidateRgn (hwnd, hRgn) ;
当您接收到一个由无效区域引起的WM_PAINT消息时,剪裁区域不一定是矩形。
您可以使用以下两个函数之一:
SelectObject (hdc, hRgn) ;
或
SelectClipRgn (hdc, hRgn) ;
通过将一个剪裁区域选进设备内容来建立自己的剪裁区域,这个剪裁区域使用设备坐标。
GDI为剪裁区域建立一份副本,所以在将它选进设备内容之后,使用者可以删除它。Windows还提供了几个对剪裁区域进行操作的函数,如ExcludeClipRect用于将一个矩形从剪裁区域里排除掉,IntersectClipRect用于建立一个新的剪裁区域,它是前一个剪裁区域与一个矩形的交,OffsetClipRgn用于将剪裁区域移动到显示区域的另一部分。
CLOVER程序
CLOVER程序用四个椭圆组成一个剪裁区域,将这个剪裁区域选进设备内容中,然后画出从窗口显示区域的中心出发的一系列直线,这些直线只出现在剪裁区域所限定的范围,结果显示如图5-20所示。
图5-20 CLOVER利用复杂的剪裁区域画出的图像 |
要用常规的方法画出这个图形,就必须根据椭圆的边线公式计算出每条直线的端点。利用复杂的剪裁区域,可以直接画出这些线条,而让Windows确定其端点。CLOVER如程序5-8所示。
程序5-8 CLOVER CLOVER.C /*-------------------------------------------------------------------------- CLOVER.C -- Clover Drawing Program Using Regions (c) Charles Petzold, 1998 ----------------------------------------------------------------------*/ #include <windows.h> #include <math.h> #define TWO_PI (2.0 * 3.14159) LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("Clover") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc= WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground= (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName= szAppName ; if (!RegisterClass (&wndclass)) { MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("Draw a Clover"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam) { static HRGN hRgnClip ; static int cxClient, cyClient ; double fAngle, fRadius ; HCURSORhCursor ; HDC hdc ; HRGN hRgnTemp[6] ; int i ; PAINTSTRUCT ps ; switch (iMsg) { case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; hCursor= SetCursor (LoadCursor (NULL, IDC_WAIT)) ; ShowCursor (TRUE) ; if (hRgnClip) DeleteObject (hRgnClip) ; hRgnTemp[0] = CreateEllipticRgn (0, cyClient / 3, cxClient / 2, 2 * cyClient / 3) ; hRgnTemp[1] = CreateEllipticRgn (cxClient / 2, cyClient / 3, cxClient, 2 * cyClient / 3) ; hRgnTemp[2] = CreateEllipticRgn (cxClient / 3, 0, 2 * cxClient / 3, cyClient / 2) ; hRgnTemp[3] = CreateEllipticRgn (cxClient / 3, cyClient / 2, 2 * cxClient / 3, cyClient) ; hRgnTemp[4] = CreateRectRgn (0, 0, 1, 1) ; hRgnTemp[5] = CreateRectRgn (0, 0, 1, 1) ; hRgnClip = CreateRectRgn (0, 0, 1, 1) ; CombineRgn (hRgnTemp[4], hRgnTemp[0], hRgnTemp[1], RGN_OR) ; CombineRgn (hRgnTemp[5], hRgnTemp[2], hRgnTemp[3], RGN_OR) ; CombineRgn (hRgnClip, hRgnTemp[4], hRgnTemp[5], RGN_XOR) ; for (i = 0 ; i < 6 ; i++) DeleteObject (hRgnTemp[i]) ; SetCursor (hCursor) ; ShowCursor (FALSE) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL) ; SelectClipRgn (hdc, hRgnClip) ; fRadius = _hypot (cxClient / 2.0, cyClient / 2.0) ; for (fAngle = 0.0 ; fAngle < TWO_PI ; fAngle += TWO_PI / 360) { MoveToEx (hdc, 0, 0, NULL) ; LineTo (hdc, (int) ( fRadius * cos (fAngle) + 0.5), (int) (-fRadius * sin (fAngle) + 0.5)) ; } EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: DeleteObject (hRgnClip) ; PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, iMsg, wParam, lParam) ; }
由于剪裁区域总是使用设备坐标,CLOVER程序必须在每次接收到WM_SIZE消息时重新建立剪裁区域。几年前,这可能需要几秒钟。现在的快速机器在一瞬间就可以画出来。
CLOVER从建立四个椭圆剪裁区域开始,这四个椭圆存放在hRgnTemp数组的头四个元素中,然后建立三个「空」剪裁区域:
hRgnTemp [4]= CreateRectRgn (0, 0, 1, 1) ; hRgnTemp [5]= CreateRectRgn (0, 0, 1, 1) ; hRgnClip = CreateRectRgn (0, 0, 1, 1) ;
显示区域左右的两个椭圆区域组合起来:
CombineRgn (hRgnTemp [4], hRgnTemp [0], hRgnTemp [1], RGN_OR) ;
同样,显示区域上下两个椭圆区域组合起来:
CombineRgn (hRgnTemp [5], hRgnTemp [2], hRgnTemp [3], RGN_OR) ;
最后,两个组合后的区域再组合到hRgnClip中:
CombineRgn (hRgnClip, hRgnTemp [4], hRgnTemp [5], RGN_XOR) ;
RGN_XOR标识符用于从结果区域中排除重迭部分。最后,删除6个临时区域:
for (i = 0 ; i < 6 ; i++) DeleteObject (hRgnTemp [i]) ;
与画出的图形比起来,WM_PAINT的处理很简单。视端口原点设定为显示区域的中心(使画直线更容易一些),在WM_SIZE消息处理期间建立的区域选择为设备内容的剪裁区域:
SetViewportOrg (hdc, xClient / 2, yClient / 2) ; SelectClipRgn (hdc, hRgnClip) ;
现在,剩下的就是画直线了,共360条,每隔一度画一条。每条线的长度为变量fRadius,这是从中心到显示区域的角落的距离:
fRadius = hypot (xClient / 2.0, yClient / 2.0) ; for (fAngle = 0.0 ; fAngle < TWO_PI ; fAngle += TWO_PI / 360) { MoveToEx (hdc, 0, 0, NULL) ; LineTo (hdc, (int) ( fRadius * cos (fAngle) + 0.5), (int) (-fRadius * sin (fAngle) + 0.5)) ; }
在处理WM_DESTROY消息时,删除该剪裁区域:
DeleteObject (hRgnClip) ;