总目录
前言
本文主要介绍WPF中的焦点,对于开发WPF应用程序来说,理解焦点这个概念是非常重要的。
一、焦点概述
在 WPF 中,有两个与焦点有关的主要概念:键盘焦点和逻辑焦点。
键盘焦点指接收键盘输入的元素,而逻辑焦点指焦点范围中具有焦点的元素。
参与焦点管理的主要类是 Keyboard 类、FocusManager 类和基本元素类(UIElement、FrameworkElement、ContentElement 和 FrameworkContentElement四个类为基元素类)
Keyboard 类主要与键盘焦点相关,而 FocusManager 主要与逻辑焦点相关,但这种区别不是绝对的。 具有键盘焦点的元素也具有逻辑焦点,但具有逻辑焦点的元素不一定具有键盘焦点。 使用 Keyboard 类来设置具有键盘焦点的元素时,这一点就很明显,因为它还在元素上设置逻辑焦点。
二、键盘焦点
定义: 键盘焦点指当前正在接收键盘输入的元素。 在整个桌面上,只能有一个具有键盘焦点的元素。
获取元素的键盘焦点状态:
- 在 WPF 中,具有键盘焦点的元素会将 IsKeyboardFocused 设置为 true。 通常用于获取该元素是否具有键盘焦点,如
bool isfocus= this.textBox.IsKeyboardFocused;
- 基元素类的 IsKeyboardFocusWithin 属性获取一个值,该值指示元素或其任何一个视觉子元素是否具有键盘焦点。
获取当前具有键盘焦点的元素: Keyboard 类的静态属性 FocusedElement 获取当前具有键盘焦点的元素。
获取键盘焦点的前提: 为使元素获得键盘焦点,必须将基元素的 Focusable 和 IsVisible 属性设置为 true。 某些类(例如 Panel 基类)默认将 Focusable 设置为 false;因此,如果要此类元素能够获得键盘焦点,必须将 Focusable 设置为 true。
设置键盘焦点的推荐位置: Loaded 事件处理程序是设置初始焦点的推荐位置。
获取键盘焦点的方式:
- 可通过用户与 UI 交互(例如,按 Tab 键导航到某个元素或者在某些元素上单击鼠标)来获取键盘焦点。
- 也可以使用 Keyboard 类的 Focus 方法以编程方式获取键盘焦点。 Focus 方法尝试为指定元素提供键盘焦点。 返回的元素是具有键盘焦点的元素,如果旧的或新的焦点对象阻止请求,则具有键盘焦点的元素可能不是请求的元素。
<!--Panel 类默认Focusable="False" 不可获取焦点,可以设置为"True" 让其可以获取焦点-->
<StackPanel Focusable="True">
<Button x:Name="YesButton" Width="100" Height="50" Margin="5" >Yes</Button>
<Button x:Name="NoButton" Width="100" Height="50" Margin="5" >No</Button>
<Button x:Name="CancelButton" Width="100" Height="50" Margin="5">Cancel</Button>
</StackPanel>
public Window2()
{
InitializeComponent();
//获取键盘焦点状态
var isFocusele1 = this.YesButton.IsKeyboardFocused;
//获取元素及其视觉树内的子元素 键盘焦点状态
var isFocusele2 = this.IsKeyboardFocusWithin;
//获取当前具有键盘焦点的元素
this.YesButton.Click += (s,e) => { MessageBox.Show(Keyboard.FocusedElement.GetType().ToString()); };
//在Loaded事件中设置初始焦点
//将初始焦点,设置在Name 为 NoButton的按钮上
this.Loaded += (s,e) => { Keyboard.Focus(NoButton); };
this.NoButton.Click += (s, e) => { MessageBox.Show("NoButton.Click"); };
}
当我们运行上述代码,在出现窗口时,我们直接按Enter 键,会发现会直接执行NoButton的Click 处理程序,因为窗口Loaded 事件中将NoButton 设置为了键盘焦点,那么按Enter 键就会直接作用于NoButton 上触发Click事件。
三、逻辑焦点
定义: 逻辑焦点是指焦点范围内的具有焦点的元素。
获取元素的键盘焦点状态: 通过元素的IsFocused获取当前元素是否具有逻辑焦点。
焦点范围: 焦点范围是一个容器元素,用于跟踪其范围内的 FocusedElement。WPF 中默认为焦点范围的类是 Window、MenuItem、ToolBar 和 ContextMenu。
键盘焦点,焦点范围与逻辑焦点:
- 键盘焦点离开焦点范围时,焦点元素会失去键盘焦点,但保留逻辑焦点。
键盘焦点返回到焦点范围时,焦点元素会再次获得键盘焦点。
这使得键盘焦点可在多个焦点范围之间切换,但确保了焦点返回到焦点范围时,焦点范围中的焦点元素仍为焦点元素。 - 一个应用程序中可以有多个具有逻辑焦点的元素,但在一个特定的焦点范围中只能有一个具有逻辑焦点的元素。
- 具有键盘焦点的元素还具有其所属焦点范围的逻辑焦点。
FocusManager焦点管理:
- 设置焦点范围,在xaml中使用
FocusManager.IsFocusScope="True"
,或者在代码中,使用FocusManager.SetIsFocusScope(focuseScopeElement, true);
将指定元素设置为焦点范围 - 获取指定元素的焦点范围: GetFocusScope 如:
FocusManager.GetFocusScope(element);
- 获取指定焦点范围的焦点元素: GetFocusedElement
如:IInputElement focusedElement = FocusManager.GetFocusedElement(focusScope1);
- 设置指定焦点范围中的焦点元素: SetFocusedElement,通常用于设置初始焦点元素。
如:FocusManager.SetFocusedElement(focusScope1, button2);
四、焦点事件
与键盘焦点相关的事件: PreviewGotKeyboardFocus、GotKeyboardFocus 和 PreviewLostKeyboardFocus、LostKeyboardFocus。
这些事件定义为 Keyboard 类上的附加事件,但更多地作为基元素类上的等效路由事件来访问
元素获得键盘焦点时会引发 GotKeyboardFocus。 元素丢失键盘焦点时会引发 LostKeyboardFocus。 如果已处理 PreviewGotKeyboardFocus 事件或 PreviewLostKeyboardFocusEvent 事件并将 Handled 设置为 true,则焦点不会改变。
与逻辑焦点相关的事件: GotFocus 和 LostFocus。 这些事件在 FocusManager 上定义为附加事件,但 FocusManager 不公开 CLR 事件包装器。 UIElement 和 ContentElement 可以更方便地公开这些事件。
通过焦点事件理解一下IsFocusScope的作用
首先通过FocusManager.IsFocusScope="True"
设置了两个焦点范围
<Grid x:Name="grid">
<StackPanel FocusManager.IsFocusScope="True" HorizontalAlignment="Left" Background="Red">
<TextBox x:Name="Tb_AAA" Text="AAA" Width="100" Height="50" Margin="15" VerticalContentAlignment="Center"></TextBox>
<TextBox x:Name="Tb_BBB" Text="BBB" Width="100" Height="50" Margin="15" VerticalContentAlignment="Center"></TextBox>
</StackPanel>
<StackPanel FocusManager.IsFocusScope="True" HorizontalAlignment="Right" Background="Green">
<TextBox x:Name="Tb_CCC" Text="CCC" Width="100" Height="50" Margin="15" VerticalContentAlignment="Center"></TextBox>
<TextBox x:Name="Tb_DDD" Text="DDD" Width="100" Height="50" Margin="15" VerticalContentAlignment="Center"></TextBox>
</StackPanel>
</Grid>
然后给两个焦点范围元素的父元素监听GotFocus、LostFocus、GotKeyboardFocus、LostKeyboardFocus 四个焦点事件
this.grid.GotFocus += (s,e) => { Debug.WriteLine($"{(e.Source as TextBox).Name}___GotFocus"); };
this.grid.LostFocus += (s, e) => { Debug.WriteLine($"{(e.Source as TextBox).Name}___LostFocus"); };
this.grid.GotKeyboardFocus+= (s,e) => { Debug.WriteLine($"{(e.Source as TextBox).Name}___GotKeyboardFocus"); };
this.grid.LostKeyboardFocus+= (s,e) => { Debug.WriteLine($"{(e.Source as TextBox).Name}___LostKeyboardFocus"); };
会发现如下结果:
当在同一个焦点范围内,如上案例,我们只点击输入框Tb_AAA 和Tb_BBB ,只要有一个获得键盘焦点和逻辑焦点,那么必定有一个失去键盘焦点和逻辑焦点
当在不同的焦点范围内,如上案例,我们先点击Tb_AAA ,然后点击Tb_CCC,就会出现,Tb_AAA 只会失去键盘焦点,而不会失去逻辑焦点。因为每个焦点范围都有自己的一个逻辑焦点,不会因为键盘焦点的离开而消失。如果我们此时再点击Tb_BBB,会首先发现Tb_CCC 也仅是失去键盘焦点,还保留着逻辑焦点。然后Tb_AAA 失去逻辑焦点(因为之前A=>C的时候,同样A保留着逻辑焦点),Tb_BBB 获得逻辑焦点和键盘焦点。
另外为什么需要设置焦点范围呢,这是因为对于某些场景下时需要有多个焦点范围,否则工作无法进行。例如,我们最简单的复制和粘贴,如果我们只有一个焦点范围,是无法操作的。
案例代码如下:
<!--使用该案例说明 为什么需要设置焦点范围,什么情况下需要焦点范围-->
<StackPanel HorizontalAlignment="Center">
<StackPanel FocusManager.IsFocusScope="True">
<Button Command="ApplicationCommands.Copy" Content="复制" Width="100" Height="50" Margin="15"></Button>
<Button Command="ApplicationCommands.Paste" Content="粘贴" Width="100" Height="50" Margin="15"></Button>
</StackPanel>
<TextBox Width="100" Height="50" Margin="15" VerticalContentAlignment="Center" />
<TextBox Width="100" Height="50" Margin="15" VerticalContentAlignment="Center"/>
</StackPanel>
上面案例中,将两个操作按钮复制和粘贴设置在一个焦点范围内,另外的两个输入框位于另一个焦点范围内,可以保证操作的进行。如果将FocusManager.IsFocusScope设置为False,会发现根本无法操作,因为自始至终只有一个控件可以获得焦点,当你操作TextBox的时候,TextBox获得焦点,那么Button 将没有焦点,成了禁用状态。
五、为控件中的焦点设置样式以及 FocusVisualStyle
WPF中提供两种用于在控件接收键盘焦点时更改其视觉外观的方案。
第一种方案是对应用于控件的样式或模板中的属性(如 IsKeyboardFocused)使用属性 setter。
第二种方案是将一个单独的样式作为 FocusVisualStyle 属性的值提供;
焦点视觉样式仅在焦点操作由键盘启动时才起作用。 任何鼠标操作或者通过编程实现的焦点更改都会禁用焦点视觉样式模式。
上面这句话就是说:只有使用键盘导航焦点的时候,才会呈现焦点的样式,比如我们平常使用Tab键切换焦点的时候,会发现按钮上有一个黑色虚线边框,那个就是默认的FocusVisualStyle。
下面案例,展示了两种修改控件焦点样式的方案:
<Window.Resources>
<Style x:Key="MyFocusVisual">
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate>
<Rectangle Margin="-2" StrokeThickness="2" Stroke="Red" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<StackPanel Background="Green">
<Button Content="原生按钮,通过Tab键 焦点导航,会出现默认焦点样式:黑色虚线边框" Width="400" Height="50" Margin="15" ></Button>
<Button Width="400" Height="50" Margin="15" Content="【第一种】通过IsKeyboardFocused 在样式或模板中设置焦点样式">
<Button.Style>
<Style TargetType="{x:Type Button}">
<Style.Triggers>
<!--通过IsKeyboardFocused 设置焦点样式-->
<Trigger Property="IsKeyboardFocused" Value="True">
<!--通过FocusVisualStyle 将原生的焦点样式去掉-->
<Setter Property="FocusVisualStyle" Value="{x:Null}"></Setter>
<Setter Property="BorderBrush" Value="Red"></Setter>
<Setter Property="BorderThickness" Value="3"></Setter>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
<Button Content="【第二种】通过FocusVisualStyle设置焦点样式" Width="400" Height="50" Margin="15" FocusVisualStyle="{StaticResource MyFocusVisual}"></Button>
<TextBox Width="400" Height="50" Margin="15" FocusVisualStyle="{StaticResource MyFocusVisual}"></TextBox>
</StackPanel>
由上案例可知,相较于在样式或模板使用IsKeyboardFocused 设置焦点样式,FocusVisualStyle的优势在于可以将焦点样式作为一个独立的资源,多次赋予各个不同的控件使用。
另外我们通常在平常使用时:如果要去掉默认的焦点视觉样式,会设置FocusVisualStyle=“{x:Null}”。
总结
以上就是今天要介绍的内容,希望以上内容可以帮助到大家,如文中有不对之处,还请批评指正。