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 traços com uma caneta, um mouse ou por 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 InkStrokeContainer do apresentador armazena os traços, permitindo que você salve, carregue, copie e crie traços 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 editar posteriormente.

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 recursos de escrita à mão em um aplicativo de produção, consulte Lacunas conhecidas nos controles para verificar o status desses controles no canal estável e conhecer alternativas.

Entenda o modelo de escrita à tinta

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 para a tela (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-a para salvar, carregar, selecionar, copiar e colar traços. 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 escrita à mão utilizam 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 entradas que o apresentador não processa como tinta digital.
  • InputConfiguration define se o apresentador processa a entrada da borracha e do botão principal do corpo da caneta.
  • InputProcessingConfiguration define se a entrada desenha traços, apaga traços ou não é processada, e como o apresentador lida com a entrada proveniente de um arrastar 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 tinta

Í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 Border branco. 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

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 gera traços 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 interromper a coleta de traços de qualquer dispositivo, por exemplo, para exibir um desenho que os usuários não possam 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 aplicam-se aos traços que os usuários desenham após a chamada. 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, utilize a propriedade InkDrawingAttributes da barra de ferramentas.

Criar predefinições de caneta

Se você não utilizar uma barra de ferramentas, pode oferecer suas próprias predefinições de caneta. O exemplo a seguir aplica atributos de caneta esferográfica, lápis, marca-texto ou caligrafia quando o usuário seleciona uma predefinição 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 o formato da ponta da caneta, e PenTipTransform rotaciona ou redimensiona essa ponta. Uma ponta retangular girada produz traços no estilo de caligrafia.

Responder a alterações na tinta

O apresentador emite 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 atualizada uma contagem de traços 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. Ao alterar os traços no código, por exemplo, usando Clear, LoadAsync ou AddStroke, atualize a interface do usuário após a chamada.

Salvar e carregar tinta digital

InkStrokeContainer economiza 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. Visualizadores de imagem podem exibir o arquivo, e seu aplicativo pode carregar o traço 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, não é necessário 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 tinta

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 tinta na posição informada 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 traços via código

Use a classe InkStrokeBuilder do Windows Runtime para criar traços a partir de pontos e adicione-os à 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);
    }
}

Botões

  • 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 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, marca-texto, borracha e estêncil (régua)
PensOnly Caneta esferográfica, lápis e marcador
AllExceptPens Borracha e estêncil (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 exibir um comando que apaga toda a tinta, defina a propriedade IsClearAllVisible do botão de 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 da caneta possui um Palette de pincéis e configurações de espessura de 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 de 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 utilize 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. Entradas via caneta e mouse sempre geram traços.

<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 à mão livre não incluem reconhecimento de caligrafia ou de formas.
  • A entrada do mouse não gera traços 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 da versão 2.5.4-experimental não os listam como corrigidos:

  • Os gabaritos de régua e transferidor não funcionam.
  • RenderTargetBitmap não captura tinta. Para criar uma imagem da tinta, salve-a no formato GifWithEmbeddedIsf.
  • Definir a propriedade Palette de um botão de caneta no XAML causa um erro de análise de 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 Método InkPresenter.ActivateCustomDrying e classe InkSynchronizer 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.
  • Para desenhar com tinta, é necessário usar um ponteiro. 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. No seu aplicativo, defina o plano de fundo da área de desenho nos dicionários de tema para que ele acompanhe o tema ativo, conforme descrito em Temas de alto 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