将 InkToolbar 添加到 Windows 应用

有两个不同的控件有助于在 Windows 应用中墨迹书写: InkCanvasInkToolbar

InkCanvas 控件提供基本的 Windows Ink 功能。 使用它可将笔输入呈现为墨迹笔划(使用颜色和粗细的默认设置)或擦除笔划。

有关 InkCanvas 实现详细信息,请参阅 Windows 应用中的笔和触笔交互

作为完全透明的覆盖层,InkCanvas 不提供任何内置 UI 来设置墨迹笔划属性。 如果要更改默认墨迹书写体验,让用户设置墨迹笔划属性并支持其他自定义墨迹书写功能,有两个选项:

  • 在后台代码中,使用绑定到 InkCanvas 的底层 InkPresenter 对象。

    InkPresenter API 支持大量自定义墨迹书写体验。 有关更多详细信息,请参阅 Windows 应用中的触控笔和触笔交互

  • InkToolbar 绑定到 InkCanvas。 默认情况下,InkToolbar 提供了一个可自定义且可扩展的按钮集合,用于激活笔划大小、墨迹颜色和笔尖等与墨迹相关的功能。

    我们将在本文中讨论“InkToolbar”。

重要 APIInkCanvas 类InkToolbar 类InkPresenter 类Windows.UI.Input.Inking

默认绘图工具栏

默认情况下,InkToolbar 提供用于绘制、擦除、突出显示以及显示工具(标尺或量角器)的按钮。 其他设置和命令(例如墨迹颜色、笔划粗细、全部擦除等)根据功能在浮出控件中提供。

InkToolbar
默认 Windows Ink 工具栏

若要将默认 InkToolbar 添加到墨迹书写应用,只需将其放置在与 InkCanvas 相同的页面上,并关联这两个控件。

  1. 在 MainPage.xaml 中,声明容器对象(在本示例中,我们使用网格控件)作为墨迹书写图面。
  2. 将一个 InkCanvas 对象声明为容器的子对象。 (InkCanvas 大小继承自容器。)
  3. 声明 InkToolbar 并使用 TargetInkCanvas 属性将其绑定到 InkCanvas。

注释

确保在 InkCanvas 之后声明 InkToolbar。 如果不是,InkCanvas 覆盖层将使 InkToolbar 无法访问。

<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" />
        <InkToolbar x:Name="inkToolbar"
          VerticalAlignment="Top"
          TargetInkCanvas="{x:Bind inkCanvas}" />
    </Grid>
</Grid>

基本自定义

在本部分中,我们将介绍一些基本的 Windows Ink 工具栏自定义方案。

指定位置和方向

将墨迹工具栏添加到应用时,可以接受工具栏的默认位置和方向,或根据应用或用户的需要进行设置。

XAML

使用工具栏的 VerticalAlignmentHorizontalAlignmentOrientation 属性显式指定其位置和方向。

违约 Explicit
默认墨迹工具栏位置和方向 显式墨迹工具栏位置和方向
Windows Ink 工具栏默认位置和方向 Windows Ink 工具栏显式位置和方向

下面是用于在 XAML 中显式设置墨迹工具栏位置和方向的代码。

<InkToolbar x:Name="inkToolbar" 
    VerticalAlignment="Center" 
    HorizontalAlignment="Right" 
    Orientation="Vertical" 
    TargetInkCanvas="{x:Bind inkCanvas}" />

基于用户首选项或设备状态进行初始化

在某些情况下,你可能希望根据用户首选项或设备状态设置墨迹工具栏的位置和方向。 以下示例演示如何根据通过设置>设备>笔和 Windows 墨迹>选择你习惯使用哪只手书写>指定的左右手书写偏好,设置墨迹工具栏的位置和方向。

主导手部设置
主导手部设置

可以通过 Windows.UI.ViewManagement 的 HandPreference 属性查询此设置,并根据返回的值设置 HorizontalAlignment 。 在此示例中,我们将工具栏放在应用程序左撇子的左侧和右撇子的右侧。

下载此示例:Ink 工具栏位置和方向示例(基本)

public MainPage()
{
    this.InitializeComponent();

    Windows.UI.ViewManagement.UISettings settings = 
        new Windows.UI.ViewManagement.UISettings();
    HorizontalAlignment alignment = 
        (settings.HandPreference == 
            Windows.UI.ViewManagement.HandPreference.LeftHanded) ? 
            HorizontalAlignment.Left : HorizontalAlignment.Right;
    inkToolbar.HorizontalAlignment = alignment;
}

动态调整以适应用户或设备状态

