0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C# - InkRecognizerContainer - WPF InkCanvasでの利用

Posted at

はじめに

C# - InkRecognizerContainer - Windows FormsとWPFでの利用 では、UWP - InkCanvas を利用した方法を記載しましたが、WPF - InkCanvas を利用する手法を記載します。

テスト環境

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

  • WPF - .NET Framework 4.8

UWP - InkRecognizerContainer 利用設定

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

  • Windows SDK 導入
    • Windows SDK | Microsoft Developer から、ターゲットプラットフォームに該当する 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 のペア利用を、上記ストロークデータの置換で行うというアプローチです。

// 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 で配置します。

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;
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)イベントハンドラを作成します。

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

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

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

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 イベントを発生させます。

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?