Windows 应用中的触控笔交互和 Windows Ink

Surface 触控笔的 Hero 图像。
Surface 触控笔 (可在 Microsoft 应用商店购买)。

概述

针对笔输入优化 Windows 应用,为用户提供标准 指针设备 功能和最佳 Windows Ink 体验。

注释

本主题重点介绍 Windows Ink 平台。 有关常规指针输入处理(类似于鼠标、触摸和触控板),请参阅 处理指针输入

在 Windows 应用中使用墨迹

使用 Windows 触控笔和墨迹生成更具吸引力的企业应用

Windows Ink 平台与笔设备一起提供了创建数字手写笔记、绘图和批注的自然方法。 该平台支持将数字化器输入捕获为墨迹数据、生成墨迹数据、管理墨迹数据、将墨迹数据呈现为输出设备上的墨迹笔划,以及通过手写识别将墨迹转换为文本。

除了在用户写入或绘图时捕获笔的基本位置和移动之外,你的应用还可以跟踪和收集整个笔划中使用的不同压力量。 此信息以及笔尖形状、大小和旋转、墨迹颜色和用途(纯墨迹、擦除、突出显示和选择)的设置,使你可以提供与使用笔、铅笔或画笔在纸上书写或绘图非常相似的用户体验。

注释

你的应用还可以支持来自其他基于指针的设备(包括触摸数字化器和鼠标设备)的墨迹输入。 

墨迹平台非常灵活。 它旨在支持各种级别的功能,具体取决于你的要求。

有关 Windows Ink UX 指南,请参阅 墨迹书写控件

Windows Ink 平台的组件

组件 DESCRIPTION
InkCanvas 一个 XAML UI 平台控件,默认情况下接收来自笔的所有输入,并将其显示为墨水笔迹或擦除笔迹。
有关如何使用 InkCanvas 的详细信息,请参阅将 Windows 墨迹笔划识别为文本并存储和检索 Windows Ink 笔划数据
InkPresenter 后台代码对象,与 InkCanvas 控件同时实例化(通过 InkCanvas.InkPresenter 属性公开)。 此对象提供 InkCanvas 公开的所有默认墨迹书写功能,以及用于其他自定义和个性化的综合 API 集。
有关如何使用 InkPresenter 的详细信息,请参阅将 Windows Ink 笔划识别为文本并存储和检索 Windows Ink 笔划数据
墨迹工具栏 XAML UI 平台控件,其中包含可自定义且可扩展的按钮集合,这些按钮在关联的 InkCanvas 中激活与墨迹相关的功能。
有关如何使用 InkToolbar 的详细信息,请参阅 将 InkToolbar 添加到 Windows 应用墨迹书写应用
IInkD2DRenderer 启用将墨迹笔划呈现到通用 Windows 应用的指定 Direct2D 设备上下文,而不是默认的 InkCanvas 控件。 这样就可以完全自定义墨迹书写体验。
有关详细信息,请参阅 复杂墨迹示例

基本使用 InkCanvas 进行墨迹书写

若要添加基本墨迹书写功能,只需将 InkCanvas UWP 平台控件放在应用中的相应页面上即可。

默认情况下, InkCanvas 仅支持笔中的墨迹输入。 输入要么使用默认颜色和粗细设置(厚度为 2 像素的黑色圆珠笔)呈现为墨迹笔划,要么被视为笔划橡皮擦(当输入来自橡皮擦尖或带有擦除按钮的笔尖时)。

注释

如果橡皮擦提示或按钮不存在,可以将 InkCanvas 配置为将笔尖的输入作为擦除笔划进行处理。

在此示例中,InkCanvas 覆盖了背景图像。

注释

InkCanvas 的默认高度 高度Width 属性为零,除非它是自动调整其子元素大小的元素的子元素(如 StackPanelGrid 控件)。

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <StackPanel x:Name="HeaderPanel" Orientation="Horizontal" Grid.Row="0">
        <TextBlock x:Name="Header"
                   Text="Basic ink sample"
                   Style="{ThemeResource HeaderTextBlockStyle}"
                   Margin="10,0,0,0" />            
    </StackPanel>
    <Grid Grid.Row="1">
        <Image Source="Assets\StoreLogo.png" />
        <InkCanvas x:Name="inkCanvas" />
    </Grid>
</Grid>

这组图像展示了如何通过此 InkCanvas 控件呈现笔输入。

