【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

 

posted @ 2017-04-29 01:34  小醉魔  阅读(3410)  评论(1编辑  收藏  举报