9. 子視窗控制項

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

9. 子視窗控制項

回憶 第七章的CHECKER程式 。這些程式顯示了矩形網格。當您在一個矩形中按下滑鼠按鍵時,該程式就畫一個x;如果您再按一次滑鼠按鍵,那麼x就消失。雖然這個程式的CHECKER1和CHECKER2版本只使用一個主視窗,但CHECKER3版本卻為每個矩形使用一個子視窗。這些矩形由一個叫做ChildProc的獨立視窗訊息處理程式維護。

如果有必要,無論矩形是否被選中,都可以給ChildProc增加一種向其父視窗訊息處理程式(WndProc)發送訊息的手段。通過呼叫GetParent,子視窗訊息處理程式能確定其父視窗的視窗代號:

其中,hwnd是子視窗的視窗代號。它可以向其父視窗訊息處理程式發送訊息:

那麼message應該設定為什麼呢?您可以隨意地設定,數值大小可以與WM_USER相同或更大,這些數字代表和預先定義的WM_ 訊息不衝突的訊息。也許對這個訊息,子視窗可以將wParam設定為它的子視窗ID。如果在該子視窗單擊,那麼lParam可以被設為1;如果未在該子視窗上單擊,那麼lParam將被設為0。這是處理方式的一種選擇。

事實上,這是在建立一個「子視窗控制項」。當子視窗的狀態改變時,子視窗處理滑鼠和鍵盤訊息並通知父視窗。使用這種方法,子視窗就變成了其父視窗的高階輸入裝置。它將與自己在螢幕上的圖形外觀相應的處理,對使用者輸入的回應以及在發生重要的輸入事件時通知另一個視窗的方法給封裝起來。

雖然您可以建立自己的子視窗控制項,但是也可以利用一些預先定義的視窗類別(和視窗訊息處理程式)來建立標準的子視窗控制項,您一定在別的Windows程式中看到過這些控制項。這些控制項採用的形式有:按鈕、核取方塊、編輯方塊、清單方塊、下拉式清單方塊、字串標籤和捲動列。例如,如果想在您的試算表程式的某個角落放置一個標有「Recalculate」的按鈕,那麼您可以通過呼叫CreateWindow來建立這個按鈕。您不必擔心滑鼠操作、按鈕顯示操作或按下該按鈕時的自動閃爍操作,這些是由Windows內部完成的。您所要做的只是攔截WM_COMMAND訊息-當按鈕被按下時,它通過這一訊息通知您的視窗訊息處理程式。真的這樣簡單嗎?是的,一點也沒錯。

子視窗控制項在對話方塊中最常用。在 第十一章 中您將會看到,子視窗控制項的位置和尺寸,是在範例程式的資源描述敘述中的對話方塊模板裡定義的。但是,您也可以使用預先定義的,在普通視窗顯示區域裡的子視窗控制項。您可以呼叫一次CreateWindow來建立一個子視窗,並通過呼叫MoveWindow來調整子視窗的位置和尺寸。父視窗訊息處理程式向子視窗控制項發送訊息,子視窗控制項向父視窗訊息處理程式傳回訊息。

在建立普通視窗時,首先定義視窗類別,並使用RegisterClass將其註冊到Windows中,然後用CreateWindow命令依據該視窗類別建立一個普通視窗,從 第三章 開始,我們就是這麼做的。但是,當您使用預先定義的某個控制項時,不必為子視窗註冊視窗類別,視窗類別已經存在於Windows之中,並且有一個預先定義的名字。您只需在CreateWindow中把它們用作視窗類別參數。CreateWindow中的視窗樣式參數準確地定義了子視窗控制項的外形和功能。Windows內建了處理發送給依據這些視窗類別建立的子視窗訊息的視窗訊息處理程式。

直接在您的視窗上使用子視窗控制項完成某些任務,這些任務的層次低於在對話方塊中使用子視窗控制項所要求的層次。這裏,對話方塊管理器在您的程式和控制項之間增加一個隔離層。值得一提的,您可能會發現在您的視窗上建立的子視窗控制項,沒有利用Tab鍵或方向鍵將輸入焦點從一個控制項移動到另一個控制項的內部功能。子視窗控制項能夠獲得輸入焦點,但是獲得後,它將不能把輸入焦點傳回給父視窗。這就是本章要解決的問題。

Windows程式設計的文件在兩個地方討論了子視窗控制項:首先是,簡單的常用控制項,我們可以在/Platform SDK/User Interface Services/Controls的文件所描述的無數對話方塊中看到。這些子視窗包括按鈕(其中包括核取方塊的單選按鈕)、靜態控制項(例如文字標籤)、編輯方塊(您可以在此編輯一行或多行文字)、捲動列、清單方塊和下拉式清單方塊。除下拉式清單方塊以外,在Windows 1.0中就包括了這些控制項。這部分的Windows文件還包括Rich Text文字編輯控制項,它與編輯方塊相似,但還允許編輯不同字體與樣式的格式化文字,以及桌面應用工具列。

相對於「常用控制項」,還有一些神秘的特殊控制項。這些控制項在/Platform SDK/User Interface Services/Shell and Common Controls/Common Controls描述。本章不討論常用控制項,但它們將出現在本書的其他部分。在這部分的Windows文件中,很容易找到您想從別的Windows應用程式中應用到您自己的應用程式裡頭那些部分資訊。

按鈕類別

下面我們將通過叫做BTNLOOK(「button look」)的程式來開始介紹按鈕視窗類別,如程式9-1所示。BTNLOOK建立10個子視窗按鈕控制項,每個控制項對應一個標準的按鈕樣式,因此共有10種標準按鈕樣式。

hwndParent = GetParent (hwnd) ;

SendMessage (hwndParent, message, wParam, lParam) ;

程式9-1 BTNLOOK BTNLOOK.C /*-------------------------------------------------------------------------- BTNLOOK.C -- Button Look Program (c) Charles Petzold, 1998 ---------------------------------------------------------------------------*/ #include <windows.h> struct { int iStyle ; TCHAR * szText ; } button[] = { BS_PUSHBUTTON, TEXT ("PUSHBUTTON"), BS_DEFPUSHBUTTON, TEXT ("DEFPUSHBUTTON"), BS_CHECKBOX, TEXT ("CHECKBOX"), BS_AUTOCHECKBOX, TEXT ("AUTOCHECKBOX"), BS_RADIOBUTTON, TEXT ("RADIOBUTTON"), BS_3STATE, TEXT ("3STATE"), BS_AUTO3STATE, TEXT ("AUTO3STATE"), BS_GROUPBOX, TEXT ("GROUPBOX"), BS_AUTORADIOBUTTON, TEXT ("AUTORADIO"), BS_OWNERDRAW, TEXT ("OWNERDRAW") } ; #define NUM (sizeof button / sizeof button[0]) LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("BtnLook") ; 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 ("Button Look"), 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 HWND hwndButton[NUM] ; static RECT rect ; static TCHAR szTop[] = TEXT ("message wParam lParam"), szUnd[] = TEXT ("_______ ______ ______"), szFormat[] = TEXT ("%-16s%04X-%04X %04X-%04X"), szBuffer[50] ; static int cxChar, cyChar ; HDC hdc ; PAINTSTRUCT ps ; int i ; switch (message) { case WM_CREATE : cxChar = LOWORD (GetDialogBaseUnits ()) ; cyChar = HIWORD (GetDialogBaseUnits ()) ; for (i = 0 ; i < NUM ; i++) hwndButton[i] = CreateWindow ( TEXT("button"),button[i].szText, WS_CHILD | WS_VISIBLE | button[i].iStyle, cxChar, cyChar * (1 + 2 * i), 20 * cxChar, 7 * cyChar / 4, hwnd, (HMENU) i, ((LPCREATESTRUCT) lParam)->hInstance, NULL) ; return 0 ; case WM_SIZE : rect.left = 24 * cxChar ; rect.top = 2 * cyChar ; rect.right = LOWORD (lParam) ; rect.bottom = HIWORD (lParam) ; return 0 ; case WM_PAINT : InvalidateRect (hwnd, &rect, TRUE) ; hdc = BeginPaint (hwnd, &ps) ; SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ; SetBkMode (hdc, TRANSPARENT) ; TextOut (hdc, 24 * cxChar, cyChar, szTop, lstrlen (szTop)) ; TextOut (hdc, 24 * cxChar, cyChar, szUnd, lstrlen (szUnd)) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DRAWITEM : case WM_COMMAND : ScrollWindow (hwnd, 0, -cyChar, &rect, &rect) ; hdc = GetDC (hwnd) ; SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ; TextOut( hdc, 24 * cxChar, cyChar * (rect.bottom / cyChar - 1), szBuffer, wsprintf (szBuffer, szFormat, message == WM_DRAWITEM ? TEXT ("WM_DRAWITEM") : TEXT ("WM_COMMAND"), HIWORD (wParam), LOWORD (wParam), HIWORD (lParam), LOWORD (lParam))) ; ReleaseDC (hwnd, hdc) ; ValidateRect (hwnd, &rect) ; break ; case WM_DESTROY : PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

單擊按鈕時,按鈕就給父視窗訊息處理程式發送一個WM_COMMAND訊息,也就是我們所熟悉的WndProc。BTNLOOK的WndProc將該訊息的wParam參數和lParam參數顯示在顯示區域的右邊,如圖9-1所示。

具有BS_OWNERDRAW樣式的按鈕在視窗上顯示為一個背景陰影,因為這種樣式的按鈕是由程式來負責繪製的。該按鈕表示它需要由包含lParam訊息參數的WM_DRAWITEM訊息來繪製,而lParam訊息參數是一個指向DRAWITEMSTRUCT型態結構的指標。在BTNLOOK中,這些訊息也同樣被顯示。我將在本章的後面更詳細地討論這種擁有者繪製(owner draw)按鈕。

建立子視窗

BTNLOOK定義了一個叫做button的結構,它包括了按鈕視窗樣式和描述性字串,它們對應於10個按鈕型態,所有按鈕視窗樣式都以字母「BS」開頭,它表示「按鈕樣式」。10個按鈕子視窗是在WndProc中處理WM_CREATE訊息的過程中使用一個for迴圈建立的。CreateWindow呼叫使用下面這些參數:

圖9-1 BTNLOOK的螢幕顯示

Class name(類別名稱)

Window text(視窗文字)

Window style(視窗樣式)

x position(x位置)

y position(y位置)

Width(寬度)

Height(高度)

Parent window(父視窗)

Child window ID(子視窗ID)

Instance handle(執行實體代號)

Extra parameters(附加參數)

TEXT ("button")

button[i].szText

WS_CHILD | WS_VISIBLE | button[i].iStyle

cxChar

cyChar * (1 + 2 * i)

20 * xChar

7 * yChar / 4

hwnd

(HMENU) i

((LPCREATESTRUCT) lParam) -> hInstance

NULL

類別名稱參數是預先定義的名字。視窗樣式使用WS_CHILD、WS_VISIBLE以及在button結構中定義的10個按鈕樣式之一(BS_PUSHBUTTON、BS_DEFPUSHBUTTON等等)。視窗文字參數(對於普通視窗來說,它是顯示在標題列中的文字)將在每個按鈕上顯示出來。我簡單地使用標識按鈕樣式文字的x位置和y位置參數,說明子視窗左上角相對於父視窗顯示區域左上角的位置。寬度和高度參數規定了每個子視窗的寬度和高度。請注意,我用的是GetDialogBaseUnits函式來獲得內定字體字元的寬度和高度。這是對話方塊用來獲得文字尺寸的函式。此函式傳回一個32位元的值,其中低字組表示寬度,高字組表示高度。由於GetDialogBaseUnits傳回的值與從GetTextMetrics獲得的值大致上相同,但GetDialogBaseUnits有時使用起來會更方便些,而且能夠與對話方塊控制項更好地保持一致。

對每個子視窗,它的子視窗ID參數應該各不相同。在處理來自子視窗的WM_COMMAND訊息時,ID幫助您的視窗訊息處理程式識別出相應的子視窗。注意子視窗ID是作為CreateWindow的一個參數傳遞的,該參數通常用於指定程式的功能表,因此子視窗ID必須被強制轉換為HMENU。

CreateWindow呼叫的執行實體代號看起來有點奇怪,但是它利用了如下的事實,亦即在處理WM_CREATE訊息的過程中,lParam實際上是指向CREATESTRUCT (「建立結構」)結構的指標,該結構有一個hInstance成員。所以將lParam轉換成指向CREATESTRUCT結構的一個指標,並取出hInstance。

(有些Windows程式使用名為hInst的整體變數,使視窗訊息處理程式能存取WinMain中的執行實體代號。在WinMain中,您只需在建立主視窗之前設定:

第七章中的CHECKER3程式 中,我們曾用GetWindowLong取得執行實體代號:

這幾種方法都是正確的。)

在呼叫CreateWindow之後,我們不必再為這些子視窗做任何事情,由Windows中的按鈕視窗訊息處理程式負責維護它們,並處理所有的重畫工作(BS_OWNERDRAW樣式的按鈕例外,它要求程式繪製它,這些將在後面加以討論)。在程式終止時,如果父視窗已經被清除,那麼Windows將清除這些子視窗。

子視窗向父視窗發訊息

