Controlos de entintagem para aplicações WinUI

Importante

InkCanvas, InkToolbar, e InkPresenter são APIs experimentais. O suporte para entintagem foi adicionado no SDK de Aplicações Windows 2.4.1-experimental. Este artigo descreve o SDK de Aplicações Windows 2.5.4-experimental, que alterou a forma como se estabelece a ligação a um InkToolbar. Estas APIs não estão disponíveis no canal estável. Podem mudar ou ser removidos numa versão futura. Não os uses em aplicações de produção.

Use o controlo experimental InkCanvas para permitir que os utilizadores desenhem a tinta com caneta, rato ou toque. Adicione um InkToolbar para dar aos utilizadores uma caneta esferográfica, lápis, marcador e borracha, com opções de cor e tamanho. Cada canvas tem um InkPresenter que recolhe e renderiza a entrada de tinta. O InkStrokeContainer do apresentador armazena os traços, para que possas guardar, carregar, copiar e criar anotações de tinta por código.

Para experimentar os controlos, instale o pacote SDK de Aplicações Windows 2.5.4-experimental e o runtime correspondente. Para links de download e informações de configuração, consulte Downloads mais recentes do SDK de Aplicações Windows.

Será este o controlo correto?

Use uma tela de desenho a tinta quando os utilizadores precisarem de:

  • Tire notas manuscritas ou anote conteúdos.
  • Esboçar, desenhar diagramas ou assinar o próprio nome.
  • Crie conteúdo à mão livre que a sua aplicação guarde e permita editá-lo mais tarde.

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

Se precisar de suporte para tinta digital numa aplicação de produção, veja Limitações conhecidas dos controlos para o estado destes controlos no canal estável e para alternativas.

Compreenda o modelo de entintagem

As APIs de entintagem funcionam em conjunto:

API Role
InkCanvas O elemento XAML que mostra a tinta e recebe a entrada de tinta. A sua propriedade InkPresenter devolve o apresentador para a canvas.
InkPresenter Recolhe a entrada de tinta, renderiza traços e mantém as definições de entrada e os atributos padrão de desenho. Não se pode criar um InkPresenter a si próprio. Obtenha-o de InkCanvas.InkPresenter.
InkStrokeContainer Armazena traços. Usa-o para guardar, abrir, selecionar, copiar e colar tinta. Obtenha-o de InkPresenter.StrokeContainer.
InkToolbar Uma barra de ferramentas opcional que permite aos utilizadores escolher uma caneta, lápis, marcador ou borracha, e alterar a cor e o tamanho da tinta.

InkCanvas não tem propriedade Background . Para dar à área de desenho um fundo e uma borda, coloque o canvas dentro de um recipiente como um Border, e defina o tamanho no recipiente. A cor padrão da tinta é preta, por isso os exemplos neste artigo usam um fundo branco.

Vários destes tipos têm os mesmos nomes que os dos tipos no espaço de nomes Windows Runtime Windows.UI.Input.Inking, como InkPresenter, InkStrokeContainer e InkHighContrastAdjustment. Os controlos de entintagem usam as versões Microsoft.UI.Xaml.Controls. Traços e atributos de desenho continuam a usar tipos de Windows Runtime, como InkStroke, InkDrawingAttributes, e InkStrokeBuilder. Se importar ambos os namespaces e usar um nome ambíguo, o compilador C# reporta 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 necessitam:

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

Também pode importar ambos os namespaces e usar nomes totalmente qualificados para os tipos ambíguos, como faz 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 começa, continua, termina ou é cancelado.
  • UnprocessedInput gera eventos de ponteiro para entrada que o presenter não processa como tinta digital.
  • InputConfiguration determina se o apontador processa a entrada da borracha e do botão lateral principal da caneta.
  • InputProcessingConfiguration define-se se a entrada desenha tinta, apaga tinta ou não é processada, e como o apresentador lida com a entrada de arrastar à direita.
  • ActivateCustomDrying retorna um InkSynchronizer para aplicações que renderizam tinta confirmada elas próprias.

Configure o seu projeto

As APIs de escrita à mão estão marcadas como experimentais. Quando os usa num projeto C#, o compilador C# reporta o aviso CS8305 e o compilador XAML reporta o aviso WMC1501. Se o seu projeto tratar os avisos como erros, esses avisos impedem a compilação.

Depois de decidir usar as APIs experimentais, pode suprimir esses avisos. Adicione as seguintes propriedades ao seu ficheiro 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.

Crie uma tela de tinta digital

Ícone da Galeria WinUI 3 A aplicação WinUI 3 Gallery inclui exemplos interativos de controlos e funcionalidades WinUI. Descarrega a aplicação na Microsoft Store ou navega pelo código-fonte no GitHub.

