WinUI アプリの手描き入力コントロール

Important

InkCanvas、 InkToolbar、および InkPresenter は試験的な API です。 Windows アプリ SDK 2.4.1-experimental で手描き入力のサポートが追加されました。 この記事では、Windows アプリ SDK 2.5.4-experimental について説明します。これは、InkToolbar を接続する方法が変更されました。 これらの API は、安定したチャネルでは使用できません。 将来のリリースで変更または削除される可能性があります。 運用環境のアプリでは使用しないでください。

実験用の InkCanvas コントロールを使用して、ユーザーがペン、マウス、またはタッチでインクを描画できるようにします。 InkToolbar アイコンを追加して、ボールペン、鉛筆、蛍光ペン、消しゴムを色とサイズのオプション付きでユーザーに提供します。 各キャンバスには、インク入力を収集してレンダリングする InkPresenter があります。 InkPresenter の InkStrokeContainer はストロークを格納するため、コード内でインクを保存、読み込み、コピー、作成できます。

コントロールを試すには、Windows アプリ SDK 2.5.4-experimental パッケージと一致するランタイムをインストールします。 ダウンロード リンクとセットアップ情報については、「最新のWindows アプリ SDKダウンロード」を参照してください。

これは適切なコントロールですか?

ユーザーが次のことを行う必要がある場合は、インク キャンバスを使用します。

  • 手書きメモを取る、またはコンテンツに注釈を付ける
  • 図面をスケッチしたり、図を描いたり、自分の名前に署名したりします。
  • アプリで保存するフリーハンド コンテンツを作成し、後でユーザーが編集できるようにします。

実験用のインク API には、手書き認識や図形認識は含まれません。 テキストを収集するには、 テキスト ボックスを使用します。

運用アプリで手書き入力が必要な場合は、安定チャネルでのこれらのコントロールの状態と代替手段については、既知のコントロール ギャップを参照してください。

インク モデルを理解する

手描き入力 API は連携して動作します。

API Role
InkCanvas インクを表示し、インク入力を受け取る XAML 要素。 その InkPresenter プロパティは、キャンバスのプレゼンターを返します。
InkPresenter インク入力を収集し、ストロークをレンダリングし、入力設定と既定の描画属性を保持します。 自分で InkPresenter を作成することはできません。 InkCanvas.InkPresenterから取得します。
InkStrokeContainer ストロークを保存します。 これを使用して、インクの保存、読み込み、選択、コピー、貼り付けを行います。 InkPresenter.StrokeContainerから取得します。
InkToolbar ユーザーがペン、鉛筆、蛍光ペン、または消しゴムを選択し、インクの色とサイズを変更できるオプションのツール バー。

InkCanvas には Background プロパティがありません。 描画領域に背景と罫線を付けるために、キャンバスを Borderなどのコンテナー内に配置し、コンテナーのサイズを設定します。 既定のインクの色は黒なので、この記事の例では白い背景を使用します。

これらの型のいくつかは、Windows.UI.Input.Inking 名前空間の型 (たとえば InkPresenter、InkStrokeContainer、InkHighContrastAdjustment) と同じ名前を持っています。 インク コントロールでは、Microsoft.UI.Xaml.Controls バージョンを使用します。 ストロークと描画属性では、InkStroke、InkDrawingAttributes、InkStrokeBuilderなどのWindows ランタイムタイプが引き続き使用されます。 両方の名前空間をインポートし、あいまいな名前を使用すると、C# コンパイラによってエラー CS0104 が報告されます。

この記事の例では、Microsoft.UI.Xaml.Controls をインポートし、必要な Windows.UI.Input.Inking 型のエイリアス ディレクティブを使用します。

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

WinUI 3 ギャラリー サンプルと同様に、両方の名前空間をインポートし、あいまいな型に完全修飾名を使用することもできます。

