Programming Windows 第五版讀書筆記 第四章 輸出文字

來源:互聯網
上載者:User
Programming Windows 5th Edition Chapter 4 輸出文字

1. 本章更進一步的闡述顯示文字的注意點,包括如何根據字型的大小來計算輸出座標,在不同的情況下如何取得裝置控制代碼並開始繪圖,如何在程式中加入捲軸等。

2. 首先需要介紹的很重要,那就是什麼情況下windows會發送WM_PAINT訊息給我們的程式,如下:

(1) 以下情況發生一種就一定會發送WM_PAINT訊息:

在使用者移動視窗或顯示視窗時,視窗中先前被隱藏的地區重新可見。
使用者改變視窗的大小(如果視窗類別別樣式有著CS_HREDRAW和CS_VREDRAW位旗標的設定)。
程式使用ScrollWindow或ScrollDC函數滾動顯示地區的一部分。
程式使用InvalidateRect或InvalidateRgn函數刻意產生WM_PAINT訊息。

(2) 以下情況時,windows試圖幫我們儲存一個顯示地區,這樣試圖不用讓我們的程式來負責重繪,但windows的儲存動作不一定成功,也就是說,以下情況windows可能會發送WM_PAINT訊息:

Windows擦除覆蓋了部分視窗的對話方塊或訊息框。
菜單下拉出來,然後被釋放。
顯示工具提示訊息。

(3) 以下情況windows總是會儲存被覆蓋的顯示地區,然後恢複,也就是說,不會發送WM_PAINT訊息:

滑鼠游標穿越顯示地區。
表徵圖拖過顯示地區。

3. 有效矩形和無效矩形。Windows在發送WM_PAINT訊息的時候,都會把需要重繪的顯示地區儲存到一個PAINTSTURCTURE中,從這裡我們 的程式就可以知道哪部分內容需要重繪。Windows不會儲存多個WM_PAINT訊息在訊息佇列中,因為在添加WM_PAINT訊息的時候,如果發現此 時隊列中已經有了一個WM_PAINT訊息,那麼windows會將上一個WM_PAINT訊息中的無效矩形取出,和目前的這個矩形合并,形成一個新的矩 形,然後讓我們的訊息處理函數處理,windows不會儲存多個WM_PAINT訊息在訊息佇列中。在前幾章,我們已經可以看到,在調用 BeginPaint的時候,需要傳入一個PAINTSTRUCT的結構,windows會將無效矩形就填充到這個資料結構中。

無效矩形的概念很重要。訊息處理函數在處理涉及到重繪的訊息時,必須將無效矩形申明成有效矩形,否則windows會認為我們的重繪工作沒有完成,其結果就是--windows一直給我們的程式發送WM_PAINT訊息!
調 用BeginPaint函數,可以使整個顯示地區變成有效,或調用ValidateRect函數,可以使顯示地區內任意矩形地區變為有效。一旦矩形變成有 效,任何涉及到該矩形地區的WM_PAINT訊息都將被刪除。所以,在我們的代碼中,即使我們在WM_PAINT的訊息處理中,什麼事都不幹,也要調用 BeginPaint, EndPaint,使無效矩形變成有效,否則WM_PAINT將一直被發送。

4. 裝置內容(DC)。在前面我們看到,在調用BeginPaint後,會返回一個hdc,這其實就是DC的一個控制代碼,DC實際上就是windows GDI內部儲存的資料結構,有了hdc,我們才具備了向特定顯示裝置輸出東西的能力。DC中大多儲存了一些圖形屬性的資料,比如TextOut的時 候,DC中儲存了文字的顏色,文字的背景色,xy座標映射到視窗顯示地區的方式,使用的字型等等。所以,要修改文字輸出的格式,顏色等,就要對DC進行操 作,然後用修改過的DC來繪圖,才能出現我們想要的結果。

5. OK,既然繪圖,輸出文字的第一步是要取得HDC,那麼,現在開始講解取得HDC的兩種辦法。註:除了調用CreateDC建立的DC之外,程式不能在兩個訊息之間儲存其他的DC控制代碼(HDC)。

(1) 方法1。該方法在處理WM_PAINT訊息的時候使用。就是調用BeginPaint, EndPaint函數對。前面說過了,處理WM_PAINT訊息,什麼都不做,也要調用這兩個函數,否則WM_PAINT訊息會一直被發送(見上面的描 述)。在調用BeginPaint的時候,我們還需要填入一個PAINTSTRUCT,她的結構如下:

