Marvin's Blog【程式人生】

Ability will never catch up with the demand for it

29 Nov 2020

UWP Design Input文档笔记

Keyboard > Access keys

键盘访问键可以快速在UI中切换和交互。Windows内建对键盘访问键的支持,并且能够通过Visual Cue也叫做Key Tips提供UI反馈。

Keyboard > Keyboard accelerators

快捷键(Accelerator keys),通常包括F1到F12以及其他包含修饰键(CTRL,SHIFT)的按键组合。

UWP空间有内建的快捷键。比如ListView可以使用Ctrl+A选中所有条目,RichEditBox支持使用Ctrl+Tab来插入Tab。这些控件快捷键只有在控件本身或者其子元素具有焦点的时候才能被执行。全部的快捷键叫做应用快捷键(App accelerators)。

应该在合适的地方尽可能放置快捷键,原因有二:

  • 有些障碍用户只能使用键盘操作
  • 高级用户可以使用键盘操作提高效率

使用KeyboardAccelerator编程接口来提供快捷键的支持。好处是不用处理多个KeyDown事件来侦测按键组合。以及可以对快捷键进行本地化。

对于最常用的快捷键,可以在菜单项或者提示框中显示出来。

UIElement的KeyboardAccelerators辖属可以设置一组KeyboardAccelerator。每个KeyboardAccelerator对象有Key和Modifiers两个辖属。前者是VirtualKey类型,后者是VirtualKeyModifiers类型。

单键快捷键(A,Delete, F2, Spacebar, Esc, Multimedia Key)以及多键快捷键(Ctrl+Shift+M)是被支持的。Gamepad virtual keys则不被支持。

对于ContextMenu,由于面向的元素比较单一,建议将其快捷键的访问范围设置在其父元素。可以使用ScopeOwner辖属来达成这一点。

KeyboardAccelerator采用 UI Automation (UIA) control pattern 来触发操作。

typically a control is invoked by clicking, double-clicking, or pressing Enter, a predefined keyboard shortcut, or some other combination of keystrokes

如果一个控件定义了多个控制模式的话,只有一个能被KeyboardAccelerator采用,采用的顺序定义如下:

  • Invoke (Button)
  • Toggle (Checkbox)
  • Selection (ListView)
  • Expand/Collapse (ComboBox)

如果无法匹配,一个调试信息会被打印出来:

“No automation patterns for this component found. Implement all desired behavior in the Invoked event. Setting Handled to true in your event handler suppresses this message.”

KeyboardAccelerator自带一个可以触发的Invoked事件,其的参数类型是 KeyboardAcceleratorInvokedEventArgs,包含下列辖属:

  • Handled
  • Element (相依对象)
  • KeyboardAccelerator,触发此Invoke的快捷键
<ListView x:Name="MyListView">
  <ListView.KeyboardAccelerators>
    <KeyboardAccelerator Key="A" Modifiers="Control,Shift" Invoked="SelectAllInvoked" />
    <KeyboardAccelerator Key="F5" Invoked="RefreshInvoked"  />
  </ListView.KeyboardAccelerators>
</ListView>
void SelectAllInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
{
  MyListView.SelectAll();
  args.Handled = true;
}

void RefreshInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
{
  MyListView.SelectionMode = ListViewSelectionMode.None;
  MyListView.SelectionMode = ListViewSelectionMode.Multiple;
  args.Handled = true;
}

一些控件会定义局部的的快捷键,比如TextBox所定义的Ctrl+C会用来拷贝选中的文字。虽然不建议覆盖全局的的快捷键,但是需要的时候会很有用。比如TextBox的例子是通过PreviewKeyDown事件来达成的:

private void TextBlock_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
 {
    var ctrlState = CoreWindow.GetForCurrentThread().GetKeyState(Windows.System.VirtualKey.Control);
    var isCtrlDown = ctrlState == CoreVirtualKeyStates.Down || ctrlState 
        ==  (CoreVirtualKeyStates.Down | CoreVirtualKeyStates.Locked);
    if (isCtrlDown && e.Key == Windows.System.VirtualKey.C)
    {
        // Your custom keyboard accelerator behavior.
        
        e.Handled = true;
    }
 }

如果一个控件被禁用了,那么控件附带的快捷键也会被禁用。父子控件可以共享相同的快捷键。这样当子元素有焦点,但是其快捷键被禁用时,父控件的快捷键会响应。

