实现一个可编辑的Windowless RichEdit

本文在《创建一个最简单的Windowless RichEdit》一文的基础上,介绍如何让Windowless RichEdit支持编辑功能。

激活并设置焦点

Windowless RichEdit需要同时处于两种状态下才能够编辑:一是活动状态,二是有输入焦点状态。

默认情况下Windowless RichEdit是处于不活动状态的,使用ITextServices的OnTxInPlaceActivate方法即可切换到活动状态。该方法接受一个类型为const RECT*的参数,表示Windowless RichEdit的客户区域,一般情况下传入nullptr即可,因为Windowless RichEdit总是会调用ITextHost的TxGetClientRect方法取得这个区域。也就是说我们还要实现TxGetClientRect:

1
2
3
4
HRESULT TxGetClientRect(LPRECT prc) override {
GetClientRect(hwnd_, prc);
return S_OK;
}

同样地,默认情况下Windowless RichEdit处于无输入焦点状态,需要使用ITextServices的TxSendMessage方法向它发送一个WM_SETFOCUS消息来切换到有输入焦点状态。所有能通过消息完成的操作都使用TxSendMessage方法,这样可以使Windowless RichEdit与Windows的消息处理更好地融合,也避免在ITextServices接口中暴露过多方法。

这两种状态可以在父窗口的WM_SETFOCUS消息中同时设置,如下所示:

1
2
3
4
5
6
7
8
9
case WM_SETFOCUS: {

g_text_service->OnTxInPlaceActivate(nullptr);

LRESULT result = 0;
g_text_service->TxSendMessage(message, wParam, lParam, &result);

return result;
}

对应地,使用ITextServices的OnTxInPlaceDeactivate方法可以切换回不活动状态;使用TxSendMessage发送WM_KILLFOCUS消息可以切换回无焦点状态。可以在父窗口的WM_KILLFOCUS消息中同时设置,如下所示:

1
2
3
4
5
6
7
8
9
case WM_KILLFOCUS: {

g_text_service->OnTxInPlaceDeactivate();

LRESULT result = 0;
g_text_service->TxSendMessage(message, wParam, lParam, &result);

return result;
}

显示输入光标

Windowless RichEdit不负责显示输入光标,它会调用ITextHost的一系列方法进行回调,由实现者来负责显示。下列方法是与输入光标相关的:

  • TxGetDC
  • TxReleaseDC
  • TxCreateCaret
  • TxShowCaret
  • TxSetCaretPos

一般情况下,实现这几个方法很简单,只要调用相应的API即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
HDC TxGetDC() override {
return GetDC(hwnd_);
}

INT TxReleaseDC(HDC hdc) override {
return ReleaseDC(hwnd_, hdc);
}

BOOL TxCreateCaret(HBITMAP hbmp, INT xWidth, INT yHeight) override {
return CreateCaret(hwnd_, hbmp, xWidth, yHeight);
}

BOOL TxShowCaret(BOOL fShow) override {
if (fShow) {
return ShowCaret(hwnd_);
}
else {
return HideCaret(hwnd_);
}
}

BOOL TxSetCaretPos(INT x, INT y) override {
return SetCaretPos(x, y);
}

TxGetDC和TxReleaseDC看起来似乎与后面三个方法无关,但却是必须实现的。如果TxGetDC返回了无效的HDC,那么后面三个方法就不会被调用。

输入字符

往Windowless RichEdit中输入字符很简单,只要让它处理下列键盘消息即可:

  • WM_KEYDOWN
  • WM_KEYUP
  • WM_CHAR

这同样是使用TxSendMessage方法完成,如下所示:

1
2
3
4
5
6
7
8
9
case WM_KEYDOWN:
case WM_KEYUP:
case WM_CHAR: {

LRESULT result = 0;
g_text_service->TxSendMessage(message, wParam, lParam, &result);

return result;
}

除此之外,Windowless RichEdit还需要重绘以显示输入的字符,它会调用ITextHost的TxInvalidateRect方法。该方法的实现也非常简单:

1
2
3
void TxInvalidateRect(LPCRECT prc, BOOL fMode) override {
InvalidateRect(hwnd_, prc, fMode);
}

