Элементы управления рукописным вводом для приложений WinUI

Important

InkCanvas, InkToolbarи InkPresenter являются экспериментальными API. Поддержка рукописного ввода была добавлена в Windows App SDK 2.4.1-experimental. В этой статье описывается Windows App SDK 2.5.4 (experimental), который изменил способ подключения InkToolbar. Эти API недоступны в стабильном канале. Они могут изменяться или удаляться в будущем выпуске. Не используйте их в продакшен-приложениях.

Используйте экспериментальный InkCanvas элемент управления, чтобы пользователи рисовали рукописный ввод с помощью пера, мыши или сенсорного ввода. Добавьте InkToolbar, чтобы предоставить пользователям шариковую ручку, карандаш, маркер и ластик с параметрами цвета и размера. Каждый холст имеет элемент InkPresenter, который собирает и отрисовывает данные рукописного ввода. Средство отображения InkStrokeContainer хранит штрихи, поэтому можно сохранять, загружать, копировать и создавать рукописный ввод программно.

Чтобы попробовать элементы управления, установите пакет Windows App SDK 2.5.4-experimental и соответствующую среду выполнения. Ссылки на скачивание и сведения о настройке см. в разделе Последние загрузки Windows App SDK.

Это правильный контроль?

Используйте рукописный холст, когда пользователям необходимо:

  • Берите рукописные заметки или аннотируйте содержимое.
  • Создавать эскизы, рисовать схемы или подписывать своё имя.
  • Создайте рукописный контент, который сохраняется в приложении и который можно редактировать позже.

Экспериментальные API цифровых чернил не включают распознавание рукописного ввода или фигур. Чтобы ввести текст, используйте текстовое поле.

Если вам требуется рукописный ввод в рабочем приложении, ознакомьтесь с известными пробелами в элементах управления, чтобы узнать о состоянии этих элементов управления в стабильном канале и об альтернативных вариантах.

Понимание модели цифровых чернил

API цифровых чернил работают вместе:

API Role
InkCanvas Элемент XAML, отображающий рукописный ввод и принимающий рукописный ввод. Его InkPresenter свойство возвращает презентер для холста.
InkPresenter Собирает входные данные ввода цифровыми чернилами, отрисовывает штрихи и содержит входные параметры и атрибуты рисования по умолчанию. Вы не можете самостоятельно создать InkPresenter. Получите его с InkCanvas.InkPresenter.
InkStrokeContainer Хранит штрихи. Используйте его для сохранения, загрузки, выбора, копирования и вставки рукописного ввода. Получите его с InkPresenter.StrokeContainer.
InkToolbar Необязательная панель инструментов, которая позволяет пользователям выбирать перо, карандаш, маркер или ластик, а также изменять цвет и размер рукописного ввода.

InkCanvas не имеет свойства Background. Чтобы предоставить области рисования фон и границу, поместите холст внутри контейнера, например Border, и задайте размер контейнера. Цвет рукописного ввода по умолчанию черный, поэтому в примерах в этой статье используется белый фон.

Некоторые из этих типов имеют те же имена, что и типы в пространстве имен среда выполнения Windows Windows.UI.Input.Inking, например InkPresenter, InkStrokeContainer и InkHighContrastAdjustment. Элементы управления рукописным вводом используют Microsoft.UI.Xaml.Controls версии. Штрихи и атрибуты рисования по-прежнему используют типы среда выполнения Windows, например InkStroke, InkDrawingAttributesи InkStrokeBuilder. При импорте обоих пространств имен и использовании неоднозначного имени компилятор C# сообщает об ошибке CS0104.

Примеры, приведенные в этой статье, импортируют Microsoft.UI.Xaml.Controls и используют директивы псевдонима для Windows.UI.Input.Inking необходимых типов:

using Microsoft.UI.Xaml.Controls;
using InkDrawingAttributes = Windows.UI.Input.Inking.InkDrawingAttributes;
using InkStroke = Windows.UI.Input.Inking.InkStroke;

Вы также можете импортировать оба пространства имен и использовать полные имена для неоднозначных типов, как это делает пример WinUI 3 Gallery.

InkPresenter также предоставляет ИНТЕРФЕЙСы API нижнего уровня, которые эта статья не охватывает:

  • StrokeInput вызывает события, когда штрих начинается, продолжается, завершается или отменяется.
  • UnprocessedInput вызывает события указателя для входных данных, которые элемент представления не обрабатывает как рукописный ввод.
  • InputConfiguration Задает, обрабатывает ли указатель входные данные от ластика пера и основной боковой кнопки.
  • InputProcessingConfiguration Задает, приводит ли ввод к рисованию рукописного ввода, удалению рукописного ввода или не обрабатывается, а также как презентатор обрабатывает ввод при перетаскивании правой кнопкой мыши.
  • ActivateCustomDrying возвращает InkSynchronizer для приложений, которые самостоятельно отрисовывают завершенный рукописный ввод.

Настройте ваш проект

API цифровых чернил помечены как экспериментальные. При использовании их в проекте C# компилятор C# сообщает предупреждение CS8305 и предупреждение компилятора XAML WMC1501. Если проект обрабатывает предупреждения как ошибки, сборка останавливается из-за этих предупреждений.

После того как вы решите использовать экспериментальные API, вы можете отключить эти предупреждения. Добавьте следующие свойства в файл проекта C# (CSPROJ):

<PropertyGroup>
  <NoWarn>$(NoWarn);CS8305</NoWarn>
  <MSBuildWarningsAsMessages>$(MSBuildWarningsAsMessages);WMC1501</MSBuildWarningsAsMessages>
</PropertyGroup>

В этой статье используются примеры на C#. Каждый полный пример показывает XAML для страницы и класса программной части страницы, MainPage.

Создать область рукописного ввода

Значок коллекции WinUI 3 Приложение WinUI 3 Gallery содержит интерактивные примеры элементов управления и функций WinUI. Получите приложение из Microsoft Store или просмотрите исходный код в GitHub.