带有背景图像的空白 InkCanvas 的屏幕截图。 具有墨迹笔划的 InkCanvas 屏幕截图。 InkCanvas 擦除一笔后的屏幕截图。
带有背景图像的空白 InkCanvas 带有墨迹笔划的InkCanvas InkCanvas 擦除了一个笔划(请注意擦除如何在整个笔划上操作,而不是一部分)。

InkCanvas 控件支持的墨迹书写功能由名为 InkPresenter的代码隐藏对象提供。

对于基础墨迹书写,您无需顾虑 InkPresenter。 但是,若要在 InkCanvas 上自定义和配置墨迹书写行为,必须访问其相应的 InkPresenter 对象。

使用 InkPresenter 进行基本自定义

InkPresenter 对象随每个 InkCanvas 控件一起实例化。

注释

无法直接实例化的对象是 InkPresenter。 而是通过 InkCanvasInkPresenter 属性访问它。 

除了提供与其相应 InkCanvas 控件相同的所有默认墨迹行为外,InkPresenter 还提供了全面的 API 组,用于进一步的笔划定制和更细致的笔输入管理(标准和修改过的)。 这包括笔划属性、支持的输入设备类型,以及输入是由对象处理还是传递给应用进行处理。

注释

标准墨迹输入(从笔尖或橡皮擦末端/按钮)未通过诸如笔杆按钮、鼠标右键或类似机制的辅助硬件功能进行修改。

默认情况下,仅笔输入支持墨迹。 在这里,我们将 InkPresenter 配置为将笔和鼠标中的输入数据解释为墨迹笔划。 我们还设置了一些用于呈现笔划的初始墨迹属性到 InkCanvas

要启用鼠标和触摸墨迹书写,请将 InkPresenterInputDeviceTypes 属性设置为所需的 CoreInputDeviceTypes 值的组合。

public MainPage()
{
    this.InitializeComponent();

    // Set supported inking device types.
    inkCanvas.InkPresenter.InputDeviceTypes =
        Windows.UI.Core.CoreInputDeviceTypes.Mouse |
        Windows.UI.Core.CoreInputDeviceTypes.Pen;

    // Set initial ink stroke attributes.
    InkDrawingAttributes drawingAttributes = new InkDrawingAttributes();
    drawingAttributes.Color = Windows.UI.Colors.Black;
    drawingAttributes.IgnorePressure = false;
    drawingAttributes.FitToCurve = true;
    inkCanvas.InkPresenter.UpdateDefaultDrawingAttributes(drawingAttributes);
}

可以动态设置墨迹笔划属性以适应用户首选项或应用要求。

在这里,我们允许用户从墨迹颜色列表中选择。

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <StackPanel x:Name="HeaderPanel" Orientation="Horizontal" Grid.Row="0">
        <TextBlock x:Name="Header"
                   Text="Basic ink customization sample"
                   VerticalAlignment="Center"
                   Style="{ThemeResource HeaderTextBlockStyle}"
                   Margin="10,0,0,0" />
        <TextBlock Text="Color:"
                   Style="{StaticResource SubheaderTextBlockStyle}"
                   VerticalAlignment="Center"
                   Margin="50,0,10,0"/>
        <ComboBox x:Name="PenColor"
                  VerticalAlignment="Center"
                  SelectedIndex="0"
                  SelectionChanged="OnPenColorChanged">
            <ComboBoxItem Content="Black"/>
            <ComboBoxItem Content="Red"/>
        </ComboBox>
    </StackPanel>
    <Grid Grid.Row="1">
        <Image Source="Assets\StoreLogo.png" />
        <InkCanvas x:Name="inkCanvas" />
    </Grid>
</Grid>

然后处理对所选颜色的更改,并相应地更新墨迹笔划属性。

// Update ink stroke color for new strokes.
private void OnPenColorChanged(object sender, SelectionChangedEventArgs e)
{
    if (inkCanvas != null)
    {
        InkDrawingAttributes drawingAttributes =
            inkCanvas.InkPresenter.CopyDefaultDrawingAttributes();

        string value = ((ComboBoxItem)PenColor.SelectedItem).Content.ToString();

        switch (value)
        {
            case "Black":
                drawingAttributes.Color = Windows.UI.Colors.Black;
                break;
            case "Red":
                drawingAttributes.Color = Windows.UI.Colors.Red;
                break;
            default:
                drawingAttributes.Color = Windows.UI.Colors.Black;
                break;
        };

        inkCanvas.InkPresenter.UpdateDefaultDrawingAttributes(drawingAttributes);
    }
}

