5.6 矩形、区域和裁剪

来源:互联网 发布:3ds max模型优化 编辑:程序博客网 时间:2024/05/16 12:01

摘录于《Windows程序(第5版,珍藏版).CHarles.Petzold 著》P161

        Windows 还有其他几个使用 RECT(矩形)结构和区域的绘图函数。一个区域指的是屏幕上的一块空间,它由矩形、多边形和椭圆组合而成

5.6.1  处理矩形

        下面三个绘图函数需呀一个指向矩形结构的指针:

[cpp] view plain copy
  1. FillRect    (hdc, &rect, hBrush);  
  2. FrameRect   (hdc, &rect, hBrush);  
  3. InvertRect  (hdc, &rect);  
在这三个函数中,参数 rect 是一个类型为 RECT 的结构,它有 4 个字段:left、top、right 和 bottom。在这个结构中,坐标是逻辑坐标

        FillRect 函数使用指定的画刷填充矩形(达到但不包括右下坐标)。这个函数不需要事先把画刷选入设备环境。

        FrameRect 使用画刷绘制一个矩形框,但是它并不填充矩形。使用画刷来绘制边框似乎有点奇怪,因为到目前为止,你所看到的绘制边框的函数(例如 Rectangle)都是由当前画笔绘制的。FrameRect 函数允许你绘制一个不一定是纯色的矩形框。矩形的边框是 1 个逻辑单位宽。如果逻辑单位大于设备单位,边框的宽度将是 2 个或者更多的像素。

        InvertRect 函数翻转矩形内所有的像素,将 1 变为 0,0 变为 1。即这个函数将白色区域变为黑色区域,黑色区域变为白色区域,绿色区域变为洋红色区域。

        Windows 还包含 9 个可用于轻松便捷地操纵 RECT 结构的函数。例如,通常使用下面的代码将 RECT 结构的 4 个字段设置为特定值:

[cpp] view plain copy
  1. rect.left     = xLeft;  
  2. rect.top      = yTop;  
  3. rect.right    = xRight;  
  4. rect.bottom   = yBottom;  
然而,通过 SetRect 函数的调用,只用一行代码即可实现相同的结果:

[cpp] view plain copy
  1. SetRect (&rect, xLeft, yTop, xRight, yBottom):  

        如果想做下列事情之一,可以方便的使用其他 8 个函数。

  • 将矩形沿 x 轴和 y 轴移动几个单位:
    [cpp] view plain copy
    1. OffsetRect (&rect, x, y);  
  • 增大或减小矩形的尺寸:
    [cpp] view plain copy
    1. InflateRect (&rect, x, y);  
  • 把矩形结构的各字段设置为0:
    [cpp] view plain copy
    1. SetRectEmpty (&rect);  
  • 将一个矩形结构复制到另一个矩形结构:
    [cpp] view plain copy
    1. CopyRect (&DestRect, &SrcRect);  
  • 获取两个矩形的交集:
    [cpp] view plain copy
    1. IntersectRect (&DestRect, &SrcRect1, &SrcRect2);  
  • 获取两个矩形的并集:
    [cpp] view plain copy
    1. UnionRect (&DestRect, &SrcRect1, &SrcRect2);  
  • 判断矩形是否为空:
    [cpp] view plain copy
    1. bEmpty = IsRectEmpty (&rect);  
  • 判断点是否在矩形内部:
    [cpp] view plain copy
    1. bInRect = PtInRect(&rect, point);  

        大多数情况下,还有一些简单的代码可以实现与这些函数相同的功能。例如,复制结构时,可以通过逐个字段的结构复制操作,来代替调用 CopyRect 函数,如下面的语句:

[cpp] view plain copy
  1. DestRect = SrcRect;  