InkPresenter また、この記事では取り上げない下位レベルの API も提供します。

  • StrokeInput は、ストロークの開始、続行、終了、または取り消し時にイベントを発生させます。
  • UnprocessedInput は、プレゼンターがインクとして処理しない入力のポインター イベントを発生させます。
  • InputConfiguration は、プレゼンターがペンの消しゴムとプライマリ バレル ボタンからの入力を処理するかどうかを設定します。
  • InputProcessingConfiguration は、入力でインクを描画するか、インクを消去するか、処理しないかを設定し、プレゼンターが右ドラッグ入力を処理する方法を設定します。
  • ActivateCustomDrying は、確定済みのインクを自らレンダリングするアプリの InkSynchronizer を返します。

プロジェクトを設定する

インク API は実験的としてマークされています。 C# プロジェクトでこれらを使用すると、C# コンパイラは警告 CS8305 を報告し、XAML コンパイラは警告WMC1501を報告します。 プロジェクトが警告をエラーとして扱う場合、これらの警告はビルドを停止します。

実験用 API の使用を決定した後は、これらの警告を抑制できます。 次のプロパティを C# プロジェクト ファイル (.csproj) に追加してください。

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

この記事の例では C# を使用します。 各完全な例は、ページの XAML とページの分離コード クラス ( MainPage) を示しています。

インク キャンバスを作成する

WinUI 3 ギャラリー アイコン WinUI 3 ギャラリー アプリには、WinUI コントロールと機能の対話型の例が含まれています。 Microsoft Store からアプリを取得するか、GitHub でソース コードを参照します。

次の例では、白い領域内に Border を配置します。 コードビハインドは、ユーザーがペン、マウス、またはタッチで描画できるように、入力デバイスの種類を設定します。

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

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

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

短くするために、後の例ではこのコードを繰り返しません。 ユーザーがマウスで描画するすべてのページで InputDeviceTypes を設定します。

インク ツールバーを表示する

キャンバスの近くに InkToolbar を配置し、その InkToolbar の TargetInkCanvas プロパティを設定して 2 つを接続します。

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

Windows アプリ SDK 2.5.4-experimental 以降では、InkPresenter プロパティを設定することで、ツール バーを TargetInkPresenter に直接接続できます。 コードで設定:

inkToolbar.TargetInkPresenter = inkCanvas.InkPresenter;

XAML: TargetInkPresenter="{x:Bind inkCanvas.InkPresenter}"で設定することもできます。 2 つのターゲット プロパティのうち 1 つだけを設定します。

既定では、ツール バーにはボールペン、鉛筆、蛍光ペン、消しゴム、ステンシル (ルーラー) ボタンが表示されます。 表示されるボタンを変更するには、「インク ツール バーをカスタマイズする」を参照してください。

入力デバイスの選択

InputDeviceTypes プロパティは、CoreInputDeviceTypes 名前空間の Windows.UI.Core フラグ列挙体を使用します。 既定では、マウス入力ではインクは描画されません。 値を結合して、インク入力を行うデバイスを選択します。

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

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

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

ユーザーが変更できない図を表示するなど、すべてのデバイスからのインクの収集を停止するには、IsInputEnabled を false に設定します。

inkCanvas.InkPresenter.IsInputEnabled = false;

ユーザーがツール バーから入力デバイスのオンとオフを切り替えるには、「カスタム トグル ボタンを追加する」を参照してください。

描画属性を設定する

描画属性 (色、サイズ、ペン先の図形など) は、ストロークの外観を制御します。 Windows ランタイム InkDrawingAttributes クラスを使用します。 新しいストロークの属性を変更するには、 CopyDefaultDrawingAttributesを呼び出し、コピーを変更して、 UpdateDefaultDrawingAttributesに渡します。

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

Colors は Microsoft.UI 名前空間にあり、 Size は Windows.Foundation 名前空間にあります。 新しい属性は、ユーザーが呼び出し後に描画するストロークに適用されます。 既存のストロークは属性を保持します。

InkToolbarを接続すると、アクティブなツールとその色とサイズの設定によって描画属性が決まります。 現在の属性を読み取る場合は、ツール バーの InkDrawingAttributes プロパティを使用します。

