Controles de escrita à tinta para aplicativos WinUI

Importante

InkCanvas, InkToolbare InkPresenter são APIs experimentais. O suporte à escrita à tinta foi adicionado em SDK do Aplicativo Windows 2.4.1-experimental. Este artigo descreve SDK do Aplicativo Windows 2.5.4-experimental, que alterou a forma como você conecta um InkToolbar. Essas APIs não estão disponíveis no canal estável. Eles podem mudar ou ser removidos em uma versão futura. Não os use em aplicativos de produção.

Use o controle experimental InkCanvas para permitir que os usuários desenhem com tinta digital usando uma caneta, um mouse ou o toque. Adicione um InkToolbar para dar aos usuários uma caneta esferográfica, lápis, marcador e borracha, com opções de cor e tamanho. Cada canvas tem um InkPresenter que coleta e renderiza a entrada de tinta. O apresentador InkStrokeContainer armazena os traços, para que você possa salvar, carregar, copiar e criar anotações a tinta via código.

Para experimentar os controles, instale o pacote experimental SDK do Aplicativo Windows 2.5.4 e o runtime correspondente. Para obter links de download e informações de instalação, consulte os downloads de SDK do Aplicativo Windows mais recentes.

Esse é o controle correto?

Use uma tela de desenho a tinta quando os usuários precisarem:

  • Faça anotações manuscritas ou anote conteúdo.
  • Esboçar, desenhar diagramas ou assinar seu nome.
  • Crie conteúdo à mão livre que seu aplicativo salva e permite editá-lo mais tarde.

As APIs experimentais de escrita à tinta não incluem reconhecimento de escrita manual nem de formas. Para coletar texto, use uma caixa de texto.

Se você precisar de entrada por tinta em um aplicativo de produção, consulte lacunas de controle conhecidas para o status desses controles no canal estável e para obter alternativas.

Entender o modelo de tinta digital

As APIs de tinta funcionam em conjunto:

API Role
InkCanvas O elemento XAML que exibe tinta e recebe entrada de tinta. Sua propriedade InkPresenter retorna o apresentador do canvas.
InkPresenter Coleta entrada de tinta, renderiza traços e mantém as configurações de entrada e os atributos de desenho padrão. Você não pode criar um InkPresenter por conta própria. Obtenha-o de InkCanvas.InkPresenter.
InkStrokeContainer Armazena traços. Use isso para salvar, carregar, selecionar, copiar e colar tinta. Obtenha-o de InkPresenter.StrokeContainer.
InkToolbar Uma barra de ferramentas opcional que permite que os usuários escolham uma caneta, lápis, marcador ou borracha e alterem a cor e o tamanho da tinta.

InkCanvas não tem uma Background propriedade. Para dar à área de desenho um plano de fundo e uma borda, coloque o canvas dentro de um contêiner, como um Border, e defina o tamanho no contêiner. A cor da tinta padrão é preta, portanto, os exemplos neste artigo usam uma tela de fundo branca.

Vários desses tipos têm os mesmos nomes que tipos no namespace do Windows Runtime Windows.UI.Input.Inking, como InkPresenter, InkStrokeContainer e InkHighContrastAdjustment. Os controles de desenho a tinta usam as versões Microsoft.UI.Xaml.Controls. Traços e atributos de desenho ainda usam tipos Windows Runtime, como InkStroke, InkDrawingAttributese InkStrokeBuilder. Se você importar os dois namespaces e usar um nome ambíguo, o compilador C# relatará o erro CS0104.

Os exemplos neste artigo importam Microsoft.UI.Xaml.Controls e usam diretivas de alias para os Windows.UI.Input.Inking tipos de que precisam:

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

Você também pode importar namespaces e usar nomes totalmente qualificados para os tipos ambíguos, como o exemplo da Galeria do WinUI 3.

InkPresenter também fornece APIs de nível inferior que este artigo não aborda:

  • StrokeInput gera eventos quando um traço é iniciado, continua, termina ou é cancelado.
  • UnprocessedInput gera eventos de ponteiro para entrada que o renderizador não processa como traço à tinta.
  • InputConfiguration define se o aplicativo processa a entrada da borracha da caneta e do botão lateral principal.
  • InputProcessingConfiguration define se a entrada desenha tinta, apaga a tinta ou não é processada e como o apresentador lida com a entrada de arraste com o botão direito.
  • ActivateCustomDrying retorna um InkSynchronizer para aplicativos que renderizam a tinta finalizada por conta própria.

Configure seu projeto