5.6.2  随机矩形

        在任何一个图形系统中,总存在这样一个有趣的程序,即简单地使用随机的尺寸和颜色不停地绘制一系列的图像,例如,随机大小和颜色的矩形。在 Windows 中可以创建这样的一个程序,但是这并不像想象的那样容易。我希望你能够意识到,不能在处理 WM_PAINT 消息中简单地使用 while(TRUE) 循环。当然,这样做会奏效,但是这样做的结果是,程序将停止对其他消息的处理,而且程序不能退出或者最小化

        一种可接受的方式是设置一个向你的窗口函数发送 WM_TIMER 消息的 Windows 计时器。(我将在第 8 章介绍计时器。)对于每个 WM_TIMER 消息,可以调用 GetDC 函数获取设备环境,然后绘制一个随机矩形,接着调用 ReleaseDC 函数释放设备环境。但是那样做又会使程序失去一些趣味性,因为程序不能很快地绘制随机矩形。必须等待每个 WM_TIMER 消息,那样会依赖于系统时钟的精度

        在 Windows 中有很多的“空闲时间”,在这期间所有的消息队列都是空的,Windows 就在等待键盘或者鼠标的输入。那么能否在空闲期间从某种程度上获取控制并绘制随机矩形,而一旦有消息加载到程序的消息队列,就释放控制呢?这正是 PeekMessage 函数的“用武之地”。下面是 PeekMessage 函数调用的一个例子:

[cpp] view plain copy
  1. PeekMessage (&msg, NULL, 0, 0, PM_REMOVE);  

        函数的前 4 个参数(一个是指向 MSG 结构的指针,一个是窗口句柄,另外两个值表示信息范围)与 GetMessage 函数相同。设置第二、三、四个参数为 NULL 或者 0,表示我们想使用 PeekMessage 函数返回程序中所有窗口的所有消息。如果要删除消息队列中的消息,可以把 PeekMessage 函数的最后一个参数设置为 PM_REMOVE。如果不想删除,就设置为 PM_NOREMOVE。这就是 PeekMessage 名字的意思,它是“偷看”而不是“获得”。它允许一个程序检查程序队列中的下一个消息,而不是真实地获得并删除它看到的消息。

        GetMessage 函数并不把控制权交还给程序,除非它从程序的消息队列中获得了消息。但是 PeekMessage 函数却总是立即返回,不管消息是否出现。当一个消息在程序的消息队列中时,PeekMessage 函数的返回值是TRUE(非 0),而消息则像正常情况一样处理。当队列中没有消息时,PeekMessage 函数返回FALSE(0)

        这允许我们替换正常的消息循环,正常的消息循环如下所示:

[cpp] view plain copy
  1. while (GetMessage (&msg, NULL, 0, 0))  
  2. {  
  3.    TranslateMessage (&msg);  
  4.    DispatchMessage (&msg);  
  5. }  
  6. return msg.wParam;  
替换后的消息循环如下:

[cpp] view plain copy
  1. while (TRUE)   
  2. {  
  3.     if (PeekMessage (&msg, NULL, 0, 0, PM_REMOVE))  
  4.     {  
  5.         if (msg.message == WM_QUIT)  
  6.             break;  
  7.         TranslateMessage (&msg);  
  8.         DispatchMesage (&msg);  
  9.     }   
  10.     else   
  11.     {  
  12.         [other program lines to do some work]  
  13.     }  
  14. }  
  15. return msg.wParam;  

注意:在这里,必须明确检查 WM_QUIT 消息,在一个正常的消息循环中,不需要这样做,因为当获取一个 WM_QUIT 消息时,GetMessage 函数的返回值是 FLASE(0)。但是 PeekMessage 函数的返回值是表示队列中是否有消息,因此检查 WM_QUIT 是必要的。


        如果 PeekMessage 函数返回 TRUE,那么消息会正常执行。如果返回 FLASE,那么程序可以在返回给 Windows 控制之前做些事情(如显示另一个随机矩形)。

        (尽管 Windows 文档中指出不能使用 PeekMessage 函数从消息队列中删除 WM_PAINT 消息,但是这并没有什么问题。毕竟,GetMessage 函数其实也不能从队列中删除 WM_PAINT 消息。使客户区的无效区域变成有效是从队列中删除 WM_PAINT 消息的唯一办法,可以使用 ValidateRect、ValidateRgn 或者成对的 BeginPaint 和 EndPaint 函数来完成。如果使用 PeekMessage 函数从消息队列获取 WM_PAINT 消息后,按照正常的方式对它进行处理,就不会又任何问题。但使用下面的代码来清除消息队列中的所有消息是不允许的

[cpp] view plain copy
  1. while (PeekMessage (&msg, NULL, 0, 0, PM_REMOVE));  
这条语句表示从你的消息队列中删除 WM_PAINT 消息之外的所有消息。如果 WM_PAINT 在队列中,你将永远陷于 while 循环无法终止。)

        PeekMessage 函数在早期版本的 Windows 中比在 Windows 98 中重要得多。这是因为 16 位版本的 Windows 使用非抢占式多任务系统。Windows 自带的 Terminal 程序使用 PeekMessage 函数循环检查从通信端口接收到的数据。打印机管理程序也使用这项技术来打印,其他的 Windows 打印程序通常也使用一个 PeekMessage 函数的循环。在抢占式多任务的 Windows 98 中,应用程序可以建立多个线程。

