WPF双击修改文本2

4 阅读2分钟

1. 与 IsEnabled 的核心区别

属性作用视觉效果事件传递
IsEnabled控件是否启用通常变灰(取决于控件模板)禁用后控件仍可见,但无法交互
IsHitTestVisible元素是否参与命中测试无默认视觉效果禁用后元素对鼠标“透明”,事件穿透到下层

2. 命中测试机制

WPF 的鼠标事件从最顶层元素开始向下传递,直到找到第一个 IsHitTestVisible = true 且能处理该事件的元素。

示例

xml

<StackPanel Background="LightGray" MouseDown="StackPanel_MouseDown">
    <Button Content="普通按钮"/>
    <Rectangle Width="100" Height="50" Fill="Red" IsHitTestVisible="False"/>
    <Button Content="被遮挡的按钮"/>
</StackPanel>
  • 点击 Rectangle 区域时,因为 IsHitTestVisible=False,事件穿透到下层
  • 如果下层是 StackPanel(背景可命中),它会接收鼠标事件

3. 实战

xml

 <Button Grid.Row="2" Width="200" Height="30" attached:NewEditBehavior.EnableDoubleClick="True">
     <Button.Content>
         <TextBox Width="195" Height="26" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" IsHitTestVisible="False" Text="{Binding NewContent}"/>
     </Button.Content>
 </Button>

未编辑时

  • IsHitTestVisible = False
  • TextBox 对鼠标“透明”
  • 点击看起来像在点 Button(Button 接收到双击事件)

编辑时

  • IsHitTestVisible = True
  • TextBox 开始接收鼠标事件
  • 用户可以输入、移动光标、选择文本

关键优势

  • 不破坏 Button 的原有交互:Button 仍然存在,只是它的 Content 区域对鼠标透明
  • 不需要替换 Content:TextBox 一直在那里,只是切换状态 cs代码:
 public static class NewEditBehavior
 {
     public static readonly DependencyProperty EnableDoubleClickProperty =
         DependencyProperty.RegisterAttached("EnableDoubleClick", typeof(bool), typeof(NewEditBehavior),
         new PropertyMetadata(false, OnEnableDoubleClickChanged));

     public static void SetEnableDoubleClick(DependencyObject obj, bool value) => obj.SetValue(EnableDoubleClickProperty, value);
     public static bool GetEnableDoubleClick(DependencyObject obj) => (bool)obj.GetValue(EnableDoubleClickProperty);

     private static void OnEnableDoubleClickChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
     {
         if (d is Button button && (bool)e.NewValue)
         {
             button.MouseDoubleClick += OnButtonDoubleClick;
         }
     }

     private static void OnButtonDoubleClick(object sender, MouseButtonEventArgs e)
     {
         var button = sender as Button;
         if (button?.Content is TextBox textBox)
         {
             // 保存原始值
             string originalText = textBox.Text;

             // 进入编辑模式
             textBox.IsHitTestVisible = true;

             // 延迟设置焦点,确保光标显示
             button.Dispatcher.BeginInvoke(new Action(() =>
             {
                 textBox.Focus();
                 textBox.SelectAll();
             }), System.Windows.Threading.DispatcherPriority.Input);

             // 临时事件处理
             RoutedEventHandler lostFocusHandler = null;
             KeyEventHandler keyDownHandler = null;
             MouseButtonEventHandler previewMouseDownHandler = null;

             lostFocusHandler = (s, args) =>
             {
                 ExitEditMode(textBox, button, originalText, false);
                 CleanupEvents(textBox, lostFocusHandler, keyDownHandler, previewMouseDownHandler, button);
             };

             keyDownHandler = (s, args) =>
             {
                 if (args.Key == Key.Enter)
                 {
                     // 保存修改
                     ExitEditMode(textBox, button, originalText, true);
                     CleanupEvents(textBox, lostFocusHandler, keyDownHandler, previewMouseDownHandler, button);
                     args.Handled = true;
                 }
                 else if (args.Key == Key.Escape)
                 {
                     // 取消修改,恢复原值
                     textBox.Text = originalText;
                     ExitEditMode(textBox, button, originalText, false);
                     CleanupEvents(textBox, lostFocusHandler, keyDownHandler, previewMouseDownHandler, button);
                     args.Handled = true;
                 }
             };

             // 处理点击空白区域失去焦点
             previewMouseDownHandler = (s, args) =>
             {
                 var clickedElement = args.OriginalSource as DependencyObject;
                 // 检查点击的元素是否在 TextBox 内部
                 if (!IsChildOfTextBox(clickedElement, textBox))
                 {
                     // 点击了外部区域,退出编辑模式(不保存)
                     ExitEditMode(textBox, button, originalText, false);
                     CleanupEvents(textBox, lostFocusHandler, keyDownHandler, previewMouseDownHandler, button);
                 }
             };

             textBox.LostFocus += lostFocusHandler;
             textBox.KeyDown += keyDownHandler;

             // 在窗口级别监听鼠标点击
             var window = Window.GetWindow(button);
             if (window != null)
             {
                 window.PreviewMouseDown += previewMouseDownHandler;
             }

             e.Handled = true;
         }
     }

     private static void ExitEditMode(TextBox textBox, Button button, string originalText, bool saveChanges)
     {
         // 如果不保存,恢复原值
         if (!saveChanges && textBox.Text != originalText)
         {
             textBox.Text = originalText;
         }

         textBox.IsHitTestVisible = false;

         // 将焦点移到 Button 上
         button.Focus();
     }

     private static void CleanupEvents(TextBox textBox, RoutedEventHandler lostFocusHandler,
         KeyEventHandler keyDownHandler, MouseButtonEventHandler previewMouseDownHandler, Button button)
     {
         textBox.LostFocus -= lostFocusHandler;
         textBox.KeyDown -= keyDownHandler;

         var window = Window.GetWindow(button);
         if (window != null && previewMouseDownHandler != null)
         {
             window.PreviewMouseDown -= previewMouseDownHandler;
         }
     }

     private static bool IsChildOfTextBox(DependencyObject element, TextBox textBox)
     {
         while (element != null)
         {
             if (element == textBox)
                 return true;
             element = VisualTreeHelper.GetParent(element);
         }
         return false;
     }
 }