As APIs de tinta estão marcadas como experimentais. Quando você os usa em um projeto em C#, o compilador C# relata o aviso CS8305 e o compilador XAML relata WMC1501 de aviso. Se o projeto tratar avisos como erros, esses avisos interromperão o build.

Depois de decidir usar as APIs experimentais, você poderá suprimir esses avisos. Adicione as seguintes propriedades ao arquivo de projeto C# (.csproj):

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

Os exemplos neste artigo usam C#. Cada exemplo completo mostra o XAML de uma página e a classe code-behind da página, MainPage.

Criar uma tela de entrada manuscrita

Ícone da Galeria do WinUI 3 O aplicativo Da Galeria do WinUI 3 inclui exemplos interativos de controles e recursos do WinUI. Obtenha o aplicativo na Microsoft Store ou navegue pelo código-fonte em GitHub.

O exemplo a seguir coloca uma tela dentro de um contêiner branco Border. O code-behind define os tipos de dispositivo de entrada para que os usuários possam desenhar com uma caneta, mouse ou toque.

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

Para mantê-los curtos, alguns exemplos posteriores não repetem esse código. Defina InputDeviceTypes em todas as páginas em que os usuários desenham com um mouse.

Conectar uma barra de ferramentas de tinta à caneta

Coloque um InkToolbar próximo à tela e defina sua TargetInkCanvas propriedade para conectar os dois:

<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>

Começando com o SDK do Aplicativo Windows 2.5.4-experimental, você pode, em vez disso, conectar uma barra de ferramentas diretamente a um InkPresenter definindo a propriedade TargetInkPresenter. Defina-o no código:

inkToolbar.TargetInkPresenter = inkCanvas.InkPresenter;

Você também pode defini-lo em XAML: TargetInkPresenter="{x:Bind inkCanvas.InkPresenter}". Defina apenas uma das duas propriedades de destino.

Por padrão, a barra de ferramentas mostra os botões caneta esferográfica, lápis, realce, borracha e estêncil (régua). Para alterar quais botões aparecem, consulte Personalizar a barra de ferramentas de tinta.

Selecionar dispositivos de entrada

A propriedade InputDeviceTypes usa a enumeração de sinalizadores CoreInputDeviceTypes do namespace Windows.UI.Core. A entrada via mouse não permite desenhar com tinta por padrão. Combine valores para escolher quais dispositivos usam tinta:

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

Para parar de capturar entrada de tinta de todos os dispositivos, por exemplo, para mostrar um desenho que os usuários não podem alterar, defina IsInputEnabled como false:

inkCanvas.InkPresenter.IsInputEnabled = false;

Para permitir que os usuários ativem ou desativem um dispositivo de entrada na barra de ferramentas, consulte Adicionar um botão de alternância personalizado.

Definir atributos de desenho

Atributos de desenho, como cor, tamanho e forma da ponta da caneta, controlam a aparência dos traços. Eles usam a classe Windows RuntimeInkDrawingAttributes. Para alterar os atributos para novos traços, chame CopyDefaultDrawingAttributes, altere a cópia e passe-a para UpdateDefaultDrawingAttributes:

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

Colors está no Microsoft.UI namespace e Size está no Windows.Foundation namespace. Os novos atributos se aplicam aos traços que os usuários desenham após a chamada da função. Os traços existentes mantêm seus atributos.

Quando você conecta uma InkToolbar, a ferramenta ativa e suas configurações de cor e tamanho determinam os atributos de desenho. Para ler os atributos atuais, use a propriedade InkDrawingAttributes da barra de ferramentas.

Criar predefinições de caneta

Se você não usar uma barra de ferramentas, poderá definir suas próprias predefinições de caneta. O exemplo a seguir aplica atributos de caneta esferográfica, lápis, realce ou caligrafia quando o usuário seleciona um predefinido ou altera o tamanho.

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

Opções de predefinição de caneta para Esferográfica, Lápis, Marca-texto e Caligrafia, com a Caligrafia selecionada, acima de um controle deslizante Tamanho e uma tela branca. A tela mostra traços em loop em tinta esferográfica preta, tinta de lápis cinza, tinta de marca-texto amarela e tinta de caligrafia azul-escura.

  • InkDrawingAttributes.CreateForPencil retorna atributos para traços de lápis. A Kind propriedade é somente leitura, portanto, use esse método para criar atributos de lápis.
  • DrawAsHighlighter renderiza o traço como uma sobreposição de realce.
  • PenTip define a forma da ponta da caneta, e PenTipTransform a gira ou dimensiona. Uma ponta retangular girada produz traços no estilo de caligrafia.

Responder a alterações de tinta digital

