☰
基于CRichEditCtrl的MFC简易RTF编辑器实现与避坑指南
2026/10/13 7:33:09 网站建设 项目流程

简介:面向MFC与C++开发者的简易RTF编辑器示例,基于RichEdit控件实现,界面模仿Word,可帮助理解富文本编辑器的整体架构与常用功能,适合课程设计、毕业设计或MFC进阶学习。压缩包共27个文件,包含11个.h头文件、8个.cpp源文件,以及sln、vcxproj工程文件、rc/ico资源文件和编译好的Word.exe,整体仅198KB,代码规模适中,便于逐项阅读、跟踪和调试。已有1192人学习下载,实用性获得不少学习者认可。编辑器实际支持的交互包括字号字体、文本/背景颜色、对齐、加粗斜体、超链接、下划线删除线、上下标、撤销恢复、复制粘贴(保留源格式/纯文本)、格式刷、保存打开、打印、段落格式、插入图片、缩放、查找替换等丰富功能,保存文件兼容Microsoft Word 2013。此外,打印功能实测有效,但存在一点小Bug,作者表示资源会不定期更新;源码中对话框、查找替换、段落设置等模块划分明确,下载后可在Visual Studio中重新编译,便于二次开发。

1. 为什么做一个 MFC 简易 RTF 编辑器,而不是直接用文本框

一个老项目里经常有这样的诉求:界面上需要一块“能排版”的编辑区,能贴入网页或 Word 里复制过来的彩色内容,能保留字体、加粗、对齐,能一键存成 .rtf 再被别的软件打开。直接上 CEdit 只能存纯文本,自绘一个排版引擎又明显超出“简易”二字的成本。最务实的答案,是用 MFC 自带的 CRichEditCtrl 控件——它本质上是把 Windows 的 RichEdit 控件封装成了 MFC 类,天然支持 RTF 流式读写、字体格式、段落格式、查找替换和 OLE 粘贴。这篇文章的目标,就是把这套东西拆开:先搞明白它的底层机制,再搭一个最小可用的编辑器工程,最后把文件读写和几个高频坑一次说清楚。适合三类人:给老 MFC 程序维护界面的工程师、想在对话框里快速塞进一个“格式化记事本”的入门开发者,以及准备评估 RichEdit 方案是否够用的人。

2. 选型与底层原理:CRichEditCtrl、RTF 流式读写的设计边界

2.1 用 CRichEditCtrl 而不是 CEdit:三行代码就能看出差距

CEdit 在多行模式(ES_MULTILINE)下只能处理纯文本,你设置加粗、颜色、行距、项目符号全部要自己管,更不用说把某个选区变成红色这样最基本的操作。CRichEditCtrl 则把字符格式、段落格式、选区、查找、流式输入输出都封装成了成员函数。差别最直观的体现,是给选中文字设置颜色的操作:

// CEdit 里你需要自己记录选区文本、再手动重建整个内容 // CRichEditCtrl 里,直接改选中部分的格式即可 CHARFORMAT cf = { 0 }; cf.cbSize = sizeof(cf); cf.dwMask = CFM_COLOR; // 只改颜色这一项 cf.dwEffects = 0; // 清除 CFE_AUTOCOLOR cf.crTextColor = RGB(200, 50, 50); m_rich.SetSelectionCharFormat(cf); // 只作用于当前选中区域

这里最关键的是dwMask:它告诉控件“这次只修改颜色,其余格式保持不变”。如果你把dwMask设成 0,或者漏掉dwEffects = 0,会出现改了颜色却顺带把字体、字号、加粗全部重置的“玄学”现象。SetSelectionCharFormat是对当前选区生效,跟SetDefaultCharFormat(对后续输入生效)、SetWordCharFormat(对光标所在的词生效)三个函数要区分开。

选择 CRichEditCtrl 而不是 CRichEditView,也是基于“简易”二字。CRichEditView 是 MFC 的 Doc/View 架构组件,自带完整的文档序列化、打印预览、查找替换对话框,功能完整但侵入性太强。在一个已有的对话框程序里塞一个 CRichEditView,需要改造 Frame、Document、Template 一堆配套对象,代价远大于用 CRichEditCtrl 直接创建一个子窗口控件。

