WPF中RichTextBox MVVM绑定:从FlowDocument到可复用组件的封装实践
上周在重构一个内部工单系统的时候组里一个小伙子问我老大RichTextBox能用MVVM直接绑定吗我说你试一下不就知道了吗。结果他回来一脸懵绑不上Text属性压根不存在。这个场景我太熟悉了几乎每个从WinForms转WPF的人都会在富文本编辑这里卡一下。WPF里的RichTextBox和MVVM之间隔着一道看不见的墙它没有Text这种普通属性只有一个只读的Document属性类型还是FlowDocument。你没法直接把ViewModel里的字符串绑进去也没法把用户输入的内容自动存回ViewModel。今天这篇文章就把这道墙拆掉带你把一个可绑定的RichTextBox封装出来并且把绑定时序、光标恢复、Undo重做、超链接命令这些后面才会碰到的坑一一起底。这篇文章适合正在做WPF项目、尤其是用MVVM框架做办公类系统或上位机界面的开发者。如果你只是需要一个纯文本输入框直接上TextBox就够了别折腾但只要你需要加粗、斜体、颜色、表格、图片这类富文本能力就必须面对RichTextBox的绑定问题。我会从根因讲起逐步给出完整封装代码和实际使用姿势最后再聊几个不踩不知道的细节。1. 富文本在MVVM框架下绑不上的真正原因1.1 FlowDocument一个看起来能用、实际上很烫手的属性先看RichTextBox的基本结构。它继承自TextBoxBase按理说富文本输入框也是输入框应该提供Text属性。但WPF的设计者没有这么做他们把富文本内容全部塞进了一个叫Document的属性里类型是FlowDocument。FlowDocument不是字符串它是一个承载文档块级元素Paragraph、Table、List等的独立对象树。这种设计让RichTextBox拥有极强的排版能力但也让它在MVVM面前变成一个异类。MVVM要求ViewModel里只有数据、没有UI对象而FlowDocument天生就是UI层的东西。你要是把FlowDocument直接放进ViewModel等于把WPF的可视化对象泄漏到了业务层序列化、单元测试、跨界面复用全部变得困难。更麻烦的是Document这个依赖属性在元数据上是只读的你不能在XAML里直接写Document{Binding SomeFlowDocument}。虽然它在代码里可以set但只读依赖属性意味着即使绑定了绑定引擎也不会帮你自动建立源到目标的通道。你只能自己在C#里赋值。此外FlowDocument内部还包含大量与输入法、光标、布局相关的状态直接序列化它几乎不可能。数据库里存一段FlowDocument对象不现实。最终你会发现ViewModel里只能存字符串而且必须是某种约定好的字符串格式比如RTF、XAML或者纯文本。1.2 常见的“伪解决方案”为什么都走不通很多人的第一反应是既然不能绑定Document那我就在View的CodeBehind里写事件把TextChanged里的内容塞给ViewModel。这种做法不是不行但一旦项目里有七八个界面都要用富文本你会写着写着就想骂人。每个窗口都要复制一遍事件处理逻辑还要手动处理ViewModel为null、绑定源未初始化、重复赋值导致死循环等问题代码会越来越脏。第二种常见做法是把FlowDocument暴露给ViewModel。我见过有项目真的这么干ViewModel里定义一个FlowDocument属性然后直接绑定。表面上看能跑通但后续问题一个接一个加载文档后FlowDocument的DataContext继承异常、超链接命令绑定不到ViewModel、后台刷新时跨线程访问UI对象、单元测试时根本没法new出一个FlowDocument来断言。这些都是把UI对象带进业务层后的必然代价。第三种做法是有人尝试用附加属性。附加属性确实能解决一部分绑定问题但它最大的劣势在于你是挂在RichTextBox外部的“补丁”无法在控件初始化时可靠地挂接事件还要自己管理Loaded/Unloaded生命周期稍不注意就会内存泄漏。它适合一次性救急不适合作为团队规范长期使用。1.3 两条核心约束文本必须是字符串UI必须可复用经过上面的分析我们可以提炼出两条铁律第一ViewModel里永远只存字符串。至于是RTF、XAML还是纯文本由项目需求决定。我推荐RTF因为RTF是剪贴板标准格式和Word兼容格式文本样式、表格、字体信息都能保留而且TextRange天然支持RTF读写省去自己解析的麻烦。XAML格式虽然也能用但反序列化时容易因为缺少程序集引用而抛异常跨项目复用比较脆弱。第二字符串和FlowDocument的转换逻辑必须封装在可复用的UI组件里。要么写一个继承自RichTextBox的自定义控件要么写一个Behavior。这样才能做到“写一次到处用”而且这个封装本身可以作为团队控件库里的标准件后续所有项目都能直接引。想清楚这两条约束接下来的实现路线就很清晰了。2. 封装选型继承控件、附加属性还是Behavior2.1 三种方案对比在动手写代码之前先花两分钟做选型。常见方案有三种继承RichTextBox写自定义控件、用依赖属性扩展附加属性、用Microsoft.Xaml.Behaviors写Behavior。我按实际使用感受做个对比。方案优点缺点适用场景继承RichTextBox可重写生命周期方法、成员直观、绑定属性完整需要建立自定义控件库或引入自定义命名空间多个界面复用、团队级标准控件附加属性不用新建类XAML里直接挂事件挂接和管理麻烦、时序不稳定、易泄漏单独界面临时救急Behavior解耦好、行为可叠加需要额外引入Behaviors库、依赖内部事件较多项目已经重度使用Behaviors体系用附加属性的典型写法是在RichTextBox上挂一个EditorHelper.RtfText然后在属性回调里找RichTextBox实例并订阅事件。听起来很轻巧但问题在于如果你在属性回调里订阅事件那么当RichTextBox被卸载、重新加载甚至GC回收时你必须保证事件被正确退订。实际开发中经常出现RichTextBox被销毁后回调还持有旧实例引用的情况。调试这种问题非常浪费时间。Behavior的思路和附加属性类似等于把订阅逻辑包了一层但引入第三方库对一个小功能来说有点重。如果团队里本来就在用Microsoft.Xaml.Behaviors.Wpf那用它也算顺手如果没在用我建议别为这一个功能引入新依赖。2.2 继承RichTextBox的理由可重写、可复用、可测试我最后选了继承RichTextBox。原因很简单我要的不只是一个能绑定的文本控件而是一个能长期维护、能继续扩展的富文本编辑器基类。继承之后你可以在控件的构造函数里订阅TextChanged事件也可以在子类里重写OnTextChanged虚方法。这个虚方法比外部订阅事件更可靠因为它是控件生命周期的一部分不会因为事件漏掉而失效。另外你还可以把光标位置记录、滚动偏移恢复、纯文本提取这些逻辑都做成控件的内部方法调用方完全不需要感知。从测试角度讲自定义控件的依赖属性可以直接在单元测试里赋值并断言比附加属性容易测得多。你可以构造一个BindableRichTextBox实例设置RtfText再读取RtfText不需要跑完整的WPF界面测试。这一点在写业务逻辑多、迭代频繁的项目里很省心。最后还有一个现实因素等这个控件在项目里用久了你很可能还会想给它加图片粘贴支持、表格编辑能力、字数统计、限制最大长度等特性。有了自定义控件这个壳这些扩展都能顺理成章地体现在属性或事件上而不会散落在各个窗口的CodeBehind里。2.3 依赖属性与普通属性的边界绑定能力的分水岭选定了继承方案后又面临一个关键决策暴露给绑定层的属性到底是普通CLR属性还是依赖属性。如果你只写一个普通属性RtfTextsetter里处理字符串到FlowDocument的转换getter里做反向转换这样在CodeBehind里用是没问题的。但它没法作为绑定的目标。WPF绑定引擎只认依赖属性只有依赖属性才能被Binding表达式赋值、才能自动监听源变化并刷新UI。所以必须在自定义控件里注册一个真正的DependencyProperty。这一步不能省也不能用DependencyObject.SetValue变通替代。依赖属性载体是控件具备MVVM接入能力的分水岭注册了依赖属性你的控件才算是MVVM里一个合格的目标绑定对象。有了这个认识我们可以开始写核心代码了。3. 完整实现一个可绑定的BindableRichTextBox3.1 数据格式约定RTF字符串与编码先把格式确认下来。我选择用RTF字符串作为ViewModel和控件之间的交换格式原因前面说过兼容性好、TextRange原生支持、剪贴板也是这个格式。关于编码这里有一个容易翻车的点。RTF文件本身就是一条ASCII字符流所有非ASCII字符比如中文在RTF内部会被转义成\uN形式从技术上说用Encoding.UTF8还是Encoding.Default读出来都是同样的字符。但为了稳妥我建议整个项目统一使用UTF8。不要今天用UTF8、明天用一个繁体系统默认编码否则会出现那种“换一台机器RTF就乱了”的灵异问题。封装类名定为BindableRichTextBox命名空间按你的项目习惯来。控件暴露的主属性叫RtfText类型string默认值string.Empty绑定模式默认TwoWay。这个属性承载的语义是外部数据源里的富文本内容。3.2 注册RtfText依赖属性先看依赖属性注册代码。这一步相当于给控件开了一扇门让绑定引擎可以往里走。using System; using System.IO; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Media; namespace YourNamespace.Controls { public class BindableRichTextBox : RichTextBox { private bool _isSyncing; public static readonly DependencyProperty RtfTextProperty DependencyProperty.Register( nameof(RtfText), typeof(string), typeof(BindableRichTextBox), new FrameworkPropertyMetadata( string.Empty, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnRtfTextChanged)); public string RtfText { get (string)GetValue(RtfTextProperty); set SetValue(RtfTextProperty, value); } private static void OnRtfTextChanged( DependencyObject d, DependencyPropertyChangedEventArgs e) { var editor (BindableRichTextBox)d; editor.OnRtfTextChanged((string)e.NewValue); } private void OnRtfTextChanged(string newValue) { if (_isSyncing) return; var current GetRtfFromDocument(); if (string.Equals(current, newValue ?? string.Empty, StringComparison.Ordinal)) return; _isSyncing true; try { SetDocumentFromRtf(newValue); } finally { _isSyncing false; } } } }BindsTwoWayByDefault这个选项非常重要它表示绑定时默认就是双向的外部使用者在XAML里写{Binding ContentRtf}就能双向同步不用每次多写ModeTwoWay。注册时我故意没有设置AffectsRender等布局选项因为内容变化会影响Document的布局但富文本本身的渲染是由Document内部控制的外部属性元数据里的AffectsRender语义并不准确。保持默认就好。3.3 从RTF到FlowDocument加载路径接下来写字符串到FlowDocument的转换。这是整个封装的核心之一需要考虑空值、异常、光标恢复等问题。private void SetDocumentFromRtf(string rtfText) { var caretOffset Document?.ContentStart.GetOffsetToPosition(CaretPosition) ?? 0; var scrollViewer FindVisualChildScrollViewer(this); var horizontalOffset scrollViewer?.HorizontalOffset ?? 0; var verticalOffset scrollViewer?.VerticalOffset ?? 0; var doc new FlowDocument(); if (string.IsNullOrEmpty(rtfText)) { doc.Blocks.Add(new Paragraph()); } else { try { var range new TextRange(doc.ContentStart, doc.ContentEnd); using (var stream new MemoryStream(Encoding.UTF8.GetBytes(rtfText))) { range.Load(stream, DataFormats.Rtf); } } catch (Exception ex) { // 记录日志后回退到空白文档避免让控件进入不可用状态 doc new FlowDocument(); doc.Blocks.Add(new Paragraph()); } } Document doc; // 尝试恢复光标位置 try { var target Document.ContentStart.GetPositionAtOffset(caretOffset); if (target ! null) { CaretPosition target; } } catch { // 位置无效就不恢复保持默认 } scrollViewer?.ScrollToHorizontalOffset(horizontalOffset); scrollViewer?.ScrollToVerticalOffset(verticalOffset); }空文档为什么要加一个空Paragraph因为完全空的FlowDocument会让RichTextBox在某些环境下无法正常接收键盘输入而且显示区域会出现奇怪的占位行为。加一个空段落是最稳妥的做法。这里我还做了两件你可能没注意到的事记录并恢复光标偏移记录并恢复滚动偏移。为什么要这么做因为每次重新赋值DocumentRichTextBox的光标都会跳到文档开头滚动条回到顶部。只要你从后端加载一段新内容用户正在阅读的位置就丢了。这个体验在办公系统里非常糟糕尤其是长文档场景。恢复操作能有效缓解这个问题。FindVisualChild是一个搜索可视化树的辅助方法代码如下private static T FindVisualChildT(DependencyObject parent) where T : DependencyObject { for (var i 0; i VisualTreeHelper.GetChildrenCount(parent); i) { var child VisualTreeHelper.GetChild(parent, i); if (child is T typed) return typed; var result FindVisualChildT(child); if (result ! null) return result; } return null; }3.4 从FlowDocument到RTF保存路径与防抖循环反向路径同样关键。当用户输入内容、调整样式、插入表格时RichTextBox内部会触发TextChanged我们要在这个事件里把当前文档的RTF字符串同步到RtfText属性再靠绑定引擎写回ViewModel。重写OnTextChanged是最干净的方式。protected override void OnTextChanged(TextChangedEventArgs e) { base.OnTextChanged(e); if (_isSyncing) return; var current GetRtfFromDocument(); if (string.Equals(current, RtfText ?? string.Empty, StringComparison.Ordinal)) return; _isSyncing true; try { RtfText current; } finally { _isSyncing false; } } private string GetRtfFromDocument() { if (Document null) return string.Empty; var range new TextRange(Document.ContentStart, Document.ContentEnd); using (var stream new MemoryStream()) { range.Save(stream, DataFormats.Rtf); return Encoding.UTF8.GetString(stream.ToArray()); } }_isSyncing是这个封装里最重要的一个标志位。它要解决的核心问题是死循环你在OnRtfTextChanged里给Document赋了新值赋值的动作会触发OnTextChanged如果不拦一下事件处理器又会把内容写回RtfText然后又触发OnRtfTextChanged于是无限循环。_isSyncing的作用就是在整个“程序赋值Document”的流程里竖起一块牌子这里正在同步别管我。还有一个比较隐蔽的细节我在OnTextChanged里先比较了一下当前RTF和目标RTF是否相等。相等直接返回不赋值。为什么要做这一步因为当ViewModel第一次把初始值推给控件时控件的默认Document是一个空段落空段落导出的RTF和一个空字符串在视觉上等价但RTF原文其实并不完全相等。如果每次都无脑赋值那么每打开一次窗口控件都会重建一次Document光标会从第一个字符跳到末尾视觉上表现为“闪一下”。做了字符串比较之后只有内容真正不同时才重建文档大部分情况下能避免无意义的闪烁。这段逻辑走通之后绑定链路就建立起来了。3.5 XAML使用方式与初始值处理现在看控件在XAML里的使用姿势Window x:ClassYourNamespace.MainWindow xmlnshttp://schemas.microsoft.com/winfx/2006/xaml/presentation xmlns:cclr-namespace:YourNamespace.Controls Grid c:BindableRichTextBox RtfText{Binding ContentRtf} MinHeight220 Margin8 VerticalScrollBarVisibilityAuto HorizontalScrollBarVisibilityAuto AcceptsReturnTrue SpellCheck.IsEnabledTrue / /Grid /WindowViewModel一侧只需要一个普通属性加上INotifyPropertyChanged实现public class DocumentViewModel : ObservableObject { private string _contentRtf; public string ContentRtf { get _contentRtf; set { _contentRtf value; OnPropertyChanged(); } } }注意一点如果源属性初始值是null绑定引擎会把null传给DP回调所以OnRtfTextChanged内部对newValue做了null处理统一转成string.Empty避免空引用异常。如果你的ViewModel属性初始值就是string.Empty那更省心。绑定时机上WPF的绑定引擎会在InitializeComponent执行后、窗口显示之前把源值推给目标属性。所以第一次打开窗口时RtfText就已经有值OnRtfTextChanged会被调用Document会被正确加载。只要不在构造函数里绕开绑定引擎手动给RtfText赋值时序都是安全的。4. 绑定之外的三个硬骨头重做、超链接与光标恢复4.1 输入状态与Undo栈替换文档的代价把绑定跑通只是第一步。在实际项目里挣扎一段时间后你会碰到三个更加顽固的问题它们不像绑定本身那样容易定位。第一个是Undo/Redo。RichTextBox自带Undo机制用户按CtrlZ可以回退输入操作这个机制依赖内部的Undo栈。问题在于一旦你用代码给Document赋了一个全新的FlowDocument对象Undo栈就会被清空。说得直白点只要程序在用户输入过程中频繁地重建文档用户会发现CtrlZ完全失效或者只能撤销很有限的几步。所以使用这个控件有个铁律不要把实时输入的每一次变化都推送到后端保存接口更不要在后端返回之后立刻把整个RTF再赋回来。理想的做法是用户输入期间只做单向的UI到VM同步VM层面的内容更新不要反过来触发DP回调除非是真正的外部数据加载比如打开新文档、切换当前编辑对象。如果确实需要响应外部数据加载比较好的做法是加载前判断当前RTF和即将加载的RTF是否相同相同就不重建。这也是我在OnRtfTextChanged里保留字符串比较的原因之一。不要小看这一行比较逻辑它能帮你避开一半以上的“Undo突然失灵”类bug。4.2 超链接命令绑定FlowDocument在逻辑树上的边界第二个硬骨头是超链接。很多时候富文本内容里会有Hyperlink点击之后要执行ViewModel里的命令。在XAML里给Hyperlink绑定命令本身很容易问题出在动态加载的场景如果RTF是用TextRange从流里加载的里面的超链接即使带着命令绑定信息在反序列化之后也不会自动建立ViewModel命令的绑定关系。更常见的情况是你从数据库读出一段RTF里面只有超链接的文本和地址没有命令参数。这种链接在运行时显示是正常的点击却没有任何反应。经验做法是在文档加载完成后遍历FlowDocument的内容找到所有Hyperlink手动设置它的Command或NavigateUri。遍历方式如下private void BindHyperlinks(FlowDocument document) { var navigator document.ContentStart; while (navigator ! null navigator.CompareTo(document.ContentEnd) 0) { if (navigator.GetAdjacentElement(LogicalDirection.Forward) is Hyperlink link) { if (link.Command null link.NavigateUri null) { link.NavigateUri new Uri(https://example.com); link.RequestNavigate (s, e) { // 在这里交给外部命令或直接打开浏览器 }; } } navigator navigator.GetNextContextPosition(LogicalDirection.Forward); } }这里要说明一个底层背景FlowDocument虽然是逻辑树的一部分但它和可视化树是分离的它的内联元素在DataContext继承上有时候表现得很飘忽。你看着某个Hyperlink好像应该能拿到窗口的DataContext实际跑起来可能绑定不到。所以不要指望靠绑定继承自动解决在控件内部显式处理是最可靠的。4.3 外部刷新不丢光标位置偏移记录与恢复第三个硬骨头是外部刷新时的光标和滚动位置恢复。我在3.3里已经给了恢复代码这里把原理再展开讲一下。FlowDocument的TextPointer是一个很敏感的对象它和具体的FlowDocument实例强关联。一旦你把Document赋成一个新对象旧的TextPointer就失效了直接保存旧指针再赋回去是行不通的。正确做法是先记录偏移量也就是当前光标位置相对于文档开头的字符偏移等新文档加载完再用Document.ContentStart.GetPositionAtOffset(offset)根据偏移量反查新指针。偏移量获取代码是var caretOffset Document.ContentStart.GetOffsetToPosition(CaretPosition);这个偏移量在某些情况下可能超出新文档长度所以GetPositionAtOffset返回null时要做空判断回退到文档开头。滚动位置恢复同理用ScrollViewer的偏移量记录与还原即可。这套逻辑做完之后你会发现一个很实际的效果用户正在编辑一篇长文后端自动刷新了富文本内容只要新旧内容相同用户的光标和滚动位置完全不会动即使内容不同光标也会落在相对接近的位置而不是粗暴地跳回顶部。用户体验提升非常明显。5. 编码过程中的高频故障与性能实操5.1 时序问题Dispatcher与跨线程更新WPF的控件只能在UI线程操作。这个大家都知道但在实际项目里很多人还是会在后台线程加载数据后直接去改ViewModel里的属性心想“反正是绑定的绑定引擎应该会处理吧”。绑定引擎确实会把属性变化从任意线程推送到UI线程但有一个前提你的ViewModel属性必须通过INotifyPropertyChanged通知触发。如果你在后台线程里设置ContentRtf通知事件的订阅方拿到事件时WPF绑定引擎会负责把更新封送到UI线程。这里的关键是最终执行DP回调的时机是UI线程所以OnRtfTextChanged里的Document操作是安全的。但如果你绕过绑定直接在后台线程调用BindableRichTextBox.SetCurrentValue(RtfTextProperty, value)就会触发跨线程异常。所以使用这个控件时应该养成一个好习惯ViewModel的更新一律走属性绑定不要在代码里直接操作控件实例。如果一定要在后台线程加载内容就把加载结果赋给VM属性剩下的交给绑定引擎。如果遇到“后台加载完了界面上没变化”的怪问题先检查ContentRtf的setter是否真的调用了OnPropertyChanged再检查后台线程是否把结果赋给了同一个VM实例。很多时候不是绑定的问题是VM实例换成新的了界面还绑在旧实例上。5.2 性能优化从防抖到增量同步富文本编辑器有一个和纯文本框完全不同的性能模型每敲一个字符整个FlowDocument的RTF序列化都要重新算一遍。文档短的时候无所谓几十KB的RTF文档每次击键都序列化一遍明显会卡。我在实际项目里采用过一种简单有效的优化策略用DispatcherTimer做输入防抖。具体逻辑是用户输入时OnTextChanged立刻触发但不马上把RTF同步到VM而是启动一个300毫秒的计时器如果300毫秒内用户又输入了内容就重置计时器直到用户停顿300毫秒以上才把最终的RTF同步出去。这种方案的好处是显著减少序列化次数坏处是VM里的内容和用户实际输入内容之间有最多300毫秒的延迟。对于大多数业务系统来说这个延迟完全可以接受。如果你需要实时校验字数也可以在校验逻辑里面等防抖结束再做思路一样。另外还有一个细节不要试图在OnTextChanged里一次性做大段文字替换或复杂格式修改。WPF的TextRange操作本身是重量级的频繁调用会让输入队列塞满重活。把复杂的文档操作抽成后台逻辑通过Dispatcher按优先级调度会流畅很多。5.3 常见异常与排查对照表把我在实际调试中遇到的几个高频问题整理成一张表供大家排查时参考。现象原因处理方式绑定后界面不显示内容VM属性没有实现INotifyPropertyChanged或绑定源为nullVM实现INPC并将初始值设置为string.Empty输入一个字光标跳回开头每次OnTextChanged都把Document整个重建开启_syncLock并在替换前比较RTF是否一致CtrlZ只能撤销一次输入过程中频繁重建Document导致Undo栈被清空用防抖方案减少同步频率外部刷新仅在内容不同时重建后台线程加载RTF抛跨线程异常绕开绑定直接操作控件实例改成通过VM属性绑定或使用Dispatcher.InvokeAsync加载非法RTF导致程序崩溃数据库里存了截断的或非UTF8编码的RTF在SetDocumentFromRtf里try-catch失败回退空白文档超链接点击没反应动态加载的Hyperlink没有命令绑定遍历FlowDocument手动绑定命令或NavigateUri中文内容变成乱码RTF读写时编码不一致统一使用Encoding.UTF8这张表基本涵盖了从接入到上线期间90%的突发状况。剩下10%大概率跟具体业务逻辑有关排查时记住一个原则先确认绑定链路通不通再看Document有没有被意外重建最后检查是不是跨线程调用。按照这个顺序排查绝大多数问题都能快速定位。5.4 一个小提醒新建项目模板缺失补充一个和本文主题不完全相关、但确实会拦住新人的环境问题。如果你装了VS2022之后发现“新建项目”里找不到WPF应用模板多半是因为安装Visual Studio时没有勾选“.NET 桌面开发”工作负载。打开Visual Studio Installer修改安装勾上这个工作负载再重启就能看到模板了。这个问题和我做封装没有直接关系但它确实会拦住刚准备上手练习的开发者。如果你在准备照着本文实践顺手检查一下环境省得卡在第一步。最后再分享一个实际操作中的体会。封装控件这件事很多人觉得把依赖属性写了一跑通了就算完事其实真正的工程量在边界条件上空文档、非法RTF、跨线程更新、Undo栈清空、光标恢复、超链接绑定这些才是决定一个控件能不能长期用的关键。我写的这套BindableRichTextBox不算复杂代码量也不大但它把这些边界条件都处理了一遍。你现在可以直接把代码搬到项目里用也可以按自己的业务再做裁剪。有一点要记住_isSyncing标志位和字符串比较这两行逻辑千万别删删了以后光标跳动和死循环会一起找上门来。