O apresentador é acionado StrokesCollected depois que um usuário desenha traços e StrokesErased depois que um usuário os apaga. A propriedade Strokes dos argumentos de cada evento contém os traçados afetados. O código a seguir mantém uma contagem de traços atualizada em um TextBlock chamado strokeCountText:

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";
}

Esses eventos ocorrem quando os usuários desenham ou apagam anotações à tinta. Quando você altera traços no código, por exemplo, com Clear, LoadAsync ou AddStroke, atualize sua interface do usuário após a chamada.

Salvar e carregar tinta digital

InkStrokeContainer salva tinta no formato Ink Serialized Format (ISF). Para escolher como os dados são armazenados, passe um InkPersistenceFormat valor para SaveAsync:

Format Saída
GifWithEmbeddedIsf Uma imagem GIF que contém os dados de tinta. Os visualizadores de imagem podem exibir o arquivo e seu aplicativo pode carregar a tinta digital novamente para edição.
Isf Somente dados de tinta, sem uma imagem. Esse é o formato mais compacto.

Adicione os seguintes métodos ao code-behind de uma página que contém um InkCanvas chamado inkCanvas. SaveInkAsync grava a tinta em um arquivo e LoadInkAsync substitui a tinta na tela pela tinta de um arquivo. LoadAsync remove os traços existentes antes de adicionar os traços carregados, portanto, você não precisa chamar Clear primeiro.

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 e AsInputStream convertem fluxos .NET para os tipos de fluxo do Windows Runtime que SaveAsync e LoadAsync usam. Para aguardar esses métodos Windows Runtime, o arquivo precisa da diretiva using System;. Sem ele, o compilador relata o erro CS4036.

Para permitir que os usuários escolham um arquivo, use os seletores de arquivo SDK do Aplicativo Windows, que retornam o caminho do arquivo selecionado. Para obter mais informações, consulte Salvar um arquivo com um seletor de SDK do Aplicativo Windows e abrir arquivos e pastas com seletores de SDK do Aplicativo Windows.

Para manter as anotações à tinta na memória, por exemplo, para restaurá-las posteriormente na mesma sessão, salve-as em um InMemoryRandomAccessStream. Passe o resultado do GetOutputStreamAt(0) fluxo para SaveAsync, e passe seu GetInputStreamAt(0) resultado para LoadAsync. O exemplo da Galeria do WinUI 3 usa essa abordagem.

Copiar e colar ink

InkStrokeContainer pode copiar traços para a área de transferência e colar tinta digital nela. CopySelectedToClipboard copia apenas os traços selecionados, de modo que o exemplo a seguir seleciona cada traço na tela esquerda antes de copiá-los. CanPasteFromClipboard verifica se a área de transferência contém tinta digital. PasteFromClipboard adiciona a anotação de tinta na posição que você passa e retorna o retângulo delimitador da área que foi alterada.

<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.";
    }
}

Os botões Copiar tela esquerda e Colar na tela direita ficam acima de duas telas brancas que mostram os mesmos traços em loop, espiral e ondulados. O texto abaixo diz: tinta colada em uma área de 255 x 69.

Para trabalhar com alguns dos traços, defina a Selected propriedade de cada traço desejado ou chame SelectWithLine ou SelectWithPolyLine. Para excluir ou mover os traços selecionados, chamar DeleteSelected ou MoveSelected.

Criar contornos no código

Use a classe Windows Runtime InkStrokeBuilder para criar traços a partir de pontos e adicioná-los à tela com AddStroke. O exemplo a seguir adiciona uma estrela e uma linha de assinatura.

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

Adicionar estrela, Adicionar linha de assinatura e Limpar acima de uma área branca que mostra uma estrela dourada de cinco pontas e uma linha de assinatura cinza com um X na extremidade esquerda.

  • CreateStrokeFromInkPoints cria um traçado a partir dos valores de InkPoint. Cada ponto combina uma posição na tela com um valor de pressão.
  • A documentação do Windows Runtime recomenda CreateStrokeFromInkPoints em vez de CreateStroke para os traços que você adiciona a um apresentador de tinta.
  • Quando FitToCurve é false, o traço usa segmentos retos entre pontos em vez de curvas.

Um traço pode pertencer a apenas um contêiner. Para copiar traços para outra tela, adicione clones dos traços:

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

Personalizar a barra de ferramentas de tinta

Escolha os controles iniciais

A propriedade InitialControls determina quais botões internos a barra de ferramentas cria:

Valor Botões internos
All (predefinição) Caneta esferográfica, lápis, marcador, borracha e molde (régua)
PensOnly Caneta esferográfica, lápis e marcador
AllExceptPens Borracha e molde (régua)
None None. Declare os botões desejados como filhos da barra de ferramentas.

