C# - InkRecognizerContainer - Windows FormsとWPFでの利用

## はじめに

手軽で賢い InkRecognizerContainer 手書き認識は、UWP で提供されています。
この InkRecognizerContainer を、Windows Forms / WPF - .NET Framework 4.8 から利用する手法を題材にしようと思います。

UWP - InkRecognizerContainer と WPF - InkCanvas のペアで利用したことがありますが、これは、正攻法ではないので、別記事にします。
まずは、UWP - InkRecognizerContainer と UWP - InkCanvas のペアで利用する方法を記載します。

::: note
今回利用する Windows Runtime（WinRT）組み込みサポートは .NET 5 で削除されたので、.NET Framework のみの情報とします。（.NET 5 以降は別手法です）

[破壊的変更: WinRT の組み込みサポートは .NET から削除されています - .NET | Microsoft Learn](
https://learn.microsoft.com/ja-jp/dotnet/core/compatibility/interop/5.0/built-in-support-for-winrt-removed)
:::

::: note
InkCanvas での入力検知（InkCanvas.InkPresenter.StrokesCollectedイベント）で、自動的に手書き認識させること、および、認識結果の複数候補を表示させることなどもできます。
簡潔なサンプルコードとするために、ボタンクリックで手書き認識して、第一候補のみ表示する形態とします。
:::

### テスト環境

ここに記載した情報/ソースコードは、Visual Studio Community 2022 を利用した下記プロジェクトで生成したモジュールを Windows 11 24H2 で動作確認しています。
- Windows Forms - .NET Framework 4.8
- WPF - .NET Framework 4.8

## 前準備

Microsoft.Windows.SDK.Contracts、Microsoft.Toolkit.Forms.UI.Controls、Microsoft.Toolkit.Wpf.UI.Controls の何れかを導入すると、Microsoft.VCRTForwarders.140 が、いつの間にか、参照に追加されてしまうようです。
Microsoft.VCRTForwarders.140 は、プラットフォームが「Any CPU」だと、ビルド時に警告が出力されます。

このため、ソリューション、プロジェクトを作成したら、まず、下記手順で、プラットフォームを「x86」にします。

ソリューション 構成マネージャーを開いて、プラットフォーム「新規作成」を選択します。

![setting-01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963267/c1198ccd-96a0-c585-2984-47c0531eaedb.png)

新しいプラットフォーム「x86」を選択して作成します。

![setting-02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963267/f85017d2-6d92-c80e-0af8-ef3129156d05.png)

次は、プラットフォーム「編集」を選択します。

![setting-03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963267/5a78b544-8d45-8d3c-293f-8c5c3c55a230.png)

「Any CPU」を削除してください。

![setting-04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963267/2bc28290-da82-cfac-f563-f1ca6ac4c71e.png)



## 手書き認識

Microsoft が Windows として提供している、手書き認識には、下記が存在します。

- InkAnalyzer
  - SDK for Vista として提供された System.Windows.Ink.InkAnalyzer（IACore.dll, IALoader.dll, IAWinFX.dll）
- InkRecognizerContainer
  - Windows 10 で UWP 用に提供された Windows.UI.Input.Inking.InkRecognizerContainer
  - Windows 10 以降で利用可能

https://learn.microsoft.com/ja-jp/uwp/api/windows.ui.input.inking.inkrecognizercontainer

InkRecognizerContainer は、InkAnalyzer と比較して、認識率が高く、使い勝手も良くなりました。
Windows 10 以降であれば、当然、InkRecognizerContainer を利用すべきです。

Windows Forms / WPF から、Windows Runtime を呼び出す手法が存在します。
今回は .NET Framework のみを対象としたので、Windows Runtime（WinRT）組み込みサポートで InkRecognizerContainer を利用します。

https://learn.microsoft.com/ja-jp/windows/apps/desktop/modernize/desktop-to-uwp-enhance

### Windows Runtime（WinRT）組み込みサポート
Windows Forms/WPF から、Windows Runtime（WinRT）組み込みサポートを利用する場合、下記手順が必要となります。

- Windows SDK 導入
  - [Windows SDK | Microsoft Developer](https://developer.microsoft.com/ja-jp/windows/downloads/windows-sdk/) から、ターゲットプラットフォームに該当する Windows SDK を入手します。今回は最新のWindows SDK（10.0.26100.0）を利用
- Windows.winmd 参照追加
  - ソリューションエクスプローラ「参照の追加」で、`C:\Program Files (x86)\Windows Kits\10\UnionMetadata\10.0.26100.0\Windows.winmd`　を追加
- NuGet パッケージマネージャー コンソールで、[NuGet Gallery | Microsoft.Windows.SDK.Contracts](https://www.nuget.org/packages/microsoft.windows.sdk.contracts) を導入
  - `PM> NuGet\Install-Package Microsoft.Windows.SDK.Contracts` 

## 手書き入力コントール

手書き入力コントールには、下記が存在します。

- Windows Forms
  - InkEdit
  - InkEdit ととして手書き認識機能あり
- WPF
  - System.Windows.Controls.InkCanvas
  - InkAnalyzer とペアで利用
- UWP
  - Windows.UI.Xaml.Controls.InkCanvas
  - InkRecognizerContainer とペアで利用

Windows Forms / WPF から、UWP コントロールを利用する手法として、XAML Islands があります。
InkRecognizerContainer を選択したので、XAML Islands で、UWP - InkCanvas を利用します。

https://learn.microsoft.com/ja-jp/windows/apps/desktop/modernize/xaml-islands/xaml-islands

https://learn.microsoft.com/ja-jp/windows/communitytoolkit/controls/wpf-winforms/inkcanvas

### XAML Islands で UWP InkCanvas 利用

Windows Forms と WPF で手順が異なるので、それぞれの手順を以下に記載します。

#### Windows Forms

NuGet パッケージマネージャー コンソールで、[NuGet Gallery | Microsoft.Toolkit.Forms.UI.Controls](https://www.nuget.org/packages/Microsoft.Toolkit.Forms.UI.Controls) を導入します。

::: note
現時点での最新 Microsoft.Toolkit.Forms.UI.Controls 6.1.2 は、
System.Net.Http >= 4.0.0
という依存関係があります。

System.Net.Http が存在しない場合、System.Net.Http 4.0.0 を取り込みますが、4.0.0 は脆弱性が検知されています。
このため、最新の System.Net.Http を先に導入します。
:::


```
PM> NuGet\Install-Package System.Net.Http
PM> NuGet\Install-Package Microsoft.Toolkit.Forms.UI.Controls
```

https://learn.microsoft.com/ja-jp/dotnet/api/microsoft.toolkit.forms.ui.controls.inkcanvas

#### WPF

NuGet パッケージマネージャー コンソールで、[NuGet Gallery | Microsoft.Toolkit.Wpf.UI.Controls](https://www.nuget.org/packages/Microsoft.Toolkit.Wpf.UI.Controls) を導入します。

```
PM> NuGet\Install-Package Microsoft.Toolkit.Wpf.UI.Controls
```

## app.manifest

ターゲットを、app.manifest で明示します。

ソリューションエクスプローラ「追加」「新しい項目」で「アプリケーションマニフェストファイル（Windowsのみ）」を選択します。
追加した app.manifest の Windows 10 定義部分のコメントを外して、maxversiontested で Windows SDK バージョンを記載します。

```xml:app.manifest
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
  <application>
    <!-- Windows 10 -->
    <maxversiontested Id="10.0.26100.0"/>
    <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
  </application>
</compatibility>
```

## サンプルコード
前述、「Windows runtime (WinRT) 組み込みサポート」「XAML Islands で UWP InkCanvas 利用」「app.manifest」に記載した手順を実施した上でのサンプルコードを記載します。

### Windows Forms - .NET Framework 4.8
```CSharp
using Windows.UI.Input.Inking;
using Microsoft.Toolkit.Forms.UI.Controls;
```

```CSharp:Form1.cs
public partial class Form1 : Form
{
  // 動的配置コントール
  private InkCanvas inkCanvas = null;
  private TextBox txtResult = null;

  public Form1()
  {
    InitializeComponent();

    // フォームサイズ
    this.Width = 440;
    this.Height = 290;

    // 手書きコントロール配置
    inkCanvas = new InkCanvas
      { Width = 400, Height = 200, Top = 10, Left = 10, Name = "inkCanvas" };
    this.Controls.Add(inkCanvas);
    // マウス・ペン・タッチ入力
    inkCanvas.InkPresenter.InputDeviceTypes = 
      Microsoft.Toolkit.Win32.UI.Controls.Interop.WinRT.CoreInputDeviceTypes.Mouse |
      Microsoft.Toolkit.Win32.UI.Controls.Interop.WinRT.CoreInputDeviceTypes.Pen |
      Microsoft.Toolkit.Win32.UI.Controls.Interop.WinRT.CoreInputDeviceTypes.Touch;

    // 認識結果テキストボックス
    txtResult = new TextBox
      { Width = 300, Height = 20, Top = 220, Left = 110, Name = "txtResult" };
    this.Controls.Add(txtResult);

    // 認識ボタン
    var btnAction = new Button
      { Width = 50, Height = 20, Top = 220, Left = 10, Text = "認識", 
        Name = "btnAction" };
    this.Controls.Add(btnAction);
    btnAction.Click += btnAction_Click;

    // 消去ボタン
    var btnClear = new Button
      { Width = 50, Height = 20, Top = 220, Left = 60, Text = "消去", 
        Name = "btnAction" };
    this.Controls.Add(btnClear);
    btnClear.Click += (sender, e) => {
      inkCanvas.InkPresenter.StrokeContainer.Clear();
      txtResult.Text = string.Empty;
    };
  }

  private async void btnAction_Click(object sender, EventArgs e)
  {
    // 手書き認識結果クリア   
    txtResult.Text = string.Empty;

    // 手書きストローク確認
    var strokes = inkCanvas.InkPresenter.StrokeContainer.GetStrokes();
    if (strokes != null && strokes.Count > 0)
    {
      var irc = new InkRecognizerContainer();
      // 手書き認識 日本語をデフォルトに設定
      var reco = irc.GetRecognizers().FirstOrDefault(r => r.Name.Contains("日本語"));
      if (reco != null)
      {
        irc.SetDefaultRecognizer(reco);
      }

      // 手書き認識結果取得
      IReadOnlyList<InkRecognitionResult> results =
        await irc.RecognizeAsync(
          inkCanvas.InkPresenter.StrokeContainer,
          InkRecognitionTarget.All);
      if (results != null && results.Count > 0)
      {
        // 第一候補のみ取得
        IReadOnlyList<string> candidates = results[0].GetTextCandidates();
        if (candidates != null && candidates.Count > 0)
        {
          txtResult.Text = candidates[0];
        }
      }
    }
  }
}
```

![InkRecognizerContainer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963267/ffdef2ee-a263-7c36-9614-05f8a8ad241f.png)



### WPF - .NET Framework 4.8

xmlns:toolkit として、Microsoft.Toolkit.Wpf.UI.Controls を定義して、Grid で各コントロールをレイアウトします。

```xml:MainWindow.xaml
<Window x:Class="MyApp.MainWindow"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:toolkit="clr-namespace:Microsoft.Toolkit.Wpf.UI.Controls;assembly=Microsoft.Toolkit.Wpf.UI.Controls"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  xmlns:local="clr-namespace:MyApp"
  mc:Ignorable="d"
  Title="MainWindow" Height="280" Width="420">
  <Grid Margin="10,10,0,0">
    <Grid.RowDefinitions>
      <RowDefinition Height="200"/>
      <RowDefinition Height="20"/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="50"/>
      <ColumnDefinition Width="50"/>
      <ColumnDefinition Width="300"/>
    </Grid.ColumnDefinitions>
    <toolkit:InkCanvas x:Name="inkCanvas" Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3"/>
    <Button x:Name="btnAction" Grid.Row="1" Grid.Column="0" Content="認識" Click="btnAction_Click" />
    <Button x:Name="btnClear" Grid.Row="1" Grid.Column="1" Content="消去" Click="btnClear_Click" />
    <TextBlock x:Name="txtResult" Grid.Row="1" Grid.Column="2" />
  </Grid>
</Window>
```


```CSharp
using Windows.UI.Input.Inking;
using Microsoft.Toolkit.Wpf.UI.Controls;
```

```CSharp:MainWindow.xaml.cs
public partial class MainWindow : Window
{
  public MainWindow()
  {
    InitializeComponent();

    // マウス・ペン・タッチ入力
    inkCanvas.InkPresenter.InputDeviceTypes =
      Microsoft.Toolkit.Win32.UI.Controls.Interop.WinRT.CoreInputDeviceTypes.Mouse |
      Microsoft.Toolkit.Win32.UI.Controls.Interop.WinRT.CoreInputDeviceTypes.Pen |
      Microsoft.Toolkit.Win32.UI.Controls.Interop.WinRT.CoreInputDeviceTypes.Touch;
  }

  // 手書き認識
  private async void btnAction_Click(object sender, RoutedEventArgs e)
  {
    // 手書き認識結果クリア 
    txtResult.Text = string.Empty;

    // 手書きストローク確認
    var strokes = inkCanvas.InkPresenter.StrokeContainer.GetStrokes();
    if (strokes != null && strokes.Count > 0)
    {
      var irc = new InkRecognizerContainer();
      // 手書き認識 日本語をデフォルトに設定
      var reco = irc.GetRecognizers().FirstOrDefault(r => r.Name.Contains("日本語"));
      if (reco != null)
      {
        irc.SetDefaultRecognizer(reco);
      }

      // 手書き認識結果取得
      IReadOnlyList<InkRecognitionResult> results =
        await irc.RecognizeAsync(
          inkCanvas.InkPresenter.StrokeContainer,
          InkRecognitionTarget.All);
      if (results != null && results.Count > 0)
      {
        // 第一候補のみ取得
        IReadOnlyList<string> candidates = results[0].GetTextCandidates();
        if (candidates != null && candidates.Count > 0)
        {
          txtResult.Text = candidates[0];
        }
      }
    }
 }

  // クリア
  private void btnClear_Click(object sender, RoutedEventArgs e)
  {
    inkCanvas.InkPresenter.StrokeContainer.Clear();
    txtResult.Text = string.Empty;
  }
}
```