六、Windows子窗口控件的清单方块类别—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 <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 ; 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。