2.2 流式读写的两个核心:EDITSTREAM 与 SF_RTF

RTF 的读写在 RichEdit 里走的是“流式”模型:控件不要求你把整个文件内容一次性塞进内存,而是通过一个EDITSTREAM结构体和一个回调函数,分块向你索要数据(读入时)或分块把数据交给你(写出时)。很多人第一次接触会不理解:为什么不用SetWindowText或者GetWindowText,偏要绕一圈回调?

因为SetWindowText只能传纯文本,传 RTF 字符串进去只会被当作普通文本显示出来;GetWindowText取出来的同样是纯文本,格式信息全部丢失。RTF 内容必须经过 RichEdit 自己的解析器/生成器,才能落成真实的字符格式。流式回调就是这套解析器和你之间的数据通道。

EDITSTREAM es = { 0 }; es.dwCookie = reinterpret_cast<DWORD_PTR>(&file); // 回调里能拿到的自定义数据 es.pfnCallback = MyStreamCallback; // 读写数据的回调函数 m_rich.StreamIn(SF_RTF, es); // 从文件读入 RTF

dwCookie的类型是DWORD_PTR,这是 64 位兼容的关键——传指针时必须用DWORD_PTR而不是DWORD。回调函数签名是DWORD CALLBACK (DWORD_PTR dwCookie, LPBYTE pbBuff, LONG cb, LONG *pcb),其中cb是控件请求的字节数,回调要把实际读到的字节数写入*pcb,返回值 0 表示继续。这三个参数任何一个写错,轻则读到一半停止,重则内存越界崩溃。

2.3 RichEdit 版本差异与初始化顺序:AFX_RICHEDIT_CLASS 与 RICHEDIT_CLASS

MFC 的CRichEditCtrl::Create默认创建的是AFX_RICHEDIT_CLASS窗口类,对应 Riched20.dll 的 RichEdit 2.0/3.0。这个版本对简易编辑器足够:支持 RTF 1.x 规范、CHARFORMAT 格式、流式读写、OLE 粘贴。

如果你需要更新的能力,比如EM_SETZOOM缩放、更好的表格支持、更完整的段落样式,可以考虑 RichEdit 4.1(Msftedit.dll 的RICHEDIT_CLASS)。但要注意,越新的版本对底层接口的细节要求越高,比如 4.1 在 MFC 下通过CRichEditCtrl包装时,某些消息需要自己SendMessage。我的建议是:简易 RTF 编辑器直接默认版本,不要为了“最新”而升级,除非你已经确认某个具体功能只有新版本才有。

初始化顺序有一个很容易踩的步骤:在对话框OnInitDialog里创建控件之前,必须调用AfxInitRichEdit()。这个函数负责加载 RichEdit 模块并注册窗口类。漏掉它,Create会失败,或者运行时弹出一个“未找到窗口类”的断言。顺序是:先AfxInitRichEdit(),再m_rich.Create(...),最后设置格式和选项。

