WPF -- 點選空白處隱藏View
本文介紹一種點選空白處使控制元件隱藏的實現方法。
問題描述
考慮如下場景,在白板類軟體中,點選按鈕彈出一個View,希望在點選空白處直接隱藏掉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;
}