O exemplo seguinte coloca uma canvas dentro de um contentor branco Border. O code-behind define os tipos de dispositivos de entrada para que os utilizadores possam desenhar com caneta, rato ou toque no ecrã.

<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 resumir, alguns exemplos posteriores não repetem este código. Define InputDeviceTypes em cada página onde os utilizadores desenham com rato.

Ligar uma barra de ferramentas de tinta

Coloque um InkToolbar perto da tela e defina a sua propriedade TargetInkCanvas para ligar ambos:

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

A partir do SDK de Aplicações Windows 2.5.4-experimental, pode, em vez disso, ligar uma barra de ferramentas diretamente a um InkPresenter definindo a TargetInkPresenter propriedade. Defina em código:

inkToolbar.TargetInkPresenter = inkCanvas.InkPresenter;

Também podes definir em XAML: TargetInkPresenter="{x:Bind inkCanvas.InkPresenter}". Defina apenas uma das duas propriedades alvo.

Por defeito, a barra de ferramentas mostra os botões de caneta esferográfica, lápis, marcador, borracha e estêncil (régua). Para alterar quais os botões que aparecem, veja Personalizar a barra de ferramentas de tinta.

Escolha de dispositivos de entrada

A InputDeviceTypes propriedade utiliza a CoreInputDeviceTypes enumeração de flags do Windows.UI.Core namespace. O input do rato não desenha com tinta digital por defeito. Combina valores para escolher que dispositivos consomem 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 deixar de recolher traços à tinta de todos os dispositivos, por exemplo, para mostrar um desenho que os utilizadores não podem alterar, defina IsInputEnabled para false:

inkCanvas.InkPresenter.IsInputEnabled = false;

Para permitir que os utilizadores liguem ou desliguem um dispositivo de entrada a partir da barra de ferramentas, veja 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 o aspeto dos traços. Eles usam a classe Windows RuntimeInkDrawingAttributes. Para alterar os atributos dos novos traços, chame CopyDefaultDrawingAttributes, altere a cópia e transmita-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 espaço de nomes, e Size está no Windows.Foundation espaço de nomes. Os novos atributos aplicam-se aos traços que os utilizadores desenham após a invocação. Os traços existentes mantêm as suas características.

Quando ligas um InkToolbar, a ferramenta ativa e as suas definiçõ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 da caneta

Se não usares uma barra de ferramentas, podes oferecer as tuas próprias predefinições de caneta. O exemplo seguinte aplica características de esferográfica, lápis, marcador ou caligrafia quando o utilizador seleciona um preset 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 para caneta para Esferográfica, Lápis, Marcador e Caligrafia, com Caligrafia selecionada, acima de um deslizador de Tamanho e uma tela branca. A tela mostra traços em laço em tinta esferográfica preta, tinta cinzenta de lápis, tinta amarela para marcador e tinta azul escura para caligrafia.

  • InkDrawingAttributes.CreateForPencil retorna os atributos dos traços de lápis. A propriedade Kind é apenas de leitura, por isso use este 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 roda-a ou dimensiona-a. Uma ponta retangular girada produz traços ao estilo caligrafia.

Responder a alterações de tinta

O reconhecedor levanta StrokesCollected depois de um utilizador desenhar traços e StrokesErased depois de o utilizador os apagar. A propriedade Strokes dos argumentos de cada evento contém os traços afetados. O código seguinte mantém a contagem de traços atualizada num TextBlock denominado 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";
}

Estes eventos ocorrem quando os utilizadores desenham ou apagam tinta digital. Quando mudas traços no código, por exemplo com Clear, LoadAsync, ou AddStroke, atualiza a interface após a chamada.

Guardar e carregar tinta

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

Format Output
GifWithEmbeddedIsf Uma imagem GIF que contém os dados de tinta. Os visualizadores de imagens podem mostrar o ficheiro, e a tua aplicação pode carregar novamente as anotações a tinta para edição.
Isf Apenas dados de tinta digital, sem imagem. Este é o formato mais compacto.

Adicione os seguintes métodos ao código por trás de uma página que contenha um InkCanvas com o nome inkCanvas. SaveInkAsync escreve a tinta num ficheiro e LoadInkAsync substitui a tinta no canvas pela tinta de um ficheiro. LoadAsync remove os traços existentes antes de adicionar os traços carregados, por isso não precisas de 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 utilizam. Para aguardar estes métodos do Windows Runtime, o ficheiro precisa da using System; diretiva. Sem ele, o compilador reporta o erro CS4036.

