四、Windows编程之插入符号(不是光标)
当您往程序中输入文字时,通常有一个底线、竖条或者方框来指示输入的下一个字符将出现在屏幕上的位置。这个标志通常称为「光标」,但是在Windows下写程序,您必须改变这个习惯。在Windows中,它称为「插入符号」。「光标」是指表示鼠标位置的那个位图图像。
插入符号函数
主要有五个插入符号函数:
-
CreateCaret 建立与窗口有关的插入符号
-
SetCaretPos 在窗口中设定插入符号的位置
-
ShowCaret 显示插入符号
-
HideCaret 隐藏插入符号
-
DestroyCaret 撤消插入符号
另外还有取得插入符号目前位置(GetCaretPos)和取得以及设定插入符号闪烁时间(GetCaretBlinkTime和SetCaretBlinkTime)的函数。
在Windows中,插入符号定义为水平线、与字符大小相同的方框,或者与字符同高的竖线。如果使用调和字体,例如Windows内定的系统字体,则推荐使用竖线插入符号。因为调和字体中的字符没有固定大小,水平线或方框不能设定为字符的大小。
如果程序中需要插入符号,那么您不应该简单地在窗口消息处理程序的WM_CREATE消息处理期间建立它,然后在WM_DESTROY消息处理期间撤消。其原因显而易见:一个消息队列只能支持一个插入符号。因此,如果您的程序有多个窗口,那么各个窗口必须有效地共享相同的插入符号。
其实,它并不像听起来那么多限制。您再想想就会发现,只有在窗口有输入焦点时,窗口内显示插入符号才有意义。事实上,闪烁的插入符号只是一种视觉提示:您可以在程序中输入文字。因为任何时候都只有一个窗口拥有输入焦点,所以多个窗口同时都有闪烁的插入符号是没有意义的。
通过处理WM_SETFOCUS和WM_KILLFOCUS消息,程序就可以确定它是否有输入焦点。正如名称所暗示的,窗口消息处理程序在有输入焦点的时候接收到WM_SETFOCUS消息,失去输入焦点的时候接收到WM_KILLFOCUS消息。这些消息成对出现:窗口消息处理程序在接收到WM_KILLFOCUS消息之前将一直接收到WM_SETFOCUS消息,并且在窗口打开期间,此窗口总是接收到相同数量的WM_SETFOCUS和WM_KILLFOCUS消息。
使用插入符号的主要规则很简单:窗口消息处理程序在WM_SETFOCUS消息处理期间呼叫CreateCaret,在WM_KILLFOCUS消息处理期间呼叫DestroyCaret。
这里还有几条其它规则:插入符号刚建立时是隐蔽的。如果想使插入符号可见,那么您在呼叫CreateCaret之后,窗口消息处理程序还必须呼叫ShowCaret。另外,当窗口消息处理程序处理一条非WM_PAINT消息而且希望在窗口内绘制某些东西时,它必须呼叫HideCaret隐藏插入符号。在绘制完毕后,再呼叫ShowCaret显示插入符号。HideCaret的影响具有累积效果,如果多次呼叫HideCaret而不呼叫ShowCaret,那么只有呼叫ShowCaret相同次数时,才能看到插入符号。
TYPER程序
程序6-5所示的TYPER程序使用了本章讨论的所有内容,您可以认为TYPER是一个相当简单的文字编辑器。在窗口中,您可以输入字符,用光标移动键(也可以称为插入符号移动键)来移动光标(I型标),按下Escape键清除窗口的内容等。缩放窗口、改变键盘输入语言时都会清除窗口的内容。本程序没有卷动,没有文字寻找和定位功能,不能储存文件,没有拼写检查,但它确实是写作一个文字编辑器的开始。
TYPER.C /*------------------------------------------------------------------------ TYPER.C -- Typing Program (c) Charles Petzold, 1998 --------------------------------------------------------------------------*/ #include <windows.h> #define BUFFER(x,y) *(pBuffer + y * cxBuffer + x) LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("Typer") ; 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 ("Typing Program"), 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 DWORD dwCharSet = DEFAULT_CHARSET ; static int cxChar, cyChar, cxClient, cyClient, cxBuffer, cyBuffer, xCaret, yCaret ; static TCHAR *pBuffer = NULL ; HDC hdc ; int x, y, i ; PAINTSTRUCT ps ; TEXTMETRIC tm ; switch (message) { case WM_INPUTLANGCHANGE: dwCharSet = wParam ; // fall through case WM_CREATE: hdc = GetDC (hwnd) ; SelectObject (hdc, CreateFont (0, 0, 0, 0, 0, 0, 0, 0, dwCharSet, 0, 0, 0, FIXED_PITCH, NULL)) ; GetTextMetrics (hdc, &tm) ; cxChar = tm.tmAveCharWidth ; cyChar = tm.tmHeight ; DeleteObject (SelectObject (hdc, GetStockObject (SYSTEM_FONT))) ; ReleaseDC (hwnd, hdc) ; // fall through case WM_SIZE: // obtain window size in pixels if (message == WM_SIZE) { cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; } // calculate window size in characters cxBuffer = max (1, cxClient / cxChar) ; cyBuffer = max (1, cyClient / cyChar) ; // allocate memory for buffer and clear it if (pBuffer != NULL) free (pBuffer) ; pBuffer = (TCHAR *) malloc (cxBuffer * cyBuffer * sizeof (TCHAR)) ; for (y = 0 ; y < cyBuffer ; y++) for (x = 0 ; x < cxBuffer ; x++) BUFFER(x,y) = ' ' ; // set caret to upper left corner xCaret = 0 ; yCaret = 0 ; if (hwnd == GetFocus ()) SetCaretPos (xCaret * cxChar, yCaret * cyChar) ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_SETFOCUS: // create and show the caret CreateCaret (hwnd, NULL, cxChar, cyChar) ; SetCaretPos (xCaret * cxChar, yCaret * cyChar) ; ShowCaret (hwnd) ; return 0 ; case WM_KILLFOCUS: // hide and destroy the caret HideCaret (hwnd) ; DestroyCaret () ; return 0 ; case WM_KEYDOWN: switch (wParam) { case VK_HOME: xCaret = 0 ; break ; case VK_END: xCaret = cxBuffer - 1 ; break ; case VK_PRIOR: yCaret = 0 ; break ; case VK_NEXT: yCaret = cyBuffer - 1 ; break ; case VK_LEFT: xCaret = max (xCaret - 1, 0) ; break ; case VK_RIGHT: xCaret = min (xCaret + 1, cxBuffer - 1) ; break ; case VK_UP: yCaret = max (yCaret - 1, 0) ; break ; case VK_DOWN: yCaret = min (yCaret + 1, cyBuffer - 1) ; break ; case VK_DELETE: for (x = xCaret ; x < cxBuffer - 1 ; x++) BUFFER (x, yCaret) = BUFFER (x + 1, yCaret) ; BUFFER (cxBuffer - 1, yCaret) = ' ' ; HideCaret (hwnd) ; hdc = GetDC (hwnd) ; SelectObject (hdc, CreateFont (0, 0, 0, 0, 0, 0, 0, 0, dwCharSet, 0, 0, 0,FIXED_PITCH, NULL)) ; TextOut (hdc, xCaret * cxChar, yCaret * cyChar, & BUFFER (xCaret, yCaret), cxBuffer - xCaret) ; DeleteObject (SelectObject (hdc, GetStockObject (SYSTEM_FONT))) ; ReleaseDC (hwnd, hdc) ; ShowCaret (hwnd) ; break ; } SetCaretPos (xCaret * cxChar, yCaret * cyChar) ; return 0 ; case WM_CHAR: for (i = 0 ; i < (int) LOWORD (lParam) ; i++) { switch (wParam) { case '\b': // backspace if (xCaret > 0) { xCaret-- ; SendMessage (hwnd, WM_KEYDOWN, VK_DELETE, 1) ; } break ; case '\t': // tab do { SendMessage (hwnd, WM_CHAR, ' ', 1) ; } while (xCaret % 8 != 0) ; break ; case '\n': // line feed if (++yCaret == cyBuffer) yCaret = 0 ; break ; case '\r': // carriage return xCaret = 0 ; if (++yCaret == cyBuffer) yCaret = 0 ; break ; case '\x1B': // escape for (y = 0 ; y < cyBuffer ; y++) for (x = 0 ; x < cxBuffer ; x++) BUFFER (x, y) = ' ' ; xCaret = 0 ; yCaret = 0 ; InvalidateRect (hwnd, NULL, FALSE) ; break ; default: // character codes BUFFER (xCaret, yCaret) = (TCHAR) wParam ; HideCaret (hwnd) ; hdc = GetDC (hwnd) ; SelectObject (hdc, CreateFont (0, 0, 0, 0, 0, 0, 0, 0, dwCharSet, 0, 0, 0, FIXED_PITCH, NULL)) ; TextOut (hdc, xCaret * cxChar, yCaret * cyChar, & BUFFER (xCaret, yCaret), 1) ; DeleteObject ( SelectObject (hdc, GetStockObject (SYSTEM_FONT))) ; ReleaseDC (hwnd, hdc) ; ShowCaret (hwnd) ; if (++xCaret == cxBuffer) { xCaret = 0 ; if (++yCaret == cyBuffer) yCaret = 0 ; } break ; } } SetCaretPos (xCaret * cxChar, yCaret * cyChar) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; SelectObject (hdc, CreateFont (0, 0, 0, 0, 0, 0, 0, 0, dwCharSet, 0, 0, 0, FIXED_PITCH, NULL)) ; for (y = 0 ; y < cyBuffer ; y++) TextOut (hdc, 0, y * cyChar, & BUFFER(0,y), cxBuffer) ; DeleteObject (SelectObject (hdc, GetStockObject (SYSTEM_FONT))) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
为了简单起见,TYPER程序使用一种等宽字体,因为编写处理调和字体的文字编辑器要困难得多。程序在好几个地方取得设备内容:在WM_CREATE消息处理期间,在WM_KEYDOWN消息处理期间,在WM_CHAR消息处理期间以及在WM_PAINT消息处理期间,每次都通过GetStockObject和SelectObject呼叫来选择等宽字体。
在WM_SIZE消息处理期间,TYPER计算窗口的字符宽度和高度并把值保存在cxBuffer和cyBuffer变量中,然后使用malloc分配缓冲区以保存在窗口内输入的所有字符。注意,缓冲区的字节大小取决于cxBuffer、cyBuffer和sizeof(TCHAR),它可以是1或2,这依赖于程序是以8位的字符处理还是以Unicode方式编译的。
xCaret和yCaret变量保存插入符号位置。在WM_SETFOCUS消息处理期间,TYPER呼叫CreateCaret来建立与字符有相同宽度和高度的插入符号,呼叫SetCaretPos来设定插入符号的位置,呼叫ShowCaret使插入符号可见。在WM_KILLFOCUS消息处理期间,TYPER呼叫HideCaret和DestroyCaret。
对WM_KEYDOWN的处理大多要涉及光标移动键。Home和End把插入符号送至一行的开始和末尾处,Page Up和Page Down把插入符号送至窗口的顶端和底部,箭头的用法不变。对Delete键,TYPER将缓冲区中从插入符号之后的那个位置开始到行尾的所有内容向前移动,并在行尾显示空格。
WM_CHAR处理Backspace、Tab、Linefeed(Ctrl-Enter)、Enter、Escape和字符键。注意,在处理WM_CHAR消息时(假设使用者输入的每个字符都非常重要),我使用了lParam中的重复计数;而在处理WM_KEYDOWN消息时却不这么作(避免有害的重复卷动)。对Backspace和Tab的处理由于使用了SendMessage函数而得到简化,Backspace与Delete做法相仿,而Tab则如同输入了若干个空格。
前面我已经提到过,在非WM_PAINT消息处理期间,如果要在窗口中绘制内容,则应该隐蔽光标。TYPER为Delete键处理WM_KEYDOWN消息和为字符键处理WM_CHAR消息时即是如此。在这两种情况下,TYPER改变缓冲区中的内容,然后在窗口中绘制一个或者多个新字符。
虽然TYPER使用了与KEYVIEW2相同的做法以在字符集之间切换(就像使用者切换键盘布局一样),但对于远东版的Windows,它还是不能正常工作。TYPER不允许使用两倍宽度的字符。此问题将在第十七章讨论,那时我们将详细讨论字体与文字输出。