You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 13:27:03