还可以使用绑定根据用户首选项、设备设置或设备状态的更改来处理 UI 更新。 在下面的示例中,我们展开了上一个示例,并演示如何使用绑定、ViewMOdel 对象和 INotifyPropertyChanged 接口基于设备方向动态定位墨迹工具栏。

Ink 工具栏位置和方向示例下载此示例(动态)

  1. 首先,让我们添加 ViewModel。

    1. 将新文件夹添加到项目并调用 ViewModels

    2. 将一个新类添加到 ViewModels 文件夹(在本示例中,我们将它称为 InkToolbarSnippetHostViewModel.cs)。

      注释

      我们使用 Singleton 模式,因为在应用程序的生命周期中,我们只需要此类型的一个对象。

    3. 将命名空间添加到 using System.ComponentModel 文件。

    4. 添加名为 Instance 的静态成员变量和名为 Instance 的静态只读属性。 将构造函数设为私有,以确保只能通过 Instance 属性访问此类。

      注释

      此类继承自 INotifyPropertyChanged 接口,该接口用于通知客户端(通常绑定客户端)属性值已更改。 我们将使用它来处理对设备方向的更改(我们将扩展此代码并在后面的步骤中进一步说明)。

      using System.ComponentModel;
      
      namespace locationandorientation.ViewModels
      {
          public class InkToolbarSnippetHostViewModel : INotifyPropertyChanged
          {
              private static InkToolbarSnippetHostViewModel instance;
      
              public static InkToolbarSnippetHostViewModel Instance
              {
                  get
                  {
                      if (null == instance)
                      {
                          instance = new InkToolbarSnippetHostViewModel();
                      }
                      return instance;
                  }
              }
          }
      
          private InkToolbarSnippetHostViewModel() { }
      }
      
    5. 向 InkToolbarSnippetHostViewModel 类添加两个布尔属性: LeftHandedLayout (与上一个仅限 XAML 的示例相同的功能)和 PortraitLayout (设备的方向)。

      注释

      PortraitLayout 属性是可设置的,包括 PropertyChanged 事件的定义。

      public bool LeftHandedLayout
      {
          get
          {
              bool leftHandedLayout = false;
              Windows.UI.ViewManagement.UISettings settings =
                  new Windows.UI.ViewManagement.UISettings();
              leftHandedLayout = (settings.HandPreference ==
                  Windows.UI.ViewManagement.HandPreference.LeftHanded);
              return leftHandedLayout;
          }
      }
      
      public bool portraitLayout = false;
      public bool PortraitLayout
      {
          get
          {
              Windows.UI.ViewManagement.ApplicationViewOrientation winOrientation = 
                  Windows.UI.ViewManagement.ApplicationView.GetForCurrentView().Orientation;
              portraitLayout = 
                  (winOrientation == 
                      Windows.UI.ViewManagement.ApplicationViewOrientation.Portrait);
              return portraitLayout;
          }
          set
          {
              if (value.Equals(portraitLayout)) return;
              portraitLayout = value;
              PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("PortraitLayout"));
          }
      }
      
  2. 现在,让我们向项目添加几个转换器类。 每个类都包含一个 Convert 对象,该对象返回对齐值( HorizontalAlignmentVerticalAlignment)。

    1. 将一个新文件夹添加到项目中,并命名为Converters

    2. 将两个新类添加到 Converters 文件夹(在本示例中,我们将调用它们 HorizontalAlignmentFromHandednessConverter.csVerticalAlignmentFromAppViewConverter.cs)。

    3. 向每个文件添加 using Windows.UI.Xamlusing Windows.UI.Xaml.Data 命名空间。

    4. 将每个类 public 更改为并指定它实现 IValueConverter 接口。

    5. ConvertConvertBack 方法添加到每个文件,如下所示(我们保留未实现 ConvertBack 方法)。

      • HorizontalAlignmentFromHandednessConverter 将墨迹工具栏定位在应用的右侧为右手用户使用,并将其定位在应用的左侧为左手用户使用。
      using System;
      
      using Windows.UI.Xaml;
      using Windows.UI.Xaml.Data;
      
      namespace locationandorientation.Converters
      {
          public class HorizontalAlignmentFromHandednessConverter : IValueConverter
          {
              public object Convert(object value, Type targetType,
                  object parameter, string language)
              {
                  bool leftHanded = (bool)value;
                  HorizontalAlignment alignment = HorizontalAlignment.Right;
                  if (leftHanded)
                  {
                      alignment = HorizontalAlignment.Left;
                  }
                  return alignment;
              }
      
              public object ConvertBack(object value, Type targetType,
                  object parameter, string language)
              {
                  throw new NotImplementedException();
              }
          }
      }
      
      • VerticalAlignmentFromAppViewConverter 在纵向模式下将墨迹工具栏定位在应用中心,横向模式下将其定位在应用顶部(虽然旨在提高可用性,但这只是出于演示目的的任意选择)。
      using System;
      
      using Windows.UI.Xaml;
      using Windows.UI.Xaml.Data;
      
      namespace locationandorientation.Converters
      {
          public class VerticalAlignmentFromAppViewConverter : IValueConverter
          {
              public object Convert(object value, Type targetType,
                  object parameter, string language)
              {
                  bool portraitOrientation = (bool)value;
                  VerticalAlignment alignment = VerticalAlignment.Top;
                  if (portraitOrientation)
                  {
                      alignment = VerticalAlignment.Center;
                  }
                  return alignment;
              }
      
              public object ConvertBack(object value, Type targetType,
                  object parameter, string language)
              {
                  throw new NotImplementedException();
              }
          }
      }
      
  3. 现在,打开MainPage.xaml.cs文件。

    1. using using locationandorientation.ViewModels 添加到命名空间列表中以关联我们的 ViewModel。
    2. 添加using Windows.UI.ViewManagement到命名空间列表中,以启用对设备方向更改的侦听。
    3. 添加 WindowSizeChangedEventHandler 代码。
    4. 将视图的 DataContext 设置为 InkToolbarSnippetHostViewModel 类的单一实例。
    using Windows.UI.Xaml;
    using Windows.UI.Xaml.Controls;
    
    using locationandorientation.ViewModels;
    using Windows.UI.ViewManagement;
    
    namespace locationandorientation
    {
        public sealed partial class MainPage : Page
        {
            public MainPage()
            {
                this.InitializeComponent();
    
                Window.Current.SizeChanged += (sender, args) =>
                {
                    ApplicationView currentView = ApplicationView.GetForCurrentView();
    
                    if (currentView.Orientation == ApplicationViewOrientation.Landscape)
                    {
                        InkToolbarSnippetHostViewModel.Instance.PortraitLayout = false;
                    }
                    else if (currentView.Orientation == ApplicationViewOrientation.Portrait)
                    {
                        InkToolbarSnippetHostViewModel.Instance.PortraitLayout = true;
                    }
                };
    
                DataContext = InkToolbarSnippetHostViewModel.Instance;
            }
        }
    }
    
  4. 接下来,打开 MainPage.xaml 文件。

    1. 添加 xmlns:converters="using:locationandorientation.Converters"Page 元素以绑定到我们的转换器。

      <Page
      x:Class="locationandorientation.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:locationandorientation"
      xmlns:converters="using:locationandorientation.Converters"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      mc:Ignorable="d">
      
    2. 添加一个 PageResources 元素,然后指定转换器的引用。

      <Page.Resources>
          <converters:HorizontalAlignmentFromHandednessConverter x:Key="HorizontalAlignmentConverter"/>
          <converters:VerticalAlignmentFromAppViewConverter x:Key="VerticalAlignmentConverter"/>
      </Page.Resources>
      
    3. 添加 InkCanvas 和 InkToolbar 元素,并绑定 InkToolbar 的 VerticalAlignment 和 HorizontalAlignment 属性。

      <InkCanvas x:Name="inkCanvas" />
      <InkToolbar x:Name="inkToolbar" 
                  VerticalAlignment="{Binding PortraitLayout, Converter={StaticResource VerticalAlignmentConverter} }" 
                  HorizontalAlignment="{Binding LeftHandedLayout, Converter={StaticResource HorizontalAlignmentConverter} }" 
                  Orientation="Vertical" 
                  TargetInkCanvas="{x:Bind inkCanvas}" />
      
  5. 返回到 InkToolbarSnippetHostViewModel.cs 文件,将我们的 PortraitLayoutLeftHandedLayout 布尔属性添加到 InkToolbarSnippetHostViewModel 类中,并添加对在该属性值更改时重新绑定 PortraitLayout 的支持。

    public bool LeftHandedLayout
    {
        get
        {
            bool leftHandedLayout = false;
            Windows.UI.ViewManagement.UISettings settings =
                new Windows.UI.ViewManagement.UISettings();
            leftHandedLayout = (settings.HandPreference ==
                Windows.UI.ViewManagement.HandPreference.LeftHanded);
            return leftHandedLayout;
        }
    }
    
    public bool portraitLayout = false;
    public bool PortraitLayout
    {
        get
        {
            Windows.UI.ViewManagement.ApplicationViewOrientation winOrientation = 
                Windows.UI.ViewManagement.ApplicationView.GetForCurrentView().Orientation;
            portraitLayout = 
                (winOrientation == 
                    Windows.UI.ViewManagement.ApplicationViewOrientation.Portrait);
            return portraitLayout;
        }
        set
        {
            if (value.Equals(portraitLayout)) return;
            portraitLayout = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("PortraitLayout"));
        }
    }
    
    #region INotifyPropertyChanged Members
    
    public event PropertyChangedEventHandler PropertyChanged;
    
    protected void OnPropertyChanged(string property)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
    }
    
    #endregion
    