Para permitir que os utilizadores escolham um ficheiro, utilize os seletores de ficheiros do SDK de Aplicações Windows, que retornam o caminho do ficheiro selecionado. Para mais informações, consulte Guardar um ficheiro com um seletor de SDK de Aplicações Windows e Abrir ficheiros e pastas com selecionadores de SDK de Aplicações Windows.

Para manter a ink na memória, por exemplo, para a restaurar mais tarde na mesma sessão, guarde-a num InMemoryRandomAccessStream. Passe o resultado do fluxo GetOutputStreamAt(0) para SaveAsync, e passe o respetivo resultado GetInputStreamAt(0) para LoadAsync. O exemplo da Galeria WinUI 3 utiliza esta abordagem.

Copiar e colar ink

InkStrokeContainer Podes copiar traços para a área de transferência e colar tinta digital a partir dela. CopySelectedToClipboard Copia apenas traços selecionados, por isso o exemplo seguinte seleciona todos os traços na tela esquerda antes de os copiar. CanPasteFromClipboard verifica se a área de transferência contém tinta digital. PasteFromClipboard Adiciona a tinta na posição que indicar e devolve o retângulo delimitador da área que mudou.

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

Copie a tela esquerda e cole na tela direita acima de duas telas brancas que mostram os mesmos traços em laço, em espiral e ondulados. O texto abaixo diz Tinta colada numa área de 255 x 69.

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

Criar contornos em código

Utilize a classe Windows Runtime InkStrokeBuilder para criar traços a partir de pontos e adicione-os ao canvas com AddStroke. O exemplo seguinte 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 botões Limpar acima de uma tela branca que mostra uma estrela dourada de cinco pontas e uma linha cinzenta com um X na extremidade esquerda.

  • CreateStrokeFromInkPoints cria um contorno a partir dos InkPoint valores. 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 adiciona a um processador de tinta.
  • Quando FitToCurve é false, o traço utiliza segmentos retos entre pontos em vez de curvas.

Um traço pode pertencer apenas a um contentor. Para copiar traços para outro canvas, 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 controlos iniciais

A InitialControls propriedade determina quais os botões incorporados que a barra de ferramentas cria:

Valor Botões incorporados
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. Declara os botões que pretende 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 (por defeito) mostra os botões de caneta esferográfica, lápis, marcador, borracha e régua. O PensOnly mostra os três botões da caneta. AllExceptPens mostra os botões de borracha e régua. None plus a pencil mostra apenas um botão de lápis, e a sua tela mostra traços cinzentos de lápis.

O exemplo seguinte declara os mesmos quatro botões do exemplo da Galeria WinUI 3, atribui a cada um um nome acessível e seleciona a caneta 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 do IsClearAllVisible botão de borracha para true. A barra de ferramentas gera o evento EraseAllClicked quando o utilizador seleciona o comando.

Personalize as cores e larguras de uma caneta

Cada botão da caneta tem Palette uma variedade de pincéis e definições de largura do traço. Defina Palette no código. Para obter detalhes, consulte Limitações conhecidas. O exemplo seguinte substitui a paleta de uma caneta esferográfica por quatro cores de marca, limita a largura do traço e mostra as características 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 cor da caneta Azul, Verde, Laranja e Roxa. O texto lê-se 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 atual do traço e o intervalo que os utilizadores podem escolher. A barra de ferramentas emite InkDrawingAttributesChanged quando os atributos da ferramenta ativa mudam.

Selecione a ferramenta ativa

Defina ActiveTool para selecionar uma ferramenta e use ActiveToolChanged para responder quando o utilizador selecionar outra. GetToolButton devolve um botão de ferramenta incorporado do tipo que fornece. Os botões incorporados podem não existir até a barra de ferramentas carregar, por isso seleciona uma ferramenta inicial no processador do evento 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 diz Lápis: Tinta de lápis, #FF000000 de cor, tamanho 4, e a tela mostra traços cinzentos de lápis.

Mude a orientação

Para mostrar a barra de ferramentas verticalmente, por exemplo ao lado do canvas, defina Orientation para 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 dos botões se abrem em relação à barra de ferramentas. O valor predefinido, Auto, permite que a barra de ferramentas escolha a posição.

Adicione um botão de alternância personalizado

Utilize um InkToolbarCustomToggleButton para adicionar uma definição que os utilizadores ativam e desativam na barra de ferramentas. O exemplo seguinte adiciona um interruptor que controla a entrada por toque. A entrada de caneta e rato desenha sempre 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 botões para caneta esferográfica, lápis, marcador, borracha e régua, seguida de um botão personalizado com um ícone tátil, que é ativado. O texto lê-se InputDeviceTypes = Touch, Pen, Mouse, e a canvas mostra traços de tinta preta.

