要在 Windows 應用程式中程式化移動焦點,你可以使用 FocusManager.TryMoveFocus 方法或 FindNextElement 方法。
TryMoveFocus 嘗試將焦點從有焦點的元素轉移到指定方向下下一個可聚焦元素,而 FindNextElement 則以 DependencyObject 形式取得該元素,該元素將根據指定導航方向獲得焦點(僅方向導覽,無法用來模擬分頁導航)。
備註
我們建議使用 FindNextElement 方法取代 FindNextFocusableElement ,因為 FindNextFocusableElement 會取得 UIElement,若下一個可聚焦元素不是 UIElement(例如超連結物件),則 UIElement 會回傳 null。
尋找範疇內的焦點候選對象
你可以自訂 TryMoveFocus 和 FindNextElement 的焦點導航行為,包括在特定 UI 樹中搜尋下一個焦點候選,或排除特定元素。
此範例使用一個 TicTacToe 遊戲來示範 TryMoveFocus 和 FindNextElement 方法。
<StackPanel Orientation="Horizontal"
VerticalAlignment="Center"
HorizontalAlignment="Center" >
<Button Content="Start Game" />
<Button Content="Undo Movement" />
<Grid x:Name="TicTacToeGrid" KeyDown="OnKeyDown">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50" />
<ColumnDefinition Width="50" />
<ColumnDefinition Width="50" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="50" />
<RowDefinition Height="50" />
<RowDefinition Height="50" />
</Grid.RowDefinitions>
<myControls:TicTacToeCell
Grid.Column="0" Grid.Row="0"
x:Name="Cell00" />
<myControls:TicTacToeCell
Grid.Column="1" Grid.Row="0"
x:Name="Cell10"/>
<myControls:TicTacToeCell
Grid.Column="2" Grid.Row="0"
x:Name="Cell20"/>
<myControls:TicTacToeCell
Grid.Column="0" Grid.Row="1"
x:Name="Cell01"/>
<myControls:TicTacToeCell
Grid.Column="1" Grid.Row="1"
x:Name="Cell11"/>
<myControls:TicTacToeCell
Grid.Column="2" Grid.Row="1"
x:Name="Cell21"/>
<myControls:TicTacToeCell
Grid.Column="0" Grid.Row="2"
x:Name="Cell02"/>
<myControls:TicTacToeCell
Grid.Column="1" Grid.Row="2"
x:Name="Cell22"/>
<myControls:TicTacToeCell
Grid.Column="2" Grid.Row="2"
x:Name="Cell32"/>
</Grid>
</StackPanel>
private void OnKeyDown(object sender, KeyRoutedEventArgs e)
{
DependencyObject candidate = null;
var options = new FindNextElementOptions ()
{
SearchRoot = TicTacToeGrid,
XYFocusNavigationStrategyOverride = XYFocusNavigationStrategyOverride.Projection
};
switch (e.Key)
{
case Windows.System.VirtualKey.Up:
candidate =
FocusManager.FindNextElement(
FocusNavigationDirection.Up, options);
break;
case Windows.System.VirtualKey.Down:
candidate =
FocusManager.FindNextElement(
FocusNavigationDirection.Down, options);
break;
case Windows.System.VirtualKey.Left:
candidate = FocusManager.FindNextElement(
FocusNavigationDirection.Left, options);
break;
case Windows.System.VirtualKey.Right:
candidate =
FocusManager.FindNextElement(
FocusNavigationDirection.Right, options);
break;
}
// Also consider whether candidate is a Hyperlink, WebView, or TextBlock.
if (candidate != null && candidate is Control)
{
(candidate as Control).Focus(FocusState.Keyboard);
}
}
使用 FindNextElementOptions 進一步自訂焦點候選人的識別方式。 此物件提供以下特性:
- SearchRoot - 將焦點導航候選搜尋範圍限制為此 DependencyObject 的子項目。 Null 表示從視覺樹的根開始搜尋。
這很重要
如果對 SearchRoot 的後代進行一個或多個轉換,使其置於方向區域之外,這些元素仍被視為候選元素。
- ExclusionRect - 聚焦導航候選物件以「虛構」邊界矩形識別,所有重疊物體皆排除在導航焦點之外。 這個矩形僅用於計算,且不會被加入視覺樹。
- HintRect - 焦點導航候選者會以一個「虛構」的邊界矩形來識別,該矩形標示最有可能獲得焦點的元素。 這個矩形僅用於計算,且不會被加入視覺樹。
- XYFocusNavigationStrategyOverride - 用於識別最佳候選元素以獲得聚焦的焦點導航策略。
下圖說明了其中一些概念。
當元素 B 有焦點時,FindNextElement 在向右導航時會將 I 識別為焦點候選。 原因包括:
- 由於 A 上的 HintRect,起始參考是 A,而非 B。
- C 不適合,因為 MyPanel 被指定為 SearchRoot
- F 不是候選,因為 ExclusionRect 與 F 重疊
使用導航提示的自訂對焦導航行為
導航焦點事件
NoFocusCandidateFound 活動
當按下 Tab 鍵或方向鍵且指定方向沒有焦點候選時,會觸發 UIElement.NoFocusCandidateFound 事件。 此事件不會對 TryMoveFocus 觸發。
由於這是路由事件,它會從焦點元素一路延伸,經過連續的父物件,直到物件樹的根節點。 這樣你就能在適當的地方處理事件。
在這裡,我們展示了當使用者嘗試將焦點移到最左側可聚焦控制項的左側時,網格如何開啟分割視圖(參見 Xbox 與電視設計)。
<Grid NoFocusCandidateFound="OnNoFocusCandidateFound">
...
</Grid>
private void OnNoFocusCandidateFound (
UIElement sender, NoFocusCandidateFoundEventArgs args)
{
if(args.NavigationDirection == FocusNavigationDirection.Left)
{
if(args.InputDevice == FocusInputDeviceKind.Keyboard ||
args.InputDevice == FocusInputDeviceKind.GameController )
{
OpenSplitPaneView();
}
args.Handled = true;
}
}
GotFocus 與 LostFocus 活動
UIElement.GotFocus 和 UIElement.LostFocus 事件分別在元素獲得焦點或失去焦點時觸發。 此事件不會為 TryMoveFocus 觸發。
由於這些事件是路由事件,它們會從聚焦元素一路延伸到連續的父物件,直到物件樹的根節點。 這樣你就能在適當的地方處理事件。
GettingFocus 與 LosingFocus 活動
UIElement.GettingFocus 和 UIElement.LosingFocus 事件會在對應的 UIElement.GotFocus 和 UIElement.LostFocus 事件之前觸發。
由於這些事件是路由事件,它們會從聚焦元素以冒泡的方式逐步傳遞到連續的父物件,直至物件樹的根節點。 因為這發生在焦點轉換之前,你可以重新導向或取消焦點轉換。
GettingFocus 和 LosingFocus 是同步事件,因此在這些事件進行期間焦點不會被移動。 然而, GotFocus 和 LostFocus 是非同步事件,這表示無法保證在處理器執行前焦點不會再次移動。
如果焦點在通話中傳送到 Control.Focus,通話中會開啟 GettingFocus ,通話結束後則會開啟 GotFocus 。
焦點導航目標可在 GettingFocus 與 LosingFocus 事件期間(焦點移動前)透過 GettingFocusEventArgs.NewFocusedElement 屬性進行更改。 即使目標被更改,事件仍會冒泡,目標仍可再次更改。
為了避免再進入問題,當這些事件正在冒泡時,如果你嘗試移動焦點(使用 TryMoveFocus 或 Control.Focus),就會拋出例外。
這些事件無論焦點移動的原因為何(包括分頁導航、方向導航及程式化導航)都會觸發。
以下是焦點事件的執行順序:
- 失焦 若焦點重置回失去焦點元素或嘗試 取消 成功,則不會觸發更多事件。
- GettingFocus 如果焦點重置回失去焦點的元素,或者 TryCancel 成功,則不會觸發更多事件。
- LostFocus
- GotFocus
下圖顯示,當XYFocus從A向右移動時,如何選擇B4作為候選。 接著 B4 會觸發 GettingFocus 事件,讓 ListView 有機會將焦點重新分配到 B3。
GettingFocus 活動中調整焦點導航目標
在這裡,我們示範如何處理 GettingFocus 事件並重新導向焦點。
<StackPanel Orientation="Horizontal">
<Button Content="A" />
<ListView x:Name="MyListView" SelectedIndex="2" GettingFocus="OnGettingFocus">
<ListViewItem>LV1</ListViewItem>
<ListViewItem>LV2</ListViewItem>
<ListViewItem>LV3</ListViewItem>
<ListViewItem>LV4</ListViewItem>
<ListViewItem>LV5</ListViewItem>
</ListView>
</StackPanel>
private void OnGettingFocus(UIElement sender, GettingFocusEventArgs args)
{
//Redirect the focus only when the focus comes from outside of the ListView.
// move the focus to the selected item
if (MyListView.SelectedIndex != -1 &&
IsNotAChildOf(MyListView, args.OldFocusedElement))
{
var selectedContainer =
MyListView.ContainerFromItem(MyListView.SelectedItem);
if (args.FocusState ==
FocusState.Keyboard &&
args.NewFocusedElement != selectedContainer)
{
args.TryRedirect(
MyListView.ContainerFromItem(MyListView.SelectedItem));
args.Handled = true;
}
}
}
在這裡,我們示範如何處理 CommandBar 的 LosingFocus 事件,以及在選單關閉時設定焦點。
<CommandBar x:Name="MyCommandBar" LosingFocus="OnLosingFocus">
<AppBarButton Icon="Back" Label="Back" />
<AppBarButton Icon="Stop" Label="Stop" />
<AppBarButton Icon="Play" Label="Play" />
<AppBarButton Icon="Forward" Label="Forward" />
<CommandBar.SecondaryCommands>
<AppBarButton Icon="Like" Label="Like" />
<AppBarButton Icon="Share" Label="Share" />
</CommandBar.SecondaryCommands>
</CommandBar>
private void OnLosingFocus(UIElement sender, LosingFocusEventArgs args)
{
if (MyCommandBar.IsOpen == true &&
IsNotAChildOf(MyCommandBar, args.NewFocusedElement))
{
if (args.TryCancel())
{
args.Handled = true;
}
}
}
找出第一個和最後一個可聚焦的元素
FocusManager.FindFirstFocusableElement 與 FocusManager.FindLastFocusableElement 方法會將焦點移至物件範圍內的第一個或最後一個可聚焦元素(UIElement 的元素樹或 TextElement 的文字樹)。 作用域在呼叫中指定(若參數為空,則作用域為目前視窗)。
若無法在範圍內識別出焦點候選,則回傳為空。
在此,我們示範如何指定 CommandBar 的按鈕具有環繞方向行為(參見 鍵盤互動)。
<CommandBar x:Name="MyCommandBar" LosingFocus="OnLosingFocus">
<AppBarButton Icon="Back" Label="Back" />
<AppBarButton Icon="Stop" Label="Stop" />
<AppBarButton Icon="Play" Label="Play" />
<AppBarButton Icon="Forward" Label="Forward" />
<CommandBar.SecondaryCommands>
<AppBarButton Icon="Like" Label="Like" />
<AppBarButton Icon="ReShare" Label="Share" />
</CommandBar.SecondaryCommands>
</CommandBar>
private void OnLosingFocus(UIElement sender, LosingFocusEventArgs args)
{
if (IsNotAChildOf(MyCommandBar, args.NewFocussedElement))
{
DependencyObject candidate = null;
if (args.Direction == FocusNavigationDirection.Left)
{
candidate = FocusManager.FindLastFocusableElement(MyCommandBar);
}
else if (args.Direction == FocusNavigationDirection.Right)
{
candidate = FocusManager.FindFirstFocusableElement(MyCommandBar);
}
if (candidate != null)
{
args.NewFocusedElement = candidate;
args.Handled = true;
}
}
}