Code: Select all
typedef struct tagPAINTSTRUCT
{
     HDC       hdc ;
     BOOL      fErase ;
     RECT      rcPaint ;
     BOOL      fRestore ;
     BOOL      fIncUpdate ;
     BYTE      rgbReserved[32] ;
} PAINTSTRUCT ;

BeginPaint 會為我們在這個結構中填入資料。其中,我們只需要關注前三個欄位,後面的是給windows用的。hdc不說了;fErase被BeginPaint標誌 為FALSE,表示Windows已經用背景擦除了無效矩形(背景定義在註冊視窗類別別的時候,前面說過了,BeginPaint調用的時 候,windows會為我們擦除無效矩形的內容),如果我們不想讓windows為我們擦除無效矩形,比如很多要求顯示迅速的繪圖,那麼我們可以處理 WM_ERASEBKGND訊息,在裡面加入我們的代碼。不過,如果我們的程式通過調用InvalidateRect來觸發WM_PAINT訊息的 話,InvalidateRect函數的最後一個參數可以用來指定是否擦除無效地區,如果這個參數為FALSE,那麼windows將不會擦除無效地區, 此時fErase這個欄位的值就是TRUE了;最後第三個欄位是rcPaint,這就是無效矩形的定義了,我們可以通過這個取到目前需要重繪哪部分,而 且,事實上,windows會自動裁減我們的繪圖操作,不在這個矩形地區內的繪圖操作將被ignore!如果我們真的需要在這個矩形外面繪製的話,可以在 調用BeginPaint之前,調用InvalidateRect(hwnd, NULL, TRUE),這樣就可以使整個顯示地區都變成無效矩形。出現這種情況,一般就是我們不管三七二十一,將整個顯示地區全部重繪一遍的做法了。

GetUpdateRect。 這個方法也可以用來獲得當前的無效矩形地區。其實BeginPaint已經可以了,所以這個方法我認為用處不大。需要注意的是,如果在調用了 BeginPaint之後,來調用這個GetUpdateRect,那麼得到的矩形將是empty rect,因為前面說過了,BeginPaint會把整個顯示地區都申明成有效地區。

(2) 方法2。方法1適用於在WM_PAINT訊息處理函數中使用,事實上,很多時候我們在其他訊息處理的時候,也需要進行繪製工作,這個時候怎麼取得hdc 呢?很簡單,調用GetDC方法即可。GetDC函數簡單一些,只需傳入一個視窗控制代碼hwnd,就會返回一個hdc,和BeginPaint, EndPaint一樣,GetDC必須和ReleaseDC配對使用。GetDC返回的hdc中包含了一個無效矩形,這個矩形就是整個顯示地區(因為這不 是在WM_PAINT訊息中)。GetDC方法一般在相應鍵盤滑鼠訊息的時候使用(比如一個用滑鼠繪圖的程式),此時我們可以根據滑鼠的輸入,立即更新顯 示地區,而不用等到WM_PAINT訊息中去處理。

GetDC, ReleaseDC方法不會將地區申明成有效矩形地區。我們可以通過調用ValidateRect函數來實現這一目的。

與 GetDC類似的函數有GetWindowDC。GetDC傳回用於寫入視窗顯示地區的裝置內容控制代碼,而GetWindowDC傳回寫入整個視窗的裝置內 容控制代碼。例如,您的程式可以使用從GetWindowDC傳回的裝置內容控制代碼在視窗的標題列上寫入文字。這種情況下,程式同樣也應該處理 WM_NCPAINT (「非顯示地區繪製」)訊息。

6. TextOut函數。這個函數原型如下:

TextOut (hdc, x, y, psText, iLength);

幾個注意點:

(1) psText字串中不要包含斷行符號,換行,刪除等控制字元,這些字元將被顯示成方塊。
(2) iLength是字元的個數,不是位元組數。
(3) x,y座標被稱為邏輯座標。和hdc中定義的座標映射模式有關。預設是MM_TEXT, 此時表示邏輯座標和實際座標相同,即左上方是0, 0,x向右增長,y向下增長。根據需要,可以定義不同的座標模式。下一章會講解。

7. 系統字型。字型必需要解釋,因為在具體繪圖的時候,我們必須知道字型的寬度,高度等資訊,才能將文字正確的繪製到我們想要的地方。這裡只講述 windows內建的標準系統字型system font,其他的字型自己研究。在Windows的早期版本中,系統字型是等寬字型,就是所有的字母都是一個寬度,現在不是了,比如W和i佔用的寬度就不 一樣,因為非等寬字型更利於閱讀。系統字型是一種點陣字型,也就是說,字元圖形被定義成象素塊,第十七章會講到TrueType(這是由輪廓定義的字 體)。