當您執行BTNLOOK時,將看到在顯示區域的左邊會顯示出不同的按鈕型態。我在前面已經提到過,用滑鼠單擊按鈕時,子視窗控制項就向其父視窗發送一個WM_COMMAND訊息。BTNLOOK攔截WM_COMMAND訊息並顯示wParam和lParam的值,它們的含義如下:

hInst = hInstance ;

GetWindowLong (hwnd, GWL_HINSTANCE)

LOWORD (wParam)

HIWORD (wParam)

lParam

子視窗ID

通知碼

子視窗代號

如果您正在移植16位元Windows程式,那麼要注意改變這些訊息參數以容納32位元的代號。

子視窗ID是在建立子視窗時傳遞給CreateWindow的值。在BTNLOOK中,這些ID被顯示在顯示區域中,並使用0到9分別標識10個按鈕。子視窗代號是Windows從CreateWindow傳回的值。

通知碼更詳細表示了訊息的含義。按鈕通知碼的可能值在Windows表頭檔案中定義如下:

表9-1

實際上,您不會看到這些按鈕值中的大多數。從1到4的通知碼是用於一種叫做BS_USERBUTTON的已不再使用的按鈕的(它已經由BS_OWNERDRAW和另一種不同的通知方式所替換)。通知碼6到7只有當按鈕樣式包括標識BS_NOTIFY才發送。通知碼5只對BS_RADIOBUTTON、BS_AUTORADIOBUTTON和BS_OWNERDRAW按鈕發送,或者當按鈕樣式中包括BS_NOTIFY時,也為其他按鈕發送。

您會注意到,在用滑鼠單擊按鈕時,該按鈕文字的周圍會有虛線。這表示該按鈕擁有了輸入焦點,所有鍵盤輸入都將傳送給子視窗按鈕控制項,而不是傳送給主視窗。但是,當該按鈕控制項擁有輸入焦點時,它將忽略所有的鍵盤輸入,除了Spacebar鍵例外,此時Spacebar鍵與滑鼠具有相同的效果。

父視窗向子視窗發送訊息

雖然BTNLOOK中沒有顯示這一事實,但是父視窗訊息處理程式也能向子視窗控制項發送訊息。這些訊息包括以字首WM開頭的許多訊息。另外,在WINUSER.H中還定義了8個按鈕說明訊息;字首BM表示「按鈕訊息」。這些按鈕訊息如下表所示:

表9-2

BM_GETCHECK和BM_SETCHECK訊息由父視窗發送給子視窗控制項,以取得或者設定核取方塊和單選按鈕的選中標記。BM_GETSTATE和BM_SETSTATE訊息表示按鈕處於正常狀態還是(滑鼠或Spacebar鍵按下時的)「按下」狀態。我們將在討論按鈕的每種型態時,看到這些訊息是如何起作用的。BM_SETSTYLE訊息允許您在按鈕建立之後改變按鈕樣式。

每個子視窗控制項都具有一個在其兄弟中唯一的視窗代號和ID值。對於代號和ID這兩者,知道其中的一個您就可以獲得另一個。如果您知道子視窗控制項的視窗代號,那麼您可以用下面的敘述來獲得ID:

第七章的CHECKER3程式 曾用此函式(與SetWindowLong一起)來維護註冊視窗類別時保留的特殊區域的資料。在建立子視窗時,Windows保留了GWL_ID識別字存取的資料。您也可以使用:

雖然函式中的「Dlg」部分指的是對話方塊,但實際上這是一個通用的函式。

知道ID和父視窗代號,您就能獲得子視窗代號:

按鍵

在BTNLOOK中顯示的前兩個按鈕是「壓入」按鈕。按鈕是一個矩形,包括了CreateWindow呼叫中視窗文字參數所指定的文字。該矩形佔用了在CreateWindow或者MoveWindow呼叫中給出的全部高度和寬度,而文字在矩形的中心。

按鍵控制項主要用來觸發一個立即回應的動作,而不保留任何形式的開/關指示。兩種型態的按鈕控制項有兩種視窗樣式,分別叫做BS_PUSHBUTTON和BS_DEFPUSHBUTTON,BS_DEFPUSHBUTTON中的「DEF」代表「內定」。當用來設計對話方塊時,BS_PUSHBUTTON控制項和BS_DEFPUSHBUTTON控制項的作用不同。但是當用作子視窗控制項時,兩種型態的按鈕作用相同,儘管BS_DEFPUSHBUTTON的邊框要粗一些。

當按鈕的高度為文字字元高度的7/4倍時,按鈕的外觀看起來最好,其中文字字元由BTNLOOK使用;而按鈕的寬度至少調節到文字的寬度再加上兩個字元的寬度。

當滑鼠游標在按鈕中時,按下滑鼠按鍵將使按鈕用三維陰影重畫自己,就好像真的被按下一樣。放開滑鼠按鍵時,就恢復按鈕的原貌,並向父視窗發送一個WM_COMMAND訊息和BN_CLICKED通知碼。與其他按鈕型態相似,當按鈕擁有輸入焦點時,在文字的周圍就有虛線,按下及釋放Spacebar鍵與按下及釋放滑鼠按鍵具有相同的效果。

您可以通過給視窗發送BM_SETSTATE訊息來模擬按鈕閃動。以下的操作將導致按鈕被按下:

下面的呼叫使按鈕恢復正常:

hwndButton視窗代號是從CreateWindow呼叫傳回的值。

您也可以向按鍵發送BM_GETSTATE訊息,子視窗控制項傳回按鈕目前的狀態:如果按鈕被按下,則傳回TRUE;如果按鈕處於正常狀態,則傳回FALSE。但是,絕大多數應用並不需要這一訊息。因為按鈕不保留任何開/關資訊,所以BM_SETCHECK訊息和BM_GETCHECK訊息不會被用到。

核取方塊

核取方塊是一個文字方塊,文字通常出現在核取方塊的右邊(如果您在建立按鈕時指定了BS_LEFTTEXT樣式,那麼文字會出現在左邊;您也許將用BS_RIGHT直接調整文字來組合此樣式)。核取方塊通常用於允許使用者對選項進行選擇的應用程式中。核取方塊的常用功能如同一個開關:單擊框一次將顯示勾選標記,再次單擊清除勾選標記。

核取方塊最常用的兩種樣式是BS_CHECKBOX和BS_AUTOCHECKBOX。在使用BS_CHECKBOX時,您需要自己向該控制項發送BM_SETCHECK訊息來設定勾選標記。wParam參數設1時設定勾選標記,設0時清除勾選標記。通過向該控制項發送BM_GETCHECK訊息,您可以得到該核取方塊的目前狀態。在處理來自控制項的WM_COMMAND訊息時,您可以用如下的指令來翻轉X標記:

注意第二個SendMessage呼叫前面的運算子「!」,其中lParam是在WM_COMMAND訊息中傳給使用者視窗訊息處理程式的子視窗代號。如果您以後又想知道按鈕的狀態,那麼可以向它發送另一條BM_GETCHECK訊息;您也可以將目前狀態儲存在您的視窗訊息處理程式中的一個靜態變數裏,或者向它發送BM_SETCHECK訊息來初始化帶勾選標記的BS_CHECKBOX核取方塊:

對BS_AUTOCHECKBOX樣式,按鈕自己觸發勾選標記的開和關,所以您的視窗訊息處理程式可以忽略WM_COMMAND訊息。當您需要按鈕目前的狀態時,可以向控制項發送BM_GETCHECK訊息:

如果該按鈕被選中,則iCheck的值為TRUE或者非零數;如果按鈕末被選中,則iCheck的值為FALSE或0。

其餘兩種核取方塊樣式是BS_3STATE和BS_AUTO3STATE,正如它們名字所暗示的,這兩種樣式能顯示第三種狀態-核取方塊內是灰色-它出現在向控制項發送wParam等於2的WM_SETCHECK訊息時。灰色是向使用者表示此框不能被選本章的或者禁止使用。

核取方塊沿矩形的左邊框對齊,並集中在呼叫CreateWindow時規定的矩形的頂邊和底邊之間,在該矩形內的任何地方按下滑鼠都會向其父視窗發送一個WM_COMMAND訊息。核取方塊的最小高度是一個字元的高度,最小寬度是文字中的字元數加2。

單選按鈕

單選按鈕的名稱在一列按鈕的後面,這些按鈕就像汽車上的收音機一樣。汽車收音機上的每一個按鈕都對應一種收音狀態,而且一次只能有一個按鈕被按下。在對話方塊中,單選按鈕組常常用來表示相互排斥的選項。與核取方塊不同,單選按鈕的工作與開關不一樣,也就是說,當第二次按單選按鈕時,它的狀態會保持不變。

單選按鈕的形狀是一個圓圈,而不是方框,除此之外,它非常像核取方塊。圓圈內的加重圓點表示該單選按鈕已經被選中。單選按鈕有視窗樣式BS_RADIOBUTTON或BS_AUTORADIOBUTTON兩種,但是後者只用於對話方塊。

當您收到來自單選按鈕的WM_COMMAND訊息時,應該向它發送wParam等於1的BM_SETCHECK訊息來顯示其選中狀態:

對同組中的其他所有單選按鈕,您可以通過向它們發送wParam等於0的BM_SETCHECK訊息來顯示其未選中狀態:

分組方塊

分組方塊即樣式為BS_GROUPBOX的選擇框,它是按鈕類中的特例,既不處理滑鼠輸入和鍵盤輸入,也不向其父視窗發送WM_COMMAND訊息。分組方塊是一個矩形框,分組方塊標題在其頂部顯示。分組方塊常用來包含其他的按鈕控制項。

改變按鈕文字

您可以通過SetWindowText來改變按鈕(或者其他任何視窗)內的文字:

其中hwnd是欲改變視窗的代號,pszString是一個指向以null為終結的字串指標。對於一般的視窗來說,這個文字是標題列的文字;對於按鈕控制項來說,它是隨著該按鈕顯示的文字。

您也可以取得視窗目前的文字:

iMaxLength指定複製到pszBuffer指向的緩衝區中的最大字元數。該函式傳回複製的字元數。您可以首先通過下面的呼叫來獲得特定文字的長度:

可見的和啟用的按鈕

為了接收滑鼠和鍵盤輸入,子視窗必須是可見的(被顯示)和被啟用的。當視窗是可見的而未被啟用時,那麼視窗將以灰色而非黑色顯示文字。

如果在建立子視窗時,您沒有將WS_VISIBLE包含在視窗類別中,那麼直到呼叫ShowWindow時子視窗才會被顯示出來:

如果您將WS_VISIBLE包含在視窗類別中,就沒有必要呼叫ShowWindow。但是,您可以通過呼叫ShowWindow將子視窗隱藏起來:

您可以通過下面的呼叫來確定子視窗是否可見:

您也可以使子視窗被啟用或者不被啟用。在內定情況下,視窗是被啟用的。您可以通過下面的呼叫使視窗不被啟用:

對於按鈕控制項,這具有使按鈕字串變成灰色的作用。按鈕將不再對滑鼠輸入和鍵盤輸入做出回應,這是表示按鈕選項目前不可用的最好方法。

您可以通過下面的呼叫使子視窗再次被啟用:

您還可以使用下面的呼叫來確定子視窗是否被啟用:

按鈕和輸入焦點

我在本章前面已經提到過,當用滑鼠單擊按鈕、核取方塊、單選框和擁有者繪製按鈕時,它們接收到輸入焦點。這些控制項使用文字周圍的虛線來表示它擁有了輸入焦點。當子視窗控制項得到輸入焦點時,其父視窗就失去了輸入焦點;所有的鍵盤輸入都進入子視窗控制項,而不會進入父視窗中。但是,子視窗控制項只對Spacebar鍵作出回應,此時Spacebar鍵的作用就如同滑鼠按鍵一樣。這種情形導致了一個明顯的問題:您的程式失去了對鍵盤處理的控制項。讓我們看看我們對此能做一些什麼。

我在第六章中已經提到過,當Windows將輸入焦點從一個視窗(例如一個父視窗)轉換到另一個視窗(例如一個子視窗控制項)時,它首先給正在失去輸入焦點的視窗發送一個WM_KILLFOCUS訊息,wParam參數是接收輸入焦點的視窗的代號。然後,Windows向正在接收輸入焦點的視窗發送一個WM_SETFOCUS訊息,同時wParam是還在失去輸入焦點的視窗的代號(在這兩種情況中,wParam值可能為NULL,它表示沒有視窗擁有或者正在接收輸入焦點)。

通過處理WM_KILLFOCUS訊息,父視窗可以阻止子視窗控制項獲得輸入焦點。假定陣列hwndChild包含了所有子視窗的視窗代號(它們是在呼叫CreateWindow來建立視窗的時候儲存到陣列中的)。 NUM是子視窗的數目:

id = GetWindowLong (hwndChild, GWL_ID) ;

id = GetDlgCtrlID (hwndChild) ;

hwndChild = GetDlgItem (hwndParent, id) ;

SendMessage (hwndButton, BM_SETSTATE, 1, 0) ;

SendMessage (hwndButton, BM_SETSTATE, 0, 0) ;

SendMessage ((HWND) lParam, BM_SETCHECK, (WPARAM) !SendMessage ((HWND) lParam, BM_GETCHECK, 0, 0), 0) ;

SendMessage (hwndButton, BM_SETCHECK, 1, 0) ;

iCheck = (int) SendMessage (hwndButton, BM_GETCHECK, 0, 0) ;

SendMessage (hwndButton, BM_SETCHECK, 1, 0) ;

