子窗口控件 |
|
回忆第七章的CHECKER程序。这些程序显示了矩形网格。当您在一个矩形中按下鼠标按键时,该程序就画一个x;如果您再按一次鼠标按键,那么x就消失。虽然这个程序的CHECKER1和CHECKER2版本只使用一个主窗口,但CHECKER3版本却为每个矩形使用一个子窗口。这些矩形由一个叫做ChildProc的独立窗口消息处理程序维护。
如果有必要,无论矩形是否被选中,都可以给ChildProc增加一种向其父窗口消息处理程序(WndProc)发送消息的手段。通过呼叫GetParent,子窗口消息处理程序能确定其父窗口的窗口句柄:
hwndParent = GetParent (hwnd) ;
其中,hwnd是子窗口的窗口句柄。它可以向其父窗口消息处理程序发送消息:
SendMessage (hwndParent, message, wParam, lParam) ;
那么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种标准按钮样式。
BTNLOOK.C /*-------------------------------------------------------------------------- BTNLOOK.C -- Button Look Program (c) Charles Petzold, 1998 ---------------------------------------------------------------------------*/ #includestruct { 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)按钮。
图9-1 BTNLOOK的屏幕显示 |
建立子窗口
BTNLOOK定义了一个叫做button的结构,它包括了按钮窗口样式和描述性字符串,它们对应于10个按钮型态,所有按钮窗口样式都以字母「BS」开头,它表示「按钮样式」。10个按钮子窗口是在WndProc中处理WM_CREATE消息的过程中使用一个for循环建立的。CreateWindow呼叫使用下面这些参数:
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中,您只需在建立主窗口之前设定:
hInst = hInstance ;
在第七章中的CHECKER3程序中,我们曾用GetWindowLong取得执行实体句柄:
GetWindowLong (hwnd, GWL_HINSTANCE)
这几种方法都是正确的。)
在呼叫CreateWindow之后,我们不必再为这些子窗口做任何事情,由Windows中的按钮窗口消息处理程序负责维护它们,并处理所有的重画工作(BS_OWNERDRAW样式的按钮例外,它要求程序绘制它,这些将在后面加以讨论)。在程序终止时,如果父窗口已经被清除,那么Windows将清除这些子窗口。
子窗口向父窗口发消息
当您执行BTNLOOK时,将看到在显示区域的左边会显示出不同的按钮型态。我在前面已经提到过,用鼠标单击按钮时,子窗口控件就向其父窗口发送一个WM_COMMAND消息。BTNLOOK拦截WM_COMMAND消息并显示wParam和lParam的值,它们的含义如下:
LOWORD (wParam) HIWORD (wParam) lParam |
子窗口ID 通知码 子窗口句柄 |
如果您正在移植16位Windows程序,那么要注意改变这些消息参数以容纳32位的句柄。
子窗口ID是在建立子窗口时传递给CreateWindow的值。在BTNLOOK中,这些ID被显示在显示区域中,并使用0到9分别标识10个按钮。子窗口句柄是Windows从CreateWindow传回的值。
通知码更详细表示了消息的含义。按钮通知码的可能值在Windows表头文件中定义如下:
表9-1 |
按钮通知码标识符 |
值 |
BN_CLICKED |
0 |
BN_PAINT |
1 |
BN_HILITE or BN_PUSHED |
2 |
BN_UNHILITE or BN_UNPUSHED |
3 |
BN_DISABLE |
4 |
BN_DOUBLECLICKED or BN_DBLCLK |
5 |
BN_SETFOCUS |
6 |
BN_KILLFOCUS |
7 |
实际上,您不会看到这些按钮值中的大多数。从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 |
0x00F0 |
BM_SETCHECK |
0x00F1 |
BM_GETSTATE |
0x00F2 |
BM_SETSTATE |
0x00F3 |
BM_SETSTYLE |
0x00F4 |
BM_CLICK |
0x00F5 |
BM_GETIMAGE |
0x00F6 |
BM_SETIMAGE |
0x00F7 |
BM_GETCHECK和BM_SETCHECK消息由父窗口发送给子窗口控件,以取得或者设定复选框和单选按钮的选中标记。BM_GETSTATE和BM_SETSTATE消息表示按钮处于正常状态还是(鼠标或Spacebar键按下时的)「按下」状态。我们将在讨论按钮的每种型态时,看到这些消息是如何起作用的。BM_SETSTYLE消息允许您在按钮建立之后改变按钮样式。
每个子窗口控件都具有一个在其兄弟中唯一的窗口句柄和ID值。对于句柄和ID这两者,知道其中的一个您就可以获得另一个。如果您知道子窗口控件的窗口句柄,那么您可以用下面的叙述来获得ID:
id = GetWindowLong (hwndChild, GWL_ID) ;
第七章的CHECKER3程序曾用此函数(与SetWindowLong一起)来维护注册窗口类别时保留的特殊区域的数据。在建立子窗口时,Windows保留了GWL_ID标识符存取的数据。您也可以使用:
id = GetDlgCtrlID (hwndChild) ;
虽然函数中的「Dlg」部分指的是对话框,但实际上这是一个通用的函数。
知道ID和父窗口句柄,您就能获得子窗口句柄:
hwndChild = GetDlgItem (hwndParent, 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消息来仿真按钮闪动。以下的操作将导致按钮被按下:
SendMessage (hwndButton, BM_SETSTATE, 1, 0) ;
下面的呼叫使按钮恢复正常:
SendMessage (hwndButton, BM_SETSTATE, 0, 0) ;
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 ((HWND) lParam, BM_SETCHECK, (WPARAM) !SendMessage ((HWND) lParam, BM_GETCHECK, 0, 0), 0) ;
注意第二个SendMessage呼叫前面的运算子「!」,其中lParam是在WM_COMMAND消息中传给使用者窗口消息处理程序的子窗口句柄。如果您以后又想知道按钮的状态,那么可以向它发送另一条BM_GETCHECK消息;您也可以将目前状态储存在您的窗口消息处理程序中的一个静态变量里,或者向它发送BM_SETCHECK消息来初始化带勾选标记的BS_CHECKBOX复选框:
SendMessage (hwndButton, BM_SETCHECK, 1, 0) ;
对BS_AUTOCHECKBOX样式,按钮自己触发勾选标记的开和关,所以您的窗口消息处理程序可以忽略WM_COMMAND消息。当您需要按钮目前的状态时,可以向控件发送BM_GETCHECK消息:
iCheck = (int) SendMessage (hwndButton, BM_GETCHECK, 0, 0) ;
如果该按钮被选中,则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消息来显示其选中状态:
SendMessage (hwndButton, BM_SETCHECK, 1, 0) ;
对同组中的其它所有单选按钮,您可以通过向它们发送wParam等于0的BM_SETCHECK消息来显示其未选中状态:
SendMessage (hwndButton, BM_SETCHECK, 0, 0) ;
分组方块
分组方块即样式为BS_GROUPBOX的选择框,它是按钮类中的特例,既不处理鼠标输入和键盘输入,也不向其父窗口发送WM_COMMAND消息。分组方块是一个矩形框,分组方块标题在其顶部显示。分组方块常用来包含其它的按钮控件。
改变按钮文字
您可以通过SetWindowText来改变按钮(或者其它任何窗口)内的文字:
SetWindowText (hwnd, pszString) ;
其中hwnd是欲改变窗口的句柄,pszString是一个指向以null为终结的字符串指针。对于一般的窗口来说,这个文字是标题列的文字;对于按钮控件来说,它是随着该按钮显示的文字。
您也可以取得窗口目前的文字:
iLength = GetWindowText (hwnd, pszBuffer, iMaxLength) ;
iMaxLength指定复制到pszBuffer指向的缓冲区中的最大字符数。该函数传回复制的字符数。您可以首先通过下面的呼叫来获得特定文字的长度:
iLength = GetWindowTextLength (hwnd) ;
可见的和启用的按钮
为了接收鼠标和键盘输入,子窗口必须是可见的(被显示)和被启用的。当窗口是可见的而未被启用时,那么窗口将以灰色而非黑色显示文字。
如果在建立子窗口时,您没有将WS_VISIBLE包含在窗口类别中,那么直到呼叫ShowWindow时子窗口才会被显示出来:
ShowWindow (hwndChild, SW_SHOWNORMAL) ;
如果您将WS_VISIBLE包含在窗口类别中,就没有必要呼叫ShowWindow。但是,您可以通过呼叫ShowWindow将子窗口隐藏起来:
ShowWindow (hwndChild, SW_HIDE) ;
您可以通过下面的呼叫来确定子窗口是否可见:
IsWindowVisible (hwndChild) ;
您也可以使子窗口被启用或者不被启用。在内定情况下,窗口是被启用的。您可以通过下面的呼叫使窗口不被启用:
EnableWindow (hwndChild, FALSE) ;
对于按钮控件,这具有使按钮字符串变成灰色的作用。按钮将不再对鼠标输入和键盘输入做出响应,这是表示按钮选项目前不可用的最好方法。
您可以通过下面的呼叫使子窗口再次被启用:
EnableWindow (hwndChild, TRUE) ;
您还可以使用下面的呼叫来确定子窗口是否被启用:
IsWindowEnabled (hwndChild) ;
按钮和输入焦点
我在本章前面已经提到过,当用鼠标单击按钮、复选框、单选框和拥有者绘制按钮时,它们接收到输入焦点。这些控件使用文字周围的虚线来表示它拥有了输入焦点。当子窗口控件得到输入焦点时,其父窗口就失去了输入焦点;所有的键盘输入都进入子窗口控件,而不会进入父窗口中。但是,子窗口控件只对Spacebar键作出回应,此时Spacebar键的作用就如同鼠标按键一样。这种情形导致了一个明显的问题:您的程序失去了对键盘处理的控件。让我们看看我们对此能做一些什么。
我在第六章中已经提到过,当Windows将输入焦点从一个窗口(例如一个父窗口)转换到另一个窗口(例如一个子窗口控件)时,它首先给正在失去输入焦点的窗口发送一个WM_KILLFOCUS消息,wParam参数是接收输入焦点的窗口的句柄。然后,Windows向正在接收输入焦点的窗口发送一个WM_SETFOCUS消息,同时wParam是还在失去输入焦点的窗口的句柄(在这两种情况中,wParam值可能为NULL,它表示没有窗口拥有或者正在接收输入焦点)。
通过处理WM_KILLFOCUS消息,父窗口可以阻止子窗口控件获得输入焦点。假定数组hwndChild包含了所有子窗口的窗口句柄(它们是在呼叫CreateWindow来建立窗口的时候储存到数组中的)。 NUM是子窗口的数目:
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结构中就是这样定义的。
wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
我们已经这么做了,因为我们经常在显示区域中显示文字,而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结束)顺序排列的:
表9-3 |
GetSysColor和SetSysColors |
系统登录键或WIN.INI标识符 |
内定的RGB值 |
COLOR_SCROLLBAR |
Scrollbar |
C0-C0-C0 |
COLOR_BACKGROUND |
Background |
00-80-80 |
COLOR_ACTIVECAPTION |
ActiveTitle |
00-00-80 |
COLOR_INACTIVECAPTION |
InactiveTitle |
80-80-80 |
COLOR_MENU |
Menu |
C0-C0-C0 |
COLOR_WINDOW |
Window |
FF-FF-FF |
COLOR_WINDOWFRAME |
WindowFrame |
00-00-00 |
COLOR_MENUTEXT |
MenuText |
C0-C0-C0 |
COLOR_WINDOWTEXT |
WindowText |
00-00-00 |
COLOR_CAPTIONTEXT |
TitleText |
FF-FF-FF |
COLOR_ACTIVEBORDER |
ActiveBorder |
C0-C0-C0 |
COLOR_INACTIVEBORDER |
InactiveBorder |
C0-C0-C0 |
COLOR_APPWORKSPACE |
AppWorkspace |
80-80-80 |
COLOR_HIGHLIGHT |
Highlight |
00-00-80 |
COLOR_HIGHLIGHTTEXT |
HighlightText |
FF-FF-FF |
COLOR_BTNFACE |
ButtonFace |
C0-C0-C0 |
COLOR_BTNSHADOW |
ButtonShadow |
80-80-80 |
COLOR_GRAYTEXT |
GrayText |
80-80-80 |
COLOR_BTNTEXT |
ButtonText |
00-00-00 |
COLOR_INACTIVECAPTIONTEXT |
InactiveTitleText |
C0-C0-C0 |
COLOR_BTNHIGHLIGHT |
ButtonHighlight |
FF-FF-FF |
COLOR_3DDKSHADOW |
ButtonDkShadow |
00-00-00 |
COLOR_3DLIGHT |
ButtonLight |
C0-C0-C0 |
COLOR_INFOTEXT |
InfoText |
00-00-00 |
COLOR_INFOBK |
InfoWindow |
FF-FF-FF |
[no identifier; use value 25] |
ButtonAlternateFace |
B8-B4-B8 |
COLOR_HOTLIGHT |
HotTrackingColor |
00-00-FF |
COLOR_GRADIENTACTIVECAPTION |
GradientActiveTitle |
00-00-80 |
COLOR_GRADIENTINACTIVECAPTION |
GradientInactiveTitle |
80-80-80 |
这29种颜色的默认值是由显示驱动程序提供的,在不同的机器上可能略有不同。
坏消息:虽然这些颜色中有许多似乎都可以从颜色常数名称上了解其代表意义(例如,COLOR_BACKGROUND是所有窗口后面的桌面区域颜色),在最近版本的Windows中系统颜色的使用变得非常混乱。以前,Windows在视觉上要比今天简单得多。实际上,在Windows 3.0以前,只定义了前13种系统颜色。但随着使用看起来越来越难以控制的立体外观,相对应地也需要更多的系统颜色。
按钮颜色
对需要多种颜色的每一个按钮来说,这个问题更加地明显。COLOR_BTNFACE被用于按键主要的表面颜色,以及其它按钮主要的背景颜色(这也是用于对话框和消息框的系统颜色)。COLOR_BTNSHADOW被建议用作按键右下边、以及复选框内部和单选按钮圆点的阴影。对于按键,COLOR_BTNTEXT被用作文字颜色;而对于其它的按钮,则使用COLOR_WINDOWTEXT作为文字颜色。还有其它几种系统颜色用于按钮设计的各个部分。
因此,如果您想在我们的显示区域表面显示按钮,那么一种避免颜色冲突的方法便是屈服于这些系统颜色。首先,在定义窗口类别时使用COLOR_BTNFACE作为您显示区域的背景颜色:
wndclass.hbrBackground = (HBRUSH) (COLOR_BTNFACE + 1) ;
您可以在BTNLOOK程序中尝试这种方法。当WNDCLASS结构中的hbrBackground值是这个值时,Windows会明白这实际上指的是一种系统颜色而非一个实际的句柄。Windows要求当您在WNDCLASS结构的hbrBackground栏中指定这些标识符时加上1,这样做的目的是防止其值为NULL,而没有任何其它目的。如果您的在程序执行过程中,系统颜色恰好发生了变化,那么显示区域将变得无效,而Windows将使用新的COLOR_BTNFACE值。但是现在我们又引发了另一个问题。当您使用TextOut显示文字时,Windows使用的是在设备内容中为背景颜色(它擦除文字后的背景)和文字颜色定义的值,其默认值为白色(背景)和黑色(文字),而不管系统颜色和窗口类别结构中的hbrBackground字段为何值。所以,您需要使用SetTextColor和SetBkColor将文字和文字背景的颜色改变为系统颜色。您可以在获得设备内容句柄之后这么做:
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所展示的那样。
OWNDRAW.C /*------------------------------------------------------------------------ OWNDRAW.C -- Owner-Draw Button Demo Program (c) Charles Petzold, 1996 -------------------------------------------------------------------------*/ #include#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样式的按钮,它允许完全自由地绘制按钮。
图9-2 OWNDRAW的屏幕显示 |
在处理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」样式(右列)是没有填入图样的矩形轮廓:
SS_BLACKRECT SS_GRAYRECT SS_WHITERECT |
SS_BLACKFRAME SS_GRAYFRAME SS_WHITEFRAME |
「BLACK」、「GRAY」、「WHITE」并不意味着黑、灰和白色,这些颜色是由系统颜色决定的,如表9-4所示。
表9-4 |
静态控件 |
系统颜色 |
BLACK |
COLOR_3DDKSHADOW |
GRAY |
COLOR_BTNSHADOW |
WHITE |
COLOR_BTNHIGHLIGHT |
对这些样式,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取得水平滚动条的高度:
GetSystemMetrics (SM_CYHSCROLL) ;
或者垂直滚动条的宽度:
GetSystemMetrics (SM_CXVSCROLL) ;
根据Windows文件,滚动条窗样式标识符SBS_LEFTALIGN、SBS_RIGHTALIGN、SBS_TOP ALIGN和SBS_BOTTOMALIGN给出滚动条的标准尺寸,但是这些样式只在对话框中对滚动条有效。
对窗口滚动条,您可以使用同样的呼叫来建立滚动条控件的范围和位置:
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」。当您挪动滚动条时,显示区域的右半部将变为三种原色混合而成的合成色,三种原色的数值显示在三个滚动条的下面。
COLORS1.C /*------------------------------------------------------------------------ COLORS1.C -- Colors Using Scroll Bars (c) Charles Petzold, 1998 -------------------------------------------------------------------------*/ #includeLRESULT 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所示。不幸的是,这些颜色在印表纸上被显示为不同深浅的灰色。
图9-3 COLORS1的屏幕显示 |
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:
i = GetWindowLong ((HWND) lParam, GWL_ID) ;
对于这三个滚动条,我们已经按习惯将其ID设为0、1、2,所以WndProc能区别出是哪个滚动条在产生消息。
由于子窗口的句柄在建立时就被储存在数组中,所以WndProc就能对相对应的滚动条消息进行处理,并通过呼叫SetScrollPos来设定相对应的新值:
SetScrollPos (hwndScroll[i], SB_CTL, color[i], TRUE) ;
WndProc也改变滚动条底部子窗口的文字:
wsprintf (szBuffer, TEXT ("%i"), color[I]) ; SetWindowText (hwndValue[i], szBuffer) ;
自动键盘接口
滚动条控件也能处理键盘输入,但是只有在拥有输入焦点时才行。下表说明怎样将键盘光标键转变为卷动消息:
表9-5 |
游标键 |
卷动消息的wParam值 |
Home |
SB_TOP |
End |
SB_BOTTOM |
Page Up |
SB_PAGEUP |
Page Down |
SB_PAGEDOWN |
左或上 |
SB_LINEUP |
右或下 |
SB_LINEDOWN |
事实上,SB_TOP和SB_BOTTOM卷动消息只能用键盘产生。在使用鼠标按动卷动列时,如果想使该卷动列获得输入焦点,那么您必须将WS_TABSTOP标识符包含到CreateWindow呼叫的窗口类别参数中。当滚动条拥有输入焦点时,在该滚动条的小方框上将显示一个闪烁的灰色块。
为了给滚动条提供全面的键盘接口,还需要另外一些工作。首先,WndProc窗口消息处理程序必须使滚动条拥有输入焦点,它是通过处理WM_SETFOCUS消息来完成这一点的,该WM_SETFOCUS消息是当滚动条获得输入焦点时其父窗口接收到的。WndProc给其中一个滚动条设定输入焦点。
SetFocus (hwndScroll[idFocus]) ;
其中idFocus是一个整体变量。
但是,还需要一些借助键盘尤其是Tab键,来从一个滚动条转换到另一个滚动条的方法。这比较困难,因为一旦某个滚动条拥有了输入焦点,它就处理所有的键盘输入,但滚动条只关心光标键,而忽略Tab键。解决这一两难处境的方法是「窗口子类别化」。我们将用它来给COLORS1增加使用Tab键从一个滚动条跳到另一个滚动条的功能。
窗口子类别化(Window Subclassing)
滚动条控件的窗口消息处理程序是Windows内部的。但是,将GWL_WNDPROC标识符作为参数来呼叫GetWindowLong,您就可以得到这个窗口消息处理程序的地址。另外,您可以呼叫SetWindowLong给该滚动条设定一个新的窗口消息处理程序,这个技术叫做「窗口子类别化」,非常有用。它能让您给现存的窗口消息处理程序设定「挂勾」,以便在自己的程序中处理一些消息,同时将其它所有消息传递给旧的窗口消息处理程序。
在COLORS1中对卷动消息进行初步处理的窗口消息处理程序叫做ScrollProc,它在COLORS1.C文件的尾部。由于ScrollProc是COLORS1中的函数,而Windows将呼叫COLORS1,所以ScrollProc必须被定义为callback函数。
对三个滚动条中的每一个,COLORS1使用SetWindowLong来设定新的滚动条窗口消息处理程序的地址,并取得现存滚动条窗口消息处理程序的地址:
OldScroll[i] = (WNDPROC) SetWindowLong (hwndScroll[i], GWL_WNDPROC, (LONG) ScrollProc)) ;
现在,函数ScrollProc得到了Windows发送到COLORS1中三个滚动条(当然不是其它程序中的滚动条)的滚动条窗口消息处理程序的全部消息。ScrollProc窗口消息处理程序在接收到Tab或者Shift-Tab键时,就将输入焦点改变到下一个(或者上一个)滚动条。它使用CallWindowProc呼叫旧的滚动条窗口消息处理程序。
给背景着色
当COLORS1定义它的窗口类别时,也为其显示区域背景定义了一个实心的黑色画刷:
wndclass.hbrBackground = CreateSolidBrush (0) ;
当您改变COLORS1的滚动条设定时,程序必须建立一个新的画刷,并将该新画刷句柄放入窗口类别结构中。如同使用GetWindowLong和SetWindowLong能得到并设定滚动条窗口消息处理程序一样,用GetClassWord和SetClassWord能得到这个画刷的句柄。
您可以建立新的画刷并将其句柄插入窗口类别结构中,然后删除旧的画刷:
DeleteObject ((HBRUSH) SetClassLong (hwnd, GCL_HBRBACKGROUND, (LONG) CreateSolidBrush (RGB (color[0], color[1], color[2])))) ;
Windows下一次重新为窗口的背景着色时,将使用这个新画刷。为了强迫Windows抹掉背景,我们将使整个显示区域无效:
InvalidateRect (hwnd, &rcColor, TRUE) ;
TRUE(非零)值作为第三个参数,表示希望在重新着色之前删去背景。
InvalidateRect使Windows在窗口消息处理程序的消息队列中放进一个WM_PAINT消息。由于WM_PAINT消息的优先等级比较低,所以,如果您还在使用鼠标或者光标键移动滚动条的话,这个消息将不会立即被处理。如果您想在颜色改变之后使该窗口立即变成最新的(目前的),那么您可以在InvalidateRect之后增加下面的叙述:
UpdateWindow (hwnd) ;
但这会使得键盘和鼠标处理变慢。
COLORS1中的WndProc函数不处理WM_PAINT消息,而是将其传给DefWindowProc。Windows对WM_PAINT消息的内定处理只是呼叫BeginPaint和EndPaint使窗口生效。因为在InvalidateRect呼叫中已经指定背景要被抹掉,所以BeginPaint呼叫使Windows发出一个WM_ERASEBKGND(删除背景)消息,WndProc也将忽略这个消息。Windows用窗口类别中指定的画刷将显示区域的背景抹去,这样就处理了这个消息。
在终止以前进行清除总是一个好主意,因此在处理WM_DESTROY消息处理期间,再一次呼叫DeleteObject:
DeleteObject ((HBRUSH) SetClassLong (hwnd, GCL_HBRBACKGROUND, (LONG) GetStockObject (WHITE_BRUSH))) ;
给滚动条和静态文字着色
在COLORS1中,三个滚动条的内部和六个文字字段中的文字着色为红、绿和蓝色。滚动条的着色是通过处理WM_CTLCOLORSCROLLBAR消息来完成的。
在WndProc中,我们为画刷定义了一个由三个句柄组成的静态数组:
static HBRUSH hBrush [3] ;
在处理WM_CREATE期间,我们建立三个画刷:
for (I = 0 ; I < 3 ; I++) hBrush[0] = CreateSolidBrush (crPrim [I]) ;
其中crPrim数组中包含三种原色的RGB值。在WM_CTLCOLORSCROLLBAR处理期间窗口消息处理程序传回这三画刷中的一个:
case WM_CTLCOLORSCROLLBAR: i = GetWindowLong ((HWND) lParam, GWL_ID) ; return (LRESULT) hBrush [i] ;
在处理WM_DESTROY消息的过程中,这些画刷必须被删除:
for (i = 0 ; i < 3 ; i++) DeleteObject (hBrush [i])) ;
同样地,静态文字字段中的文字是在处理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程序将逐步增强以使用菜单、对话框(加载与储存文件)和打印。最后的版本将是一个简单而完整的文字编辑器,且其程序代码将非常简洁。
POPPAD1.C /*--------------------------------------------------------------------------- POPPAD1.C -- Popup Editor using child window edit box (c) Charles Petzold, 1998 ---------------------------------------------------------------------------*/ #include#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来定义的。编辑控件的尺寸被简单地设定为主窗口的尺寸:
MoveWindow (hwndEdit, 0, 0, LOWORD (lParam), HIWORD (lParam), TRUE) ;
对于单行编辑控件,控件的高度必须可以容纳一个字符。如果编辑控件有边界(大多数都有),那么使用一个字符高度的1.5倍(包括外部间距)。
编辑控件通知
编辑控件给父窗口消息处理程序发送WM_COMMAND消息,对按钮控件来说,wParam和lParam变量的含义是相同的:
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将选择的内容从编辑控件中删除,但是不向剪贴簿中发送。
您也可以将剪贴簿上的文字插入到编辑控件中的光标位置:
SendMessage (hwndEdit, WM_PASTE, 0, 0) ;
您可以取得目前选择的起始位置和末尾位置:
SendMessage (hwndEdit, EM_GETSEL, (WPARAM) &iStart, (LPARAM) &iEnd) ;
结束位置实际上是最后一个选择字符的位置加1。
您可以选择文字:
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) ;
行数从0开始计算,iLine值为-1时传回包含游标所在行的偏移量。您可以取得行的长度:
iLength = SendMessage (hwndEdit, EM_LINELENGTH, iLine, 0) ;
并将行本身复制到一个缓冲区中:
iLength = SendMessage (hwndEdit, EM_GETLINE, iLine, (LPARAM) szBuffer) ;
清单方块类别
我在本章讨论的最后一个预先定义子窗口控件是清单方块。一个清单方块是字符串的集合,这些字符串是一个矩形中可以卷动显示的清单。-程序通过向清单方块窗口消息处理程序发送消息,可以在清单中增加或者删除字符串。当清单方块中的某项被选择时,清单方块控件就向其父窗口发送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,它包含了最常用的样式,其定义如下:
(LBS_NOTIFY | LBS_SORT | WS_VSCROLL | WS_BORDER)
您也可以采用WS_SIZEBOX和WS_CAPTION标识符,但是这两个标识符允许您重新定义清单方块的大小,也允许您在清单方块父窗口的显示区域中移动清单方块。
清单方块的宽度应该能够容纳最长字符串的宽度加上滚动条的宽度。您可以使用:
GetSystemMetrics (SM_CXVSCROLL) ;
来获得垂直滚动条的宽度。您用一个字符的高度乘以想要在视端口中显示的项目数来计算出清单方块的高度。
将字符串放入清单方块
建立清单方块之后,下一步是将字符串放入其中,您可以通过呼叫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消息:
SendMessage (hwndList, LB_ADDSTRING, 0, (LPARAM) szString) ;
如果您没有采用LBS_SORT,那么可以使用LB_INSERTSTRING指定一个索引值,将字符串插入到清单方块中:
SendMessage (hwndList, LB_INSERTSTRING, iIndex, (LPARAM) szString) ;
例如,如果iIndex等于4,那么szString将变为索引值为4的字符串-从顶头开始算起的第5个字符串(因为是从0开始计数的),位于这个点后面的所有字符串都将向后推移。索引值为-1时,将字符串增加在最后。您可以对样式为LBS_SORT的清单方块使用LB_INSERTSTRING,但是这个清单方块的内容不能被重新排序(您也可以使用LB_DIR消息将字符串插入到清单方块中,这将在本章的最后进行讨论)。
您可以在指定索引值的同时使用LB_DELETESTRING参数,这就可以从清单方块中删除字符串:
SendMessage (hwndList, LB_DELETESTRING, iIndex, 0) ;
您可以使用LB_RESETCONTENT清除清单方块中的内容:
SendMessage (hwndList, LB_RESETCONTENT, 0, 0) ;
当在清单方块中增加或者删除字符串时,清单方块窗口消息处理程序将更新显示。如果您有许多字符串需要增加或者删除,那么您也许希望暂时阻止这一动作,其方法是关掉控件的重画旗标:
SendMessage (hwndList, WM_SETREDRAW, FALSE, 0) ;
当您完成后,可以再打开重画旗标:
SendMessage (hwndList, WM_SETREDRAW, TRUE, 0) ;
使用LBS_NOREDRAW样式建立的清单方块开始时其重画旗标是关闭的。
选择和取得项
SendMessage完成了下面所描述的任务之后,通常传回一个值。如果出错,那么这个值将被设定为LB_ERR(定义为-1)。
当清单方块中放入一些项目之后,您可以弄清楚清单方块中有多少项目:
iCount = SendMessage (hwndList, LB_GETCOUNT, 0, 0) ;
其它一些呼叫对单项选择清单方块和多项选择清单方块是不同的。让我们先来看看单项选择清单方块。
通常,您让使用者在清单方块中选择条目。但是如果您想加亮显示一个内定选择,则可以使用:
SendMessage (hwndList, LB_SETCURSEL, iIndex, 0) ;
将iParam设定为-1则取消所有选择。
您也可以根据项目的第一个字母来选择:
iIndex = SendMessage (hwndList, LB_SELECTSTRING, iIndex, (LPARAM) szSearchString) ;
在SendMessage呼叫中将iIndex作为iParam参数时,iIndex是索引,可以根据它搜索其开头字符与szSearchString相匹配的项目。iIndex的值等于-1时从头开始搜索,SendMessage传回被选中项目的索引。如果没有开头字符与szSearchString相匹配的项目时,SendMessage传回LB_ERR。
当您得到来自清单方块的WM_COMMAND消息时(或者在任何其它时候),您可以使用LB_GETCURSEL来确定目前选项的索引:
iIndex = SendMessage (hwndList, LB_GETCURSEL, 0, 0) ;
如果没有项目被选中,那么从呼叫中传回的iIndex值为LB_ERR。
您可以确定清单方块中字符串的长度:
iLength = SendMessage (hwndList, LB_GETTEXTLEN, iIndex, 0) ;
并可以将某项目复制到文字缓冲区中:
iLength = SendMessage ( hwndList, LB_GETTEXT, iIndex, (LPARAM) szBuffer) ;
在这两种情况下,从呼叫传回的iLength值是字符串的长度。对以NULL字符终结的字符串长度来说,szBuffer数组必须够大。您也许想用LB_GETTEXTLEN先分配一些局部内存来存放字符串。
对于一个多项选择清单方块,您不能使用LB_SETCURSEL、LB_GETCURSEL或者LB_SELECTSTRING,但是您可以使用LB_SETSEL来设定某特定项目的选择状态,而不影响有可能被选择的其它项:
SendMessage (hwndList, LB_SETSEL, wParam, iIndex) ;
wParam参数不为0时,选择并加亮某一项目;wParam为0时,取消选择。如果wParam等于-1,那么将选择所有项目或者取消所有被选中的项目。您可以如下确定某特定项目的选择状态:
iSelect = SendMessage (hwndList, LB_GETSEL, iIndex, 0) ;
其中,如果由iIndex指定的项目被选中,iSelect被设为非0,否则被设为0。
接收来自清单方块的消息
当使用者用鼠标单击清单方块时,清单方块将接收输入焦点。下面的操作可以使父窗口将输入焦点转交给清单方块控件:
SetFocus (hwndList) ;
当清单方块拥有输入焦点时,光标移动键、字母键和Spacebar键都可以用来在该清单方块中选择某项。
清单方块控件向其父窗口发送WM_COMMAND消息,对按钮和编辑控件来说,wParam和lParam变量的含义是相同的:
] |
|
LOWORD (wParam) |
子窗口ID |
HIWORD (wParam) |
通知码 |
lParam |
子窗口句柄 |
通知码及其值如下所示:
LBN_ERRSPACE |
-2 |
LBN_SELCHANGE |
1 |
LBN_DBLCLK |
2 |
LBN_SELCANCEL |
3 |
LBN_SETFOCUS |
4 |
LBN_KILLFOCUS |
5 |
只有清单方块窗口样式包括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)。当您选择一个环境变量时,其内容将显示在显示区域的顶部。
ENVIRON.C /*------------------------------------------------------------------------- ENVIRON.C -- Environment List Box (c) Charles Petzold, 1998 ---------------------------------------------------------------------------*/ #include#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,这是功能最强的清单方块消息。它用文件目录列表填入清单方块,并且可以选择将子目录和有效的磁盘驱动器也包括进来:
SendMessage (hwndList, LB_DIR, iAttr, (LPARAM) szFileSpec) ;
使用文件属性码
iAttr参数是文件属性代码,其最低字节是文件属性代码,该代码可以是表9-6数据的组合:
表9-6 |
iAttr |
值 |
属性 |
DDL_READWRITE |
0x0000 |
普通文件 |
DDL_READONLY |
0x0001 |
只读文件 |
DDL_HIDDEN |
0x0002 |
隐藏文件 |
DDL_SYSTEM |
0x0004 |
系统文件 |
DDL_DIRECTORY |
0x0010 |
子目录 |
DDL_ARCHIVE |
0x0020 |
归档位设立的档案 |
高字节提供了一些对所要求项目的附加控制:
表9-7 |
iAttr |
值 |
属性 |
DDL_DRIVES |
0x4000 |
包括磁盘驱动器句柄 |
DDL_EXCLUSIVE |
0x8000 |
互斥搜索 |
前缀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的内容。
HEAD.C /*------------------------------------------------------------------------- HEAD.C -- Displays beginning (head) of file (c) Charles Petzold, 1998 --------------------------------------------------------------------------*/ #include#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 ; caseWM_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。