1. 程式人生 > 其它 >WPF -- 點選空白處隱藏View

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;
}