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