ペンのプリセットを作成する

ツールバーを使用しない場合は、独自のペン プリセットを提供できます。 次の例では、ユーザーがプリセットを選択するか、サイズを変更するときに、ボールペン、鉛筆、蛍光ペン、または書道の属性を適用します。

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

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

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

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

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

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

        double size = sizeSlider.Value;
        InkDrawingAttributes attributes;

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

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

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

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

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

サイズ スライダーと白いキャンバスの上に、カリグラフィが選択されたボールペン、鉛筆、蛍光ペン、カリグラフィのペン プリセット オプションが表示されています。キャンバスには、黒いボールペンのインク、灰色の鉛筆の線、黄色の蛍光ペンのインク、濃い青色のカリグラフィ インクによるループ状のストロークが表示されています。

  • InkDrawingAttributes.CreateForPencil は、鉛筆ストロークの属性を返します。 Kind プロパティは読み取り専用であるため、このメソッドを使用してペン属性を作成します。
  • DrawAsHighlighter は、ストロークを蛍光ペン オーバーレイとしてレンダリングします。
  • PenTip はペン先の形状を設定し、PenTipTransform は回転または拡大縮小します。 回転した長方形のペン先で、カリグラフィ風の線を描けます。

インクの変更に対応する

発表者は、ユーザーがストロークを描画した後にStrokesErasedを発生させ、ユーザーがストロークを消去した後にStrokesCollectedを発生させます。 各イベントの引数の Strokes プロパティには、影響を受けるストロークが含まれています。 次のコードは、TextBlockという名前の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";
}

これらのイベントは、ユーザーがインクを描画または消去するときに発生します。 Clear、LoadAsync、AddStrokeなどを使ってコード内の線を変更する場合は、呼び出し後に UI を更新します。

インクを保存して読み込む

InkStrokeContainer はインクをインク シリアル化形式 (ISF) で保存します。 データの保存方法を選択するには、InkPersistenceFormat の値を SaveAsync に渡します:

Format Output
GifWithEmbeddedIsf インク データを含む GIF イメージ。 画像ビューアーはファイルを表示でき、アプリは Ink を再び読み込んで編集できます。
Isf インク データのみ(画像なし)。 これは最もコンパクトな形式です。

InkCanvasという名前のinkCanvasを含むページの分離コードに、次のメソッドを追加します。 SaveInkAsync はインクをファイルに書き込み、LoadInkAsync はキャンバス上のインクをファイルから読み込んだインクに置き換えます。 LoadAsync は、読み込まれたストロークを追加する前に既存のストロークを削除するため、最初に Clear 呼び出す必要はありません。

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

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

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

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

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

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

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

AsOutputStream と AsInputStream は、.NET ストリームを SaveAsync と LoadAsync が使用する Windows ランタイム のストリーム型に変換します。 これらの Windows ランタイム メソッドを await するには、ファイルに using System; ディレクティブが必要です。 これを指定しないと、コンパイラはエラー CS4036 を報告します。

ユーザーがファイルを選択できるようにするには、選択したファイルのパスを返すWindows アプリ SDK ファイル ピッカーを使用します。 詳細については、「Windows アプリ SDK ピッカーを使用してファイルを保存する」および「Windows アプリ SDK ピッカーを使用してファイルとフォルダーを開く」を参照してください。

インクをメモリ内に保持する (たとえば、同じセッションで後で復元する) には、InMemoryRandomAccessStream に保存してください。 ストリームの GetOutputStreamAt(0) 結果を SaveAsyncに渡し、その GetInputStreamAt(0) 結果を LoadAsyncに渡します。 WinUI 3 ギャラリーのサンプルでは、この方法を使用します。

インクのコピーと貼り付け