BOOL CSimpleRtfDlg::OnInitDialog() { CDialogEx::OnInitDialog(); AfxInitRichEdit(); // 必须放在 Create 之前 m_rich.Create( WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP | ES_MULTILINE | ES_WANTRETURN | WS_VSCROLL | WS_HSCROLL, CRect(0, 0, 400, 300), this, IDC_RICH_EDIT); return TRUE; }

ES_WANTRETURN在 RichEdit 里意味着回车键产生换行而不是触发默认按钮;ES_MULTILINE是必须的,否则控件退化成单行输入。创建一个像样的编辑区,这两组样式缺一不可。

3. 先搭一个能跑的最小工程:对话框里放一个可以编辑 RTF 的控件

3.1 最小工程里创建控件的三件套:AfxInitRichEdit、Create、SetOptions

如果你不想在资源编辑器里拖控件,完全可以用代码动态创建。一个最小可用的对话框类长这样,头文件里只需要一个CRichEditCtrl成员和三个方法:

// SimpleRtfDlg.h class CSimpleRtfDlg : public CDialogEx { public: CSimpleRtfDlg(CWnd* pParent = nullptr); enum { IDD = IDD_SIMPLE_RTF_DIALOG }; protected: CRichEditCtrl m_rich; virtual BOOL OnInitDialog(); afx_msg void OnSize(UINT nType, int cx, int cy); afx_msg BOOL PreTranslateMessage(MSG* pMsg); DECLARE_MESSAGE_MAP() };

OnInitDialog里做创建和初始格式设置。控件占满整个客户区之后,在OnSize里跟着窗口尺寸调整,这样对话框拉伸时编辑区也跟着走。这是 MFC 对话框程序最常见的布局手法。

BOOL CSimpleRtfDlg::OnInitDialog() { CDialogEx::OnInitDialog(); AfxInitRichEdit(); CRect rc; GetClientRect(&rc); m_rich.Create( WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP | ES_MULTILINE | ES_WANTRETURN | WS_VSCROLL | WS_HSCROLL, rc, this, IDC_RICH_EDIT); // 自动滚动 + 按 Del 时删除整行(符合文本编辑器直觉) m_rich.SetOptions(ECOOP_OR, ECO_AUTOVSCROLL | ECO_AUTOHSCROLL); // 设置默认字体,避免默认的“Segoe UI”显示中文发虚 CHARFORMAT cf = { 0 }; cf.cbSize = sizeof(cf); cf.dwMask = CFM_FACE | CFM_SIZE | CFM_COLOR; cf.yHeight = 210; // 10.5 磅,单位是缇(1 磅 = 20 缇) cf.dwEffects = 0; _tcscpy_s(cf.szFaceName, _T("Microsoft YaHei")); m_rich.SetDefaultCharFormat(cf); return TRUE; } void CSimpleRtfDlg::OnSize(UINT nType, int cx, int cy) { CDialogEx::OnSize(nType, cx, cy); if (m_rich.GetSafeHwnd()) m_rich.MoveWindow(0, 0, cx, cy); }

SetOptions(ECOOP_OR, ...)里的ECOOP_OR表示把后面的选项追加到控件当前的选项集合里,而不是覆盖。这一点新手容易误解:直接调SetOptions(0, ECO_AUTOVSCROLL)会把原有选项清空,导致 Tab 键行为、自动滚动全部异常。cf.yHeight的单位是缇(twip),1 磅等于 20 缇,所以 10.5 磅就是 210。字号设不对,通常就是忘了这个换算关系。

3.2 设置默认字体、初始格式与页面尺寸三个参数

SetDefaultCharFormat决定的是“用户开始输入时”的初始格式,它不会回改已经输入的字符。如果你打开一个空编辑器,敲出来的字用默认字体;但如果粘贴了一段网页富文本,粘贴进来的内容会带着它自己的格式,默认格式不会强加给它。这个行为符合用户直觉,但如果你希望“粘贴也必须用我的字体”,就得在OnPaste里自行处理后处理,简易编辑器一般不需要。

页面尺寸的默认值是控件自身的显示尺寸。这在打印时会有问题,一个 800x600 的窗口里编辑的内容,打印到 A4 纸上时换行位置可能完全错位。简易编辑器如果不做打印,可以不管;如果要存心做好,可以调用SetTargetDevice指定一个目标设备(比如打印机 DC),RichEdit 会按照目标设备的物理尺寸来排版换行。

// 让编辑器按 A4 纸宽度排版,而不是按窗口宽度 CDC* pDC = GetDC(); // 这里需要拿到打印机 DC 再调用 // m_rich.SetTargetDevice(*pDC, 0); ReleaseDC(pDC);

这个函数在中文环境里有一个常见副作用:设置目标设备后,屏幕上有些字体渲染细节会变化,尤其是缩放比例不是 100% 时字迹发淡。所以简易 RTF 编辑器建议跳过SetTargetDevice,保持“所见即窗口所得”,打印功能留到真有需求时再做。

3.3 与主框架的接口:把 CTRL+S 直接接到保存文件

对话框程序的快捷键跟单文档程序不一样,没有现成的OnFileSave命令路由。最直接的办法是在PreTranslateMessage里拦下 Ctrl+S,把保存动作和控件解耦。这样用户按 Ctrl+S 时,不会因为对话框没有加速键表而在系统里发出“叮”的一声没反应。

BOOL CSimpleRtfDlg::PreTranslateMessage(MSG* pMsg) { if (pMsg->message == WM_KEYDOWN && pMsg->wParam == 'S' && (GetKeyState(VK_CONTROL) & 0x8000)) { SaveFile(); // 保存动作在下一章实现 return TRUE; } return CDialogEx::PreTranslateMessage(pMsg); }

拦截快捷键的小细节是(GetKeyState(VK_CONTROL) & 0x8000)的判断,不能用GetAsyncKeyState,因为在消息循环里GetAsyncKeyState可能拿到键盘状态已经变化的瞬间,导致连续触发两次保存。PreTranslateMessage里返回 TRUE 之后消息不会继续分发,也不会有“叮”的提示音。

到这里,一个能打开、能输入、能拉伸、能 Ctrl+S 的壳子已经成型。壳子本身不会让你写出漂亮的 RTF,但它是接下来所有文件读写逻辑的依附点。

4. 把 RTF 落地到磁盘:StreamIn / StreamOut 与三种常见误用

4.1 从文件读入 RTF:StreamIn + 回调 + DWORD_PTR 缺一不可

读文件的核心是把CFile的读取能力交给回调,再让控件通过StreamIn自己拉数据。回调每次请求的块大小由控件决定,常见的是 4KB 到 16KB,回调不需要关心“这些数据是什么”,只需要忠实地把文件字节交出去。

DWORD CALLBACK RtfStreamIn(DWORD_PTR dwCookie, LPBYTE pbBuff, LONG cb, LONG* pcb) { CFile* pFile = reinterpret_cast<CFile*>(dwCookie); UINT nRead = pFile->Read(pbBuff, cb); // 读不到整块时返回实际字节数 *pcb = (LONG)nRead; return 0; // 0 表示继续,非 0 表示出错中止 } BOOL CSimpleRtfDlg::OpenFile(const CString& strPath) { CFile file; if (!file.Open(strPath, CFile::modeRead | CFile::typeBinary)) return FALSE; // 覆盖整个文档的通用做法:先全选,再用 SFF_SELECTION 写入 m_rich.SetSel(0, -1); EDITSTREAM es = { 0 }; es.dwCookie = reinterpret_cast<DWORD_PTR>(&file); es.pfnCallback = RtfStreamIn; m_rich.StreamIn(SF_RTF | SFF_SELECTION, es); return es.dwError == 0; }

这里有两个关键点。第一,dwCookie必须是DWORD_PTR,在 64 位程序里如果用DWORD接收&file,地址会被截断成 32 位,回调里拿到的是错误指针,后果是崩溃或者读到完全错误的数据。第二,SetSel(0, -1)配合SFF_SELECTION,语义是“全选当前内容并用流替换选区”。如果你不用SFF_SELECTION,StreamIn会做“全文档替换”,两者效果类似,但对于已经有一段文字选中的情况,后者会让你之前的高亮状态残留。用SFF_SELECTION的写法最直觉:先选什么,流就替换什么。

4.2 写回文件:StreamOut、换行与版本标记

写文件的流程跟读文件对称:回调负责接收控件给出的字节流,StreamOut负责触发这个动作。RTF 里所有的换行、缩进、段落格式都是由控制字表示的,不会出现“到底用 \n 还是 \r\n”的纠结,因为控件已经替你处理好了。

DWORD CALLBACK RtfStreamOut(DWORD_PTR dwCookie, LPBYTE pbBuff, LONG cb, LONG* pcb) { CFile* pFile = reinterpret_cast<CFile*>(dwCookie); pFile->Write(pbBuff, cb); *pcb = cb; return 0; // 0 表示继续 } BOOL CSimpleRtfDlg::SaveFile(const CString& strPath) { CFile file; if (!file.Open(strPath, CFile::modeCreate | CFile::modeWrite | CFile::typeBinary)) return FALSE; EDITSTREAM es = { 0 }; es.dwCookie = reinterpret_cast<DWORD_PTR>(&file); es.pfnCallback = RtfStreamOut; m_rich.StreamOut(SF_RTF, es); return es.dwError == 0; }

StreamOut完成后,打开生成的 .rtf 文件,第一行一定是{\rtf1\ansi\ansicpg936...}这样的头,这是 RTF 规范要求的版本标记。如果你拿到一个文件第一行是{\rtf0或根本没有\rtf标记,说明生成器不是 RichEdit 而是某个简化库,Word 打开时可能会弹“文件格式与扩展名不匹配”。RichEdit 生成的头部一般不需要自己干预,除非你想刻意修改\ansicpg代码页标记。

4.3 只读写选中区域:SFF_SELECTION 做局部交换

SFF_SELECTION不只是用于“全选替换”,它还可以做出一个很实用的功能:把选中的一小段内容导出成一个独立的 RTF 片段,或者把一个 RTF 片段插入到任意位置。比如做一个“收藏格式片段”的笔记工具,选中一段带格式的文字,点按钮存成 .rtf 片段,之后在别的文档里选中一个插入点,再点按钮把片段读回来。

// 导出选中区域到文件 void CSimpleRtfDlg::ExportSelection(const CString& strPath) { CFile file; if (!file.Open(strPath, CFile::modeCreate | CFile::modeWrite | CFile::typeBinary)) return; EDITSTREAM es = { 0 }; es.dwCookie = reinterpret_cast<DWORD_PTR>(&file); es.pfnCallback = RtfStreamOut; // SFF_SELECTION 让 StreamOut 只输出当前选中的内容 m_rich.StreamOut(SF_RTF | SFF_SELECTION, es); } // 在插入点导入片段 void CSimpleRtfDlg::ImportSelection(const CString& strPath) { CFile file; if (!file.Open(strPath, CFile::modeRead | CFile::typeBinary)) return; long nStart, nEnd; m_rich.GetSel(nStart, nEnd); // 记住当前光标位置 m_rich.SetSel(nStart, nStart); // 把选区设成空插入点 EDITSTREAM es = { 0 }; es.dwCookie = reinterpret_cast<DWORD_PTR>(&file); es.pfnCallback = RtfStreamIn; m_rich.StreamIn(SF_RTF | SFF_SELECTION, es); }

这个操作的边界条件要注意:如果导入前你忘记GetSel记录的只是插入点而用户实际选中了一段文字,SetSel(nStart, nStart)会把选区缩成一个点,之后的导入会替换掉原本选中的文字。这不是 Bug,是SFF_SELECTION的语义——它替换“当前选区”,无论选区是一个点还是一段文字。写代码时最好在导入前弹一次确认,明确告知“插入到光标处”还是“替换当前选中”。

4.4 纯文本模式兜底:Unicode 转码前后的注意事项

RTF 文件本身是 ASCII 转义文本,没有编码问题。但你的编辑器如果还要打开 .txt 纯文本,RichEdit 不会聪明地自动探测 UTF-8。它只会按当前系统的 ANSI 代码页(简体中文环境是 CP936)解析传入的字节流。一个 UTF-8 编码的 txt 文件直接StreamIn(SF_TEXT),中文会全部变成乱码,因为 UTF-8 的每个中文字符是 3 字节,而 CP936 是按 2 字节解析的。

// 先读全部字节,判断有没有 UTF-8 BOM CStringA strAnsi; BYTE buf[4] = { 0 }; // 实际实现里用 file.Read 把整个文件读入 strAnsi BOOL bUtf8 = (strAnsi.GetLength() > 2 && (BYTE)strAnsi[0] == 0xEF && (BYTE)strAnsi[1] == 0xBB && (BYTE)strAnsi[2] == 0xBF); if (bUtf8) { // 去掉 BOM 后从 UTF-8 转成 UTF-16 CStringW strW = CA2W((LPCSTR)strAnsi + 3, CP_UTF8); m_rich.SetWindowText(strW); }

这里用CA2W(ATL 字符串转换宏)实现编码转换,之前在OnInitDialog里调用过AfxInitRichEdit而没有调用AfxOleInit,此时用纯文本方式读入的 Unicode 内容不会再被转成 RTF,而是以控件内部格式保存。注意:一旦你用SetWindowText放入了纯文本,之后保存文件时仍然应该走StreamOut(SF_RTF),控件会把内存里的文本重新生成 RTF 控制字,不需要你手动转码。

5. 简易编辑器最容易翻车的 5 个坑:现象、原因与处理

5.1 打开文件只显示两三个字符,甚至崩溃

现象:程序不报错,但打开 RTF 文件后编辑区只有开头几个字符,或者直接崩溃退出。原因通常是回调函数里*pcb的赋值逻辑错误——特别是用DWORD而不是DWORD_PTR接收dwCookie,在 64 位下指针被截断,回调拿到的CFile*是野指针。也可能是回调返回了非零值,控件认为读取出错而中止了整个流输入。解决:第一确认EDITSTREAM的dwCookie用reinterpret_cast<DWORD_PTR>转换;第二确认回调里每次必须把实际读取字节数写入*pcb,文件读完时写入 0;第三,回调返回后检查es.dwError,不为 0 时就弹一个“文件解析失败”的提示,不要默默吞掉。

5.2 大文件读取时界面卡死十几秒

现象:打开一个几百 KB、带大量图片的 RTF,窗口无响应,转圈很久。原因:StreamIn是在 UI 线程同步执行的,RichEdit 需要逐条解析 RTF 控制字、重建段落和字符格式,这是一个 CPU 密集的流程。把回调里的一次性读取块调小,并不能解决卡顿——卡顿的瓶颈在解析而不是读取。解决:简易编辑器能做的,是在打开前判断文件大小,超过阈值时先用SetRedraw(FALSE)关闭控件重绘,StreamIn完成后再SetRedraw(TRUE)一次性刷新。这个技巧可以把卡顿的“视觉感”大幅缩短,因为控件不再边解析边重绘。

m_rich.SetRedraw(FALSE); m_rich.StreamIn(SF_RTF | SFF_SELECTION, es); m_rich.SetRedraw(TRUE); m_rich.Invalidate(FALSE);

5.3 中文变成乱码,且只在另存为纯文本后出现

现象:打开一个中文 RTF 文件显示正常,但把内容另存为 txt 后用记事本打开,中文全乱。原因:StreamOut(SF_TEXT)输出的是控件内存里的 UTF-16 内容转换到 ANSI 代码页的纯文本,而你的工程是 Unicode(_UNICODE)时,RichEdit 内部的默认输出可能是 UTF-16 但没有正确写入 BOM,记事本用 UTF-8 猜测,结果错位。解决:写纯文本时,自己控制转码,不走SF_TEXT。正确做法是先用GetWindowText取出 UTF-16 文本,再用CW2A按 CP_UTF8 转换成 UTF-8,主动写入 BOM(EF BB BF)后再写文件。RTF 文件则不要用这个逻辑,它直接SF_RTF输出即可。

5.4 粘贴网页内容后,图片直接消失

现象:从浏览器复制一段带插图的内容,粘贴到编辑器里文字正常,图片不显示。原因:RichEdit 对 OLE 对象的支持依赖控件的EM_SETOLECALLBACK机制,MFC 默认创建的AFX_RICHEDIT_CLASS版本在这个环节比较脆弱;而且如果你的程序没有初始化 OLE 环境,剪贴板里的图片对象根本挂载不上去。解决:程序初始化时调用一次AfxOleInit()初始化 COM,然后确认剪贴板里确实是“带 OLE 对象的 HTML/RTF”,有时浏览器复制的是一段 HTML,控件能解析 HTML 里的文字却解析不了其中的<img>引用。简易编辑器对图片的态度应该是“能接受文字带格式就行,图片没了就没了”,否则就升级到 RichEdit 4.1 并实现完整的IRichEditOleCallback,那已经是另一个量级的工作量。

5.5 用 GetWindowText 取内容,格式全部丢失

现象:把编辑器内容保存到数据库时,调了GetWindowText,存进去的东西再读回来变成没有格式的纯文本。原因:GetWindowText从设计上就只返回纯文本,不包含任何格式信息;而且 RichEdit 控件在文本较长时,WM_GETTEXT消息本身就有容量限制,超过限制会静默截断。解决:要保存格式,不要用GetWindowText,用流式输出把内容转成 RTF 字符串,再存数据库或文件。

// 把 RTF 内容拿到内存里的正确姿势 CString strRtf; EDITSTREAM es = { 0 }; es.dwCookie = reinterpret_cast<DWORD_PTR>(&strRtf); es.pfnCallback = RtfStreamToString; // 回调里 strRtf.Append 字节 m_rich.StreamOut(SF_RTF, es);

这里的RtfStreamToString回调跟写文件的回调几乎一样,只是把Write换成了CString的追加操作。记住一个原则:保存到磁盘、数据库、剪贴板的一切带格式内容,统一走StreamOut(SF_RTF);只有纯文本需求(比如做全文搜索的索引)时才用GetWindowText。

6. 扩展手感:几个我一直在用的实用改进

6.1 用查找替换把编辑器变成“顺手工具”

CRichEditCtrl 提供了FindText成员函数,配合FINDTEXTEX结构可以拿到找到的精确范围。一个完整的“查找下一个”逻辑要记住上次查找结束的位置,避免永远从开头找重复结果。

FINDTEXTEX ft = { 0 }; ft.chrg.cpMin = nSearchStart; // 从当前光标位置开始 ft.chrg.cpMax = -1; // 查到文末 ft.lpstrText = strFind; long nPos = m_rich.FindText(FR_DOWN, &ft); if (nPos != -1) { m_rich.SetSel(ft.chrgText.cpMin, ft.chrgText.cpMax); m_rich.SetFocus(); }

FindText的返回值是找到的起始位置,找不到返回 -1。找到后要用ft.chrgText.cpMin和cpMax设置选区,而不是用返回值nPos——后者在匹配包含换行或者特殊字符时,可能与实际范围不一致。查找逻辑里我的习惯是:替换一个之后,把nSearchStart设为ft.chrgText.cpMax,这样“全部替换”循环不会在同一个匹配上死循环。

6.2 只读模式、行号显示与其他小细节

一个 RTF 编辑器做成只读之后,就是一个格式化的“查看器”。只需一行m_rich.SetReadOnly(TRUE)。但要注意,只读模式下用户可以复制文字,但不能删除、不能粘贴、不能修改格式,这个状态经常用于“审批意见预览”这样的场景,比用 PDF 控件轻量得多。

行号显示不属于 RichEdit 的默认能力,简易方案是在编辑区左边放一个静态文本窗口,监听ON_WM_VSCROLL和EN_UPDATE消息,用EM_LINEFROMCHAR和EM_GETLINECOUNT计算当前第一行和总行数来刷新行号区。这个功能如果做得太重会拖累滚动性能,我的经验是行号区能显示多少行就只刷新多少行,不要每次重画全部行号。

我最后想保留的习惯是:每次写完一个StreamIn/StreamOut的封装,都会用一个 Word 能打开的样例文件做一次交叉验证。因为 RichEdit 在不同版本、不同工程字符集(Unicode / MBCS)下的表现有细微差别,自己写一个简单编辑器很容易只在自己的环境下正常,换个系统就翻车。交叉验证花不了两分钟,却能在交付前把“用户那边打不开”的风险降到最低。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询