В следующем примере холст помещается внутрь белого Border. Код программной части задает типы устройств ввода, чтобы пользователи могли рисовать с помощью пера, мыши или сенсорного ввода.

<Border
    Height="300"
    Background="White"
    BorderBrush="{ThemeResource SystemControlForegroundChromeHighBrush}"
    BorderThickness="1">
    <InkCanvas x:Name="inkCanvas" AutomationProperties.Name="Drawing surface" />
</Border>
using Microsoft.UI.Xaml.Controls;
using Windows.UI.Core;

public sealed partial class MainPage : Page
{
    public MainPage()
    {
        InitializeComponent();

        // Mouse input doesn't draw ink by default. Choose every device type that can draw ink.
        inkCanvas.InkPresenter.InputDeviceTypes =
            CoreInputDeviceTypes.Pen | CoreInputDeviceTypes.Mouse | CoreInputDeviceTypes.Touch;
    }
}

Чтобы сохранить их короткими, некоторые более поздние примеры не повторяют этот код. Задайте InputDeviceTypes на каждой странице, где пользователи рисуют мышью.

Подключите панель инструментов рукописного ввода

Поместите рядом с холстом InkToolbar и задайте его свойству TargetInkCanvas значение, чтобы соединить их:

<StackPanel Spacing="8">
    <InkToolbar
        x:Name="inkToolbar"
        AutomationProperties.Name="Drawing tools"
        TargetInkCanvas="{x:Bind inkCanvas}" />
    <Border
        Height="300"
        Background="White"
        BorderBrush="{ThemeResource SystemControlForegroundChromeHighBrush}"
        BorderThickness="1">
        <InkCanvas x:Name="inkCanvas" AutomationProperties.Name="Drawing surface" />
    </Border>
</StackPanel>

Начиная с Windows App SDK 2.5.4-experimental, можно вместо этого подключить панель инструментов непосредственно к InkPresenter, задав свойство TargetInkPresenter. Задайте его в коде:

inkToolbar.TargetInkPresenter = inkCanvas.InkPresenter;

Его также можно задать в XAML: TargetInkPresenter="{x:Bind inkCanvas.InkPresenter}". Задайте только одно из двух целевых свойств.

По умолчанию на панели инструментов отображаются кнопки пера, карандаша, выделения, ластика и трафарета (линейка). Чтобы изменить отображаемые кнопки, см. статью Настройка панели инструментов рукописного ввода.

Выберите устройства ввода

Свойство InputDeviceTypes использует CoreInputDeviceTypes перечисление флагов из Windows.UI.Core пространства имен. Ввод мышью по умолчанию не позволяет рисовать рукописные штрихи. Объедините значения, чтобы выбрать, какие устройства используют рукописный ввод:

// Add mouse input and keep the other device types.
inkCanvas.InkPresenter.InputDeviceTypes |= CoreInputDeviceTypes.Mouse;

// Stop accepting ink from touch input.
inkCanvas.InkPresenter.InputDeviceTypes &= ~CoreInputDeviceTypes.Touch;

// Check whether pen input is enabled.
bool isPenEnabled = inkCanvas.InkPresenter.InputDeviceTypes.HasFlag(CoreInputDeviceTypes.Pen);

Чтобы прекратить сбор рукописного ввода с каждого устройства, например, чтобы отобразить рисунок, который пользователи не могут изменить, установите IsInputEnabled в значение false:

inkCanvas.InkPresenter.IsInputEnabled = false;

Чтобы пользователи могли включать или выключать устройство ввода на панели инструментов, см. статью "Добавить настраиваемую кнопку переключателя".

Задать атрибуты рисования

Атрибуты рисования, такие как цвет, размер и фигура кончика пера, управляют тем, как выглядят штрихи. Они используют класс среда выполнения WindowsInkDrawingAttributes. Чтобы изменить атрибуты для новых штрихов, вызовите CopyDefaultDrawingAttributes, измените копию и передайте её в UpdateDefaultDrawingAttributes:

InkDrawingAttributes attributes = inkCanvas.InkPresenter.CopyDefaultDrawingAttributes();
attributes.Color = Colors.DarkBlue;
attributes.Size = new Size(4, 4);
inkCanvas.InkPresenter.UpdateDefaultDrawingAttributes(attributes);

Colors находится в Microsoft.UI пространстве имен и Size находится в Windows.Foundation пространстве имен. Новые атрибуты применяются к штрихам, которые пользователи рисуют после звонка. Существующие штрихи сохраняют свои атрибуты.

При подключении InkToolbar активный инструмент и его параметры цвета и размера определяют атрибуты рисования. Чтобы прочитать текущие атрибуты, используйте свойство InkDrawingAttributes панели инструментов.

Создать предустановки пера

Если вы не используете панель инструментов, вы можете настроить собственные предустановки пера. В следующем примере применяются атрибуты шариковой ручки, карандаша, маркера или каллиграфии, когда пользователь выбирает предустановку или изменяет размер.

<StackPanel Spacing="12">
    <RadioButtons
        x:Name="presetButtons"
        Header="Pen preset"
        MaxColumns="4"
        SelectedIndex="0"
        SelectionChanged="PresetButtons_SelectionChanged">
        <x:String>Ballpoint</x:String>
        <x:String>Pencil</x:String>
        <x:String>Highlighter</x:String>
        <x:String>Calligraphy</x:String>
    </RadioButtons>
    <Slider
        x:Name="sizeSlider"
        Width="240"
        HorizontalAlignment="Left"
        Header="Size"
        Maximum="16"
        Minimum="1"
        Value="4"
        ValueChanged="SizeSlider_ValueChanged" />
    <Border
        Width="480"
        Height="280"
        HorizontalAlignment="Left"
        Background="White"
        BorderBrush="{ThemeResource SystemControlForegroundChromeHighBrush}"
        BorderThickness="1">
        <InkCanvas x:Name="inkCanvas" AutomationProperties.Name="Drawing surface" />
    </Border>