这些图像显示了 InkPresenter如何处理和自定义笔输入。

这是一张显示默认黑色墨迹笔触的 InkCanvas 的屏幕截图。

具有默认黑色墨迹笔划的 InkCanvas

InkCanvas 的屏幕截图,其中用户选择了红色墨迹笔划。

InkCanvas 用户所选的红色墨迹笔划。

若要提供墨迹书写和擦除以外的功能(如笔划选择),你的应用必须识别 InkPresenter 的特定输入,以便让你的应用直接处理这些输入。

用于高级处理的直通输入

默认情况下,InkPresenter 将所有输入处理为墨迹笔划或擦除笔划,包括由辅助硬件功能(如笔杆按钮、鼠标右键或类似功能)修改的输入。 但是,用户通常期望这些次要功能或期待其具有经过修改的行为。

在某些情况下,你可能需要为没有次要功能(通常不与笔尖关联的功能)的笔公开额外的功能,或者公开其他类型的输入设备功能,或根据用户在应用程序UI中的选择来展示某种修改后的行为。

为了支持此功能,可以将 InkPresenter 配置为保留未处理的特定输入。 然后,此未处理的输入将传递到应用进行处理。

示例 - 使用未处理的输入实现笔划选择

Windows Ink 平台不提供对需要经过修改的输入操作的内置支持,例如笔划选择。 若要支持此类功能,必须在应用中提供自定义解决方案。

