天天看點

5.6 矩形、區域和裁剪

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

         因為區域總是使用裝置坐标 ,是以 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. for (fAngle = 0.0; fAngle < TWO_PI; fAngle += TWO_PI / 360)  
  3. {  
  4.      MoveToEx(hdc, 0, 0, NULL);  
  5.      LineTo(hdc, (int) ( fRadius * cos (fAngle) + 0.5),  
  6.                  (int) (-fRadius * sin (fAngle) + 0.5));  
  7. }  

在處理 WM_DESTROY 消息期間,裁剪區域被删除:

[cpp]  view plain  copy

  1. DeleteObject(hRgnClip);