[WPF自定义控件库] 让Form在加载后自动获得焦点
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. 参考
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
- 查看并行进程的一些简单信息(r3笔记第17天)
- 多行数据的批处理之bulk collect(r3笔记第16天)
- pl/sql中错误的异常处理 (r3笔记第15天)
- 关于session leak的问题分析(r3笔记第13天)
- 执行计划的偏差导致的性能问题(r3笔记第12天)
- 关于评审开发人员的sql语句(r3笔记第11天)
- 性能下降的不定时炸弹_过旧的sql_profile(r3笔记第9天)
- 关于抓取session信息的一个脚本(r3笔记第8天)
- oracle PL/SQL中的重载 (r3笔记27天)
- 使用Python进行描述性统计
- 关于修改分区表的准备和操作细则(r3笔记26天)
- 一条"简单"的sql语句和小兔子买面包的故事 (r3笔记第25天)
- 生产环境sql语句调优实战第八篇(r3笔记第24天)
- Python做文本挖掘的情感极性分析
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- Python 图像处理篇-利用opencv库和numpy库读取包含中文路径下的本地图片实例演示
- 从头创建您自己的vue.js——第2部分(虚拟DOM基础)
- Manage Jenkins报错:"依赖错误: 部分插件由于缺少依赖无法加载...",解决办法
- 从头创建您自己的vuei .js——第3部分(构建VDOM)
- adb 模拟上下左右滑动,示例演示
- python 技术篇-pythoncom.PumpMessag()关闭、杀死它的进程,pythoncom.PumpMessag()运行卡住解决办法
- PyQt5 技术篇-QWidget、QDialog程序窗口关闭closeEvent()触发事件方法重写
- 恕我直言你可能真的不会java第6篇:Stream性能差?不要人云亦云
- python-技术篇-打印详细报错日志,获取报错信息位置行数
- React从入门到放弃,一个关于网页速度的故事
- python 技术篇-日志定期清理设置,自动清理上个月的日志实例演示
- python 技术篇-日志模块自定义时间格式
- 恕我直言你可能真的不会java第12篇-如何使用Stream API对Map元素排序
- JavaScript错误处理完全指南
- 从头创建您自己的vue.js——第4部分(构建反应性)