你现在应该有一个墨迹书写应用,它适应用户的主要手部首选项,并动态响应用户设备的方向。

指定所选按钮

初始化时选择的铅笔按钮
在初始化时选择铅笔按钮的 Windows Ink 工具栏

默认情况下,启动应用并初始化工具栏时,会选择第一个(或最左侧)按钮。 在默认的 Windows Ink 工具栏中,这是圆点笔按钮。

由于框架定义了内置按钮的顺序,因此第一个按钮可能不是默认情况下要激活的笔或工具。

可以重写此默认行为,并在工具栏上指定所选按钮。

在本示例中,我们将使用所选铅笔按钮和铅笔激活(而不是圆点笔)初始化默认工具栏。

  1. 使用上一示例中 InkCanvas 和 InkToolbar 的 XAML 声明。
  2. 在代码隐藏中,为 InkToolbar 对象的 Loaded 事件设置处理程序。
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// Here, we set up InkToolbar event listeners.
/// </summary>
public MainPage_CodeBehind()
{
    this.InitializeComponent();
    // Add handlers for InkToolbar events.
    inkToolbar.Loaded += inkToolbar_Loaded;
}
  1. Loaded 事件的处理程序中:

    1. 获取对内置 InkToolbarPencilButton 的引用。

    GetToolButton 方法中传递 InkToolbarTool.Pencil 对象时,将返回一个关联到 InkToolbarToolButtonInkToolbarPencilButton 对象。

    1. ActiveTool 设置为上一步中返回的对象。
