欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

WPF -- 点击空白处隐藏View

程序员文章站 2022-10-27 20:22:09
本文介绍一种点击空白处使控件隐藏的实现方法。 问题描述 考虑如下场景,在白板类软件中,点击按钮弹出一个View,希望在点击空白处直接隐藏掉View,同时可以直接书写,如下图: 实现该需求,可以通过View间通信解决,但这样会增加代码耦合且使逻辑显得复杂。 本文通过派生UserControl,将处理逻 ......

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

问题描述

考虑如下场景,在白板类软件中,点击按钮弹出一个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;
}