记得前面(忘了是哪天写的,反正是前些天,请用力点击这里观看)老周讲了一个14393新增的控件,可以很轻松地结合InkCanvas来完成涂鸦。其实,InkCanvas除了涂鸦外,另一个大用途是墨迹识别,就是手写识别。
识别功能早在Win 8 App的API中就有了,到了UWP,同样使用,这叫传承,一路学过来,都是一个体系的,我不明白为什么某些人一遇到升级就说SDK变化太大,适应不了。我是不明白了,有什么适应不了的,该不会是你笨吧,或者学习方法不对。反正老周在以前的博客中都说过了,学习要学活,不要把知识学死了,把东西往死里学,就是古人所说的书呆子。
好了,不谈论书呆子的事了,因为“书呆子”在民间有太多的误解,咱们还是说正题。
处理数字墨迹有两种方式:
1、一种是脱离InkCanvas控件的方法,处理过程是面向笔触(Stroke)的,这就需要你手动去管理好你的墨迹数据了;
2、要是上一种方法太麻烦,与InkCanvas关联的做法较好,这样不用自己去搞UI部分的内容。
本着易用、久用、耐用、实用、妙用等伟大原则,我们实现手写识别还是不要脱离InkCanvas控件,这样的话实现起来会轻松很多,除非你要搞很高级的应用场景。
不讲过多的理论,免得大家看的头晕,老周简单说一个原理,大家懂了原理后,直接干活,这是学编程的万能招数。
先看看大致的步骤:
1、大家知道,InkCanvas有个关联的InkPresenter属性,引用的是InkPresenter实例,这个你得知道,不然后面的步骤就无法玩了。
2、InkPresenter类有个StrokeContainer属性,类型为InkStrokeContainer,它表示墨迹笔触的集合,被收集到的输入数据就存放到这个集合中。一个笔触通常是指你用笔/手指/鼠标按下时开始,直到你释放笔/手指/鼠标这一阶段中,所绘制出来的一段墨迹(从下笔到提笔)。一花一世界,一落一起一笔触。
3、实例化InkRecognizerContainer类,调用RecognizeAsync方法执行识别,上面为啥要提到InkStrokeContainer呢?因为执行识别需要它,你想啊,没有用户输入的墨迹(笔触)数据,一片空白,你识别个球。
4、识别后返回一个InkRecognitionResult列表,对于中文,通常只有一个InkRecognitionResult对象,但对于英文单词,可能会多个,一个InkRecognitionResult表示一个单词。对于一个InkRecognitionResult来说,访问GetTextCandidates方法返回一个字符串列表,即候选项,匹配度高的字符串排在前面。
5、也可以访问InkRecognizerContainer.GetRecognizers方法获取当前系统中已安装的语言识别引擎,中文系统至少会有一个简体中文的识别引擎。你可以到系统设置里面安装其他语言的引擎。
OK,基本思路有了,下面就可以做事情了。
首先,布置一下UI,XAML代码如下:
<Grid Margin="15"> <Grid.RowDefinitions> <RowDefinition Height="auto"/> <RowDefinition Height="300"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <ComboBox Name="cmbRecons" Header="选一个:" DisplayMemberPath="Name"/> <Border Background="LightGray" Grid.Row="1" Margin="2,6"> <InkCanvas Name="inkcv" /> </Border> <TextBlock Grid.Row="2" Name="tbresult" TextWrapping="Wrap" Foreground="Red" FontSize="24"/> </Grid>
ComboBox控件用来显示当前系统中安装的手写识别引擎,TextBlock用来显示识别结果。
现在,切换到代码视图,首先在页面类级别声明一个InkRecognizerContainer变量,并且实例化。
InkRecognizerContainer inkRecognContainer = new InkRecognizerContainer();
另外,还需要一个Timer,作用是在墨迹收集2秒钟后进行识别。
DispatcherTimer timer = new DispatcherTimer(); …… // 准备计时器 // 延迟2秒,应该不算慢吧 timer.Interval = TimeSpan.FromSeconds(2d); timer.Tick += onTimerTick; // 处理ink操作事件 inkcv.InkPresenter.StrokeInput.StrokeStarted += (k1, k2) => { // 人家正要下笔呢,没有在此时识别的道理 timer.Stop(); }; inkcv.InkPresenter.StrokesCollected += (t1, t2) => { // 墨迹已收集,可以进行识别 timer.Start(); };
当下笔开始书写时,会发生StrokeStarted事件,在此时,应该停止计时,你总不能人家一边写你就一边识别,没什么意思。但InkCanvas收集到输入笔触后,会发生StrokesCollected事件,这时候就可以开始计时了,2秒钟后进行识别。说白了就是在用户停止手写2秒钟后识别。
在ComboBox控件中显示系统已安装的识别引擎:
// 获取已安装的识别引擎列表 var inkrecogs = inkRecognContainer.GetRecognizers(); // 将这些列表显示到ComboBox控件中 cmbRecons.ItemsSource = inkrecogs; // 处理选项更改事件 cmbRecons.SelectionChanged += (s1, s2) => { // 将选中的识别引擎设为默认 InkRecognizer currec = (InkRecognizer)cmbRecons.SelectedItem; inkRecognContainer.SetDefaultRecognizer(currec); }; if (cmbRecons.Items.Count > 0) cmbRecons.SelectedIndex = 0;
当ComboBox控件做出选择后,引发SelectionChanged事件,在事件处理代码中可以调用SetDefaultRecognizer方法设置默认的识别引擎。
还有一件事,不要忘了,让InkCanvas支持笔、手触、鼠标来书写。
// 全能书写 inkcv.InkPresenter.InputDeviceTypes = Windows.UI.Core.CoreInputDeviceTypes.Mouse | Windows.UI.Core.CoreInputDeviceTypes.Touch | Windows.UI.Core.CoreInputDeviceTypes.Pen;
下面是核心代码,就是上面那个Timer的Tick事件处理,在处理代码中,执行手写识别,并显示识别的结果。
// 如果InkStrokeContainer中没有收集笔触,那就没有识别的必要了 // 所以Count应大于0 if (inkcv.InkPresenter.StrokeContainer.GetStrokes().Count > 0) { IReadOnlyList<InkRecognitionResult> results = await inkRecognContainer.RecognizeAsync(inkcv.InkPresenter.StrokeContainer, InkRecognitionTarget.All); // 处理结果 if (results.Count > 0) { StringBuilder strbd = new StringBuilder(); strbd.AppendLine("结果:"); // 每个InkRecognitionResult实例表示一个汉字/单词的识别结果 // 而单个结果中又包含候选列表,最接近的识别结果优先级更高 for(int x = 0; x < results.Count; x++) { string s = string.Join(",", results[x].GetTextCandidates().ToArray()); strbd.AppendLine(s); } // 显示结果 tbresult.Text = strbd.ToString(); // 清理墨迹 inkcv.InkPresenter.StrokeContainer.Clear(); } }
不是很复杂,代码你应该看得懂的,不然,学.NET这么多年,太对不起自己了。注意的是,识别后返回多个结果,对于中文,通常只返回一个,因为多个汉字是可以一起识别,并放到字符候选列表中。
在代码的最后面有这么一句:
inkcv.InkPresenter.StrokeContainer.Clear();
这句代码的作用是清除所收集的所有墨迹,清除后,InkCanvas会变回空白。
运行一下程序,然后手写一些字,看看识别效果。