SendMessage (hwndButton, BM_SETCHECK, 0, 0) ;

SetWindowText (hwnd, pszString) ;

iLength = GetWindowText (hwnd, pszBuffer, iMaxLength) ;

iLength = GetWindowTextLength (hwnd) ;

ShowWindow (hwndChild, SW_SHOWNORMAL) ;

ShowWindow (hwndChild, SW_HIDE) ;

IsWindowVisible (hwndChild) ;

EnableWindow (hwndChild, FALSE) ;

EnableWindow (hwndChild, TRUE) ;

IsWindowEnabled (hwndChild) ;

case WM_KILLFOCUS : for ( i = 0 ; i < NUM ; i++) if (hwndChild [i] == (HWND) wParam) { SetFocus (hwnd) ; break ; } return 0 ;

在這段程式碼中,當父視窗獲知它正在失去輸入焦點,而讓它的某個子視窗得到輸入焦點時,它將呼叫SetFocus來重新取得輸入焦點。

下面是可達到相同目的、但更為簡單(但不太直觀)的方法:

case WM_KILLFOCUS : if (hwnd == GetParent ((HWND) wParam)) SetFocus (hwnd) ; return 0 ;

但是,這兩種方法都有缺點:它們阻止按鈕對Spacebar鍵作出回應,因為該按鈕總是得不到輸入焦點。一個更好的方法是使按鈕得到輸入焦點,也能讓使用者用Tab鍵從一個按鈕轉移到另一個按鈕。這聽起來似乎不太可能,在本章的後面,我們將要說明在COLORS1程式中如何用「視窗子類別化」技術來實作這種方法。

控制項與顏色

您可以在圖9-1中看到,許多按鈕的顯示看起來並不正確。按鍵還好,但是其他按鈕卻帶有一個本不應該在那裏的一個矩形灰色背景。這是因為這些按鈕本來是為對話方塊中的顯示而設計的,而在Windows 98中,對話方塊有一個灰色的表面。我們的視窗有一個白色的表面,這是因為我們在WNDCLASS結構中就是這樣定義的。

我們已經這麼做了,因為我們經常在顯示區域中顯示文字,而GDI使用在內定裝置內容中定義的文字顏色和背景顏色,它們總是黑色和白色。為了使這些按鈕更加美觀一些,我們必須要改變顯示區域的顏色使之和按鈕的背景顏色一致,所以要以某種方法將按鈕的背景顏色改為白色。

解決此問題的第一步,是理解Windows對「系統顏色」的使用。

系統顏色

Windows保留了29種系統顏色以供各種顯示使用。您可以使用GetSysColor和SetSysColors來獲得和設定這些顏色。在Windows表頭檔案中定義的識別字規定了系統顏色。使用SetSysColors設定的系統顏色只在目前Windows對話過程中有效。

借助Windows「控制台」程式的「顯示器」部分,您可以改變一些(但不是全部)系統顏色。若是Microsoft Windows NT,選中的顏色會儲存在系統登錄中;若是Microsoft Windows 98,則儲存在WIN.INI檔案中。系統登錄和WIN.INI檔案都為這29種系統顏色使用了關鍵字(與GetSysColor和SetSysColors的識別字不同),在系統顏色的後面跟著紅、綠、藍三種顏色的值,該值的變化範圍是0到255。下表說明了這29種系統顏色是如何在GetSysColor、SetSysColors以及WIN.INI關鍵字中用常數來標識的。這張表是按照COLOR_ 常數值(從0開始到28結束)順序排列的:

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

表9-3

這29種顏色的預設值是由顯示驅動程式提供的,在不同的機器上可能略有不同。

壞消息:雖然這些顏色中有許多似乎都可以從顏色常數名稱上了解其代表意義(例如,COLOR_BACKGROUND是所有視窗後面的桌面區域顏色),在最近版本的Windows中系統顏色的使用變得非常混亂。以前,Windows在視覺上要比今天簡單得多。實際上,在Windows 3.0以前,只定義了前13種系統顏色。但隨著使用看起來越來越難以控制的立體外觀,相對應地也需要更多的系統顏色。

按鈕顏色

對需要多種顏色的每一個按鈕來說,這個問題更加地明顯。COLOR_BTNFACE被用於按鍵主要的表面顏色,以及其他按鈕主要的背景顏色(這也是用於對話方塊和訊息方塊的系統顏色)。COLOR_BTNSHADOW被建議用作按鍵右下邊、以及核取方塊內部和單選按鈕圓點的陰影。對於按鍵,COLOR_BTNTEXT被用作文字顏色;而對於其他的按鈕,則使用COLOR_WINDOWTEXT作為文字顏色。還有其他幾種系統顏色用於按鈕設計的各個部分。

因此,如果您想在我們的顯示區域表面顯示按鈕,那麼一種避免顏色衝突的方法便是屈服於這些系統顏色。首先,在定義視窗類別時使用COLOR_BTNFACE作為您顯示區域的背景顏色:

您可以在BTNLOOK程式中嘗試這種方法。當WNDCLASS結構中的hbrBackground值是這個值時,Windows會明白這實際上指的是一種系統顏色而非一個實際的代號。Windows要求當您在WNDCLASS結構的hbrBackground欄中指定這些識別字時加上1,這樣做的目的是防止其值為NULL,而沒有任何其他目的。如果您的在程式執行過程中,系統顏色恰好發生了變化,那麼顯示區域將變得無效,而Windows將使用新的COLOR_BTNFACE值。但是現在我們又引發了另一個問題。當您使用TextOut顯示文字時,Windows使用的是在裝置內容中為背景顏色(它擦除文字後的背景)和文字顏色定義的值,其預設值為白色(背景)和黑色(文字),而不管系統顏色和視窗類別結構中的hbrBackground欄位為何值。所以,您需要使用SetTextColor和SetBkColor將文字和文字背景的顏色改變為系統顏色。您可以在獲得裝置內容代號之後這麼做:

這樣,顯示區域背景、文字背景和文字的顏色都與按鈕的顏色一致了。但是,如果當您的程式執行時,使用者改變了系統顏色,您可能要改變文字背景顏色和文字顏色。這時您可以使用下面的程式碼:

wndclass.hbrBackground = (HBRUSH) (COLOR_BTNFACE + 1) ;

SetBkColor (hdc, GetSysColor (COLOR_BTNFACE)) ; SetTextColor (hdc, GetSysColor (COLOR_WINDOWTEXT)) ;

case WM_SYSCOLORCHANGE: InvalidateRect (hwnd, NULL, TRUE) ; break ;

WM_CTLCOLORBTN訊息

在這邊已經看到了如何將顯示區域的顏色和文字顏色調節成按鈕的背景顏色。我們是否可以將程式中按鈕的顏色調節為我們喜歡的顏色呢?理論上沒有問題,但在實際中請別這樣做。用SetSysColors來改變按鈕的外觀可能不是您想做的,這會影響目前在Windows下執行的所有程式,這也是使用者不太喜歡的。

更好的方法(同樣也只是理論上)是處理WM_CTLCOLORBTN訊息,這是當子視窗即將為其顯示區域著色時,由按鈕控制項發送給其父視窗訊息處理程式的一個訊息。父視窗可以利用這個機會來改變子視窗訊息處理程式將用來著色的顏色(在Windows的16位元版本中,一個稱為WM_CTLCOLOR的訊息被用於所有的控制項,現在針對每種型態的標準控制項,分別代之以不同的訊息)。

當父視窗訊息處理程式收到WM_CTLCOLORBTN訊息時,wParam訊息參數是按鈕的裝置內容代號,lParam是按鈕的視窗代號。當父視窗訊息處理程式得到這個訊息時,按鈕控制項已經獲得了它的裝置內容。當您的視窗訊息處理程式處理一個WM_CTLCOLORBTN訊息時,您必須完成以下三個動作:

  • 使用SetTextColor選擇設定一種文字顏色。
  • 使用SetBkColor選擇設定一種文字背景顏色。
  • 將一個畫刷代號傳回給子視窗。

理論上,子視窗使用該畫刷來著色背景。當不再需要這個畫刷時,您應該負責清除它。

下面是使用WM_CTLCOLORBTN的問題所在:只有按鍵和擁有者繪製按鈕才給其父視窗發送WM_CTLCOLORBTN,而只有擁有者繪製按鈕才會回應父視窗訊息處理程式對訊息的處理,而使用畫刷來著色背景。這基本上是沒有意義的,因為無論怎樣都是由父視窗來負責繪製擁有者繪製按鈕。

在本章後面,我們將說明,在某些情況下,一些類似於WM_CTLCOLORBTN但適用於其他型態控制項的訊息將更為有用。

擁有者繪製按鈕

如果您想對按鈕的所有可見部分實行全面控制,而不想被鍵盤和滑鼠訊息處理所干擾,那麼您可以建立BS_OWNERDRAW樣式的按鈕,如程式9-2所展示的那樣。

程式9-2 OWNDRAW OWNDRAW.C /*------------------------------------------------------------------------ OWNDRAW.C -- Owner-Draw Button Demo Program (c) Charles Petzold, 1996 -------------------------------------------------------------------------*/ #include <windows.h> #define ID_SMALLER 1 #define ID_LARGER 2 #define BTN_WIDTH ( 8 * cxChar) #define BTN_HEIGHT ( 4 * cyChar) LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; HINSTANCE hInst ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("OwnDraw") ; MSG msg ; HWND hwnd ; WNDCLASS wndclass ; hInst = hInstance ; 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 = szAppName ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow ( szAppName, TEXT ("Owner-Draw Button Demo"), 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 ; } void Triangle (HDC hdc, POINT pt[]) { SelectObject (hdc, GetStockObject (BLACK_BRUSH)) ; Polygon (hdc, pt, 3) ; SelectObject (hdc, GetStockObject (WHITE_BRUSH)) ; } LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam) { static HWND hwndSmaller, hwndLarger ; static int cxClient, cyClient, cxChar, cyChar ; int cx, cy ; LPDRAWITEMSTRUCT pdis ; POINT pt[3] ; RECT rc ; switch (message) { case WM_CREATE : cxChar = LOWORD (GetDialogBaseUnits ()) ; cyChar = HIWORD (GetDialogBaseUnits ()) ; // Create the owner-draw pushbuttons hwndSmaller = CreateWindow (TEXT ("button"), TEXT (""), WS_CHILD | WS_VISIBLE | BS_OWNERDRAW, 0, 0, BTN_WIDTH, BTN_HEIGHT, hwnd, (HMENU) ID_SMALLER, hInst, NULL) ; hwndLarger = CreateWindow (TEXT ("button"), TEXT (""), WS_CHILD | WS_VISIBLE | BS_OWNERDRAW, 0, 0, BTN_WIDTH, BTN_HEIGHT, hwnd, (HMENU) ID_LARGER, hInst, NULL) ; return 0 ; case WM_SIZE : cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; // Move the buttons to the new center MoveWindow ( hwndSmaller, cxClient / 2 - 3 * BTN_WIDTH / 2, cyClient / 2 - BTN_HEIGHT / 2, BTN_WIDTH, BTN_HEIGHT, TRUE) ; MoveWindow ( hwndLarger, cxClient / 2 + BTN_WIDTH / 2,cyClient / 2 - BTN_HEIGHT / 2, BTN_WIDTH, BTN_HEIGHT, TRUE) ; return 0 ; case WM_COMMAND : GetWindowRect (hwnd, &rc) ; // Make the window 10% smaller or larger switch (wParam) { case ID_SMALLER : rc.left += cxClient / 20 ; rc.right -= cxClient / 20 ; rc.top += cyClient / 20 ; rc.bottom -= cyClient / 20 ; break ; case ID_LARGER : rc.left -= cxClient / 20 ; rc.right += cxClient / 20 ; rc.top -= cyClient / 20 ; rc.bottom += cyClient / 20 ; break ; } MoveWindow ( hwnd, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, TRUE) ; return 0 ; case WM_DRAWITEM : pdis = (LPDRAWITEMSTRUCT) lParam ; // Fill area with white and frame it black FillRect (pdis->hDC, &pdis->rcItem, (HBRUSH) GetStockObject (WHITE_BRUSH)) ; FrameRect ( pdis->hDC, &pdis->rcItem, ( HBRUSH) GetStockObject (BLACK_BRUSH)) ; // Draw inward and outward black triangles cx = pdis->rcItem.right - pdis->rcItem.left ; cy = pdis->rcItem.bottom - pdis->rcItem.top ; switch (pdis->CtlID) { case ID_SMALLER : pt[0].x = 3 * cx / 8 ; pt[0].y = 1 * cy / 8 ; pt[1].x = 5 * cx / 8 ; pt[1].y = 1 * cy / 8 ; pt[2].x = 4 * cx / 8 ; pt[2].y = 3 * cy / 8 ; Triangle (pdis->hDC, pt) ; pt[0].x = 7 * cx / 8 ; pt[0].y = 3 * cy / 8 ; pt[1].x = 7 * cx / 8 ; pt[1].y = 5 * cy / 8 ; pt[2].x = 5 * cx / 8 ; pt[2].y = 4 * cy / 8 ; Triangle (pdis->hDC, pt) ; pt[0].x = 5 * cx / 8 ; pt[0].y = 7 * cy / 8 ; pt[1].x = 3 * cx / 8 ; pt[1].y = 7 * cy / 8 ; pt[2].x = 4 * cx / 8 ; pt[2].y = 5 * cy / 8 ; Triangle (pdis->hDC, pt) ; pt[0].x = 1 * cx / 8 ; pt[0].y = 5 * cy / 8 ; pt[1].x = 1 * cx / 8 ; pt[1].y = 3 * cy / 8 ; pt[2].x = 3 * cx / 8 ; pt[2].y = 4 * cy / 8 ; Triangle (pdis->hDC, pt) ; break ; case ID_LARGER : pt[0].x = 5 * cx / 8 ; pt[0].y = 3 * cy / 8 ; pt[1].x = 3 * cx / 8 ; pt[1].y = 3 * cy / 8 ; pt[2].x = 4 * cx / 8 ; pt[2].y = 1 * cy / 8 ; Triangle (pdis->hDC, pt) ; pt[0].x = 5 * cx / 8 ; pt[0].y = 5 * cy / 8 ; pt[1].x = 5 * cx / 8 ; pt[1].y = 3 * cy / 8 ; pt[2].x = 7 * cx / 8 ; pt[2].y = 4 * cy / 8 ; Triangle (pdis->hDC, pt) ; pt[0].x = 3 * cx / 8 ; pt[0].y = 5 * cy / 8 ; pt[1].x = 5 * cx / 8 ; pt[1].y = 5 * cy / 8 ; pt[2].x = 4 * cx / 8 ; pt[2].y = 7 * cy / 8 ; Triangle (pdis->hDC, pt) ; pt[0].x = 3 * cx / 8 ; pt[0].y = 3 * cy / 8 ; pt[1].x = 3 * cx / 8 ; pt[1].y = 5 * cy / 8 ; pt[2].x = 1 * cx / 8 ; pt[2].y = 4 * cy / 8 ; Triangle (pdis->hDC, pt) ; break ; } // Invert the rectangle if the button is selected if (pdis->itemState & ODS_SELECTED) InvertRect (pdis->hDC, &pdis->rcItem) ; // Draw a focus rectangle if the button has the focus if (pdis->itemState & ODS_FOCUS) { pdis->rcItem.left += cx / 16 ; pdis->rcItem.top += cy / 16 ; pdis->rcItem.right -= cx / 16 ; pdis->rcItem.bottom-= cy / 16 ; DrawFocusRect (pdis->hDC, &pdis->rcItem) ; } return 0 ; case WM_DESTROY : PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

