天天看點

【Win 10 應用開發】将墨迹儲存到圖像的兩種方法

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

運作應用後,随便寫點啥上去。如下圖。

【Win 10 應用開發】将墨迹儲存到圖像的兩種方法

 然後點選按鈕,儲存一下。生成的圖檔如下圖所示。

【Win 10 應用開發】将墨迹儲存到圖像的兩種方法

 好,第一種方案完結,接下來咱們用第二種方案。

這是 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 元素上寫點東東。

【Win 10 應用開發】将墨迹儲存到圖像的兩種方法

然後點選底部的按鈕儲存為圖檔,如下圖所示。

【Win 10 應用開發】将墨迹儲存到圖像的兩種方法

OK,本文就扯到這裡了,開飯,不然飯菜涼了。