參加實習(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
http://files.cnblogs.com/files/lary/UserSignatureDemo.rar