相关新闻

基于深度学习的海上捕鱼方式识别:围网、刺网、拖网三分类实战

基于深度学习的海上捕鱼方式识别:围网、刺网、拖网三分类实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/5 6:03:04 阅读更多 →
扩散模型如何落地工业缺陷检测:从原理到工程优化的完整实践

扩散模型如何落地工业缺陷检测:从原理到工程优化的完整实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/5 6:03:04 阅读更多 →
计算机视觉论文投稿实战:从Applied Intelligence被拒到The Visual Computer录用

计算机视觉论文投稿实战:从Applied Intelligence被拒到The Visual Computer录用

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/5 6:03:04 阅读更多 →

最新新闻

Aperant GitHub Handlers 模块架构:Electron 主进程 GitHub 集成的模块化改造实战指南

Aperant GitHub Handlers 模块架构:Electron 主进程 GitHub 集成的模块化改造实战指南

人工智能AI Agent自主智能体代码智能体桌面应用前端开发工具 【免费下载链接】Aperant Autonomous multi-session AI coding 项目地址: https://gitcode.com/gh_mirrors/au/Aperant 点击查看 免费下载 Aperant 桌面端(Electron 应用)通过 ap…

2026/10/5 6:37:21 阅读更多 →
cppcheck 的 mallocOnClassError/mallocOnClassWarning 检查:用 malloc() 分配 C++ 类实例的未定义行为诊断

