4. 輸出文字

Post date: 2012/3/23 上午 05:36:19

4. 輸出文字

前一章 ,您看到了一個簡單的Windows 98程式,它在視窗中央,或者更準確地說,在顯示區域中央顯示一行文字。正如我們學到的,顯示區域是整個應用程式視窗中未被標題列、視窗邊框,以及可選的功能表列、工具列、狀態列和捲動列佔據的部分。簡而言之,顯示區域是視窗中可以由程式任意書寫和傳遞視覺資訊的部分。

對於程式的顯示區域,您幾乎可以為所欲為,只不過您不能假定視窗大小是某一特定尺寸,或者在程式執行時其大小會保持不變。如果您不熟悉圖形視窗環境的程式設計,這些限制可能會使您感到驚訝:不能再假設螢幕上的一行文字一定有80個字元了。您的程式必須與其他Windows程式共用視訊顯示器。Windows使用者控制程式視窗在螢幕上顯示的方式。儘管可以建立固定大小的視窗(這對於計算器之類的應用是合理的),但在大多數情況下,使用者應該能夠改變應用程式視窗的大小。您的程式必須能夠接受指定給它的大小,並且合理地利用這一空間。

這有兩種可能的情況。一種可能是,程式只有僅能顯示「hello」的顯示區域;還有另一種可能,即程式在一個大螢幕、高解析度的系統上執行,其顯示區域大得足以顯示兩整頁文字。靈活地處理這兩種極端是Windows程式設計的要點之一。

這一章,我們將講述程式在顯示區域顯示資訊的方式,但比 上一章 說明的顯示方式更加複雜。當程式在顯示區域顯示文字或圖形時,它經常要「繪製」它的顯示區域。本章著重講述繪製的方法。

儘管Windows為顯示圖形提供了強大的圖形裝置介面(GDI)函式,但在這一章中,我只介紹簡單文字行的顯示。我也將忽略Windows能夠使用的不同字體外形及字體大小,僅使用Windows的內定系統字體。這看起來似乎是一種限制,其實不然,本章涉及和解決的問題適用於所有Windows程式設計。在混合顯示文字和圖形時,Windows內定字體的字元大小通常決定了圖形的尺寸。

本章表面上是討論繪圖的方法,實際上是討論與裝置無關的程式設計基礎。Windows程式只能對顯示區域大小甚至字元的大小做很少的假定,相反地,必須使用Windows提供的功能來取得關於程式執行環境的資訊。

繪製和更新

在文字模式環境下,程式可以在顯示器的任意部分輸出,程式輸出到螢幕上的內容會停留在原處,不會神秘地消失。因此,程式可以丟掉重新生成螢幕顯示時所需的資訊。

在Windows中,只能在視窗的顯示區域繪製文字和圖形,而且不能確保在顯示區域內顯示的內容會一直保留到程式下一次有意地改寫它時還保留在那裡。例如,使用者可能會在螢幕上移動另一個程式的視窗,這樣就可能覆蓋您的應用程式視窗的一部分。Windows不會保存您的視窗中被其他程式覆蓋的區域,當程式移開後,Windows會要求您的程式更新顯示區域的這個部分。

Windows是一個訊息驅動系統。它通過把訊息投入應用程式訊息佇列中或者把訊息發送給合適的視窗訊息處理程式,將發生的各種事件通知給應用程式。Windows通過發送WM_PAINT訊息通知視窗訊息處理程式,視窗的部分顯示區域需要繪製。

WM_PAINT訊息

大多數Windows程式在WinMain中進入訊息迴圈之前的初始化期間都要呼叫函式UpdateWindow。Windows利用這個機會給視窗訊息處理程式發送第一個WM_PAINT訊息。這個訊息通知視窗訊息處理程式:必須繪製顯示區域。此後,視窗訊息處理程式應在任何時刻都準備好處理其他WM_PAINT訊息,必要的話,甚至重新繪製視窗的整個顯示區域。在發生下面幾種事件之一時,視窗訊息處理程式會接收到一個WM_PAINT訊息:

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

在某些情況下,顯示區域的一部分被臨時覆蓋,Windows試圖保存一個顯示區域,並在以後恢復它,但這不一定能成功。在以下情況下,Windows可能發送WM_PAINT訊息:

  • Windows擦除覆蓋了部分視窗的對話方塊或訊息方塊。
  • 功能表下拉出來,然後被釋放。
  • 顯示工具提示訊息。
      • 在某些情況下,Windows總是保存它所覆蓋的顯示區域,然後恢復它。這些情況是:
  • 滑鼠游標穿越顯示區域。
  • 圖示拖過顯示區域。

處理WM_PAINT訊息要求程式寫作者改變自己向顯示器輸出的思維方式。程式應該組織成可以保留繪製顯示區域需要的所有資訊,並且僅當「回應要求」-即Windows給視窗訊息處理程式發送WM_PAINT訊息時才進行繪製。如果程式在其他時間需要更新其顯示區域,它可以強制Windows產生一個WM_PAINT訊息。這看來似乎是在螢幕上顯示內容的一種捨近求遠的方法。但您的程式結構可以從中受益。

有效矩形和無效矩形

儘管視窗訊息處理程式一旦接收到WM_PAINT訊息之後,就準備更新整個顯示區域,但它經常只需要更新一個較小的區域(最常見的是顯示區域中的矩形區域)。顯然,當對話方塊覆蓋了部分顯示區域時,情況即是如此。在擦除對話方塊之後,需要重畫的只是先前被對話方塊遮住的矩形區域。

這個區域稱為「無效區域」或「更新區域」。正是顯示區域內無效區域的存在,才會讓Windows將一個WM_PAINT訊息放在應用程式的訊息佇列中。只有在顯示區域的某一部分失效時,視窗才會接受WM_PAINT訊息。

Windows內部為每個視窗保存一個「繪圖資訊結構」,這個結構包含了包圍無效區域的最小矩形的座標以及其他資訊,這個矩形就叫做「無效矩形」,有時也稱為「無效區域」。如果在視窗訊息處理程式處理WM_PAINT訊息之前顯示區域中的另一個區域變為無效,則Windows計算出一個包圍兩個區域的新的無效區域(以及一個新的無效矩形),並將這種變化後的資訊放在繪製資訊結構中。Windows不會將多個WM_PAINT訊息都放在訊息佇列中。

視窗訊息處理程式可以通過呼叫InvalidateRect使顯示區域內的矩形無效。如果訊息佇列中已經包含一個WM_PAINT訊息,Windows將計算出新的無效矩形。否則,它將一個新的WM_PAINT訊息放入訊息佇列中。在接收到WM_PAINT訊息時,視窗訊息處理程式可以取得無效矩形的座標(我們馬上就會看到這一點)。通過呼叫GetUpdateRect,可以在任何時候取得這些座標。

在處理WM_PAINT訊息處理期間,視窗訊息處理程式在呼叫了BeginPaint之後,整個顯示區域即變為有效。程式也可以通過呼叫ValidateRect函式使顯示區域內的任意矩形區域變為有效。如果這呼叫具有令整個無效區域變為有效的效果,則目前佇列中的任何WM_PAINT訊息都將被刪除。

要在視窗的顯示區域繪圖,可以使用Windows的圖形裝置介面(GDI)函式。Windows提供了幾個GDI函式,用於將字串輸出到視窗的顯示區域內。我們已經在 上一章 看過DrawText函式,但是目前使用最為普遍的文字輸出函式是TextOut。該函式的格式如下:

TextOut向視窗的顯示區域寫入字串。psText參數是指向字串的指標,iLength是字串的長度。x和y參數定義了字串在顯示區域的開始位置(不久會講述關於它們的詳細情況)。hdc參數是「裝置內容代號」,它是GDI的重要部分。實際上,每個GDI函式都需要將這個代號作為函式的第一個參數。

裝置內容

讀者可能還記得,代號只不過是一個數值,Windows以它在內部使用物件。程式寫作者從Windows取得代號,然後在其他函式中使用該代號。裝置內容代號是GDI函式的視窗「通行證」,有了這種裝置內容代號,程式寫作者就能自如地在顯示區域上繪圖,使圖形如自己所願地變得好看或者難看。

裝置內容(簡稱為「DC」)實際上是GDI內部保存的資料結構。裝置內容與特定的顯示設備(如視訊顯示器或印表機)相關。對於視訊顯示器,裝置內容總是與顯示器上的特定視窗相關。

裝置內容中的有些值是圖形「屬性」,這些屬性定義了GDI繪圖函式工作的細節。例如,對於TextOut,裝置內容的屬性確定了文字的顏色、文字的背景色、x座標和y座標映射到視窗的顯示區域的方式,以及顯示文字時Windows使用的字體。

當程式需要繪圖時,它必須先取得裝置內容代號。在取得了該代號後,Windows用內定的屬性值填入內部裝置內容結構。在後面的章節中您會看到,可以通過呼叫不同的GDI函式改變這些預設值。利用其他的GDI函式可以取得這些屬性的目前值。當然,還有其他的GDI函式能夠在視窗的顯示區域真正地繪圖。

當程式在顯示區域繪圖完畢後,它必須釋放裝置內容代號。代號被程式釋放後就不再有效,且不能再被使用。程式必須在處理單個訊息處理期間取得和釋放代號。除了呼叫CreateDC(函式,在本章暫不講述)建立的裝置內容之外,程式不能在兩個訊息之間保存其他裝置內容代號。

Windows應用程式一般使用兩種方法來取得裝置內容代號,以備在螢幕上繪圖。

取得裝置內容代號:方法一

在處理WM_PAINT訊息時,使用這種方法。它涉及BeginPaint和EndPaint兩個函式,這兩個函式需要視窗代號(作為參數傳給視窗訊息處理程式)和PAINTSTRUCT結構的變數(在WINUSER.H表頭檔案中定義)的地址為參數。Windows程式寫作者通常把這一結構變數命名為ps並且在視窗訊息處理程式中定義它:

在處理WM_PAINT訊息時,視窗訊息處理程式首先呼叫BeginPaint。BeginPaint函式一般在準備繪製時導致無效區域的背景被擦除。該函式也填入ps結構的欄位。BeginPaint傳回的值是裝置內容代號,這一傳回值通常被保存在叫做hdc的變數中。它在視窗訊息處理程式中的定義如下:

HDC資料型態定義為32位元的無正負號整數。然後,程式就可以使用需要裝置內容代號的TextOut等GDI函式。呼叫EndPaint即可釋放裝置內容代號。

一般地,處理WM_PAINT訊息的形式如下:

GDI簡介

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

PAINTSTRUCT ps ;

HDC hdc ;

case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; 使用GDI函式 EndPaint (hwnd, &ps) ; return 0 ;

在處理WM_PAINT訊息時,必須成對地呼叫BeginPaint和EndPaint。如果視窗訊息處理程式不處理WM_PAINT訊息,則它必須將WM_PAINT訊息傳遞給Windows中DefWindowProc(內定視窗訊息處理程式)。DefWindowProc以下列代碼處理WM_PAINT訊息:

case WM_PAINT: BeginPaint (hwnd, &ps) ; EndPaint (hwnd, &ps) ; return 0 ;

這兩個BeginPaint和EndPaint呼叫之間中沒有任何敘述,僅僅使先前無效區域變為有效。但以下方法是錯誤的:

Windows將一個WM_PAINT訊息放到訊息佇列中,是因為顯示區域的一部分無效。如果不呼叫BeginPaint和EndPaint(或者ValidateRect),則Windows不會使該區域變為有效。相反,Windows將發送另一個WM_PAINT訊息,且一直發送下去。

繪圖資訊結構

前面提到過,Windows為每個視窗保存一個「繪圖資訊結構」,這就是PAINTSTRUCT,定義如下:

case WM_PAINT: return 0 ; // WRONG !!!

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

在程式呼叫BeginPaint時,Windows會適當填入該結構的各個欄位值。使用者程式只使用前三個欄位,其他欄位由Windows內部使用。hdc欄位是裝置內容代號。在舊版本的Windows中,BeginPaint的傳回值也曾是這個裝置內容代號。在大多數情況下, fErase被標誌為FALSE(0),這意味著Windows已經擦除了無效矩形的背景。這最早在BeginPaint函式中發生(如果要在視窗訊息處理程式中自己定義一些背景擦除行為,可以自行處理WM_ERASEBKGND訊息)。Windows使用WNDCLASS結構的hbrBackground欄位指定的畫刷來擦除背景,這個WNDCLASS結構是程式在WinMain初始化期間登錄視窗類別時使用的。許多Windows程式使用白色畫刷。以下敘述設定視窗類別結構欄位值:

不過,如果程式通過呼叫Windows函式InvalidateRect使顯示區域中的矩形失效,則該函式的最後一個參數會指定是否擦除背景。如果這個參數為FALSE(即0),則Windows將不會擦除背景,並且在呼叫完BeginPaint後PAINTSTRUCT結構的fErase欄位將為TRUE(非零)。

PAINTSTRUCT結構的rcPaint欄位是RECT型態的結構。您已經在第三章中看到,RECT結構定義了一個矩形,其四個欄位為left、top、right和bottom。PAINTSTRUCT結構的rcPaint欄位定義了無效矩形的邊界,如圖4-1所示。這些值均以圖素為單位,並相對於顯示區域的左上角。無效矩形是應該重畫的區域。

PAINTSTRUCT中的rcPaint矩形不僅是無效矩形,它還是一個「剪取」矩形。這意味著Windows將繪圖操作限制在剪取矩形內(更確切地說,如果無效矩形區域不為矩形,則Windows將繪圖操作限制在這個區域內)。

在處理WM_PAINT訊息時,為了在更新的矩形外繪圖,可以使用如下呼叫:

該呼叫在BeginPaint呼叫之前進行,它使整個顯示區域變為無效,並擦除背景。但是,如果最後一個參數等於FALSE,則不擦除背景,原有的東西將保留在原處。

通常這是Windows程式在無論何時收到WM_PAINT訊息而不考慮rcPaint結構的情況下簡單地重畫整個顯示區域最方便的方法。例如,如果在顯示區域的顯示輸出中包括了一個圓,但是只有圓的一部分落到了無效矩形中,它就使僅繪製圓的無效部分變得沒有意義。這需要畫整個圓。在您使用從BeginPaint傳回的裝置內容代號時,Windows不會繪製rcPaint矩形外的任何部分。

第三章的HELLOWIN程式 中,我們並不關心處理WM_PAINT訊息時的無效矩形。如果文字顯示區域恰巧在無效矩形內,則由DrawText恢復之。否則,在處理DrawText呼叫的某個時刻,Windows會確定它無須向顯示器上輸出。不過,這一決定需要時間。關心程式性能和速度的程式寫作者希望在處理WM_PAINT期間使用無效矩形範圍,以避免不必要的GDI呼叫。如果繪製時需要存取例如點陣圖這樣的磁片檔案,則這就顯得尤其重要。

取得裝置內容代號:方法二

雖然最好是在處理WM_PAINT訊息處理期間更新整個顯示區域,但是您也會發現在處理非WM_PAINT訊息處理期間繪製顯示區域的某個部分也是非常有用的。或者您需要將裝置內容代號用於其他目的,如取得裝置內容的資訊。

要得到視窗顯示區域的裝置內容代號,可以呼叫GetDC來取得代號,在使用完後呼叫ReleaseDC:

wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;

InvalidateRect (hwnd, NULL, TRUE) ;

圖4-1 無效矩形的邊界

hdc = GetDC (hwnd) ; 使用GDI函式 ReleaseDC (hwnd, hdc) ;

與BeginPaint和EndPaint一樣,GetDC和ReleaseDC函式必須成對地使用。如果在處理某訊息時呼叫GetDC,則必須在退出視窗訊息處理程式之前呼叫ReleaseDC。不要在一個訊息中呼叫GetDC卻在另一個訊息呼叫ReleaseDC。

與從BeginPaint傳回裝置內容代號不同,GetDC傳回的裝置內容代號具有一個剪取矩形,它等於整個顯示區域。可以在顯示區域的某一部分繪圖,而不只是在無效矩形上繪圖(如果確實存在無效矩形)。與BeginPaint不同,GetDC不會使任何無效區域變為有效。如果需要使整個顯示區域有效,可以呼叫

一般可以呼叫GetDC和ReleaseDC來對鍵盤訊息(如在字處理程式中)和滑鼠訊息(如在畫圖程式中)作出反應。此時,程式可以立刻根據使用者的鍵盤或滑鼠輸入來更新顯示區域,而不需要考慮為了視窗的無效區域而使用WM_PAINT訊息。不過,一旦確實收到了WM_PAINT訊息,程式就必須要收集足夠的資訊後才能更新顯示。

與GetDC相似的函式是GetWindowDC。GetDC傳回用於寫入視窗顯示區域的裝置內容代號,而GetWindowDC傳回寫入整個視窗的裝置內容代號。例如,您的程式可以使用從GetWindowDC傳回的裝置內容代號在視窗的標題列上寫入文字。然而,程式同樣也應該處理WM_NCPAINT (「非顯示區域繪製」)訊息。

TextOut:細節

TextOut是用於顯示文字的最常用的GDI函式。語法是:

以下將詳細地討論這個函式。

第一個參數是裝置內容代號,它既可以是GetDC的傳回值,也可以是在處理WM_PAINT訊息時BeginPaint的傳回值。

裝置內容的屬性控制了被顯示的字串的特徵。例如,裝置內容中有一個屬性指定文字顏色,內定顏色為黑色;內定裝置內容還定義了白色的背景。在程式向顯示器輸出文字時,Windows使用這個背景色來填入字元周圍的矩形空間(稱為「字元框」)。

該文字背景色與定義視窗類別時設置的背景並不相同。視窗類別中的背景是一個畫刷,它是一種純色或者非純色組成的畫刷,Windows用它來擦除顯示區域,它不是裝置內容結構的一部分。在定義視窗類別結構時,大多數Windows應用程式使用WHITE_BRUSH,以便內定裝置內容中的內定文字背景顏色與Windows用以擦除顯示區域背景的畫刷顏色相同。

psText參數是指向字串的指標,iLength是字串中字元的個數。如果psText指向Unicode字串,則字串中的位元組數就是iLength值的兩倍。字串中不能包含任何ASCII控制字元(如回車、換行、製表或退格),Windows會將這些控制字元顯示為實心塊。Text0ut不識別作為字串結束標誌的內容為零的位元組(對於Unicode,是一個短整數型態的0),而需要由nLength參數指明長度。

TextOut中的x和y定義顯示區域內字串的開始位置,x是水平位置,y是垂直位置。字串中第一個字元的左上角位於座標點(x,y)。在內定的裝置內容中,原點(x和y均為0的點)是顯示區域的左上角。如果在TextOut中將x和y設為0,則將從顯示區域左上角開始輸出字串。

當您閱讀GDI繪圖函式(例如TextOut)的文件時,就會發現傳遞給函式的座標常常被稱為「邏輯座標」。在 第五章 會詳細地解釋這種情況。現在請注意,Windows有許多「座標映射方式」,它們用來控制GDI函式指定的邏輯座標轉換為顯示器的實際圖素座標的方式。映射方式在裝置內容中定義,內定映射方式是MM_TEXT(使用WINGDI.H中定義的識別字)。在MM_TEXT映射方式下,邏輯單位與實際單位相同,都是圖素;x的值從左向右遞增,y的值從上向下遞增(參看圖4-2)。MM_TEXT座標系與Windows在PAINTSTRUCT結構中定義無效矩形時使用的座標系相同,這為我們帶來了很多方便(但是,其他映射方式並非如此)。

裝置內容也定義了一個剪裁區域。您已經看到,對於從GetDC取得的裝置內容代號,內定剪裁區域是整個顯示區域;而對於從BeginPaint取得的裝置內容代號,則為無效區域。Windows不會在剪裁區域之外的任何位置顯示字串。如果一個字元有一部分在剪裁區域外,則Windows將只顯示此區域內的那部分。要想將輸出寫到視窗的顯示區域之外不是那麼容易的,所以不用擔心會無意間出現這種事情。