下面的代码示例(所有代码都在 MainPage.xaml 和 MainPage.xaml.cs 文件中)逐步介绍如何在使用笔桶按钮(或鼠标右键)修改输入时启用笔划选择。

  1. 首先,我们在 MainPage.xaml 中设置 UI。

    在这里,我们将添加画布(InkCanvas下方)来绘制选择笔划。 使用单独的层来绘制选择笔划会使 InkCanvas 及其内容保持不变。

    使用基础选择画布 空白 InkCanvas 的屏幕截图。

      <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <Grid.RowDefinitions>
          <RowDefinition Height="Auto"/>
          <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <StackPanel x:Name="HeaderPanel" Orientation="Horizontal" Grid.Row="0">
          <TextBlock x:Name="Header"
            Text="Advanced ink customization sample"
            VerticalAlignment="Center"
            Style="{ThemeResource HeaderTextBlockStyle}"
            Margin="10,0,0,0" />
        </StackPanel>
        <Grid Grid.Row="1">
          <!-- Canvas for displaying selection UI. -->
          <Canvas x:Name="selectionCanvas"/>
          <!-- Inking area -->
          <InkCanvas x:Name="inkCanvas"/>
        </Grid>
      </Grid>
    
  2. 在MainPage.xaml.cs中,我们声明了几个全局变量来保留对所选 UI 的各个方面的引用。 具体而言,套索选择笔划和标出所选笔划的边界矩形。

      // Stroke selection tool.
      private Polyline lasso;
      // Stroke selection area.
      private Rect boundingRect;
    
  3. 接下来,我们将 InkPresenter 配置为将笔和鼠标的输入数据解释为墨迹笔划,并设置一些初始的墨迹笔划属性,用于在 InkCanvas上渲染笔划。

    最重要的是,我们使用 InkPresenterInputProcessingConfiguration 属性来指示应用应处理任何修改的输入。 通过将 InputProcessingConfiguration.RightDragAction 的值设定为 InkInputRightDragAction.LeaveUnprocessed来指定修改后的输入。 设置此值后, InkPresenter 将传递到 InkUnprocessedInput 类,这是一组指针事件供你处理。

    我们为未处理的 PointerPressedPointerMovedPointerReleased 事件分配侦听器,这些事件是通过 InkPresenter传递的。 所有选择功能都在这些事件的处理程序中实现。

    最后,我们为 InkPresenterStrokeStartedStrokesErased 事件分配侦听器。 如果启动新笔划或清除现有笔划,我们将使用这些事件的处理程序来清理选择 UI。

    “高级墨迹自定义”示例应用程序的屏幕截图,显示了默认黑色墨迹笔划的墨迹画布。

      public MainPage()
      {
        this.InitializeComponent();
    
        // Set supported inking device types.
        inkCanvas.InkPresenter.InputDeviceTypes =
          Windows.UI.Core.CoreInputDeviceTypes.Mouse |
          Windows.UI.Core.CoreInputDeviceTypes.Pen;
    
        // Set initial ink stroke attributes.
        InkDrawingAttributes drawingAttributes = new InkDrawingAttributes();
        drawingAttributes.Color = Windows.UI.Colors.Black;
        drawingAttributes.IgnorePressure = false;
        drawingAttributes.FitToCurve = true;
        inkCanvas.InkPresenter.UpdateDefaultDrawingAttributes(drawingAttributes);
    
        // By default, the InkPresenter processes input modified by
        // a secondary affordance (pen barrel button, right mouse
        // button, or similar) as ink.
        // To pass through modified input to the app for custom processing
        // on the app UI thread instead of the background ink thread, set
        // InputProcessingConfiguration.RightDragAction to LeaveUnprocessed.
        inkCanvas.InkPresenter.InputProcessingConfiguration.RightDragAction =
            InkInputRightDragAction.LeaveUnprocessed;
    
        // Listen for unprocessed pointer events from modified input.
        // The input is used to provide selection functionality.
        inkCanvas.InkPresenter.UnprocessedInput.PointerPressed +=
            UnprocessedInput_PointerPressed;
        inkCanvas.InkPresenter.UnprocessedInput.PointerMoved +=
            UnprocessedInput_PointerMoved;
        inkCanvas.InkPresenter.UnprocessedInput.PointerReleased +=
            UnprocessedInput_PointerReleased;
    
        // Listen for new ink or erase strokes to clean up selection UI.
        inkCanvas.InkPresenter.StrokeInput.StrokeStarted +=
            StrokeInput_StrokeStarted;
        inkCanvas.InkPresenter.StrokesErased +=
            InkPresenter_StrokesErased;
      }
    
  4. 然后,我们定义未处理的 PointerPressedPointerMoved的处理程序,以及由 InkPresenter传递的 PointerReleased 事件。

    所有选择功能都在这些处理器中实现,包括套索功能和边界矩形。

    选择套索的屏幕截图。

      // Handle unprocessed pointer events from modified input.
      // The input is used to provide selection functionality.
      // Selection UI is drawn on a canvas under the InkCanvas.
      private void UnprocessedInput_PointerPressed(
        InkUnprocessedInput sender, PointerEventArgs args)
      {
        // Initialize a selection lasso.
        lasso = new Polyline()
        {
            Stroke = new SolidColorBrush(Windows.UI.Colors.Blue),
            StrokeThickness = 1,
            StrokeDashArray = new DoubleCollection() { 5, 2 },
            };
    
            lasso.Points.Add(args.CurrentPoint.RawPosition);
    
            selectionCanvas.Children.Add(lasso);
        }
    
        private void UnprocessedInput_PointerMoved(
          InkUnprocessedInput sender, PointerEventArgs args)
        {
          // Add a point to the lasso Polyline object.
          lasso.Points.Add(args.CurrentPoint.RawPosition);
        }
    
        private void UnprocessedInput_PointerReleased(
          InkUnprocessedInput sender, PointerEventArgs args)
        {
          // Add the final point to the Polyline object and
          // select strokes within the lasso area.
          // Draw a bounding box on the selection canvas
          // around the selected ink strokes.
          lasso.Points.Add(args.CurrentPoint.RawPosition);
    
          boundingRect =
            inkCanvas.InkPresenter.StrokeContainer.SelectWithPolyLine(
              lasso.Points);
    
          DrawBoundingRect();
        }
    
  5. 为了结束 PointerReleased 事件处理程序,我们清除所有内容(套索笔划)的选择层,然后在套索区域包含的墨迹笔划周围绘制单个边界矩形。

    所选内容边界矩形的屏幕截图。

      // Draw a bounding rectangle, on the selection canvas, encompassing
      // all ink strokes within the lasso area.
      private void DrawBoundingRect()
      {
        // Clear all existing content from the selection canvas.
        selectionCanvas.Children.Clear();
    
        // Draw a bounding rectangle only if there are ink strokes
        // within the lasso area.
        if (!((boundingRect.Width == 0) ||
          (boundingRect.Height == 0) ||
          boundingRect.IsEmpty))
          {
            var rectangle = new Rectangle()
            {
              Stroke = new SolidColorBrush(Windows.UI.Colors.Blue),
                StrokeThickness = 1,
                StrokeDashArray = new DoubleCollection() { 5, 2 },
                Width = boundingRect.Width,
                Height = boundingRect.Height
            };
    
            Canvas.SetLeft(rectangle, boundingRect.X);
            Canvas.SetTop(rectangle, boundingRect.Y);
    
            selectionCanvas.Children.Add(rectangle);
          }
        }
    
  6. 最后,我们定义 StrokeStartedStrokesErased InkPresenter 事件的处理程序。

    这两者只需调用相同的清理函数,以在检测到新笔划时清除当前选择。

      // Handle new ink or erase strokes to clean up selection UI.
      private void StrokeInput_StrokeStarted(
        InkStrokeInput sender, Windows.UI.Core.PointerEventArgs args)
      {
        ClearSelection();
      }
    
      private void InkPresenter_StrokesErased(
        InkPresenter sender, InkStrokesErasedEventArgs args)
      {
        ClearSelection();
      }
    
  7. 下面是在启动新笔划或擦除现有笔划时从选择画布中删除所有选择 UI 的函数。

      // Clean up selection UI.
      private void ClearSelection()
      {
        var strokes = inkCanvas.InkPresenter.StrokeContainer.GetStrokes();
        foreach (var stroke in strokes)
        {
          stroke.Selected = false;
        }
        ClearDrawnBoundingRect();
       }
    
      private void ClearDrawnBoundingRect()
      {
        if (selectionCanvas.Children.Any())
        {
          selectionCanvas.Children.Clear();
          boundingRect = Rect.Empty;
        }
      }
    