O botão de alternar mostra apenas um ícone, por isso o exemplo dá-lhe um nome acessível e uma dica de ferramenta. Neste exemplo, a barra de ferramentas também cria os seus botões predefinidos, e o botão personalizado aparece depois deles.

A barra de ferramentas suporta outros botões personalizados:

Criar a barra de ferramentas em código

Também podes criar o ecrã e a barra de ferramentas em código. O exemplo seguinte adiciona-as a um Border chamado inkHost, liga a barra de ferramentas com TargetInkPresenter, e seleciona o lápis quando a barra de ferramentas carrega.

<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 digital com o botão lápis selecionado acima de uma tela de desenho com traços de lápis cinzentos.

Limitações conhecidas

  • Os controlos de entintagem estão disponíveis apenas em versões experimentais do SDK de Aplicações Windows. Não estão disponíveis no canal estável.
  • As APIs de tinta não incluem escrita à mão nem reconhecimento de formas.
  • O input do rato não desenha tinta até adicionar CoreInputDeviceTypes.Mouse a InputDeviceTypes.

Os seguintes problemas foram reportados com o SDK de Aplicações Windows 2.4.1-experimental. As notas de lançamento 2.5.4-experimental não os listam como corrigidos:

  • Os estênceis da régua e do transportador não funcionam.
  • RenderTargetBitmap não reconhece a tinta manuscrita. Para criar uma imagem da anotação, guarde-a no formato GifWithEmbeddedIsf.
  • Definir a propriedade Palette de um botão de caneta no XAML causa um erro de análise sintática do XAML. Defina Palette no código em vez disso, como mostrado em Personalizar as cores e larguras da caneta.
  • Os menus desdobráveis dos botões podem parecer vazios, e mudar ButtonFlyoutPlacement pode não alterar onde abrem.

Alterações em relação a 2.4.1-experimental

Se usou as APIs de entintagem no SDK de Aplicações Windows 2.4.1-experimental, atualize o seu código para estas alterações na 2.5.4-experimental:

API na versão 2.4.1-experimental Mudança 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.
propriedades InkToolbarEraserButton.SelectedEraser, IsStrokeEraserVisible e ArePrecisionErasersVisible; evento InkToolbar.EraserFlyoutItemClicked; tipos InkToolbarEraserKind, InkToolbarEraserFlyoutItemKind e InkToolbarEraserFlyoutItemClickedEventArgs Removido.
Não disponível InkPresenter.ActivateCustomDrying método e InkSynchronizer classe adicionados.

Acessibilidade e alto contraste

A tinta é conteúdo visual, por isso planeie como todos os utilizadores podem usar as suas funcionalidades de tinta:

  • Dê a cada canvas e barra de ferramentas um nome acessível com AutomationProperties.Name, tal como fazem os exemplos neste artigo. Atribua um nome e uma dica de ferramenta aos botões personalizados que mostrem apenas um ícone.
  • InkCanvasAutomationPeer expõe o canvas ao Automatização da Interface de Utilizador, mas não descreve os traços. Se a tinta tiver significado, como uma assinatura ou um diagrama rotulado, forneça uma alternativa em texto.
  • A escrita a tinta requer entrada por apontador. Não faças da tinta a única forma de completar uma tarefa. Por exemplo, permitir que os utilizadores escrevam o seu nome como alternativa a assiná-lo.
  • Os elementos de automação para os botões de ferramentas incorporados suportam os padrões de controlo SelectionItem e ExpandCollapse. Teste a navegação por teclado e os menus desdobráveis dos botões com a configuração da barra de ferramentas que a sua aplicação utiliza.

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

Valor Cor da tinta num tema de contraste
UseSystemColorsWhenNecessary A própria cor da tinta, se contrastar o suficiente com o fundo. Caso contrário, uma cor do sistema.
UseSystemColors Uma cor de sistema. A tinta de realce usa a cor de realce do sistema.
UseOriginalColors A própria cor da tinta.
inkCanvas.InkPresenter.HighContrastAdjustment = InkHighContrastAdjustment.UseSystemColorsWhenNecessary;

Para os manter curtos, os exemplos deste artigo apresentam um fundo branco diretamente. Na sua aplicação, defina o fundo do canvas nos dicionários de temas para que siga o tema ativo, conforme descrito nos temas Contrast. Depois testa a tua aplicação com cada tema de contraste e verifica se o primeiro plano, o fundo e o rebordo se mantêm distinguíveis.

Para mais informações, consulte Exposição de informações básicas de acessibilidade e Acessibilidade do teclado.

Ver também