5.6.3  建立和绘制区域

         一个区域是对显示器一块空间的描述,这个空间可以是矩形、多边形和椭圆的组合可以使用区域进行绘图或者裁剪。将区域选入设备环境,就可以使用这个区域来裁剪(也就是说,将绘制动作限制在客户区的一个特定部分)。同画笔和画刷一样,区域也就是 GDI 对象,应当通过调用 DeleteObject 函数来删除所有建立的区域。

        当建立一个区域时,Windows 返回一个类型为 HRGN 的区域句柄。最简单的区域类型是一个矩形区域。可以用下面的两种办法建立一个矩形区域:

[cpp] view plain copy
  1. hRgn = CreateRectRgn (xLeft, yTop, xRight, yBottom);  
或者

[cpp] view plain copy
  1. hRgn = CreateRectRgnIndirect (&rect);  
也可以使用下面的函数建立椭圆区域:

[cpp] view plain copy
  1. hRgn = CreateEllipticRgn (xLeft, yTop, xRight, yBottom);  
或者

[cpp] view plain copy
  1. hRgn = CreateEllipticRgnIndirect (&rect);  
创建圆角矩形区域可以通过 CreateRoundRectRgn 函数实现。

        创建一个多边形区域的函数和 Polygon 函数类似:

[cpp] view plain copy
  1. hRgn = CreatePolygonRgn (&point, iCount, iPolyFillMode);  
参数 point 是一个类型为 POINT 结构的数组, iCount 是点的个数,iPolyFillMode 或者是 ALTERNATE,或者是 WINDING。你也可以调用 CreatePolygonRgn 函数创建多个多边形区域。

        那么你会问,区域有什么特别之处吗?下面的函数显示了区域的作用:

[cpp] view plain copy
  1. iRgnType = CombineRgn (hDestRgn, hSrcRgn1, hSrcRgn2, iCombine);  
这个函数将两个源区域(hSrcRgn1 和 hSrcRgn2)结合起来,并产生目标区域句柄(hDestRgn) 来表示那个组合区域。这三个区域句柄都必须有效,但是函数调用后 hDestRgn 先前描述的区域都被销毁了。(当使用这个函数时,可能要让 hDestRgn 在初始时表示一个很小的矩形区域。)

        参数 iCombine 描述 hSrcRgn1 区域和 hSrcRgn2 区域结合的方式:

iCombine 值新的区域 RGN_AND 两个源区域的公共部分 RGN_OR 两个源区域的全部 RGN_XOR 两个源区域的全部,但除去公共部分 RGN_DIFF hSrcRgn1 不在 hSrcRgn2 中的部分 RGN_COPY hSrcRgn1 的全部(忽略 hSrcRgn2)

        iRgnType 值是从 CombineRgn 返回的下列值之一:NULLREGION,指的是一个空的区域;SIMPLEREGION,指的是一个简单的矩形、椭圆或者多边形;COMPLEXREGION,指的是矩形、椭圆或多边形的组合;ERROR,指的是有错误发生。

        一旦有了一个区域的句柄,就可以使用下面 4 个绘图函数:

[cpp] view plain copy
  1. FillRgn   (hdc, hRgn, hBrush);  
  2. FrameRgn  (hdc, hRgn, hBrush, xFrame, yFrame);  
  3. InvertRgn (hdc, hRgn);  
  4. PaintRgn  (hdc, hRgn);  
FillRgn、FrameRgn 和 InvertRgn 函数类似于 FillRect、FrameRect 和 InvertRect 函数。FrameRgn 的参数 xFrame 和 yFrame 是表示在区域周围的、要绘制的边框的逻辑宽度和高度。PaintRgn 函数使用当前被选入设备环境的画刷来填充区域。所有的这些函数都假定使用的是逻辑坐标。

        用完一个区域后,可以用于删除其他 GDI 对象相同的函数来删除它:

[cpp] view plain copy
  1. DeleteObject (hRgn);  

5.6.4  矩形与区域的裁剪

        区域在裁剪中也扮演着重要角色。InvalidRect 函数使显示的矩形区域无效,并产生一个 WM_PAINT 消息。例如,可以使用 InvalidateRect 函数来擦除客户区的内容,并产生一个 WM_PAINT 消息:

[cpp] view plain copy
  1. InvalidateRect (hwnd, NULL, TRUE);  
可以通过调用 GetUpdateRect 函数获取无效矩形的坐标,并且使用 ValidateRect 使客户区的矩形有效。当接收到一个 WM_PAINT 消息时,PAINTSTRUCT 结构中的无效矩形的坐标是可以利用的。这个结构是通过 BeginPaint 函数填充的。这个无效矩形也定义了一个“裁剪区域”。不能在裁剪区域之外绘图

        Windows 有两个类似 InvalidateRect 和 ValidateRect 的函数,用于处理区域而不是矩形;

[cpp] view plain copy
  1. InvalidateRgn (hwnd, hRgn, bErase);  

[cpp] view plain copy
  1. ValidateRgn (hwnd, hRgn);  
当接收一条由无效区域产生的 WM_PAINT 消息时,裁剪区域在形状不一定是矩形。

        可以通过将一个区域选入到设备环境来创建你自己的裁剪区域,将区域选入设备环境可以使用

[cpp] view plain copy
  1. SelectObject (hdc, hRgn);  

[cpp] view plain copy
  1. SelectClipRgn (hdc, hRgn);  
裁剪区域被假定使用的是设备坐标

        GDI 为裁剪区域做了一个副本,因此当把区域对象选入到设备环境后,可以删除它。Windows 还包括几个操纵这个裁剪区域的函数,例如 ExcludeClipRect 函数用来从裁剪区域中去除一个矩形;IntersectClipRect 函数用来建立一个新的裁剪区域,这个新的裁剪区域是先前的裁剪区域和某个矩形的交集;OffsetClipRgn 函数用来把一个裁剪区域移动到客户区的另外一部分。

5.6.5  CLOVER 程序

        CLOVER 程序由四个椭圆形成一个区域,然后把这个区域选入设备环境,接着从窗口区中心发散绘制一系列直线。这些直线仅出现剪裁区域内。

        如果使用传统的方法绘制这个图形,必须依据椭圆的圆周角公式计算出每条线段的端点。但是通过使用一个复杂的裁剪区域,就可以直接绘制直线,而让 Windows 去确定这些端点。