鼠标相关

鼠标在进行文字编辑时也有重要的作用。例如,当鼠标移动到可编辑的文本框上时,指针会变成I形状,此时点击鼠标可以修改输入光标的位置;拖拽鼠标可以选择一段文字。下面是实现这些功能的方法。

鼠标指针的改变需要调用ITextServices的OnTxSetCursor来触发,理想的调用点是在处理WM_SETCURSOR消息的时候,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
case WM_SETCURSOR: {

HDC hdc = GetDC(hwnd);

POINT position = { 0 };
GetCursorPos(&position);
ScreenToClient(hwnd, &position);

RECT rect = { 0 };
GetClientRect(hwnd, &rect);

g_text_service->OnTxSetCursor(
DVASPECT_CONTENT,
0,
nullptr,
nullptr,
hdc,
nullptr,
&rect,
position.x,
position.y
);

ReleaseDC(hwnd, hdc);

return TRUE;
}

注意不能直接用TxSendMessage发送WM_SETCURSOR消息,这样做并不能得到想要的效果。

然后Windowless RichEdit会调用ITextHost的TxSetCursor来设置鼠标指针,只要在该方法中调用SetCursor即可:

1
2
3
void TxSetCursor(HCURSOR hcur, BOOL fText) override {
SetCursor(hcur);
}

最后,要让Windowless RichEdit处理下列鼠标消息:

  • WM_MOUSEMOVE
  • WM_LBUTTONDOWN
  • WM_LBUTTONUP

同样是使用TxSendMessage完成:

1
2
3
4
5
6
7
8
9
case WM_MOUSEMOVE:
case WM_LBUTTONDOWN:
case WM_LBUTTONUP: {

LRESULT result = 0;
g_text_service->TxSendMessage(message, wParam, lParam, &result);

return result;
}

Windowless RichEdit会在需要的时候调用ITextHost的TxSetCapture来捕获或释放鼠标。该方法的实现如下:

1
2
3
4
5
6
7
8
void TxSetCapture(BOOL fCapture) override {
if (fCapture) {
SetCapture(hwnd_);
}
else {
ReleaseCapture();
}
}

完整代码

在本文的最后,附上完整的示例代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
#include <Windows.h>
#include <atlbase.h>
#include <Richedit.h>
#include <TextServ.h>
#include <memory>

