Controles de entrada manuscrita para aplicaciones WinUI

Importante

InkCanvas, InkToolbary InkPresenter son API experimentales. La compatibilidad con la entrada manuscrita se agregó en SDK de Aplicaciones para Windows 2.4.1-experimental. En este artículo se describe SDI de Aplicaciones para Windows 2.5.4-experimental, que ha cambiado la forma de conectar un InkToolbar. Estas API no están disponibles en el canal estable. Pueden cambiar o quitarse en una versión futura. No los use en aplicaciones de producción.

Use el control experimental InkCanvas para permitir que los usuarios dibujen con lápiz, mouse o entrada táctil. Agregue un InkToolbar para proporcionar a los usuarios un bolígrafo, un lápiz, un subrayador y un borrador, con opciones de color y tamaño. Cada lienzo tiene un InkPresenter que recopila, procesa y muestra la entrada manuscrita. El InkStrokeContainer del presentador almacena los trazos, por lo que puede guardar, cargar, copiar y crear entradas manuscritas mediante código.

Para probar los controles, instale el paquete experimental SDK de Aplicaciones para Windows 2.5.4 y el entorno de ejecución coincidente. Para obtener vínculos de descarga e información de configuración, consulte Descargas de SDK de Aplicaciones para Windows más recientes.

¿Es este el control adecuado?

Use un lienzo de entrada manuscrita cuando los usuarios necesiten:

  • Tome notas manuscritas o anote el contenido.
  • Haga bocetos, dibuje diagramas o firme su nombre.
  • Crea contenido a mano alzada que la aplicación guarda y les permite editarlo más adelante.

Las API experimentales de entintado no incluyen reconocimiento de escritura a mano ni de formas. Para recopilar texto, use un cuadro de texto.

Si necesita la entrada manuscrita en una aplicación de producción, consulte Brechas de control conocidas para el estado de estos controles en el canal estable y para ver alternativas.

Comprender el modelo de entrada manuscrita

Las APIs de entintado funcionan juntas:

API Rol
InkCanvas Elemento XAML que muestra la entrada manuscrita y recibe la entrada manuscrita. Su propiedad InkPresenter devuelve el presentador del lienzo.
InkPresenter Recopila la entrada de tinta, representa los trazos y contiene la configuración de entrada y los atributos de dibujo predeterminados. No puede crear un InkPresenter por sí mismo. Obtenerlo de InkCanvas.InkPresenter.
InkStrokeContainer Almacena trazos. Úselo para guardar, cargar, seleccionar, copiar y pegar trazos de tinta. Obtenerlo de InkPresenter.StrokeContainer.
InkToolbar Una barra de herramientas opcional que permite a los usuarios elegir un bolígrafo, un lápiz, un subrayador o un borrador, y cambiar el color y el tamaño de la entrada manuscrita.

InkCanvas no tiene la propiedad Background. Para proporcionar al área de dibujo un fondo y un borde, coloque el lienzo dentro de un contenedor como Border, y establezca el tamaño en el contenedor. El color de tinta predeterminado es negro, por lo que los ejemplos de este artículo usan un fondo blanco.

Varios de estos tipos tienen los mismos nombres que los tipos del espacio de nombres Windows.UI.Input.Inking de Windows Runtime, como InkPresenter, InkStrokeContainer y InkHighContrastAdjustment. Los controles de entrada manuscrita usan las versiones Microsoft.UI.Xaml.Controls. Los trazos y los atributos de dibujo siguen usando tipos de Windows Runtime, como InkStroke, InkDrawingAttributesy InkStrokeBuilder. Si importa ambos espacios de nombres y usa un nombre ambiguo, el compilador de C# notifica el error CS0104.

Los ejemplos de este artículo importan Microsoft.UI.Xaml.Controls y usan directivas de alias para los Windows.UI.Input.Inking tipos que necesitan:

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

También puede importar espacios de nombres y usar nombres completos para los tipos ambiguos, como hace el ejemplo de la galería de WinUI 3.