系統字體

裝置內容還定義了在您呼叫TextOut顯示文字時Windows使用的字體。內定字體為「系統字體」,或用Windows表頭檔案中的識別字,即SYSTEM_FONT。系統字體是Windows用來在標題列、功能表和對話方塊中顯示字串的內定字體。

在Windows的早期版本中,系統字體是等寬(fixed-pitch)字體,這意味著所有字元均具有同樣的寬度,非常類似於打字機。然而,從Windows 3.0開始,系統字體成為一種變寬(variable-pitch)字體,這意味著不同的字元具有不同的大小,比如,「W」要比「i」寬。變寬字體比等寬字體好讀,這已經是公認的事實。不過,可以想見,這一轉變使很多原來的Windows程式碼不再適用,從而要求程式寫作者學習一些使用字體的新技術。

系統字體是一種「點陣字體」,這意味著字元被定義為圖素塊(在 第十七章 ,將討論TrueType字體,它是由輪廓定義的)。至於確切的大小,系統字體的字元大小取決於視訊顯示器的大小。系統字體設計為至少能在顯示器上顯示25行80列文字。

字元大小

要用TextOut顯示多行文字,就必須確定字體的字元大小,可以根據字元的高度來定位字元的後續行,以及根據字元的寬度來定位字元的後續列。

系統字體的字元高度和平均寬度是多少?這個問題取決於視訊顯示器的圖素大小。Windows需要的最小顯示大小是640×480,但是許多使用者更喜歡800×600或1024×768的顯示大小。另外,對於這些較大的顯示尺寸,Windows允許使用者選擇不同大小的系統字體。

程式可以呼叫GetSystemMetrics函式以取使用者介面上各類視覺元件大小的資訊,呼叫GetTextMetrics取得字體大小。GetTextMetrics傳回裝置內容中目前選取的字體資訊,因此它需要裝置內容代號。Windows將文字大小的不同值複製到在WINGDI.H中定義的TEXTMETRIC型態的結構中。TEXTMETRIC結構有20個欄位,我們只使用前七個:

ValidateRect (hwnd, NULL) ;

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

圖4-2 MM_TEXT映射方式下的x座標和y座標

typedef struct tagTEXTMETRIC { LONG tmHeight ; LONG tmAscent ; LONG tmDescent ; LONG tmInternalLeading ; LONG tmExternalLeading ; LONG tmAveCharWidth ; LONG tmMaxCharWidth ; 其他結構欄位 } TEXTMETRIC, * PTEXTMETRIC ;

這些欄位值的單位取決於選定的裝置內容映射方式。在內定裝置內容下,映射方式是MM_TEXT,因此值的大小是以圖素為單位。

要使用GetTextMetrics函式,需要先定義一個結構變數(通常稱為tm):

在需要確定文字大小時,先取得裝置內容代號,再呼叫GetTextMetrics:

TEXTMETRIC tm ;

hdc = GetDC (hwnd) ; GetTextMetrics (hdc, &tm) ; ReleaseDC (hwnd, hdc) ;

此後,您就可以查看文字尺寸結構中的值,並有可能保存其中的一些以備將來使用。

文字大小:細節

TEXTMETRIC結構提供了關於目前裝置內容中選用的字體的豐富資訊。但是,字體的縱向大小只由5個值確定,其中4個值如圖4-3所示。

最重要的值是tmHeight,它是tmAscent和tmDescent的和。這兩個值表示了基準線上下字元的最大縱向高度。「間距」(leading)指印表機在兩行文字間插入的空間。在TEXTMETRIC結構中,內部的間距包括在tmAscent中(因此也在tmHeight中),並且它經常是重音符號出現的地方。tmInternalLeading欄位可被設成0,在這種情況下,加重音的字母會稍稍縮短以便容納重音符號。

TEXTMETRIC結構還包括一個不包含在tmHeight值中的欄位tmExternalLeading。它是字體設計者建議加在橫向字元之間的空間大小。在安排文字行之間的空隙時,您可以接受設計者建議的值,也可以拒絕它。在系統字體中tmExternalLeading可以為0,因此我沒有在圖4-3中顯示它。(儘管我不想告訴你們,圖4-3確實就是Windows在640×480的顯示解析度中使用的系統字體。)

TEXTMETRICS結構包含有描述字元寬度的兩個欄位,即tmAveCharWidth(小寫字母加權平均寬度)和tmMaxCharWidth(字體中最寬字元的寬度)。對於定寬字體,這兩個值是相等的(圖4-3中這些值分別為7和14)。

本章的範例程式還需要另一種字元寬度,即大寫字母的平均寬度,這可以用tmAveCharWidth乘以150%大致計算出來。

必須認識到,系統字體的大小取決於Windows所執行的視訊顯示器的解析度,在某些情況下,取決於使用者選取的系統字體的大小。Windows提供了一個與裝置無關的圖形介面,但程式寫作者還是有事情要處理的。不要想當然耳地猜測字體大小來寫作Windows程式,也不要把值定死,您可以使用GetTextMetrics函式取得這一資訊。

格式化文字

Windows啟動後,系統字體的大小就不會發生改變,所以在程式執行過程中,程式寫作者只需要呼叫一次GetTexMetrics。最好是在視窗訊息處理程式中處理WM_CREATE訊息時進行此呼叫,WM_CREATE訊息是視窗訊息處理程式接收的第一個訊息。在WinMain中呼叫CreateWindow時,Windows會以一個WM_CREATE訊息呼叫視窗訊息處理程式。

假設要編寫一個Windows程式,在顯示區域顯示幾行文字,這需要先取得字元寬度和高度。您可以在視窗訊息處理程式內定義兩個變數來保存平均字元寬度(cxChar)和總的字元高度(cyChar):

變數名的字首c代表「count」,在這裏指圖素數,與x和y結合,分別指寬和高。這些變數定義為static靜態變數,因為它們在視窗訊息處理程式中處理其他訊息(如WM_PAINT)時也應該是有效的。如果變數在函式外面定義,則不需要定義為static。

下面是取得系統字體的字元寬度和高度的WM_CREATE程式碼:

static int cxChar, cyChar ;

圖4-3 定義字體中縱向字元大小的4個值

case WM_CREATE: hdc = GetDC (hwnd) ; GetTextMetrics (hdc, &tm) ; cxChar = tm.tmAveCharWidth ; cyChar = tm.tmHeight + tm.tmExternalLeading ; ReleaseDC (hwnd, hdc) ; return 0 ;

注意我在計算cyChar時包括了tmExternalLeading欄位,雖然該欄位在系統字體中為0,但是因為它使得文字的可讀性更好,所以還是應該把它包括進去。沿著視窗向下每隔cyChar圖素就會顯示一行文字。

您會發現常常需要顯示格式化的數字跟簡單的字串。我在第二章講到過,您不能使慣用的工具(可愛的printf函式)來完成這項工作,但是可以使用sprintf和Windows版的sprintf-wsprintf。這些函式與printf相似,只是把格式化字串放到字串中。然後,可以用TextOut將字串輸出到顯示器上。非常方便的是,從sprintf和wsprintf傳回的值就是字串的長度。您可以將這個值傳遞給TextOut作為iLength參數。下面的程式碼顯示了wsprintf與TextOut的典型組合:

int iLength ; TCHAR szBuffer [40] ; 其他行程式 iLength = wsprintf (szBuffer, TEXT ("The sum of %i and %i is %i"), iA, iB, iA + iB) ; TextOut (hdc, x, y, szBuffer, iLength) ;

對於這樣簡單的情況,可以將nLength的定義值與TextOut放在同一條敘述中,從而無需定義iLength:

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

雖然這樣子寫起來不好看,但是功能與前者是一樣的。

綜合使用

現在,我們似乎已經具備了在螢幕上顯示多行文字所需要的所有知識。我們知道如何在WM_PAINT訊息處理期間取得一個裝置內容代號,如何使用TextOut函式以及如何根據字元大小來安排字距,剩下的就是顯示一點有意義的東西了。

前一章 裏,我們大概知道從Windows的GetSystemMetrics函式中取得的資訊是很有意義的,該函式傳回Windows中不同視覺元件的大小資訊,如圖示、游標、標題列和捲動列等。它們的大小因顯示卡和驅動程式的不同而有所不同。GetSystemMetrics是在程式中完成與裝置無關圖形輸出的重要函式。

該函式需要一個參數,叫做「索引」,在Windows表頭檔案定義了75個整數索引識別字(識別字的數量隨著每個版本的Windows的發佈而不斷地增加,在Windows 1.0的程式寫作者文件中僅列出了26個)。GetSystemMetrics傳回一個整數,這個整數通常就是參數中指定的圖形元件大小。

讓我們來編寫一個程式,顯示一些可以從GetSystemMetrics呼叫中取得的資訊,顯示格式為每種視覺元件一行。如果我們建立一個表頭檔案,在表頭檔案中定義一個結構陣列,此結構包含GetSystemMetrics索引對應的Windows表頭檔案識別字和呼叫所傳回的每個值對應的字串,這樣處理起來要容易一些。表頭檔案名為SYSMETS.H,如程式4-1所示。

