摘錄于《Windows程式(第5版,珍藏版).CHarles.Petzold 著》P161
Windows 還有其他幾個使用 RECT(矩形)結構和區域的繪圖函數。一個區域指的是螢幕上的一塊空間,它由矩形、多邊形和橢圓組合而成。
5.6.1 處理矩形
下面三個繪圖函數需呀一個指向矩形結構的指針:
[cpp] view plain copy
- FillRect (hdc, &rect, hBrush);
- FrameRect (hdc, &rect, hBrush);
- 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
- rect.left = xLeft;
- rect.top = yTop;
- rect.right = xRight;
- rect.bottom = yBottom;
然而,通過 SetRect 函數的調用,隻用一行代碼即可實作相同的結果:
[cpp] view plain copy
- SetRect (&rect, xLeft, yTop, xRight, yBottom):
如果想做下列事情之一,可以友善的使用其他 8 個函數。
- 将矩形沿 x 軸和 y 軸移動幾個機關: [cpp] view plain copy
- OffsetRect (&rect, x, y);
- 增大或減小矩形的尺寸: [cpp] view plain copy
- InflateRect (&rect, x, y);
- 把矩形結構的各字段設定為0: [cpp] view plain copy
- SetRectEmpty (&rect);
- 将一個矩形結構複制到另一個矩形結構: [cpp] view plain copy
- CopyRect (&DestRect, &SrcRect);
- 擷取兩個矩形的交集: [cpp] view plain copy
- IntersectRect (&DestRect, &SrcRect1, &SrcRect2);
- 擷取兩個矩形的并集: [cpp] view plain copy
- UnionRect (&DestRect, &SrcRect1, &SrcRect2);
- 判斷矩形是否為空: [cpp] view plain copy
- bEmpty = IsRectEmpty (&rect);
- 判斷點是否在矩形内部: [cpp] view plain copy
- bInRect = PtInRect(&rect, point);
大多數情況下,還有一些簡單的代碼可以實作與這些函數相同的功能。例如,複制結構時,可以通過逐個字段的結構複制操作,來代替調用 CopyRect 函數,如下面的語句:
[cpp] view plain copy
- 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
- 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
- while (GetMessage (&msg, NULL, 0, 0))
- {
- TranslateMessage (&msg);
- DispatchMessage (&msg);
- }
- return msg.wParam;
替換後的消息循環如下:
[cpp] view plain copy
- while (TRUE)
- {
- if (PeekMessage (&msg, NULL, 0, 0, PM_REMOVE))
- {
- if (msg.message == WM_QUIT)
- break;
- TranslateMessage (&msg);
- DispatchMesage (&msg);
- }
- else
- {
- [other program lines to do some work]
- }
- }
- 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
- 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
- hRgn = CreateRectRgn (xLeft, yTop, xRight, yBottom);
或者
[cpp] view plain copy
- hRgn = CreateRectRgnIndirect (&rect);
也可以使用下面的函數建立橢圓區域:
[cpp] view plain copy
- hRgn = CreateEllipticRgn (xLeft, yTop, xRight, yBottom);
或者
[cpp] view plain copy
- hRgn = CreateEllipticRgnIndirect (&rect);
建立圓角矩形區域可以通過 CreateRoundRectRgn 函數實作。
建立一個多邊形區域的函數和 Polygon 函數類似:
[cpp] view plain copy
- hRgn = CreatePolygonRgn (&point, iCount, iPolyFillMode);
參數 point 是一個類型為 POINT 結構的數組, iCount 是點的個數,iPolyFillMode 或者是 ALTERNATE,或者是 WINDING。你也可以調用 CreatePolygonRgn 函數建立多個多邊形區域。
那麼你會問,區域有什麼特别之處嗎?下面的函數顯示了區域的作用:
[cpp] view plain copy
- 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
- 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 對象相同的函數來删除它:
[cpp] view plain copy
- DeleteObject (hRgn);
5.6.4 矩形與區域的裁剪
區域在裁剪中也扮演着重要角色。InvalidRect 函數使顯示的矩形區域無效,并産生一個 WM_PAINT 消息。例如,可以使用 InvalidateRect 函數來擦除客戶區的内容,并産生一個 WM_PAINT 消息:
[cpp] view plain copy
- InvalidateRect (hwnd, NULL, TRUE);
可以通過調用 GetUpdateRect 函數擷取無效矩形的坐标,并且使用 ValidateRect 使客戶區的矩形有效。當接收到一個 WM_PAINT 消息時,PAINTSTRUCT 結構中的無效矩形的坐标是可以利用的。這個結構是通過 BeginPaint 函數填充的。這個無效矩形也定義了一個“裁剪區域”。 不能在裁剪區域之外繪圖 。
Windows 有兩個類似 InvalidateRect 和 ValidateRect 的函數,用于處理區域而不是矩形;
[cpp] view plain copy
- InvalidateRgn (hwnd, hRgn, bErase);
和
[cpp] view plain copy
- ValidateRgn (hwnd, hRgn);
當接收一條由無效區域産生的 WM_PAINT 消息時,裁剪區域在形狀不一定是矩形。
可以通過将一個區域選入到裝置環境來建立你自己的裁剪區域,将區域選入裝置環境可以使用
[cpp] view plain copy
- SelectObject (hdc, hRgn);
或
[cpp] view plain copy
- SelectClipRgn (hdc, hRgn);
裁剪區域被假定使用的是裝置坐标 。
GDI 為裁剪區域做了一個副本,是以當把區域對象選入到裝置環境後,可以删除它。Windows 還包括幾個操縱這個裁剪區域的函數,例如 ExcludeClipRect 函數用來從裁剪區域中去除一個矩形;IntersectClipRect 函數用來建立一個新的裁剪區域,這個新的裁剪區域是先前的裁剪區域和某個矩形的交集;OffsetClipRgn 函數用來把一個裁剪區域移動到客戶區的另外一部分。
5.6.5 CLOVER 程式
CLOVER 程式由四個橢圓形成一個區域,然後把這個區域選入裝置環境,接着從視窗區中心發散繪制一系列直線。這些直線僅出現剪裁區域内。
如果使用傳統的方法繪制這個圖形,必須依據橢圓的圓周角公式計算出每條線段的端點。但是通過使用一個複雜的裁剪區域,就可以直接繪制直線,而讓 Windows 去确定這些端點。
[cpp] view plain copy
- #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 message, WPARAM wParam, LPARAM lParam)
- {
- static HRGN hRgnClip;
- static int cxClient, cyClient;
- double fAngle, fRadius;
- HCURSOR hCursor;
- HDC hdc;
- HRGN hRgnTemp[6];
- int i;
- PAINTSTRUCT ps;
- switch (message)
- {
- 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, message, wParam, lParam);
- }
因為區域總是使用裝置坐标 ,是以 CLOVER 程式不得不在每次收到 WM_SIZE 消息時 重新建立區域 。幾年前,運作 Windows 的機器要花費幾秒鐘來重繪這個圖形。今天,快速的機器幾乎在瞬間就能完成繪制。
CLOVER 先建立 4 個橢圓區域,它們被存儲在 hRgnTemp 數組的前 4 個元素中。接着,程式建立三個 “空”區域:
[cpp] view plain copy
- hRgnTemp[4] = CreateRectRgn(0, 0, 1, 1);
- hRgnTemp[5] = CreateRectRgn(0, 0, 1, 1);
- hRgnClip = CreateRectRgn(0, 0, 1, 1);
在客戶區左邊和右邊的兩個區域先合并:
[cpp] view plain copy
- CombineRgn(hRgnTemp[4], hRgnTemp[0], hRgnTemp[1], RGN_OR);
同樣地,在客戶區頂部的兩個橢圓區域也合并了:
[cpp] view plain copy
- CombineRgn(hRgnTemp[5], hRgnTemp[2], hRgnTemp[3], RGN_OR);
最後兩個合并後的區域再合并成 hRgnClip:
[cpp] view plain copy
- CombineRgn(hRgnClip, hRgnTemp[4], hRgnTemp[5], RGN_XOR);
RGN_XOR 辨別符表示要從結果區域中排除重疊的區域。最後,6 個臨時的區域被删除:
[cpp] view plain copy
- for (i = 0; i < 6; ++ i)
- DeleteObject(hRgnTemp[i]);
相對結果而言,WM_PAINT 消息處理很簡單。視口原點設定在客戶區的中心(這樣使畫直線更容易),在處理 WM_SIZE 消息時建立的區域被選入裝置環境作為裁剪區域:
[cpp] view plain copy
- SetViewportOrgEx(hdc, cxClient / 2, cyClient / 2, NULL);
- SelectClipRgn(hdc, hRgnClip);
現在,剩下要做的就是畫直線了,一共畫 360 條,每一度畫一條。每條線的長度是變量 fRadius,它表示的是從中心到客戶區角落的距離:
[cpp] view plain copy
- 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));
- }
在處理 WM_DESTROY 消息期間,裁剪區域被删除:
[cpp] view plain copy
- DeleteObject(hRgnClip);