[WPF]为什么使用SaveFileDialog创建文件需要删除权限?
1. 问题
好像很少人会遇到这种需求。假设有一个文件夹,用户有几乎所有权限,但没有删除的权限,如下图所示:
这时候使用savefiledialog在这个文件夹里创建文件居然会报如下错误:
这哪里是网络位置了,我又哪里去找个管理员?更奇怪的是,虽然报错了,但文件还是会创建出来,不过这是个空文件。不仅wpf,普通的记事本也会有这个问题,savefiledialog会创建一个空文件,记事本则没有被保存。具体可以看以下gif:
2. 问题原因
其实当savefiledialog关闭前,对话框会创建一个测试文件,用于检查文件名、文件权限等,然后又删除它。所以如果有文件的创建权限,而没有文件的删除权限,在创建测试文件后就没办法删除这个测试文件,这时候就会报错,而测试文件留了下来。
有没有发现savefiledialog
中有一个属性options?
// // 摘要: // 获取 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(); }
注意其中的这句:
openfilename.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