InkPresenter también proporciona API de nivel inferior que este artículo no cubre:

  • StrokeInput genera eventos cuando se inicia, continúa, finaliza o se cancela un trazo.
  • UnprocessedInput genera eventos de puntero para la entrada que el presentador no procesa como entrada manuscrita.
  • InputConfiguration establece si el presentador procesa la entrada desde el borrador de un lápiz y el botón contextual principal.
  • InputProcessingConfiguration establece si la entrada dibuja tinta, borra la tinta o no se procesa, y cómo controla el presentador el arrastre con el botón derecho.
  • ActivateCustomDrying devuelve un InkSynchronizer para las aplicaciones que representan los trazos de tinta confirmados.

Configura tu proyecto

Las API de entrada manuscrita están marcadas como experimentales. Cuando los usas en un proyecto de C#, el compilador de C# notifica la advertencia CS8305 y el compilador XAML notifica la advertencia WMC1501. Si el proyecto trata las advertencias como errores, estas advertencias detienen la compilación.

Después de decidir usar las API experimentales, puede suprimir estas advertencias. Agregue las siguientes propiedades al archivo de proyecto de C# (.csproj):

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

En los ejemplos de este artículo se usa C#. Cada ejemplo completo muestra el XAML de una página y la clase de code-behind de la página, MainPage.

Crear un lienzo de tinta

Icono de la galería de WinUI 3 La aplicación Galería de WinUI 3 incluye ejemplos interactivos de características y controles winUI. Obtenga la aplicación desde el Microsoft Store o busque el código fuente en GitHub.

En el ejemplo siguiente se coloca un lienzo dentro de un Border blanco. El código subyacente establece los tipos de dispositivo de entrada para que los usuarios puedan dibujar con un lápiz, un mouse o una entrada táctil.

<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 mantenerlos cortos, algunos ejemplos posteriores no repiten este código. Establece InputDeviceTypes en cada página donde los usuarios dibujan con un mouse.

Conectar una barra de herramientas de entrada manuscrita

Coloque un InkToolbar cerca del lienzo y establezca su propiedad TargetInkCanvas para conectar los dos:

<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 de SDK de Aplicaciones para Windows 2.5.4-experimental, puede conectar una barra de herramientas directamente a un InkPresenter estableciendo la TargetInkPresenter propiedad . Establézcalo en el código:

inkToolbar.TargetInkPresenter = inkCanvas.InkPresenter;

También puedes establecerlo en XAML: TargetInkPresenter="{x:Bind inkCanvas.InkPresenter}". Establezca solo una de las dos propiedades de destino.

De forma predeterminada, la barra de herramientas muestra los botones bolígrafo, lápiz, subrayador, borrador y plantilla (regla). Para cambiar qué botones aparecen, consulte Personalizar la barra de herramientas de entrada de lápiz.

Seleccionar dispositivos de entrada

La InputDeviceTypes propiedad usa la CoreInputDeviceTypes enumeración flags del Windows.UI.Core espacio de nombres . La entrada del mouse no realiza entradas manuscritas forma predeterminada. Combine valores para elegir qué dispositivos realizan entradas manuscritas:

// 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 dejar de recopilar trazos de tinta de todos los dispositivos, por ejemplo, para mostrar un dibujo que los usuarios no pueden cambiar, establezca IsInputEnabled en false:

inkCanvas.InkPresenter.IsInputEnabled = false;

Para permitir que los usuarios activen o desactiven un dispositivo de entrada desde la barra de herramientas, consulte Agregar un botón de alternancia personalizado.

Establecer atributos de dibujo

Los atributos de dibujo, como color, tamaño y forma de punta del lápiz, controlan el aspecto de los trazos. Usan la clase Windows RuntimeInkDrawingAttributes. Para cambiar los atributos de los nuevos trazos, llame a CopyDefaultDrawingAttributes, cambie la copia y pásela a UpdateDefaultDrawingAttributes:

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