OK, 和GetSystemMetrics函數一樣,我們用GetTextMetrics就可以取道字型的資訊。GetTextMetrics填充TEXTMETRIC結構,這個結構有20個欄位,我們只關心前七個:

Code: Select all
typedef struct tagTEXTMETRIC
{
     LONG tmHeight ;
     LONG tmAscent ;
     LONG tmDescent ; 
     LONG tmInternalLeading ;
     LONG tmExternalLeading ;
     LONG tmAveCharWidth ;
     LONG tmMaxCharWidth ;
          [other structure fields]
}
TEXTMETRIC, * PTEXTMETRIC ;

// Get the text metrics
hdc = GetDC (hwnd) ;
GetTextMetrics (hdc, &tm) ;
ReleaseDC (hwnd, hdc) ;

這些欄位的含義參考 附件1

在我們日常編程的過程中,我們只需要關注這麼幾個關鍵點就可以了(計算字元的寬度和高度,以方便我們在TextOut的時候確定繪製座標):

(1) 小寫字元一般取tmAveCharWidth來確定寬度
(2) 大寫字元的寬度:tm.tmPitchAndFamily & 1 ? 3 : 2) * tmAveCharWidth / 2,也就是首先看tmPitchAndFamily是0還是1,是0表示大寫字元寬度和小寫字元一樣,取1,大寫字元的寬度設成小寫字元的平均寬度的 1.5倍
(3) 字元的高度取成:tm.tmHeight + tm.tmExternalLeading

8. 我們可以把確定字元寬度和高度的代碼放在WM_CREATE訊息中初始化,然後在使用TextOut的時候,可以考慮使用wsprintf函數來格式化字 符串,而且wsprintf函數非常棒的是會返回格式化後的字串的字元個數,這個值正好用於TextOut的iLength參數,如下:

TextOut (hdc, x, y, szBuffer, wsprintf (szBuffer, TEXT("The sum of %i and %i is %i"), iA, iB, iA + iB)) ;

9. 然後書中給出了一個例子,可以看練習代碼。例子很好理解,裡面的SetTextAlign(hdc, TA_RIGHT|TA_TOP); 指明了TextOut的x,y座標是從字元右上方的座標,而不是預設的左上方,從而實現了字串靠右對齊。

10. 上面的代碼例子SysMets1有一個很明顯的缺點就是顯示空間不夠顯示我們的繪製,所以自然就引入了捲軸的概念。本章中共講述了兩種捲軸的做法,第 二種做法是目前常用的做法,也是科學的做法,第一種做法有缺陷,而且捲軸方塊的大小也是固定的。我們來一個一個講述,都很有價值。

11. 無論那種捲軸做法,我們都需要在視窗大小發生改變的時候,設定滾動範圍和輸出的文字,所以,下面的代碼中都有對WM_SIZE的訊息處理。我們在WM_SIZE訊息中可以獲得當前視窗的大小:

Code: Select all
case WM_SIZE:
   cxClient = LOWORD (lParam) ;
   cyClient = HIWORD (lParam) ;
   return 0 ;

在WM_SIZE訊息中,lParam的低16位表示當前顯示地區(不是整個視窗哦)的寬度,高16位表示當前顯示地區的高度。用windows提供給我們的LOWORD和HIWORD兩個宏可以很方面的取出這兩個數值。

12. 要在視窗中加入捲軸很簡單,在CreateWindow的第三個參數,定義視窗樣式的時候,加入WS_VSCROLL, WS_HSCROLL的風格就可以在視窗中加入縱向和橫向捲軸了。首先我們來看第一種捲軸實現的方法,他是通過這麼幾個關鍵函數來做到的:

// iBar設成SB_VERT或SB_HORZ,表示縱向和橫向捲軸
// iMin, iMax表示捲軸的取值範圍
SetScrollRange (hwnd, iBar, iMin, iMax, bRedraw) ;

// 設定捲軸當前的位置
SetScrollPos (hwnd, iBar, iPos, bRedraw) ;

在 使用者單擊了捲軸之後,windows給我們發送WM_VSCROLL或WM_HSCROLL訊息,這兩個訊息中,很自然有wParam和lParam兩 個訊息參數。對於作為視窗的一部分而建立的捲軸,可以忽略lParam參數,因為lParam只有當捲軸在子視窗中才有意義(通常在一個對話方塊內)。

OK, 來看wParam。wParam的低16位是一個數值,表示當前滑鼠對捲軸進行的操作-也就是一個通知碼。捲軸的通知碼有這麼一些(定義在WINUSER.H中):

附件2

