1. 问题#
好像很少人会遇到这种需求。假设有一个文件夹,用户有几乎所有权限,但没有删除的权限,如下图所示:
这时候使用SaveFileDialog在这个文件夹里创建文件居然会报如下错误:
这哪里是网络位置了,我又哪里去找个管理员?更奇怪的是,虽然报错了,但文件还是会创建出来,不过这是个空文件。不仅WPF,普通的记事本也会有这个问题,SaveFileDialog会创建一个空文件,记事本则没有被保存。具体可以看以下GIF:
2. 问题原因#
其实当SaveFileDialog关闭前,对话框会创建一个测试文件,用于检查文件名、文件权限等,然后又删除它。所以如果有文件的创建权限,而没有文件的删除权限,在创建测试文件后就没办法删除这个测试文件,这时候就会报错,而测试文件留了下来。
有没有发现SaveFileDialog中有一个属性Options?
Copy//// 摘要:// 获取 Win32 通用文件对话框标志,文件对话框使用这些标志来进行初始化。//// 返回结果:// 一个包含 Win32 通用文件对话框标志的 System.Int32,文件对话框使用这些标志来进行初始化。protected int Options { get; }
本来应该可以设置一个NOTESTFILECREATE的标志位,但WPF中这个属性是只读的,所以WPF的SaveFileDialog肯定会创建测试文件。
3. 解决方案#
SaveFileDialog本身只是Win32 API的封装,我们可以参考SaveFileDialog的源码,伪装一个调用方法差不多的MySaveFileDialog,然后自己封装GetSaveFileName这个API。代码大致如下:
internal class FOS { public const int OVERWRITEPROMPT = 0x00000002 ; public const int STRICTFILETYPES = 0x00000004 ; public const int NOCHANGEDIR = 0x00000008 ; public const int PICKFOLDERS = 0x00000020 ; public const int FORCEFILESYSTEM = 0x00000040 ; public const int ALLNONSTORAGEITEMS = 0x00000080 ; public const int NOVALIDATE = 0x00000100 ; public const int ALLOWMULTISELECT = 0x00000200 ; public const int PATHMUSTEXIST = 0x00000800 ; public const int FILEMUSTEXIST = 0x00001000 ; public const int CREATEPROMPT = 0x00002000 ; public const int SHAREAWARE = 0x00004000 ; public const int NOREADONLYRETURN = 0x00008000 ; public const int NOTESTFILECREATE = 0x00010000 ; public const int HIDEMRUPLACES = 0x00020000 ; public const int HIDEPINNEDPLACES = 0x00040000 ; public const int NODEREFERENCELINKS = 0x00100000 ; public const int DONTADDTORECENT = 0x02000000 ; public const int FORCESHOWHIDDEN = 0x10000000 ; public const int DEFAULTNOMINIMODE = 0x20000000 ; public const int FORCEPREVIEWPANEON = 0x40000000 ; } [ StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto) ] public class OpenFileName { internal int structSize = 0 ; internal IntPtr hwndOwner = IntPtr.Zero; internal IntPtr hInstance = IntPtr.Zero; internal string filter = null ; internal string custFilter = null ; internal int custFilterMax = 0 ; internal int filterIndex = 0 ; internal string file = null ; internal int maxFile = 0 ; internal string fileTitle = null ; internal int maxFileTitle = 0 ; internal string initialDir = null ; internal string title = null ; internal int flags = 0 ; internal short fileOffset = 0 ; internal short fileExtMax = 0 ; internal string defExt = null ; internal int custData = 0 ; internal IntPtr pHook = IntPtr.Zero; internal string template = null ; } public class LibWrap { // Declare a managed prototype for the unmanaged function. [ DllImport( "Comdlg32.dll" , SetLastError = true, ThrowOnUnmappableChar = true, CharSet = CharSet.Auto) ] public static extern bool GetSaveFileName ( [In, Out] OpenFileName ofn ) ; } public bool ? ShowDialog() { var openFileName = new OpenFileName(); Window window = Application.Current.Windows.OfType<Window>().Where(w => w.IsActive).FirstOrDefault(); if (window != null ) { var wih = new WindowInteropHelper(window); IntPtr hWnd = wih.Handle; openFileName.hwndOwner = hWnd; } openFileName.structSize = Marshal.SizeOf(openFileName); openFileName.filter = MakeFilterString(Filter); openFileName.filterIndex = FilterIndex; openFileName.fileTitle = new string ( new char [ 64 ]); openFileName.maxFileTitle = openFileName.fileTitle.Length; openFileName.initialDir = InitialDirectory; openFileName.title = Title; openFileName.defExt = DefaultExt; openFileName.structSize = Marshal.SizeOf(openFileName); openFileName.flags |= FOS.NOTESTFILECREATE | FOS.OVERWRITEPROMPT; if (RestoreDirectory) openFileName.flags |= FOS.NOCHANGEDIR; // lpstrFile // Pointer to a buffer used to store filenames. When initializing the // dialog, this name is used as an initial value in the File Name edit // control. When files are selected and the function returns, the buffer // contains the full path to every file selected. char [] chars = new char [FILEBUFSIZE]; for ( int i = 0 ; i < FileName.Length; i++) { chars[i] = FileName[i]; } openFileName.file = new string (chars); // nMaxFile // Size of the lpstrFile buffer in number of Unicode characters. openFileName.maxFile = FILEBUFSIZE; if (LibWrap.GetSaveFileName(openFileName)) { FileName = openFileName.file; return true ; } return false ; } /// <summary> /// Converts the given filter string to the format required in an OPENFILENAME_I /// structure. /// </summary> private static string MakeFilterString ( string s, bool dereferenceLinks = true ) { if ( string .IsNullOrEmpty(s)) { // Workaround for VSWhidbey bug #95338 (carried over from Microsoft implementation) // Apparently, when filter is null, the common dialogs in Windows XP will not dereference // links properly. The work around is to provide a default filter; " |*.*" is used to // avoid localization issues from description text. // // This behavior is now documented in MSDN on the OPENFILENAME structure, so I don't // expect it to change anytime soon. if (dereferenceLinks && System.Environment.OSVersion.Version.Major >= 5 ) { s = " |*.*" ; } else { // Even if we don't need the bug workaround, change empty // strings into null strings. return null ; } } StringBuilder nullSeparatedFilter = new StringBuilder(s); // Replace the vertical bar with a null to conform to the Windows // filter string format requirements nullSeparatedFilter.Replace( '|' , '\0' ); // Append two nulls at the end nullSeparatedFilter.Append( '\0' ); nullSeparatedFilter.Append( '\0' ); // Return the results as a string. return nullSeparatedFilter.ToString(); }
注意其中的这句:
CopyopenFileName.flags |= FOS.NOTESTFILECREATE | FOS.OVERWRITEPROMPT;
因为我的需求就是不创建TestFile,所以我直接这么写而不是提供可选项。一个更好的方法是给WPF提ISSUE,我已经这么做了:
Make SaveFileDialog support NOTESTFILECREATE.
但看来我等不到有人处理的这天,如果再有这种需求,还是将就着用我的这个自创的SaveFileDialog吧:
4. 参考#
Common Item Dialog (Windows) Microsoft Docs
GetSaveFileNameA function (commdlg.h) - Win32 apps Microsoft Docs
相关推荐
0评论