InkStrokeContainer では、ストロークをクリップボードにコピーし、そこからインクを貼り付けることができます。 CopySelectedToClipboard は選択したストロークのみをコピーするため、次の使用例は、左側のキャンバス上のすべてのストロークを選択してからコピーします。 CanPasteFromClipboard は、クリップボードにインクが含まれているかどうかを確認します。 PasteFromClipboard は、渡した位置にインクを追加し、変更された領域の外接する四角形を返します。

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

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

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

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

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

        source.CopySelectedToClipboard();

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

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

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

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

左側のキャンバスをコピーし、同じループ、スパイラル、波状のストロークを示す 2 つの白いキャンバスの上にあるボタンを使って右側のキャンバスに貼り付けます。下のテキストには、「255 x 69 の領域にインクを貼り付けました」と表示されています。

一部のストロークを扱うには、必要な各ストロークの Selected プロパティを設定するか、SelectWithLine または SelectWithPolyLine を呼び出します。 選択したストロークを削除または移動するには、DeleteSelected または MoveSelected を選択します。

コードでストロークを作成する

Windows ランタイム InkStrokeBuilder クラスを使用して、ポイントからストロークを作成し、AddStrokeを使用してキャンバスに追加します。 次の例では、星と署名の行を追加します。

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

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

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

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

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

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

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

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

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

白いキャンバスの上には、[星の追加]、[署名欄の追加]、[クリア]ボタンがあります。このキャンバスには、金色の五芒星と、左端に X が付いた灰色の署名線が表示されています。

  • CreateStrokeFromInkPoints は、 InkPoint 値からストロークを作成します。 各ポイントは、キャンバス上の位置と圧力値を含みます。
  • Windows ランタイムドキュメントでは、インク プレゼンターに追加するストロークには、CreateStrokeFromInkPointsではなくCreateStrokeを使用することをお勧めします。
  • FitToCurveがfalseされると、ストロークは曲線ではなく点間の直線セグメントを使用します。

ストロークは 1 つのコンテナーにのみ属することができます。 ストロークを別のキャンバスにコピーするには、ストロークのクローンを追加します。

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

インク ツール バーをカスタマイズする

最初のコントロールを選択

InitialControls プロパティは、ツール バーが作成する組み込みボタンを決定します。

Value 内蔵ボタン
All (既定値) ボールペン、鉛筆、蛍光ペン、消しゴム、定規 (ルーラー)
PensOnly ボールペン、鉛筆、蛍光ペン
AllExceptPens 消しゴムと型 (定規)
None None. ツールバーの子として使用するボタンを宣言します。

サンプル ストロークを含むキャンバスの上にある 4 つのインク ツール バー。InitialControls All (既定値) というラベルの付いたツール バーには、ボールペン、鉛筆、蛍光ペン、消しゴム、ルーラー ボタンが表示されます。PensOnly には、3 つのペン ボタンが表示されます。AllExceptPens には、消しゴムボタンとルーラー ボタンが表示されます。None と鉛筆は鉛筆ボタンのみを表示し、キャンバスには灰色の鉛筆のストロークが表示されます。

次の例では、WinUI 3 ギャラリー サンプルと同じ 4 つのボタンを宣言し、それぞれにアクセス可能な名前を付け、ツール バーが読み込まれるときにボールペンを選択します。

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

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

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

すべてのインクを消去するコマンドを使用できるようにするには、消しゴム ボタンの IsClearAllVisible プロパティを true に設定します。 ユーザーがコマンドを選択すると、ツール バーは EraseAllClicked イベントを発生させます。

ペンの色と幅をカスタマイズする

