转自:http://blog.csdn.net/skyever2100/archive/2008/11/13/3292480.aspx
一、 概述
在Windows Form应用中,Windows界面系统通过消息与应用程序进行交互,每个窗口都有相应的消息处理器,处理各自的用户输入及界面重绘等逻辑。窗口都有自己的类名,需要先把该类名及它对应的消息处理器注册到Windows界面系统中,再根据该类名来创建自己的窗口。
Windows也为我们准备了文本输入框,对于简单的文本输入,这个功能已经很完美了,不过如果我们要做一个功能强大的文本编辑器,就像开发环境的IDE那样,那么从头来写它会更好,可以实现我们想要的任何逻辑。
文本框是这样一个窗口,它响应键盘消息,并实时重绘窗口中的文本,还要响应鼠标消息来移动光标位置。
我尝试着用Windows API来实现了一个简单的单行文本框,它仅有以下几个功能:
1、 响应用户的普通字符输入
2、 可以用光标键及HOME、END键来移动光标
3、 可以用鼠标键来移动光标
4、 可以用BACKSPACE及DELETE键来删除输入的内容
另外,它不具有选择文本的功能及剪切、复制、粘贴等功能,这个文本框是用纯C来写的,不具有对象化的特征,也就是说,没有将代码封装成类,不能在界面上放置两个文本框,这是为了简化代码,只说明它的原理,如果要封装成类,可以采用MFC等类库来编写这个文本框。
在本文的最后,附带了本程序的全部代码,为了书写方便,把所有的代码都放在了一个代码文件中了。
本文本框运行界面如下:
二、 技术要点
1、 注册文本框类并创建文本框窗口
可以使用API函数RegisterClassEx来注册文本框类,如下:
WNDCLASSEX wc;
::ZeroMemory(&wc, sizeof(wc));
wc.cbSize = sizeof(wc);
wc.style = CS_VREDRAW | CS_HREDRAW | CS_DBLCLKS; // 指定当窗口尺寸发生变化时重绘窗口,并且响应鼠标双击事件
wc.hInstance = _HInstance;
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); // 指定窗口背景颜色为系统颜色“窗口背景”
wc.lpszClassName = _T("MySimpleTextBox"); // 指定要注册的窗口类名,创建窗口时要以此类名为标识符
wc.lpfnWndProc = _TextBoxWndProc; // 处理窗口消息的函数
::RegisterClassEx(&wc); // 调用API函数注册文本框窗口
在注册文本框类的时候,需要为其指定消息处理过程,就是那个名为_TextBoxWndProc的函数,函数原型如下:
LRESULT CALLBACK _TextBoxWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
使用类名“MySimpleTextBox”来注册了一个文本框类,并且为其指定了消息处理过程,下一步就是要使用这个类名来创建一个文本框了,使用CreateWindow可以创建该窗口:
HWND hWnd = ::CreateWindow(__T("MySimpleTextBox"), NULL, WS_CHILDWINDOW | WS_VISIBLE,
left, top, width, height, hParentWnd, NULL, _HInstance, NULL);
其中的left、top为、width、height为文本框的位置及尺寸,_HInstance为父窗口的句柄。
2、 绘制文本框及文本
在文本框的消息处理过程中,响应消息WM_PAINT,可以实现对文本的绘制,假设使用默认的字体及字号,则代码如下:
static PAINTSTRUCT ps;
static RECT rect;
HDC hDC = ::BeginPaint(hWnd, &ps); // 开始绘制操作
::GetClientRect(hWnd, &rect); // 获取客户区的尺寸
::DrawEdge(hDC, &rect, EDGE_SUNKEN, BF_RECT); // 绘制边框,EDGE_SUNKEN表示绘制样式为内嵌样式,BF_RECT表示绘制矩形边框
int len = ::_tcslen(_String);
::TextOut(hDC, 4, 2, _String, len);
::EndPaint(hWnd, &ps); // 结束绘制操作
其中,_String为定义的一个全局变量:TCHAR _String[TEXTBOX_MAXLENGTH+1];
其中API函数DrawEdge可以绘制文本框的边缘。
3、 光标操作
我们可以自己一绘制闪烁的光标,不过Windows为我们提供了一套和光标有关的API函数,可以省去我们绘制光标的繁琐过程:
CreateCaret(HWND hWnd, HBITMAP hBitmap, int width, int height);
该API函数用于创建一个光标,第一个参数是窗口的句柄,第二个参数是光标的位图,用于定义光标的形状,第三、四个参数为光标的尺寸。
我们通常见到的光标是一个黑色的竖线,在Insert模式下(按了Insert键)为一个黑色的方块,如果是使用这种简单的光标,就把第二个参数设置为NULL就可以了。
ShowCaret(HWND hWnd);
该API函数用于显示光标,其中并没有指定显示哪个光标的参数,这是因为光标是与当前线程有关的资源,一个线程只能创建一个光标,在该线程中创建的光标,可以在该线程中对它执行其它的操作。
SetCaretPos(int x, int y);
该API函数用于设置光标的位置,当输入字符后或响应光标键时,可以调用该函数重新设置光标的位置。
在调用CrateCaret创建了光标时,它是隐藏状态,要显示它需要调用ShowCaret函数。
HideCaret(HWND hWnd);
该API函数用于隐藏光标。如果两次调用了HideCaret来隐藏光标,也需要调用两次ShowCaret才能显示它。
DestroyCaret(HWND hWnd);
该API函数用于销毁光标。
通常来说,我们需要在文本框得到焦点的时候创建并显示光标,而在文本框失去焦点的时候隐藏并销毁光标。
通过处理两个Windows Form消息可以实现上面的逻辑:
处理WM_SETFOCUS消息创建并显示光标:
::CreateCaret(hWnd, (HBITMAP)NULL, 1, TEXTBOX_HEIGHT-5); // 创建光标
::SetCaretPos(x, y); // 设置光标位置
::ShowCaret(hWnd); // 显示光标
处理WM_KILLFOCUS消息隐藏并销毁光标:
::HideCaret(hWnd); // 隐藏光标
::DestroyCaret(); // 销毁光标
在窗口绘制之前,我们需要先隐藏光标,绘制完成之后再显示光标,否则屏幕上将会残留光标的痕迹,但在处理WM_PAINT消息时我们并没有这样做,是因为BeginPaint和EndPaint已经为我们做了这件事情。
4、 响应按键消息
Windows有若干与键盘相关的消息,例如:WM_KEYDOWN、WM_KEYUP、WM_CHAR等,我们需要处理WM_CHAR消息在光标的位置显示所输入的字符,消息处理过程的参数wParam即为所输入的字符,显示出字符之后,需要调用SetCaretPos来重新设置光标位置。
如何将字符立即显示出来呢,首先要指定文本框上的无效区域,按照Windows Form的编程约定,当Windows发现某个窗口上出现无效区域时,会向该窗口发送WM_PAINT消息来通知消息处理过程重绘这个区域。
API函数InvalidateRect可以指定窗口的某个区域无效,最简单的办法是让整个窗口都无效,如下:
RECT rect;
::GetClientRect(hWnd, &rect);
::InvalidateRect(hWnd, &rect, TRUE);
::UpdateWindow(hWnd);
API函数UpdateWindow在执行时会立即重绘窗口,以便让用户的输入会在界面上及时做出响应。
光标键及HOME、END等键不会产生WM_CHAR消息,我们可以响应WM_KEYDOWN消息来移动光标的位置。
5、 响应鼠标消息
我们需要处理鼠标的单击消息WM_LBUTTONDOWN来移动光标,如何知道鼠标点击在哪个字符上呢,也就是如何取得指定位置处的字符呢?Windows API并没有为我们提供现成的功能,好在写一个这样的功能也不复杂,API函数GetTextExtentPoint(HDC hDc, LPCSTR lpString, int count, LPSIZE lpSize)可以获取指定的设置描述表下,指定字符串的尺寸。
基于这样的原理,我们可以逐渐求得每个字符所在的位置,与鼠标单击的位置来对照,便可以计算出鼠标是单击了哪个字符,如下:
int x = LOWORD(lParam);
HDC hDc = ::GetDC(hWnd);
int strLen = ::_tcslen(_String), strPos = 0;
SIZE size;
for (strPos=0; strPos<strLen; strPos++)
{
::GetTextExtentPoint(hDc, _String, strPos, &size);
if(size.cx + 4 >= x)
break;
}
_StringPosition = strPos;
::GetTextExtentPoint(hDc, _String, strPos, &size);
::SetCaretPos(size.cx + 4, 3);
::ReleaseDC(hWnd, hDc);
三、 遗留问题
1、 文本缓冲区问题
本示例中为了简单起见,定义了一个固定大小的文本缓冲区,当输入的字符数量到达固定大小时,将忽略字符消息的处理。显然这种处理方式不实用,当文本较少时会造成内存缓存区的浪费,当文本较多时内存缓冲区不能够满足要求,并且插入和删除字符时,都会移动大量的文本,效率也比较慢。
我们需要用变长的字符串来解决字符缓冲区大小这个问题,变长字符串会有许多逻辑,可以用类来封装这些逻辑,例如MFC中的CString类。
2、 如何用面向对象的思维来写一个文本框
本简单的文本框显示不具有重用特征,字符串缓冲区、光标位置等数据都定义为全局变量,这导致无法在界面上放置两个文本框。如果采用面向对象的逻辑,应该把这些数据封装在一个类中,之所以没有采用面向对象的方式来写,是因为Windows API本身就是面向过程的,从整体架构上来讲,我们需要实现一套面向对象的开发框架,定义各种窗口共有的基类,在这个基类上派出生各种窗口,例如MFC就是这样做的。
3、 文本选择的逻辑
实现这个逻辑的关系在于以下两点:
一是当用户拖拽鼠标或用Shift+光标键等进行选择时,消息处理过程需要对这些鼠标和键盘的消息正确地响应,确定出当前所选择的区域
二是如何向用户呈现所选择的文本区域,通常它们具有指定颜色的底色,这牵涉到界面重绘的问题。可以对这部分文本设置好背景色和前景色进行绘制。
4、 重绘的效率问题
本示例中每次输入和删除都要重绘整个文本区域,实际上,我们可以判断窗口哪个位置无效了,一般是光标后面的文本无效。在绘制时先取得其无效区域,仅对这一小部分进行绘制,可以提高重绘效率。
5、 多行文本的问题
显然,该示例程序只能输入单行文本,如果要输入多行文本,可以响应回车键另起一行,在窗口绘制时,如果遇到回车键,便跳到下一行的最左侧区域进行绘制。
也可以采取每行文本使用一个字符串缓冲区的办法,以防止在大量文本时引起的大量内存移动,这需要定义一个文本管理器的类来处理多个缓冲区的逻辑。
6、 其它问题
围绕文本编辑器可以展开若干问题,例如字体、字号、颜色、行间距等,更高级的,像开发环境的IDE,会自动把关键字突出显示,如果要做这样一个文本编辑器,就是非常复杂的事情了,不过办法总比问题多,这些有激情的问题会带领我们进入一个广阔的思维空间。
为了书写方便,把所有的代码都放在了一个代码文件中了。
关于对该代码技术要点的解释,请参见:《用Windows API实现一个简单的文本输入框(上)》
该代码中大部分地方都加了注释,有不妥之处,敬请批评指正:
1 #include <tchar.h>
2
3 #include <windows.h>
4
5
6
7 HINSTANCE _HInstance; // 应用程序句柄
8
9 TCHAR _Title[] = _T("简单文本框"); // 定义窗口的标题
10
11
12
13 TCHAR _WindowClass[] = _T("MySimpleTextBoxApp");// 主窗口类名
14
15 ATOM _RegisterClass(); // 注册主窗口类
16
17 HWND _CreateWindow(int nCmdShow); // 创建主窗口
18
19 LRESULT CALLBACK _WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); // 主窗口消息处理函数
20
21
22
23 TCHAR _TextBoxClass[] = _T("MySimpleTextBox"); // 文本框的类名
24
25 ATOM _RegisterTextBoxClass(); // 注册文本框的类
26
27 HWND _CreateTextBoxWindow(HWND hParentWnd); // 创建文本框
28
29 LRESULT CALLBACK _TextBoxWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); // 文本框窗口消息处理函数
30
31 void _DrawText(HDC hDC); // 绘制文本
32
33 void _SetCaretPos(HWND hWnd); // 设置光标位置
34
35 void _UpdateWindow(HWND hWnd); // 更新窗口
36
37
38
39
40
41 // 一些常量定义
42
43 #define MAINWINDOW_WIDTH 400 // 主窗口宽度
44
45 #define MAINWINDOW_HEIGHT 200 // 主窗口高度
46
47 #define TEXTBOX_WIDTH 300 // 文本框宽度
48
49 #define TEXTBOX_HEIGHT 20 // 文本框高度
50
51 #define TEXTBOX_MAXLENGTH 1024 // 文本框中文本的最大长度
52
53
54
55 TCHAR _String[TEXTBOX_MAXLENGTH + 1] = _T(""); // 文本
56
57 int _StringPosition = ::_tcslen(_String); // 光标插入点所在的位置
58
59
60
61 int APIENTRY _tWinMain(HINSTANCE hInstance, // 当前的应用程序句柄
62
63 HINSTANCE hPrevInstance, // 前一个应用程序实例的句柄(在Win32上,始终为NULL)
64
65 LPTSTR lpCmdLine, // 命令行参数
66
67 int nCmdShow // 窗口的显示样式
68
69 )
70
71 {
72
73 _HInstance = hInstance;
74
75
76
77 _RegisterClass(); // 注册窗口类
78
79 if(_CreateWindow(nCmdShow) == NULL) // 创建窗口
80
81 return FALSE;
82
83
84
85 MSG msg;
86
87 while (::GetMessage(&msg, NULL, 0, 0)) // 从消息队列中获取消息
88
89 {
90
91 ::TranslateMessage(&msg); // 转译一些特殊的消息
92
93 ::DispatchMessage(&msg); // 执行消息处理
94
95 }
96
97
98
99 return (int)msg.wParam;
100
101 }
102
103
104
105
106
107 // 注册应用程序窗口类
108
109 ATOM _RegisterClass()
110
111 {
112
113 WNDCLASSEX wc;
114
115 ::ZeroMemory(&wc, sizeof(wc)); // 作为一步清空,是为了让未赋值的字段的默认值为(或NULL)
116
117
118
119 wc.cbSize = sizeof(wc);
120
121 wc.style = CS_HREDRAW | CS_VREDRAW; // 指定当窗口横向和纵向的尺寸发生变化时都会重绘窗口
122
123 wc.hInstance = _HInstance;
124
125 wc.hbrBackground = (HBRUSH)( COLOR_APPWORKSPACE + 1); // 指定主窗口背景为“工作区域”系统颜色
126
127 wc.lpszClassName = _WindowClass; // 此为要注册的类名,创建窗口时要以此类名为标识符
128
129 wc.lpfnWndProc = _WndProc; // 此为处理窗口消息的函数
130
131
132
133 return ::RegisterClassEx(&wc); // 调用API函数注册窗口类
134
135 }
136
137
138
139 // 创建窗口
140
141 HWND _CreateWindow(int nCmdShow)
142
143 {
144
145 HWND hWnd = ::CreateWindow(_WindowClass, _Title, WS_OVERLAPPEDWINDOW,
146
147 CW_USEDEFAULT, CW_USEDEFAULT, MAINWINDOW_WIDTH, MAINWINDOW_HEIGHT, NULL, NULL, _HInstance, NULL);
148
149
150
151 if(hWnd == NULL)
152
153 return NULL;
154
155
156
157 ::ShowWindow(hWnd, nCmdShow);
158
159 ::UpdateWindow(hWnd);
160
161
162
163 return hWnd;
164
165 }
166
167
168
169 // 窗口处理过程
170
171 LRESULT CALLBACK _WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
172
173 {
174
175 static HWND hTextBoxWnd;
176
177
178
179 switch (message)
180
181 {
182
183 case WM_CREATE: {
184
185 _RegisterTextBoxClass(); // 注册文本框的类
186
187 hTextBoxWnd = _CreateTextBoxWindow(hWnd); // 创建文本框
188
189 } break;
190
191
192
193 case WM_ACTIVATE: // 当窗口被激活时,将焦点设置在文本框上
194
195 ::SetFocus(hTextBoxWnd);
196
197 break;
198
199
200
201 case WM_SETCURSOR: { // 设置光标形状
202
203 static HCURSOR hCursor = ::LoadCursor(NULL, IDC_ARROW);
204
205 ::SetCursor(hCursor);
206
207 } break;
208
209
210
211 case WM_DESTROY: // 应用程序被关闭
212
213 ::PostQuitMessage(0);
214
215 break;
216
217
218
219 default:
220
221 return ::DefWindowProc(hWnd, message, wParam, lParam);
222
223 }
224
225
226
227 return (LRESULT)0;
228
229 }
230
231
232
233 // 注册文本框的类
234
235 ATOM _RegisterTextBoxClass()
236
237 {
238
239 WNDCLASSEX wc;
240
241 ::ZeroMemory(&wc, sizeof(wc));
242
243
244
245 wc.cbSize = sizeof(wc);
246
247 wc.style = CS_VREDRAW | CS_HREDRAW | CS_DBLCLKS; // 指定当窗口尺寸发生变化时重绘窗口,并且响应鼠标双击事件
248
249 wc.hInstance = _HInstance;
250
251 wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); // 指定窗口背景颜色为系统颜色“窗口背景”
252
253 wc.lpszClassName = _TextBoxClass; // 指定要注册的窗口类名,创建窗口时要以此类名为标识符
254
255 wc.lpfnWndProc = _TextBoxWndProc; // 处理窗口消息的函数
256
257
258
259 return ::RegisterClassEx(&wc); // 调用API函数注册文本框窗口
260
261 }
262
263
264
265
266
267 // 创建文本框
268
269 HWND _CreateTextBoxWindow(HWND hParentWnd)
270
271 {
272
273 // 之下代码是为了让文本框显示在父窗口中央,而计算位置
274
275 RECT parentWndRect;
276
277 ::GetClientRect(hParentWnd, &parentWndRect); // 获取父窗口客户区的位置
278
279 int left = (parentWndRect.right - TEXTBOX_WIDTH) / 2, top = (parentWndRect.bottom - TEXTBOX_HEIGHT) / 2;
280
281
282
283 // 创建文本框
284
285 HWND hWnd = ::CreateWindow(_TextBoxClass, NULL, WS_CHILDWINDOW | WS_VISIBLE,
286
287 left, top, TEXTBOX_WIDTH, TEXTBOX_HEIGHT,
288
289 hParentWnd, NULL, _HInstance, NULL);
290
291
292
293 return hWnd;
294
295 }
296
297
298
299 // 文本框消息的处理过程
300
301 LRESULT CALLBACK _TextBoxWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
302
303 {
304
305 switch (message)
306
307 {
308
309 case WM_PAINT: { // 绘制这里之所以加一对大括号,是为了让之下定义的变量局部化
310
311
312
313 static PAINTSTRUCT ps;
314
315 static RECT rect;
316
317 HDC hDC = ::BeginPaint(hWnd, &ps); // 开始绘制操作
318
319
320
321 ::GetClientRect(hWnd, &rect); // 获取客户区的尺寸
322
323 ::DrawEdge(hDC, &rect, EDGE_SUNKEN, BF_RECT); // 绘制边框,EDGE_SUNKEN表示绘制样式为内嵌样式,BF_RECT表示绘制矩形边框
324
325 _DrawText(hDC); // 绘制文本
326
327 ::EndPaint(hWnd, &ps); // 结束绘制操作
328
329
330
331 } break;
332
333
334
335 case WM_SETFOCUS: { // 获得焦点
336
337 ::CreateCaret(hWnd, (HBITMAP)NULL, 1, TEXTBOX_HEIGHT-5); // 创建光标
338
339 _SetCaretPos(hWnd); // 设置光标位置
340
341 ::ShowCaret(hWnd); // 显示光标
342
343 } break;
344
345
346
347 case WM_KILLFOCUS: // 失去焦点
348
349 ::HideCaret(hWnd); // 隐藏光标
350
351 ::DestroyCaret(); // 销毁光标
352
353 break;
354
355
356
357 case WM_SETCURSOR: { // 设置光标形状
358
359 static HCURSOR hCursor = ::LoadCursor(NULL, IDC_IBEAM);
360
361 ::SetCursor(hCursor);
362
363 } break;
364
365
366
367 case WM_CHAR: { // 字符消息
368
369 TCHAR code = (TCHAR)wParam;
370
371 int len = ::_tcslen(_String);
372
373 if(code < (TCHAR)' ' || len >= TEXTBOX_MAXLENGTH)
374
375 return 0;
376
377
378
379 ::MoveMemory(_String + _StringPosition + 1, _String + _StringPosition, (len - _StringPosition + 1) * sizeof(TCHAR));
380
381 _String[_StringPosition ++] = code;
382
383
384
385 _UpdateWindow(hWnd);
386
387 _SetCaretPos(hWnd);
388
389
390
391 } break;
392
393
394
395 case WM_KEYDOWN: { // 键按下消息
396
397 TCHAR code = (TCHAR)wParam;
398
399
400
401 switch (code)
402
403 {
404
405 case VK_LEFT: // 左光标键
406
407 if(_StringPosition > 0)
408
409 _StringPosition --;
410
411 break;
412
413
414
415 case VK_RIGHT: // 右光标键
416
417 if(_StringPosition < (int)::_tcslen(_String))
418
419 _StringPosition ++;
420
421 break;
422
423
424
425 case VK_HOME: // HOME 键
426
427 _StringPosition = 0;
428
429 break;
430
431
432
433 case VK_END: // END 键
434
435 _StringPosition = ::_tcslen(_String);
436
437 break;
438
439
440
441 case VK_BACK: // 退格键
442
443 if(_StringPosition > 0)
444
445 {
446
447 ::MoveMemory(_String + _StringPosition - 1, _String + _StringPosition, (::_tcslen(_String)-_StringPosition + 1) * sizeof(TCHAR));
448
449 _StringPosition --;
450
451 _UpdateWindow(hWnd);
452
453 }
454
455 break;
456
457
458
459 case VK_DELETE: { // 删除键
460
461 int len = ::_tcslen(_String);
462
463 if(_StringPosition < len)
464
465 {
466
467 ::MoveMemory(_String + _StringPosition, _String + _StringPosition + 1, (::_tcslen(_String) - _StringPosition + 1) * sizeof(TCHAR));
468
469 _UpdateWindow(hWnd);
470
471 }
472
473
474
475 } break;
476
477
478
479 }
480
481
482
483 _SetCaretPos(hWnd);
484
485
486
487 } break;
488
489
490
491 case WM_LBUTTONDOWN: { // 鼠标单击,设置光标位置
492
493 int x = LOWORD(lParam);
494
495 HDC hDc = ::GetDC(hWnd);
496
497
498
499 int strLen = ::_tcslen(_String), strPos = 0;
500
501 SIZE size;
502
503
504
505 for (strPos=0; strPos<strLen; strPos++)
506
507 {
508
509 ::GetTextExtentPoint(hDc, _String, strPos, &size);
510
511
512
513 if(size.cx + 4 >= x)
514
515 break;
516
517 }
518
519
520
521 _StringPosition = strPos;
522
523 ::GetTextExtentPoint(hDc, _String, strPos, &size);
524
525 ::SetCaretPos(size.cx + 4, 3);
526
527
528
529 ::ReleaseDC(hWnd, hDc);
530
531
532
533 } break;
534
535
536
537 default:
538
539 return ::DefWindowProc(hWnd, message, wParam, lParam);
540
541 }
542
543
544
545 return (LRESULT)0;
546
547 }
548
549
550
551 // 更新窗口
552
553 void _UpdateWindow(HWND hWnd)
554
555 {
556
557 RECT rect;
558
559 ::GetClientRect(hWnd, &rect);
560
561 ::InvalidateRect(hWnd, &rect, TRUE);
562
563 ::UpdateWindow(hWnd);
564
565 }
566
567
568
569 // 绘制文本
570
571 void _DrawText(HDC hDC)
572
573 {
574
575 int len = ::_tcslen(_String);
576
577 ::TextOut(hDC, 4, 2, _String, len);
578
579 }
580
581
582
583 // 设置光标位置
584
585 void _SetCaretPos(HWND hWnd)
586
587 {
588
589 HDC hDC = ::GetDC(hWnd);
590
591
592
593 SIZE size;
594
595 ::GetTextExtentPoint(hDC, _String, _StringPosition, &size);
596
597 ::SetCaretPos(4 + size.cx, 3);
598
599
600
601 ::ReleaseDC(hWnd, hDC);
602
603
604
605 }
606
607