记一次背时的 WPF 富文本编辑器调试 作者: Semesse 时间: 2019-11-24 分类: 千叶 某天(昨天)晚上打算给心爱的编译器加上代码高亮。说起富文本编辑器啊,那真是一个无比深坑。像 WPF 这种,API 改来改去,StackOverflow 上面的大部分内容都已经过时了,只能对着 M$ 的文档和 SO 的过时内容去猜应该怎么写。 #### 1.指针问题 说起 WPF 的 RichText 也是一把辛酸泪,RichText 中的实际内容类似于 xml,像是 `This is a text` 但是WPF的指针移动并不单独计算 Char 的数量: > GetPositionAtOffset returns a TextPointer to the position indicated by the specified **offset, in symbols,** from the beginning of the current TextPointer. > Any of the following is considered to be a symbol: > > - An opening or closing tag for the TextElement element. > - A UIElement element contained in an InlineUIContainer or BlockUIContainer. Note that such a UIElement is always counted as exactly one symbol; any additional content or elements contained by the UIElement are not counted as symbols. > - A 16-bit Unicode character inside of a text Run element. 也就是说这些不可见的 Symbol 占用着 Offset,我们需要编写自己的函数来跳过这些符号。 ```C# while (i < endOffset) { if (start.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.Text) { i++; } else if (start.GetPositionAtOffset(1, LogicalDirection.Forward) == null) break; start = start.GetNextInsertionPosition(LogicalDirection.Forward); } endPointer = start; ``` 这里还有一个坑,GetNextInsetionPosition 会跳过 \r\n ,但是换行是字符,其实应该用`GetPositionAtOffset(1, LogicalDirection.Foward)`来往后遍历的。 #### 2.修改颜色 StackOverflow 上面有一些奇怪的 RichTextBox 中部分文字颜色的方法, 像是`Selection.Color=`,但是评论上说这是 WinForm 的,而且也用不了。正确方式是用`.ApplyPropertyValue(TextElement.ForegroundProperty, Brushes.Red)`。要使用自定义的颜色的话还需要用`SolidColorBrush.FromRgb()`来创建。 #### 3.触发高亮 我本来写了在 Paste 和按下空格的时候触发分析进行高亮,但是实际上只有按下空格的时候才会高亮。两个事件调用的是同一个函数,按理说应该不会出现问题。既然按 KeyDown 事件里可以正常处理那不如在 Paste 事件里触发 Keydown 事件,搜了一下改成了触发 Keydown,显然这样也没有解决问题。于是搜了一下打开调试器打上断点,发现 Paste 触发时编辑器内还没有内容,当然也不会高亮。那我们不如等粘贴完了以后再进行高亮(一股浓浓的 js 风味): ```C# Task.Delay(200).ContinueWith(_ => { Highlight(); } ); ``` 你以为这样就完了吗?Highlight 函数确实被调用了,也确实可以拿到有编辑器的内容找关键字。但是仍然没有高亮。(今天早上起来以后)继续打断点找 bug, 发现单步调试在粘贴事件触发的高亮中的`GetNextInsertionPosition`神秘消失(运行到那点下一步就回到程序了。改来改去总会在某个函数调用的时候消失(`new TextRange(start, end)`也会消失),但是在消失前的一瞬,调试器会不能调用`new TextRange(start, end)`来创建 TextRange 查看 start 和 end 之间的文本,报错为 > 调用线程无法访问此对象,因为另一个线程拥有该对象。 事多线程!AWSL! 换 js 就没有这么多屁事。但是我并不清楚内部具体有啥,文档上也没标哪个在啥线程上,只能打算放弃。我打算还是写个 Timer 每隔一段时间高亮一次(又是 js 风格),打开了谷歌搜“wpf timer”,出来的第一篇文章是[WPF中Timer与DispatcherTimer类的区别](https://www.cnblogs.com/zhchbin/archive/2012/03/06/2381693.html)。我点进去看了看,讲的是多线程。里面是这样写的 > Google搜索了一下发现:”访问 Windows 窗体控件本质上不是线程安全的。如果有两个或多个线程操作某一控件的状态,则可能会迫使该控件进入一种不一致的状态。还可能出现其他与线程相关的 bug,包括争用情况和死锁。确保以线程安全方式访问控件非常重要。“——来自MSDN http://msdn.microsoft.com/zh-cn/library/ms171728(en-us,VS.80).aspx > 哎,在群上回答说了一句:子线程无法访问界面线程的对象。被指没学过C#(的确,我学C#就是在一周之内看完一本几百页的书),这句话说的的确不够严谨,因为是有方法的,可以通过委托的方式进行访问,同时,这个界面线程的对象是指控件,如Label,TextBox之类。之前都写一个Qt程序的时候也遇到类似的问题,也是类似的情况:在子线程中试图直接更新界面上的一张图片,结果程序奔溃了,调试了很久才发现问题所在。 > 这时候我就想起来前阵子做过的一个练习,使用DispatcherTimer来实现更新。测试了一下,发现可行啊!! > 于是好奇这两者有什么不同?仔细阅读MSDN上的文档后,可以得知:DispatcherTimer在界面线程中实现的,当然可以安全地访问,修改界面内容。 > > If a System.Timers.Timer is used in a WPF application, it is worth noting that the System.Timers.Timer runs on a different thread then the user interface (UI) thread. In order to access objects on the user interface (UI) thread, it is necessary to post the operation onto the Dispatcher of the user interface (UI) thread using Invoke or BeginInvoke. Reasons for using a DispatcherTimer opposed to a System.Timers.Timer are that theDispatcherTimer runs on the same thread as the Dispatcher and a DispatcherPriority can be set on the DispatcherTimer. > > ? 呵呵,区别就是在这里了!! > 附:感谢群里高手的指点,采用Timer,使用Invoke或者BeginInvoke的方式进行UI的更新的方式(好处在于:在DispatcherTimer里面执行等待动作或者时间过长,可能会导致UI假死): 一下就懂了,改成 ```C# Task.Delay(200).ContinueWith(_ => { this.Dispatcher.Invoke(Highlight); } ); ``` 就可以安全地进行高亮。得来全不费工夫 辣鸡 WPF 标签: 多线程, 富文本编辑器, WPF