該程式在其顯示區域的中央包含了兩個按鈕,如圖9-2所示。左邊的按鈕有四個三角形指向按鈕的中央,按下該按鈕時,視窗的尺寸將縮小10%。右邊的按鈕有四個向外指的三角形,按下此按鈕時,視窗的尺寸將增大10%。

如果您只需要在按鈕中顯示圖示或點陣圖,您可以用BS_ICON或BS_BITMAP樣式,並用BM_SETIMAGE訊息設定點陣圖。但是,對於BS_OWNERDRAW樣式的按鈕,它允許完全自由地繪製按鈕。

在處理WM_CREATE訊息處理期間,OWNDRAW建立了兩個BS_OWNERDRAW樣式的按鈕;按鈕的寬度是系統字體的8倍,高度是系統字體的4倍(在使用預先定義好的點陣圖繪製按鈕時,這些尺寸在VGA上建立的按鈕為64圖素寬64圖素高,知道這些資料將非常有用)。這些按鈕尚未就定位,在處理WM_SIZE訊息處理期間,通過呼叫MoveWindow函式,OWNDRAW將按鈕位置放在顯示區域的中心。

按下這些按鈕時,它們就會產生WM_COMMAND訊息。為了處理這些WM_COMMAND訊息,OWNDRAW呼叫GetWindowRect,將整個視窗(不只是顯示區域)的位置和尺寸存放在RECT(矩形)結構中,這個位置是相對於螢幕的。然後,根據按下的是左邊還是右邊的按鈕,OWNDRAW調節這個矩形結構的各個欄位值。程式再通過呼叫MoveWindow來重新確定位置和尺寸。這將產生另一個WM_SIZE訊息,按鈕被重新定位在顯示區域的中央。

如果這是程式所做的全部處理,那麼這完全可以,只不過按鈕是不可見的。使用BS_OWNERDRAW樣式建立的按鈕會在需要重新著色的任何時候都向它的父視窗發送一個WM_DRAWITEM訊息。這出現在以下幾種情況中:當按鈕被建立時,當按鈕被按下或被放開時,當按鈕得到或者失去輸入焦點時,以及當按鈕需要重新著色的任何時候。

在處理WM_DRAWITEM訊息處理期間,lParam訊息參數是指向型態DRAWITEMSTRUCT結構的指標,OWNDRAW程式將這個指標儲存在pdis變數中,這個結構包含了畫該按鈕時程式所必需的訊息(這個結構也可以讓自繪清單方塊和功能表使用)。對按鈕而言非常重要的結構欄位有hDC (按鈕的裝置內容)、rcItem(提供按鈕尺寸的RECT結構)、CtlID(控制項視窗ID)和itemState (它說明按鈕是否被按下,或者按鈕是否擁有輸入焦點)。

呼叫FillRect用白色畫刷抹掉按鈕的內面,呼叫FrameRect在按鈕的周圍畫上黑框,由此OWNDRAW便啟動了WM_DRAWITEM處理過程。然後,通過呼叫Polygon,OWNDRAW在按鈕上畫出4個黑色實心的三角形。這是一般的情形。

如果按鈕目前被按下,那麼DRAWITEMSTRUCT的itemState欄位中的某位元將被設為1。您可以使用ODS_SELECTED常數來測試這些位元。如果這些位元被設立,那麼OWNDRAW將通過呼叫InvertRect將按鈕翻轉為相反的顏色。如果按鈕擁有輸入焦點,那麼itemState的ODS_FOCUS位元將被設立。在這種情況下,OWNDRAW通過呼叫DrawFocusRect,在按鈕的邊界內畫一個虛線的矩形。

在使用擁有者繪製按鈕時,應該注意以下幾個方面:Windows獲得裝置內容並將其作為DRAWITEMSTRUCT結構的一個欄位。保持裝置內容處於您找到它時所處的狀態,任何被選進裝置內容的GDI物件都必需被釋放。另外,當心不要在定義按鈕邊界的矩形外面進行繪製。

靜態類別

在CreateWindow函式中指定視窗類別為「static」,您就可以建立靜態文字的子視窗控制項。這些子視窗非常「文靜」。它既不接收滑鼠或鍵盤輸入,也不向父視窗發送WM_COMMAND訊息。

當您在靜態子視窗上移動或者按下滑鼠時,這個子視窗將攔截WM_NCHITTEST訊息並將HTTRANSPARENT的值傳回給Windows,這將使Windows向其下層視窗,通常是它的父視窗,發送相同的WM_NCHITTEST訊息。父視窗常常將該訊息傳遞給DefWindowProc,在這裏,它被轉換為顯示區域的滑鼠訊息。

前六個靜態視窗樣式只簡單地在子視窗的顯示區域內畫一個矩形或者邊框。在下表的上部,「RECT」靜態樣式(左列)是填入圖樣的矩形樣式;三個「FRAME」樣式(右列)是沒有填入圖樣的矩形輪廓:

圖9-2 OWNDRAW的螢幕顯示

SS_BLACKRECT

SS_GRAYRECT

SS_WHITERECT

SS_BLACKFRAME

SS_GRAYFRAME

SS_WHITEFRAME

「BLACK」、「GRAY」、「WHITE」並不意味著黑、灰和白色,這些顏色是由系統顏色決定的,如表9-4所示。

表9-4

對這些樣式,CreateWindow呼叫中的視窗文字欄位被忽略。矩形的左上角開始於x位置座標和y位置座標,這些座標都相對於父視窗。您也可以使用SS_ETCHEDHORZ、SS_ETCHEDVERT或者SS_ETCHEDFRAME ,採用灰色和白色建立一個形似陰影的邊框。

靜態類別也包括了三種文字樣式:SS_LEFT、SS_RIGHT和SS_CENTER。它們建立左對齊、置右對齊和居中文字。文字在CreateWindow呼叫的視窗文字參數中給出,並且在以後可以用SetWindowText來改變它。當靜態控制項的視窗訊息處理程式顯示文字時,它使用DrawText函式以及DT_WORDBREAK、DT_NOCLIP和DT_EXPANDTABS參數。文字在子視窗的矩形內可以按文字進行換行。

這三種文字樣式子視窗的背景通常為COLOR_BTNFACE,而文字本身是COLOR_WINDOWTEXT。在攔截WM_CTLCOLORSTATIC訊息時,您可以通過呼叫SetTextColor來改變文字顏色,通過SetBkColor來改變背景顏色,並傳回背景畫刷代號。後面的COLORS1程式展示了這一點。

最後,靜態類別還包括了視窗樣式SS_ICON和SS_USERITEM,但是當它們被用作子視窗控制項時卻沒有任何意義。我們在討論對話方塊時還要提及它們。

捲動列類別

我在 第四章 首次討論了捲動列,也討論了「視窗捲動列」和「捲動列控制項」之間的一些區別。SYSMETS程式使用視窗捲動列,它出現在視窗的右邊和底部。您可以在建立視窗時通過將識別字WS_VSCROLL、WS_HSCROLL或者兩者都包含在視窗樣式中,讓視窗加上捲動列。現在我們準備建立一些捲動列控制項,它們是能在父視窗的顯示區域的任何地方出現的子視窗。您可以使用預先定義的視窗類別「scrollbar」以及兩個捲動列樣式SBS_VERT和SBS_HORZ中的一個來建立子視窗捲動列控制項。

與按鈕控制項(以及將在後面討論的編輯和清單方塊控制項)不同,捲動列控制項不向父視窗發送WM_COMMAND訊息,而是像視窗捲動列那樣發送WM_VSCROLL和WM_HSCROLL訊息。在處理捲動訊息時,您可以通過lParam參數來區分視窗捲動列與捲動列控制項。對子視窗捲動列其值為0,對於捲動列控制項其值為捲動列視窗代號。對視窗捲動列和捲動列控制項來說,wParam參數的高字組和低字組的含義相同。

雖然視窗捲動列有固定的寬度,Windows使用CreateWindow呼叫中(或者在後面的MoveWindow呼叫中)給定的矩形尺寸來確定捲動列控制項的尺寸。您可以建立細而長的捲動列控制項,也可以建立短而粗的捲動列控制項。

如果您想建立與視窗捲動列尺寸相同的捲動列控制項,那麼可以使用GetSystemMetrics取得水平捲動列的高度:

或者垂直捲動列的寬度:

根據Windows文件,捲動列窗樣式識別字SBS_LEFTALIGN、SBS_RIGHTALIGN、SBS_TOP ALIGN和SBS_BOTTOMALIGN給出捲動列的標準尺寸,但是這些樣式只在對話方塊中對捲動列有效。

對視窗捲動列,您可以使用同樣的呼叫來建立捲動列控制項的範圍和位置:

GetSystemMetrics (SM_CYHSCROLL) ;

GetSystemMetrics (SM_CXVSCROLL) ;

SetScrollRange (hwndScroll, SB_CTL, iMin, iMax, bRedraw) ; SetScrollPos (hwndScroll, SB_CTL, iPos, bRedraw) ; SetScrollInfo (hwndScroll, SB_CTL, &si, bRedraw) ;

其區別在於:視窗捲動列將父視窗的代號作為第一個參數,並且以SB_VERT或者SB_HORZ作為第二個參數。

令人吃驚的是,名為COLOR_SCROLLBAR的系統顏色不再用於捲動列。兩端的按鈕和小方塊的顏色由COLOR_BTNFACE、COLOR_BTNHILIGHT、COLOR_BTNSHADOW、COLOR_BTNTEXT (用於小箭頭)、COLOR_DKSHADOW和COLOR_BTNLIGHT決定。兩端按鈕之間區域的顏色由COLOR_BTNFACE和COLOR_BTNHIGHLIGHT決定。

如果您攔截了WM_CTLCOLORSCROLLBAR訊息,那麼可以在訊息處理中傳回畫刷以取代該顏色。讓我們來試一下。

COLORS1程式

為瞭解捲動列和靜態子視窗的一些用法-也為了深入瞭解顏色-我們將使用COLORS1程式,如程式9-3所示。COLORS1在顯示區域的左半部顯示三種捲動列,並分別標以「Red」、「 Green」和「Blue」。當您挪動捲動列時,顯示區域的右半部將變為三種原色混合而成的合成色,三種原色的數值顯示在三個捲動列的下面。