像讲述者这种屏幕阅读器会声告快捷键,如果有多个可能的快捷键,只有第一个可以被声告。通过AutomationProperties.AcceleratorKey 这个寄放辖属,可以对此做一些定制。

为了减少用户的记忆负担,最好采用采用约定俗成的快捷键组合:

关于可用性的一些考虑。

可以使用Tooltips来显示快捷键。在1803的Win10,一个快捷键生命了之后会自动在Tooltip中显示(除了MenuFlyoutItem以及ToggleMenuFlyoutItem之外,因为对于这些控件,会直接显示在控件内)。如果有多个可选快捷键,那么只有第一个会显示。

显示指定Tooltip会覆盖上述行为。

可以通过KeyboardAcceleratorPlacementMode以及KeyboardAcceleratorPlacementTarget来调整显示行为。

有时候直观地在控件标签上显示出其支持地快捷键是一个好主意。前面提到地MenuFlyoutItem以及ToggleMenuFlyoutItem默认提供这样地支持。AppBarButton还有AppBarToggleButton在CommandBar的溢出菜单中也会有这样的行为。如果不需要这样的行为,那么可以把KeyboardAcceleratorTextOverride设置为空白字符。

下面是高级主题。

默认情况下,按下的快捷键会被反复触发,这是无法自定义的。

快捷键被按下的时候,原本的按键事件冒泡会被取消,事件会被标记为已处理。

对于文本相关的控件,比如TextBox。CharaterReceived事件的产生在KeyDown事件之后。所以你有机会在KeyDown的事件处理器中阻止ChracterReceived事件。

PreviewKeyDown以及PreviewKeyUp比快捷键优先级更高,它们的触发顺序如下:

  • Preview KeyDown events
App accelerator
OnKeyDown method
KeyDown event
App accelerators on the parent
OnKeyDown method on the parent
KeyDown event on the parent
(Bubbles to the root)
  • CharacterReceived event
  • PreviewKeyUp events
  • KeyUpEvents

快捷键的处理是基于事件冒泡的,所以XAML会回溯节点树,找到合适的快捷键宿主。

局部快捷键只在该局部具有焦点的时候才会触发。

可以在程序中限定快捷键的范围。UIElement.TryInvokeKeyboardAccelerator可以在一颗XAML子节点树中触发一个快捷键。UIElement.OnProcessKeyboardAccelerators会在快捷键生效之前触发。在其ProcessKeyboardAcceleratorArgs参数中可以将keyboard accelerator设置为handled。

OnProcessKeyboardAccelerators 和OnKeyDown 一样,对于handled的事件同样会触发。

下面的例子把快捷键限制在某个XAML子节点树:

protected override void OnProcessKeyboardAccelerators(
  ProcessKeyboardAcceleratorArgs args)
{
  if(args.Handled != true)
  {
    this.TryInvokeKeyboardAccelerator(args);
    args.Handled = true;
  }
}

键盘快捷键是可以被本地化的。可以在.resw文件中对于不同的语言指定不同的快捷键。

<Button x:Uid="myButton" Click="OnSave">
  <Button.KeyboardAccelerators>
    <KeyboardAccelerator x:Uid="myKeyAccelerator" Modifiers="Control"/>
  </Button.KeyboardAccelerators>
</Button>

在代码中设置快捷键:

void AddAccelerator(
  VirtualKeyModifiers keyModifiers, 
  VirtualKey key, 
  TypedEventHandler<KeyboardAccelerator, KeyboardAcceleratorInvokedEventArgs> handler )
  {
    var accelerator = 
      new KeyboardAccelerator() 
      { 
        Modifiers = keyModifiers, Key = key
      };
    accelerator.Invoked += handler;
    this.KeyboardAccelerators.Add(accelerator);
  }

值得注意的是KeyboardAccelerator是不能在多个UIElement之间共享的。

可以对KeyboardAccelerator.Invoked事件进行处理,达成某些自定义的行为:

public class MyListView : ListView
{
  
  protected override void OnKeyboardAcceleratorInvoked(KeyboardAcceleratorInvokedEventArgs args) 
  {
    if(args.Accelerator.Key == VirtualKey.A 
      && args.Accelerator.Modifiers == KeyboardModifiers.Control)
    {
      CustomSelectAll(TypeOfSelection.OnlyNumbers); 
      args.Handled = true;
    }
  }
  
}

(我是底线)

Categories

Tags