</StackPanel>
using System;
using System.Numerics;
using Microsoft.UI;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Windows.Foundation;
using Windows.UI.Core;
using InkDrawingAttributes = Windows.UI.Input.Inking.InkDrawingAttributes;
using PenTipShape = Windows.UI.Input.Inking.PenTipShape;

public sealed partial class MainPage : Page
{
    public MainPage()
    {
        InitializeComponent();

        inkCanvas.InkPresenter.InputDeviceTypes =
            CoreInputDeviceTypes.Pen | CoreInputDeviceTypes.Mouse | CoreInputDeviceTypes.Touch;
        ApplyPreset();
    }

    private void PresetButtons_SelectionChanged(object sender, SelectionChangedEventArgs e) => ApplyPreset();

    private void SizeSlider_ValueChanged(object sender, RangeBaseValueChangedEventArgs e) => ApplyPreset();

    private void ApplyPreset()
    {
        // SelectionChanged and ValueChanged can occur while the page is still loading.
        if (inkCanvas is null || presetButtons is null || sizeSlider is null)
        {
            return;
        }

        double size = sizeSlider.Value;
        InkDrawingAttributes attributes;

        switch (presetButtons.SelectedItem as string)
        {
            case "Pencil":
                // CreateForPencil returns attributes for pencil-style strokes.
                attributes = InkDrawingAttributes.CreateForPencil();
                attributes.Color = Colors.DimGray;
                attributes.Size = new Size(size, size);
                break;

            case "Highlighter":
                attributes = new InkDrawingAttributes
                {
                    Color = Colors.Yellow,
                    DrawAsHighlighter = true,
                    PenTip = PenTipShape.Rectangle,
                    Size = new Size(size, size * 3),
                };
                break;

            case "Calligraphy":
                attributes = new InkDrawingAttributes
                {
                    Color = Colors.DarkBlue,
                    PenTip = PenTipShape.Rectangle,
                    Size = new Size(Math.Max(1, size / 2), size * 2),
                    // Rotate the nib 45 degrees so the stroke width changes with the stroke direction.
                    PenTipTransform = Matrix3x2.CreateRotation(MathF.PI / 4),
                };
                break;

            default:
                attributes = new InkDrawingAttributes
                {
                    Color = Colors.Black,
                    PenTip = PenTipShape.Circle,
                    Size = new Size(size, size),
                };
                break;
        }

        // The new attributes apply to strokes that the user draws after this call.
        inkCanvas.InkPresenter.UpdateDefaultDrawingAttributes(attributes);
    }
}

Параметры предустановки пера для Ballpoint, Карандаша, Маркера и Каллиграфии, с выбранным параметром «Каллиграфия», над ползунком «Размер» и белым холстом. На холсте показаны петлеобразные штрихи, выполненные черной шариковой ручкой, серым карандашом, желтым маркером и темно-синей каллиграфией.

  • InkDrawingAttributes.CreateForPencil возвращает атрибуты для штрихов карандаша. Свойство Kind доступно только для чтения, поэтому используйте этот метод для создания атрибутов стилуса.
  • DrawAsHighlighter Отрисовывает обводку как наложение маркера.
  • PenTip задает фигуру кончика пера и PenTipTransform поворачивает или масштабирует его. Повернутый прямоугольный кончик создает штрихи в стиле каллиграфии.

Реагирование на изменения цифровых чернил

Средство представления вызывает StrokesCollected после того, как пользователь рисует штрихи, и StrokesErased после того, как пользователь стирает их. Свойство Strokes аргументов каждого события содержит затронутые росчерки. Следующий код поддерживает актуальное количество штрихов в strokeCountText с именем TextBlock:

inkCanvas.InkPresenter.StrokesCollected += (sender, args) => UpdateStrokeCount();
inkCanvas.InkPresenter.StrokesErased += (sender, args) => UpdateStrokeCount();
private void UpdateStrokeCount()
{
    int count = inkCanvas.InkPresenter.StrokeContainer.GetStrokes().Count;
    strokeCountText.Text = $"{count} strokes";
}

Эти события происходят при рисовании или удалении рукописного ввода пользователями. Когда вы изменяете штрихи в коде, например с помощью Clear, LoadAsync или AddStroke, обновите пользовательский интерфейс после вызова.

Сохранить и загрузить рукописный ввод

InkStrokeContainer сохраняет рукописный ввод в сериализованном формате рукописного ввода (ISF). Чтобы выбрать способ хранения данных, передайте значение InkPersistenceFormat в SaveAsync:

Format Output
GifWithEmbeddedIsf Изображение GIF, содержащее данные рукописного ввода. Средства просмотра изображений могут отображать файл, и приложение может снова загрузить рукописный ввод для редактирования.
Isf Только рукописные данные без изображения. Это самый компактный формат.

Добавьте следующие методы в кодовую часть страницы, содержащей элемент InkCanvas с именем inkCanvas. SaveInkAsync записывает рукописный ввод в файл, и LoadInkAsync заменяет рукописный ввод на холсте рукописным вводом из файла. LoadAsync удаляет существующие штрихи перед добавлением загруженных штрихов, поэтому сначала не нужно вызывать Clear .

using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.UI.Xaml.Controls;
using Windows.Storage.Streams;
using InkPersistenceFormat = Windows.UI.Input.Inking.InkPersistenceFormat;

public sealed partial class MainPage : Page
{
    private async Task SaveInkAsync(string path)
    {
        InkStrokeContainer container = inkCanvas.InkPresenter.StrokeContainer;

        // File.Create replaces any existing file, so the file contains only the new ink.
        using FileStream fileStream = File.Create(path);
        using IOutputStream output = fileStream.AsOutputStream();

        // GifWithEmbeddedIsf writes a GIF image that also contains the editable ink data.
        await container.SaveAsync(output, InkPersistenceFormat.GifWithEmbeddedIsf);
        await output.FlushAsync();
    }