程式9-3 COLORS1 COLORS1.C /*------------------------------------------------------------------------ COLORS1.C -- Colors Using Scroll Bars (c) Charles Petzold, 1998 -------------------------------------------------------------------------*/ #include <windows.h> LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; LRESULT CALLBACK ScrollProc (HWND, UINT, WPARAM, LPARAM) ; int idFocus ; WNDPROC OldScroll[3] ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("Colors1") ; 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 = CreateSolidBrush (0) ; 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 ("Color Scroll"), 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 COLORREF crPrim[3] = { RGB (255, 0, 0), RGB (0, 255, 0), RGB (0, 0, 255) } ; static HBRUSH hBrush[3], hBrushStatic ; static HWND hwndScroll[3], hwndLabel[3], hwndValue[3], hwndRect ; static int color[3], cyChar ; static RECT rcColor ; static TCHAR * szColorLabel[] = { TEXT ("Red"), TEXT ("Green"), TEXT ("Blue") } ; HINSTANCE hInstance ; int i, cxClient, cyClient ; TCHAR szBuffer[10] ; switch (message) { case WM_CREATE : hInstance = (HINSTANCE) GetWindowLong (hwnd, GWL_HINSTANCE) ; // Create the white-rectangle window against which the // scroll bars will be positioned. The child window ID is 9. hwndRect = CreateWindow (TEXT ("static"), NULL, WS_CHILD | WS_VISIBLE | SS_WHITERECT, 0, 0, 0, 0, hwnd, (HMENU) 9, hInstance, NULL) ; for (i = 0 ; i < 3 ; i++) { // The three scroll bars have IDs 0, 1, and 2, with // scroll bar ranges from 0 through 255. hwndScroll[i] = CreateWindow (TEXT ("scrollbar"), NULL, WS_CHILD | WS_VISIBLE | WS_TABSTOP | SBS_VERT, 0, 0, 0, 0, hwnd, (HMENU) i, hInstance, NULL) ; SetScrollRange (hwndScroll[i], SB_CTL, 0, 255, FALSE) ; SetScrollPos (hwndScroll[i], SB_CTL, 0, FALSE) ; // The three color-name labels have IDs 3, 4, and 5, // and text strings "Red", "Green", and "Blue". hwndLabel [i] = CreateWindow (TEXT ("static"), zColorLabel[i], WS_CHILD | WS_VISIBLE | SS_CENTER, 0, 0, 0, 0, hwnd, (HMENU) (i + 3), hInstance, NULL) ; // The three color-value text fields have IDs 6, 7, // and 8, and initial text strings of "0". hwndValue [i] = CreateWindow (TEXT ("static"), TEXT ("0"), WS_CHILD | WS_VISIBLE | SS_CENTER, 0, 0, 0, 0, hwnd, (HMENU) (i + 6), hInstance, NULL) ; OldScroll[i] = (WNDPROC) SetWindowLong (hwndScroll[i], GWL_WNDPROC, (LONG) ScrollProc) ; hBrush[i] = CreateSolidBrush (crPrim[i]) ; } hBrushStatic = CreateSolidBrush ( GetSysColor (COLOR_BTNHIGHLIGHT)) ; cyChar = HIWORD (GetDialogBaseUnits ()) ; return 0 ; case WM_SIZE : cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; SetRect (&rcColor, cxClient / 2, 0, cxClient, cyClient) ; MoveWindow (hwndRect, 0, 0, cxClient / 2, cyClient, TRUE) ; for (i = 0 ; i < 3 ; i++) { MoveWindow (hwndScroll[i], (2 * i + 1) * cxClient / 14, 2 * cyChar, cxClient / 14, cyClient - 4 * cyChar, TRUE) ; MoveWindow (hwndLabel[i], (4 * i + 1) * cxClient / 28, cyChar / 2, cxClient / 7, cyChar, TRUE) MoveWindow (hwndValue[i], (4 * i + 1) * cxClient / 28, cyClient - 3 * cyChar / 2, cxClient / 7, cyChar, TRUE) ; } SetFocus (hwnd) ; return 0 ; case WM_SETFOCUS : SetFocus (hwndScroll[idFocus]) ; return 0 ; case WM_VSCROLL : i = GetWindowLong ((HWND) lParam, GWL_ID) ; switch (LOWORD (wParam)) { case SB_PAGEDOWN : color[i] += 15 ; // fall through case SB_LINEDOWN : color[i] = min (255, color[i] + 1) ; break ; case SB_PAGEUP : color[i] -= 15 ; // fall through case SB_LINEUP : color[i] = max (0, color[i] - 1) ; break ; case SB_TOP : color[i] = 0 ; break ; case SB_BOTTOM : color[i] = 255 ; break ; case SB_THUMBPOSITION : case SB_THUMBTRACK : color[i] = HIWORD (wParam) ; break ; default : break ; } SetScrollPos (hwndScroll[i], SB_CTL, color[i], TRUE) ; wsprintf (szBuffer, TEXT ("%i"), color[i]) ; SetWindowText (hwndValue[i], szBuffer) ; DeleteObject ((HBRUSH) SetClassLong (hwnd, GCL_HBRBACKGROUND, (LONG) CreateSolidBrush (RGB (color[0], color[1], color[2])))) ; InvalidateRect (hwnd, &rcColor, TRUE) ; return 0 ; case WM_CTLCOLORSCROLLBAR : i = GetWindowLong ((HWND) lParam, GWL_ID) ; return (LRESULT) hBrush[i] ; case WM_CTLCOLORSTATIC : i = GetWindowLong ((HWND) lParam, GWL_ID) ; if (i >= 3 && i <= 8) // static text controls { SetTextColor ((HDC) wParam, crPrim[i % 3]) ; SetBkColor ((HDC) wParam, GetSysColor (COLOR_BTNHIGHLIGHT)); return (LRESULT) hBrushStatic ; } break ; case WM_SYSCOLORCHANGE : DeleteObject (hBrushStatic) ; hBrushStatic = CreateSolidBrush (GetSysColor(COLOR_BTNHIGHLIGHT)) ; return 0 ; case WM_DESTROY : DeleteObject ((HBRUSH) SetClassLong (hwnd, GCL_HBRBACKGROUND, (LONG) GetStockObject (WHITE_BRUSH))) ; for (i = 0 ; i < 3 ; i++) DeleteObject (hBrush[i]) ; DeleteObject (hBrushStatic) ; PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; } LRESULT CALLBACK ScrollProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { int id = GetWindowLong (hwnd, GWL_ID) ; switch (message) { case WM_KEYDOWN : if (wParam == VK_TAB) SetFocus (GetDlgItem (GetParent (hwnd), (id + (GetKeyState (VK_SHIFT) < 0 ? 2 : 1)) % 3)) ; break ; case WM_SETFOCUS : idFocus = id ; break ; } return CallWindowProc (OldScroll[id], hwnd, message, wParam,lParam) ; }

COLORS1利用子視窗進行工作,該程式使用10個子視窗控制項:3個捲動列、6個靜態文字視窗和1個靜態矩形框。COLORS1攔截WM_CTLCOLORSCROLLBAR訊息來給紅、綠、藍3個捲動列的內部著色,並攔截WM_CTLCOLORSTATIC訊息來著色靜態文字。

您可以使用滑鼠或者鍵盤來挪動捲動列,從而利用COLORS1作為一種實驗顏色顯示的開發工具,為您自己的Windows程式選擇漂亮的顏色(或者,您可能更喜歡難看的顏色)。COLORS1的顯示如圖9-3所示。不幸的是,這些顏色在印表紙上被顯示為不同深淺的灰色。

COLORS1不處理WM_PAINT訊息,所有的工作幾乎都是由子視窗完成的。

顯示區域右半部顯示的顏色實際上是視窗的背景顏色。SS_WHITERECT樣式的靜態子視窗顯示在顯示區域的左半部。三個捲動列是SBS_VERT樣式的子視窗控制項,它們被定位在SS_WHITERECT子視窗的頂部。另外六個SS_CENTER樣式(居中文字)的靜態子視窗提供標籤和顏色值。COLORS1在WinMain函式中用CreateWindow建立它的普通重疊式視窗和10個子視窗。SS_WHITERECT和SS_CENTER靜態視窗使用視窗類別「static」;三個捲動列使用視窗類別「scrollbar」。

CreateWindow呼叫中的x位置、y位置、寬度和高度參數最初設為0,因為位置和大小都取決於顯示區域的尺寸,而它目前尚未確定。COLORS1的視窗訊息處理程式在接收到WM_SIZE訊息時,就使用MoveWindow給10個子視窗重新確定大小。所以,每當您對COLORS1視窗進行縮放時,捲動列的尺寸就會按比例變化。

當WndProc視窗訊息處理程式收到WM_VSCROLL訊息時,lParam參數的高字組就是子視窗的代號。我們可以使用GetWindowWord來得到子視窗的ID:

對於這三個捲動列,我們已經按習慣將其ID設為0、1、2,所以WndProc能區別出是哪個捲動列在產生訊息。

由於子視窗的代號在建立時就被儲存在陣列中,所以WndProc就能對相對應的捲動列訊息進行處理,並通過呼叫SetScrollPos來設定相對應的新值:

WndProc也改變捲動列底部子視窗的文字:

自動鍵盤介面

捲動列控制項也能處理鍵盤輸入,但是只有在擁有輸入焦點時才行。下表說明怎樣將鍵盤游標鍵轉變為捲動訊息:

i = GetWindowLong ((HWND) lParam, GWL_ID) ;

SetScrollPos (hwndScroll[i], SB_CTL, color[i], TRUE) ;

wsprintf (szBuffer, TEXT ("%i"), color[I]) ; SetWindowText (hwndValue[i], szBuffer) ;

圖9-3 COLORS1的螢幕顯示

表9-5

事實上,SB_TOP和SB_BOTTOM捲動訊息只能用鍵盤產生。在使用滑鼠按動捲動列時,如果想使該捲動列獲得輸入焦點,那麼您必須將WS_TABSTOP識別字包含到CreateWindow呼叫的視窗類別參數中。當捲動列擁有輸入焦點時,在該捲動列的小方框上將顯示一個閃爍的灰色塊。

為了給捲動列提供全面的鍵盤介面,還需要另外一些工作。首先,WndProc視窗訊息處理程式必須使捲動列擁有輸入焦點,它是通過處理WM_SETFOCUS訊息來完成這一點的,該WM_SETFOCUS訊息是當捲動列獲得輸入焦點時其父視窗接收到的。WndProc給其中一個捲動列設定輸入焦點。

其中idFocus是一個整體變數。

但是,還需要一些借助鍵盤尤其是Tab鍵,來從一個捲動列轉換到另一個捲動列的方法。這比較困難,因為一旦某個捲動列擁有了輸入焦點,它就處理所有的鍵盤輸入,但捲動列只關心游標鍵,而忽略Tab鍵。解決這一兩難處境的方法是「視窗子類別化」。我們將用它來給COLORS1增加使用Tab鍵從一個捲動列跳到另一個捲動列的功能。

視窗子類別化(Window Subclassing)

捲動列控制項的視窗訊息處理程式是Windows內部的。但是,將GWL_WNDPROC識別字作為參數來呼叫GetWindowLong,您就可以得到這個視窗訊息處理程式的位址。另外,您可以呼叫SetWindowLong給該捲動列設定一個新的視窗訊息處理程式,這個技術叫做「視窗子類別化」,非常有用。它能讓您給現存的視窗訊息處理程式設定「掛勾」,以便在自己的程式中處理一些訊息,同時將其他所有訊息傳遞給舊的視窗訊息處理程式。

在COLORS1中對捲動訊息進行初步處理的視窗訊息處理程式叫做ScrollProc,它在COLORS1.C檔案的尾部。由於ScrollProc是COLORS1中的函式,而Windows將呼叫COLORS1,所以ScrollProc必須被定義為callback函式。

對三個捲動列中的每一個,COLORS1使用SetWindowLong來設定新的捲動列視窗訊息處理程式的位址,並取得現存捲動列視窗訊息處理程式的位址:

現在,函式ScrollProc得到了Windows發送到COLORS1中三個捲動列(當然不是其他程式中的捲動列)的捲動列視窗訊息處理程式的全部訊息。ScrollProc視窗訊息處理程式在接收到Tab或者Shift-Tab鍵時,就將輸入焦點改變到下一個(或者上一個)捲動列。它使用CallWindowProc呼叫舊的捲動列視窗訊息處理程式。

給背景著色

當COLORS1定義它的視窗類別時,也為其顯示區域背景定義了一個實心的黑色畫刷:

當您改變COLORS1的捲動列設定時,程式必須建立一個新的畫刷,並將該新畫刷代號放入視窗類別結構中。如同使用GetWindowLong和SetWindowLong能得到並設定捲動列視窗訊息處理程式一樣,用GetClassWord和SetClassWord能得到這個畫刷的代號。

您可以建立新的畫刷並將其代號插入視窗類別結構中,然後刪除舊的畫刷:

SetFocus (hwndScroll[idFocus]) ;

OldScroll[i] = (WNDPROC) SetWindowLong (hwndScroll[i], GWL_WNDPROC, (LONG) ScrollProc)) ;

wndclass.hbrBackground = CreateSolidBrush (0) ;

DeleteObject ((HBRUSH) SetClassLong (hwnd, GCL_HBRBACKGROUND, (LONG) CreateSolidBrush (RGB (color[0], color[1], color[2])))) ;

Windows下一次重新為視窗的背景著色時,將使用這個新畫刷。為了強迫Windows抹掉背景,我們將使整個顯示區域無效:

TRUE(非零)值作為第三個參數,表示希望在重新著色之前刪去背景。

InvalidateRect使Windows在視窗訊息處理程式的訊息佇列中放進一個WM_PAINT訊息。由於WM_PAINT訊息的優先等級比較低,所以,如果您還在使用滑鼠或者游標鍵移動捲動列的話,這個訊息將不會立即被處理。如果您想在顏色改變之後使該視窗立即變成最新的(目前的),那麼您可以在InvalidateRect之後增加下面的敘述:

但這會使得鍵盤和滑鼠處理變慢。

