WPF — 点击空白处隐藏View

  • A+
所属分类:.NET技术
摘要

本文介绍一种点击空白处使控件隐藏的实现方法。考虑如下场景,在白板类软件中,点击按钮弹出一个View,希望在点击空白处直接隐藏掉View,同时可以直接书写,如下图:

本文介绍一种点击空白处使控件隐藏的实现方法。

问题描述

考虑如下场景,在白板类软件中,点击按钮弹出一个View,希望在点击空白处直接隐藏掉View,同时可以直接书写,如下图:

WPF -- 点击空白处隐藏View

实现该需求,可以通过View间通信解决,但这样会增加代码耦合且使逻辑显得复杂。

本文通过派生UserControl,将处理逻辑封装在View内部,从而降低代码耦合度。

解决方案

通过分析需求可以想到,点击空白处时,该View会失去焦点,因此可以通过监听LostFocus事件来处理。

首先,需要设置Focusable属性为true,其默认值为false。然后监听LostFocus事件,当View失去焦点时,Visibility属性置为Collapsed。

此处有个问题,如果点击View内部的子控件,View会先LostFocus,然后立马GotFocus,通过测试间隔在20ms内。因此还要响应下GotFocus事件,获取到焦点时,Visibility属性置为Visible。

另外,当点击按钮显示View时,此View并未获取焦点,因此需要监听IsVisibleChanged事件,当NewValue为true时,通过调用Focus使View获取焦点。

还需要处理一个问题。如上文动图所示,需点击按钮显示,再次点击按钮隐藏。但再次点击按钮时,View已经失去了焦点,此时已隐藏,所以再次点击会导致View隐藏后立马显示。经过测试统计,点击按钮执行命令,到View响应命令执行显示/隐藏,时间在(50,200)ms范围内。因此如果在该范围内View先隐藏后显示,需将其Visibility置为Collapsed。

至此,逻辑基本处理完了,但是还有一个坑。如果使用bool值绑定Visibility(Mode需设置为TwoWay),点击按钮修改bool时,PropertyChanged事件会通知监听者属性改变,此时由上个步骤中的逻辑知道,我们需要修改Visibility的值,这理论上又会导致bool值的改变,但bool值并未修改(属性未修改完再次修改),这就导致Visibility与bool值不一致,再次点击按钮不会显示View。我们只需要异步执行上个步骤,就可以解决。

通过上述处理,点击空白处隐藏View的逻辑就封装到View里面了,核心代码如下所示,感兴趣的可以下载完整demo试试。如果有其它好的方法,欢迎交流(WPF或开源库或许有更好的解决方案)。

// 派生UserControl public class MyAutoHideControl : UserControl {     public MyAutoHideControl()         : base()     {         Focusable = true;         _lastTimeCollapsed = DateTime.Now.Ticks / 10000;          IsVisibleChanged += AutoHideControl_IsVisibleChanged;         GotFocus += AutoHideControl_GotFocus;         LostFocus += AutoHideControl_LostFocus;     }      private void AutoHideControl_GotFocus(object sender, RoutedEventArgs e)     {         if (Visibility != Visibility.Visible)             Visibility = Visibility.Visible;     }      private void AutoHideControl_LostFocus(object sender, RoutedEventArgs e)     {         if (Visibility == Visibility.Visible)             Visibility = Visibility.Collapsed;     }      private void AutoHideControl_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)     {         if ((bool)e.NewValue == (bool)e.OldValue)             return;          if ((bool)e.NewValue)         {             long interval = DateTime.Now.Ticks / 10000 - _lastTimeCollapsed;             if (interval > MinInterval && interval < MaxInterval)             {                 if (Visibility == Visibility.Visible)                 {                     Dispatcher.BeginInvoke(new Action(() =>                     {                         Visibility = Visibility.Collapsed;                     }));                 }             }             else                 Focus();         }         else             _lastTimeCollapsed = DateTime.Now.Ticks / 10000;     }      private long _lastTimeCollapsed;      // 需处理再次点击按钮隐藏的情况     private const long MinInterval = 50;     private const long MaxInterval = 200; } 
// View <Window ...         xmlns:c="clr-namespace:CalcBinding;assembly=CalcBinding"         xmlns:local="clr-namespace:AutoHideControl"         Title="AutoHideControl" Height="200" Width="350">      <Window.Resources>         <BooleanToVisibilityConverter x:Key="BooleanToVisibility"/>     </Window.Resources>      <Grid>         <InkCanvas Background="LightCyan"/>         <DockPanel VerticalAlignment="Bottom" Margin="10" Height="Auto">             <local:MyAutoHideView DockPanel.Dock="Top" Width="150" Height="50" Margin="10"                               Visibility="{Binding ShowView,Converter={StaticResource BooleanToVisibility},Mode=TwoWay}"/>             <Button Width="80" Height="30" Command="{Binding ButtonClickedCommand}"                     Content="{c:Binding ShowView ? 'Hide' : 'Show'}"/>         </DockPanel>     </Grid> </Window>  // ViewModel public class MainWindowViewModel : INotifyPropertyChanged {     public bool ShowView     {         get => _showView;         set         {             _showView = value;             OnPropertyChanged();         }     }      public DelegateCommand ButtonClickedCommand =>         _buttonClickedCommand ?? (_buttonClickedCommand = new DelegateCommand         {             ExecuteAction = (_)=> ShowView = !_showView         });      public void OnPropertyChanged([CallerMemberName] string name = "")=>         PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));      public event PropertyChangedEventHandler PropertyChanged;      private bool _showView;     private DelegateCommand _buttonClickedCommand; }