    private async Task LoadInkAsync(string path)
    {
        InkStrokeContainer container = inkCanvas.InkPresenter.StrokeContainer;

        using FileStream fileStream = File.OpenRead(path);
        using IInputStream input = fileStream.AsInputStream();

        // LoadAsync removes the current strokes before it adds the loaded strokes.
        await container.LoadAsync(input);
    }
}

AsOutputStream и AsInputStream преобразуют потоки .NET в типы потоков среда выполнения Windows, которые используют SaveAsync и LoadAsync. Чтобы ожидать эти методы среда выполнения Windows, файлу требуется директиваusing System;. Без него компилятор сообщает об ошибке CS4036.

Чтобы разрешить пользователям выбирать файл, используйте средства выбора Windows App SDK файлов, возвращающие путь выбранного файла. Дополнительные сведения см. в статье "Сохранение файла с помощью средства выбора Windows App SDK" и "Открытие файлов и папок с помощью средства выбора Windows App SDK".

Чтобы сохранить рукописный ввод в памяти, например восстановить его позже в том же сеансе, сохраните его в объект InMemoryRandomAccessStream. Передайте результат потока SaveAsync в GetOutputStreamAt(0), а его результат GetInputStreamAt(0) — в LoadAsync. В примере WinUI 3 Gallery используется этот подход.

Копирование и вставка рукописного ввода

InkStrokeContainer может скопировать штрихи в буфер обмена и вставить рукописный ввод из него. CopySelectedToClipboard копирует только выбранные штрихи, поэтому следующий пример выбирает каждый штрих на левом холсте перед их копированием. CanPasteFromClipboard проверяет, содержит ли буфер обмена рукописный ввод. PasteFromClipboard добавляет штрих в положение, которое вы передаете, и возвращает ограничивающий прямоугольник области, которая изменилась.

<StackPanel Spacing="12">
    <StackPanel Orientation="Horizontal" Spacing="8">
        <Button Click="CopyButton_Click" Content="Copy left canvas" />
        <Button
            x:Name="pasteButton"
            Click="PasteButton_Click"
            Content="Paste into right canvas"
            IsEnabled="False" />
    </StackPanel>
    <StackPanel Orientation="Horizontal" Spacing="12">
        <Border
            Width="300"
            Height="220"
            Background="White"
            BorderBrush="{ThemeResource SystemControlForegroundChromeHighBrush}"
            BorderThickness="1">
            <InkCanvas x:Name="sourceCanvas" AutomationProperties.Name="Source drawing surface" />
        </Border>
        <Border
            Width="300"
            Height="220"
            Background="White"
            BorderBrush="{ThemeResource SystemControlForegroundChromeHighBrush}"
            BorderThickness="1">
            <InkCanvas x:Name="targetCanvas" AutomationProperties.Name="Target drawing surface" />
        </Border>
    </StackPanel>
    <TextBlock x:Name="statusText" Text="Draw on the left canvas, then copy it." TextWrapping="Wrap" />
</StackPanel>
using System.Collections.Generic;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Windows.Foundation;
using Windows.UI.Core;
using InkStroke = Windows.UI.Input.Inking.InkStroke;

public sealed partial class MainPage : Page
{
    public MainPage()
    {
        InitializeComponent();

        foreach (InkCanvas canvas in new[] { sourceCanvas, targetCanvas })
        {
            canvas.InkPresenter.InputDeviceTypes =
                CoreInputDeviceTypes.Pen | CoreInputDeviceTypes.Mouse | CoreInputDeviceTypes.Touch;
        }
    }

    private void CopyButton_Click(object sender, RoutedEventArgs e)
    {
        InkStrokeContainer source = sourceCanvas.InkPresenter.StrokeContainer;
        IReadOnlyList<InkStroke> strokes = source.GetStrokes();
        if (strokes.Count == 0)
        {
            statusText.Text = "Draw on the left canvas first.";
            return;
        }

        // CopySelectedToClipboard copies only selected strokes, so select all strokes first.
        foreach (InkStroke stroke in strokes)
        {
            stroke.Selected = true;
        }

        source.CopySelectedToClipboard();

        foreach (InkStroke stroke in strokes)
        {
            stroke.Selected = false;
        }

        pasteButton.IsEnabled = targetCanvas.InkPresenter.StrokeContainer.CanPasteFromClipboard();
        statusText.Text = $"Copied {strokes.Count} stroke(s) to the clipboard.";
    }

    private void PasteButton_Click(object sender, RoutedEventArgs e)
    {
        InkStrokeContainer target = targetCanvas.InkPresenter.StrokeContainer;
        if (!target.CanPasteFromClipboard())
        {
            statusText.Text = "The clipboard doesn't contain ink.";
            return;
        }

        // Place the pasted ink near the upper-left corner of the target canvas.
        Rect pasted = target.PasteFromClipboard(new Point(16, 16));
        statusText.Text = $"Pasted ink into a {pasted.Width:0} x {pasted.Height:0} area.";
    }
}

Кнопки

Чтобы работать с некоторыми штрихами, задайте свойство Selected для каждого нужного штриха или вызовите SelectWithLine или SelectWithPolyLine. Чтобы удалить или переместить выбранные штрихи, вызовите DeleteSelected или MoveSelected.

Создание обводок в коде

Используйте класс среда выполнения Windows InkStrokeBuilder для создания штрихов из точек и добавления их на холст с помощью AddStroke. В следующем примере добавляется звезда и строка подписи.

<StackPanel Spacing="12">
    <StackPanel Orientation="Horizontal" Spacing="8">
        <Button Click="AddStarButton_Click" Content="Add star" />
        <Button Click="AddSignatureLineButton_Click" Content="Add signature line" />
        <Button Click="ClearButton_Click" Content="Clear" />
    </StackPanel>
    <Border
        Width="480"
        Height="280"
        HorizontalAlignment="Left"
        Background="White"
        BorderBrush="{ThemeResource SystemControlForegroundChromeHighBrush}"
        BorderThickness="1">
        <InkCanvas x:Name="inkCanvas" AutomationProperties.Name="Drawing surface" />
    </Border>
