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