/// <summary>
/// Handle the Loaded event of the InkToolbar.
/// By default, the active tool is set to the first tool on the toolbar.
/// Here, we set the active tool to the pencil button.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void inkToolbar_Loaded(object sender, RoutedEventArgs e)
{
    InkToolbarToolButton pencilButton = inkToolbar.GetToolButton(InkToolbarTool.Pencil);
    inkToolbar.ActiveTool = pencilButton;
}

指定内置按钮

初始化时包含的特定按钮
初始化时包含的特定按钮

如前所述,Windows Ink 工具栏包含默认内置按钮的集合。 这些按钮按以下顺序显示(从左到右):

在本示例中,我们仅使用内置的球点笔、铅笔和橡皮擦按钮初始化工具栏。

可以使用 XAML 或后台代码来完成此操作。

XAML

从第一个示例修改 InkCanvas 和 InkToolbar 的 XAML 声明。

注释

按钮按框架定义的顺序添加到工具栏中,而不是此处指定的顺序。

<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" />
        <!-- Clear the default InkToolbar buttons by setting InitialControls to None. -->
        <!-- Set the active tool to the pencil button. -->
        <InkCanvas x:Name="inkCanvas" />
        <InkToolbar x:Name="inkToolbar"
                    VerticalAlignment="Top"
                    TargetInkCanvas="{x:Bind inkCanvas}"
                    InitialControls="None">
            <!--
             Add only the ballpoint pen, pencil, and eraser.
             Note that the buttons are added to the toolbar in the order
             defined by the framework, not the order we specify here.
            -->
            <InkToolbarEraserButton />
            <InkToolbarBallpointPenButton />
            <InkToolbarPencilButton/>
        </InkToolbar>
    </Grid>
</Grid>

代码隐藏

  1. 使用第一个示例中 InkCanvas 和 InkToolbar 的 XAML 声明。
<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" />
        <InkToolbar x:Name="inkToolbar"
        VerticalAlignment="Top"
        TargetInkCanvas="{x:Bind inkCanvas}" />
    </Grid>
</Grid>
  1. 在代码隐藏中,为 InkToolbar 对象的 Loading 事件设置一个处理程序。
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// Here, we set up InkToolbar event listeners.
/// </summary>
public MainPage_CodeBehind()
{
    this.InitializeComponent();
    // Add handlers for InkToolbar events.
    inkToolbar.Loading += inkToolbar_Loading;
}
  1. InitialControls 设置为“None”。
  2. 为应用所需的按钮创建对象引用。 在这里,我们仅添加 InkToolbarBallpointPenButtonInkToolbarPencilButtonInkToolbarEraserButton