Colors está en el Microsoft.UI espacio de nombres y Size está en el Windows.Foundation espacio de nombres . Los nuevos atributos se aplican a los trazos que los usuarios dibujan después de la llamada. Los trazos existentes conservan sus atributos.

Al conectar un InkToolbar, la herramienta activa y su configuración de color y tamaño determinan los atributos de dibujo. Para leer los atributos actuales, use la propiedad InkDrawingAttributes de la barra de herramientas.

Crear preajustes de bolígrafo

Si no usa una barra de herramientas, puede ofrecer sus propios preajustes del lápiz. En el ejemplo siguiente se aplican los atributos de bolígrafo, lápiz, resaltador o caligrafía cuando el usuario selecciona un valor preestablecido o cambia el tamaño.

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

Opciones preestablecidas de lápiz para bolígrafo, lápiz, subrayador y caligrafía, con la caligrafía seleccionada, encima de un control deslizante Tamaño y un lienzo blanco. El lienzo muestra trazos en bucle en tinta de bolígrafo negra, tinta de lápiz gris, tinta de subrayador amarillo y tinta de caligrafía azul oscuro.

  • InkDrawingAttributes.CreateForPencil devuelve atributos para trazos de lápiz. La Kind propiedad es de solo lectura, por lo que usa este método para crear atributos de lápiz.
  • DrawAsHighlighter muestra el trazo como una superposición de resaltador.
  • PenTip establece la forma de la punta del bolígrafo y PenTipTransform la gira o la escala. Una punta rectangular inclinada genera trazos de estilo caligráfico.

Responder a los cambios de entrada manuscrita

El presentador activa StrokesCollected después de que un usuario dibuje trazos y StrokesErased después de que un usuario los borre. La Strokes propiedad de los argumentos de cada evento contiene los trazos afectados. El código siguiente mantiene actualizado un recuento de pulsaciones en un 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";
}

Estos eventos se producen cuando los usuarios realizan entradas manuscritas o las borran. Al cambiar los trazos en el código, por ejemplo, con Clear, LoadAsync o AddStroke, se actualiza la interfaz de usuario después de la llamada.

Guardar y cargar tinta

InkStrokeContainer guarda la entrada manuscrita en Formato serializado de tinta (ISF). Para elegir cómo se almacenan los datos, pase un InkPersistenceFormat valor a SaveAsync:

Formato Output
GifWithEmbeddedIsf Imagen GIF que contiene los datos de tinta. Los visores de imágenes pueden mostrar el archivo y la aplicación puede volver a cargar el trazo de tinta para su edición.
Isf Solo datos de entrada manuscrita, sin una imagen. Este es el formato más compacto.

Agregue los métodos siguientes al código subyacente de una página que contiene un InkCanvas denominado inkCanvas. SaveInkAsync escribe la entrada manuscrita en un archivo y LoadInkAsync reemplaza la entrada manuscrita en el lienzo por la entrada manuscrita de un archivo. LoadAsync quita los trazos existentes antes de agregar los trazos cargados, por lo que no es necesario llamar Clear primero.

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 y AsInputStream convierten secuencias de .NET en los tipos de flujo de Windows Runtime que SaveAsync y LoadAsync usan. Para utilizar await con estos métodos de Windows Runtime, el archivo necesita la directiva using System;. Sin él, el compilador notifica el error CS4036.

Para permitir que los usuarios elijan un archivo, use los selectores de archivos SDK de Aplicaciones para Windows, que devuelven la ruta de acceso del archivo seleccionado. Para obtener más información, vea Guardar un archivo con un selector de SDK de Aplicaciones para Windows y Abrir archivos y carpetas con selectores de SDK de Aplicaciones para Windows.

Para mantener la tinta digital en la memoria, por ejemplo, para restaurarla más adelante en la misma sesión, guárdela en un InMemoryRandomAccessStream. Pase el resultado GetOutputStreamAt(0) de la secuencia a SaveAsync, y pase su resultado de GetInputStreamAt(0) a LoadAsync. El ejemplo de la galería de WinUI 3 usa este enfoque.

