1. 需求

加载后让第一个输入框或者焦点是个很基本的功能,典型的如“登录”对话框。一般来说“登录”对话框加载后“用户名”应该马上获得焦点,用户只需输入用户名,点击tab,再输入密码,点击回车就完成了登录操作。

在wpf中要让一个控件在加载时获得焦点应该很简单,只需要在loaded事件后调用focus()就行了。但有时表单是动态添加的,或者第一个表单元素会根据某些条件显示或隐藏,这时很难简单地让第一个控件获得焦点。

为了实现这个功能我创建了一个叫focusservice的工具类,这篇文章介绍这个类的使用及原理,以及补充一些wpf焦点的知识。

2. 实现

public static readonly dependencyproperty isautofocusproperty =
    dependencyproperty.registerattached("isautofocus", typeof(bool), typeof(focusservice), new propertymetadata(default(bool), onisautofocuschanged));

public static bool getisautofocus(dependencyobject obj) => (bool)obj.getvalue(isautofocusproperty);

public static void setisautofocus(dependencyobject obj, bool value) => obj.setvalue(isautofocusproperty, value);

private static void onisautofocuschanged(dependencyobject obj, dependencypropertychangedeventargs args)
{
    var oldvalue = (bool)args.oldvalue;
    var newvalue = (bool)args.newvalue;
    if (oldvalue == newvalue)
    {
        return;
    }

    if (obj is frameworkelement target)
    {
        target.loaded -= ontargetloaded;
        if (newvalue)
        {
            target.loaded += ontargetloaded;
        }
    }
}

private static void ontargetloaded(object sender, routedeventargs e)
{
    var element = sender as frameworkelement;
    if (system.componentmodel.designerproperties.getisindesignmode(element))
        return;

    var request = new traversalrequest(focusnavigationdirection.next);
    element.movefocus(request);
}

上面是focusservice的代码,它使用isautofocus这个附加属性控制是否自动获得焦点,做成附加属性是为了可在xaml上控制。这个附加属性不仅可以用在control上,还可以用在grid等其它ui元素上。在form中是在defaultstyle设用setter设置了默认值,以前提过一般情况下附加属性和依赖属性都不会在代码里设置默认值。

<setter property="local:focusservice.isautofocus"
        value="true" />

movefocus

在frameworkelement上将isautofocus附加属性设置为true的话(false不处理),这个frameworkelement会在loaded事件调用movefocus函数将键盘焦点移动到自身visualtree中第一个可以接受焦点的元素上。大致上,movefocus的具体操作是使用深度优先的方式遍历visualtree,找到第一个istabstob、focusable和isvisible都为true的元素并调用keyboard.focus函数。所谓的“第一个”,基本上和用户直觉上理解的一致。

designerproperties.getisindesignmode

designerproperties.getisindesignmode方法用于确定元素是否运行在设计器中。visualstudio的设计器太过强大,几乎是所见即所得,大部分代码都可以在设计视图里运行。ontargetloaded里判断如果是运行在设计器就不执行后面的操作,是避免每次刷新设计视图都让它获得焦点。

visualstudio的设计器真的十分强大,但有时又会因为程序的数据没准备好或各种原因而报错,如果遇到设计器的错误又不想处理具体原因可以考虑简单粗暴地使用designerproperties.getisindesignmode判断并直接return。

3. 两种焦点类型

作为补充知识,这篇文章将简单介绍一下wpf的焦点。

3.1 键盘焦点

键盘焦点指当前正在接收键盘输入的ui元素。 在整个桌面上,只能有一个具有键盘焦点的元素。为了使ui元素可以获得焦点,它的focusable和isvisible必须为true。通常,对于非控件类focusable属性值的默认值为false。

keyboard类可以用于处理键盘焦点,代码如下:

keyboard.focus(firsttextbox);

focus函数如果执行成功,ui元素的iskeyboardfocused将被设置为true,并且它本身或visualtree上各级父元素的iskeyboardfocuswithin都会变成true。

当然,如果ui元素并未加载到visualtree上focus函数不会执行成功,所以通常在loaded事件以后才执行focus函数。

3.2 逻辑焦点

逻辑焦点是指focusscope中的focusmanager.focusedelement,一个应用程序中可以有多个获得逻辑焦点的元素,但只有一个获得键盘焦点的元素。获得键盘焦点的元素同时也获得逻辑焦点。

focusscope

focusscope可以通过focusmanager.isfocusscope改变。

<stackpanel name="focusscope1" 
            focusmanager.isfocusscope="true"
            height="200" width="200">
  <button name="button1" height="50" width="50"/>
  <button name="button2" height="50" width="50"/>
</stackpanel>
stackpanel focusescope2 = new stackpanel();
focusmanager.setisfocusscope(focusescope2, true);

focusedelement

focusmanager还用于管理逻辑焦点,它使用getfocusedelement(dependencyobject)获取focusscope中获得逻辑焦点的元素,使用setfocusedelement(dependencyobject, iinputelement)将元素设置为逻辑焦点。

3.3 window的逻辑焦点

window默认为focusscope,它在静态构造函数中将isfocusscope设置为true(不在defaultstyle中设置):

focusmanager.isfocusscopeproperty.overridemetadata(typeof(window), new frameworkpropertymetadata(true));

在window加载(或者window本身被激活)时,它都会用类似的代码让window中的逻辑焦点元素获得焦点。

dependencyobject docontent = content as dependencyobject;
if (docontent != null)
{
    iinputelement focusedelement = focusmanager.getfocusedelement(docontent) as iinputelement;
    if (focusedelement != null)
        focusedelement.focus();
}

4. 结语

其实没有这个类也可以,反正代码简单,只是想通过这个类介绍下附加属性和focus的用法。

做自定义控件要做好焦点管理,尤其是现在,因为很多设计师、产品经理、开发者都有丰富的手机应用开发设计经验,由于手机上的键盘导航逻辑和桌面应用的有些出入,所以键盘导航的细节很容易被忽视。

不过,通常来说用着用着觉得不顺手就会有人提出需求,细心的开发者总会渐渐把键盘导航做好。

5. 参考

焦点概述 microsoft docs

输入概述 microsoft docs

focusmanager class (system.windows.input) microsoft docs

keyboard.focus(iinputelement) method (system.windows.input) microsoft docs

uielement.movefocus(traversalrequest) method (system.windows) microsoft docs

6. 源码

kino.toolkit.wpf_focusservice.cs