</StackPanel>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Microsoft.UI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Windows.Foundation;
using Windows.UI;
using InkDrawingAttributes = Windows.UI.Input.Inking.InkDrawingAttributes;
using InkPoint = Windows.UI.Input.Inking.InkPoint;
using InkStroke = Windows.UI.Input.Inking.InkStroke;
using InkStrokeBuilder = Windows.UI.Input.Inking.InkStrokeBuilder;

public sealed partial class MainPage : Page
{
    public MainPage()
    {
        InitializeComponent();
    }

    private void AddStarButton_Click(object sender, RoutedEventArgs e)
    {
        // Alternate between outer and inner points to draw a five-pointed star.
        Point center = new(inkCanvas.ActualWidth / 2, 90);
        List<Point> points = new();
        for (int i = 0; i <= 10; i++)
        {
            double radius = i % 2 == 0 ? 70 : 28;
            double angle = -Math.PI / 2 + i * Math.PI / 5;
            points.Add(new Point(center.X + radius * Math.Cos(angle), center.Y + radius * Math.Sin(angle)));
        }

        AddStroke(points, Colors.Goldenrod, 4);
    }

    private void AddSignatureLineButton_Click(object sender, RoutedEventArgs e)
    {
        double left = 40;
        double right = inkCanvas.ActualWidth - 40;
        double baseline = inkCanvas.ActualHeight - 40;

        AddStroke(new[] { new Point(left, baseline), new Point(right, baseline) }, Colors.Gray, 2);
        AddStroke(new[] { new Point(left, baseline - 28), new Point(left + 18, baseline - 10) }, Colors.Gray, 2);
        AddStroke(new[] { new Point(left + 18, baseline - 28), new Point(left, baseline - 10) }, Colors.Gray, 2);
    }

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

    private void AddStroke(IEnumerable<Point> points, Color color, double size)
    {
        InkStrokeBuilder builder = new();
        builder.SetDefaultDrawingAttributes(new InkDrawingAttributes
        {
            Color = color,
            Size = new Size(size, size),
            // Keep straight segments and sharp corners instead of smoothing them into curves.
            FitToCurve = false,
        });

        // Give every point the same pressure so that the stroke has a uniform width.
        IEnumerable<InkPoint> inkPoints = points.Select(point => new InkPoint(point, 0.5f));
        InkStroke stroke = builder.CreateStrokeFromInkPoints(inkPoints, Matrix3x2.Identity);
        inkCanvas.InkPresenter.StrokeContainer.AddStroke(stroke);
    }
}

Кнопки

  • CreateStrokeFromInkPoints создает обводку из значений InkPoint. Каждая точка содержит позицию на холсте и значение давления.
  • В документации по среда выполнения Windows рекомендуется использовать CreateStrokeFromInkPoints вместо CreateStroke для росчерков, которые вы добавляете в объект рукописного ввода.
  • Когда FitToCurve имеет значение false, штрих использует прямые сегменты между точками вместо кривых.

Штрих может принадлежать только одному контейнеру. Чтобы скопировать штрихи на другой холст, добавьте клоны штрихов:

private void CopyStrokesTo(InkCanvas target)
{
    foreach (InkStroke stroke in inkCanvas.InkPresenter.StrokeContainer.GetStrokes())
    {
        target.InkPresenter.StrokeContainer.AddStroke(stroke.Clone());
    }
}

Настройте панель инструментов рукописного ввода

Выберите начальные контролы

Свойство InitialControls определяет, какие встроенные кнопки создает панель инструментов:

Value Встроенные кнопки
All (по умолчанию) Шариковая ручка, карандаш, маркер, ластик и трафарет (линейка)
PensOnly Шариковая ручка, карандаш и маркер
AllExceptPens Ластик и трафарет (линейка)
None None. Объявите кнопки, которые вы хотите использовать в качестве дочерних элементов панели инструментов.

Четыре рукописные панели инструментов, каждая над холстом с примерами штрихов. На панели инструментов с меткой InitialControls All (по умолчанию) отображаются шариковые ручки, карандаш, выделение, ластик и кнопки линейки. PensOnly показывает три кнопки пера. AllExceptPens отображает кнопки ластика и линейки. None плюс карандаш отображает только кнопку карандаша, и на холсте показаны серые росчерки карандаша.

В следующем примере объявляются те же четыре кнопки, что и в примере коллекции WinUI 3, каждой из них присваивается доступное имя, а при загрузке панели инструментов выбирается шариковая ручка:

<StackPanel Spacing="8">
    <InkToolbar
        x:Name="inkToolbar"
        AutomationProperties.Name="Drawing tools"
        InitialControls="None"
        TargetInkCanvas="{x:Bind inkCanvas}">
        <InkToolbarBallpointPenButton x:Name="ballpointButton" AutomationProperties.Name="Ballpoint pen" />
        <InkToolbarPencilButton AutomationProperties.Name="Pencil" />
        <InkToolbarHighlighterButton AutomationProperties.Name="Highlighter" />
        <InkToolbarEraserButton AutomationProperties.Name="Eraser" />
    </InkToolbar>
    <Border
        Height="300"
        Background="White"
        BorderBrush="{ThemeResource SystemControlForegroundChromeHighBrush}"
        BorderThickness="1">
        <InkCanvas x:Name="inkCanvas" AutomationProperties.Name="Drawing surface" />
    </Border>
</StackPanel>
using Microsoft.UI.Xaml.Controls;

