17611538698
webmaster@21cto.com

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

资讯 0 4675 2017-04-27 11:55:19
参加实习(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/lib ... atrix(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

 

评论