Copiar y pegar entrada manuscrita

InkStrokeContainer puede copiar trazos en el portapapeles y pegar entrada manuscrita desde el mismo. CopySelectedToClipboard copia solo los trazos seleccionados, por lo que en el ejemplo siguiente se seleccionan todos los trazos del lienzo izquierdo antes de copiarlos. CanPasteFromClipboard comprueba si el Portapapeles contiene tinta digital. PasteFromClipboard agrega la entrada manuscrita en la posición que se pasa y devuelve el rectángulo delimitador del área que cambió.

<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 el lienzo izquierdo y pegue en los botones del lienzo derecho sobre los dos lienzos blancos que muestran los mismos trazos en bucle, espiral y ondulados. El texto siguiente dice Se ha pegado la entrada manuscrita en un área de 255 x 69.

Para trabajar con algunos de los trazos, establezca la Selected propiedad de cada trazo que desee, o llame a SelectWithLine o SelectWithPolyLine. Para eliminar o mover los trazos seleccionados, use DeleteSelected o MoveSelected.

Crear trazos en el código

Use la clase Windows Runtime InkStrokeBuilder para crear trazos a partir de puntos y agréguelos al lienzo con AddStroke. En el ejemplo siguiente se agrega una estrella y una línea de firma.

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

Los botones Agregar estrella, Agregar línea de firma y Borrar aparecen encima de un lienzo blanco que muestra una estrella dorada de cinco puntas y una línea de firma gris con una X en su extremo izquierdo.

  • CreateStrokeFromInkPoints crea un trazo a partir de InkPoint valores. Cada punto combina una posición en el lienzo con un valor de presión.
  • La documentación de Windows Runtime recomienda CreateStrokeFromInkPoints en lugar de CreateStroke para trazos que agregue a un presentador de entrada manuscrita.
  • Cuando FitToCurve es false, el trazo usa segmentos rectos entre puntos en lugar de curvas.

Un trazo solo puede pertenecer a un único contenedor. Para copiar trazos en otro lienzo, agregue clones de los trazos:

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

Personalizar la barra de herramientas de entrada manuscrita

Elegir los controles iniciales

La InitialControls propiedad determina qué botones integrados crea la barra de herramientas:

Value Botones integrados
All (valor predeterminado) Bolígrafo, lápiz, subrayador, borrador y plantilla (regla)
PensOnly Bolígrafo, lápiz y resaltador
AllExceptPens Goma de borrar y plantilla (regla)
None None. Declare los botones que desee como elementos secundarios de la barra de herramientas.

Cuatro barras de herramientas de entrada manuscrita, cada una encima de un lienzo con trazos de ejemplo. La barra de herramientas con la etiqueta InitialControls All (valor predeterminado) muestra el bolígrafo, el lápiz, el resaltador, el borrador y los botones de regla. PensOnly muestra los tres botones de lápiz. AllExceptPens muestra los botones de borrador y regla. None más un lápiz muestra solo un botón de lápiz y su lienzo muestra trazos de lápiz grises.

En el ejemplo siguiente se declaran los mismos cuatro botones que el ejemplo de la galería de WinUI 3, se asigna a cada uno un nombre accesible y se selecciona el bolígrafo cuando se carga la barra de herramientas:

<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 un comando que borra toda la entrada manuscrita, establezca la propiedad IsClearAllVisible del botón del borrador en true. La barra de herramientas genera el EraseAllClicked evento cuando el usuario selecciona el comando.

Personalizar los colores y anchos de un bolígrafo

Cada botón de lápiz tiene una Palette selección de pinceles y ajustes de ancho de trazo. Establezca Palette en el código. Para más información, consulte las limitaciones conocidas. En el ejemplo siguiente se reemplaza la paleta de un bolígrafo por cuatro colores de marca, se limita el ancho del trazo y se muestran los atributos del bolígrafo activo.

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

