記得前面(忘了是哪天寫的,反正是前些天,請用力點擊這里觀看)老周講了一個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會變回空白。
運行一下程序,然后手寫一些字,看看識別效果。