各ペン ボタンには、ブラシとストローク幅の設定の Palette メニューがあります。 コードで Palette を設定します。 詳細については、「既知の制限事項」を参照してください。 次の使用例は、ボールペンのパレットを 4 つのブランドカラーに置き換え、ストロークの幅を制限し、アクティブなペンの属性を表示します。

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

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

    public MainPage()
    {
        InitializeComponent();

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

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

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

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

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

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

青、緑、オレンジ、紫のペンの色オプションの上にボールペン、消しゴム、ルーラー ボタンが表示されたインク ツール バー。テキストは色 #FF0078D4、幅 6 (範囲 2 ~ 16) を読み取り、キャンバスには青、緑、オレンジ、紫のループストロークが表示されます。

SelectedBrushIndex は、 Paletteから色を選択します。 SelectedStrokeWidth、MinStrokeWidth、およびMaxStrokeWidthは、現在のストロークの幅とユーザーが選択できる範囲を設定します。 ツールバーは、アクティブなツールの属性が変更されると、InkDrawingAttributesChanged を発生させます。

アクティブなツールを選択する

ActiveTool を設定してツールを選択し、ユーザーが別のツールを選択したときに ActiveToolChanged を処理して応答します。 GetToolButton は、指定した種類の組み込みのツール ボタンを返します。 ツール バーが読み込まれるまで組み込みボタンが存在しない可能性があるため、ツール バーの Loaded イベント ハンドラーで開始ツールを選択してください。

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

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

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

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

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

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

鉛筆ボタンが選択されたインク ツール バー。その下のテキストは「鉛筆」と表示されています。鉛筆インク、色 #FF000000、サイズ 4、キャンバスには灰色の鉛筆のストロークが表示されます。

向きを変更する

キャンバスの横など、ツールバーを垂直方向に表示するには、 Orientation を Verticalに設定します。

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

黒いインク ストロークがあるキャンバスの左側に、ボールペン、鉛筆、蛍光ペン、消しゴム、ルーラーのボタンが付いた縦型のインク ツール バーがあります。

ButtonFlyoutPlacement プロパティは、ツール バーを基準にしてボタンのポップアップを開く場所を設定します。 既定値の Auto では、ツールバーが位置を選択します。

カスタムトグルボタンを追加する

InkToolbarCustomToggleButtonを使用して、ユーザーがツール バーでオンとオフを切り替える設定を追加します。 次の例では、タッチ入力を制御するトグルを追加します。 ペンとマウスの入力では、常にインク入力になります。

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

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

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

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

        ShowDevices();
    }

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

ボールペン、鉛筆、蛍光ペン、消しゴム、ルーラー ボタンを含むインク ツール バーの後に、タッチ アイコンがオンになっているカスタム トグル ボタンが続きます。テキストには「InputDeviceTypes = Touch, Pen, Mouse」と表示され、キャンバスには黒いインクの線が表示されています。

このトグルにはアイコンのみが表示されるため、例では、このトグルにアクセシブルな名前とツールチップを付けています。 この例では、ツール バーによって既定のボタンも作成され、その後にカスタム トグルが配置されます。

ツールバーは、他のカスタム ボタンに対応しています。

  • InkToolbarCustomToolButton は、独自のツールを追加します。
  • InkToolbarCustomPenButton は、独自の描画属性を持つペンを追加します。 その CustomPen プロパティを、 InkToolbarCustomPen から派生し、 CreateInkDrawingAttributesCore メソッドをオーバーライドするクラスのインスタンスに設定します。

コードでツール バーを作成する

キャンバスとツール バーをコードで作成することもできます。 次の例では、inkHostという名前のBorderに追加し、TargetInkPresenterでツール バーを接続し、ツール バーが読み込まれるときに鉛筆を選択します。

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

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

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

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

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

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

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

        inkHost.Child = layout;
    }
}

灰色の鉛筆のストロークが付いたキャンバスの上に、鉛筆ボタンが選択されたインク ツール バー。

既知の制限

  • 手描き入力コントロールは、Windows アプリ SDKの試験段階のリリースでのみ使用できます。 これらは安定したチャネルでは利用できません。
  • インク API には、手書き認識や図形認識は含まれません。
  • マウス入力では、CoreInputDeviceTypes.MouseにInputDeviceTypesを追加するまでインクは描画されません。