注释

按钮按框架定义的顺序添加到工具栏中,而不是此处指定的顺序。

  1. 将按钮添加到 InkToolbar。
/// <summary>
/// Handles the Loading event of the InkToolbar.
/// Here, we identify the buttons to include on the InkToolbar.
/// </summary>
/// <param name="sender">The InkToolbar</param>
/// <param name="args">The InkToolbar event data.
/// If there is no event data, this parameter is null</param>
private void inkToolbar_Loading(FrameworkElement sender, object args)
{
    // Clear all built-in buttons from the InkToolbar.
    inkToolbar.InitialControls = InkToolbarInitialControls.None;

    // Add only the ballpoint pen, pencil, and eraser.
    // Note that the buttons are added to the toolbar in the order
    // defined by the framework, not the order we specify here.
    InkToolbarBallpointPenButton ballpoint = new InkToolbarBallpointPenButton();
    InkToolbarPencilButton pencil = new InkToolbarPencilButton();
    InkToolbarEraserButton eraser = new InkToolbarEraserButton();
    inkToolbar.Children.Add(eraser);
    inkToolbar.Children.Add(ballpoint);
    inkToolbar.Children.Add(pencil);
}

自定义按钮和墨迹书写功能

可以自定义和扩展通过 InkToolbar 提供的按钮(和相关墨迹书写功能)的集合。

InkToolbar 由两组不同的按钮类型组成:

  1. “工具”按钮组包含内置绘图、擦除和突出显示按钮。 此处添加了自定义笔和工具。

注意 功能选择是相互排斥的。

  1. 一组包含内置标尺按钮的“切换”按钮。 自定义切换按钮将添加到该组。

注意 功能不是相互排斥的,可以与其他活动工具同时使用。

根据应用程序和所需的墨迹书写功能,可以将以下任意按钮(与自定义墨迹功能绑定)添加到 InkToolbar:

  • 自定义笔 - 由主机定义其墨迹调色板和笔尖属性(如形状、旋转和大小)的笔。
  • 自定义工具 - 由主机应用定义的非笔工具。
  • 自定义切换 - 将应用定义功能的状态设置为打开或关闭。 启用后,该功能可与活动工具结合使用。

注意 无法更改内置按钮的显示顺序。 默认显示顺序为:圆珠笔、铅笔、荧光笔、橡皮擦和标尺。 自定义笔将追加到最后一个默认笔,在最后一个笔按钮和橡皮擦按钮之间添加自定义工具按钮,并在标尺按钮后添加自定义切换按钮。 (自定义按钮按指定的顺序添加。)

自定义笔

可以创建自定义笔(通过自定义笔按钮激活),可在其中定义墨迹调色板和笔尖属性,例如形状、旋转和大小。

自定义书法笔按钮
自定义书法笔按钮

在本示例中,我们定义了一个自定义笔,并配有一个宽笔尖,便于绘制基本的书法笔画。 我们还自定义按钮浮出控件上显示的调色板中的画笔集合。

代码隐藏

首先,我们定义自定义笔,并在 code-behind 中指定绘图属性。 稍后我们将从 XAML 引用此自定义笔。

  1. 右键单击解决方案资源管理器中的项目,然后选择“添加 -> 新建”项。
  2. 在 Visual C# -> 代码下,添加新的类文件并将其调用CalligraphicPen.cs。
  3. 在Calligraphic.cs中,将默认使用块替换为以下内容:
using System.Numerics;
using Windows.UI;
using Windows.UI.Input.Inking;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
  1. 指定 CalligraphicPen 类派生自 InkToolbarCustomPen
class CalligraphicPen : InkToolbarCustomPen
{
}
  1. 重写 CreateInkDrawingAttributesCore 以指定自己的画笔和笔划大小。
class CalligraphicPen : InkToolbarCustomPen
{
    protected override InkDrawingAttributes
      CreateInkDrawingAttributesCore(Brush brush, double strokeWidth)
    {
    }
}
  1. 创建InkDrawingAttributes对象,并设置笔尖形状笔尖旋转笔划大小墨迹颜色