Barra de herramientas de entrada de lápiz con bolígrafo, borrador y botones de regla encima de las opciones de color azul, verde, naranja y púrpura. El texto dice Color #FF0078D4, ancho 6 (intervalo 2-16) y el lienzo muestra trazos en bucle en azul, verde, naranja y púrpura.

SelectedBrushIndex selecciona un color en Palette. SelectedStrokeWidth, MinStrokeWidth y MaxStrokeWidth establecen el ancho del trazo actual y el rango de valores entre los que los usuarios pueden elegir. La barra de herramientas emite InkDrawingAttributesChanged cuando cambian los atributos de la herramienta activa.

Seleccione la herramienta activa

Use ActiveTool para seleccionar una herramienta y controle ActiveToolChanged para responder cuando el usuario selecciona otra. GetToolButton devuelve un botón de herramienta integrado del tipo que se le pasa. Es posible que los botones integrados no existan hasta que se cargue la barra de herramientas, por lo que seleccione una herramienta de inicio en el controlador de eventos de Loaded de la barra de herramientas.

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

Barra de herramientas de lápiz con el botón de lápiz seleccionado. El texto de abajo dice: Lápiz: tinta de lápiz, color #FF000000, tamaño 4, y el lienzo muestra trazos de lápiz gris.

Cambiar la orientación

Para mostrar la barra de herramientas verticalmente, por ejemplo junto al lienzo, establezca Orientation en 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>

Barra de herramientas de tinta vertical con bolígrafo, lápiz, resaltador, borrador y botón de regla a la izquierda de un lienzo con trazos de tinta negra.

La ButtonFlyoutPlacement propiedad establece dónde se abren los desplegables del botón en relación con la barra de herramientas. El valor predeterminado, Auto, permite que la barra de herramientas elija la posición.

Agregar un botón de alternancia personalizado

Use un InkToolbarCustomToggleButton para agregar un ajuste que los usuarios activen y desactiven desde la barra de herramientas. En el ejemplo siguiente se agrega un botón de alternancia que controla la entrada táctil. La entrada del lápiz y el mouse siempre generan entradas manuscritas.

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

Una barra de herramientas de entrada manuscrita con botones de bolígrafo, lápiz, subrayador, borrador y regla seguidos de un botón de alternancia personalizado con un icono táctil, que está activado. El texto indica InputDeviceTypes = toque, bolígrafo, mouse y el lienzo muestra trazos negros de entrada manuscrita.

El botón de alternancia muestra solo un icono, por lo que el ejemplo le proporciona un nombre accesible y una información sobre herramientas. En este ejemplo, la barra de herramientas también crea sus botones predeterminados y el botón de alternancia personalizado aparece después de ellos.

La barra de herramientas admite otros botones personalizados:

Cree la barra de herramientas en el código

También puede crear el lienzo y la barra de herramientas mediante código. En el ejemplo siguiente, los agrega a un Border denominado inkHost, se conecta la barra de herramientas con TargetInkPresenter y se selecciona el lápiz cuando se carga la barra de herramientas.

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

Barra de herramientas de lápiz con el botón de lápiz seleccionado encima de un lienzo con trazos de lápiz grises.

Limitaciones conocidas

  • Los controles de entrada manuscrita solo están disponibles en versiones experimentales del SDK de Aplicaciones para Windows. No están disponibles en el canal estable.
  • Las API de entrada de lápiz no incluyen reconocimiento de escritura manuscrita ni de formas.
  • La entrada del mouse no dibuja tinta hasta que se agrega CoreInputDeviceTypes.Mouse a InputDeviceTypes.

Los siguientes problemas se notificaron con SDK de Aplicaciones para Windows 2.4.1-experimental. Las notas de la versión 2.5.4-experimental no las enumeran como corregidas:

  • Las plantillas de regla y de transportador no funcionan.
  • RenderTargetBitmap no captura la entrada manuscrita. Para crear una imagen de la entrada manuscrita, guárdela en el formato GifWithEmbeddedIsf.
  • Al establecer la propiedad Palette de un botón de bolígrafo en XAML, se produce un error de análisis de XAML. Establezca Palette en el código en su lugar, como se muestra en Personalizar los colores y anchos de una pluma.
  • Los menús desplegables de botón pueden aparecer vacíos, y es posible que cambiar ButtonFlyoutPlacement no cambie dónde se abren.

