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,本文就扯到这里了,开饭,不然饭菜凉了。