WPF文本框自定义拼写检查Adorner鼠标悬停效果实现求助
实现TextBox拼写检查Adorner的下划线鼠标悬停效果(无全局重绘)
要实现单个下划线的鼠标悬停效果且避免全局重绘,核心思路是放弃一次性绘制所有下划线的方式,改用独立的DrawingVisual实例管理每个错误单词的下划线(包括换行拆分的部分)。每个DrawingVisual是独立的视觉元素,支持单独绑定鼠标事件和重绘,完全不需要调用InvalidateVisual刷新整个Adorner。
具体实现步骤
1. 重构Adorner核心结构
- 用
VisualCollection存储所有错误单词对应的DrawingVisual - 定义数据结构关联错误单词的位置信息和对应的Visual集合
- 准备正常状态和悬停状态的画笔
2. 为每个错误单词创建独立Visual
- 针对每个错误单词(包括换行拆分的段落),生成对应的
DrawingVisual并添加到VisualCollection - 绘制下划线时,将对应的
Rect存储到Visual的Tag属性,方便后续悬停时重绘
3. 绑定鼠标交互事件
- 给每个
DrawingVisual绑定MouseEnter和MouseLeave事件 - 事件触发时,仅重绘当前单词关联的所有Visual,切换画笔样式
完整代码示例
using System.Collections.Generic; using System.Windows; using System.Windows.Documents; using System.Windows.Media; public class SpellCheckAdorner : Adorner { private readonly TextBox _textbox; private readonly VisualCollection _visuals; private readonly Pen _normalPen; private readonly Pen _hoverPen; private readonly List<MisspelledWordInfo> _misspelledWords = new List<MisspelledWordInfo>(); // 存储错误单词的位置和关联Visual private class MisspelledWordInfo { public int CharacterIndex { get; set; } public int Length { get; set; } public List<DrawingVisual> Visuals { get; set; } = new List<DrawingVisual>(); } public SpellCheckAdorner(TextBox textbox) : base(textbox) { _textbox = textbox; _visuals = new VisualCollection(this); // 初始化两种状态的画笔 _normalPen = new Pen(new SolidColorBrush(Colors.Red), 2) { EndLineCap = PenLineCap.Square, StartLineCap = PenLineCap.Square }; _hoverPen = new Pen(new SolidColorBrush(Colors.Blue), 2) { EndLineCap = PenLineCap.Square, StartLineCap = PenLineCap.Square }; // 初始化错误单词高亮(替换成你的拼写检查逻辑) UpdateMisspelledWords(); } // 更新错误单词列表及对应的高亮Visual public void UpdateMisspelledWords() { _misspelledWords.Clear(); _visuals.Clear(); // 替换为你的实际拼写检查逻辑,获取所有错误单词的(起始索引, 长度) var misspelledItems = GetMisspelledWordsFromChecker(); foreach (var (index, length) in misspelledItems) { var wordInfo = new MisspelledWordInfo { CharacterIndex = index, Length = length }; CreateWordHighlightVisuals(wordInfo); _misspelledWords.Add(wordInfo); } } // 创建单个错误单词的所有高亮Visual(处理换行) private void CreateWordHighlightVisuals(MisspelledWordInfo wordInfo) { int startIndex = wordInfo.CharacterIndex; int totalLength = wordInfo.Length; if (startIndex + totalLength > _textbox.Text.Length) return; var startRect = _textbox.GetRectFromCharacterIndex(startIndex); var endRect = _textbox.GetRectFromCharacterIndex(startIndex + totalLength - 1, true); if (startRect.Y < 0 || startRect.Bottom > _textbox.ActualHeight) return; // 处理当前行的高亮 var correctedStartRect = startRect.Bottom != endRect.Bottom ? new Rect(_textbox.Padding.Left, startRect.Y, startRect.Width, startRect.Height) : startRect; var highlightArea = Rect.Union(correctedStartRect, endRect); AddHighlightVisual(wordInfo, highlightArea); // 递归处理换行后的剩余部分 if (startRect.Bottom != endRect.Bottom) { CreateWrappedWordVisuals(wordInfo, startIndex, endRect, totalLength); } } // 创建换行后剩余部分的高亮Visual private void CreateWrappedWordVisuals(MisspelledWordInfo wordInfo, int originalIndex, Rect currentEndRect, int remainingLength) { // 计算剩余字符的起始索引(需根据实际文本布局调整,这里是简化逻辑) int charsInCurrentLine = (int)((currentEndRect.Right - _textbox.Padding.Left) / _textbox.FontSize); int newStartIndex = originalIndex + (wordInfo.Length - remainingLength) + charsInCurrentLine; int newRemainingLength = remainingLength - charsInCurrentLine; if (newStartIndex + newRemainingLength > _textbox.Text.Length) return; var newStartRect = _textbox.GetRectFromCharacterIndex(newStartIndex); var newEndRect = _textbox.GetRectFromCharacterIndex(newStartIndex + newRemainingLength - 1, true); var highlightArea = Rect.Union(newStartRect, newEndRect); AddHighlightVisual(wordInfo, highlightArea); // 继续处理后续换行 if (newStartRect.Bottom != newEndRect.Bottom) { CreateWrappedWordVisuals(wordInfo, newStartIndex, newEndRect, newRemainingLength); } } // 添加单个高亮Visual并绑定鼠标事件 private void AddHighlightVisual(MisspelledWordInfo wordInfo, Rect highlightArea) { var visual = new DrawingVisual(); // 将Rect存储到Tag,方便后续重绘 visual.Tag = highlightArea; // 绘制初始状态的下划线 DrawHighlightLine(visual, highlightArea, _normalPen); // 绑定鼠标事件 AttachMouseEvents(visual, wordInfo); _visuals.Add(visual); wordInfo.Visuals.Add(visual); } // 绘制下划线 private void DrawHighlightLine(DrawingVisual visual, Rect highlightArea, Pen pen) { using (var dc = visual.RenderOpen()) { dc.DrawLine(pen, highlightArea.BottomLeft, highlightArea.BottomRight); } } // 绑定鼠标悬停事件 private void AttachMouseEvents(DrawingVisual visual, MisspelledWordInfo wordInfo) { visual.MouseEnter += (s, e) => { // 切换当前单词所有Visual为悬停样式 foreach (var v in wordInfo.Visuals) { if (v.Tag is Rect area) { DrawHighlightLine(v, area, _hoverPen); } } }; visual.MouseLeave += (s, e) => { // 恢复当前单词所有Visual为正常样式 foreach (var v in wordInfo.Visuals) { if (v.Tag is Rect area) { DrawHighlightLine(v, area, _normalPen); } } }; } // 必须重写的Adorner视觉子元素方法 protected override int VisualChildrenCount => _visuals.Count; protected override Visual GetVisualChild(int index) { if (index < 0 || index >= _visuals.Count) throw new ArgumentOutOfRangeException(nameof(index)); return _visuals[index]; } // 示例:模拟拼写检查返回错误单词,替换为你的实际逻辑 private IEnumerable<(int Index, int Length)> GetMisspelledWordsFromChecker() { // 这里仅作示例,实际从拼写检查器获取 yield return (5, 4); yield return (12, 5); } }
关键优势
- 无全局重绘:仅在鼠标悬停/离开时重绘当前单词的关联Visual,不会影响其他下划线的渲染
- 精准交互:鼠标事件直接绑定到单个下划线Visual,不会出现误触发
- 支持换行:保留了你原有的换行处理逻辑,拆分后的多行下划线会同步响应悬停状态
内容的提问来源于stack exchange,提问作者Richard Gale
相关产品推荐
相关产品推荐