COLORS1中的WndProc函式不處理WM_PAINT訊息,而是將其傳給DefWindowProc。Windows對WM_PAINT訊息的內定處理只是呼叫BeginPaint和EndPaint使視窗生效。因為在InvalidateRect呼叫中已經指定背景要被抹掉,所以BeginPaint呼叫使Windows發出一個WM_ERASEBKGND(刪除背景)訊息,WndProc也將忽略這個訊息。Windows用視窗類別中指定的畫刷將顯示區域的背景抹去,這樣就處理了這個訊息。

在終止以前進行清除總是一個好主意,因此在處理WM_DESTROY訊息處理期間,再一次呼叫DeleteObject:

InvalidateRect (hwnd, &rcColor, TRUE) ;

UpdateWindow (hwnd) ;

DeleteObject ((HBRUSH) SetClassLong (hwnd, GCL_HBRBACKGROUND, (LONG) GetStockObject (WHITE_BRUSH))) ;

給捲動列和靜態文字著色

在COLORS1中,三個捲動列的內部和六個文字欄位中的文字著色為紅、綠和藍色。捲動列的著色是通過處理WM_CTLCOLORSCROLLBAR訊息來完成的。

在WndProc中,我們為畫刷定義了一個由三個代號組成的靜態陣列:

在處理WM_CREATE期間,我們建立三個畫刷:

其中crPrim陣列中包含三種原色的RGB值。在WM_CTLCOLORSCROLLBAR處理期間視窗訊息處理程式傳回這三畫刷中的一個:

static HBRUSH hBrush [3] ;

for (I = 0 ; I < 3 ; I++) hBrush[0] = CreateSolidBrush (crPrim [I]) ;

case WM_CTLCOLORSCROLLBAR: i = GetWindowLong ((HWND) lParam, GWL_ID) ; return (LRESULT) hBrush [i] ;

在處理WM_DESTROY訊息的過程中,這些畫刷必須被刪除:

同樣地,靜態文字欄位中的文字是在處理WM_CTLCOLORSTATIC訊息中呼叫SetTextColor來著色的。文字背景用SetBkColor函式設定為系統顏色COLOR_BTNHIGHLIGHT,這導致文字背景顏色和捲動列與文字後面的靜態矩形控制項的顏色一樣。對於靜態文字控制項,這種文字背景顏色只用於字串中每個字元後面的矩形,而不會用於整個控制項視窗。為了實作這一點,視窗訊息處理程式還必須傳回COLOR_BTNHIGHLIGHT顏色畫刷的代號。這個畫刷被稱為hBrushStatic,它在WM_CREATE訊息處理期間建立,在WM_DESTROY訊息處理期間清除。

在WM_CREATE訊息處理期間依據COLOR_BTNHIGHLIGHT顏色建立畫刷,並且在執行期間使用這一畫刷時,我們遇到了一個小問題。如果程式在執行期間改變了COLOR_BTNHIGHLIGHT顏色,那麼靜態矩形的顏色將發生變化,並且文字背景的顏色也會變化,但是文字視窗控制項的整個背景將保持原有的COLOR_BTNHIGHLIGHT顏色。

為了解決這個問題,COLORS1也簡單地通過使用新顏色重新建立hBrushStatic來處理WM_SYSCOLORCHANGE訊息。

編輯類別

在某些方面,編輯類別是最簡單的預先定義視窗類別;在另一方面,它又是最複雜的視窗類別。當您使用類別名稱「edit」建立子視窗時,您根據CreateWindow呼叫中的x位置、y位置、寬度和高度這些參數定義了一個矩形。此矩形含有可編輯文字。當子視窗控制項擁有輸入焦點時,您可以輸入文字,移動游標,使用滑鼠或者Shift鍵與一個游標鍵來選取部分文字,按Ctrl-X來刪除所選文字或按Ctrl-C來複製所選文字、並送到剪貼簿上,按Ctrl-V鍵插入剪貼簿上的文字。

編輯控制項的最簡單的應用之一是作為單行輸入區域。但是編輯控制項並不僅限於單行,這一點我將在程式9-4 POPPAD1中說明。和我們在這本書中所遇到的各種其他問題一樣, POPPAD程式將逐步增強以使用功能表、對話方塊(載入與儲存檔案)和列印。最後的版本將是一個簡單而完整的文字編輯器,且其程式碼將非常簡潔。

for (i = 0 ; i < 3 ; i++) DeleteObject (hBrush [i])) ;

程式9-4 POPPAD1 POPPAD1.C /*--------------------------------------------------------------------------- POPPAD1.C -- Popup Editor using child window edit box (c) Charles Petzold, 1998 ---------------------------------------------------------------------------*/ #include <windows.h> #define ID_EDIT 1 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM); TCHAR szAppName[] = TEXT ("PopPad1") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { 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, szAppName, 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 HWND hwndEdit ; switch (message) { case WM_CREATE : hwndEdit = CreateWindow (TEXT ("edit"), NULL, WS_CHILD | WS_VISIBLE | WS_HSCROLL | WS_VSCROLL | WS_BORDER | ES_LEFT | ES_MULTILINE | ES_AUTOHSCROLL | ES_AUTOVSCROLL, 0, 0, 0, 0, hwnd, (HMENU) ID_EDIT, ((LPCREATESTRUCT) lParam) -> hInstance, NULL) ; return 0 ; case WM_SETFOCUS : SetFocus (hwndEdit) ; return 0 ; case WM_SIZE : MoveWindow (hwndEdit, 0, 0, LOWORD (lParam), HIWORD (lParam), TRUE) ; return 0 ; case WM_COMMAND : if (LOWORD (wParam) == ID_EDIT) if (HIWORD (wParam) == EN_ERRSPACE || HIWORD (wParam) == EN_MAXTEXT) MessageBox (hwnd, TEXT ("Edit control out of space."), szAppName, MB_OK | MB_ICONSTOP) ; return 0 ; case WM_DESTROY : PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

POPPAD1是一個多行編輯器(只是沒有檔案I/O),其C語言原始碼不到100行(不過,有一個缺陷,即預先定義的多行編輯控制項只限於30,000字元的文字)。您可以看到,POPPAD1本身並沒有做多少工作,預先定義的編輯控制項完成了許多工作,這樣,您可以知道,無需額外的程式時編輯控制項能做些什麼。

編輯類別樣式

如前面所提到的,在CreateWindow呼叫中將「edit」作為視窗類別建立了一個編輯控制項,視窗樣式是WS_CHILD加上幾個選項。如同在靜態子視窗控制項中一樣,編輯控制項中的文字可以置左對齊、置右對齊或者居中,您使用視窗樣式ES_LEFT、ES_RIGHT和ES_CENTER來指定這些格式。

內定狀態下,編輯控制項是單行的。您使用ES_MULTILINE視窗樣式可以建立多行編輯控制項。對於單行編輯控制項,您一般只可以在編輯控制項矩形的尾部輸入文字。要建立一個自動水平捲動的編輯控制項,您可以採用樣式ES_AUTOHSCROLL。對一個多行編輯控制項,文字會自動跳行,除非使用ES_AUTOHSCROLL樣式。在這種情況下,您必須按Enter鍵來開始新的一行。您還可以便用樣式ES_AUTOVSCROLL來將垂直捲動列包括在多行編輯控制項中。

當您在多行編輯控制項中包括這些捲動樣式時,也許還想給編輯控制項增加捲動列。要做到這些,可以對非子視窗使用同一視窗樣式識別字WS_HSCROLL和WS_VSCROLL。內定狀態下,編輯控制項沒有邊界,利用樣式WS_BORDER則可以增加邊界。

當您在編輯控制項中選擇文字時,Windows將選擇的文字反白顯示。但是當編輯控制項失去輸入焦點時,被選擇的文字將不再被加亮。如果希望在編輯控制項沒有輸入焦點時被選擇的文字仍然被加亮,您可以使用樣式ES_NOHIDESEL。

在POPPAD1建立其編輯控制項時,CreateWindow呼叫依如下形式給出樣式:

WS_CHILD | WS_VISIBLE | WS_HSCROLL | WS_VSCROLL | WS_BORDER | ES_LEFT | ES_MULTILINE | ES_AUTOHSCROLL | ES_AUTOVSCROLL

在POPPAD1中,編輯控制項的大小是後來當WndProc接收到WM_SIZE訊息時通過呼叫MoveWindow來定義的。編輯控制項的尺寸被簡單地設定為主視窗的尺寸:

對於單行編輯控制項,控制項的高度必須可以容納一個字元。如果編輯控制項有邊界(大多數都有),那麼使用一個字元高度的1.5倍(包括外部間距)。

編輯控制項通知

編輯控制項給父視窗訊息處理程式發送WM_COMMAND訊息,對按鈕控制項來說,wParam和lParam變數的含義是相同的:

MoveWindow (hwndEdit, 0, 0, LOWORD (lParam), HIWORD (lParam), TRUE) ;

LOWORD (wParam)

HIWORD (wParam)

lParam

子視窗ID

通知碼

子視窗代號

通知碼如下所示:

EN_SETFOCUS

EN_KILLFOCUS

EN_CHANGE

EN_UPDATE

EN_ERRSPACE

EN_MAXTEXT

EN_HSCROLL

EN_VSCROLL

編輯控制項已經獲得輸入焦點

編輯控制項已經失去輸入焦點

編輯控制項的內容將改變

編輯控制項的內容已經改變

編輯控制項執行已經超出中間

編輯控制項在插入時執行超出空間

編輯控制項的水平捲動列已經被按下

編輯控制項的垂直捲動列已經被按下

POPPAD1只攔截EN_ERRSPACE和EN_MAXTEXT通知碼,並顯示一個訊息方塊。

使用編輯控制項

如果在您的主視窗上使用了幾個單行編輯控制項,那麼您需要將視窗子類別化以便把輸入焦點從一個控制項轉移到另一個控制項。您可以通過攔截Tab鍵和Shift-Tab鍵來完成這種移動,非常像COLORS1中所做的(視窗子類別化的另一個例子在後面的HEAD程式中說明)。如何處理Enter鍵取決於您,可以像Tab鍵那樣使用,也可以當成給程式的信號,表示所有的編輯欄位都準備好了。

如果您想在編輯區中插入文字,那麼可以使用SetWindowText來做到。將文字從編輯控制項中取出涉及了GetWindowTextLength和GetWindowText,我們將在POPPAD程式的修訂版本中看到這些操作的實例。

發送給編輯控制項的訊息

因為用SendMessage發送給編輯控制項的訊息很多,並且其中的幾個還將在後面POPPAD修訂版本中用到,所以這裏不解說所有用SendMessage發送給編輯控制項的訊息,只概要地說明一下。

這些訊息允許您剪下、複製或者清除目前被選擇的文字。使用者使用滑鼠或者Shift鍵加上游標控制項鍵來選擇文字並進行上面的操作,這樣,在編輯控制項中選中的文字將被加亮:

SendMessage (hwndEdit, WM_CUT, 0, 0) ; SendMessage (hwndEdit, WM_COPY, 0, 0) ; SendMessage (hwndEdit, WM_CLEAR, 0, 0) ;

WM_CUT將目前選擇的文字從編輯控制項中移走,並將其發送到剪貼簿中;WM_COPY將選擇的文字複製到剪貼簿上並保持編輯控制項中的內容完好無損;WM_CLEAR將選擇的內容從編輯控制項中刪除,但是不向剪貼簿中發送。

您也可以將剪貼簿上的文字插入到編輯控制項中的游標位置:

您可以取得目前選擇的起始位置和末尾位置:

結束位置實際上是最後一個選擇字元的位置加1。

您可以選擇文字:

您還可以使用別的文字來置換目前的選擇內容:

對多行編輯控制項,您可以取得行數:

對任何特定的行,您可以取得距離編輯緩衝區文字開頭的偏移量:

行數從0開始計算,iLine值為-1時傳回包含游標所在行的偏移量。您可以取得行的長度:

並將行本身複製到一個緩衝區中:

清單方塊類別

我在本章討論的最後一個預先定義子視窗控制項是清單方塊。一個清單方塊是字串的集合,這些字串是一個矩形中可以捲動顯示的清單。-程式通過向清單方塊視窗訊息處理程式發送訊息,可以在清單中增加或者刪除字串。當清單方塊中的某項被選擇時,清單方塊控制項就向其父視窗發送WM_COMMAND訊息,父視窗也就可以確定選擇的是哪一項。

一個清單方塊可以是單選的,也可以是多選的,後者允許使用者從清單方塊中選擇多個項目。當清單方塊擁有輸入焦點時,其中項目的周圍顯示有虛線。在清單方塊中,游標位置並不指明被選擇的項目。被選擇的項目被加亮顯示,並且是反白顯示的。

在單項選擇的清單方塊中,使用者按Spacebar鍵就可以選擇游標所在位置的項目。方向鍵移動游標和目前選擇指示,並且能夠滾動清單方塊的內容。Page Up和Page Down鍵也能滾動清單方塊,但它移動的是游標而不是選擇指示。按字母鍵能將游標和選擇指示移到以此字母開頭的第一個(或下一個)選項。也可以使用滑鼠在要選擇的項目上單擊或者雙擊來選擇它。

在多項選擇清單方塊中,Spacebar鍵可以切換游標所在位置的項目的選擇狀態(如果該項已經被選擇,則取消選擇)。如同在單項選擇清單方塊中一樣,方向鍵取消前面選擇過的項目,並且移動游標和選擇指示。但是,Ctrl鍵和方向鍵能夠在移動游標的同時不移動選擇,Shift鍵加方向鍵能擴展一個選擇。

