Surface Dial 与 Surface Studio 的图像
Surface Dial 与 Surface Studio 和 Surface 触控笔 (可在 Microsoft Store 购买)。

本教程逐步讲解如何自定义轮设备(如 Surface Dial)支持的用户交互体验。 我们使用示例应用中的代码片段(可从 GitHub 下载)(请参阅 示例代码 ),以演示每个步骤中讨论的各种功能和关联的 RadialController API。

我们专注于以下事项:

  • 指定 RadialController 菜单上显示哪些内置工具
  • 向菜单添加自定义工具
  • 控制触觉反馈
  • 自定义单击交互
  • 自定义旋转交互
  • 有关实现这些和其他功能的详细信息,请参阅 Windows 应用中 Surface Dial 交互

    Surface Dial 是辅助输入设备,可帮助用户在与主要输入设备(如笔、触摸或鼠标)一起使用时提高工作效率。 作为次要输入设备,Dial 通常用非主导手操作,以便访问系统命令以及其他更具上下文性的工具和功能。

    Dial 支持三种基本手势:

  • 长按以显示内置命令菜单。
  • 旋转以突出显示菜单项(如果菜单处于活动状态),或者修改应用中的当前作(如果菜单未处于活动状态)。
  • 单击此项可选择突出显示的菜单项(如果菜单处于活动状态),或调用应用中的命令(如果菜单未处于活动状态)。
  • 运行 Windows 10 创意者更新或更高版本的计算机(或虚拟机)
  • Visual Studio 2019 Windows 10 SDK (10.0.15063.0)
  • 旋钮设备(目前仅 Surface Dial
  • 如果你不熟悉使用 Visual Studio 进行 Windows 应用开发,请先了解以下主题,然后再开始本教程:
      安装适用于 Windows 应用 SDK 的工具 创建“Hello, world”应用(XAML)

      设置您的设备

    • 确保 Windows 设备已打开。
    • 转到 “开始 ”,选择 “设置 > 设备 > 蓝牙”和其他设备 ,然后打开 蓝牙
    • 删除 Surface Dial 底部以打开电池舱,并确保里面有两个 AAA 电池。
    • 如果“旋钮”下方存在电池隔片,请将其取下。
    • 按住电池旁边的小型内插按钮,直到蓝牙灯闪烁。
    • 返回到 Windows 设备并选择“ 添加蓝牙或其他设备 ”。
    • 在“ 添加设备 对话框中,选择 蓝牙 > Surface Dial 。 Surface Dial 现在应连接并添加到 蓝牙和其他设备 设置页上 鼠标、键盘和笔 的设备列表中。
    • 按住拨盘几秒钟来测试拨号,以显示内置菜单。
    • 如果屏幕上未显示菜单(拨号还应振动),请返回到蓝牙设置,删除设备,然后再次尝试连接设备。
    • 滚轮设备可以通过 Wheel 设置进行配置:

    • 开始 菜单上,选择 设置
    • 选择 设备 > 轮子

      在本教程中,我们使用示例应用来演示所讨论的概念和功能。

      GitHub 下载此 Visual Studio 示例和源代码,具体路径为 windows-appsample-get-started-radialcontroller 示例

    • 选择绿色 克隆或下载 按钮。
    • 如果有 GitHub 帐户,可以通过选择“在 Visual Studio 中打开” 将存储库克隆到本地计算机。
    • 如果没有 GitHub 帐户,或者只需要项目的本地副本,请选择 “下载 ZIP ”(必须定期查看以下载最新更新)。
    • 示例中的大部分代码都会注释掉。当我们完成本主题中的每个步骤时,系统会要求你取消注释代码的各个部分。 在 Visual Studio 中,只需突出显示代码行,然后按 CTRL-K,然后按 Ctrl-U。

      支持轮子功能的组件

      这些对象为 Windows 应用提供大部分滚轮设备体验。

      DESCRIPTION IRadialControllerConfigurationInterop / IRadialControllerInterop
      此处未介绍此功能,有关详细信息,请参阅 Windows 桌面示例 。 启用与 Windows 应用的互作性。

      步骤 1:运行示例

      下载 RadialController 示例应用后,请验证它是否运行:

    • 在 Visual Studio 中打开示例项目。
    • 解决方案平台 下拉列表设置为非 Arm 的选项。
    • 按 F5 编译、部署和运行。
    • 或者,可以选择 “调试 > ”菜单项,或选择此处显示的 “本地计算机 运行”按钮: Visual Studio 生成项目按钮

      应用窗口打开后,启动画面显示几秒钟,然后你将看到这个初始屏幕。

      好吧,我们现在有基本的 Windows 应用,我们将在整个本教程的其余部分使用。 在以下步骤中,我们将添加我们的 RadialController 功能。

      步骤 2:基本 RadialController 功能

      当应用在前台运行时,按住 Surface Dial 以显示 RadialController 菜单。

      我们尚未为应用进行任何自定义,因此菜单包含一组默认的上下文工具。

      这些图像显示了默认菜单的两种变体。 (许多其他工具,包括 Windows 桌面处于活动状态且没有应用处于前台时的基本系统工具、InkToolbar 存在时的附加墨迹书写工具,以及使用 Maps 应用时的映射工具。

      RadialController 菜单 (默认值) RadialController 菜单(默认状态下播放媒体) Content="Initialize sample" /> <ToggleButton x:Name="AddRemoveToggleButton" HorizontalAlignment="Center" Margin="10" Content="Remove Item" IsChecked="True" IsEnabled="False"/> <Button x:Name="ResetControllerButton" HorizontalAlignment="Center" Margin="10" Content="Reset RadialController menu" IsEnabled="False"/> <Slider x:Name="RotationSlider" Minimum="0" Maximum="10" Width="300" HorizontalAlignment="Center"/> <TextBlock Text="{Binding ElementName=RotationSlider, Mode=OneWay, Path=Value}" Margin="0,0,0,20" HorizontalAlignment="Center"/> <ToggleSwitch x:Name="ClickToggle" MinWidth="0" Margin="0,0,0,20" HorizontalAlignment="center"/>

      现在,仅有 初始化示例 按钮、滑块和切换开关是启用的。 其他按钮用于在后续步骤中添加和删除 RadialController 菜单项,这些菜单项提供滑块和切换开关的访问权限。

    • 查找标记为此步骤标题的代码(“// 步骤 4:基本 RadialController 菜单自定义”)。
    • 取消注释以下行:
      • 后续步骤中将使用 Windows.UI.Input Windows.Storage.Streams 类型引用实现相应功能:

        // Using directives for RadialController functionality.
        using Windows.UI.Input;
        
      • 这些全局对象(RadialControllerRadialControllerConfigurationRadialControllerMenuItem)在整个应用中使用。

        private RadialController radialController;
        private RadialControllerConfiguration radialControllerConfig;
        private RadialControllerMenuItem radialControllerMenuItem;
        
      • 在这里,我们指定按钮的 Click 处理程序,该处理程序启用我们的控件并初始化我们的自定义 RadialController 菜单项。

        InitializeSampleButton.Click += (sender, args) =>
        { InitializeSample(sender, args); };
        
      • 接下来,初始化 RadialController 对象,并为 RotationChangedButtonClicked 事件设置处理程序。

        // Set up the app UI and RadialController.
        private void InitializeSample(object sender, RoutedEventArgs e)
            ResetControllerButton.IsEnabled = true;
            AddRemoveToggleButton.IsEnabled = true;
            ResetControllerButton.Click += (resetsender, args) =>
            { ResetController(resetsender, args); };
            AddRemoveToggleButton.Click += (togglesender, args) =>
            { AddRemoveItem(togglesender, args); };
            InitializeController(sender, e);
        
      • 在这里,我们初始化自定义 RadialController 菜单项。 我们使用 CreateForCurrentView 获取对 RadialController 对象的引用, 我们使用 RotationResolutionInDegrees 属性将旋转敏感度设置为“1”,然后使用 CreateFromFontGlyph 创建 RadialControllerMenuItem,我们将菜单项添加到 RadialController 菜单项集合,最后,我们使用 SetDefaultMenuItems 清除默认菜单项,只保留自定义工具。

        // Configure RadialController menu and custom tool.
        private void InitializeController(object sender, RoutedEventArgs args)
            // Create a reference to the RadialController.
            radialController = RadialController.CreateForCurrentView();
            // Set rotation resolution to 1 degree of sensitivity.
            radialController.RotationResolutionInDegrees = 1;
            // Create the custom menu items.
            // Here, we use a font glyph for our custom tool.
            radialControllerMenuItem =
                RadialControllerMenuItem.CreateFromFontGlyph("SampleTool", "\xE1E3", "Segoe MDL2 Assets");
            // Add the item to the RadialController menu.
            radialController.Menu.Items.Add(radialControllerMenuItem);
            // Remove built-in tools to declutter the menu.
            // NOTE: The Surface Dial menu must have at least one menu item. 
            // If all built-in tools are removed before you add a custom 
            // tool, the default tools are restored and your tool is appended 
            // to the default collection.
            radialControllerConfig =
                RadialControllerConfiguration.GetForCurrentView();
            radialControllerConfig.SetDefaultMenuItems(
                new RadialControllerSystemMenuItemKind[] { });
            // Declare input handlers for the RadialController.
            // NOTE: These events are only fired when a custom tool is active.
            radialController.ButtonClicked += (clicksender, clickargs) =>
            { RadialController_ButtonClicked(clicksender, clickargs); };
            radialController.RotationChanged += (rotationsender, rotationargs) =>
            { RadialController_RotationChanged(rotationsender, rotationargs); };
        // Connect wheel device rotation to slider control.
        private void RadialController_RotationChanged(
            object sender, RadialControllerRotationChangedEventArgs args)
            if (RotationSlider.Value + args.RotationDeltaInDegrees >= RotationSlider.Maximum)
                RotationSlider.Value = RotationSlider.Maximum;
            else if (RotationSlider.Value + args.RotationDeltaInDegrees < RotationSlider.Minimum)
                RotationSlider.Value = RotationSlider.Minimum;
                RotationSlider.Value += args.RotationDeltaInDegrees;
        // Connect wheel device click to toggle switch control.
        private void RadialController_ButtonClicked(
            object sender, RadialControllerButtonClickedEventArgs args)
            ClickToggle.IsOn = !ClickToggle.IsOn;
        

        好吧,让我们连接这些按钮。

        步骤 5:在运行时配置菜单

        在此步骤中,我们将 添加/删除项 按钮 和 RadialController 菜单重置 按钮连接起来,以展示如何动态自定义菜单。

      • 打开MainPage_Basic.xaml.cs文件。

      • 查找标记为此步骤标题的代码(“//步骤 5:在运行时配置菜单”)。

      • 取消注释下面方法中的代码,然后再次运行应用,但不要选择任何按钮(这一步留到下一步再做)。

        // Add or remove the custom tool.
        private void AddRemoveItem(object sender, RoutedEventArgs args)
            if (AddRemoveToggleButton?.IsChecked == true)
                AddRemoveToggleButton.Content = "Remove item";
                if (!radialController.Menu.Items.Contains(radialControllerMenuItem))
                    radialController.Menu.Items.Add(radialControllerMenuItem);
            else if (AddRemoveToggleButton?.IsChecked == false)
                AddRemoveToggleButton.Content = "Add item";
                if (radialController.Menu.Items.Contains(radialControllerMenuItem))
                    radialController.Menu.Items.Remove(radialControllerMenuItem);
                    // Attempts to select and activate the previously selected tool.
                    // NOTE: Does not differentiate between built-in and custom tools.
                    radialController.Menu.TrySelectPreviouslySelectedMenuItem();
        // Reset the RadialController to initial state.
        private void ResetController(object sender, RoutedEventArgs arg)
            if (!radialController.Menu.Items.Contains(radialControllerMenuItem))
                radialController.Menu.Items.Add(radialControllerMenuItem);
            AddRemoveToggleButton.Content = "Remove item";
            AddRemoveToggleButton.IsChecked = true;
            radialControllerConfig.SetDefaultMenuItems(
                new RadialControllerSystemMenuItemKind[] { });
        
      • 选择 “删除项目”按钮,然后按住 Dial 以再次显示菜单。

        请注意,菜单现在包含工具的默认集合。 回想一下,在步骤 3 中设置自定义菜单时,我们删除了所有默认工具,并仅添加了自定义工具。 我们还指出,当菜单设置为空集合时,将恢复当前上下文的默认项。 (我们在删除默认工具之前添加了自定义工具。

      • 选择 添加项目 按钮,然后按并按住 Dial。

        请注意,菜单现在包含默认的工具集合和自定义工具。

      • 选择 “重置 RadialController”菜单 按钮,然后按住拨盘。

        请注意,菜单返回到其原始状态。

        步骤 6:自定义设备触觉

        Surface Dial 和其他滚轮设备可以为用户提供与当前交互对应的触觉反馈(基于单击或旋转)。

        在此步骤中,我们将展示如何通过关联滑块和切换开关控件,来自定义触觉反馈并使用这些控件来动态指定触觉反馈行为。 对于本示例,切换开关必须设置为启用,以便启用反馈,而滑块值指定单击反馈的重复频率。

        用户可以在“设置>设备> 页面”中禁用触觉反馈。

      • 打开App.xaml.cs文件。

      • 查找标记为此步骤标题的代码(“步骤 6:自定义设备触觉”)。

      • 注释第一行和第三行(“MainPage_Basic”和“MainPage”),并取消注释第二行(“MainPage_Haptics”)。

        rootFrame.Navigate(typeof(MainPage_Basic), e.Arguments);
        rootFrame.Navigate(typeof(MainPage_Haptics), e.Arguments);
        rootFrame.Navigate(typeof(MainPage), e.Arguments);
        
      • 打开 MainPage_Haptics.xaml 文件。

      • 查找标记为此步骤标题的代码(“<--步骤 6:自定义设备触觉 -->”

      • 取消注释以下行。 (此 UI 代码只是指示当前设备支持哪些触觉功能。

        <StackPanel x:Name="HapticsStack" 
                    Orientation="Vertical" 
                    HorizontalAlignment="Center" 
                    BorderBrush="Gray" 
                    BorderThickness="1">
            <TextBlock Padding="10" 
                        Text="Supported haptics properties:" />
            <CheckBox x:Name="CBDefault" 
                        Content="Default" 
                        Padding="10" 
                        IsEnabled="False" 
                        IsChecked="True" />
            <CheckBox x:Name="CBIntensity" 
                        Content="Intensity" 
                        Padding="10" 
                        IsEnabled="False" 
                        IsThreeState="True" 
                        IsChecked="{x:Null}" />
            <CheckBox x:Name="CBPlayCount" 
                        Content="Play count" 
                        Padding="10" 
                        IsEnabled="False" 
                        IsThreeState="True" 
                        IsChecked="{x:Null}" />
            <CheckBox x:Name="CBPlayDuration" 
                        Content="Play duration" 
                        Padding="10" 
                        IsEnabled="False" 
                        IsThreeState="True" 
                        IsChecked="{x:Null}" />
            <CheckBox x:Name="CBReplayPauseInterval" 
                        Content="Replay/pause interval" 
                        Padding="10" 
                        IsEnabled="False" 
                        IsThreeState="True" 
                        IsChecked="{x:Null}" />
            <CheckBox x:Name="CBBuzzContinuous" 
                        Content="Buzz continuous" 
                        Padding="10" 
                        IsEnabled="False" 
                        IsThreeState="True" 
                        IsChecked="{x:Null}" />
            <CheckBox x:Name="CBClick" 
                        Content="Click" 
                        Padding="10" 
                        IsEnabled="False" 
                        IsThreeState="True" 
                        IsChecked="{x:Null}" />
            <CheckBox x:Name="CBPress" 
                        Content="Press" 
                        Padding="10" 
                        IsEnabled="False" 
                        IsThreeState="True" 
                        IsChecked="{x:Null}" />
            <CheckBox x:Name="CBRelease" 
                        Content="Release" 
                        Padding="10" 
                        IsEnabled="False" 
                        IsThreeState="True" 
                        IsChecked="{x:Null}" />
            <CheckBox x:Name="CBRumbleContinuous" 
                        Content="Rumble continuous" 
                        Padding="10" 
                        IsEnabled="False" 
                        IsThreeState="True" 
                        IsChecked="{x:Null}" />
        </StackPanel>
        
      • 打开MainPage_Haptics.xaml.cs文件

      • 查找带有此步骤标题的代码(“步骤 6:触觉定制”)

      • 取消注释以下行:

        Windows.Devices.Haptics 类型引用用于实现后续步骤中的功能。

        using Windows.Devices.Haptics;
        
      • 在这里,我们指定选择自定义 RadialController 菜单项时触发的 ControlAcquired 事件的处理程序。

        radialController.ControlAcquired += (rc_sender, args) =>
        { RadialController_ControlAcquired(rc_sender, args); };
        
      • 接下来,定义 ControlAcquired 处理程序,在其中我们禁用默认的触觉反馈并初始化我们的触觉界面。

        private void RadialController_ControlAcquired(
            RadialController rc_sender,
            RadialControllerControlAcquiredEventArgs args)
            // Turn off default haptic feedback.
            radialController.UseAutomaticHapticFeedback = false;
            SimpleHapticsController hapticsController =
                args.SimpleHapticsController;
            // Enumerate haptic support.
            IReadOnlyCollection<SimpleHapticsControllerFeedback> supportedFeedback =
                hapticsController.SupportedFeedback;
            foreach (SimpleHapticsControllerFeedback feedback in supportedFeedback)
                if (feedback.Waveform == KnownSimpleHapticsControllerWaveforms.BuzzContinuous)
                    CBBuzzContinuous.IsEnabled = true;
                    CBBuzzContinuous.IsChecked = true;
                else if (feedback.Waveform == KnownSimpleHapticsControllerWaveforms.Click)
                    CBClick.IsEnabled = true;
                    CBClick.IsChecked = true;
                else if (feedback.Waveform == KnownSimpleHapticsControllerWaveforms.Press)
                    CBPress.IsEnabled = true;
                    CBPress.IsChecked = true;
                else if (feedback.Waveform == KnownSimpleHapticsControllerWaveforms.Release)
                    CBRelease.IsEnabled = true;
                    CBRelease.IsChecked = true;
                else if (feedback.Waveform == KnownSimpleHapticsControllerWaveforms.RumbleContinuous)
                    CBRumbleContinuous.IsEnabled = true;
                    CBRumbleContinuous.IsChecked = true;
            if (hapticsController?.IsIntensitySupported == true)
                CBIntensity.IsEnabled = true;
                CBIntensity.IsChecked = true;
            if (hapticsController?.IsPlayCountSupported == true)
                CBPlayCount.IsEnabled = true;
                CBPlayCount.IsChecked = true;
            if (hapticsController?.IsPlayDurationSupported == true)
                CBPlayDuration.IsEnabled = true;
                CBPlayDuration.IsChecked = true;
            if (hapticsController?.IsReplayPauseIntervalSupported == true)
                CBReplayPauseInterval.IsEnabled = true;
                CBReplayPauseInterval.IsChecked = true;
        
      • RotationChangedButtonClicked 事件处理程序中,我们将相应的滑块和切换按钮控件连接到自定义触觉。

        // Connect wheel device rotation to slider control.
        private void RadialController_RotationChanged(
            object sender, RadialControllerRotationChangedEventArgs args)
            if (ClickToggle.IsOn && 
                (RotationSlider.Value > RotationSlider.Minimum) && 
                (RotationSlider.Value < RotationSlider.Maximum))
                SimpleHapticsControllerFeedback waveform = 
                    FindWaveform(args.SimpleHapticsController, 
                    KnownSimpleHapticsControllerWaveforms.BuzzContinuous);
                if (waveform != null)
                    args.SimpleHapticsController.SendHapticFeedback(waveform);
        private void RadialController_ButtonClicked(
            object sender, RadialControllerButtonClickedEventArgs args)
            if (RotationSlider?.Value > 0)
                SimpleHapticsControllerFeedback waveform = 
                    FindWaveform(args.SimpleHapticsController, 
                    KnownSimpleHapticsControllerWaveforms.Click);
                if (waveform != null)
                    args.SimpleHapticsController.SendHapticFeedbackForPlayCount(
                        waveform, 1.0, 
                        (int)RotationSlider.Value, 
                        TimeSpan.Parse("1"));
        
      • 最后,我们获取请求的用于触觉反馈的 波形(如果受支持)。

        // Get the requested waveform.
        private SimpleHapticsControllerFeedback FindWaveform(
            SimpleHapticsController hapticsController,
            ushort waveform)
            foreach (var hapticInfo in hapticsController.SupportedFeedback)
                if (hapticInfo.Waveform == waveform)
                    return hapticInfo;
            return null;
        

        现在再次运行应用,通过更改滑块值和切换开关状态来试用自定义触觉。

        步骤 7:为 Surface Studio 和类似设备定义屏幕交互

        与 Surface Studio 配对后,Surface Dial 可以提供更独特的用户体验。

        除了介绍的默认按下和按住菜单体验之外,Surface Dial 还可以直接放置在 Surface Studio 的屏幕上。 这将启用特殊的“屏幕”菜单。

        通过同时检测 Surface Dial 的接触位置和边界,系统处理设备的遮挡问题,并显示一个围绕 Dial 外部的较大版本菜单。 你的应用还可以使用此相同的信息来调整 UI,以便同时适应设备的存在及其预期使用情况,例如用户的手部和手臂的位置。

        本教程附带的示例包含一个稍微复杂一些的示例,该示例演示了其中一些功能。

        要观看此操作(您需要 Surface Studio):

      • 在 Surface Studio 设备上下载示例(已安装 Visual Studio)

      • 在 Visual Studio 中打开示例

      • 打开App.xaml.cs文件

      • 查找标记为此步骤标题的代码(“步骤 7:为 Surface Studio 和类似设备定义屏幕交互”)

      • 将第一行和第二行(“MainPage_Basic”和“MainPage_Haptics”)注释掉,并将第三行(“MainPage”)取消注释。

        rootFrame.Navigate(typeof(MainPage_Basic), e.Arguments);
        rootFrame.Navigate(typeof(MainPage_Haptics), e.Arguments);
        rootFrame.Navigate(typeof(MainPage), e.Arguments);
        
      • 运行应用并将 Surface Dial 放置在两个控制区域中的每一个区域,并在两个区域之间交替使用。

        恭喜你,你已完成 入门教程:支持在 Windows 应用中使用 Surface Dial(以及其他滚轮设备)! 我们展示了在 Windows 应用中支持滚轮设备所需的基本代码,以及如何通过 RadialController API 提供更高级的用户体验。

        Surface Dial 的交互

        API 参考

        RadialController
      • RadialControllerButtonClickedEventArgs
      • RadialControllerConfiguration
      • RadialControllerControlAcquiredEventArgs
      • RadialControllerMenu
      • RadialControllerMenuItem
      • RadialControllerRotationChangedEventArgs
      • RadialControllerScreenContact
      • RadialControllerScreenContactContinuedEventArgs
      • RadialControllerScreenContactStartedEventArgs
      • RadialControllerMenuKnownIcon 枚举
      • RadialControllerSystemMenuItemKind 枚举类型
      • 调节器的自定义设置

        涂色本示例

        通用 Windows 平台示例(C# 和 C++)

        Windows 桌面示例

  •