class CalligraphicPen : InkToolbarCustomPen
{
    protected override InkDrawingAttributes
      CreateInkDrawingAttributesCore(Brush brush, double strokeWidth)
    {
        InkDrawingAttributes inkDrawingAttributes =
          new InkDrawingAttributes();
        inkDrawingAttributes.PenTip = PenTipShape.Circle;
        inkDrawingAttributes.Size =
          new Windows.Foundation.Size(strokeWidth, strokeWidth * 20);
        SolidColorBrush solidColorBrush = brush as SolidColorBrush;
        if (solidColorBrush != null)
        {
            inkDrawingAttributes.Color = solidColorBrush.Color;
        }
        else
        {
            inkDrawingAttributes.Color = Colors.Black;
        }

        Matrix3x2 matrix = Matrix3x2.CreateRotation(45);
        inkDrawingAttributes.PenTipTransform = matrix;

        return inkDrawingAttributes;
    }
}

XAML

接下来,我们在 MainPage.xaml 中添加对自定义笔的必要引用。

  1. 我们声明了一个本地页面资源字典,该字典创建对CalligraphicPen.cs中定义的自定义笔(CalligraphicPen)的引用,以及自定义笔支持的 画笔集合CalligraphicPenPalette)。
<Page.Resources>
    <!-- Add the custom CalligraphicPen to the page resources. -->
    <local:CalligraphicPen x:Key="CalligraphicPen" />
    <!-- Specify the colors for the palette of the custom pen. -->
    <BrushCollection x:Key="CalligraphicPenPalette">
        <SolidColorBrush Color="Blue" />
        <SolidColorBrush Color="Red" />
    </BrushCollection>
</Page.Resources>
  1. 然后添加一个包含子元素InkToolbarCustomPenButton的InkToolbar。

自定义笔按钮包括页面资源中声明的两个静态资源引用: CalligraphicPenCalligraphicPenPalette

我们还指定笔划大小滑块的范围(MinStrokeWidth、MaxStrokeWidthSelectedStrokeWidth)、所选画笔(SelectedBrushIndex)和自定义笔按钮图标(SymbolIcon)。

<Grid Grid.Row="1">
    <InkCanvas x:Name="inkCanvas" />
    <InkToolbar x:Name="inkToolbar"
                VerticalAlignment="Top"
                TargetInkCanvas="{x:Bind inkCanvas}">
        <InkToolbarCustomPenButton
            CustomPen="{StaticResource CalligraphicPen}"
            Palette="{StaticResource CalligraphicPenPalette}"
            MinStrokeWidth="1" MaxStrokeWidth="3" SelectedStrokeWidth="2"
            SelectedBrushIndex ="1">
            <SymbolIcon Symbol="Favorite" />
            <InkToolbarCustomPenButton.ConfigurationContent>
                <InkToolbarPenConfigurationControl />
            </InkToolbarCustomPenButton.ConfigurationContent>
        </InkToolbarCustomPenButton>
    </InkToolbar>
</Grid>

自定义切换

可以创建自定义切换(通过自定义切换按钮激活)以将应用定义功能的状态设置为打开或关闭。 启用后,该功能可与活动工具结合使用。

在此示例中,我们定义了一个自定义切换控件,用于启用触摸输入的墨迹书写功能(默认情况下,此功能未启用)。

注释

如果需要支持触控书写,我们建议您使用 CustomToggleButton 启用功能,并按照此示例指定图标和 工具提示

通常,触摸输入用于直接操控对象或应用程序用户界面。 为了演示启用触摸墨迹书写时的行为差异,我们将 InkCanvas 放置在 ScrollViewer 容器中,并将 ScrollViewer 的尺寸设置为小于 InkCanvas。

应用启动时,仅支持笔墨迹书写,触摸用于平移或缩放墨迹书写图面。 启用触摸墨迹功能后,不能通过触摸输入来平移或缩放墨迹表面。

注释

参见 墨迹控件,了解 InkCanvasInkToolbar 的 UX 指南。 以下建议与此示例相关:

  • InkToolbar 和墨迹书写通常最好通过活动笔进行体验。 但是,如果应用需要,则可以支持使用鼠标和触碰进行墨迹书写。
  • 如果支持使用触摸输入进行墨迹书写,我们建议使用来自“Segoe MLD2 Assets”字体的“ED5F”图标作为切换按钮,并附带“触摸书写”提示信息。

