【WPF】学习笔记(一)——做一个简单的电子签名板
参加实习(WPF)已经有两个多周的时间了,踩了一些坑,也算积累了一些小东西,准备慢慢拿出来分享一下。(●'◡'●)
这次呢就讲讲一个简单的电子签名板的实现。
先上张图(PS:字写得比较丑,不要太在意哈):
1.任务目标
最基本的需求:1.签名功能 2.清除签名 3.保存签名(让用户选择文件夹、签名保存为PNG格式的图片)
尝试额外功能:1.Ctrl + Z实现撤销功能 2.Ctrl + Y实现重做功能 3.保存签名后打开文件位置并选中文件
2.搞事情
1)UI方面
如图,总体来说,一个InkCanvas加上两个Button就解决问题了。
A. InkCanvas
<InkCanvas Grid.Column="1" Grid.Row="1" Background="White" Height="240" Name="ink"> <InkCanvas.DefaultDrawingAttributes> <DrawingAttributes Color="#FF000000" StylusTip="Ellipse" Height="6" Width="6" IgnorePressure="False" FitToCurve="False"> <!--调整画笔形状--> <DrawingAttributes.StylusTipTransform> <!--https://msdn.microsoft.com/library/system.windows.media.matrix(v=vs.110).aspx--> <Matrix M11="1" M12="0" M21="0" M22="1" OffsetX="0" OffsetY="0"/> </DrawingAttributes.StylusTipTransform> </DrawingAttributes> </InkCanvas.DefaultDrawingAttributes> </InkCanvas>
关于调整画笔形状的部分(对,就是那个矩阵),就我个人来说并不是很了解,所以就不作什么解释了,感兴趣的童鞋可以访问对应的微软官方文档查看相关资料。
B. Button
<Button x:Name="btnClearSign" HorizontalAlignment="Left" VerticalAlignment="Top" Width="100" Padding="0" Margin="12,6,0,0" Click="btnClearSign_Click"> <Button.Template> <ControlTemplate> <Grid> <Label Cursor="Hand" Foreground="Red" FontFamily="Microsoft YaHei UI" FontSize="20"> <Underline> <Run Text="清除签名"></Run> </Underline> </Label> </Grid> </ControlTemplate> </Button.Template> </Button>
图中的两个按钮都是同一个套路,所以就只展示一个按钮的代码。(PS:为了让按钮显得不要太俗,我们为按钮弄一个类似于超链接的样式)
2)逻辑代码
签名功能我们就不用操心了,InkCanvas会处理好的。
A. 清除签名
ink.Strokes.Clear();
这么一行代码就足够了。说明一下,这里的ink就是我们在UI部分写的那个InkCanvas。
B.将签名保存为PNG图片
// 判断签名板内是否有内容 if (ink.Strokes.Any()) { // 让用户自己选择文件夹保存 // 需要在工程中添加对System.Windows.Forms的引用 // References => Add Reference => 勾选 System.Windows.Forms 项 => OK var folderPicker = new FolderBrowserDialog(); var res = folderPicker.ShowDialog(); // 判断用户有没有选中文件夹 if (res == System.Windows.Forms.DialogResult.Cancel) return; // 文件保存路径 var folderPath = folderPicker.SelectedPath; var fileName = DateTime.Now.ToString("yyyyMMddHHmmss"); var fileUri = folderPath + "\\" + fileName + ".png"; // windows系统下默认dpi貌似为96,但目前本机测试认为dpi设置为72较为合适 // dpi的大小会直接影响签名保存结果是否完整,关于dpi的知识网上还是比较多的,请各位自行了解 // 下一行代码的第三个参数用于确定位图的横向dpi,第四个参数为纵向dpi var renderBitmap = new RenderTargetBitmap((int)ink.ActualWidth, (int)ink.ActualHeight, 72d, 72d, PixelFormats.Pbgra32); renderBitmap.Render(ink); using (var stream = new FileStream(fileUri, FileMode.Create)) { var encoder = new PngBitmapEncoder(); encoder.Frames.Add(BitmapFrame.Create(renderBitmap)); encoder.Save(stream); } undoList.Clear(); // 打开签名文件所在位置 FileUtil.LocateFile(fileUri); } else { System.Windows.MessageBox.Show("尚未进行签名,不能执行保存操作!"); }
注:A.这个部分存在一定的问题,请容许我在另一篇的博客中进行相关解释。
B.代码中的undoList.Clear() 以及FileUtil.LocateFile(fileUri) 各位暂时不用理睬,稍后我会进行相关解释。
C.下方图片讲解的是如何添加对System.Windows.Forms的引用。
C.实现撤销和重做功能
由于InkCanvas自身实现貌似并没这样的方法,所以,我们就自己动动手吧。方法其实还是比较简单的:首先我们需要明白的是,InkCanvas将每一个笔划都以一个Stroke类的对象保存在一个集合里边(InkCanvas的Strokes属性,StrokeCollection类型)。所以,实现撤销/重做功能就变成了对一个Collection的操作,撤销即移除顶部的元素(当然我们需要将移除的元素暂存一下,以便后续的重做操作),重做即向Collection顶部增添一项。下面来看看代码:
Stack<Stroke> undoList = new Stack<Stroke>();
声明一个全局变量(Stroke的一个栈),用于存储进行撤销操作时移除的Stroke,也用于在进行重做功能时提供资源。
private void MainWindow_Loaded(object sender, RoutedEventArgs e) { this.KeyDown += (s, args) => { // Undo => 检测 Ctrl + Z if((Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control && args.Key == Key.Z) { if (ink.Strokes.Any()) { undoList.Push(ink.Strokes[ink.Strokes.Count - 1]); ink.Strokes.RemoveAt(ink.Strokes.Count - 1); } } // Redo => 检测 Ctrl + Y if ((Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control && args.Key == Key.Y) { if (undoList.Any()) { ink.Strokes.Add(undoList.Pop()); } } }; }
在Window的Loaded事件里加上对Ctrl + Z以及Ctrl + Y的检测,具体套路就如上方代码中显示的那样。
D.打开签名所在位置
先扯点题外话,这个地方我使用的时P/Invoke的方式,调用C++的方法进行实现的。由于我自己对跨语言调用这一块知之甚少,所以无法做出多少解释,只是在运气作用下一番摸索后达到了目的而已。如果以后感觉对这一块了解更多一些东西后,再单独写一篇博客进行相关解释。
回到正题,先上代码:
public static class FileUtil { /// <summary> /// 依据给定文件路径,打开文件位置并选中 /// </summary> /// <param name="path">文件完全路径</param> public static void LocateFile(string path) { /* // 此方法会导致每次新开一个文件资源管理器窗口,不喜欢 * string domain = ""; * var psi = new ProcessStartInfo("Explorer.exe"); * psi.Arguments = "/c,/select," + path; * domain = psi.Domain; * var p = Process.Start(psi); */ IntPtr ppidl = IntPtr.Zero; uint psfgaoOut; FileManager.SHParseDisplayName(path, IntPtr.Zero, out ppidl, 0, out psfgaoOut); var res = FileManager.OpenFolderAndSelectItems(ppidl, 0, IntPtr.Zero, 0); } class FileManager { [DllImport("shell32.dll", EntryPoint = "SHOpenFolderAndSelectItems")] public static extern long OpenFolderAndSelectItems(IntPtr pidlFolder, UInt32 cidl, IntPtr apidl, UInt32 dwFlags); [DllImport("shell32.dll", EntryPoint = "SHParseDisplayName")] public static extern void SHParseDisplayName([MarshalAs(UnmanagedType.LPWStr)] string name, IntPtr bindingContext, [Out()] out IntPtr pidl, uint sfgaoIn, [Out()] out uint psfgaoOut); } }
这个家伙又要开始偏(哔)题(哔)了,请不用理睬:
正如代码中所说的,注释的部分也可以在一定程度上实现我们的需求,但它存在一定的问题。所以我就果断寻求另一个解决方案,终于打探到shell32.dll(位于Windows\System32目录下)里的SHOpenFolderAndSelectItems方法可以满足我的需求。在经历了一段时间的搜索相关资料,又看了看这位哥的经验分享后,我终于用C#的方式把SHOpenFolderAndSelectItems方法怼成了上方代码中的模样。但是我悲催的发现,只有OpenFolderAndSelectItems方法貌似依旧不行(根本没有正确的定位到对应的文件/文件夹),在经过一番资料查阅[msdn, pinvoke.net]后,总算是搞出了个可用的版本。
3.Demo
https://files.cnblogs.com/files/lary/UserSignatureDemo.rar