class TextHost : public ITextHost {
public:
TextHost(HWND hwnd) : hwnd_(hwnd) { }

HRESULT __stdcall QueryInterface(REFIID riid, void** ppvObject) override {

if (ppvObject == nullptr) {
return E_POINTER;
}

if ((riid == IID_IUnknown) || (riid == IID_ITextHost)) {
*ppvObject = this;
return S_OK;
}

*ppvObject = nullptr;
return E_NOINTERFACE;
}

ULONG __stdcall AddRef(void) override { return 0; }

ULONG __stdcall Release(void) override { return 0; }

HDC TxGetDC() override {
return GetDC(hwnd_);
}

INT TxReleaseDC(HDC hdc) override {
return ReleaseDC(hwnd_, hdc);
}

BOOL TxShowScrollBar(INT fnBar, BOOL fShow) override {
return FALSE;
}

BOOL TxEnableScrollBar(INT fuSBFlags, INT fuArrowflags) override {
return FALSE;
}

BOOL TxSetScrollRange(INT fnBar, LONG nMinPos, INT nMaxPos, BOOL fRedraw) override {
return FALSE;
}

BOOL TxSetScrollPos(INT fnBar, INT nPos, BOOL fRedraw) override {
return FALSE;
}

void TxInvalidateRect(LPCRECT prc, BOOL fMode) override {
InvalidateRect(hwnd_, prc, fMode);
}

void TxViewChange(BOOL fUpdate) override {

}

BOOL TxCreateCaret(HBITMAP hbmp, INT xWidth, INT yHeight) override {
return CreateCaret(hwnd_, hbmp, xWidth, yHeight);
}

BOOL TxShowCaret(BOOL fShow) override {
if (fShow) {
return ShowCaret(hwnd_);
}
else {
return HideCaret(hwnd_);
}
}

BOOL TxSetCaretPos(INT x, INT y) override {
return SetCaretPos(x, y);
}

BOOL TxSetTimer(UINT idTimer, UINT uTimeout) override {
return FALSE;
}

void TxKillTimer(UINT idTimer) override {

}

void TxScrollWindowEx(INT dx, INT dy, LPCRECT lprcScroll, LPCRECT lprcClip, HRGN hrgnUpdate, LPRECT lprcUpdate, UINT fuScroll) override {

}

void TxSetCapture(BOOL fCapture) override {
if (fCapture) {
SetCapture(hwnd_);
}
else {
ReleaseCapture();
}
}

void TxSetFocus() override {

}

void TxSetCursor(HCURSOR hcur, BOOL fText) override {
SetCursor(hcur);
}

BOOL TxScreenToClient(LPPOINT lppt) override {
return FALSE;
}

BOOL TxClientToScreen(LPPOINT lppt) override {
return FALSE;
}

HRESULT TxActivate(LONG * plOldState) override {
return E_NOTIMPL;
}

HRESULT TxDeactivate(LONG lNewState) override {
return E_NOTIMPL;
}

HRESULT TxGetClientRect(LPRECT prc) override {
GetClientRect(hwnd_, prc);
return S_OK;
}

HRESULT TxGetViewInset(LPRECT prc) override {
return E_NOTIMPL;
}

HRESULT TxGetCharFormat(const CHARFORMATW **ppCF) override {

if (char_format_ == nullptr) {
char_format_ = std::make_unique<CHARFORMATW>();
char_format_->cbSize = sizeof(CHARFORMATW);
}

*ppCF = char_format_.get();
return S_OK;
}

HRESULT TxGetParaFormat(const PARAFORMAT **ppPF) override {

if (para_format_ == nullptr) {
para_format_ = std::make_unique<PARAFORMAT>();
para_format_->cbSize = sizeof PARAFORMAT;
}

*ppPF = para_format_.get();
return S_OK;
}

COLORREF TxGetSysColor(int nIndex) override {
return GetSysColor(nIndex);
}

HRESULT TxGetBackStyle(TXTBACKSTYLE *pstyle) override {
return E_NOTIMPL;
}

HRESULT TxGetMaxLength(DWORD *plength) override {
return E_NOTIMPL;
}

HRESULT TxGetScrollBars(DWORD *pdwScrollBar) override {
return E_NOTIMPL;
}

HRESULT TxGetPasswordChar(_Out_ TCHAR *pch) override {
return E_NOTIMPL;
}

HRESULT TxGetAcceleratorPos(LONG *pcp) override {
return E_NOTIMPL;
}

HRESULT TxGetExtent(LPSIZEL lpExtent) override {
return E_NOTIMPL;
}

HRESULT OnTxCharFormatChange(const CHARFORMATW * pCF) override {
return E_NOTIMPL;
}

HRESULT OnTxParaFormatChange(const PARAFORMAT * pPF) override {
return E_NOTIMPL;
}

HRESULT TxGetPropertyBits(DWORD dwMask, DWORD *pdwBits) override {
*pdwBits = 0;
return S_OK;
}

HRESULT TxNotify(DWORD iNotify, void *pv) override {
return E_NOTIMPL;
}

HIMC TxImmGetContext() override {
return nullptr;
}

void TxImmReleaseContext(HIMC himc) override {

}

HRESULT TxGetSelectionBarWidth(LONG *lSelBarWidth) override {
*lSelBarWidth = 0;
return S_OK;
}

private:
HWND hwnd_;
std::unique_ptr<CHARFORMATW> char_format_;
std::unique_ptr<PARAFORMAT> para_format_;
};

EXTERN_C const IID IID_ITextServices = {
0x8d33f740,
0xcf58,
0x11ce,
{ 0xa8, 0x9d, 0x00, 0xaa, 0x00, 0x6c, 0xad, 0xc5 }
};

EXTERN_C const IID IID_ITextHost = {
0xc5bdd8d0,
0xd26e,
0x11ce,
{ 0xa8, 0x9e, 0x00, 0xaa, 0x00, 0x6c, 0xad, 0xc5 }
};