自定义墨水渲染

默认情况下,墨迹输入在低延迟的后台线程上进行处理,并在绘制过程中进行“湿”状态的实时呈现。 完成笔划后(抬起笔或手指或松开鼠标按钮),笔划将在 UI 线程上进行处理,并“干燥地”呈现到 InkCanvas 层(位于应用程序内容之上,并替代湿墨迹)。

你可以更改此默认行为,并通过“自定义干燥”尚未干燥的墨迹笔划的过程,完全控制墨迹书写体验。 虽然默认行为通常足以用于大多数应用程序,但在某些情况下可能需要自定义干燥,其中包括:

  • 更高效地管理大量或复杂的墨迹笔划集合
  • 在大型墨迹画布上更有效的平移和缩放功能支持
  • 交错墨迹和其他对象,如形状或文本,同时保持 z 轴顺序
  • 将墨迹同步干燥并转换为 DirectX 形状(例如,直线或形状光栅化并集成到应用程序内容中,而不是作为单独的 InkCanvas 层)。

自定义干燥需要 IInkD2DRenderer 对象来管理墨迹输入并将其呈现到通用 Windows 应用的 Direct2D 设备上下文,而不是默认的 InkCanvas 控件。

通过调用 ActivateCustomDrying(加载 InkCanvas 之前),应用将创建一个 InkSynchronizer 对象,以自定义墨迹笔划如何干化到 SurfaceImageSourceVirtualSurfaceImageSource

SurfaceImageSourceVirtualSurfaceImageSource 都为应用提供了一个 DirectX 共享图面,供绘制和组合应用程序的内容,尽管 VSIS 提供了一个比屏幕更大的虚拟图面,以提升平移和缩放性能。 由于这些图面的视觉更新与 XAML UI 线程同步,因此当墨迹被渲染到任一对象时,可以同时从 InkCanvas 中删除湿墨迹。

还可以将干墨自定义为 SwapChainPanel,但无法保证与 UI 线程同步,并且当墨迹呈现到 SwapChainPanel 以及从 InkCanvas 中删除墨迹时,可能存在延迟。

有关此功能的完整示例,请参阅 复杂墨迹示例

注释

自定义干燥和 墨水工具栏
如果您的应用使用自定义干燥实现来替代 InkPresenter 的默认墨迹渲染行为,那么呈现的墨迹笔划将无法用于 InkToolbar,并且 InkToolbar 的内置擦除命令将无法按预期运作。 若要提供擦除功能,必须处理所有指针事件、对每个笔划执行命中测试,并重写内置的“擦除所有墨迹”命令。

主题 DESCRIPTION
识别墨水笔迹 使用手写识别将墨迹笔划转换为文本,或使用自定义识别将墨迹笔划转换为形状。
存储和检索墨迹笔划 使用嵌入式墨迹序列化格式(ISF)元数据在图形交换格式(GIF)文件中存储墨迹笔划数据。
向 Windows 墨迹书写应用添加 InkToolbar 将默认的 InkToolbar 添加到 Windows 墨迹应用中,添加自定义笔按钮到 InkToolbar,并将自定义笔按钮绑定到自定义笔定义。

应用程序接口

示例

存档样本