创建一个最简单的Windowless RichEdit

RichEdit是Windows上很常用的富文本控件,它有一个无窗口化的版本,即Windowless RichEdit,关于它的介绍,可以参考官方文档:https://msdn.microsoft.com/en-us/library/windows/desktop/bb787609(v=vs.85).aspx 。Windowless RichEdit与普通RichEdit在行为表现上毫无二致,但是在使用方法上却有较大的差异;而且Windowless RichEdit的官方文档少之又少,说明不够全面,甚至连一个完整的示例也没有;更甚者,在不同的Windows平台和开发环境下,Windowless RichEdit的用法都有差异。这让初次接触Windowless RichEdit的人举步维艰,处处碰壁,正应了“万事开头难”这句话。因此,本文聚焦于“开头”,介绍一下创建一个最简单的Windowless RichEdit需要做哪些事情。

Windowless RichEdit的接口

Windowless RichEdit是一个COM组件,使用者需要用到两个接口,分别是ITexService和ITextHost。ITextService表示Windowless RichEdit控件本身,提供了一系列访问该控件的方法。ITextHost是一个回调接口,需要由使用者实现,并传递给ITextService,当ITextService有需要的时候会调用该接口的方法。

实现ITextHost

在创建ITextService的时候需要传递一个ITextHost对象给它,所以首先要做的是实现ITextHost。ITextHost定义于TextServ.h,它包含了一系列回调方法,其中下面几个是必须要实现的:

  • TxGetPropertyBits
  • TxGetCharFormat
  • TxGetParaFormat
  • TxGetSysColor
  • TxGetSelectionBarWidth

TxGetPropertyBits用于获取Windowless RichEdit的各种属性,例如是否支持多行,是否支持富文本等。最简单的实现方式是使用默认属性,如下所示:

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

如果该方法的返回值不是S_OK,在创建ITextService的时候会失败。

TxGetCharFormat和TxGetParaFormat分别用于获取默认的字符格式和段落格式。字符格式包括字体、大小、颜色、加粗、倾斜等属性;段落格式包括对齐方式等属性。最简单的实现方式是使用默认的格式,如下所示:

1
2
3
4
5
6
7
8
9
10
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;
}
1
2
3
4
5
6
7
8
9
10
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;
}

这两个方法都要取得结构体的指针,这要求实现者自己来维护这些结构体的生命周期。这里使用std::unique_ptr智能指针来维护这两个结构体,并且把它们作为成员变量。这两个结构体除了cbSize之外,其它字段都可以置为0,表示使用默认设置。

如果这两个方法的返回值不是S_OK,在创建ITextService的时候会失败。

TxGetSysColor用于获取各种默认颜色值,例如背景颜色、字符颜色等。如果没有特殊需求,只要调用GetSysColor即可,如下所示:

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

TxGetSelectionBarWidth用于获取selection bar的宽度。所谓selection bar就是位于控件左侧的竖直长条形区域,这块区域不可见,当鼠标移动到上面时,鼠标指针会水平翻转,此时点击左键就可以快速选择一整行的内容。之所以要实现这个方法,是因为Windowless RichEdit没有初始化表示selection bar宽度的变量,导致这个宽度在大部分情况下远远超出了Windowless RichEdit的可视区域,造成文字绘制不出来的假象。所以初始化这个变量的任务要由实现者来完成了,如下所示:

1
2
3
4
HRESULT TxGetSelectionBarWidth(LONG *lSelBarWidth) override {
*lSelBarWidth = 0;
return S_OK;
}

创建ITextService

实现了ITextHost之后,即可使用函数CreateTextServices来创建ITextService,该函数也位于TextServ.h,其声明如下:

1
2
3
4
5
HRESULT CreateTextServices(
_In_ IUnknown *punkOuter,
_In_ ITextHost *pITextHost,
_Out_ IUnknown **ppUnk
);

第一个参数punkOuter用于对象聚合,一般情况下传nullptr即可;第二个参数plTextHost即ITextHost对象;第三个参数ppUnk是获取返回结果的输出参数,注意该参数的实际类型是IUnknown,而不是ITextService,所以之后还要再调用一次QueryInterface来取得ITextService。

官方文档指出使用CreateTextServices时需要导入riched20.lib这个库,但是在Visual Studio 2013环境下是找不到这个文件的,所以只能动态加载riched20.dll并且找出CreateTextServices的地址。更旧的Visual Studio版本可能没有这个问题。

同样的,查询ITextService时用到的IID_ITextService常量也要用这种方式来得到。不过,据说在riched20.dll中导出的IID_ITextService常量是错误的,使用它查询不到ITextService。但在Windows 7下试验过是可以的,可能在这个版本的Windows中已经修复了这个问题。如果从DLL中拿出来的IID_ITextService确实有问题,那么可以自己来定义这个常量:

1
2
3
4
5
6
EXTERN_C const IID IID_ITextServices = {
0x8d33f740,
0xcf58,
0x11ce,
{ 0xa8, 0x9d, 0x00, 0xaa, 0x00, 0x6c, 0xad, 0xc5 }
};

通过动态加载DLL来创建ITextService的过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
g_text_host = std::make_shared<TextHost>();

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));

上述代码通过LoadLibrary加载riched20.dll,然后通过GetProcAddress得到CreateTextServices和IID_ITextService的地址。注意,当还在使用ITextService的时候,不能调用FreeLibrary卸载riched20.dll,否则会出错。

绘制Windowless RichEdit

最后要做的就是把Windowless RichEdit显示出来,只要调用ITextService的TxDraw方法即可完成。该方法的声明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
HRESULT TxDraw(
DWORD dwDrawAspect,
LONG lindex,
void *pvAspect,
DVTARGETDEVICE *ptd,
HDC hdcDraw,
HDC hicTargetDev,
LPCRECTL lprcBounds,
LPCRECTL lprcWBounds,
LPRECT lprcUpdate,
BOOL CALLBACK *pfnContinue,
DWORD dwContinue,
LONG lViewId
);

该方法的参数繁多,但大部分情况下只需要关注其中的dwDrawAspecthdcDrawlprcBounds即可。dwDrawAspect指定要绘制控件的哪一部分,传入DVASPECT_CONTENT即可,表示绘制控件内容;hdcDraw即用来绘制的DeviceContext句柄;lprcBounds指定要绘制的区域。其它参数只要指定无效值即可,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
);

lprcBounds的使用的类型是RECTL,与平常使用的RECT不一样,但是它们的定义是一模一样的,因此可以使用强制类型转换。

完整代码

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

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
#include <Windows.h>
#include <atlbase.h>
#include <Richedit.h>
#include <TextServ.h>
#include <memory>

class TextHost : public ITextHost {
public:
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 nullptr;
}

INT TxReleaseDC(HDC hdc) override {
return 0;
}

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 {

}

void TxViewChange(BOOL fUpdate) override {

}

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

BOOL TxShowCaret(BOOL fShow) override {
return FALSE;
}

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

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 {

}

void TxSetFocus() override {

}

void TxSetCursor(HCURSOR hcur, BOOL fText) override {

}

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 {
return E_NOTIMPL;
}

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:
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 = 0;
default_class.hInstance = nullptr;
default_class.hIcon = nullptr;
default_class.hCursor = LoadCursor(nullptr, IDI_APPLICATION);
default_class.hbrBackground = reinterpret_cast<HBRUSH>(COLOR_WINDOW + 1);
default_class.lpszMenuName = nullptr;
default_class.lpszClassName = L"WindowlessRichEdit";
default_class.hIconSm = nullptr;

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>();

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;
}

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