Quatro barras de ferramentas de tinta, cada uma acima de uma tela com traços de exemplo. A barra de ferramentas rotulada InitialControls All (padrão) mostra a caneta esferográfica, lápis, marca-texto, borracha e botões de régua. PensOnly mostra os botões de três canetas. AllExceptPens mostra os botões de borracha e régua. None mais um lápis mostra apenas um botão de lápis, e sua tela mostra traços de lápis cinza.

O exemplo a seguir declara os mesmos quatro botões que o exemplo da Galeria do WinUI 3, fornece a cada um um nome acessível e seleciona a caneta de esferográfica quando a barra de ferramentas é carregada:

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

Para mostrar um comando que apaga toda a tinta, defina a propriedade IsClearAllVisible do botão da borracha como true. A barra de ferramentas aciona o EraseAllClicked evento quando o usuário seleciona o comando.

Personalize as cores e a largura de uma caneta

Cada botão de caneta tem um conjunto Palette de pincéis e configurações de largura do traço. Definir Palette no código. Para obter detalhes, confira Limitações conhecidas. O exemplo a seguir substitui a paleta de uma caneta esferográfica por quatro cores de marca, limita a largura do traço e mostra os atributos da caneta ativa.

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

Uma barra de ferramentas de tinta com caneta esferográfica, borracha e botões de régua acima das opções de cores Azul, Verde, Laranja e Roxo. O texto lê Cor #FF0078D4, largura 6 (intervalo 2-16) e a tela mostra traços em loop em azul, verde, laranja e roxo.

SelectedBrushIndex seleciona uma cor em Palette. SelectedStrokeWidth, MinStrokeWidth e MaxStrokeWidth definem a largura do traço atual e o intervalo do qual os usuários podem escolher. A barra de ferramentas aciona InkDrawingAttributesChanged quando os atributos da ferramenta ativa são alterados.

Selecione a ferramenta ativa

Defina ActiveTool para selecionar uma ferramenta e trate ActiveToolChanged para responder quando o usuário selecionar outra. GetToolButton retorna um botão de ferramenta interno do tipo que você fornece como argumento. Os botões internos podem não existir até que a barra de ferramentas seja carregada, portanto, selecione uma ferramenta inicial no manipulador de eventos Loaded da barra de ferramentas.

<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}";
    }
}

Uma barra de ferramentas de tinta com o botão lápis selecionado. O texto abaixo lê Lápis: Tinta de lápis, cor #FF000000, tamanho 4 e a tela mostra traços de lápis cinza.

Alterar a orientação

Para mostrar a barra de ferramentas verticalmente, por exemplo, ao lado da tela, defina Orientation como 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>

Uma barra de ferramentas de tinta vertical com caneta esferográfica, lápis, marcador, borracha e botões de régua à esquerda de uma tela com traços de tinta preta.

A propriedade ButtonFlyoutPlacement define onde os menus desdobráveis do botão são abertos em relação à barra de ferramentas. O valor Autopadrão permite que a barra de ferramentas escolha a posição.

Adicionar um botão de alternância personalizado

Use um InkToolbarCustomToggleButton para adicionar uma configuração que os usuários ativam e desativam na barra de ferramentas. O exemplo a seguir adiciona uma alternância que controla a entrada por toque. A entrada da caneta e do mouse sempre desenha traços de tinta.

<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}";
    }
}

Uma barra de ferramentas de tinta com caneta esferográfica, lápis, marcador, borracha e botões de régua seguidos por um botão de alternância personalizado com um ícone de toque, que está ativado. O texto lê InputDeviceTypes = Touch, Pen, Mouse e a tela mostra traços de tinta preta.

O botão de alternância mostra apenas um ícone, de modo que o exemplo fornece um nome acessível e uma dica de ferramenta. Neste exemplo, a barra de ferramentas também cria seus botões padrão e a alternância personalizada é exibida após eles.

A barra de ferramentas dá suporte a outros botões personalizados:

Criar a barra de ferramentas no código

Você também pode criar o canvas e a barra de ferramentas no código. O exemplo a seguir os adiciona a um Border nomeado inkHost, conecta a barra de ferramentas com TargetInkPresenter e seleciona o lápis quando a barra de ferramentas é carregada.

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

Uma barra de ferramentas de tinta com o botão lápis selecionado acima de uma tela com traços de lápis cinza.