程式4-1 SYSMETS.H /*--------------------------------------------------------- SYSMETS.H -- System metrics display structure -----------------------------------------------------------*/ #define NUMLINES ((int) (sizeof sysmetrics / sizeof sysmetrics [0])) struct { int Index ; TCHAR * szLabel ; TCHAR * szDesc ; } sysmetrics [] = { SM_CXSCREEN, TEXT ("SM_CXSCREEN"), TEXT ("Screen width in pixels"), SM_CYSCREEN, TEXT ("SM_CYSCREEN"), TEXT ("Screen height in pixels"), SM_CXVSCROLL, TEXT ("SM_CXVSCROLL"), TEXT ("Vertical scroll width"), SM_CYHSCROLL, TEXT ("SM_CYHSCROLL"), TEXT ("Horizontal scroll height"), SM_CYCAPTION, TEXT ("SM_CYCAPTION"), TEXT ("Caption bar height"), SM_CXBORDER, TEXT ("SM_CXBORDER"), TEXT ("Window border width"), SM_CYBORDER, TEXT ("SM_CYBORDER"), TEXT ("Window border height"), SM_CXFIXEDFRAME,TEXT ("SM_CXFIXEDFRAME"), TEXT ("Dialog window frame width"), SM_CYFIXEDFRAME,TEXT ("SM_CYFIXEDFRAME"), TEXT ("Dialog window frame height"), SM_CYVTHUMB, TEXT ("SM_CYVTHUMB"), TEXT ("Vertical scroll thumb height"), SM_CXHTHUMB, TEXT ("SM_CXHTHUMB"), TEXT ("Horizontal scroll thumb width"), SM_CXICON, TEXT ("SM_CXICON"), TEXT ("Icon width"), SM_CYICON, TEXT ("SM_CYICON"), TEXT ("Icon height"), SM_CXCURSOR, TEXT ("SM_CXCURSOR"), TEXT ("Cursor width"), SM_CYCURSOR, TEXT ("SM_CYCURSOR"), TEXT ("Cursor height"), SM_CYMENU, TEXT ("SM_CYMENU"), TEXT ("Menu bar height"), SM_CXFULLSCREEN,TEXT ("SM_CXFULLSCREEN"), TEXT ("Full screen client area width"), SM_CYFULLSCREEN,TEXT ("SM_CYFULLSCREEN"), TEXT ("Full screen client area height"), SM_CYKANJIWINDOW,TEXT ("SM_CYKANJIWINDOW"), TEXT ("Kanji window height"), SM_MOUSEPRESENT, TEXT ("SM_MOUSEPRESENT"), TEXT ("Mouse present flag"), SM_CYVSCROLL, TEXT ("SM_CYVSCROLL"), TEXT ("Vertical scroll arrow height"), SM_CXHSCROLL, TEXT ("SM_CXHSCROLL"), TEXT ("Horizontal scroll arrow width"), SM_DEBUG, TEXT ("SM_DEBUG"), TEXT ("Debug version flag"), SM_SWAPBUTTON, TEXT ("SM_SWAPBUTTON"), TEXT ("Mouse buttons swapped flag"), SM_CXMIN, TEXT ("SM_CXMIN"), TEXT ("Minimum window width"), SM_CYMIN, TEXT ("SM_CYMIN"), TEXT ("Minimum window height"), SM_CXSIZE, TEXT ("SM_CXSIZE"), TEXT ("Min/Max/Close button width"), SM_CYSIZE, TEXT ("SM_CYSIZE"), TEXT ("Min/Max/Close button height"), SM_CXSIZEFRAME, TEXT ("SM_CXSIZEFRAME"), TEXT ("Window sizing frame width"), SM_CYSIZEFRAME, TEXT ("SM_CYSIZEFRAME"), TEXT ("Window sizing frame height"), SM_CXMINTRACK, TEXT ("SM_CXMINTRACK"), TEXT ("Minimum window tracking width"), SM_CYMINTRACK, TEXT ("SM_CYMINTRACK"), TEXT ("Minimum window tracking height"), SM_CXDOUBLECLK, TEXT ("SM_CXDOUBLECLK"), TEXT ("Double click x tolerance"), SM_CYDOUBLECLK, TEXT ("SM_CYDOUBLECLK"), TEXT ("Double click y tolerance"), SM_CXICONSPACING,TEXT ("SM_CXICONSPACING"), TEXT ("Horizontal icon spacing"), SM_CYICONSPACING,TEXT ("SM_CYICONSPACING"), TEXT ("Vertical icon spacing"), SM_MENUDROPALIGNMENT, TEXT ("SM_MENUDROPALIGNMENT"), TEXT ("Left or right menu drop"), SM_PENWINDOWS, TEXT ("SM_PENWINDOWS"), TEXT ("Pen extensions installed"), SM_DBCSENABLED, TEXT ("SM_DBCSENABLED"), TEXT ("Double-Byte Char Set enabled"), SM_CMOUSEBUTTONS, TEXT ("SM_CMOUSEBUTTONS"), TEXT ("Number of mouse buttons"), SM_SECURE, TEXT ("SM_SECURE"), TEXT ("Security present flag"), SM_CXEDGE, TEXT ("SM_CXEDGE"), TEXT ("3-D border width"), SM_CYEDGE, TEXT ("SM_CYEDGE"), TEXT ("3-D border height"), SM_CXMINSPACING, TEXT ("SM_CXMINSPACING"), TEXT ("Minimized window spacing width"), SM_CYMINSPACING, TEXT ("SM_CYMINSPACING"), TEXT ("Minimized window spacing height"), SM_CXSMICON, TEXT ("SM_CXSMICON"), TEXT ("Small icon width"), SM_CYSMICON, TEXT ("SM_CYSMICON"), TEXT ("Small icon height"), SM_CYSMCAPTION, TEXT ("SM_CYSMCAPTION"), TEXT ("Small caption height"), SM_CXSMSIZE, TEXT ("SM_CXSMSIZE"), TEXT ("Small caption button width"), SM_CYSMSIZE, TEXT ("SM_CYSMSIZE"), TEXT ("Small caption button height"), SM_CXMENUSIZE, TEXT ("SM_CXMENUSIZE"), TEXT ("Menu bar button width"), SM_CYMENUSIZE, TEXT ("SM_CYMENUSIZE"), TEXT ("Menu bar button height"), SM_ARRANGE, TEXT ("SM_ARRANGE"), TEXT ("How minimized windows arranged"), SM_CXMINIMIZED, TEXT ("SM_CXMINIMIZED"), TEXT ("Minimized window width"), SM_CYMINIMIZED, TEXT ("SM_CYMINIMIZED"), TEXT ("Minimized window height"), SM_CXMAXTRACK, TEXT ("SM_CXMAXTRACK"), TEXT ("Maximum draggable width"), SM_CYMAXTRACK, TEXT ("SM_CYMAXTRACK"), TEXT ("Maximum draggable height"), SM_CXMAXIMIZED, TEXT ("SM_CXMAXIMIZED"), TEXT ("Width of maximized window"), SM_CYMAXIMIZED, TEXT ("SM_CYMAXIMIZED"), TEXT ("Height of maximized window"), SM_NETWORK, TEXT ("SM_NETWORK"), TEXT ("Network present flag"), SM_CLEANBOOT, TEXT ("SM_CLEANBOOT"), TEXT ("How system was booted"), SM_CXDRAG, TEXT ("SM_CXDRAG"), TEXT ("Avoid drag x tolerance"), SM_CYDRAG, TEXT ("SM_CYDRAG"), TEXT ("Avoid drag y tolerance"), SM_SHOWSOUNDS, TEXT ("SM_SHOWSOUNDS"), TEXT ("Present sounds visually"), SM_CXMENUCHECK, TEXT ("SM_CXMENUCHECK"), TEXT ("Menu check-mark width"), SM_CYMENUCHECK, TEXT ("SM_CYMENUCHECK"), TEXT ("Menu check-mark height"), SM_SLOWMACHINE, TEXT ("SM_SLOWMACHINE"), TEXT ("Slow processor flag"), SM_MIDEASTENABLED, TEXT ("SM_MIDEASTENABLED"), TEXT ("Hebrew and Arabic enabled flag"), SM_MOUSEWHEELPRESENT, TEXT ("SM_MOUSEWHEELPRESENT"), TEXT ("Mouse wheel present flag"), SM_XVIRTUALSCREEN, TEXT ("SM_XVIRTUALSCREEN"), TEXT ("Virtual screen x origin"), SM_YVIRTUALSCREEN, TEXT ("SM_YVIRTUALSCREEN"), TEXT ("Virtual screen y origin"), SM_CXVIRTUALSCREEN, TEXT ("SM_CXVIRTUALSCREEN"), TEXT ("Virtual screen width"), SM_CYVIRTUALSCREEN, TEXT ("SM_CYVIRTUALSCREEN"), TEXT ("Virtual screen height"), SM_CMONITORS, TEXT ("SM_CMONITORS"), TEXT ("Number of monitors"), SM_SAMEDISPLAYFORMAT, TEXT ("SM_SAMEDISPLAYFORMAT"), TEXT ("Same color format flag") } ;

顯示資訊的程式命名為SYSMETS1。SYSMETS1.C的原始碼如程式4-2所示。現在大多數程式碼看起來都很熟悉。WinMain中的程式碼實際上與HELLOWIN中的程式碼相同,並且WndProc中的大部分程式碼都已經討論過了。

程式4-2 SYSMETS1.C /*------------------------------------------------------------------ SYSMETS1.C -- System Metrics Display Program No. 1 (c) Charles Petzold, 1998 ----------------------------------------------------------------*/ #include <windows.h> #include "sysmets.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("SysMets1") ; 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 ("Get System Metrics No. 1"), 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 int cxChar, cxCaps, cyChar ; HDC hdc ; int i ; PAINTSTRUCT ps ; TCHAR szBuffer [10] ; TEXTMETRIC tm ; switch (message) { case WM_CREATE: hdc = GetDC (hwnd) ; GetTextMetrics (hdc, &tm) ; cxChar = tm.tmAveCharWidth ; cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2 ; cyChar = tm.tmHeight + tm.tmExternalLeading ; ReleaseDC (hwnd, hdc) ; return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; for (i = 0 ; i < NUMLINES ; i++) { TextOut (hdc, 0, cyChar * i, sysmetrics[i].szLabel, lstrlen (sysmetrics[i].szLabel)) ; TextOut (hdc, 22 * cxCaps, cyChar * i, sysmetrics[i].szDesc, lstrlen (sysmetrics[i].szDesc)) ; SetTextAlign (hdc, TA_RIGHT | TA_TOP) ; TextOut (hdc, 22 * cxCaps + 40 * cxChar, cyChar * i, szBuffer, wsprintf (szBuffer, TEXT ("%5d"), GetSystemMetrics (sysmetrics[i].iIndex))) ; SetTextAlign (hdc, TA_LEFT | TA_TOP) ; } EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY : PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

