C# - InkRecognizerContainer - WPF InkCanvasでの利用

## はじめに

[C# - InkRecognizerContainer - Windows FormsとWPFでの利用](https://qiita.com/chai0917/items/a77069ba18e85a95c221) では、UWP - InkCanvas を利用した方法を記載しましたが、WPF - InkCanvas を利用する手法を記載します。


https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.controls.inkcanvas

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

- WPF - .NET Framework 4.8


## UWP - InkRecognizerContainer 利用設定

::: note
「C# - InkRecognizerContainer - Windows FormsとWPFでの利用」とは、NuGet 導入パッケージが異なります。
:::

- 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 で System.Runtime.WindowsRuntime を導入
  - `PM> NuGet\Install-Package System.Runtime.WindowsRuntime` 
- NuGet で System.Runtime.WindowsRuntime.UI.Xaml を導入
  - `PM> NuGet\Install-Package System.Runtime.WindowsRuntime.UI.Xaml` 


## ストロークデータ置換

WPF - InkCanvas でのストロークデータは、System.Windows.Ink.Stroke です。
UWP - InkRecognizerContainer でのストロークデータは、Windows.UI.Input.Inking.InkStrok となります。

WPF - InkCanvas と UWP - InkRecognizerContainer のペア利用を、上記ストロークデータの置換で行うというアプローチです。

```CSharp
// WPF - Stroke から UWP - Stroke に置換
private Windows.UI.Input.Inking.InkStroke StrokeWpf2Uwp(
  System.Windows.Ink.Stroke wpfStroke, double inkWidth, double inkHeight)
{
  var uwpPoints = new List<Windows.Foundation.Point>();
  foreach (var point in wpfStroke.StylusPoints)
  {
    uwpPoints.Add(new Windows.Foundation.Point(point.X, point.Y));
  }
  var builder = new Windows.UI.Input.Inking.InkStrokeBuilder();
  var uwpStroke = builder.CreateStroke(uwpPoints);
  var ida = new Windows.UI.Input.Inking.InkDrawingAttributes();
  ida.Size = new Windows.Foundation.Size(inkWidth, inkHeight);
  ida.PenTip = Windows.UI.Input.Inking.PenTipShape.Rectangle;
  uwpStroke.DrawingAttributes = ida;
  return (uwpStroke);
}
```

## サンプルコード
### ボタンクリックで手書き認識

WPF - InkCanvas などを Gird で配置します。

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

```
using Windows.UI.Input.Inking;
```

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

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

    // 手書きストローク確認
    if (inkCanvas.Strokes.Count > 0)
    {
      var upwStrokeContainer = new Windows.UI.Input.Inking.InkStrokeContainer();
      foreach (var wpfStroke in inkCanvas.Strokes)
      {
        upwStrokeContainer.AddStroke(
          StrokeWpf2Uwp(
            wpfStroke,
            inkCanvas.DefaultDrawingAttributes.Width,
            inkCanvas.DefaultDrawingAttributes.Height));
      }
   
      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(
          upwStrokeContainer,
          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.Strokes.Clear();
    txtResult.Text = string.Empty;
  }
}
```

### 自動手書き認識
「ボタンクリックで手書き認識」コードに、自動手書き認識を追加します。
MainWindow に対して、InkCanvas のストローク開始（MouseDown）、ストローク完了（StrokeCollected）のイベントハンドラ、および、手書き自動認識クラスの認識結果更新（DispatchUpdated）イベントハンドラを作成します。


::: note
ペン/タッチの開始時にも MouseDown イベントが発生するので、MouseDown イベントで、マウス/ペン/タッチ全てのストローク開始を捕捉できます。
:::


```CSharp:MainWindow.xaml.cs
public partial class MainWindow : Window
{
  // 手書き自動認識クラス
  private InkDispatch mInkDispatch = null;

  public MainWindow()
  {
    InitializeComponent();

    // InkCanvas - 入力開始と入力完了（マウス/ペン/タッチ）
    inkCanvas.MouseDown += new MouseButtonEventHandler(inkCanvas_MouseDown);
    inkCanvas.StrokeCollected += 
      new InkCanvasStrokeCollectedEventHandler(inkCanvas_StrokeCollected);

    // 手書き自動認識クラス
    mInkDispatch = new InkDispatch();
    mInkDispatch.DispatchUpdated += new DispatchUpdatedHandler(InkDispatch_Updated);
  }

  private void inkCanvas_MouseDown(object sender, MouseButtonEventArgs e)
  {
    // ストローク開始 → 自動認識停止
    mInkDispatch?.AnalyzeTimerSusppend();
  }

  private void inkCanvas_StrokeCollected(object sender, InkCanvasStrokeCollectedEventArgs e)
  {
    // ストローク完了 → UWPストロークに変換/追加して自動認識タイマースタート
    mInkDispatch?.AddStroke(
      StrokeWpf2Uwp(e.Stroke,
        inkCanvas.DefaultDrawingAttributes.Width,
        inkCanvas.DefaultDrawingAttributes.Height));
  }

  private void InkDispatch_Updated(object sender, DispatchUpdatedEventArgs e)
  {
    // 手書き自動認識 認識結果更新 - 第一候補を表示
    txtResult.Text = e.Results?[0] ?? string.Empty;
  }
}
```

消去ボタン クリックイベントハンドラに対して、手書き認識クラスのクリアを追加します。

```CSharp:MainWindow.xaml.cs
private void btnClear_Click(object sender, RoutedEventArgs e)
{
  inkCanvas.Strokes.Clear();
  txtResult.Text = string.Empty;

  mInkDispatch.Clear();   // 追加
}
```

続いて、手書き自動認識クラス - InkDispatch.cs を実装します。
ポイントは、文字入力途中の不完全ストローク状態での自動認識を抑止するために、DispatcherTimer を用いて、入力完了後 1.2 秒後に自動認識を動作させる形態としています。
また、次の文字入力が開始されたら、AnalyzeTimerSusppend で、DispatcherTimer を停止します。

手書き認識が動作したら、DispatchUpdated イベントを発生させます。


```CSharp:InkDispatch.cs
// 手書き自動認識 認識結果更新イベント
public delegate void DispatchUpdatedHandler(object sender, DispatchUpdatedEventArgs e);
public class DispatchUpdatedEventArgs : EventArgs
{
  private readonly List<string> mResults = null;
  public List<String> Results  { get { return mResults; } }
  public DispatchUpdatedEventArgs(List<string> results)
  {
    this.mResults = results;
  }
}

public class InkDispatch
{
  // 内部変数
  private InkStrokeContainer mContainer = null;
  private InkRecognizerContainer mRecognizer = null;
  private System.Windows.Threading.DispatcherTimer mDelayTimer = null;

  public InkDispatch()
  {
    mContainer = new InkStrokeContainer();
    mRecognizer = new InkRecognizerContainer();
    // 手書き認識 日本語をデフォルトに設定
    var reco = mRecognizer.GetRecognizers().FirstOrDefault(r => r.Name.Contains("日本語"));
    if (reco != null)
    {
      mRecognizer.SetDefaultRecognizer(reco);
    }

    // DispatcherTimer
    mDelayTimer = new System.Windows.Threading.DispatcherTimer();
    mDelayTimer.Interval = TimeSpan.FromSeconds(1.20d);
    mDelayTimer.Tick += DelayTimer_TickAsync;
  }
  ~InkDispatch()
  {
    mDelayTimer?.Stop();
    mDelayTimer = null;
    mContainer = null;
    mRecognizer = null;
  }

  public void Clear()
  {
    // 自動認識 - 停止
    AnalyzeTimerSusppend();

    // 蓄積ストローククリア - 再生成
    mContainer = new InkStrokeContainer();
  }

  public void AddStroke(InkStroke uwpStroke)
  {
    // 自動認識 - 停止
    AnalyzeTimerSusppend();

    // ストローク追加
    mContainer?.AddStroke(uwpStroke);

    // 自動認識 - 遅延実行
    AnalyzeTimerRestart();
  }  

  // 認識結果更新イベント
  public event DispatchUpdatedHandler DispatchUpdated;
  protected virtual void OnDispatchUpdated(DispatchUpdatedEventArgs e)
  {
    if (DispatchUpdated != null)
    {
      DispatchUpdated(this, e);
    }
  }

  // 自動認識 - 停止
  public void AnalyzeTimerSusppend()
  {
    bool bEnabled = mDelayTimer?.IsEnabled ?? false;
    if (bEnabled)
    {
      mDelayTimer?.Stop();
    }
  }

  // 自動認識 - 遅延実行
  public void AnalyzeTimerRestart()
  {
    bool bEnabled = mDelayTimer?.IsEnabled ?? false;
    if (!bEnabled)
    {
      mDelayTimer?.Start();
    }
  }

  // 自動認識
  private async void DelayTimer_TickAsync(object sender, object e)
  {
    // 自動認識 - 停止
    AnalyzeTimerSusppend();

    List<String> words = new List<string>();
    int count = mContainer?.GetStrokes()?.Count ?? 0;
    if (count > 0)
    {
      IReadOnlyList<InkRecognitionResult> results =
        await mRecognizer.RecognizeAsync(mContainer, InkRecognitionTarget.All);
      if (results.Count > 0)
      {
        foreach (InkRecognitionResult result in results)
        {
          foreach (string word in result.GetTextCandidates())
          {
            if (!String.IsNullOrEmpty(word))
            {
              words.Add(word);
            }
          }
        }
      }
    }
    DispatchUpdatedEventArgs arg = new DispatchUpdatedEventArgs(words);
    OnDispatchUpdated(arg);
  }
}
```