Windows アプリ SDK 2.4.1-experimental で次の問題が報告されました。 2.5.4-experimental リリース ノートには、それらは修正済みとして記載されていません。

  • 定規ステンシルと分度器ステンシルは動作しません。
  • RenderTargetBitmap ではインクを取り込めません。 インクのイメージを作成するには、GifWithEmbeddedIsf 形式で保存します。
  • XAML でペン ボタンの Palette プロパティを設定すると、XAML 解析エラーが発生します。 Paletteに示すように、代わりにコードでを設定します。
  • ボタンのポップアップは空で表示される場合があり、ButtonFlyoutPlacement を変更しても、開く場所は変わらない可能性があります。

2.4.1-experimental からの変更

Windows アプリ SDK 2.4.1-experimental で手描き入力 API を使用した場合は、2.5.4-experimental でのこれらの変更に合わせてコードを更新してください。

2.4.1-experimental の API 2.5.4-experimental の変更
InkToolbar.TargetInkPresenter ( Object型) プロパティの種類は InkPresenter です。
InkPresenter.GetHighContrastAdjustment および InkPresenter.SetHighContrastAdjustment メソッド。 これはInkPresenter.HighContrastAdjustmentプロパティに置き換えられます。
InkStrokeInput.GetInkPresenter および InkUnprocessedInput.GetInkPresenter メソッド。 各クラスの InkPresenter プロパティに置き換えられます。
InkToolbarEraserButton.SelectedEraser、 IsStrokeEraserVisible、 ArePrecisionErasersVisible プロパティ、 InkToolbar.EraserFlyoutItemClicked イベント、 InkToolbarEraserKind、 InkToolbarEraserFlyoutItemKind、および InkToolbarEraserFlyoutItemClickedEventArgs 型 削除。
該当なし InkPresenter.ActivateCustomDrying メソッドと InkSynchronizer クラスが追加されました。

アクセシビリティとハイ コントラスト

インクは視覚的なコンテンツであるため、すべてのユーザーが手描き入力機能を使用する方法を検討してください。

  • この記事の例のように、各キャンバスとツール バーに AutomationProperties.Nameを使用してアクセシブルな名前を付けます。 アイコンのみを表示するカスタム ボタンに名前とヒントを付けます。
  • InkCanvasAutomationPeerはキャンバスをUI オートメーションに公開しますが、ストロークは記述しません。 インクに署名やラベル付き図などの意味がある場合は、代替テキストを提供してください。
  • インクの描画にはポインター入力が必要です。 タスクを完了する唯一の方法をインクだけにしないでください。 たとえば、ユーザーが署名の代わりに自分の名前を入力できるようにします。
  • 組み込みのツール ボタンのオートメーション ピアは、SelectionItem コントロール パターンと ExpandCollapse コントロール パターンをサポートします。 アプリで使用するツール バー構成を使用して、キーボード ナビゲーションとボタンポップアップをテストします。

コントラスト テーマがアクティブな場合、発表者はインクの色をシステムの色に置き換えることができます。 この動作を選択するには、HighContrastAdjustment プロパティを InkHighContrastAdjustment 名前空間の Microsoft.UI.Xaml.Controls 値に設定します。 同じ名前の列挙体のWindows ランタイムドキュメントでは、次の方法で値について説明します。

Value コントラスト テーマのインクの色
UseSystemColorsWhenNecessary 背景と十分なコントラストがある場合は、インク自体の色を使用します。 それ以外の場合は、システムカラーです。
UseSystemColors システムの色。 蛍光ペンのインクでは、システムの蛍光ペンの色が使用されます。
UseOriginalColors インク本来の色。
inkCanvas.InkPresenter.HighContrastAdjustment = InkHighContrastAdjustment.UseSystemColorsWhenNecessary;

例を短くするために、この記事の例では白い背景を直接設定します。 アプリで、コントラスト テーマで説明されているように、キャンバスの背景がアクティブなテーマに従うよう、テーマ ディクショナリで定義します。 次に、各コントラスト テーマでアプリをテストし、インク、背景、境界線が区別可能であることを確認します。

詳細については、「アクセシビリティに関 する基本的な情報を公開する 」および「 キーボードアクセシビリティ」を参照してください。

参照