cppcheck 的 mallocOnClassError/mallocOnClassWarning 检查:用 malloc() 分配 C++ 类实例的未定义行为诊断

开发工具静态分析代码质量质量保障 【免费下载链接】cppcheck static analysis of C/C code 项目地址: https://gitcode.com/gh_mirrors/cpp/cppcheck 点击查看 免费下载 导读 本文基于 cppcheck 仓库中的官方检查器文档 man/checkers/mallocOnClassError.md&…

2026/10/5 6:37:21 阅读更多 →
Talebook 元数据体系深度解析:字段规格、别名聚合、搜索与互联网同步

Talebook 元数据体系深度解析:字段规格、别名聚合、搜索与互联网同步

后端前端CMS 【免费下载链接】talebook 一个简单好用的个人书库 项目地址: https://gitcode.com/gh_mirrors/ta/talebook 点击查看 免费下载 Talebook(一个简单好用的个人书库)把「一本书是什么」的全部描述信息统称为元数据,并围…

2026/10/5 6:37:21 阅读更多 →
cppcheck redundantCopyLocalConst 检查器详解:消除 const 局部变量的无谓拷贝

cppcheck redundantCopyLocalConst 检查器详解:消除 const 局部变量的无谓拷贝

开发工具静态分析代码质量质量保障 【免费下载链接】cppcheck static analysis of C/C code 项目地址: https://gitcode.com/gh_mirrors/cpp/cppcheck 点击查看 免费下载 导读 redundantCopyLocalConst 是 cppcheck 静态分析器中一项面向 C 代码的性能优化检查&am…