在多項選擇清單方塊中,單擊或者雙擊滑鼠按鍵能取消之前所有的選擇,而選擇被點中的項目。但是,如果在滑鼠點中某一項的同時也按下Shift鍵,則只能切換該項的選擇狀態,而不會改變任何其他項的選擇狀態。

清單方塊樣式

當您使用CreateWindow建立清單方塊子視窗時,您應該將「listbox」作為視窗類別,將WS_CHILD作為視窗樣式。但是,這個內定清單方塊樣式不向其父視窗發送WM_COMMAND訊息,這樣一來,程式必須向清單方塊詢問其中的項目的選擇狀態(借助於發送給清單方塊控制項的訊息)。所以,清單方塊控制項通常都包括清單方塊樣式識別字LBS_NOTIFY,它允許父視窗接收來自清單方塊的WM_COMMAND訊息。如果您希望清單方塊控制項對清單方塊中的項目進行排序,那麼您可以使用另一種常用的樣式LBS_SORT。

內定情況下,清單方塊是單項選擇的。多項選擇的清單方塊相當少。如果您想建立一個多項選擇清單方塊,那麼您可以使用樣式LBS_MULTIPLESEL。通常,當給有捲動列的清單方塊增加新項目時,清單方塊本身會自己重畫。您可以通過將樣式LBS_NOREDRAW包含進去來防止這種現象。但是您也許不想使用這種樣式,這時可以使用WM_SETREDRAW訊息來暫時防止清單方塊控制項重新畫過,我將在稍後討論WM_SETREDRAW訊息。

內定狀態下,清單方塊視窗訊息處理程式只顯示列表項目,它的周圍沒有任何邊界。您可以使用視窗樣式識別字WS_BORDER來加上邊界。另外,您可以使用視窗樣式識別字WS_VSCROLL來增加垂直捲動列,以便用滑鼠來捲動列表項目。

Windows表頭檔案定義了一個清單方塊樣式,叫做LBS_STANDARD,它包含了最常用的樣式,其定義如下:

您也可以採用WS_SIZEBOX和WS_CAPTION識別字,但是這兩個識別字允許您重新定義清單方塊的大小,也允許您在清單方塊父視窗的顯示區域中移動清單方塊。

清單方塊的寬度應該能夠容納最長字串的寬度加上捲動列的寬度。您可以使用:

來獲得垂直捲動列的寬度。您用一個字元的高度乘以想要在視埠中顯示的項目數來計算出清單方塊的高度。

將字串放入清單方塊

建立清單方塊之後,下一步是將字串放入其中,您可以通過呼叫SendMessage為清單方塊視窗訊息處理程式發送訊息來做到這一點。字串通常通過以0開始計數的索引數來引用,其中0對應於最頂上的項目。在下面的例子中,hwndList是子視窗清單方塊控制項的代號,而iIndex是索引值。在使用SendMessage傳遞字串的情況下,lParam參數是指向以null字元結尾字串的指標。

在大多數例子中,當視窗訊息處理程式儲存的清單方塊內容超過了可用記憶體空間時,SendMessage將傳回LB_ERRSPACE(定義為-2)。如果是因為其他原因而出錯,那麼SendMessage將傳回LB_ERR(-1)。如果操作成功,那麼SendMessage將傳回LB_OKAY(0)。您可以通過測試SendMessage的非零值來判斷這兩種錯誤。

如果您採用LBS_SORT樣式(或者如果您在清單方塊中按照想要呈現的順序排列字串),那麼填入清單方塊最簡單的方法是借助LB_ADDSTRING訊息:

如果您沒有採用LBS_SORT,那麼可以使用LB_INSERTSTRING指定一個索引值,將字串插入到清單方塊中:

例如,如果iIndex等於4,那麼szString將變為索引值為4的字串-從頂頭開始算起的第5個字串(因為是從0開始計數的),位於這個點後面的所有字串都將向後推移。索引值為-1時,將字串增加在最後。您可以對樣式為LBS_SORT的清單方塊使用LB_INSERTSTRING,但是這個清單方塊的內容不能被重新排序(您也可以使用LB_DIR訊息將字串插入到清單方塊中,這將在本章的最後進行討論)。

您可以在指定索引值的同時使用LB_DELETESTRING參數,這就可以從清單方塊中刪除字串:

您可以使用LB_RESETCONTENT清除清單方塊中的內容:

當在清單方塊中增加或者刪除字串時,清單方塊視窗訊息處理程式將更新顯示。如果您有許多字串需要增加或者刪除,那麼您也許希望暫時阻止這一動作,其方法是關掉控制項的重畫旗標:

當您完成後,可以再打開重畫旗標:

使用LBS_NOREDRAW樣式建立的清單方塊開始時其重畫旗標是關閉的。

選擇和取得項

SendMessage完成了下面所描述的任務之後,通常傳回一個值。如果出錯,那麼這個值將被設定為LB_ERR(定義為-1)。

當清單方塊中放入一些項目之後,您可以弄清楚清單方塊中有多少項目:

其他一些呼叫對單項選擇清單方塊和多項選擇清單方塊是不同的。讓我們先來看看單項選擇清單方塊。

通常,您讓使用者在清單方塊中選擇條目。但是如果您想加亮顯示一個內定選擇,則可以使用:

將iParam設定為-1則取消所有選擇。

您也可以根據項目的第一個字母來選擇:

在SendMessage呼叫中將iIndex作為iParam參數時,iIndex是索引,可以根據它搜索其開頭字元與szSearchString相匹配的項目。iIndex的值等於-1時從頭開始搜索,SendMessage傳回被選中項目的索引。如果沒有開頭字元與szSearchString相匹配的項目時,SendMessage傳回LB_ERR。

當您得到來自清單方塊的WM_COMMAND訊息時(或者在任何其他時候),您可以使用LB_GETCURSEL來確定目前選項的索引:

如果沒有項目被選中,那麼從呼叫中傳回的iIndex值為LB_ERR。

您可以確定清單方塊中字串的長度:

並可以將某項目複製到文字緩衝區中:

在這兩種情況下,從呼叫傳回的iLength值是字串的長度。對以NULL字元終結的字串長度來說,szBuffer陣列必須夠大。您也許想用LB_GETTEXTLEN先分配一些局部記憶體來存放字串。

對於一個多項選擇清單方塊,您不能使用LB_SETCURSEL、LB_GETCURSEL或者LB_SELECTSTRING,但是您可以使用LB_SETSEL來設定某特定項目的選擇狀態,而不影響有可能被選擇的其他項:

wParam參數不為0時,選擇並加亮某一項目;wParam為0時,取消選擇。如果wParam等於-1,那麼將選擇所有項目或者取消所有被選中的項目。您可以如下確定某特定項目的選擇狀態:

其中,如果由iIndex指定的項目被選中,iSelect被設為非0,否則被設為0。

接收來自清單方塊的訊息

當使用者用滑鼠單擊清單方塊時,清單方塊將接收輸入焦點。下面的操作可以使父視窗將輸入焦點轉交給清單方塊控制項:

當清單方塊擁有輸入焦點時,游標移動鍵、字母鍵和Spacebar鍵都可以用來在該清單方塊中選擇某項。

清單方塊控制項向其父視窗發送WM_COMMAND訊息,對按鈕和編輯控制項來說,wParam和lParam變數的含義是相同的:

SendMessage (hwndEdit, WM_PASTE, 0, 0) ;

SendMessage (hwndEdit, EM_GETSEL, (WPARAM) &iStart, (LPARAM) &iEnd) ;

SendMessage (hwndEdit, EM_SETSEL, iStart, iEnd) ;

SendMessage (hwndEdit, EM_REPLACESEL, 0, (LPARAM) szString) ;

iCount = SendMessage (hwndEdit, EM_GETLINECOUNT, 0, 0) ;

iOffset = SendMessage (hwndEdit, EM_LINEINDEX, iLine, 0) ;

iLength = SendMessage (hwndEdit, EM_LINELENGTH, iLine, 0) ;

iLength = SendMessage (hwndEdit, EM_GETLINE, iLine, (LPARAM) szBuffer) ;

(LBS_NOTIFY | LBS_SORT | WS_VSCROLL | WS_BORDER)

GetSystemMetrics (SM_CXVSCROLL) ;

SendMessage (hwndList, LB_ADDSTRING, 0, (LPARAM) szString) ;

SendMessage (hwndList, LB_INSERTSTRING, iIndex, (LPARAM) szString) ;

SendMessage (hwndList, LB_DELETESTRING, iIndex, 0) ;

SendMessage (hwndList, LB_RESETCONTENT, 0, 0) ;

SendMessage (hwndList, WM_SETREDRAW, FALSE, 0) ;

SendMessage (hwndList, WM_SETREDRAW, TRUE, 0) ;

iCount = SendMessage (hwndList, LB_GETCOUNT, 0, 0) ;

SendMessage (hwndList, LB_SETCURSEL, iIndex, 0) ;

iIndex = SendMessage (hwndList, LB_SELECTSTRING, iIndex, (LPARAM) szSearchString) ;

iIndex = SendMessage (hwndList, LB_GETCURSEL, 0, 0) ;

iLength = SendMessage (hwndList, LB_GETTEXTLEN, iIndex, 0) ;

iLength = SendMessage ( hwndList, LB_GETTEXT, iIndex, (LPARAM) szBuffer) ;

SendMessage (hwndList, LB_SETSEL, wParam, iIndex) ;

iSelect = SendMessage (hwndList, LB_GETSEL, iIndex, 0) ;

SetFocus (hwndList) ;

]

LOWORD (wParam)

HIWORD (wParam)

lParam

子視窗ID

通知碼

子視窗代號

通知碼及其值如下所示:

只有清單方塊視窗樣式包括LBS_NOTIFY時,清單方塊控制項才會向父視窗發送LBN_SELCHANGE和LBN_DBLCLK。

LBN_ERRSPACE表示清單方塊已經超出執行空間。LBN_SELCHANGE表示目前選擇已經被改變。這些訊息出現在下列情況下:使用者在清單方塊中移動加亮的項目時,使用者使用Spacebar鍵切換選擇狀態或者使用滑鼠單擊某項時。LBN_DBLCLK說明某項目已經被滑鼠雙擊(LBN_SELCHANGE和LBN_DBLCLK通知碼的值表示滑鼠按下的次數)。

根據應用的需要,您也許要使用LBN_SELCHANGE或LBN_DBLCLK,也許二者都要使用。您的程式會收到許多LBN_SELCHANGE訊息,但是LBN_DBLCLK訊息只有當使用者雙擊滑鼠時才會出現。如果您的程式使用雙擊,那麼您需要提供一個複製LBN_DBLCLK的鍵盤介面。

一個簡單的清單方塊應用程式

既然您知道了如何建立清單方塊,如何使用文字項目填入清單方塊,如何接收來自清單方塊的控制項以及如何取得字串,現在是到了寫一個應用程式的時候了。如程式9-5中所示,ENVIRON程式在顯示區域中使用清單方塊來顯示目前作業系統環境變數(例如PATH和WINDIR)。當您選擇一個環境變數時,其內容將顯示在顯示區域的頂部。

程式9-5 ENVIRON ENVIRON.C /*------------------------------------------------------------------------- ENVIRON.C -- Environment List Box (c) Charles Petzold, 1998 ---------------------------------------------------------------------------*/ #include <windows.h> #define ID_LIST 1 #define ID_TEXT 2 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("Environ") ; 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) (COLOR_WINDOW + 1) ; 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 ("Environment List Box"), 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 ; } void FillListBox (HWND hwndList) { int iLength ; TCHAR * pVarBlock, * pVarBeg, * pVarEnd, * pVarName ; pVarBlock = GetEnvironmentStrings () ; // Get pointer to environment block while (*pVarBlock) { if (*pVarBlock != '=') // Skip variable names beginning with '=' { pVarBeg = pVarBlock ; // Beginning of variable name while (*pVarBlock++ != '=') ; // Scan until '=' pVarEnd = pVarBlock - 1 ; // Points to '=' sign iLength = pVarEnd - pVarBeg ; // Length of variable name // Allocate memory for the variable name and terminating // zero. Copy the variable name and append a zero. pVarName = calloc (iLength + 1, sizeof (TCHAR)) ; CopyMemory (pVarName, pVarBeg, iLength * sizeof (TCHAR)) ; pVarName[iLength] = '\0' ; // Put the variable name in the list box and free memory. SendMessage (hwndList, LB_ADDSTRING, 0, (LPARAM) pVarName) ; free (pVarName) ; } while (*pVarBlock++ != '\0') ; // Scan until terminating zero } FreeEnvironmentStrings (pVarBlock) ; } LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam) { static HWND hwndList, hwndText ; int iIndex, iLength, cxChar, cyChar ; TCHAR * pVarName, * pVarValue ; switch (message) { case WM_CREATE : cxChar = LOWORD (GetDialogBaseUnits ()) ; cyChar = HIWORD (GetDialogBaseUnits ()) ; // Create listbox and static text windows. hwndList = CreateWindow (TEXT ("listbox"), NULL, WS_CHILD | WS_VISIBLE | LBS_STANDARD, cxChar, cyChar * 3, cxChar * 16 + GetSystemMetrics (SM_CXVSCROLL), cyChar * 5, hwnd, (HMENU) ID_LIST, (HINSTANCE) GetWindowLong (hwnd, GWL_HINSTANCE), NULL) ; hwndText = CreateWindow (TEXT ("static"), NULL, WS_CHILD | WS_VISIBLE | SS_LEFT, cxChar, cyChar, GetSystemMetrics (SM_CXSCREEN), cyChar, hwnd, (HMENU) ID_TEXT, (HINSTANCE) GetWindowLong (hwnd, GWL_HINSTANCE), NULL) ; FillListBox (hwndList) ; return 0 ; case WM_SETFOCUS : SetFocus (hwndList) ; return 0 ; case WM_COMMAND : if (LOWORD (wParam) == ID_LIST && HIWORD (wParam) == LBN_SELCHANGE) { // Get current selection. iIndex = SendMessage (hwndList, LB_GETCURSEL, 0, 0) ; iLength = SendMessage (hwndList, LB_GETTEXTLEN, iIndex, 0) + 1 ; pVarName = calloc (iLength, sizeof (TCHAR)) ; SendMessage (hwndList, LB_GETTEXT, iIndex, (LPARAM) pVarName) ; // Get environment string. iLength = GetEnvironmentVariable (pVarName, NULL, 0) ; pVarValue = calloc (iLength, sizeof (TCHAR)) ; GetEnvironmentVariable (pVarName, pVarValue, iLength) ; // Show it in window. SetWindowText (hwndText, pVarValue) ; free (pVarName) ; free (pVarValue) ; } return 0 ; case WM_DESTROY : PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