圖4-4顯示了在標準VGA上執行的SYSMETS1。在程式顯示區域的前兩行可以看到,螢幕寬度是640個圖素,螢幕高度是480個圖素,這兩個值以及程式所顯示的其他值可能會因視訊顯示器型態的不同而不同。

SYSMETS1.C視窗訊息處理程式

SYSMETS1.C程式中的WndProc視窗訊息處理程式處理三個訊息:WM_CREATE、WM_PAINT和WM_DESTROY。WM_DESTROY訊息的處理方法與 第三章的HELLOWIN程式 相同。

WM_CREATE訊息是視窗訊息處理程式接收到的第一個訊息。在CreateWindow函式建立視窗時,Windows產生這個訊息。在處理WM_CREATE訊息時,SYSMETS1呼叫GetDC取得視窗的裝置內容,並呼叫GetTextMetrics取得內定系統字體的文字大小。SYSMETS1將平均字元寬度保存在cxChar中,將字元的總高度(包括外部間距)保存在cyChar中。

SYSMETS1還將大寫字母的平均寬度保存在靜態變數cxCaps中。對於固定寬度的字體, cxCaps等於cxChar。對於可變寬度字體,cxCaps設定為cxChar乘以150%。對於可變寬度字體,TEXTMETRIC結構中的tmPitchAndFamily欄位的低位元為1,對於固定寬度字體,該值為0。 SYSMETS1使用這個位元從cxChar計算cxCaps:

SYSMETS1在處理WM_PAINT訊息處理期間完成所有視窗建立工作。通常,視窗訊息處理程式先呼叫BeginPaint取得裝置內容代號,然後用一道for敘述對SYSMETS.H中定義的sysmetrics結構的每一行進行迴圈。三列文字用三個TextOut函式顯示,對於每一列,TextOut的第三個參數都設定為:

這個參數指示了字串頂端相對於顯示區域頂部的圖素位置。

第一條TextOut敘述在第一列顯示了大寫識別字。TextOut的第二個參數是0,這是說文字從顯示區域的左邊緣開始。文字的內容來自sysmetrics結構的szLabel欄位。我使用Windows函式lstrlen來計算字串的長度,它是TextOut需要的最後一個參數。

第二條TextOut敘述顯示了對系統尺寸值的描述。這些描述存放在sysmetrics結構的szDesc欄位中。在這種情況下,TextOut的第二個參數設定為:

第一列顯示的最長的大寫識別字有20個字元,因此第二列必須在第一列文字開頭向右20 × cxCaps處開始。我使用22,以在兩列之間加一點多餘的空間。

第三條TextOut敘述顯示從GetSystemMetrics函式取得的數值。變寬字體使得格式化向右對齊的數值有些棘手。從0到9的數字具有相同的寬度,但是這個寬度比空格寬度大。數值可以比一個數字寬,所以不同的數值應該從不同的橫向位置開始。

那麼,如果我們指定字串結束的圖素位置,而不是指定字串的開始位置,以此向右對齊數值,是否會容易一些呢?用SetTextAlign函式就可以做到這一點。在SYSMETS1呼叫:

之後,傳給後續TextOut函式的座標將指定字串的右上角,而不是左上角。

顯示列數的TextOut函式的第二個參數設定為:

值40*cxChar包含了第二列的寬度和第三列的寬度。在TextOut函式之後,另一個對SetTextAlign的呼叫將對齊方式設定回普通方式,以進行下次迴圈。

空間不夠

在SYSMETS1程式中存在著一個很難處理的問題:除非您有一個大螢幕跟高解析度的顯示卡,否則就無法看到系統尺度列表的最後幾行。如果視窗太窄,甚至根本看不到值。

SYSMETS1不知道這個問題。否則我們就會顯示一個訊息方塊說「抱歉!」程式甚至不知道它的顯示區域有多大,它從視窗頂部開始輸出文字,並仰賴Windows裁剪超出顯示區域底部的內容。

顯然,這很不理想。為了解決這個問題,我們的第一個任務是確定程式在顯示區域內能輸出多少內容。

顯示區域的大小

如果您使用過現有的Windows應用程式,可能會發現視窗的尺寸變化極大。視窗最大化時(假定視窗只有標題列並且沒有功能表),顯示區域幾乎佔據了整個螢幕。這一最大化了的顯示區域的尺寸可以通過以SM_CXFULLSCREEN和SM_CYFULLSCREEN為參數呼叫GetSystemMetrics來獲得。視窗的最小尺寸可以很小,有時甚至不存在,更不用說顯示區域了。

在最近一章,我們使用GetClientRect函式來取得顯示區域的大小。使用這個函式沒有什麼不好,但是在您每次要使用資訊時就去呼叫它一遍是沒有效率的。確定視窗顯示區域大小的更好方法是在視窗訊息處理程式中處理WM_SIZE訊息。在視窗大小改變時,Windows給視窗訊息處理程式發送一個WM_SIZE訊息。傳給視窗訊息處理程式的lParam參數的低字組中包含顯示區域的寬度,高字組中包含顯示區域的高度。要保存這些尺寸,需要在視窗訊息處理程式中定義兩個靜態變數:

與cxChar和cyChar相似,這兩個變數在視窗訊息處理程式內定義為靜態變數,因為在以後處理其他訊息時會用到它們。處理WM_SIZE的方法如下:

cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2 ;

cyChar * i

22 * cxCaps

SetTextAlign (hdc, TA_RIGHT | TA_TOP) ;

22 * cxCaps + 40 * cxChar

static int cxClient, cyClient ;

圖4-4 SYSMETS1的顯示

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

實際上您會在每個Windows程式中看到類似的程式碼。LOWORD和HIWORD巨集在Windows表頭檔案WINDEF.H中定義。這些巨集的定義看起來像這樣:

這兩個巨集傳回WORD值(16位元的無正負號整數,範圍從0到0xFFFF)。一般,將這些值保存在32位元有號整數中。這就不會牽扯到任何轉換問題,並使得這些值在以後需要的任何計算中易於使用。

在許多Windows程式中,WM_SIZE訊息必然跟著一個WM_PAINT訊息。為什麼呢?因為在我們定義視窗類別時指定視窗類別樣式為:

這種視窗類別樣式告訴Windows,如果水平或者垂直大小發生改變, 則強制更新顯示區域。

用如下公式計算可以在顯示區域內顯示的文字的總行數:

如果顯示區域的高度太小以至無法顯示一個完整的字元,這個公式的結果可以為0。類似地,在顯示區域的水平方向可以顯示的小寫字元的近似數目為:

如果在處理WM_CREATE訊息處理期間取得cxChar和cyChar,則不用擔心在這兩個計算公式中會出現被0除的情況。在WinMain呼叫CreateWindow時,視窗訊息處理程式接收一個WM_CREATE訊息。在WinMain呼叫ShowWindow之後接收到第一個WM_CREATE訊息,此時cxChar和cyChar已經被賦予正的非零值了。

如果顯示區域的大小不足以容納所有的內容,那麼,知道視窗顯示區域的大小只是為使用者提供了在顯示區域內捲動文字的第一步。如果您對其他有類似需求的Windows應用程式很熟悉,就很可能知道,這種情況下,我們需要使用「捲動列」。

捲動列

捲動列是圖形使用者介面中最好的功能之一,它很容易使用,而且提供了很好的視覺回饋效果。您可以使用捲動列顯示任何東西--無論是文字、圖形、表格、資料庫記錄、圖像或是網頁,只要它所需的空間超出了視窗的顯示區域所能提供的空間,就可以使用捲動列。

捲動列既有垂直方向的(供上下移動),也有水平方向的(供左右移動)。使用者可以使用滑鼠在捲動列兩端的箭頭上或者在箭頭之間的區域中點一下,這時,「捲動方塊」在捲動列內的移動位置與所顯示的資訊在整個文件中的近似相關位置成比例。使用者也可以用滑鼠拖動捲動方塊到特定的位置。圖4-5顯示了垂直捲動列的建議用法。

有時,程式寫作者對捲動概念很難理解,因為他們的觀點與使用者的觀點不同:使用者向下捲動是想看到文件較下面的部分;但是,程式實際上是將文件相對於顯示視窗向上移動。Windows文件和表頭檔案識別字是依據使用者的觀點:向上捲動意味著朝文件的開頭移動;向下捲動意味著朝文件尾部移動。

很容易在應用程式中包含水平或者垂直的捲動列,程式寫作者只需要在CreateWindow的第三個參數中包括視窗樣式(WS)識別字WS_VSCROLL(垂直捲動)和/或WS_HSCROLL(水平捲動)即可。這些捲動列通常放在視窗的右部和底部,伸展為顯示區域的整個長度或寬度。顯示區域不包含捲動列所佔據的空間。對於特定的顯示驅動程式和顯示解析度,垂直捲動列的寬度和水平捲動列的高度是恒定的。如果需要這些值,可以使用GetSystemMetrics呼叫來取得(如前面的程式那樣)。

Windows負責處理對捲動列的所有滑鼠操作,但是,視窗捲動列沒有自動的鍵盤介面。如果想用游標鍵來完成捲動功能,則必須提供這方面的程式碼(我們將在下一章另一個版本的SYSMETS程式中做到這一點)。

捲動列的範圍和位置

每個捲動列均有一個相關的「範圍」(這是一對整數,分別代表最小值和最大值)和「位置」(它是捲動方塊在此範圍內的位置)。當捲動方塊在捲動列的頂部(或左部)時,捲動方塊的位置是範圍的最小值;在捲動列的底部(或右部)時,捲動方塊的位置是範圍的最大值。

在內定情況下,捲動列的範圍是從0(頂部或左部)至100(底部或右部),但將範圍改變為更方便於程式的數值也是很容易的:

參數iBar為SB_VERT或者SB_HORZ,iMin和iMax分別是範圍的最小值和最大值。如果想要Windows根據新範圍重畫捲動列,則設置bRedraw為TRUE(如果在呼叫SetScrollRange後,呼叫了影響捲動列位置的其他函式,則應該將bRedraw設定為FALSE以避免過多的重畫)。

捲動方塊的位置總是離散的整數值。例如,範圍為0至4的捲動列具有5個捲動方塊位置,如圖4-6所示。

您可以使用SetScrollPos在捲動列範圍內設置新的捲動方塊位置:

參數iPos是新位置,它必須在iMin至iMax的範圍內。Windows提供了類似的函式(GetScrollRange和GetScrollPos)來取得捲動列的目前範圍和位置。

在程式內使用捲動列時,程式寫作者與Windows共同負責維護捲動列以及更新捲動方塊的位置。下面是Windows對捲動列的處理:

#define LOWORD(l) ((WORD)(l)) #define HIWORD(l) ((WORD)(((DWORD)(l) >> 16) & 0xFFFF))

CS_HREDRAW | CS_VREDRAW

cyClient / cyChar

cxClient / cxChar

SetScrollRange (hwnd, iBar, iMin, iMax, bRedraw) ;

SetScrollPos (hwnd, iBar, iPos, bRedraw) ;

圖4-6 具有5個捲動方塊位置的捲動列

圖4-5 垂直捲動列

  • 處理所有捲動列滑鼠事件
  • 當使用者在捲動列內單擊滑鼠時,提供一種「反相顯示」的閃爍
  • 當使用者在捲動列內拖動捲動方塊時,移動捲動方塊
  • 為包含捲動列視窗的視窗訊息處理程式發送捲動列訊息

以下是程式寫作者應該完成的工作:

  • 初始化捲動列的範圍和位置
  • 處理視窗訊息處理程式的捲動列訊息
  • 更新捲動列內捲動方塊的位置
  • 更改顯示區域的內容以回應對捲動列的更改

像生活中的大多數事情一樣,在我們看一些程式碼時這些會顯得更加有意義。

捲動列訊息

在用滑鼠單擊捲動列或者拖動捲動方塊時,Windows給視窗訊息處理程式發送WM_VSCROLL(供上下移動)和WM_HSCROLL(供左右移動)訊息。在捲動列上的每個滑鼠動作都至少產生兩個訊息,一條在按下滑鼠按鈕時產生,一條在釋放按鈕時產生。

和所有的訊息一樣, WM_VSCROLL和WM_HSCROLL也帶有wParam和lParam訊息參數。對於來自作為視窗的一部分而建立的捲動列訊息,您可以忽略lParam;它只用于作為子視窗而建立的捲動列(通常在對話方塊內)。

wParam訊息參數被分為一個低字組和一個高字組。wParam的低字組是一個數值,它指出了滑鼠對捲動列進行的操作。這個數值被看作一個「通知碼」。通知碼的值由以SB(代表「scroll bar(捲動列)」)開頭的識別字定義。以下是在WINUSER.H中定義的通知碼:

#define SB_LINEUP 0 #define SB_LINELEFT 0 #define SB_LINEDOWN 1 #define SB_LINERIGHT 1 #define SB_PAGEUP 2 #define SB_PAGELEFT 2 #define SB_PAGEDOWN 3 #define SB_PAGERIGHT 3 #define SB_THUMBPOSITION 4 #define SB_THUMBTRACK 5 #define SB_TOP 6 #define SB_LEFT 6 #define SB_BOTTOM 7 #define SB_RIGHT 7 #define SB_ENDSCROLL 8

包含LEFT和RIGHT的識別字用於水平捲動列,包含UP、DOWN、TOP和BOTTOM的識別字用於垂直捲動列。滑鼠在捲動列的不同區域單擊所產生的通知碼如圖4-7所示。

如果在捲動列的各個部位按住滑鼠鍵,程式就能收到多個捲動列訊息。當釋放滑鼠鍵後,程式會收到一個帶有SB_ENDSCROLL通知碼的訊息。一般可以忽略這個訊息,Windows不會去改變捲動方塊的位置,而您可以在程式中呼叫SetScrollPos來改變捲動方塊的位置。

當把滑鼠的游標放在捲動方塊上並按住滑鼠鍵時,您就可以移動捲動方塊。這樣就產生了帶有SB_THUMBTRACK和SB_THUMBPOSITION通知碼的捲動列訊息。在wParam的低字組是SB_THUMBTRACK時,wParam的高字組是使用者在拖動捲動方塊時的目前位置。該位置位於捲動列範圍的最小值和最大值之間。在wParam的低字組是SB_THUMBPOSITION時,wParam的高字組是使用者釋放滑鼠鍵後捲動方塊的最終位置。對於其他的捲動列操作,wParam的高字組應該被忽略。

為了給使用者提供回饋,Windows在您用滑鼠拖動捲動方塊時移動它,同時您的程式會收到SB_THUMBTRACK訊息。然而,如果不通過呼叫SetScrollPos來處理SB_THUMBTRACK或SB_THUMBPOSITION訊息,在使用者釋放滑鼠鍵後,捲動方塊會迅速跳回原來的位置。

程式能夠處理SB_THUMBTRACK或SB_THUMBPOSITION訊息,但一般不同時處理兩者。如果處理SB_THUMBTRACK訊息,在使用者拖動捲動方塊時您需要移動顯示區域的內容。而如果處理SB_THUMBPOSITION訊息,則只需在使用者停止拖動捲動方塊時移動顯示區域的內容。處理SB_THUMBTRACK訊息更好一些(但更困難),對於某些型態的資料,您的程式可能很難跟上產生的訊息。

WINUSER.H表頭檔案還包括SB_TOP、SB_BOTTOM、SB_LEFT和SB_RIGHT通知碼,指出捲動列已經被移到了它的最小或最大位置。然而,對於作為應用程式視窗一部分而建立的捲動列來說,永遠不會接收到這些通知碼。

在捲動列範圍使用32位元的值也是有效的,儘管這不常見。然而,wParam的高字組只有16位元的大小,它不能適當地指出SB_THUMBTRACK和SB_THUMBPOSITION操作的位置。在這種情況下,需要使用GetScrollInfo函式(在下面描述)來得到資訊。

在SYSMETS中加入捲動功能

前面的說明已經很詳盡了,現在,要將那些東西動手做做看了。讓我們開始時簡單些,從垂直捲動著手,因為我們實在太需要垂直捲動了,而暫時還可以不用水平捲動。SYSMET2如程式4-3所示。這個程式可能是捲動列的最簡單的應用。

圖4-7 用於捲動列訊息的wParam值的識別字

程式4-3 SYSMETS2.C /*------------------------------------------------------------------ SYSMETS2.C -- System Metrics Display Program No. 2 (c) Charles Petzold, 1998 ------------------------------------------------------------------*/ #include <windows.h> #include "sysmets.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("SysMets2") ; 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 ("Get System Metrics No. 2"), WS_OVERLAPPEDWINDOW | WS_VSCROLL, 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 int cxChar, cxCaps, cyChar, cyClient, iVscrollPos ; HDC hdc ; int i, y ; PAINTSTRUCT ps ; TCHAR szBuffer[10] ; TEXTMETRIC tm ; switch (message) { case WM_CREATE: hdc = GetDC (hwnd) ; GetTextMetrics (hdc, &tm) ; cxChar = tm.tmAveCharWidth ; cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2 ; cyChar = tm.tmHeight + tm.tmExternalLeading ; ReleaseDC (hwnd, hdc) ; SetScrollRange (hwnd, SB_VERT, 0, NUMLINES - 1, FALSE) ; SetScrollPos (hwnd, SB_VERT, iVscrollPos, TRUE) ; return 0 ; case WM_SIZE: cyClient = HIWORD (lParam) ; return 0 ; case WM_VSCROLL: switch (LOWORD (wParam)) { case SB_LINEUP: iVscrollPos -= 1 ; break ; case SB_LINEDOWN: iVscrollPos += 1 ; break ; case SB_PAGEUP: iVscrollPos -= cyClient / cyChar ; break ; case SB_PAGEDOWN: iVscrollPos += cyClient / cyChar ; break ; case SB_THUMBPOSITION: iVscrollPos = HIWORD (wParam) ; break ; default : break ; } iVscrollPos = max (0, min (iVscrollPos, NUMLINES - 1)) ; if (iVscrollPos != GetScrollPos (hwnd, SB_VERT)) { SetScrollPos (hwnd, SB_VERT, iVscrollPos, TRUE) ; InvalidateRect (hwnd, NULL, TRUE) ; } return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; for (i = 0 ; i < NUMLINES ; i++) { y = cyChar * (i - iVscrollPos) ; TextOut (hdc, 0, y, sysmetrics[i].szLabel, lstrlen (sysmetrics[i].szLabel)) ; TextOut (hdc, 22 * cxCaps, y, sysmetrics[i].szDesc, lstrlen (sysmetrics[i].szDesc)) ; SetTextAlign (hdc, TA_RIGHT | TA_TOP) ; TextOut (hdc, 22 * cxCaps + 40 * cxChar, y, szBuffer, wsprintf (szBuffer, TEXT ("%5d"), GetSystemMetrics (sysmetrics[i].iIndex))) ; SetTextAlign (hdc, TA_LEFT | TA_TOP) ; } EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

新的CreateWindow呼叫在第三個參數中包含了WS_VSCROLL視窗樣式,從而在視窗中加入了垂直捲動列,其視窗樣式為:

WndProc視窗訊息處理程式在處理WM_CREATE訊息時增加了兩條敘述,以設置垂直捲動列的範圍和初始位置:

sysmetrics結構具有NUMLINES行文字,所以捲動列範圍被設定為0至NUMLINES-1。捲動列的每個位置對應于在顯示區域頂部顯示的一個文字行。如果捲動方塊的位置為0,則第一行會被放置在顯示區域的頂部。如果位置大於0,其他行就會出現在顯示區域的頂部。當位置為NUMLINES-1時,則最後一行文字出現在顯示區域的頂部。