public sealed partial class MainPage : Page
{
    public MainPage()
    {
        InitializeComponent();

        // Select a starting tool after the toolbar loads.
        inkToolbar.Loaded += (sender, args) =>
        {
            if (inkToolbar.ActiveTool is null)
            {
                inkToolbar.ActiveTool = ballpointButton;
            }
        };
    }
}

Чтобы отобразить команду, которая удаляет все рукописные фрагменты, задайте свойству кнопки ластика IsClearAllVisible значение true. Панель инструментов вызывает событие EraseAllClicked, когда пользователь выбирает команду.

Настройте цвета и ширину пера

Каждая кнопка пера содержит набор Palette кистей и параметры ширины штриха. Задайте Palette в коде. Дополнительные сведения см. в разделе со сведениями об известных ограничениях. Следующий пример заменяет палитру шариковой ручки четырьмя цветами бренда, ограничивает ширину штриха и показывает атрибуты активного пера.

<StackPanel Spacing="12">
    <InkToolbar
        x:Name="inkToolbar"
        AutomationProperties.Name="Drawing tools"
        InitialControls="AllExceptPens"
        TargetInkCanvas="{x:Bind inkCanvas}">
        <InkToolbarBallpointPenButton
            x:Name="brandPen"
            AutomationProperties.Name="Brand pen"
            MaxStrokeWidth="16"
            MinStrokeWidth="2"
            SelectedStrokeWidth="6" />
    </InkToolbar>
    <RadioButtons
        x:Name="colorButtons"
        Header="Pen color"
        MaxColumns="4"
        SelectedIndex="0"
        SelectionChanged="ColorButtons_SelectionChanged">
        <x:String>Blue</x:String>
        <x:String>Green</x:String>
        <x:String>Orange</x:String>
        <x:String>Purple</x:String>
    </RadioButtons>
    <TextBlock x:Name="penInfo" />
    <Border
        Width="480"
        Height="240"
        HorizontalAlignment="Left"
        Background="White"
        BorderBrush="{ThemeResource SystemControlForegroundChromeHighBrush}"
        BorderThickness="1">
        <InkCanvas x:Name="inkCanvas" AutomationProperties.Name="Drawing surface" />
    </Border>
</StackPanel>
using System.Linq;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Windows.UI;

public sealed partial class MainPage : Page
{
    private static readonly Color[] BrandColors =
    {
        Color.FromArgb(0xFF, 0x00, 0x78, 0xD4), // Blue
        Color.FromArgb(0xFF, 0x10, 0x7C, 0x10), // Green
        Color.FromArgb(0xFF, 0xD8, 0x3B, 0x01), // Orange
        Color.FromArgb(0xFF, 0x5C, 0x2D, 0x91), // Purple
    };

    public MainPage()
    {
        InitializeComponent();

        // Replace the pen's default palette and select the first color.
        brandPen.Palette = BrandColors.Select(color => (Brush)new SolidColorBrush(color)).ToList();
        brandPen.SelectedBrushIndex = 0;

        inkToolbar.InkDrawingAttributesChanged += (sender, args) => ShowPenInfo();

        // The toolbar sets up its buttons when it loads, so select the starting tool then.
        inkToolbar.Loaded += (sender, args) =>
        {
            inkToolbar.ActiveTool = brandPen;
            ShowPenInfo();
        };
    }

    private void ColorButtons_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        // SelectionChanged can occur while the page is still loading.
        if (brandPen is null || colorButtons is null || penInfo is null || colorButtons.SelectedIndex < 0)
        {
            return;
        }

        // SelectedBrushIndex selects an entry in Palette, like choosing a color in the pen flyout.
        brandPen.SelectedBrushIndex = colorButtons.SelectedIndex;
        inkToolbar.ActiveTool = brandPen;
        ShowPenInfo();
    }

    private void ShowPenInfo()
    {
        // InkDrawingAttributes describes the ink that the active tool draws.
        if (inkToolbar.InkDrawingAttributes is { } attributes)
        {
            penInfo.Text = $"Color {attributes.Color}, width {attributes.Size.Width:0} (range {brandPen.MinStrokeWidth:0}-{brandPen.MaxStrokeWidth:0})";
        }
    }
}

Панель инструментов рукописного ввода с кнопками шариковой ручки, ластика и линейки над параметрами синего, зеленого, оранжевого и фиолетового цвета пера. Текст: Цвет #FF0078D4, ширина 6 (диапазон 2–16), а на холсте показаны петлеобразные штрихи синего, зеленого, оранжевого и фиолетового цвета.

SelectedBrushIndex Выберите цвет из Palette. SelectedStrokeWidth, MinStrokeWidth и MaxStrokeWidth задают текущую толщину штриха и диапазон, из которого пользователи могут выбирать. Панель инструментов генерирует событие InkDrawingAttributesChanged при изменении атрибутов активного средства.

Выберите активный инструмент

Задайте ActiveTool для выбора инструмента и обработайте ActiveToolChanged, чтобы реагировать, когда пользователь выбирает другой инструмент. GetToolButton возвращает встроенную кнопку инструмента того типа, который вы передаете. Встроенные кнопки могут не существовать до загрузки панели инструментов, поэтому выберите инструмент, выбранный по умолчанию, в обработчике события Loaded панели инструментов.

<StackPanel Spacing="12">
    <InkToolbar
        x:Name="inkToolbar"
        AutomationProperties.Name="Drawing tools"
        TargetInkCanvas="{x:Bind inkCanvas}" />
    <TextBlock x:Name="toolInfo" />
    <Border
        Width="480"
        Height="280"
        HorizontalAlignment="Left"
        Background="White"
        BorderBrush="{ThemeResource SystemControlForegroundChromeHighBrush}"
        BorderThickness="1">
        <InkCanvas x:Name="inkCanvas" AutomationProperties.Name="Drawing surface" />
    </Border>
</StackPanel>
using Microsoft.UI.Xaml.Controls;