ENVIRON建立兩個子視窗:一個是LBS_STANDARD樣式的清單方塊,另一個是SS_LEFT樣式(置左對齊文字)的靜態視窗。ENVIRON使用函式GetEnvironmentStrings來獲得一個指標,該指標指向存有全部環境變數名及其值的記憶體區塊。ENVIRON用FillListBox函式來分析此記憶體區塊,並使用LB_ADDSTRING訊息來指定清單方塊視窗訊息處理程式將每個字串放入清單方塊中。

當您執行ENVIRON時,可以使用滑鼠或者鍵盤來選擇環境變數。每次您改變選擇時,清單方塊都會給其父視窗WndProc發送一個WM_COMMAND訊息。當WndProc收到WM_COMMAND訊息時,它就檢查wParam的低字組是否為ID_LIST(清單方塊的子視窗ID)和wParam的高字組(通知碼)是否等於LBN_SELCHANGE。如果是的,那麼它就使用LB_GETCURSEL訊息來獲得選中項目的索引,並使用LB_GETTEXT來獲得外部環境變數名的字串本身。ENVIRON程式使用C語言函式GetEnvironmentVariable來獲得與變數相對應的環境字串,使用SetWindowText將該字串傳遞到靜態子視窗控制項中,這個靜態子視窗控制項被用來顯示文字。

檔案列表

我將最好的留在最後:LB_DIR,這是功能最強的清單方塊訊息。它用檔案目錄列表填入清單方塊,並且可以選擇將子目錄和有效的磁碟機也包括進來:

使用檔案屬性碼

iAttr參數是檔案屬性代碼,其最低位元組是檔案屬性代碼,該代碼可以是表9-6資料的組合:

SendMessage (hwndList, LB_DIR, iAttr, (LPARAM) szFileSpec) ;

表9-6

高位元組提供了一些對所要求項目的附加控制:

表9-7

字首DDL表示「對話目錄列表」。

當LB_DIR訊息的iAttr值為DDL_READWRITE時,清單方塊列出普通檔案、唯讀檔案和歸檔位元設立的檔案。當值為DDL_DIRECTORY時,清單方塊除了列出上述檔案之外,還列出子目錄,目錄位於中括號之內。當值為DDL_DRIVES | DDL_DIRECTORY時,那麼列表將擴展到包括所有有效的磁碟機,而磁碟機代號顯示在虛線之間。

將iAttr的最高位元設立就可以只列出符合條件的檔案,而不包括其他檔案。例如,對Windows的檔案備份程式,也許您只想列出最後一次備份後修改過的檔案,這種檔案的歸檔位元設立,因此您可以使用DDL_EXCLUSIVE | DDL_ARCHIVE。

檔案列表的排序

lParam參數是指向檔案指定字串如「*.*」的指標,這個檔案指定字串不影響清單方塊中的子目錄。

您也許希望給列有檔案清單的清單方塊使用LBS_SORT訊息。清單方塊首先列出符合檔案指定要求的檔案,再(可選擇)列出子目錄名。列出的第一個子目錄名將採用下面的格式:

[..]

這一個「兩個點」的子目錄項允許使用者向根目錄回溯一層(在根目錄下列出檔案名時此項目不會出現)。最後,具體的子目錄名稱採用下面的形式:

[SUBDIR]

再來是以下列形式列出的有效磁碟機(也是可選擇的):

[-A-]

Windows的head程式

UNIX中有一個著名的實用程式叫做head,它顯示檔案開始的幾行。讓我們使用清單方塊為Windows編寫一個類似的程式。如程式9-6所示,HEAD將所有檔案和子目錄列在清單方塊中。您可以挑選某個被選擇的檔案來顯示,方法是在該檔案上使用滑鼠雙擊或者使用Enter鍵按下要選的檔案。您也可以使用這兩種方法之一來改變子目錄。這個程式在HEAD視窗顯示區域的右邊,從檔案的開頭開始顯示,它最多能夠顯示8 KB的內容。

程式9-6 HEAD HEAD.C /*------------------------------------------------------------------------- HEAD.C -- Displays beginning (head) of file (c) Charles Petzold, 1998 --------------------------------------------------------------------------*/ #include <windows.h> #define ID_LIST 1 #define ID_TEXT 2 #define MAXREAD 8192 #define DIRATTR (DDL_READWRITE | DDL_READONLY | DDL_HIDDEN | DDL_SYSTEM | \ DDL_DIRECTORY | DDL_ARCHIVE | DDL_DRIVES) #define DTFLAGS (DT_WORDBREAK | DT_EXPANDTABS | DT_NOCLIP |DT_NOPREFIX) LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; LRESULT CALLBACK ListProc (HWND, UINT, WPARAM, LPARAM) ; WNDPROC OldList ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("head") ; 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) (COLOR_BTNFACE + 1) ; 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 ("head"), WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN, 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 BOOL bValidFile ; static BYTE buffer[MAXREAD] ; static HWND hwndList, hwndText ; static RECT rect ; static TCHAR szFile[MAX_PATH + 1] ; HANDLE hFile ; HDC hdc ; int i, cxChar, cyChar ; PAINTSTRUCT ps ; TCHAR szBuffer[MAX_PATH + 1] ; switch (message) { case WM_CREATE : cxChar = LOWORD (GetDialogBaseUnits ()) ; cyChar = HIWORD (GetDialogBaseUnits ()) ; rect.left = 20 * cxChar ; rect.top = 3 * cyChar ; hwndList = CreateWindow (TEXT ("listbox"), NULL, WS_CHILDWINDOW | WS_VISIBLE | LBS_STANDARD, cxChar, cyChar * 3, cxChar * 13 + GetSystemMetrics (SM_CXVSCROLL), cyChar * 10, hwnd, (HMENU) ID_LIST, (HINSTANCE) GetWindowLong (hwnd, GWL_HINSTANCE), NULL) ; GetCurrentDirectory (MAX_PATH + 1, szBuffer) ; hwndText = CreateWindow (TEXT ("static"), szBuffer, WS_CHILDWINDOW | WS_VISIBLE | SS_LEFT, cxChar, cyChar, cxChar * MAX_PATH, cyChar, hwnd, (HMENU) ID_TEXT, (HINSTANCE) GetWindowLong (hwnd, GWL_HINSTANCE), NULL) ; OldList = (WNDPROC) SetWindowLong (hwndList, GWL_WNDPROC, (LPARAM) ListProc) ; SendMessage (hwndList, LB_DIR, DIRATTR, (LPARAM) TEXT ("*.*")) ; return 0 ; case WM_SIZE : rect.right = LOWORD (lParam) ; rect.bottom = HIWORD (lParam) ; return 0 ; case WM_SETFOCUS : SetFocus (hwndList) ; return 0 ; case WM_COMMAND : if (LOWORD (wParam) == ID_LIST && HIWORD (wParam) == LBN_DBLCLK) { if (LB_ERR == (i = SendMessage (hwndList, LB_GETCURSEL, 0, 0))) break ; SendMessage (hwndList, LB_GETTEXT, i, (LPARAM) szBuffer) ; if (INVALID_HANDLE_VALUE != (hFile = CreateFile (szBuffer, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL))) { CloseHandle (hFile) ; bValidFile = TRUE ; lstrcpy (szFile, szBuffer) ; GetCurrentDirectory (MAX_PATH + 1, szBuffer) ; if (szBuffer [lstrlen (szBuffer) - 1] != '\\') lstrcat (szBuffer, TEXT ("\\")) ; SetWindowText (hwndText, lstrcat (szBuffer, szFile)) ; } else { bValidFile = FALSE ; szBuffer [lstrlen (szBuffer) - 1] = '\0' ; // If setting the directory doesn't work, maybe it's // a drive change, so try that. if (!SetCurrentDirectory (szBuffer + 1)) { szBuffer [3] = ':' ; szBuffer [4] = '\0' ; SetCurrentDirectory (szBuffer + 2) ; } // Get the new directory name and fill the list box. GetCurrentDirectory (MAX_PATH + 1, szBuffer) ; SetWindowText (hwndText, szBuffer) ; SendMessage (hwndList, LB_RESETCONTENT, 0, 0) ; SendMessage (hwndList, LB_DIR, DIRATTR, (LPARAM) TEXT ("*.*")) ; } InvalidateRect (hwnd, NULL, TRUE) ; } return 0 ; case WM_PAINT : if (!bValidFile) break ; if (INVALID_HANDLE_VALUE == (hFile = CreateFile (szFile, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL))) { bValidFile = FALSE ; break ; } ReadFile (hFile, buffer, MAXREAD, &i, NULL) ; CloseHandle (hFile) ; // i now equals the number of bytes in buffer. // Commence getting a device context for displaying text. hdc = BeginPaint (hwnd, &ps) ; SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ; SetTextColor (hdc, GetSysColor (COLOR_BTNTEXT)) ; SetBkColor (hdc, GetSysColor (COLOR_BTNFACE)) ; // Assume the file is ASCII DrawTextA (hdc, buffer, i, &rect, DTFLAGS) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY : PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; } LRESULT CALLBACK ListProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { if (message == WM_KEYDOWN && wParam == VK_RETURN) SendMessage (GetParent (hwnd), WM_COMMAND, MAKELONG (1, LBN_DBLCLK), (LPARAM) hwnd) ; return CallWindowProc (OldList, hwnd, message, wParam, lParam) ; }

在ENVIRON中,當我們選擇一個環境變數時-無論是使用滑鼠還是鍵盤-程式都將顯示一個環境字串。但是,如果我們在HEAD中使用這種選擇顯示方法,那麼程式回應會很慢,這是因為在清單方塊中移動選擇時,程式仍然要不斷地打開和關閉檔案。然而,HEAD要求檔案或者子目錄被雙擊,從而引起一些問題,這是因為清單方塊控制項沒有滑鼠雙擊的自動鍵盤介面。前面講過,如果可能,應該儘量提供鍵盤介面。

解決的方法是什麼呢?當然是視窗子類別化。HEAD中的清單方塊子類則函式叫做ListProc,它尋找wParam參數等於VK_RETURN的WM_KEYDOWN訊息,並給其父視窗發送一條帶有LBN_DBLCLK通知碼的WM_COMMAND訊息。在WndProc中,對WM_COMMAND的處理使用了Windows函式的CreateFile來檢查清單方塊中的選擇。如果CreateFile傳回一個錯誤資訊,則表示該選擇不是檔案,而可能是一個子目錄。然後HEAD使用SetCurrentDirectory來改變這個子目錄。如果SetCurrentDirectory不能執行,程式將假定使用者已經選擇了一個磁碟機代號。改變磁碟機也需要呼叫SetCurrentDirectory,作為該函式參數的字串則為是選擇字串中拿掉開頭的斜線,並加上一個冒號。它向清單方塊發送一條LB_RESETCONTENT訊息來清除其中的內容,再發送一條LB_DIR訊息,使用新子目錄中的檔案來填入清單方塊。

WndProc中的WM_PAINT訊息是用Windows的CreateFile函式來打開檔案的,這將傳回一個檔案代號,該代號可以傳遞給Windows的ReadFile和CloseHandle函式。

現在,在本章中,我們第一次碰到這個問題:Unicode。我們所希望最完美的方式大概就是讓作業系統辨認文字檔案的種類,使ReadFile能將ASCII檔案轉換成Unicode文字,或者將Unicode檔案轉換成ASCII文字。但現實並非如此完美。ReadFile的功能只是讀取檔案中未經轉換的位元組,也就是說,DrawTextA(在編譯好的可執行檔中沒有定義UNICODE識別字)會把文字解釋為ASCII,而DrawTextW(Unicode版)會假設文字是Unicode的。

因此程式真正應該做的是去判別檔案所包含的是ASCII文字還是Unicode文字,然後再恰當地呼叫DrawTextA或者DrawTextW。實際上,HEAD採用一個比較簡單的方式,它只呼叫了DrawTextA。