1. 程式人生 > >[WPF自定義控制元件庫] 讓Form在載入後自動獲得焦點

[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. 參考

焦點概述 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