public sealed partial class MainPage : Page
{
    public MainPage()
    {
        InitializeComponent();

        inkToolbar.ActiveToolChanged += (sender, args) => ShowToolInfo();
        inkToolbar.InkDrawingAttributesChanged += (sender, args) => ShowToolInfo();

        // The toolbar creates its built-in buttons when it loads, so select the starting tool then.
        inkToolbar.Loaded += (sender, args) =>
        {
            inkToolbar.ActiveTool = inkToolbar.GetToolButton(InkToolbarTool.Pencil);
            ShowToolInfo();
        };
    }

    private void ShowToolInfo()
    {
        if (inkToolbar.ActiveTool is not { } tool)
        {
            toolInfo.Text = "No active tool";
            return;
        }

        toolInfo.Text = inkToolbar.InkDrawingAttributes is { } attributes
            ? $"{tool.ToolKind}: {attributes.Kind} ink, color {attributes.Color}, size {attributes.Size.Width:0}"
            : $"{tool.ToolKind}";
    }
}

Панель инструментов рукописного ввода с выбранной кнопкой карандаша. Текст под ним считывает карандаш: рукописный карандаш, цвет #FF000000, размер 4, а на холсте показаны серые росчерки карандаша.

Изменить ориентацию

Чтобы отобразить панель инструментов по вертикали, например рядом с холстом, установите Orientation значение Vertical:

<Grid HorizontalAlignment="Left" ColumnSpacing="8">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="Auto" />
    </Grid.ColumnDefinitions>
    <InkToolbar
        VerticalAlignment="Top"
        AutomationProperties.Name="Drawing tools"
        Orientation="Vertical"
        TargetInkCanvas="{x:Bind inkCanvas}" />
    <Border
        Grid.Column="1"
        Width="440"
        Height="320"
        VerticalAlignment="Top"
        Background="White"
        BorderBrush="{ThemeResource SystemControlForegroundChromeHighBrush}"
        BorderThickness="1">
        <InkCanvas x:Name="inkCanvas" AutomationProperties.Name="Drawing surface" />
    </Border>
</Grid>

Вертикальная панель инструментов рукописного ввода с пером, карандашом, маркером, ластиком и кнопками линейки слева от холста с черными рукописными росчерками.

Свойство ButtonFlyoutPlacement задает, где всплывающие меню кнопки открываются относительно панели инструментов. Значение Autoпо умолчанию позволяет панели инструментов выбрать позицию.

Добавить настраиваемую кнопку переключения

Используйте элемент InkToolbarCustomToggleButton, чтобы добавить параметр, который пользователи включают и отключают с панели инструментов. В следующем примере добавляется переключатель, который управляет сенсорным вводом. Ввод с помощью пера и мыши всегда выполняет рукописный ввод.

<StackPanel Spacing="12">
    <InkToolbar AutomationProperties.Name="Drawing tools" TargetInkCanvas="{x:Bind inkCanvas}">
        <InkToolbarCustomToggleButton
            x:Name="touchWritingToggle"
            AutomationProperties.Name="Touch writing"
            Checked="TouchWritingToggle_Changed"
            ToolTipService.ToolTip="Touch writing"
            Unchecked="TouchWritingToggle_Changed">
            <SymbolIcon Symbol="TouchPointer" />
        </InkToolbarCustomToggleButton>
    </InkToolbar>
    <TextBlock x:Name="deviceInfo" />
    <Border
        Width="480"
        Height="280"
        HorizontalAlignment="Left"
        Background="White"
        BorderBrush="{ThemeResource SystemControlForegroundChromeHighBrush}"
        BorderThickness="1">
        <InkCanvas x:Name="inkCanvas" AutomationProperties.Name="Drawing surface" />
    </Border>
</StackPanel>
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Windows.UI.Core;

public sealed partial class MainPage : Page
{
    public MainPage()
    {
        InitializeComponent();

        // Pen and mouse input always draw ink. The custom toggle controls touch input.
        inkCanvas.InkPresenter.InputDeviceTypes = CoreInputDeviceTypes.Pen | CoreInputDeviceTypes.Mouse;
        ShowDevices();
    }

    private void TouchWritingToggle_Changed(object sender, RoutedEventArgs e)
    {
        if (touchWritingToggle.IsChecked == true)
        {
            inkCanvas.InkPresenter.InputDeviceTypes |= CoreInputDeviceTypes.Touch;
        }
        else
        {
            inkCanvas.InkPresenter.InputDeviceTypes &= ~CoreInputDeviceTypes.Touch;
        }

        ShowDevices();
    }

    private void ShowDevices()
    {
        deviceInfo.Text = $"InputDeviceTypes = {inkCanvas.InkPresenter.InputDeviceTypes}";
    }
}

Панель инструментов рукописного ввода с шариковой ручкой, карандашом, выделением, ластиком и кнопкой линейки, за которой следует пользовательский переключатель со значком сенсорного ввода, который включен. Текст: InputDeviceTypes = Touch, Pen, Mouse, и холст отображает черные рукописные росчерки.

Переключатель отображает только значок, поэтому в примере ему задаются доступное имя и подсказка. В этом примере панель инструментов также создает свои кнопки по умолчанию, а настраиваемый переключатель появляется после них.

Панель инструментов поддерживает другие пользовательские кнопки:

  • InkToolbarCustomToolButton добавляет ваш собственный инструмент.
  • InkToolbarCustomPenButton добавляет перо с собственными атрибутами рисования. Присвойте свойству CustomPen экземпляр класса, который наследуется от CreateInkDrawingAttributesCore и переопределяет метод InkToolbarCustomPen.

Создайте панель инструментов в коде

Вы также можете создать холст и панель инструментов в коде. В следующем примере они добавляются в объект Border с именем inkHost, подключаются к панели инструментов с помощью TargetInkPresenter, и при загрузке панели инструментов выбирается карандаш.

<Border x:Name="inkHost" HorizontalAlignment="Left" />
using Microsoft.UI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Automation;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Windows.UI.Core;