XAML

  1. 首先,我们使用 Click 事件侦听器声明 InkToolbarCustomToggleButton 元素(toggleButton),该侦听器指定事件处理程序(Toggle_Custom)。
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>

    <StackPanel Grid.Row="0" 
                x:Name="HeaderPanel" 
                Orientation="Horizontal">
        <TextBlock x:Name="Header" 
                   Text="Basic ink sample" 
                   Style="{ThemeResource HeaderTextBlockStyle}" 
                   Margin="10" />
    </StackPanel>

    <ScrollViewer Grid.Row="1" 
                  HorizontalScrollBarVisibility="Auto" 
                  VerticalScrollBarVisibility="Auto">
        
        <Grid HorizontalAlignment="Left" VerticalAlignment="Top">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>
            
            <InkToolbar Grid.Row="0" 
                        Margin="10"
                        x:Name="inkToolbar" 
                        VerticalAlignment="Top"
                        TargetInkCanvas="{x:Bind inkCanvas}">
                <InkToolbarCustomToggleButton 
                x:Name="toggleButton" 
                Click="CustomToggle_Click" 
                ToolTipService.ToolTip="Touch Writing">
                    <SymbolIcon Symbol="{x:Bind TouchWritingIcon}"/>
                </InkToolbarCustomToggleButton>
            </InkToolbar>
            
            <ScrollViewer Grid.Row="1" 
                          Height="500"
                          Width="500"
                          x:Name="scrollViewer" 
                          ZoomMode="Enabled" 
                          MinZoomFactor=".1" 
                          VerticalScrollMode="Enabled" 
                          VerticalScrollBarVisibility="Auto" 
                          HorizontalScrollMode="Enabled" 
                          HorizontalScrollBarVisibility="Auto">
                
                <Grid x:Name="outputGrid" 
                      Height="1000"
                      Width="1000"
                      Background="{ThemeResource SystemControlBackgroundChromeWhiteBrush}">
                    <InkCanvas x:Name="inkCanvas"/>
                </Grid>
                
            </ScrollViewer>
        </Grid>
    </ScrollViewer>
</Grid>

代码隐藏

  1. 在前面的代码片段中,我们在自定义切换按钮上声明了一个 Click 事件侦听器和处理程序(Toggle_Custom),用于触摸墨迹书写(toggleButton)。 此处理程序只需通过 InkPresenter 的 InputDeviceTypes 属性切换对 CoreInputDeviceTypes.Touch 的支持。

    我们还使用 SymbolIcon 元素和 {x:Bind} 标记扩展为按钮指定了一个图标,该扩展将其绑定到代码隐藏文件中定义的字段(TouchWritingIcon)。

    以下代码片段包括 Click 事件处理程序和 TouchWritingIcon 的定义。

namespace Ink_Basic_InkToolbar
{
    /// <summary>
    /// An empty page that can be used on its own or navigated to within a Frame.
    /// </summary>
    public sealed partial class MainPage_AddCustomToggle : Page
    {
        Symbol TouchWritingIcon = (Symbol)0xED5F;

        public MainPage_AddCustomToggle()
        {
            this.InitializeComponent();
        }

        // Handler for the custom toggle button that enables touch inking.
        private void CustomToggle_Click(object sender, RoutedEventArgs e)
        {
            if (toggleButton.IsChecked == true)
            {
                inkCanvas.InkPresenter.InputDeviceTypes |= CoreInputDeviceTypes.Touch;
            }
            else
            {
                inkCanvas.InkPresenter.InputDeviceTypes &= ~CoreInputDeviceTypes.Touch;
            }
        }
    }
}

自定义工具

可以创建自定义工具按钮来调用应用定义的非笔工具。

默认情况下, InkPresenter 将所有输入作为墨迹笔划或擦除笔划进行处理。 这包括由辅助硬件提供(如笔杆按钮、鼠标右键或类似按键)修改的输入。 但是,可以将 InkPresenter 配置为保留未处理的特定输入,然后可以传递到应用进行自定义处理。

在此示例中,我们定义了一个自定义工具按钮,当该按钮被选中时,随后的笔划会被处理并呈现为选择套索(虚线)而非墨迹。 所选区域边界内的所有墨迹笔划都设置为 “已选中”。

注释

请参阅 InkCanvas 和 InkToolbar UX 指南的墨迹书写控件。 以下建议与此示例相关:

  • 如果提供笔划选择,建议使用工具按钮的“Segoe MLD2 Assets”字体中的“EF20”图标和“选择工具”工具提示。