std::shared_ptr<TextHost> g_text_host;
CComPtr<ITextServices> g_text_service;

LRESULT CALLBACK WindowProcedure(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE, HINSTANCE, char*, int) {

WNDCLASSEX default_class = { 0 };
default_class.cbSize = sizeof(default_class);
default_class.style = CS_HREDRAW | CS_VREDRAW;
default_class.lpfnWndProc = WindowProcedure;
default_class.cbClsExtra = 0;
default_class.cbWndExtra = sizeof(LONG_PTR);
default_class.hInstance = NULL;
default_class.hIcon = NULL;
default_class.hCursor = LoadCursor(NULL, IDI_APPLICATION);
default_class.hbrBackground = reinterpret_cast<HBRUSH>(COLOR_WINDOW + 1);
default_class.lpszMenuName = nullptr;
default_class.lpszClassName = L"WindowlessRichEdit";
default_class.hIconSm = NULL;

RegisterClassEx(&default_class);

HWND window_handle = CreateWindowEx(
0,
L"WindowlessRichEdit",
nullptr,
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
nullptr,
nullptr,
nullptr,
nullptr
);

MSG msg;
while (GetMessage(&msg, nullptr, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}

return 0;
}


LRESULT CALLBACK WindowProcedure(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {

switch (message) {
case WM_CREATE: {

g_text_host = std::make_shared<TextHost>(hwnd);

HMODULE module_handle = LoadLibrary(L"riched20.dll");

typedef HRESULT(_stdcall*CreateTextServicesFunction)(IUnknown*, ITextHost*, IUnknown**);
CreateTextServicesFunction create_function = reinterpret_cast<CreateTextServicesFunction>(GetProcAddress(module_handle, "CreateTextServices"));

const IID* iid_text_service = reinterpret_cast<IID*>(GetProcAddress(module_handle, "IID_ITextServices"));

CComPtr<IUnknown> unknown;
create_function(nullptr, g_text_host.get(), &unknown);
unknown->QueryInterface(*iid_text_service, reinterpret_cast<void**>(&g_text_service));

g_text_service->TxSetText(L"Windowless RichEdit");
return 0;
}

case WM_PAINT: {

PAINTSTRUCT paint_struct;
HDC hdc = BeginPaint(hwnd, &paint_struct);

RECT rect;
GetClientRect(hwnd, &rect);

g_text_service->TxDraw(
DVASPECT_CONTENT,
0,
nullptr,
nullptr,
hdc,
nullptr,
reinterpret_cast<LPCRECTL>(&rect),
nullptr,
nullptr,
nullptr,
0,
0
);

EndPaint(hwnd, &paint_struct);
return 0;
}

case WM_SETFOCUS: {

g_text_service->OnTxInPlaceActivate(nullptr);

LRESULT result = 0;
g_text_service->TxSendMessage(message, wParam, lParam, &result);

return result;
}

case WM_KILLFOCUS: {

g_text_service->OnTxInPlaceDeactivate();

LRESULT result = 0;
g_text_service->TxSendMessage(message, wParam, lParam, &result);

return result;
}

case WM_KEYDOWN:
case WM_KEYUP:
case WM_CHAR:{

LRESULT result = 0;
g_text_service->TxSendMessage(message, wParam, lParam, &result);

return result;
}

case WM_SETCURSOR: {

HDC hdc = GetDC(hwnd);

POINT position = { 0 };
GetCursorPos(&position);
ScreenToClient(hwnd, &position);

RECT rect = { 0 };
GetClientRect(hwnd, &rect);

g_text_service->OnTxSetCursor(
DVASPECT_CONTENT,
0,
nullptr,
nullptr,
hdc,
nullptr,
&rect,
position.x,
position.y
);

ReleaseDC(hwnd, hdc);

return TRUE;
}

case WM_MOUSEMOVE:
case WM_LBUTTONDOWN:
case WM_LBUTTONUP: {

LRESULT result = 0;
g_text_service->TxSendMessage(message, wParam, lParam, &result);

return result;
}

default:
return CallWindowProc(DefWindowProc, hwnd, message, wParam, lParam);
}
}