為了有助於處理WM_VSCROLL訊息,在視窗訊息處理程式中定義了一個靜態變數iVscrollPos,這一變數是捲動列內捲動方塊的目前位置。對於SB_LINEUP和SB_LINEDOWN,只需要將捲動方塊調整一個單位的位置。對於SB_PAGEUP和SB_PAGEDOWN,我們想移動一整面的內容,或者移動cyClient /cyChar個單位的位置。對於SB_THUMBPOSITION,新的捲動方塊位置是wParam的高字組。SB_ENDSCROLL和SB_THUMBTRACK訊息被忽略。

在程式依據收到的WM_VSCROLL訊息計算出新的iVscrollPos值後,用min和max巨集來調整iVscrollPos,以確保它在最大值與最小值之間。程式然後將iVscrollPos與呼叫GetScrollPos取得的先前位置相比較,如果捲動位置發生了變化,則使用SetScrollPos來進行更新,並且呼叫InvalidateRect使整個視窗無效。

InvalidateRect呼叫產生一個WM_PAINT訊息。SYSMETS1在處理WM_PAINT訊息時,每一行的y座標計算公式為:

在SYSMETS2中,計算公式為:

迴圈仍然顯示NUMLINES行文字,但是對於非零值的iVscrollPos是負數。程式實際上在顯示區域以外顯示這些文字行。當然,Windows不會顯示這些行,因此螢幕顯得乾淨和漂亮。

前面說過,我們一開始不想弄得太複雜,這樣的程式碼很浪費,效率很低。下面我們對此加以修改,但是先要考慮在WM_VSCROLL訊息之後更新顯示區域的方法。

繪圖程式的組織

在處理完捲動列訊息後,SYSMETS2不更新顯示區域,相反,它呼叫InvalidateRect使顯示區域失效。這導致Windows將一個WM_PAINT訊息放入訊息佇列中。

最好能使Windows程式在回應WM_PAINT訊息時完成所有的顯示區域繪製功能。因為程式必須在一接收到WM_PAINT訊息時就更新整個顯示區域,如果在程式的其他部分也繪製的話,將很可能使程式碼重複。

首先,您可能對這種拐彎抹角的方式感到厭煩。在Windows的早期,因為這種方式與文字模式的程式設計差別太大,程式寫作者感到這種概念很難理解。並且,程式要不斷地通過馬上繪製畫面來回應鍵盤和滑鼠。這樣做既方便又有效,但是在很多情況下,這完全不必要。當您掌握了在回應WM_PAINT訊息時積累繪製顯示區域所需要的全部資訊的原則之後,會對這種結果感到滿意的。

如同SYSMETS2示範的,程式仍然需要在處理非WM_PAINT訊息時更新特定的顯示區域,使用InvalidateRect就很方便,您可以用它使顯示區域內的特定矩形或者整個顯示區域失效。

只將視窗顯示區域標記為無效以產生WM_PAINT訊息,對於某些應用程式來說也許不是完全令人滿意的選擇。在呼叫InvalidateRect之後,Windows將WM_PAINT訊息放入訊息佇列中,最後由視窗訊息處理程式處理它。然而,Windows將WM_PAINT訊息當成低優先順序訊息,如果系統有許多其他的動作正在發生,那麼也許會讓您等待一會兒工夫。這時,當對話方塊消失時,將會出現一些空白的「洞」,程式仍然等待更新它的視窗。

如果您希望立即更新無效區域,可以在呼叫InvalidateRect之後呼叫UpdateWindow:

如果顯示區域的任一部分無效,則UpdateWindow將導致Windows用WM_PAINT訊息呼叫視窗訊息處理程式(如果整個顯示區域有效,則不呼叫視窗訊息處理程式)。這一WM_PAINT訊息不進入訊息佇列,直接由Windows呼叫視窗訊息處理程式。視窗訊息處理程式完成更新後立即退出,Windows將控制傳回給程式中UpdateWindow呼叫之後的敘述。

您可能注意到,UpdateWindow與WinMain中用來產生第一個WM_PAINT訊息的函式相同。最初建立視窗時,整個顯示區域內容變為無效,UpdateWindow指示視窗訊息處理程式繪製顯示區域。

建立更好的滾動

SYSMETS2動作良好,但它只是模仿其他程式中的捲動列,並且效率很低。很快我將示範一個新的版本,改進它的不足。也許最有趣的是這個新版本不使用目前所討論的四個捲動列函式。相反,它將使用Win32 API中才有的新函式。

捲動列資訊函式

捲動列文件(在/Platform SDK/User Interface Services/Controls/Scroll Bars中)指出SetScrollRange、SetScrollPos、GetScrollRange和GetScrollPos函式是「過時的」,但這並不完全正確。這些函式在Windows 1.0中就出現了,在Win32 API中升級以處理32位元參數。它們仍然具有良好的功能。而且,它們不與Windows程式設計中新函式相衝突,這就是我在此書中仍使用它們的原因。

Win32 API介紹的兩個捲動列函式稱作SetScrollInfo和GetScrollInfo。這些函式可以完成以前函式的全部功能,並增加了兩個新特性。

第一個功能涉及捲動方塊的大小。您可能注意到,捲動方塊大小在SYSMETS2程式中是固定的。然而,在您可能使用到的一些Windows應用程式中,捲動方塊大小與在視窗中顯示的文件大小成比例。顯示的大小稱作「頁面大小」。演算法為:

可以使用SetScrollInfo來設置頁面大小(從而設置了捲動方塊的大小),如將要看到的SYSMETS3程式所示。

GetScrollInfo函式增加了第二個重要的功能,或者說它改進了目前API的不足。假設您要使用65,536或更大單位的範圍,這在16位元Windows中是不可能的。當然在Win32中,函式被定義為可接受32位元參數,因此是沒有問題的。(記住如果使用這樣大的範圍,捲動方塊的實際物理位置數仍然由捲動列的圖素大小限制)。然而,當使用SB_THUMBTRACK或SB_THUMBPOSITION通知碼得到WM_VSCROLL或WM_HSCROLL訊息時,只提供了16位元資料來指出捲動方塊的目前位置。通過GetScrollInfo函式可以取得真實的32位元值。

SetScrollInfo和GetScrollInfo函式的語法是

像在其他捲動列函式中那樣,iBar參數是SB_VERT或SB_HORZ,它還可以是用於捲動列控制的SB_CTL。SetScrollInfo的最後一個參數可以是TRUE或FALSE,指出了是否要Windows重新繪製計算了新資訊後的捲動列。

兩個函式的第三個參數是SCROLLINFO結構,定義為:

WS_OVERLAPPEDWINDOW | WS_VSCROLL

SetScrollRange (hwnd, SB_VERT, 0, NUMLINES - 1, FALSE) ; SetScrollPos (hwnd, SB_VERT, iVscrollPos, TRUE) ;

cyChar * i

cyChar * (i - iVscrollPos)

UpdateWindow (hwnd) ;

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

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 ;

在程式中,可以定義如下的SCROLLINFO結構型態:

在呼叫SetScrollInfo或GetScrollInfo之前,必須將cbSize欄位設定為結構的大小:

逐漸熟悉Windows後,您就會發現另外幾個結構像這個結構一樣,第一個欄位指出了結構大小。這個欄位使將來的Windows版本可以擴充結構並添加新的功能,並且仍然與以前編譯的版本相容。

把fMask欄位設定為一個以上以SIF字首開頭的旗標,並且可以使用C的位元操作OR運算子(|)組合這些旗標。

SetScrollInfo函式使用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只使用SIF_TRACKPOS旗標。從函式的傳回中,SCROLLINFO結構的nTrackPos欄位將指出目前的32位元的捲動方塊位置。

在SetScrollInfo函式中僅使用SIF_DISABLENOSCROLL旗標。如果指定了此旗標,而且新的捲動列參數使捲動列消失,則該捲動列就不能使用了(下面會有更多的解釋)。

SIF_ALL旗標是SIF_RANGE、SIF_POS、SIF_PAGE和SIF_TRACKPOS的組合。在WM_SIZE訊息處理期間設置捲動列參數時,這是很方便的(在SetScrollInfo函式中指定SIF_TRACKPOS後,它會被忽略)。這在處理捲動列訊息時也是很方便的。

捲動範圍

在SYSMETS2中,捲動範圍設置最小為0,最大為NUMLINES-1。當捲動列位置是0時,第一行資訊顯示在顯示區域的頂部;當捲動列的位置是NUMLINES-1時,最後一行顯示在顯示區域的頂部,並且看不見其他行。

可以說SYSMETS2捲動範圍太大。事實上只需把資訊最後一行顯示在顯示區域的底部而不是頂部即可。我們可以對SYSMETS2作出一些修改以達到此點。當處理WM_CREATE訊息時不設置捲動列範圍,而是等到接收到WM_SIZE訊息後再做此工作:

假定NUMLINES等於75,並假定特定視窗大小是:50(cyChar除以cyClient)。換句話說,我們有75行資訊但只有50行可以顯示在顯示區域中。使用上面的兩行程式碼,把範圍設置最小為0,最大為25。當捲動列位置等於0時,程式顯示0到49行。當捲動列位置等於1時,程式顯示1到50行;並且當捲動列位置等於25(最大值)時,程式顯示25到74行。很明顯需要對程式的其他部分做出修改,但這是可行的。

新捲動列函式的一個好的功能是當使用與捲動列範圍一樣大的頁面時,它已經為您做掉了一大堆雜事。可以像下面的程式碼一樣使用SCROLLINFO結構和SetScrollInfo:

SCROLLINFO si ;

si.cbSize = sizeof (si) ;

si.cbSize = sizeof (SCROLLINFO) ;

iVscrollMax = max (0, NUMLINES - cyClient / cyChar) ; SetScrollRange (hwnd, SB_VERT, 0, iVscrollMax, TRUE) ;

si.cbSize = sizeof (SCROLLINFO) ; si.cbMask = SIF_RANGE | SIF_PAGE ; si.nMin = 0 ; si.nMax = NUMLINES - 1 ; si.nPage = cyClient / cyChar ; SetScrollInfo (hwnd, SB_VERT, &si, TRUE) ;