XAML

  1. 首先,我们声明 InkToolbarCustomToolButton 元素(customToolButton),并为其附加一个 Click 事件侦听器,用以指定配置笔划选择的事件处理程序(customToolButton_Click)。 我们还添加了一组按钮,用于复制、剪切和粘贴选择的内容。)

  2. 我们还添加一个 Canvas 元素来绘制所选笔划。 使用单独的层绘制选择笔划可确保 InkCanvas 及其内容保持不变。

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <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>
    <StackPanel x:Name="ToolPanel" Orientation="Horizontal" Grid.Row="1">
        <InkToolbar x:Name="inkToolbar" 
                    VerticalAlignment="Top" 
                    TargetInkCanvas="{x:Bind inkCanvas}">
            <InkToolbarCustomToolButton 
                x:Name="customToolButton" 
                Click="customToolButton_Click" 
                ToolTipService.ToolTip="Selection tool">
                <SymbolIcon Symbol="{x:Bind SelectIcon}"/>
            </InkToolbarCustomToolButton>
        </InkToolbar>
        <Button x:Name="cutButton" 
                Content="Cut" 
                Click="cutButton_Click"
                Width="100"
                Margin="5,0,0,0"/>
        <Button x:Name="copyButton" 
                Content="Copy"  
                Click="copyButton_Click"
                Width="100"
                Margin="5,0,0,0"/>
        <Button x:Name="pasteButton" 
                Content="Paste"  
                Click="pasteButton_Click"
                Width="100"
                Margin="5,0,0,0"/>
    </StackPanel>
    <Grid Grid.Row="2" x:Name="outputGrid" 
              Background="{ThemeResource SystemControlBackgroundChromeWhiteBrush}" 
              Height="Auto">
        <!-- Canvas for displaying selection UI. -->
        <Canvas x:Name="selectionCanvas"/>
        <!-- Canvas for displaying ink. -->
        <InkCanvas x:Name="inkCanvas" />
    </Grid>
</Grid>

代码隐藏

  1. 然后,在 MainPage.xaml.cs 代码隐藏文件中处理 InkToolbarCustomToolButton 的 Click 事件。

    此处理程序将 InkPresenter 配置为将未处理的输入传递到应用。

    有关此代码的更详细步骤:请参阅 Windows 应用中笔交互和 Windows Ink 的高级处理部分的直通输入。

    我们还使用 SymbolIcon 元素和 {x:Bind} 标记扩展为按钮指定了一个图标,该扩展将其绑定到代码隐藏文件中定义的字段(SelectIcon)。

    以下代码片段包括 Click 事件处理程序和 SelectIcon 的定义。

namespace Ink_Basic_InkToolbar
{
    /// <summary>
    /// An empty page that can be used on its own or navigated to within a Frame.
    /// </summary>
    public sealed partial class MainPage_AddCustomTool : Page
    {
        // Icon for custom selection tool button.
        Symbol SelectIcon = (Symbol)0xEF20;

        // Stroke selection tool.
        private Polyline lasso;
        // Stroke selection area.
        private Rect boundingRect;

        public MainPage_AddCustomTool()
        {
            this.InitializeComponent();

            // Listen for new ink or erase strokes to clean up selection UI.
            inkCanvas.InkPresenter.StrokeInput.StrokeStarted +=
                StrokeInput_StrokeStarted;
            inkCanvas.InkPresenter.StrokesErased +=
                InkPresenter_StrokesErased;
        }

        private void customToolButton_Click(object sender, RoutedEventArgs e)
        {
            // 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;
        }

        // 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();
        }

        private void cutButton_Click(object sender, RoutedEventArgs e)
        {
            inkCanvas.InkPresenter.StrokeContainer.CopySelectedToClipboard();
            inkCanvas.InkPresenter.StrokeContainer.DeleteSelected();
            ClearSelection();
        }

        private void copyButton_Click(object sender, RoutedEventArgs e)
        {
            inkCanvas.InkPresenter.StrokeContainer.CopySelectedToClipboard();
        }

        private void pasteButton_Click(object sender, RoutedEventArgs e)
        {
            if (inkCanvas.InkPresenter.StrokeContainer.CanPasteFromClipboard())
            {
                inkCanvas.InkPresenter.StrokeContainer.PasteFromClipboard(
                    new Point(0, 0));
            }
            else
            {
                // Cannot paste from clipboard.
            }
        }

        // Clean up selection UI.
        private void ClearSelection()
        {
            var strokes = inkCanvas.InkPresenter.StrokeContainer.GetStrokes();
            foreach (var stroke in strokes)
            {
                stroke.Selected = false;
            }
            ClearBoundingRect();
        }

        private void ClearBoundingRect()
        {
            if (selectionCanvas.Children.Any())
            {
                selectionCanvas.Children.Clear();
                boundingRect = Rect.Empty;
            }
        }

        // 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();
        }

        // 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);
            }
        }
    }
}

自定义墨迹呈现

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

通过墨迹平台,你可以覆盖此行为,并通过自定义干涸墨水输入来完全定制墨迹书写体验。

有关自定义干燥的详细信息,请参阅 Windows 应用中的笔交互和 Windows Ink

注释

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

主题示例

其他示例