基于MFC CRichEditCtrl的简易RTF编辑器开发实践

📅 发布时间:2026/9/9 2:41:43
基于MFC CRichEditCtrl的简易RTF编辑器开发实践
简介这是一款基于MFC与RichEdit控件开发的简易RTF编辑器源码面向有C基础、希望深入理解MFC界面编程或富文本编辑机制的开发者。程序模仿Word常用操作实现了字号字体、文本颜色、背景色、对齐、加粗、斜体、下划线、删除线、上下标、超链接、格式刷、撤销恢复、复制粘贴保留源格式/纯文本、查找替换、插入图片、缩放、打印等丰富功能输出文件兼容Microsoft Word 2013。资源共27个文件以h头文件与cpp源文件为核心另含Visual Studio解决方案/工程配置、RC资源描述及可直接运行的exe压缩包仅198KB便于快速运行或重新编译。代码中封装的查找、替换、缩放、段落、字体等对话框类可作为MFC自定义对话框与控件综合使用的学习范例。目前已有1190人学习适合需要实现仿Word文本编辑器、研究RichEdit高级用法或完善MFC项目结构的开发者参考。 MFC的简易RTF编辑器这个需求放在今天一堆Web富文本、Electron编辑器面前确实显得复古。但我这几年帮人收拾老项目的经历里这种需求反而很常见某个内部工具或者行业软件需要在内嵌窗口里编辑带格式的图文不能为这一个功能引入一整套浏览器内核打开RTF文件时还不能让字体样式乱掉。MFC自带的CRichEditCtrl恰好是解决这类问题的现成组件花一两天就能搭出一个能用的简易RTF编辑器。这篇文章就按这个标题把完整思路、关键代码套路和文档里不会写清楚的坑捋一遍适合要在MFC程序里快速集成富文本能力或者想把手头老项目维护明白的人参考。1. 方案选型为什么是MFC加CRichEditCtrl1.1 自己画文本渲染的坑有多大如果不用现成控件自己实现富文本渲染基本等于重写一个轻量排版引擎成本远超预期。你要处理的不仅是“把字符串画到屏幕上”还有字体布局、行高、段落缩进、选区高亮、滚动条联动、Undo栈、OLE对象嵌入再加上中文换行和字体回退每一件都是无底洞。CRichEditCtrl封装的是Windows系统富文本控件底层是Riched20.dll天生支持RTF读写、文字样式、段落样式、OLE嵌入、撤销重做和MFC对话框里的按钮、列表框组合使用也顺手。我见过有人放着这个控件不用非要在CListCtrl里模拟文本编辑最后光是处理光标闪烁和选中态就改了两周。做简易编辑器“简易”两个字的关键不在于从零造轮子而在于复用系统已经打磨好的能力。1.2 为什么目标格式选RTF而不是HTMLRTF、HTML、纯文本这三条路我简单对比如下。格式格式保留系统原生支持适用场景RTF强Rich Edit原生支持无需额外解析Window桌面工具、Office兼容HTML强需要引入解析与渲染工作量大Web端或需要外发网页纯文本无最简单只需要内容的场景RTF本质是带控制字的纯文本比如文件头通常是{\rtf1\ansi一旦打开出错用记事本看一眼就能定位问题。CRichEditCtrl对RTF的支持是系统级的不需要自己实现字体编码转换也不需要管Unicode控制字的细节这是简易编辑器最看重的“省事”。相比之下HTML虽然Web端更通用但要在MFC控件里渲染一套HTML光CSS子集就够折腾。1.3 功能边界简到什么程度合适功能边界决定项目周期。我建议把“一期功能”锁定在这几项新建、打开、保存、另存为字体、字号、加粗、斜体、下划线、字体颜色段落左对齐、居中、右对齐查找替换撤销重做剪切复制粘贴有条件的可以加一个插入图片。这些覆盖了日常图文编辑八成以上的需求。不建议做的有页眉页脚、多节排版、表格绘制、样式模板库、拼写检查、多人协同。这些功能任何一个拿出来都够单独做一个月而且会把“简易”项目迅速拖成“烂尾”项目。遇到用户提这类需求我的习惯是先问一句“你多久用一次”多半能砍掉。2. 核心机制拆解读写、格式与查找替换2.1 RTF文件读写的核心是StreamIn和StreamOutCRichEditCtrl读写文件走的不是简单的LoadFromFile而是流机制。控件本身不关心文件路径只通过EDITSTREAM结构体加一个回调函数从你提供的数据源读数据或写数据。这样做的好处是你既可以操作磁盘文件也可以操作内存缓冲区、数据库BLOB甚至网络流通用性很强。EDITSTREAM es { 0 }; es.dwCookie (DWORD_PTR)pFile; // 自定义数据回调里转回文件指针 es.pfnCallback StreamInCallback; // 回调函数 m_richEdit.StreamIn(SF_RTF, es);回调函数的原型大概是DWORD CALLBACK StreamInCallback(DWORD_PTR dwCookie, LPBYTE pbBuff, LONG cb, LONG *pcb)读文件时把字节copy到pbBuff写入*pcb返回0表示成功返回非0会中断操作。我踩过的一个坑是回调里忘记处理“读了多少算多少”的部分导致大文件只导入一半后来一律用循环读。保存时用StreamOut(SF_RTF, es)同样的套路。需要注意保存前先判断是否有选中内容防止用户只想导出选区结果把全文覆盖了。流回调在整个RTF编辑器里是最值得先跑通的一环通了文件读写就通了。2.2 字符格式与段落格式的设置套路设置字体、字号、加粗这些操作核心是CHARFORMAT2结构体但它有个容易踩的坑不能简单定义一个结构体然后赋几个字段就SetSelectionCharFormat你必须先用GetSelectionCharFormat把当前格式取出来再修改要改的字段最后设回去。否则会把选区原有的格式全部覆盖掉用户设个字号结果下划线也没了。CHARFORMAT2 cf { 0 }; cf.cbSize sizeof(cf); m_richEdit.GetSelectionCharFormat(cf); cf.dwMask CFM_BOLD | CFM_ITALIC; cf.dwEffects ^ CFE_BOLD; // 切换加粗 m_richEdit.SetSelectionCharFormat(cf);dwMask决定哪些字段生效设计意图就是让你只动想动的部分所以mask别一股脑全部置位否则会改到不想改的格式。段落格式用PARAFORMAT2设置对齐和缩进操作套路和字符格式一模一样先Get再改再Set。对齐示例PARAFORMAT2 pf { 0 }; pf.cbSize sizeof(pf); m_richEdit.GetParaFormat(pf); pf.dwMask PFM_ALIGNMENT; pf.wAlignment PFA_CENTER; m_richEdit.SetParaFormat(pf);2.3 查找替换的实现细节与常见误区查找替换看起来简单最容易翻车的是“自己去遍历控件文本”。一旦文档里有OLE对象、特殊字符、或者嵌入了图片控件内部文本偏移量和屏幕位置并不完全对应用GetWindowText取全文再去匹配经常出现找到了却选不中或者跳错位置的情况。更稳的是用控件自己的EM_FINDTEXTEX消息让Rich Edit自己找FINDTEXTEX ft { 0 }; ft.chrg.cpMin nStartPos; // 搜索起始位置 ft.chrg.cpMax -1; // 搜索到末尾 ft.lpstrText _T(需要查找的文字); LRESULT pos m_richEdit.SendMessage(EM_FINDTEXTEX, FR_DOWN, (LPARAM)ft); if (pos ! -1) { m_richEdit.SetSel(ft.chrgText.cpMin, ft.chrgText.cpMax); }循环查找时注意下一次搜索起点要设置为ft.chrgText.cpMax否则会永远找到同一个位置形成死循环。替换操作在找到后用ReplaceSel输入新文本这一步会保留目标区域的字体格式实测效果很自然。查找对话框不一定要自己画MFC的CFindReplaceDialog可以省不少事但要记得用RegisterWindowMessage(FINDMSGSTRING)注册消息来接收查找和替换通知这个坑让我当年调试了半个晚上。2.4 撤销重做与剪贴板CRichEditCtrl默认支持撤销但Redo在Rich Edit 1.0时代是没有的。这也是为什么我坚持在InitInstance里调AfxInitRichEdit2()确保加载的是Rich Edit 2.0以上版本。2.0以上才能调用Redo()否则这个按钮永远是灰色的用户一旦撤销过头就只能干瞪眼。可以用SetUndoLimit限制撤销步数默认是100步对简易编辑器够用。剪贴板操作直接调用Cut、Copy、Paste即可不需要自己操作全局剪贴板控件内部会处理格式信息RTF内容复制到Word里通常不会乱。3. 实操过程从创建工程到打包发布3.1 建立工程与初始化富文本控件以VS2013里的MFC对话框工程为例从工具箱拖一个CRichEditCtrl到对话框模板把属性里的Multiline设为TrueWant Return设为TrueVertical Scroll设为TrueAuto VScroll也打开这样控件看起来才像个正经的文本编辑区。初始化这一步最容易被忽略在InitInstance里一定要调用AfxInitRichEdit2()。如果不调用MFC加载的是老版本Rich Edit 1.0控件虽然基本编辑没问题但Redo、部分高级格式功能会缺失而且同一个程序在不同Windows上表现不一致。你可以在对话框的OnInitDialog里加一行验证代码比如判断GetRichEditOle()是否非空提前发现问题。3.2 菜单、工具栏与快捷键的联动功能都实现了界面整合也有讲究。我的做法是把“打开”“保存”“加粗”“斜体”等所有操作都用一组命令ID菜单和工具栏共用同一套ON_COMMAND映射这样响应函数不用写两遍代码少一半。比如加粗菜单项和工具栏按钮都映射到OnFormatBold函数体就是那句切换CFE_BOLD的代码。快捷键需要通过加速键表实现。对话框程序默认的消息循环会处理加速键但如果你发现CtrlB没反应优先检查两件事一是加速键表资源里确实加了对应条目二是PreTranslateMessage里把消息正确转给了IsDialogMessage。我排查过一个诡异问题CtrlB有时候有效有时候无效最后发现是焦点不在富文本控件上加速键被其他控件抢走了给控件SetFocus后问题消失。为了反映加粗、斜体等按钮的选中状态用ON_UPDATE_COMMAND_UI更新菜单和工具栏的检查状态根据当前选区的CHARFORMAT2里CFE_BOLD是否置位来勾选或取消勾选。这种联动做出来后整个编辑器的完成度立刻上一个档次。3.3 控件自适应窗体与显示刷新对话框大小一变里面的编辑区最好跟着变。处理WM_SIZE时用GetClientRect拿到新的客户区尺寸然后MoveWindow把控件铺满注意去掉菜单栏和其他固定区域的高度。如果编辑器区域不只是单个控件旁边还有查找栏就要根据比例计算每个控件的新位置纯手工布局。我建议设置一个窗口最小尺寸避免用户把窗口拖到太小导致工具栏按钮挤成一团。用OnGetMinMaxInfo限制最小宽度和高度。大文件打开时控件会有一段明显的绘制等待可以先弹一个“正在加载”的等待光标或者干脆在读取前SetRedraw(FALSE)读取完再SetRedraw(TRUE)并RedrawWindow这样能避免界面闪成一片。3.4 打包部署不装运行库怎么跑MFC程序打包是很多新人卡住的地方。如果用的是动态MFC、动态运行库目标机器没装VC运行库或MFC运行时程序会直接报“缺少mfc120u.dll”之类错误。对内部小工具最简单的做法是工程属性里选择“在静态库中使用MFC”运行库选择多线程/MT这样MFC和C/C运行库都会编进exe目标机器不需要额外安装运行库。RTF编辑器本身还依赖系统的Riched20.dll这个在Win10、Win11上都是系统自带的不用打包。打包后用依赖查看工具看一眼exe依赖确认没有指向开发机上的私有DLL就够了。实测下来静态链接的exe虽然体积大个两三兆但换到一台干净的老机器上能直接跑省去一堆“为什么我这里打不开”的沟通成本。4. 实测中踩过的坑与排查技巧4.1 打开文本文件乱码与RTF头判断有用户把纯文本文件改后缀成.rtf后直接用编辑器打开结果一堆乱码。原因是文件内容根本不是RTF控件用RTF解析器解析纯文本自然会出问题。解决思路很简单打开文件后先读前几个字节判断是否以{\rtf开头如果不是就按纯文本模式导入或者干脆提示用户另存为纯文本。这个检查花不了几行代码但能省掉大量“文件打不开”的反馈。另一个关于中文的知识点RTF文件里中文经常以\uN?控制字出现比如\u20320?这是正常的Unicode转义不要当成乱码去改。只要保存和读取都用SF_RTF控件会自动处理这些细节反而是你自己手动去修改文本会有破坏结构的风险。4.2 查找替换后界面跳转与选择异常查找后点击替换经常遇到界面不跳到目标位置或者选区不显示的问题。第一反应先检查SetSel之后有没有调用SetFocus让控件获得焦点没有焦点时控件有时不会刷新选中高亮。另一个排查点是消息发送时机EM_FINDTEXTEX用SendMessage同步发送没问题但如果用了PostMessage异步发送后续立刻读ft.chrgText会读到尚未返回的结果解决方法是改成同步发送简单直接。查找不到时最好把光标恢复成用户查找前的位置而不是留在控件末尾。可根据cpMin记录当前工作位置查找失败时重新SetSel回原位置。这个小细节决定了查找功能整个过程的体验是否顺畅。4.3 大文件与中文路径问题RTF文件超过几MB后Rich Edit控件会明显变卡插入字符都有延迟。简易编辑器不需要做虚拟化渲染这种高难度操作但至少可以在打开前判断文件大小超过设定阈值比如10MB就提示“文件过大建议拆分成小文档再编辑”避免项目被一个异常文件拖崩。中文路径问题主要出现在调用std::ifstream或fopen时用了char字符串而VS2013以后MFC工程默认是Unicode文件对话框拿到的可能是宽字符路径转来转去很容易丢字符。我后来统一用CFile或_wfopen操作文件宽字符路径直接传给APIREADME和代码注释里都写上“不要用char版文件操作函数”。这个坑不致命但触发一次就够头疼。4.4 插入图片和OLE对象的基本方案如果想把图片插进RTF最符合控件设计的方式是通过OLE对象插入。MFC里通常先用CRichEditOle获得oleClient接口再调用InsertNewObject或者InsertObject。这个流程涉及COM接口调用代码量不小而且不同版本的Office兼容性要靠实测。我的经验是如果不要求图片跟随文字排版做复杂布局只要求“能在文档里显示图片并保存到RTF”可以直接处理拖放消息WM_DROPFILES拿到图片文件路径后调用系统OLE相关接口插入。这一步确实比普通菜单项麻烦但做出来后效果很直观用户从资源管理器拖一张png进来文档里就出现了图片再保存RTF图片能正常存进去。很多内部工具对这个功能评价很高。真要做到图片实时缩放就要深入OLE的IOleClientSite体系那是单独一篇的内容了。4.5 一个小提醒先做稳再扩展在实际使用中我最大的体会是这类项目别一上来就追求花哨先把“打开老文档格式不乱、查找替换顺手、打包后干净机器能跑”这三件事做扎实80%的需求就已经满足了。用户真正关心的不是用了多新的技术而是文档打开后字体颜色对不对、保存后别的地方能不能正常打开。后续真要扩展可以加个Markdown的导入导出把RTF当中间层用既能兼容旧文档又能接上现代工作流。但那是后话当前版本稳稳跑起来比什么都重要。本文还有配套的精品资源点击获取