news 2026/10/10 1:47:57

WPF富文本编辑器开发实战:从FlowDocument到仿Word开源Demo

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
WPF富文本编辑器开发实战:从FlowDocument到仿Word开源Demo

简介:这是一份基于WPF的开源富文本编辑器代码与演示项目,面向希望在.NET桌面应用中实现类Word编辑功能的开发者,尤其适合具备一定C#与XAML基础、想研究富文本处理机制的中高级人员。压缩包共289个文件,约753KB,以cs源码、gif与png界面素材、baml与xaml界面定义、dll依赖库及csproj工程文件为主,另含少量css、xml与resx资源,结构完整便于直接编译运行。项目已积累2873人学习下载,参考价值较高。功能上覆盖基础文本编辑、查找替换、查看与编辑HTML源码、打印、导出纯文本、插入图片与表格等,并配有图片、表格、超链接、颜色与字体选择等对话框实现。开发者可通过研读核心库与演示工程,理解WPF图形模型下的编辑操作、文本与图像数据处理及组件交互方式,并在此基础上扩展格式化选项或集成第三方库,快速搭建自定义的类Word编辑环境。

1. 从零拆解 WPF 富文本编辑器:为什么我要用开源 Demo 复刻 Word 的核心体验

做过桌面端文档工具的人都有一个共识:WPF 富文本编辑器这个方向,看起来简单,做起来全是坑。你打开 Visual Studio,拖一个RichTextBox上去,以为万事大吉,结果用户第一句话就是「怎么加粗按钮点了没反应」「粘贴进来的图片怎么不显示」「表格列宽拖不动」。这就是我当初决定找一个开源代码 demo 仿 word来研究的起点——不是因为它多高大上,而是因为 Word 那套交互逻辑已经被用户训练成了肌肉记忆,你做的编辑器只要偏离一点点,用户就觉得「不好用」。

这篇文章面向的是有 C# 和 WPF 基础、想做一个能真正落地的富文本编辑器的开发者。我会从FlowDocument的底层模型讲起,把工具栏绑定、命令系统、序列化存储、表格与图片处理这几块拆开,每一步都给出可复现的代码和参数说明。读完你应该能自己搭出一个支持加粗/斜体/下划线、字体字号切换、段落对齐、插入表格和图片、并能保存为自定义格式的编辑器骨架。适合谁?适合那些不想从零造轮子、但又不想被商业控件绑死的独立开发者和中小团队。

2. WPF 富文本编辑器的底层模型:FlowDocument 到底怎么组织内容

2.1 为什么 RichTextBox 不是「一个文本框」那么简单

很多人第一次用RichTextBox会懵:Text属性拿不到带格式的内容,Document属性又是个FlowDocument,里面套着Paragraph、Run、Span、Table、Figure一堆东西。这其实是 WPF 的设计哲学——内容与呈现分离。FlowDocument是内容模型,RichTextBox只是它的一个可视化宿主。你真正要操作的是FlowDocument这棵树。

理解这一点很关键。比如你想实现「加粗」,不是去改什么FontWeight属性,而是要在当前选区上应用一个EditingCommands.ToggleBold命令,让 WPF 自动把选中的Run包一层Bold,或者修改Run的FontWeight。这就是为什么直接给RichTextBox.FontWeight赋值没用——那只改了控件默认样式,不改已有内容。

FlowDocument的块级元素主要有Paragraph(段落)、Section(节)、List(列表)、Table(表格)、BlockUIContainer(嵌入 UI 元素)。行内元素有Run(纯文本)、Span(带格式的容器)、Bold、Italic、Underline、Hyperlink、InlineUIContainer(嵌入图片等)。记住这个层级:块级管布局,行内管样式。

2.2 用代码构建一个最小可用的 FlowDocument

先别急着拖控件,我们用纯代码构建一个FlowDocument,这样你能看清结构:

// 创建一个 FlowDocument,设置页面内边距和列宽 FlowDocument doc = new FlowDocument(); doc.PagePadding = new Thickness(50); // 页面内边距,模拟 Word 的页边距 doc.ColumnWidth = double.PositiveInfinity; // 不分栏,单栏铺满 doc.FontFamily = new FontFamily("微软雅黑"); // 默认字体 doc.FontSize = 14; // 默认字号 // 第一个段落:普通文本 + 加粗 + 斜体 Paragraph p1 = new Paragraph(); p1.Inlines.Add(new Run("这是一段普通文本,")); p1.Inlines.Add(new Bold(new Run("这是加粗部分"))); p1.Inlines.Add(new Run(",")); p1.Inlines.Add(new Italic(new Run("这是斜体部分"))); p1.Inlines.Add(new Run("。")); doc.Blocks.Add(p1); // 第二个段落:设置对齐和行高 Paragraph p2 = new Paragraph(); p2.TextAlignment = TextAlignment.Center; // 居中对齐 p2.LineHeight = 24; // 固定行高 24 像素 p2.Inlines.Add(new Run("这一行居中显示,行高固定。")); doc.Blocks.Add(p2); // 把文档挂到 RichTextBox 上 myRichTextBox.Document = doc;

这段代码的逻辑很直白:FlowDocument是容器,Paragraph是块,Run/Bold/Italic是行内元素。参数上注意ColumnWidth设为double.PositiveInfinity是为了让内容不自动分栏,否则窗口一宽就会变成两栏,用户会以为出了 bug。PagePadding用Thickness(50)是模拟 A4 纸的边距感,你可以按需调成 20 到 80 之间。

提示:LineHeight如果设了固定值,字体放大时可能被裁切,建议用LineStackingStrategy = LineStackingStrategy.BlockLineHeight配合。

2.3 选区操作:拿到用户选中的内容并修改

编辑器最核心的交互就是「选中一段文字,点加粗」。WPF 提供了RichTextBox.Selection属性,它是一个TextSelection对象,能告诉你选区的起点、终点和当前格式。下面是一个获取选区文本并手动加粗的示例:

private void BoldButton_Click(object sender, RoutedEventArgs e) { // 获取当前选区 TextSelection selection = myRichTextBox.Selection; // 如果选区为空,直接返回,避免误操作 if (selection.IsEmpty) return; // 获取选区纯文本,用于调试 string selectedText = selection.Text; // 应用加粗:WPF 内置命令,会自动处理 Run 的拆分与合并 EditingCommands.ToggleBold.Execute(null, myRichTextBox); // 把焦点还给 RichTextBox,否则光标会留在按钮上 myRichTextBox.Focus(); }

这里的关键是EditingCommands.ToggleBold。它比手动遍历Run再改FontWeight靠谱得多,因为 WPF 会自动处理跨段落、跨行内元素的情况。参数说明:Execute的第一个参数是命令参数,这里传null;第二个参数是命令目标,必须是RichTextBox本身。执行完一定要Focus(),否则用户连续点两次加粗,第二次焦点在按钮上,命令不会生效——这是新手最常见的翻车点。

同理,斜体用EditingCommands.ToggleItalic,下划线用EditingCommands.ToggleUnderline。对齐用EditingCommands.AlignLeft、AlignCenter、AlignRight、AlignJustify。这些命令都定义在System.Windows.Documents命名空间下,不需要自己写逻辑。

3. 工具栏与命令绑定:让按钮真正驱动编辑器

3.1 用 Command 绑定替代 Click 事件

如果你用Click事件去调EditingCommands,代码会越写越乱,而且按钮的启用/禁用状态没法自动同步。WPF 的Command机制就是干这个的。RichTextBox内置支持大部分EditingCommands,你只需要在 XAML 里把按钮的Command指向对应命令,并设置CommandTarget为RichTextBox:

<ToolBar> <Button Command="EditingCommands.ToggleBold" CommandTarget="{Binding ElementName=myRichTextBox}" Content="B" FontWeight="Bold" Width="30"/> <Button Command="EditingCommands.ToggleItalic" CommandTarget="{Binding ElementName=myRichTextBox}" Content="I" FontStyle="Italic" Width="30"/> <Button Command="EditingCommands.ToggleUnderline" CommandTarget="{Binding ElementName=myRichTextBox}" Content="U" Width="30"/> <Separator/> <Button Command="EditingCommands.AlignLeft" CommandTarget="{Binding ElementName=myRichTextBox}" Content="左对齐"/> <Button Command="EditingCommands.AlignCenter" CommandTarget="{Binding ElementName=myRichTextBox}" Content="居中"/> <Button Command="EditingCommands.AlignRight" CommandTarget="{Binding ElementName=myRichTextBox}" Content="右对齐"/> </ToolBar>

这样写的好处是:当光标在普通文本上时,加粗按钮可用;当光标已经在加粗文本上时,按钮会自动显示为按下状态(如果你用了ToggleButton)。CommandTarget必须绑定到RichTextBox,否则命令找不到目标,按钮会一直是灰的。

3.2 字体、字号下拉框的绑定与同步

字体和字号不能用命令,得用ComboBox加事件。但难点在于:用户选中不同格式的文本时,下拉框要显示当前格式。这就需要监听SelectionChanged事件,读取选区格式并更新下拉框。

private void MyRichTextBox_SelectionChanged(object sender, RoutedEventArgs e) { // 获取选区当前格式 object fontWeight = myRichTextBox.Selection.GetPropertyValue(TextElement.FontWeightProperty); object fontStyle = myRichTextBox.Selection.GetPropertyValue(TextElement.FontStyleProperty); object fontSize = myRichTextBox.Selection.GetPropertyValue(TextElement.FontSizeProperty); object fontFamily = myRichTextBox.Selection.GetPropertyValue(TextElement.FontFamilyProperty); // 如果选区跨多种格式,GetPropertyValue 会返回 DependencyProperty.UnsetValue // 此时下拉框应该显示空白或默认值,而不是乱跳 if (fontFamily != DependencyProperty.UnsetValue) FontFamilyCombo.SelectedValue = fontFamily; else FontFamilyCombo.SelectedIndex = -1; if (fontSize != DependencyProperty.UnsetValue) FontSizeCombo.Text = fontSize.ToString(); else FontSizeCombo.Text = ""; }

参数说明:GetPropertyValue返回的是object,可能是FontWeight、double、FontFamily等。如果选区跨了多种格式,返回DependencyProperty.UnsetValue,这时候千万别强行转换,否则会抛异常。我一般会判断一下,是UnsetValue就把下拉框清空。

字号下拉框的SelectionChanged事件里,用EditingCommands没有直接设字号的命令,得手动应用:

private void FontSizeCombo_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (FontSizeCombo.SelectedItem == null) return; if (myRichTextBox.Selection.IsEmpty) return; // 解析选中的字号 if (double.TryParse(FontSizeCombo.SelectedItem.ToString(), out double size)) { // 对选区应用字号 myRichTextBox.Selection.ApplyPropertyValue(TextElement.FontSizeProperty, size); myRichTextBox.Focus(); } }

ApplyPropertyValue是TextSelection的方法,能把属性应用到整个选区,包括跨段落的复杂情况。注意FontSize是double类型,别传字符串。

3.3 用 MVVM 解耦:把命令逻辑抽到 ViewModel

如果你用WPF MVVM模式,工具栏按钮不应该直接绑EditingCommands,而是绑 ViewModel 里的ICommand。但EditingCommands是静态的,怎么绑?常见做法是写一个RelayCommand,在Execute里调用EditingCommands.ToggleBold.Execute(null, richTextBox)。这样 ViewModel 需要拿到RichTextBox引用,有点破坏 MVVM 纯度,但实用。

另一种做法是用CommandBinding在RichTextBox上挂自定义命令,ViewModel 只负责业务逻辑。我一般会选第一种,因为快,而且富文本编辑器的交互逻辑本来就和 View 耦合很紧,强行解耦反而增加复杂度。

4. 序列化与存储:把 FlowDocument 存成文件再读回来

4.1 用 XamlWriter 保存为 XAML 格式

FlowDocument支持直接序列化为 XAML 字符串,这是最简单的存储方式:

// 保存:把 FlowDocument 转成 XAML 字符串 public string SaveDocumentToXaml(FlowDocument doc) { TextRange range = new TextRange(doc.ContentStart, doc.ContentEnd); using (MemoryStream ms = new MemoryStream()) { range.Save(ms, DataFormats.Xaml); ms.Position = 0; using (StreamReader reader = new StreamReader(ms)) { return reader.ReadToEnd(); } } } // 读取:从 XAML 字符串还原 FlowDocument public FlowDocument LoadDocumentFromXaml(string xaml) { FlowDocument doc = new FlowDocument(); TextRange range = new TextRange(doc.ContentStart, doc.ContentEnd); using (MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(xaml))) { range.Load(ms, DataFormats.Xaml); } return doc; }

参数说明:DataFormats.Xaml是 WPF 内置格式,保存出来的 XAML 包含所有格式信息,但不包含图片二进制数据——图片是以BitmapImage的 URI 形式存在的。如果你插入的是本地图片,保存后换台机器就打不开了。解决办法是用DataFormats.XamlPackage,它会把图片打包进一个 ZIP 容器。

4.2 用 XamlPackage 打包图片和字体

// 保存为 XamlPackage,包含图片等资源 public void SaveDocumentToPackage(FlowDocument doc, string filePath) { TextRange range = new TextRange(doc.ContentStart, doc.ContentEnd); using (FileStream fs = new FileStream(filePath, FileMode.Create)) { range.Save(fs, DataFormats.XamlPackage); } } // 从 XamlPackage 读取 public FlowDocument LoadDocumentFromPackage(string filePath) { FlowDocument doc = new FlowDocument(); TextRange range = new TextRange(doc.ContentStart, doc.ContentEnd); using (FileStream fs = new FileStream(filePath, FileMode.Open)) { range.Load(fs, DataFormats.XamlPackage); } return doc; }

XamlPackage本质上是一个 ZIP 包,里面包含Xaml文件和Resources文件夹。图片会被存成 PNG 或 JPEG,字体如果用了嵌入字体也会被打包。文件体积会比纯 XAML 大,但可移植性好。我一般默认用XamlPackage,除非用户明确要求纯文本。

4.3 导出为 RTF 和纯文本

如果你需要和 Word 互通,RTF 是更好的选择:

// 导出为 RTF public void ExportToRtf(FlowDocument doc, string filePath) { TextRange range = new TextRange(doc.ContentStart, doc.ContentEnd); using (FileStream fs = new FileStream(filePath, FileMode.Create)) { range.Save(fs, DataFormats.Rtf); } } // 导出为纯文本 public void ExportToText(FlowDocument doc, string filePath) { TextRange range = new TextRange(doc.ContentStart, doc.ContentEnd); File.WriteAllText(filePath, range.Text, Encoding.UTF8); }

DataFormats.Rtf保存出来的 RTF 文件 Word 能直接打开,格式基本保留,但复杂表格和浮动图片可能会丢。range.Text只拿纯文本,所有格式丢失,适合做「复制为纯文本」功能。

注意:TextRange.Load会清空原有内容再加载,如果你是想追加内容,得先拿到doc.ContentEnd再插入。

5. 表格、图片与撤销重做:仿 Word 体验的三个硬骨头

5.1 插入表格并让列宽可拖动

FlowDocument支持Table,但默认列宽是自动的,用户拖不动。要仿 Word 的拖拽列宽,得用TableColumn的Width属性,并配合GridSplitter或者自定义 Adorner。先看插入表格:

private void InsertTable(int rows, int columns) { Table table = new Table(); table.CellSpacing = 0; // 单元格间距为 0 table.BorderBrush = Brushes.Gray; // 边框颜色 table.BorderThickness = new Thickness(1); // 边框粗细 // 定义列 for (int i = 0; i < columns; i++) { TableColumn col = new TableColumn(); col.Width = new GridLength(100); // 固定列宽 100 像素 table.Columns.Add(col); } // 定义行和单元格 for (int r = 0; r < rows; r++) { TableRow row = new TableRow(); for (int c = 0; c < columns; c++) { TableCell cell = new TableCell(new Paragraph(new Run($"单元格 {r},{c}"))); cell.BorderBrush = Brushes.LightGray; cell.BorderThickness = new Thickness(1); cell.Padding = new Thickness(5); // 单元格内边距 row.Cells.Add(cell); } table.RowGroups.Add(new TableRowGroup()); table.RowGroups[0].Rows.Add(row); } // 把表格插入到当前光标位置 myRichTextBox.Document.Blocks.Add(table); }

参数说明:TableColumn.Width设成GridLength(100)是固定宽度,用户拖不动。要可拖动,得把Width设成GridLength.Auto或GridLength(1, GridUnitType.Star),然后在TableCell上放一个Thumb控件,监听DragDelta事件动态改列宽。这部分代码量较大,开源 Demo 里一般会封装一个TableAdorner类。

5.2 插入图片并控制显示尺寸

插入图片用InlineUIContainer包一个Image控件:

private void InsertImage(string imagePath) { BitmapImage bitmap = new BitmapImage(); bitmap.BeginInit(); bitmap.UriSource = new Uri(imagePath, UriKind.Absolute); bitmap.CacheOption = BitmapCacheOption.OnLoad; // 立即加载,避免文件被占用 bitmap.EndInit(); Image image = new Image(); image.Source = bitmap; image.Width = 300; // 默认显示宽度 image.Stretch = Stretch.Uniform; // 保持宽高比 InlineUIContainer container = new InlineUIContainer(image); // 插入到当前光标位置 myRichTextBox.CaretPosition.Paragraph.Inlines.Add(container); }

BitmapCacheOption.OnLoad很重要,否则图片文件会被锁定,用户想删除原图会失败。Stretch.Uniform保证图片不变形。宽度设 300 是经验值,太宽会撑破页面。

5.3 撤销重做:用命令栈还是内置 Undo

RichTextBox自带Undo()和Redo()方法,也支持Ctrl+Z。但如果你手动改了FlowDocument结构(比如直接Blocks.Add),内置撤销栈可能不记录。解决办法是调用myRichTextBox.BeginChange()和EndChange()把操作包起来:

myRichTextBox.BeginChange(); try { // 执行你的修改操作 myRichTextBox.Document.Blocks.Add(new Paragraph(new Run("新段落"))); } finally { myRichTextBox.EndChange(); }

这样 WPF 会把这段操作当成一个原子变更,Ctrl+Z能一次性撤销。如果你要做更复杂的撤销重做(比如跨文档操作),就得自己维护命令栈,每个命令记录反向操作。开源 Demo 里常见的是用CommandPattern实现,但代码量不小。

6. 避坑与排查:WPF 富文本编辑器最常见的 5 个翻车现场

6.1 粘贴图片不显示,或者显示成红叉

现象:用户从网页复制一张图粘贴进来,编辑器里显示一个红叉或者空白。原因:剪贴板里的图片是BitmapSource,但RichTextBox默认只处理文本和 XAML 格式,不自动处理图片。另外,如果图片来自网络 URL,WPF 可能因为跨线程或权限问题加载失败。解决:重写Paste命令,检查剪贴板里有没有DataFormats.Bitmap,有就手动插入:

private void MyRichTextBox_Paste(object sender, ExecutedRoutedEventArgs e) { if (Clipboard.ContainsImage()) { BitmapSource bitmap = Clipboard.GetImage(); Image image = new Image(); image.Source = bitmap; image.Width = 300; InlineUIContainer container = new InlineUIContainer(image); myRichTextBox.CaretPosition.Paragraph.Inlines.Add(container); e.Handled = true; // 阻止默认粘贴 } }

6.2 加粗按钮点了没反应,或者只加粗了一半

现象:选中一段文字点加粗,结果只有部分文字变粗,或者完全没反应。原因:选区跨了多个Run,而EditingCommands.ToggleBold在某些边界情况下会拆分Run但没合并。另外,如果焦点不在RichTextBox上,命令不会执行。解决:确保按钮点击后调用myRichTextBox.Focus()。如果还有问题,手动遍历选区内的Run,统一设置FontWeight:

TextPointer start = myRichTextBox.Selection.Start; TextPointer end = myRichTextBox.Selection.End; TextRange range = new TextRange(start, end); range.ApplyPropertyValue(TextElement.FontWeightProperty, FontWeights.Bold);

6.3 保存后重新打开,字体全变了

现象:保存为 XAML 再加载,发现字体从「微软雅黑」变成了「宋体」。原因:FlowDocument的FontFamily如果没有显式设置在Run上,加载时会用系统默认字体。XAML 序列化只保存显式设置的属性。解决:在创建FlowDocument时设置默认字体,并且在每个Run上也显式设置,或者加载后遍历所有Run补上字体。

6.4 表格列宽拖不动,或者拖动后错位

现象:用户想拖表格列宽,发现鼠标放上去没反应,或者拖了之后表格线对不齐。原因:TableColumn.Width设成了固定值,没有实现拖拽逻辑。或者拖拽时只改了TableColumn.Width,没同步更新TableCell的Width。解决:用Thumb控件做拖拽手柄,在DragDelta事件里同时更新TableColumn.Width和对应TableCell的Width。注意TableCell的ColumnSpan会影响计算。

6.5 撤销栈爆掉,内存飙升

现象:编辑长文档时,内存占用越来越高,最后卡死。原因:每次修改都调用了BeginChange/EndChange,但没限制撤销栈深度。WPF 默认撤销栈是无限的。解决:设置myRichTextBox.IsUndoEnabled = true后,可以通过UndoLimit属性限制步数:

myRichTextBox.UndoLimit = 100; // 最多保留 100 步

另外,大图片插入时尽量用缩略图,原图单独存储,避免FlowDocument里塞太多二进制数据。

7. 进阶技巧:用 Adorner 实现 Word 那样的浮动工具栏和格式刷

7.1 浮动工具栏:选中文字时弹出快捷菜单

Word 里选中文字会弹出一个半透明的浮动工具栏,这个用 WPF 的Adorner可以实现。核心思路是:监听SelectionChanged,当选区非空时,在选区上方显示一个Popup或AdornerLayer。

private void MyRichTextBox_SelectionChanged(object sender, RoutedEventArgs e) { if (myRichTextBox.Selection.IsEmpty) { FloatingToolbar.Visibility = Visibility.Collapsed; return; } // 获取选区起始位置的屏幕坐标 TextPointer start = myRichTextBox.Selection.Start; Rect rect = start.GetCharacterRect(LogicalDirection.Forward); Point screenPoint = myRichTextBox.PointToScreen(rect.TopLeft); // 显示浮动工具栏 FloatingToolbar.Visibility = Visibility.Visible; FloatingToolbar.HorizontalOffset = screenPoint.X; FloatingToolbar.VerticalOffset = screenPoint.Y - 40; // 向上偏移 40 像素 }

参数说明:GetCharacterRect返回的是相对于RichTextBox的坐标,PointToScreen转成屏幕坐标。Popup的HorizontalOffset和VerticalOffset是相对于窗口的,所以要用PointToScreen再转回窗口坐标,或者直接用PlacementTarget。

7.2 格式刷:复制选区格式并应用到另一段

格式刷的本质是读取源选区的格式属性,存成一个字典,再应用到目标选区:

private Dictionary<DependencyProperty, object> _formatBrush = new Dictionary<DependencyProperty, object>(); // 复制格式 private void CopyFormat() { TextSelection selection = myRichTextBox.Selection; _formatBrush.Clear(); _formatBrush[TextElement.FontFamilyProperty] = selection.GetPropertyValue(TextElement.FontFamilyProperty); _formatBrush[TextElement.FontSizeProperty] = selection.GetPropertyValue(TextElement.FontSizeProperty); _formatBrush[TextElement.FontWeightProperty] = selection.GetPropertyValue(TextElement.FontWeightProperty); _formatBrush[TextElement.FontStyleProperty] = selection.GetPropertyValue(TextElement.FontStyleProperty); _formatBrush[TextElement.ForegroundProperty] = selection.GetPropertyValue(TextElement.ForegroundProperty); } // 应用格式 private void ApplyFormat() { if (_formatBrush.Count == 0) return; TextSelection selection = myRichTextBox.Selection; foreach (var kvp in _formatBrush) { if (kvp.Value != DependencyProperty.UnsetValue) selection.ApplyPropertyValue(kvp.Key, kvp.Value); } }

这个方案能覆盖大部分行内格式,但段落格式(对齐、缩进)需要单独处理,因为它们是Paragraph的属性,不是TextElement的。

7.3 性能优化:大文档编辑不卡顿

当文档超过几万字时,RichTextBox会明显卡顿。我一般会做三件事:一是关闭实时拼写检查SpellCheck.SetIsEnabled(myRichTextBox, false);二是把FlowDocument的PageWidth设成固定值,避免频繁重排;三是用TextRange批量操作代替逐个Run修改。如果还卡,就得考虑分页加载,只渲染可视区域的内容——这就是另一个大工程了。

提示:RichTextBox的IsDocumentEnabled属性设为true可以支持超链接点击,但会带来安全风险,慎用。

7.4 一个我踩过的坑:XamlPackage 里的图片路径

用XamlPackage保存时,图片会被打包成相对路径。如果你在加载后想替换某张图片,不能直接改Image.Source,因为Source指向的是包内资源。正确做法是先把XamlPackage解压,替换图片文件,再重新打包。或者干脆不用XamlPackage,自己管理图片文件夹,FlowDocument里只存相对路径。

我现在的习惯是:编辑器内部用XamlPackage保证可移植性,导出时再转成 RTF 或 HTML。这样既方便用户保存,又方便和外部工具互通。如果你也在做类似的东西,建议先把FlowDocument的序列化跑通,再往上堆功能,否则后期改存储格式会痛不欲生。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 23:50:52

PyCharm Python应用开发实战:从环境配置到调试部署

简介&#xff1a;本资源是Packt出版的《Hands-On Application Development with PyCharm》配套代码库&#xff0c;面向Python初学者及希望提升开发效率的中阶开发者&#xff0c;聚焦PyCharm这一主流IDE的工程化实践能力培养。资源以ZIP压缩包形式提供&#xff0c;共包含百余个结…

作者头像 李华
网站建设 2026/10/8 23:50:33

WinSW 实战:将任意程序注册为 Windows 服务与避坑指南

简介&#xff1a;WinSW 是一款开源轻量级的 Windows 服务包装工具&#xff0c;主要面向开发人员与系统管理员&#xff0c;用于把 .NET、Java 或自定义可执行程序注册为 Windows 系统服务&#xff0c;从而获得开机自启、后台常驻与统一的服务管理能力。本资源包共 3 个文件&…

作者头像 李华
网站建设 2026/10/8 23:49:47

C# Winform酒店管理系统源码解析:从工程结构到开单退房实战

简介&#xff1a;面向C#课程设计与毕业设计的酒店管理系统源码项目&#xff0c;采用C# Winform与SQL Server实现&#xff0c;以酒店业务为场景&#xff0c;涵盖信息维护、数据管理等核心逻辑&#xff0c;适合Winform初学者阅读&#xff0c;也可作为管理系统大作业或毕业设计的完…

作者头像 李华
网站建设 2026/10/8 23:47:24

大规模智能体训练沙箱DSec:弹性计算架构设计与实践

去年下半年&#xff0c;我们团队把智能体训练从单机脚本时代推进到了平台化时代。说出来有点丢人&#xff0c;真正的导火索不是技术演进&#xff0c;而是一次事故&#xff1a;有人在一台共享GPU服务器上跑评测脚本时&#xff0c;误删了另一个同事正在训练的checkpoint目录&…

作者头像 李华
网站建设 2026/10/8 23:47:01

2026智能体项目必备:编排引擎如何解决流程、状态与协作难题

1. 为什么2026年的智能体项目&#xff0c;离不开编排引擎先说一个反直觉的结论&#xff1a;在2026年&#xff0c;决定一个智能体项目能不能从Demo走到生产的&#xff0c;往往不是模型本身有多强&#xff0c;而是它背后的智能体编排工具够不够稳。我去年年底接手过一个客服智能体…

作者头像 李华
网站建设 2026/10/10 1:45:03

How to Write a Linux Health Check Script (With Examples)

Are you looking to create custom health check scripts for your Linux systems? Need practical, step-by-step instructions with real-world examples? This comprehensive guide covers everything you need to know about creating effective health check scripts fo…

作者头像 李华