2026/10/5 6:37:21 阅读更多 →
QGroundControl 单元测试指南:从编译开关到命令行全量/单测运行

QGroundControl 单元测试指南:从编译开关到命令行全量/单测运行

无人机智能硬件 【免费下载链接】qgroundcontrol Cross-platform ground control station for drones (Android, iOS, Mac OS, Linux, Windows) 项目地址: https://gitcode.com/gh_mirrors/qg/qgroundcontrol 点击查看 免费下载 QGroundControl(QGC&…

2026/10/5 6:37:21 阅读更多 →
OpenStack生产级功能验证清单与实操指南

OpenStack生产级功能验证清单与实操指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/5 6:36:21 阅读更多 →

日新闻

马斯克杀回智能体战场,Grok 4.5万亿参数撑腰,Cursor接手数字白领项目:用TaoToken统一Key跑通多模型Agent工作流

马斯克杀回智能体战场,Grok 4.5万亿参数撑腰,Cursor接手数字白领项目:用TaoToken统一Key跑通多模型Agent工作流

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/5 0:00:22 阅读更多 →
AI编程工具插件机制详解:plugin.json配置与加载失败排查指南

AI编程工具插件机制详解:plugin.json配置与加载失败排查指南

1. 从“plugins”这个词说起:它到底在解决什么问题如果你最近在折腾 AI 编程工具,尤其是 Cursor、Codex CLI、Claude Code 这类带 CLI 的编辑器或命令行助手,那你大概率绕不开一个词——plugins。这个词本身不新鲜,从浏览器到 IDE…

2026/10/5 0:00:23 阅读更多 →
第26课:OpenClaw|日志审计与问题诊断:把日志链路改到 TaoToken 的排查清单

第26课:OpenClaw|日志审计与问题诊断:把日志链路改到 TaoToken 的排查清单

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/5 0:00:23 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/5 5:06:42 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/5 1:10:22 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/5 3:06:17 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 11:40:45 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 9:43:54 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 20:14:29 阅读更多 →