• 【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(位于WindowsSystem32目录下)里的SHOpenFolderAndSelectItems方法可以满足我的需求。在经历了一段时间的搜索相关资料,又看了看这位哥的经验分享后,我终于用C#的方式把SHOpenFolderAndSelectItems方法怼成了上方代码中的模样。但是我悲催的发现,只有OpenFolderAndSelectItems方法貌似依旧不行(根本没有正确的定位到对应的文件/文件夹),在经过一番资料查阅[msdn, pinvoke.net]后,总算是搞出了个可用的版本。

    3.Demo

    https://files.cnblogs.com/files/lary/UserSignatureDemo.rar

  • 相关阅读:
    LeetCode24-Swap_Pairs
    LeeCode
    LeetCode3-Longest_Substring_Without_Repeating_Characters
    治愈 JavaScript 疲态的学习计划【转载】
    前端冷知识集锦[转载]
    知道这20个正则表达式,能让你少写1,000行代码[转载]
    关于简历和面试【整理自知乎】
    正念冥想方法
    一些职场经验【转载自知乎】
    犹太复国计划向世界展现了一个不一样的民族——观《犹太复国血泪史》有感
  • 原文地址:https://www.cnblogs.com/lary/p/6784171.html
Copyright © 2020-2023  润新知