首页 > 代码库 > 【WPF】学习笔记(一)——做一个简单的电子签名板

【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("尚未进行签名,不能执行保存操作!");}

注:代码中的undoList.Clear() 以及FileUtil.LocateFile(fileUri) 各位暂时不用理睬,稍后我会进行相关解释。

     下方图片讲解的是如何添加对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

http://files.cnblogs.com/files/lary/Demo.rar

 

【WPF】学习笔记(一)——做一个简单的电子签名板