一 般情況下,我們可以忽略SB_ENDSCROLL的通知碼,因為我們會在相應的捲軸通知碼中已經設定了捲軸的當前位置,不需要在 SB_ENDSCROLL的時候再處理了。這裡面有意思的是SB_THUMBTRACK和SB_THUMBPOSITION,這兩個其實就是我們點住滾動 條的方塊進行操作的時候發送的通知碼。如果我們處理SB_THUMBTRACK,那麼很明顯,我們要不停的對視窗進行重繪,因為使用者只要拖一下捲軸的方 塊,我們就會收到這個通知,如果我們處理SB_THUMBPOSITION,那隻有在放開了捲軸方塊的時候我們才會處理,此時的效果就是我們在拖動滾動 條的時候,看不到任何反應,只有當放開捲軸方塊的時候,顯示地區才會重新整理。一般情況下,我們只需要處理這兩個通知碼中的一個就可以了。

除了上述的通知碼之外,WINUSER.H中還定義了SB_TOP,SB_BOTTOM,SB_LEFT和SB_RIGHT通知碼,指出捲軸已經被移到了它的最小或最大位置。然而,對於作為應用程式視窗一部分而建立的捲軸來說,永遠不會接收到這些通知碼。

13. 查看最後一個附件能看到上述的含捲軸的程式SysMets2,這個程式中,每單擊一下捲軸的兩端箭頭按鈕,每次滾動一行資訊,整個程式中,出於效能考 慮,我們沒有響應SB_THUMBTRACK,該而響應SB_THUMBPOSITION,而且調用了InvalidateRect函數,從而產生 WM_PAINT訊息,然後重繪了顯示地區,如果響應SB_THUMBTRACK,我們可以先調用InvalidateRect,產生無效矩形地區,然後 立刻調用UpdateWindow,使windows將WM_PAINT訊息不入隊,直接調用訊息處理函數中對WM_PAINT訊息的處理,從而實現顯示 地區內的內容快速重繪的目的。這個程式工作的不錯,但是回顧一下,這種使用捲軸的方法有這麼幾個問題:

(1) 在響應WM_VSCROLL, WM_HSCROLL訊息的時候,我們在wParam中取出捲軸當前的位置,但是發現沒有,這個wParam中只有16 bit用來表示捲軸的位置,這就限制了捲軸的最大設定範圍
(2) 這種樣子實現的捲軸,滾動方塊永遠是一個大小,滾動方塊不會根據我們能滾動的地區來動態改變大小,而目前的windows程式捲軸方塊都是能根據篇幅大小自動改變大小的。

14. 現在讓我們來看看更好的捲軸實現,能解決上述的問題。如果我們在MSDN中查看SetScrollRange, SetScrollPos, GetScrollRange, GetScrollPos函數,會被告知這些函數是過時的函數,其實不然,這些函數從windows 1.0開始就有了,而且在32位windows中升級成了32位的參數,但是的確有更好的捲軸處理函數,那就是現在說的“捲軸資訊函 數”--SetScrollInfo, GetScrollInfo。

這兩個函數可以完成上面那些函數的所有功能,並解決了上述的兩個問題。

SetScrollInfo (hwnd, iBar, &si, bRedraw) ;
GetScrollInfo (hwnd, iBar, &si) ;

這兩個函數的第三個參數變成了一個Struct,名為ScrollInfo:

Code: Select all
typedef struct tagSCROLLINFO
{
     UINT cbSize ;     // set to sizeof (SCROLLINFO)
     UINT fMask ;      // values to set or get
     int  nMin ;       // minimum range value
     int  nMax ;       // maximum range value
     UINT nPage ;      // page size
     int  nPos ;       // current position
     int  nTrackPos ;  // current tracking position
}
SCROLLINFO, * PSCROLLINFO ;

這就是關鍵所在了:

cbSize -- 一般設定成si.cbSize = sizeof(si); or si.cbSize = sizeof(SCROLLINFO); 以後會發現windows中很多struct都有這樣的欄位,這樣的欄位可以使將來的windows版本可以擴充該結構並添加新的功能,同時能和以前寫的 代碼相容。

fMask -- 這是關鍵。fMask是一個flag,定義成一堆以SIF開頭的常量,這些常量可以用 | 來組合。他們具體有:

在SetScrollInfo函數中,如果fMask設定成SIF_RANGE時,則必須把nMin和nMax欄位設定為所需的捲軸範圍。GetScrollInfo函數使用SIF_RANGE旗標時,nMin和nMax就是滾動範圍。

SIF_POS旗標也一樣。當通過SetScrollInfo使用它時,必須把結構的nPos欄位設定為所需的位置。可以通過GetScrollInfo使用SIF_POS旗標來取得目前位置。

