摘录于《Windows程序(第5版,珍藏版).CHarles.Petzold 著》P213
当你向程序中输入文本时,通常会有下划线、竖线或方框指示你输入的下一个字符将出现屏幕上的位置。你也许认为这是“光标”,但在编写 Windows 程序时,你必须避免这种习惯。在 Windows 中,它被称为“插入符号”(caret)。“光标”(cursor)特指表示鼠标位置的位图图像,即鼠标指针。
6.5.1 一些关于插入符号的函数
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 才能使插入符号可见。
6.5.2 TYPER 程序
TYPER 程序使用了我们在这章中学到的大部分内容。你可以认为 TYPER 是一个非常基础的文本编辑器。你能在窗口中输入文本,用光标移动键(或者该叫插入符号移动键?)移动光标(这里是指插入符号),按下 Esc 键擦除窗口的内容。当你调整窗口大小或改变键盘输入语言时,窗口内容也被擦除。TYPER 程序中没有滚动功能,没有搜索和替换,没有保存文件的方法,没有拼写检查程序,而且没有人形的曲别针(在 微软 Office XP 中,它表示帮助的意思),但它的确是一个文本编辑器的雏形。
/*--------------------------------------------------------
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)。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 键、换行(Ctrl+Enter)键,Enter 键,Esc 键和字符键。注意当处理 WM_CHAR 消息(假定用户输入的每个字符都是重要的)时,我使用了 lParam 参数中的重复计数。但是在 WM_KEYDOWN 消息中,我没有利用重复计数(以防止无意思的过度滚动)。通过使用 SendMessage 函数可简化处理 Backspace 键和 Tab 键的逻辑。Backspace 键的处理逻辑是由处理 Delete 键的逻辑模仿而成的,Tab 则如同输入了一连串的空格。
像我前面提到的那样,只要窗口过程处理的是非 WM_PAINT 消息,那么当要在窗口内绘制图形时,程序就应该隐藏插入符号。当为 Delete 键处理 WM_KEYDOWN 消息和为字符键处理 WM_CHAR 消息时,TYPER 就这样做了。TYPER 改变缓冲区的内容,然后在窗口中绘制一个或多个新字符。
尽管 TYPER 程序使用同 KEYVIEW2 中一样的逻辑来实现在用户切换键盘布局时切换字符集的功能,但它仍然不能在 Windows 的远东版本下正常工作。TYPER 不允许使用双字节宽度的字符。这就提出了新的问题,我们将在第 17 章详细讨论字体和文本输出。