Cambios de 2.4.1-experimental

Si ha usado las API de entrada manuscrita en SDK de Aplicaciones para Windows 2.4.1-experimental, actualice el código para estos cambios en la versión 2.5.4-experimental:

API en 2.4.1-experimental Cambio en 2.5.4-experimental
InkToolbar.TargetInkPresenter (tipo Object) El tipo de propiedad es InkPresenter.
métodos InkPresenter.GetHighContrastAdjustment y InkPresenter.SetHighContrastAdjustment Reemplazado por la propiedad InkPresenter.HighContrastAdjustment.
métodos InkStrokeInput.GetInkPresenter y InkUnprocessedInput.GetInkPresenter Reemplazado por una InkPresenter propiedad en cada clase.
InkToolbarEraserButton.SelectedEraser, IsStrokeEraserVisible y ArePrecisionErasersVisible propiedades; InkToolbar.EraserFlyoutItemClicked evento; InkToolbarEraserKind, InkToolbarEraserFlyoutItemKind, y InkToolbarEraserFlyoutItemClickedEventArgs tipos Eliminado.
No disponible InkPresenter.ActivateCustomDrying método y clase InkSynchronizer agregados.

Accesibilidad y contraste alto

La tinta digital es contenido visual, así que planee cómo todos los usuarios pueden usar las características de entrada manuscrita:

  • Asigne a cada lienzo y barra de herramientas un nombre accesible con AutomationProperties.Name, como lo hacen los ejemplos de este artículo. Asigne botones personalizados que muestren solo un icono, un nombre e información sobre una herramienta.
  • InkCanvasAutomationPeerexpone el lienzo a Automatización de la interfaz de usuario, pero no describe los trazos. Si la entrada manuscrita tiene significado, como una firma o un diagrama etiquetado, proporcione una alternativa de texto.
  • La entrada manuscrita requiere la entrada de puntero. No haga que la entrada manuscrita sea la única manera de completar una tarea. Por ejemplo, permitir que los usuarios escriban su nombre como alternativa a firmarlo.
  • Los elementos de automatización del mismo nivel para los botones de herramientas integrados admiten los patrones de control SelectionItem y ExpandCollapse. Pruebe la navegación del teclado y los desplegables del botón con la configuración de la barra de herramientas que usa la aplicación.

Cuando un tema de contraste está activo, el presentador puede reemplazar los colores de la tinta por colores del sistema. Para elegir este comportamiento, establezca la HighContrastAdjustment propiedad en un InkHighContrastAdjustment valor del Microsoft.UI.Xaml.Controls espacio de nombres. La documentación de Windows Runtime para la enumeración con el mismo nombre describe los valores de esta manera:

Value Color de tinta en un tema de contraste
UseSystemColorsWhenNecessary El propio color de la tinta si contrasta lo suficiente con el fondo. De lo contrario, un color del sistema.
UseSystemColors Color del sistema. La tinta del resaltador utiliza el color del resaltador del sistema.
UseOriginalColors El propio color de la tinta.
inkCanvas.InkPresenter.HighContrastAdjustment = InkHighContrastAdjustment.UseSystemColorsWhenNecessary;

Para mantenerlos cortos, los ejemplos de este artículo establecen directamente un fondo blanco. En la aplicación, defina el fondo del lienzo en diccionarios de temas para que siga el tema activo, como se describe en Temas de contraste. A continuación, pruebe la aplicación con cada tema de contraste y compruebe que la entrada manuscrita, el fondo y el borde se distinguen en todo momento.

Para obtener más información, vea Exponer información de accesibilidad básica y accesibilidad de teclado.

Consulte también