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

如何为双向绑定的TextBox追加不可编辑的幽灵文本?

解决TextBox追加不可编辑幽灵文本的方案

针对你需要在双向绑定的可编辑TextBox末尾追加不可编辑幽灵文本的需求,这里提供两种可行的WPF实现方案:

方案一:使用Adorner绘制幽灵文本

这种方案通过Adorner在TextBox文本上方绘制幽灵文本,既不干扰用户输入的双向绑定,也无需手动计算文本位置,同时能严格限制用户无法编辑幽灵区域。

实现步骤

  1. 创建幽灵文本装饰器类:
public class GhostTextAdorner : Adorner
{
    private readonly TextBox _textBox;
    public static readonly DependencyProperty GhostTextProperty =
        DependencyProperty.RegisterAttached("GhostText", typeof(string), typeof(GhostTextAdorner),
            new PropertyMetadata(string.Empty, OnGhostTextChanged));

    public static string GetGhostText(DependencyObject obj) => (string)obj.GetValue(GhostTextProperty);
    public static void SetGhostText(DependencyObject obj, string value) => obj.SetValue(GhostTextProperty, value);

    private static void OnGhostTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is TextBox textBox)
        {
            var adornerLayer = AdornerLayer.GetAdornerLayer(textBox);
            var existingAdorner = adornerLayer?.GetAdorners(textBox)?.OfType<GhostTextAdorner>().FirstOrDefault();
            if (existingAdorner != null)
            {
                adornerLayer.Remove(existingAdorner);
            }
            if (!string.IsNullOrEmpty(e.NewValue as string))
            {
                adornerLayer?.Add(new GhostTextAdorner(textBox));
            }
        }
    }

    public GhostTextAdorner(TextBox textBox) : base(textBox)
    {
        _textBox = textBox;
        _textBox.TextChanged += OnTextBoxTextChanged;
        _textBox.SelectionChanged += OnTextBoxSelectionChanged;
        _textBox.PreviewKeyDown += OnTextBoxPreviewKeyDown;
        IsHitTestVisible = false; // 确保点击穿透到TextBox
    }

    private void OnTextBoxTextChanged(object sender, TextChangedEventArgs e)
    {
        InvalidateVisual();
    }

    private void OnTextBoxSelectionChanged(object sender, RoutedEventArgs e)
    {
        // 限制选区不能包含幽灵文本区域
        var ghostText = GetGhostText(_textBox);
        if (!string.IsNullOrEmpty(ghostText) && _textBox.SelectionStart > _textBox.Text.Length)
        {
            _textBox.SelectionStart = _textBox.Text.Length;
            _textBox.SelectionLength = 0;
        }
    }

    private void OnTextBoxPreviewKeyDown(object sender, KeyEventArgs e)
    {
        var ghostText = GetGhostText(_textBox);
        if (!string.IsNullOrEmpty(ghostText))
        {
            // 阻止光标移动到幽灵文本区域,或删除幽灵文本
            if (_textBox.CaretIndex > _textBox.Text.Length)
            {
                _textBox.CaretIndex = _textBox.Text.Length;
                e.Handled = true;
            }
            // 处理退格键,防止删除幽灵文本(如果光标在末尾)
            if (e.Key == Key.Back && _textBox.CaretIndex == _textBox.Text.Length)
            {
                e.Handled = true;
            }
        }
    }

    protected override void OnRender(DrawingContext drawingContext)
    {
        base.OnRender(drawingContext);
        var ghostText = GetGhostText(_textBox);
        if (string.IsNullOrEmpty(ghostText) || string.IsNullOrEmpty(_textBox.Text))
            return;

        // 获取TextBox文本末尾的位置
        var formattedText = new FormattedText(_textBox.Text, CultureInfo.CurrentCulture,
            _textBox.FlowDirection, _textBox.FontFamily.GetTypefaces().FirstOrDefault(),
            _textBox.FontSize, _textBox.Foreground);
        var textEndPoint = new Point(formattedText.Width, 0);

        // 绘制幽灵文本
        var ghostFormattedText = new FormattedText(ghostText, CultureInfo.CurrentCulture,
            _textBox.FlowDirection, _textBox.FontFamily.GetTypefaces().FirstOrDefault(),
            _textBox.FontSize, Brushes.Gray);
        drawingContext.DrawText(ghostFormattedText, textEndPoint);
    }
}
  1. 在XAML中使用:
<TextBox Text="{Binding UserInput, Mode=TwoWay}"
         local:GhostTextAdorner.GhostText="/new-directory" />

方案二:自定义TextBox分离用户输入与幽灵文本

这种方案通过自定义TextBox控件,将用户输入文本和幽灵文本分离显示,实际绑定的仅为用户输入部分,从根源上阻止对幽灵文本的编辑。

实现步骤

  1. 创建自定义TextBox:
public class GhostTextBox : TextBox
{
    public static readonly DependencyProperty GhostTextProperty =
        DependencyProperty.Register("GhostText", typeof(string), typeof(GhostTextBox),
            new PropertyMetadata(string.Empty, OnGhostTextChanged));

    public string GhostText
    {
        get => (string)GetValue(GhostTextProperty);
        set => SetValue(GhostTextProperty, value);
    }

    private string _actualText;

    protected override void OnTextChanged(TextChangedEventArgs e)
    {
        // 分离用户输入和幽灵文本
        var ghostText = GhostText;
        if (!string.IsNullOrEmpty(ghostText) && Text.EndsWith(ghostText))
        {
            _actualText = Text.Substring(0, Text.Length - ghostText.Length);
            // 保持显示文本为用户输入+幽灵文本,但实际绑定的是_actualText
            SetCurrentValue(TextProperty, _actualText + ghostText);
            CaretIndex = _actualText.Length;
        }
        else
        {
            _actualText = Text;
        }
        // 更新绑定的源属性
        var binding = GetBindingExpression(TextProperty);
        binding?.UpdateSource();
        base.OnTextChanged(e);
    }

    protected override void OnSelectionChanged(RoutedEventArgs e)
    {
        var ghostText = GhostText;
        if (!string.IsNullOrEmpty(ghostText) && SelectionStart > _actualText.Length)
        {
            SelectionStart = _actualText.Length;
            SelectionLength = 0;
        }
        base.OnSelectionChanged(e);
    }

    protected override void OnPreviewTextInput(TextCompositionEventArgs e)
    {
        var ghostText = GhostText;
        if (!string.IsNullOrEmpty(ghostText) && CaretIndex > _actualText.Length)
        {
            e.Handled = true;
        }
        base.OnPreviewTextInput(e);
    }

    private static void OnGhostTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var textBox = (GhostTextBox)d;
        textBox.SetCurrentValue(TextProperty, textBox._actualText + e.NewValue);
    }

    // 重写Text属性的绑定,确保源更新的是_actualText
    protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
    {
        if (e.Property == TextProperty && !string.IsNullOrEmpty(GhostText))
        {
            if (e.NewValue is string newText && !newText.EndsWith(GhostText))
            {
                _actualText = newText;
                SetCurrentValue(TextProperty, newText + GhostText);
                CaretIndex = newText.Length;
            }
        }
        base.OnPropertyChanged(e);
    }
}
  1. XAML中使用:
<local:GhostTextBox Text="{Binding UserInput, Mode=TwoWay}"
                    GhostText="/new-directory" />

方案优势

  • 两种方案都无需手动计算文本位置,避免了TextBlock定位时的溢出和裁剪问题;
  • 严格限制用户无法编辑幽灵文本区域,不会出现RichTextBox中误删内容或混淆可编辑区域的问题;
  • 保持TextBox的双向绑定功能,用户输入的文本正常同步到绑定源。

内容的提问来源于stack exchange,提问作者Andy Wynn

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.04 05:30:50