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

问题描述

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