はじめに
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 で配置します。
<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;
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 イベントで、マウス/ペン/タッチ全てのストローク開始を捕捉できます。
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;
}
}
消去ボタン クリックイベントハンドラに対して、手書き認識クラスのクリアを追加します。
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 イベントを発生させます。
// 手書き自動認識 認識結果更新イベント
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);
}
}