ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

WPF 仿 Word 富文本编辑器开源 Demo 实战解析

WPF 仿 Word 富文本编辑器开源 Demo 实战解析 简介这是一份基于WPF的开源富文本编辑器代码与Demo面向希望在.NET桌面应用中实现类Word编辑功能的开发者尤其适合具备一定C#与XAML基础、想研究富文本处理机制的中高级人员。项目实现了基本文本编辑、查看与编辑HTML源码、打印、导出纯文本、插入图片和表格等能力可作为自定义编辑器的起点进行扩展与定制。压缩包共289个文件约753KB以76个cs源码、10个xaml界面文件、15个baml资源及57个gif、54个png演示素材为主另含dll、csproj、sln等工程与依赖文件结构完整可直接编译运行。目前已有2873人学习下载。通过研读源码读者可掌握WPF图形模型下的复杂编辑操作、文本与图像数据处理及与.NET组件的交互方式快速搭建类似Word的编辑环境。1. 仿 Word 的 WPF 富文本编辑器一份能直接跑起来的开源 Demo 到底长什么样做过桌面端文档类产品的同行大概都有过这种经历产品经理一句“我们要在软件里内嵌一个像 Word 那样的编辑区”后端和前端都沉默了。Web 端还能靠 contenteditable 或者第三方富文本库糊过去到了 WPF 桌面端很多人第一反应是去 NuGet 上翻 RichTextBox 的封装翻完一圈发现要么功能残缺要么文档少得可怜要么干脆就是个半成品。这份开源 Demo 的价值就在这里——它把“仿 Word”这件事拆成了一个能编译、能运行、能改的 WPF 工程而不是一段只存在于博客里的伪代码。它本质上是一个基于 WPF 的富文本编辑器示例工程核心围绕 RichTextBox 这个控件做扩展覆盖了文字排版、字体样式、段落格式、撤销重做、文件读写这些 Word 里最基础也最常用的能力。适合两类人一类是正在做 WPF 桌面应用、需要内嵌文档编辑功能的开发者可以直接拿它当起点改另一类是刚接触 WPF、想通过一个完整案例理解数据绑定、命令、控件模板怎么协同工作的人。下面我按“它怎么搭起来 → 关键功能怎么实现 → 哪里容易翻车 → 怎么往下扩展”的顺序把这份代码拆开讲。2. 从 RichTextBox 到仿 Word 编辑区核心架构与数据绑定怎么落地WPF 里做富文本绕不开 RichTextBox。但原生 RichTextBox 只是个“能显示带格式文本的框”离 Word 那种带工具栏、状态栏、实时格式反馈的编辑区还差得远。这份 Demo 的思路是把 RichTextBox 当作渲染和输入的核心外面套一层 MVVM 结构用命令和绑定把工具栏、格式状态、文档内容串起来。理解这个分层是能不能改得动的关键。2.1 为什么选 RichTextBox 而不是自己画一个有人会想既然要仿 Word为什么不干脆用 Canvas 或者自定义控件从头画答案很简单文本排版是深水区。光标定位、选区、换行、字体回退、输入法候选框跟随这些如果自己实现工作量远超一个 Demo 的范畴。RichTextBox 底层是 FlowDocument 模型天然支持段落、Run、Span、Table、List 这些结构等于微软已经帮你把文本排版引擎写好了。你要做的是在它上面加“Word 感”。具体到这份代码它没有去动 RichTextBox 的渲染层而是把精力放在三件事上一是工具栏按钮和 RichTextBox 当前选区格式的双向同步二是把 FlowDocument 的序列化/反序列化封装成打开和保存三是用命令模式统一处理撤销重做和格式切换。这个取舍很务实——先让功能跑通再谈性能优化。2.2 MVVM 结构下 RichTextBox 的绑定难题WPF 的 RichTextBox 有个出了名的坑它的 Document 属性不是依赖属性没法直接Text{Binding Document}这样绑。很多新手在这里卡住最后退化成在代码后台直接操作控件MVVM 就名存实亡了。这份 Demo 常见的处理方式是附加属性Attached Property或者行为Behavior把 Document 的变更桥接到 ViewModel。下面是一个附加属性的简化写法用来实现 Document 的双向绑定public static class RichTextBoxHelper { // 定义附加属性 DocumentBinding public static readonly DependencyProperty DocumentBindingProperty DependencyProperty.RegisterAttached( DocumentBinding, typeof(FlowDocument), typeof(RichTextBoxHelper), new FrameworkPropertyMetadata(null, OnDocumentChanged)); public static FlowDocument GetDocumentBinding(DependencyObject obj) (FlowDocument)obj.GetValue(DocumentBindingProperty); public static void SetDocumentBinding(DependencyObject obj, FlowDocument value) obj.SetValue(DocumentBindingProperty, value); private static void OnDocumentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is RichTextBox rtb e.NewValue is FlowDocument doc) { // 把 ViewModel 里的 FlowDocument 挂到控件上 rtb.Document doc; } } }逻辑说明附加属性让 XAML 里可以写local:RichTextBoxHelper.DocumentBinding{Binding CurrentDocument}从而绕开 Document 不是依赖属性的限制。参数上FrameworkPropertyMetadata里如果需要支持从控件回写到 ViewModel还要加BindsTwoWayByDefault并在 RichTextBox 的TextChanged事件里反向更新。注意这里有个性能陷阱每次 TextChanged 都重建 FlowDocument 会导致输入卡顿正确做法是只标记脏状态保存时再序列化。2.3 工具栏与选区格式的实时同步Word 的工具栏有个细节光标放在加粗文字上加粗按钮是按下状态换到普通文字按钮弹起。这个“格式状态回显”在 WPF 里要靠SelectionChanged事件去读EditingCommands对应的属性。Demo 里一般会封装一个GetCurrentSelectionProperty方法private void RichTextBox_SelectionChanged(object sender, RoutedEventArgs e) { var rtb sender as RichTextBox; if (rtb null) return; // 读取当前选区的字重判断是否加粗 object weight rtb.Selection.GetPropertyValue(TextElement.FontWeightProperty); bool isBold weight ! DependencyProperty.UnsetValue (FontWeight)weight FontWeights.Bold; // 通知 ViewModel 更新按钮状态 ViewModel.IsBoldChecked isBold; }逻辑说明GetPropertyValue返回的是依赖属性值如果选区里格式不统一会返回DependencyProperty.UnsetValue这时候按钮应该显示为不确定态而不是简单置灰。参数上FontWeightProperty、FontStyleProperty、TextAlignmentProperty都是常用的探测目标。这里容易翻车的地方是在 SelectionChanged 里直接改 ViewModel 属性可能触发重入稳妥做法是加一个_isUpdating标志位做保护。3. 打开、保存与格式序列化FlowDocument 的 XAML 与 RTF 两条路一个编辑器能不能用很大程度上看它的文件读写靠不靠谱。这份 Demo 在文档持久化上通常会给两条路一条是 XAML 序列化一条是 RTF。选哪条取决于你要的是“自己存自己读”还是“和外部文档互通”。3.1 XAML 序列化最简单也最可控FlowDocument 本身支持XamlWriter.Save和XamlReader.Load这是最省事的方案。保存时把 Document 写成 XAML 字符串或文件打开时再读回来格式、段落、内联样式基本都能保留。// 保存把 FlowDocument 序列化为 XAML 文件 public void SaveToXaml(FlowDocument doc, string path) { using (var fs new FileStream(path, FileMode.Create)) { // 第三个参数控制是否保留缩进调试时可设 true XamlWriter.Save(doc, fs); } } // 打开从 XAML 文件还原 FlowDocument public FlowDocument LoadFromXaml(string path) { using (var fs new FileStream(path, FileMode.Open)) { return XamlReader.Load(fs) as FlowDocument; } }逻辑说明XamlWriter.Save会把 FlowDocument 的整个对象树序列化包括你自定义的附加属性前提是类型可序列化。参数上FileMode.Create会覆盖同名文件如果要保留历史版本得自己加备份逻辑。注意 XAML 序列化出来的文件体积偏大而且和 WPF 版本有一定耦合跨版本读取偶尔会报类型找不到这是它的边界。3.2 RTF 互通和 Word 交换文件的现实选择如果需求是“用户能把 Word 里写好的内容粘进来也能把编辑结果导出成 Word 能打开的格式”那 RTF 比 XAML 更合适。WPF 的 RichTextBox 原生支持 RTF 的加载和保存通过TextRange的Load和Save方法指定DataFormats.Rtf即可。// 导出为 RTFWord 可直接打开 public void SaveToRtf(RichTextBox rtb, string path) { var range new TextRange(rtb.Document.ContentStart, rtb.Document.ContentEnd); using (var fs new FileStream(path, FileMode.Create)) { // 第二个参数指定数据格式为 RTF range.Save(fs, DataFormats.Rtf); } } // 从 RTF 文件导入 public void LoadFromRtf(RichTextBox rtb, string path) { var range new TextRange(rtb.Document.ContentStart, rtb.Document.ContentEnd); using (var fs new FileStream(path, FileMode.Open)) { range.Load(fs, DataFormats.Rtf); } }逻辑说明TextRange代表文档里的一段范围ContentStart到ContentEnd就是全文。DataFormats.Rtf是 WPF 内置支持的格式不需要额外引用库。参数上RTF 对复杂排版比如嵌套表格、文本框、公式的支持有限导入后可能出现样式丢失这是格式本身的限制不是代码 bug。如果项目里涉及公式图片转 Word 这类需求RTF 这条路走不通得考虑 OpenXML SDK 直接操作 docx。3.3 两种格式怎么选对比项XAML 序列化RTF实现难度低几行代码低TextRange 原生支持格式保真度高WPF 对象完整保留中复杂排版会丢与 Word 互通不能直接打开可以Word 原生支持文件体积偏大较小适用场景应用内部存档、草稿恢复导入导出、和外部交换常见做法是内部草稿用 XAML导出给用户用 RTF 或 docx。如果非要和 Word 深度互通建议直接上 OpenXML SDK但那是另一个工程量级了。4. 避坑与排查WPF 富文本编辑器最容易翻车的五个地方这份 Demo 能跑起来不代表你改的时候不踩坑。下面这几条是我在实际项目里反复遇到的按“现象 → 原因 → 解决”整理照着排查能省不少时间。4.1 输入中文时卡顿或候选框错位现象用拼音输入法打字候选框不跟随光标或者每敲一个字界面就卡一下。 原因RichTextBox 的 TextChanged 事件里做了重活比如每次都重新序列化整个 FlowDocument或者每次都刷新工具栏所有按钮状态。 解决把重活从 TextChanged 里挪出去用 DispatcherTimer 做防抖或者只在保存和切换选区时序列化。候选框错位通常是自定义了控件模板但没保留原生的 Adorner 层检查一下有没有覆盖 RichTextBox 的默认模板。4.2 粘贴外部内容后格式全乱现象从网页或 Word 复制一段文字粘进来字体、行距、颜色全变了甚至带进来一堆隐藏样式。 原因剪贴板里的数据包含 HTML 或 RTF 格式RichTextBox 默认会尽量保留原格式。 解决拦截DataObject.Pasting事件只取纯文本或者做格式清洗。常见做法是private void RichTextBox_Pasting(object sender, DataObjectPastingEventArgs e) { // 只允许纯文本粘贴避免外部样式污染 if (e.DataObject.GetDataPresent(DataFormats.UnicodeText)) { string text e.DataObject.GetData(DataFormats.UnicodeText) as string; e.DataObject new DataObject(DataFormats.UnicodeText, text); } else { e.CancelCommand(); } }参数说明DataFormats.UnicodeText保证拿到的是纯文本e.CancelCommand()用于拒绝不支持的格式。如果产品要求保留部分格式可以改成取 RTF 后做白名单过滤。4.3 撤销重做栈被意外清空现象用户编辑到一半点了一下工具栏的某个格式按钮再按 CtrlZ 发现之前的操作全没了。 原因直接给rtb.Document赋新对象会重置撤销栈或者在代码里手动改了 FlowDocument 结构但没走EditingCommands。 解决所有格式操作尽量通过EditingCommands或者TextRange.ApplyPropertyValue来做不要整体替换 Document。如果确实需要替换先提示用户保存。4.4 大数据量文档打开慢甚至假死现象打开一个几万字的文档界面卡住十几秒。 原因FlowDocument 在 UI 线程上一次性构建加上 XAML 反序列化本身就不快。 解决分页加载或者延迟加载先把文档骨架显示出来内容分块填充。另一个思路是打开时禁用拼写检查和自动格式这两个功能在大文档上开销很大。4.5 高 DPI 下字体模糊或工具栏错位现象在 4K 屏上编辑区文字发虚工具栏图标位置偏移。 原因WPF 默认的 DPI 感知模式和应用清单里的设置不匹配。 解决在 app.manifest 里开启 PerMonitorV2 DPI 感知同时检查图片资源有没有提供多倍图。字体模糊有时候是因为用了TextOptions.TextFormattingModeDisplay改成Ideal会清晰一些但渲染开销略增。5. 进阶玩法把编辑器接进真实业务流程Demo 跑通只是起点真正要落地得把它嵌进你的业务里。这一章讲两个具体方向一是怎么把编辑内容和数据绑定打通二是怎么用命令模式扩展自定义功能。5.1 让编辑内容参与数据绑定和校验很多业务场景下编辑区的内容需要实时同步到 ViewModel参与保存、校验、联动。前面提到的附加属性只能解决 Document 挂载要做到内容变更通知还得监听 TextChanged 并做节流。// 在 ViewModel 里暴露一个可绑定的文本属性 private string _plainText; public string PlainText { get _plainText; set { _plainText value; OnPropertyChanged(); } } // 控件侧TextChanged 时提取纯文本节流后回写 private DispatcherTimer _throttleTimer; private void RichTextBox_TextChanged(object sender, TextChangedEventArgs e) { _throttleTimer?.Stop(); _throttleTimer new DispatcherTimer { Interval TimeSpan.FromMilliseconds(300) }; _throttleTimer.Tick (s, args) { _throttleTimer.Stop(); var range new TextRange(rtb.Document.ContentStart, rtb.Document.ContentEnd); ViewModel.PlainText range.Text; }; _throttleTimer.Start(); }逻辑说明用 300 毫秒的 DispatcherTimer 做防抖避免每次按键都触发绑定更新。参数上Interval 可以根据文档大小调整大文档建议放到 500 毫秒以上。注意range.Text拿到的是纯文本格式信息会丢如果需要保留格式得序列化成 XAML 或 RTF 再存。5.2 用命令模式扩展自定义按钮WPF 的RoutedCommand和CommandBinding是扩展工具栏的标准姿势。比如加一个“插入当前时间”的按钮// 定义命令 public static readonly RoutedUICommand InsertTimeCommand new RoutedUICommand(插入时间, InsertTime, typeof(MainWindow)); // 在窗口构造函数里绑定命令和执行逻辑 CommandBindings.Add(new CommandBinding(InsertTimeCommand, (s, e) { var rtb editor; // 在光标位置插入当前时间 rtb.CaretPosition.InsertTextInRun(DateTime.Now.ToString(yyyy-MM-dd HH:mm)); }));逻辑说明RoutedUICommand的好处是可以自动冒泡到父级元素方便做全局快捷键。参数上InsertTextInRun会在当前 Run 里插入文本如果光标跨段落需要先判断CaretPosition所在的段落结构。这个模式可以复制到任何自定义功能上比如插入表格、插入图片、导出 PDF。5.3 一个我自己的习惯早些年我接手一个文档编辑模块图省事直接在代码后台操作 RichTextBox结果后来要加单元测试发现逻辑全耦合在 UI 里重构花了两周。从那以后我每次拿到这类 Demo第一件事就是确认它的 MVVM 分层是不是干净——View 只做渲染和事件转发逻辑尽量往 ViewModel 和 Service 里挪。这份代码在这点上做得还算克制工具栏命令和文档操作基本都走了绑定改起来不至于牵一发动全身。如果你打算拿它做二次开发建议先把 Document 的序列化和反序列化抽成独立服务后面换存储格式或者加版本兼容会轻松很多。希望这份拆解能帮到你少走点我当年走过的弯路。本文还有配套的精品资源点击获取
返回列表