如何为双向绑定的TextBox追加不可编辑的幽灵文本?
解决TextBox追加不可编辑幽灵文本的方案
针对你需要在双向绑定的可编辑TextBox末尾追加不可编辑幽灵文本的需求,这里提供两种可行的WPF实现方案:
方案一:使用Adorner绘制幽灵文本
这种方案通过Adorner在TextBox文本上方绘制幽灵文本,既不干扰用户输入的双向绑定,也无需手动计算文本位置,同时能严格限制用户无法编辑幽灵区域。
实现步骤
- 创建幽灵文本装饰器类:
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); } }
- 在XAML中使用:
<TextBox Text="{Binding UserInput, Mode=TwoWay}" local:GhostTextAdorner.GhostText="/new-directory" />
方案二:自定义TextBox分离用户输入与幽灵文本
这种方案通过自定义TextBox控件,将用户输入文本和幽灵文本分离显示,实际绑定的仅为用户输入部分,从根源上阻止对幽灵文本的编辑。
实现步骤
- 创建自定义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); } }
- XAML中使用:
<local:GhostTextBox Text="{Binding UserInput, Mode=TwoWay}" GhostText="/new-directory" />
方案优势
- 两种方案都无需手动计算文本位置,避免了TextBlock定位时的溢出和裁剪问题;
- 严格限制用户无法编辑幽灵文本区域,不会出现RichTextBox中误删内容或混淆可编辑区域的问题;
- 保持TextBox的双向绑定功能,用户输入的文本正常同步到绑定源。
内容的提问来源于stack exchange,提问作者Andy Wynn
相关产品推荐
相关产品推荐

