IT界最近這幾年,各種亂七八糟的東西不斷出現,其中能用在實際工作與生活中的,大概也就那么幾個。Web 前端也冒出各種框架,這就為那些喜歡亂用框架的公司提供了很好的機會,於是造成很多項目體積越來越龐大,越來越難維護。一切變得越來越沒有標准,所以,很多公司在招聘碼農時就特能亂寫,還要求你精通 AA,BB,CC,DD,EE,FF,GG……甚至有的不下二三十項要求。老周覺得這些公司基本上是神經病,先不說世界沒有人能精通那么多東西,就算真有人能精通那么多,那估計這個人也活不久了,早晚得累死的。
實際上,Web 前端你能學會三樣東西就夠了——HTML、CSS、JS,其他純屬娛樂。
所以,學習編程的話,你抓幾個有代表性地學就好了,比如C/C++,.net,PHP,Java 這些,其余的嘛,現學現用,用完就扔。你要是想讓自己變成高手的話,那你就必須挑一個方向,縱向深度發展。什么都學等於什么都不通,學亂七八糟的東西是成不了高手的。就拿黑客這一活兒來說,只有第一代,第二代黑客比較強,后面的基本是菜鳥,一代不如一代。沒辦法,浮躁的時代,IT業也不可幸免的。
好了,上面的都是P話,下面老周開始說正題,今天咱們談談如何將電子墨跡保存到圖像。在近年來出現的各種花拳綉腿技術中,電子墨跡還算是有實用價值的東西。還有觸控、虛擬化這些,也有一定的用途。人工智障倒是可有可無,可作為輔助,但不太可靠,最起碼它代替不了人腦(笨蛋例外),我估計將來搞藝術可能吃香,畢竟機器是不懂藝術的。普工可能會大量失業,因為他們做的事情可以讓機器做了(主要是重復性,機械性的工作)。
拿筆寫字是人的本能,千萬不要鼠標鍵盤用多了連筆都拿不動(這已經是“鼠標手”的輕度症狀了,不及時治療,以后會很難看的)。科技再發達,人類的本能絕不能丟,就好比哪天你連穿衣吃飯都不會了,那你活該餓死。
本文就介紹兩種比較簡單的方法:
第一種是運用 win 2D 封裝的功能來完成。老周做的那個“練字神器”應用就是用這種方法保存你的書法作品的,其中的宣紙紙紋原理也很簡單,就是分層繪制,首先在底層繪制紙張的紋理圖案,然后再把墨跡繪制到底紋之上即可。
第二種不需要借助其他 Nuget 上的庫,只要使用 1709 最新的 API 就能實現。
先說第一種方案。
為了演示,老周就做簡單一點。下面 XAML 代碼在界面上聲明了一個 InkCanvas ,用來收集輸入的墨跡,然后一個 Button ,點擊后選擇文件路徑,然后保存為 png 圖片。
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition Height="auto"/> </Grid.RowDefinitions> <InkCanvas Name="inkcv"/> <Button Content="保存墨跡" Click="OnClick" Grid.Row="1" Margin="2,9.5"/> </Grid>
接着,你要打開 nuget 管理器,向項目添加 Win 2D 的引用。這個老周不多說了,你懂怎么操作的。
如果你繪制的墨跡圖像需要在界面上顯示,可以用 CanvasControl 控件,然后處理 Draw 事件,如果不需要在界面上顯示,例如這個例子,我們是直接保存為圖像文件的,所以不需要在界面上添加 CanvasControl 元素了。
前面在寫 UI Composition 的文章時,老周曾用過 Win 2D 做演示,負責繪制操作的是 CanvasDrawingSession 類,其中,你會發現,它有一個方法叫 DrawInk,對的,我們用的就是它,它可以把我們從用戶輸入收集到的墨跡繪制下來。它有兩個重載,其中一個是指定是否繪制成高對比度模式。
好,理論上的屁話不多說,我直接上代碼,你一看就懂的。
不過,在頁面類的構造函數中,我們得先設置一下書寫的參數,比如筆觸大小、顏色等。
public MainPage() { this.InitializeComponent(); // 支持筆,手觸,鼠標輸入 inkcv.InkPresenter.InputDeviceTypes = Windows.UI.Core.CoreInputDeviceTypes.Mouse | Windows.UI.Core.CoreInputDeviceTypes.Pen | Windows.UI.Core.CoreInputDeviceTypes.Touch; // 設定筆跡顏色為紅色 InkDrawingAttributes data = new InkDrawingAttributes(); data.Color = Colors.Red; // 筆觸大小 data.Size = new Size(15d, 15d); // 忽略筆的傾斜識別,畢竟只有新型的筆才有這感應 data.IgnoreTilt = true; // 更新參數 inkcv.InkPresenter.UpdateDefaultDrawingAttributes(data); }
隨后就可以處理 Button 的 Click 事件了。
private async void OnClick(object sender, RoutedEventArgs e) { // 如果沒有輸入墨跡,那就別浪費 CPU 時間了 if(inkcv.InkPresenter.StrokeContainer.GetStrokes().Any() == false) { return; } // 選擇保存文件 FileSavePicker picker = new FileSavePicker(); picker.FileTypeChoices.Add("PNG 圖像", new string[] { ".png" }); picker.SuggestedFileName = "sample"; picker.SuggestedStartLocation = PickerLocationId.Desktop; StorageFile file = await picker.PickSaveFileAsync(); if (file == null) return; // 建一個在內存中用的畫板(不顯示在 UI 上) // 獲取共享的 D2D 設備引用 CanvasDevice device = CanvasDevice.GetSharedDevice(); // 圖像大小與 InkCanvas 控件大小相同 float width = (float)inkcv.ActualWidth; float height = (float)inkcv.ActualHeight; // DPI 為 96 float dpi = 96f; CanvasRenderTarget drawtarget = new CanvasRenderTarget(device, width, height, dpi); // 開始作畫 using(var drawSession = drawtarget.CreateDrawingSession()) { // 我們上面設置了用的是紅筆 // 為了生成圖片后看得清楚 // 把牆刷成白色 drawSession.Clear(Colors.White); // 畫墨跡 drawSession.DrawInk(inkcv.InkPresenter.StrokeContainer.GetStrokes()); } // 保存到輸出文件 await drawtarget.SaveAsync(await file.OpenAsync(FileAccessMode.ReadWrite), CanvasBitmapFileFormat.Png, 1.0f); // 釋放資源 drawtarget.Dispose(); }
運行應用后,隨便寫點啥上去。如下圖。
然后點擊按鈕,保存一下。生成的圖片如下圖所示。
好,第一種方案完結,接下來咱們用第二種方案。
這是 1709 (秋季創作者更新)的新功能。新的 SDK 中增加了一個 CoreInkPresenterHost 類(位於 Windows.UI.Input.Inking.Core 命名空間),使用這類,你可以不需要 InkCanvas 控件,你可以把墨跡接收圖面放到任意的 XAML 元素上。因為該類公開一個 RootVisual 屬性,注意它不是指向 XAML 可視化元素,而是 ContainerVisual 對象。這是 UI Composition 中的容器類。
老周前不久剛寫過一堆與 UI Composition 有關的文章,如果你不了解相關內容,可以看老周前面的爛文。通過前面對 UI Composition 的學習,我們知道,可以將可視化對象添加到任意 XAML 可視化元素上。對,這個 CoreInkPresenterHost 類就是運用了這個特點,使得墨跡收集可以脫離 InkCanvas 控件,以后,你愛在哪個元素上收集墨跡都行,比如,你想讓用戶可以對圖像進行塗鴉,你就可以把這個類放到 Image 元素上。
P話少說,咱們來點干貨。下面的例子,其界面和前一個例子相似,只是沒有用上 InkCanvas 控件,而只是聲明了個 Border 元素。
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition Height="auto"/> </Grid.RowDefinitions> <Border Name="bd" Margin="3" BorderThickness="1" BorderBrush="Green"/> <Button Grid.Row="1" Margin="4,8" Content="保存墨跡" Click="OnClick"/> </Grid>
然后切換到代碼文件,在頁面類的構造函數中,進行一下初始化。初始化的東西挺多,包括用 Compositor 創建用來承載墨跡的容器 Visual ,以及設置筆觸參數。
CoreInkPresenterHost inkHost = null; public MainPage() { this.InitializeComponent(); // 組裝一個 UI,把一個可視化容器放到 Border 上 Visual bdvisual = ElementCompositionPreview.GetElementVisual(bd); var compositor = bdvisual.Compositor; // 創建一個容器 ContainerVisual inkContainer = compositor.CreateContainerVisual(); // 此時因為各元素的寬度和高度都為0,所以用動畫來更新容器的大小 var expressAnimate = compositor.CreateExpressionAnimation(); expressAnimate.Expression = "bd.Size"; expressAnimate.SetReferenceParameter("bd", bdvisual); inkContainer.StartAnimation("Size", expressAnimate); // 設置容器與 Border 關聯 ElementCompositionPreview.SetElementChildVisual(bd, inkContainer); // 處理墨跡收集關聯 inkHost = new CoreInkPresenterHost(); inkHost.RootVisual = inkContainer; inkHost.InkPresenter.InputDeviceTypes = Windows.UI.Core.CoreInputDeviceTypes.Mouse | Windows.UI.Core.CoreInputDeviceTypes.Pen | Windows.UI.Core.CoreInputDeviceTypes.Touch; // 設置筆觸參數 InkDrawingAttributes attrib = new InkDrawingAttributes(); attrib.Color = Colors.SkyBlue; attrib.Size = new Size(15f, 15f); attrib.IgnoreTilt = true; // 更新參數 inkHost.InkPresenter.UpdateDefaultDrawingAttributes(attrib); }
創建了容器 Visual 后,記得要通過 CoreInkPresenterHost 對象的 RootVisual 屬性來關聯。當然你不能忘了把這個 visual 加到 Border 的子元素序列上。
現在處理 Click 事件,用 RenderTargetBitmap 類,把 Border 的內容畫出來,這樣會連同它上面的墨跡也一起畫出來。
// 這個類可以繪制 XAML 元素,以前介紹過 RenderTargetBitmap rtarget = new RenderTargetBitmap(); await rtarget.RenderAsync(bd);
然后用圖像編碼器寫入文件就行了。
// 獲取像素數據 var pxBuffer = await rtarget.GetPixelsAsync(); // 開始為圖像編碼 using(var stream = await outFile.OpenAsync(FileAccessMode.ReadWrite)) { BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, stream); encoder.SetPixelData(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied, (uint)rtarget.PixelWidth, (uint)rtarget.PixelHeight, 96d, 96d, pxBuffer.ToArray()); await encoder.FlushAsync(); }
完整的事件處理代碼如下。
private async void OnClick(object sender, RoutedEventArgs e) { if (inkHost.InkPresenter.StrokeContainer.GetStrokes().Any() == false) return; FileSavePicker picker = new FileSavePicker(); picker.FileTypeChoices.Add("PNG 圖像文件", new string[] { ".png" }); picker.SuggestedFileName = "sample"; StorageFile outFile = await picker.PickSaveFileAsync(); if (outFile == null) return; // 這個類可以繪制 XAML 元素,以前介紹過 RenderTargetBitmap rtarget = new RenderTargetBitmap(); await rtarget.RenderAsync(bd); // 獲取像素數據 var pxBuffer = await rtarget.GetPixelsAsync(); // 開始為圖像編碼 using(var stream = await outFile.OpenAsync(FileAccessMode.ReadWrite)) { BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, stream); encoder.SetPixelData(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied, (uint)rtarget.PixelWidth, (uint)rtarget.PixelHeight, 96d, 96d, pxBuffer.ToArray()); await encoder.FlushAsync(); } }
好,完事了,現在運行一下,直接中 Border 元素上寫點東東。
然后點擊底部的按鈕保存為圖片,如下圖所示。
OK,本文就扯到這里了,開飯,不然飯菜涼了。