Limitações conhecidas

  • Os controles de escrita à tinta estão disponíveis apenas em versões experimentais do SDK do Aplicativo Windows. Eles não estão disponíveis no canal estável.
  • As APIs de escrita à tinta não incluem escrita manual nem reconhecimento de forma.
  • A entrada do mouse não desenha traços à tinta até que você adicione CoreInputDeviceTypes.Mouse a InputDeviceTypes.

Os problemas a seguir foram relatados com SDK do Aplicativo Windows 2.4.1-experimental. As notas de versão 2.5.4 experimentais não as listam como corrigidas:

  • A régua e os estênceis de transferidor não funcionam.
  • RenderTargetBitmap não captura nanquim. Para criar uma imagem da tinta digital, salve-a no formato GifWithEmbeddedIsf.
  • Definir a propriedade Palette de um botão de caneta em XAML causa um erro de análise XAML. Em vez disso, defina Palette em código, conforme mostrado em Personalizar as cores e larguras de uma caneta.
  • Submenus de botão podem aparecer vazios e alterar ButtonFlyoutPlacement pode não mudar onde eles abrem.

Alterações desde a versão 2.4.1-experimental

Se você usou as APIs de escrita à tinta no SDK do Aplicativo Windows 2.4.1-experimental, atualize seu código para essas alterações no 2.5.4-experimental:

API em 2.4.1-experimental Alteração em 2.5.4-experimental
InkToolbar.TargetInkPresenter (tipo Object) O tipo de propriedade é InkPresenter.
Métodos InkPresenter.GetHighContrastAdjustment e InkPresenter.SetHighContrastAdjustment Substituído pela propriedade InkPresenter.HighContrastAdjustment.
Métodos InkStrokeInput.GetInkPresenter e InkUnprocessedInput.GetInkPresenter Substituído por uma InkPresenter propriedade em cada classe.
InkToolbarEraserButton.SelectedEraser, IsStrokeEraserVisiblee ArePrecisionErasersVisible propriedades; evento InkToolbar.EraserFlyoutItemClicked; InkToolbarEraserKind, InkToolbarEraserFlyoutItemKinde InkToolbarEraserFlyoutItemClickedEventArgs tipos Removido.
Não disponível InkPresenter.ActivateCustomDrying método e InkSynchronizer classe adicionados.

Acessibilidade e alto contraste

Tinta é conteúdo visual, portanto, planeje como todos os usuários podem usar seus recursos de escrita à tinta:

  • Dê a cada canvas e barra de ferramentas um nome acessível, AutomationProperties.Namecomo fazem os exemplos neste artigo. Dê um nome e uma dica de ferramenta a botões personalizados que mostram apenas um ícone.
  • InkCanvasAutomationPeer expõe o canvas à Automação da Interface do Usuário, mas não descreve os traços. Se a tinta tiver significado, como uma assinatura ou um diagrama rotulado, forneça uma alternativa de texto.
  • A tinta de desenho requer entrada por caneta. Não faça da tinta a única forma de concluir uma tarefa. Por exemplo, permitir que os usuários digitem seu nome como uma alternativa para assiná-lo.
  • Os pares de automação para os botões de ferramenta integrados dão suporte aos padrões de controle SelectionItem e ExpandCollapse. Teste a navegação do teclado e os menus suspensos dos botões com a configuração da barra de ferramentas que seu aplicativo usa.

Quando um tema de contraste está ativo, o apresentador pode substituir cores de tinta por cores do sistema. Para escolher esse comportamento, defina a HighContrastAdjustment propriedade como um InkHighContrastAdjustment valor do Microsoft.UI.Xaml.Controls namespace. A documentação Windows Runtime para a enumeração com o mesmo nome descreve os valores dessa maneira:

Valor Cor da tinta em um tema de contraste
UseSystemColorsWhenNecessary A cor da tinta se ela contrasta o suficiente com o plano de fundo. Caso contrário, uma cor do sistema.
UseSystemColors Uma cor do sistema. A tinta do marca-texto usa a cor de realce do sistema.
UseOriginalColors A cor da tinta.
inkCanvas.InkPresenter.HighContrastAdjustment = InkHighContrastAdjustment.UseSystemColorsWhenNecessary;

Para mantê-los curtos, os exemplos neste artigo definem um plano de fundo branco diretamente. Em seu aplicativo, defina o plano de fundo da tela em dicionários de temas para que ele siga o tema ativo, conforme descrito em Temas de contraste. Em seguida, teste seu aplicativo com cada tema de contraste e verifique se o primeiro plano, o plano de fundo e a borda permanecem distinguíveis.

Para obter mais informações, consulte Expor informações básicas de acessibilidade e acessibilidade do teclado.

Consulte também