使用SIF_PAGE旗標能夠取得頁面大小。用SetScrollInfo函數把nPage設定為所需的頁面大小。GetScrollInfo使用SIF_PAGE旗標可以取得目前頁面的大小。如果不想得到比例化的捲軸,就不要使用該旗標。

當處理帶有SB_THUMBTRACK或SB_THUMBPOSITION通知碼的WM_VSCROLL或WM_HSCROLL訊息時,只能調用 GetScrollInfo方法。此時fMask應設成SIF_TRACKPOS旗標。從函數的傳回中,SCROLLINFO結構的nTrackPos字 段將指出目前的32位的捲動方塊位置。

還有一個fMask是SIF_DISABLENOSCROLL旗標,只能給SetScrollInfo用,表示禁用捲軸。

SIF_ALL旗標是SIF_RANGE、SIF_POS、SIF_PAGE和SIF_TRACKPOS的組合。在WM_SIZE訊息處理期間設 置捲軸參數時,這是很方便的,這在處理捲軸訊息時也是很方便的。因為設定了SIF_ALL之後,ScrollInfo中各個欄位就都有值了。

所以,從上面可以看出,首先,牽涉到捲軸position和range的參數都是32位的了,沒有了上述16位的限制;其次,多出了一個 Page的東西,這個Page定義了一個Page(頁面)能顯示的範圍,這樣,Page和nMin,nMax結合起來,捲軸方塊就能根據這個比例來顯示 出不同的大小了。不過這裡也要注意,有點繞,比如我們設定nMin=0, nMax=75, nPage=50,那麼此時捲軸其實只有25個可滾動單元了哦,而不是75個哦,因為一屏(一個Page)就能顯示50條,那麼點25下就能來到最後一 行了哦!

15. OK,針對上面寫出的SysMets3,是個最完善的帶捲軸的程式了,將縱向和橫向捲軸都加上了。可以仔細看裡面的代碼,說幾點:

(1) 當nPage大於或等於nMax的時候,表明目前一屏足以顯示所有的資料了,此時windows會隱藏捲軸,如果不想隱藏,可以自己用SetScrollInfo設定SIF_DISABLENOSCROLL,此時捲軸將不能使用,而不會隱藏。

(2) 在響應WM_SIZE的時候,設定了捲軸的range和page,這是很自然的做法。對於page的設定,設成了當前顯示地區的高度除以一行字元的高度,表示一屏能顯示多少行字元。

(3) 在響應滾動訊息的時候,首先我們設定了新的捲軸位置,然後用將新的位置取出來,和滾動前的位置對比,如果發生了變化,那麼,調用 ScorllWindow,這是函數很複雜,目前已被ScrollWindowEx代替,用了這個函數,就不需要InvalidateRect了,因為這 個函數也會產生WM_PAINT,用這個函數表明當前滾動的大小,第二個參數是水平滾動的改變,第三個參數是垂直滾動的大小。後兩個NULL表示更新整個 顯示地區。

(4) 在垂直滾動方面,我們處理了SB_THUMBTRACK,在水平滾動方面,我們響應了SB_THUMBPOSITION。

(5) 在WM_PAINT中,我們根據無效地區的大小,選擇性的重繪了顯示地區,這樣帶來了更好的效能。

(6) 今後可以看看ScrollWindowEx,看做了哪些改進,目前的這個程式,滑鼠滾輪是無法滾動視窗的,換成ScrollWindowEx,是否就可以了呢?

緊接著上一貼的最後一個問題:

為什麼我們要用ScrollWindow, ScrollWindowEx而不是使用InvalidateRect來產生WM_PAINT呢?

網上搜來的一些回答:

ScrollWindowEx()執行的操作流程是先將位元影像塊在螢幕DC上移動(硬體操作),然後對新暴露出的地區發送WM_ERASEBKGND(可選)並對該視窗設定新的無效地區(你在WM_PAINT中可以得到這個地區)。

也就是說,windows首先通過圖形的方式幫我們把一些仍然需要顯示的東西搬到螢幕的適當位置,然後把新出現的需要我們重繪的部分設成無效矩形地區,然後我們在WM_PAINT中取得這個地區後,就只需要重繪整個地區了,的確高效很多啊。

如果用InvalidateRect,由於我們很難判斷滾動的地區然後做象素級的操作,所以我們只能將整個顯示地區設成無效,這樣WM_PAINT的時候就要重繪整個顯示地區,對於拖著捲軸方塊進行的滾動操作,這樣的重繪方式效能會非常低。

 

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.