這樣做之後,Windows會把最大的捲動列位置限制為si.nMax - si.nPage +1而不是si.nMax。像前面那樣做出假設:NUMLINES等於75 (所以si.nMax等於74),si.nPage等於50。這意味著最大的捲動列位置限制為74 - 50 + 1,即25。這正是我們想要的。

當頁面大小與捲動列範圍一樣大時,會發生什麼情況呢?在這個例子中,就是nPage等於75或更大的情況。Windows通常隱藏捲動列,因為它並不需要。如果不想隱藏捲動列,可在呼叫SetScrollInfo時使用SIF_DISABLENOSCROLL,Windows只是讓那個捲動列不能被使用,而不隱藏它。

新SYSMETS

SYSMETS3-此章中最後的SYSMETS程式版本-顯示在程式4-4中。此版本使用SetScrollInfo和GetScrollInfo函式,添加左右捲動的水平捲動列,並能更有效地重畫顯示區域。

程式4-4 SYSMETS3 SYSMETS3.C /*------------------------------------------------------------------ SYSMETS3.C -- System Metrics Display Program No. 3 (c) Charles Petzold, 1998 ----------------------------------------------------------------*/ #include <windows.h> #include "sysmets.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("SysMets3") ; 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 ("Program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("Get System Metrics No. 3"), WS_OVERLAPPEDWINDOW | WS_VSCROLL | WS_HSCROLL, 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 int cxChar, cxCaps, cyChar, cxClient, cyClient, iMaxWidth ; HDC hdc ; int i, x, y, iVertPos, iHorzPos, iPaintBeg, iPaintEnd ; PAINTSTRUCT ps ; SCROLLINFO si ; TCHAR szBuffer[10] ; TEXTMETRIC tm ; switch (message) { case WM_CREATE: hdc = GetDC (hwnd) ; GetTextMetrics (hdc, &tm) ; cxChar = tm.tmAveCharWidth ; cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2 ; cyChar = tm.tmHeight + tm.tmExternalLeading ; ReleaseDC (hwnd, hdc) ; // Save the width of the three columns iMaxWidth = 40 * cxChar + 22 * cxCaps ; return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; // Set vertical scroll bar range and page size si.cbSize = sizeof (si) ; si.fMask = SIF_RANGE | SIF_PAGE ; si.nMin = 0 ; si.nMax = NUMLINES - 1 ; si.nPage = cyClient / cyChar ; SetScrollInfo (hwnd, SB_VERT, &si, TRUE) ; // Set horizontal scroll bar range and page size si.cbSize = sizeof (si) ; si.fMask = SIF_RANGE | SIF_PAGE ; si.nMin = 0 ; si.nMax = 2 + iMaxWidth / cxChar ; si.nPage = cxClient / cxChar ; SetScrollInfo (hwnd, SB_HORZ, &si, TRUE) ; return 0 ; case WM_VSCROLL: // Get all the vertical scroll bar information si.cbSize = sizeof (si) ; si.fMask = SIF_ALL ; GetScrollInfo (hwnd, SB_VERT, &si) ; // Save the position for comparison later on iVertPos = si.nPos ; switch (LOWORD (wParam)) { case SB_TOP: si.nPos = si.nMin ; break ; case SB_BOTTOM: si.nPos = si.nMax ; break ; case SB_LINEUP: si.nPos - = 1 ; break ; case SB_LINEDOWN: si.nPos += 1 ; break ; case SB_PAGEUP: si.nPos -= si.nPage ; break ; case SB_PAGEDOWN: si.nPos += si.nPage ; break ; case SB_THUMBTRACK: si.nPos = si.nTrackPos ; break ; default: break ; } // Set the position and then retrieve it. Due to adjustments // by Windows it may not be the same as the value set. si.fMask = SIF_POS ; SetScrollInfo (hwnd, SB_VERT, &si, TRUE) ; GetScrollInfo (hwnd, SB_VERT, &si) ; // If the position has changed, scroll the window and update it if (si.nPos != iVertPos) { ScrollWindow ( hwnd, 0, cyChar * (iVertPos - si.nPos), NULL, NULL) ; UpdateWindow (hwnd) ; } return 0 ; case WM_HSCROLL: // Get all the vertical scroll bar information si.cbSize = sizeof (si) ; si.fMask = SIF_ALL ; // Save the position for comparison later on GetScrollInfo (hwnd, SB_HORZ, &si) ; iHorzPos = si.nPos ; switch (LOWORD (wParam)) { case SB_LINELEFT: si.nPos -= 1 ; break ; case SB_LINERIGHT: si.nPos += 1 ; break ; case SB_PAGELEFT: si.nPos -= si.nPage ; break ; case SB_PAGERIGHT: si.nPos += si.nPage ; break ; case SB_THUMBPOSITION: si.nPos = si.nTrackPos ; break ; default : break ; } // Set the position and then retrieve it. Due to adjustments // by Windows it may not be the same as the value set. si.fMask = SIF_POS ; SetScrollInfo (hwnd, SB_HORZ, &si, TRUE) ; GetScrollInfo (hwnd, SB_HORZ, &si) ; // If the position has changed, scroll the window if (si.nPos != iHorzPos) { ScrollWindow ( hwnd, cxChar * (iHorzPos - si.nPos), 0, NULL, NULL) ; } return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; // Get vertical scroll bar position si.cbSize = sizeof (si) ; si.fMask = SIF_POS ; GetScrollInfo (hwnd, SB_VERT, &si) ; iVertPos = si.nPos ; // Get horizontal scroll bar position GetScrollInfo (hwnd, SB_HORZ, &si) ; iHorzPos = si.nPos ; // Find painting limits iPaintBeg = max (0, iVertPos + ps.rcPaint.top / cyChar) ; iPaintEnd = min ( NUMLINES - 1, iVertPos + ps.rcPaint.bottom / cyChar) ; for (i = iPaintBeg ; i <= iPaintEnd ; i++) { x = cxChar * (1 - iHorzPos) ; y = cyChar * (i - iVertPos) ; TextOut (hdc, x, y, sysmetrics[i].szLabel, lstrlen (sysmetrics[i].szLabel)) ; TextOut (hdc, x + 22 * cxCaps, y, sysmetrics[i].szDesc, lstrlen (sysmetrics[i].szDesc)) ; SetTextAlign (hdc, TA_RIGHT | TA_TOP) ; TextOut (hdc, x + 22 * cxCaps + 40 * cxChar, y, szBuffer, wsprintf (szBuffer, TEXT ("%5d"), GetSystemMetrics (sysmetrics[i].iIndex))) ; SetTextAlign (hdc, TA_LEFT | TA_TOP) ; } EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY : PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

這個版本的程式仰賴Windows保存捲動列資訊並做邊界檢查。在WM_VSCROLL和WM_HSCROLL處理的開始,它取得所有的捲動列資訊,根據通知碼調整位置,然後呼叫SetScrollInfo設置其位置。程式然後呼叫GetScrollInfo。如果該位置超出了SetScrollInfo呼叫的範圍,則由Windows來糾正該位置並且在GetScrollInfo呼叫中傳回正確的值。

SYSMETS3使用ScrollWindow函式在視窗的顯示區域中捲動資訊而不是重畫它。雖然該函式很複雜(在新版本的Windows中已被更複雜的ScrollWindowEx所替代),SYSMETS3仍以相當簡單的方式使用它。函式的第二個參數給出了水平捲動顯示區域的數值,第三個參數是垂直捲動顯示區域的數值,單位都是圖素。

ScrollWindow的最後兩個參數設定為NULL,這指出了要捲動整個顯示區域。Windows自動把顯示區域中未被捲動操作覆蓋的矩形設為無效。這會產生WM_PAINT訊息。再也不需要InvalidateRect了。注意ScrollWindow不是GDI函式,因為它不需裝置內容代號。它是少數幾個非GDI的Windows函式之一,它可以改變視窗的顯示區域外觀。很特殊但不方便,它是隨捲動列函式一起記載在文件中。

WM_HSCROLL處理攔截SB_THUMBPOSITION通知碼並忽略SB_THUMBTRACK。因而,如果使用者在水平捲動列上拖動捲動方塊,在使用者釋放滑鼠按鈕之前,程式不會水平捲動視窗的內容。

WM_VSCROLL的方法與之不同:程式攔截SB_THUMBTRACK訊息並忽略SB_THUMBPOSITION。因而,程式隨使用者在垂直捲動列上拖動捲動方塊而垂直地滾動內容。這種想法很好,但應注意:一旦使用者發現程式會立即回應拖動的捲動方塊,他們就會不斷地來回拖動捲動方塊。幸運的是現在的PC快得可以勝任這種嚴酷的測試。但是在較慢的機器上,可以考慮為GetSystemMetrics使用SB_SLOWMACHINE參數來替代這種處理。

加快WM_PAINT處理的一個方法由SYSMETS3展示:WM_PAINT處理程式確定無效區域中的文字行並僅僅重畫這些行。當然,程式碼複雜一些,但速度很快。

不用滑鼠怎麼辦

在Windows的早期,有大量的使用者不喜歡使用滑鼠,而且,Windows自身也不要求必須有滑鼠。雖然,沒有滑鼠的PC現在走上了單色顯示器和點陣印表機的沒落之路,但我仍然建議您編寫可以使用鍵盤來產生與滑鼠操作相同效果的程式,尤其對於像捲動列這樣的基本操作物件更是如此。因為我們的鍵盤有一組游標移動鍵,所以應該實作同樣的操作。

下一章 ,您將學習使用鍵盤和在SYSMETS3中增加鍵盤介面的方法。您可能會注意到,SYSMETS3似乎在通知碼等於SB_TOP和SB_BOTTOM時處理了WM_VSCROLL訊息。前面已經提到過,視窗訊息處理程式不從捲動列接收這些訊息,所以,目前這是多餘的程式碼。當我們在 下一章 再次回到這個程式時,您將會明白這樣做的原因。