[cpp] view plain copy
  1. /*------------------------------------------------------------ 
  2.     CLOVER.C  --  Clover Drawing Program Using Regions 
  3.                     (c) Charles Petzold, 1998 
  4. ------------------------------------------------------------*/  
  5. #include <windows.h>  
  6. #include <math.h>  
  7.   
  8. #define TWO_PI (2.0 * 3.14159)  
  9.   
  10. LRESULT CALLBACK WndProc(HWNDUINTWPARAMLPARAM);  
  11.   
  12. int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,  
  13.                    PSTR szCmdLine, int iCmdShow)  
  14. {  
  15.     static TCHAR szAppName[] = TEXT ("Clover");  
  16.     HWND         hwnd;  
  17.     MSG          msg;  
  18.     WNDCLASS     wndclass;  
  19.   
  20.     wndclass.style          = CS_HREDRAW | CS_VREDRAW;  
  21.     wndclass.lpfnWndProc    = WndProc;  
  22.     wndclass.cbClsExtra     = 0;  
  23.     wndclass.cbWndExtra     = 0;  
  24.     wndclass.hInstance      = hInstance;  
  25.     wndclass.hIcon          = LoadIcon (NULL, IDI_APPLICATION);  
  26.     wndclass.hCursor        = LoadCursor (NULL, IDC_ARROW);  
  27.     wndclass.hbrBackground  = (HBRUSH) GetStockObject (WHITE_BRUSH);  
  28.     wndclass.lpszMenuName   = NULL;  
  29.     wndclass.lpszClassName  = szAppName;  
  30.   
  31.     if (!RegisterClass (&wndclass))  
  32.     {  
  33.         MessageBox (NULL, TEXT ("This program requires Windows NT!"),  
  34.                     szAppName, MB_ICONERROR);  
  35.         return 0;  
  36.     }  
  37.   
  38.     hwnd = CreateWindow (szAppName, TEXT("Draw a Clover"),  
  39.                          WS_OVERLAPPEDWINDOW,  
  40.                          CW_USEDEFAULT, CW_USEDEFAULT,  
  41.                          CW_USEDEFAULT, CW_USEDEFAULT,  
  42.                          NULL, NULL, hInstance, NULL);  
  43.     ShowWindow (hwnd, iCmdShow);  
  44.     UpdateWindow (hwnd);  
  45.   
  46.     while (GetMessage (&msg, NULL, 0, 0))  
  47.     {  
  48.         TranslateMessage (&msg);  
  49.         DispatchMessage (&msg);  
  50.     }  
  51.     return msg.wParam;  
  52. }  
  53.   
  54. LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)  
  55. {  
  56.     static HRGN hRgnClip;  
  57.     static int  cxClient, cyClient;  
  58.     double      fAngle, fRadius;  
  59.     HCURSOR     hCursor;  
  60.     HDC         hdc;  
  61.     HRGN        hRgnTemp[6];  
  62.     int         i;  
  63.     PAINTSTRUCT ps;  
  64.   
  65.     switch (message)  
  66.     {  
  67.     case WM_SIZE:  
  68.         cxClient = LOWORD(lParam);  
  69.         cyClient = HIWORD(lParam);  
  70.   
  71.         hCursor = SetCursor(LoadCursor(NULL, IDC_WAIT));  
  72.         ShowCursor(TRUE);  
  73.   
  74.         if (hRgnClip)  
  75.             DeleteObject(hRgnClip);  
  76.   
  77.         hRgnTemp[0] = CreateEllipticRgn(0, cyClient / 3,  
  78.                                         cxClient / 2, 2 * cyClient / 3);  
  79.   
  80.         hRgnTemp[1] = CreateEllipticRgn(cxClient / 2, cyClient / 3,  
  81.                                         cxClient, 2 * cyClient / 3);  
  82.   
  83.         hRgnTemp[2] = CreateEllipticRgn(cxClient / 3, 0,  
  84.                                         2 * cxClient / 3, cyClient / 2);  
  85.   
  86.         hRgnTemp[3] = CreateEllipticRgn(cxClient / 3, cyClient / 2,  
  87.                                         2 * cxClient / 3, cyClient);  
  88.   
  89.         hRgnTemp[4] = CreateRectRgn(0, 0, 1, 1);  
  90.         hRgnTemp[5] = CreateRectRgn(0, 0, 1, 1);  
  91.         hRgnClip    = CreateRectRgn(0, 0, 1, 1);  
  92.   
  93.         CombineRgn(hRgnTemp[4], hRgnTemp[0], hRgnTemp[1], RGN_OR);  
  94.         CombineRgn(hRgnTemp[5], hRgnTemp[2], hRgnTemp[3], RGN_OR);  
  95.         CombineRgn(hRgnClip,    hRgnTemp[4], hRgnTemp[5], RGN_XOR);  
  96.   
  97.         for (i = 0; i < 6; ++ i)  
  98.             DeleteObject(hRgnTemp[i]);  
  99.   
  100.         SetCursor(hCursor);  
  101.         ShowCursor(FALSE);  
  102.         return 0;  
  103.   
  104.     case WM_PAINT:  
  105.         hdc = BeginPaint(hwnd, &ps);  
  106.   
  107.         SetViewportOrgEx(hdc, cxClient / 2, cyClient / 2, NULL);  
  108.         SelectClipRgn(hdc, hRgnClip);  
  109.   
  110.         fRadius = hypot(cxClient / 2.0, cyClient / 2.0);  
  111.   
  112.         for (fAngle = 0.0; fAngle < TWO_PI; fAngle += TWO_PI / 360)  
  113.         {  
  114.             MoveToEx(hdc, 0, 0, NULL);  
  115.             LineTo(hdc, (int) ( fRadius * cos (fAngle) + 0.5),  
  116.                         (int) (-fRadius * sin (fAngle) + 0.5));  
  117.         }  
  118.   
  119.         EndPaint(hwnd, &ps);  
  120.         return 0;  
  121.     case WM_DESTROY:  
  122.         DeleteObject(hRgnClip);  
  123.         PostQuitMessage(0);  
  124.         return 0;  
  125.     }  
  126.     return DefWindowProc(hwnd, message, wParam, lParam);  
  127. }  

        因为区域总是使用设备坐标,所以 CLOVER 程序不得不在每次收到 WM_SIZE 消息时重新创建区域。几年前,运行 Windows 的机器要花费几秒钟来重绘这个图形。今天,快速的机器几乎在瞬间就能完成绘制。

        CLOVER 先创建 4 个椭圆区域,它们被存储在 hRgnTemp 数组的前 4 个元素中。接着,程序创建三个 “空”区域:

[cpp] view plain copy
  1. hRgnTemp[4] = CreateRectRgn(0, 0, 1, 1);  
  2. hRgnTemp[5] = CreateRectRgn(0, 0, 1, 1);  
  3. hRgnClip    = CreateRectRgn(0, 0, 1, 1);  
在客户区左边和右边的两个区域先合并:

[cpp] view plain copy
  1. CombineRgn(hRgnTemp[4], hRgnTemp[0], hRgnTemp[1], RGN_OR);  
同样地,在客户区顶部的两个椭圆区域也合并了:

[cpp] view plain copy
  1. CombineRgn(hRgnTemp[5], hRgnTemp[2], hRgnTemp[3], RGN_OR);  
最后两个合并后的区域再合并成 hRgnClip:

[cpp] view plain copy
  1. CombineRgn(hRgnClip,    hRgnTemp[4], hRgnTemp[5], RGN_XOR);  
RGN_XOR 标识符表示要从结果区域中排除重叠的区域。最后,6 个临时的区域被删除:

[cpp] view plain copy
  1. for (i = 0; i < 6; ++ i)  
  2.      DeleteObject(hRgnTemp[i]);  

        相对结果而言,WM_PAINT 消息处理很简单。视口原点设置在客户区的中心(这样使画直线更容易),在处理 WM_SIZE 消息时创建的区域被选入设备环境作为裁剪区域:

[cpp] view plain copy
  1. SetViewportOrgEx(hdc, cxClient / 2, cyClient / 2, NULL);  
  2. SelectClipRgn(hdc, hRgnClip);  

        现在,剩下要做的就是画直线了,一共画 360 条,每一度画一条。每条线的长度是变量 fRadius,它表示的是从中心到客户区角落的距离:

[cpp] view plain copy
  1. fRadius = hypot(cxClient / 2.0, cyClient / 2.0);  
  2.   
  3. for (fAngle = 0.0; fAngle < TWO_PI; fAngle += TWO_PI / 360)  
  4. {  
  5.      MoveToEx(hdc, 0, 0, NULL);  
  6.      LineTo(hdc, (int) ( fRadius * cos (fAngle) + 0.5),  
  7.                  (int) (-fRadius * sin (fAngle) + 0.5));  
  8. }  
在处理 WM_DESTROY 消息期间,裁剪区域被删除:

[cpp] view plain copy
  1. DeleteObject(hRgnClip);  
0 0