public sealed partial class MainPage : Page
{
    public MainPage()
    {
        InitializeComponent();
        CreateInkingUI();
    }

    private void CreateInkingUI()
    {
        InkCanvas canvas = new();
        AutomationProperties.SetName(canvas, "Drawing surface");
        canvas.InkPresenter.InputDeviceTypes =
            CoreInputDeviceTypes.Pen | CoreInputDeviceTypes.Mouse | CoreInputDeviceTypes.Touch;

        InkToolbar toolbar = new() { TargetInkPresenter = canvas.InkPresenter };
        AutomationProperties.SetName(toolbar, "Drawing tools");

        // The toolbar creates its built-in buttons when it loads.
        toolbar.Loaded += (sender, args) => toolbar.ActiveTool = toolbar.GetToolButton(InkToolbarTool.Pencil);

        Border canvasFrame = new()
        {
            Width = 480,
            Height = 280,
            Background = new SolidColorBrush(Colors.White),
            BorderBrush = new SolidColorBrush(Colors.DarkGray),
            BorderThickness = new Thickness(1),
            Child = canvas,
        };

        StackPanel layout = new() { Spacing = 8 };
        layout.Children.Add(toolbar);
        layout.Children.Add(canvasFrame);

        inkHost.Child = layout;
    }
}

Панель инструментов рукописного ввода с кнопкой карандаша, выбранной над холстом с серыми штрихами карандаша.

Известные ограничения

  • Элементы управления рукописным вводом доступны только в экспериментальных выпусках Windows App SDK. Они недоступны в стабильном канале.
  • API для рукописного ввода не включают распознавание рукописного текста или фигур.
  • Ввод с помощью мыши не отображает рукописный ввод, пока вы не добавите CoreInputDeviceTypes.Mouse в InputDeviceTypes.

Следующие проблемы были зарегистрированы в Windows App SDK 2.4.1-experimental. Заметки о выпуске 2.5.4-experimental не перечисляют их как исправленные:

  • Трафареты линейки и транспортира не работают.
  • RenderTargetBitmap не захватывает ввод пером. Чтобы создать изображение чернил, сохраните его в формате GifWithEmbeddedIsf.
  • Установка свойства кнопки стилуса Palette в XAML приводит к ошибке синтаксического анализа XAML. Задайте Palette в коде, как показано в разделе "Настройка цветов и ширины пера".
  • Всплывающие элементы кнопки могут отображаться пустыми, и изменение ButtonFlyoutPlacement может не изменить место, где они открываются.

Изменения в 2.4.1-experimental

Если вы использовали API рукописного ввода в Windows App SDK 2.4.1-экспериментальной версии, обновите код для этих изменений в 2.5.4-экспериментальной версии:

API в версии 2.4.1-experimental Изменение в 2.5.4-experimental
InkToolbar.TargetInkPresenter (тип Object) Тип свойства — InkPresenter.
методы InkPresenter.GetHighContrastAdjustment и InkPresenter.SetHighContrastAdjustment Заменено свойством InkPresenter.HighContrastAdjustment .
методы InkStrokeInput.GetInkPresenter и InkUnprocessedInput.GetInkPresenter Заменяется свойством InkPresenter в каждом классе.
InkToolbarEraserButton.SelectedEraser, IsStrokeEraserVisible и InkToolbarEraserKind свойства; InkToolbar.EraserFlyoutItemClicked событие; InkToolbarEraserFlyoutItemKind, InkToolbarEraserFlyoutItemClickedEventArgs и ArePrecisionErasersVisible типы Удалено.
Недоступно InkPresenter.ActivateCustomDrying добавлен метод и InkSynchronizer класс.

Специальные возможности и высокая контрастность

Цифровые чернила — это визуальное содержимое, поэтому спланируйте, как все пользователи могут использовать функции рукописного ввода:

  • Присвойте каждому холсту и панели инструментов доступное имя с помощью AutomationProperties.Name, как показано в примерах в этой статье. Присвойте пользовательским кнопкам, отображающим только значок, имя и всплывающую подсказку.
  • InkCanvasAutomationPeer делает холст доступным для модель автоматизации пользовательского интерфейса, но он не описывает штрихи. Если рукописный ввод имеет значение, например подпись или помеченную схему, укажите замещающий текст.
  • Для рисования требуется ввод с помощью указателя. Не делайте ввод с помощью пера единственным способом выполнения задачи. Например, позвольте пользователям вводить свое имя вместо подписи.
  • Одноранговые объекты автоматизации для встроенных кнопок панели инструментов поддерживают шаблоны элементов управления SelectionItem и ExpandCollapse. Проверьте навигацию с помощью клавиатуры и всплывающие меню кнопок с конфигурацией панели инструментов, которую использует ваше приложение.

Если тема контраста активна, выступающий может заменить цвета рукописного ввода системными цветами. Чтобы выбрать это поведение, задайте свойству HighContrastAdjustment значение InkHighContrastAdjustment из пространства имен Microsoft.UI.Xaml.Controls. В документации среда выполнения Windows для перечисления с тем же именем значения описываются следующим образом:

Value Цвет чернил в теме контрастности
UseSystemColorsWhenNecessary Собственный цвет чернил, если он достаточно контрастен фону. В противном случае используется системный цвет.
UseSystemColors Системный цвет. Чернила маркера используют системный цвет средства выделения.
UseOriginalColors Собственный цвет чернил.
inkCanvas.InkPresenter.HighContrastAdjustment = InkHighContrastAdjustment.UseSystemColorsWhenNecessary;

Для краткости примеры в этой статье задают белый фон напрямую. В приложении определите фон холста в словарях тем, чтобы он следовал активной теме, как описано в Контрастные темы. Затем протестируйте приложение с каждой темой контрастности и убедитесь, что цвет переднего плана, фон и граница остаются различимыми.

Дополнительные сведения см. в разделе Предоставление основных сведений о специальных возможностях и Клавиатурная доступность.

См. также