专栏首页dino.c的专栏[WPF]为什么使用SaveFileDialog创建文件需要删除权限?

[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吧:

CustomSaveFileDialog

4. 参考

Common Item Dialog (Windows) Microsoft Docs

GetSaveFileNameA function (commdlg.h) - Win32 apps Microsoft Docs

OPENFILENAMEW (commdlg.h) - Win32 apps Microsoft Docs

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • [UWP]本地化入门

    在以前的XAML平台,resx资源文件是一种很方便的本地化方案,但在UWP中微软又再次推荐x:Uid方案,默认的资源文件也变成resw资源文件。虽然后缀名只差了...

    dino.c
  • [UWP]了解TypeConverter

    TypeConverter是XAML解释器的幕后功臣,它做了大量工作,从WPF诞生以来,几乎每一次XAML的运作都有它的参与。虽然UWP中TypeConvert...

    dino.c
  • [UWP 自定义控件]了解模板化控件(6):使用附加属性

    之前的ContentView2添加了PointerOver等效果,和TextBox等本来就有Header的控件放在一起反而变得鹤立鸡群。

    dino.c
  • 【LeetCode】 两个数组的交集

    韩旭051
  • 第2章 变量和基本类型

    用户1653704
  • 2.C++中的bool类型,三目运算符,引用

    在C++中,bool类型只有true(非0)和flase(0)两个值,且bool类型只占用了一个字节.

    张诺谦
  • 设置一个计划任务,到了时间就运行代码

    其实这篇文章的内容很简单,说白了就是设置一个计划任务,当触发到设定的条件后就运行。

    伪君子
  • 547. 两数组的交

    样例 nums1 = [1, 2, 2, 1], nums2 = [2, 2], 返回 [2].要求去重,这样还是稍微有点难度。

    和蔼的zhxing
  • 【每日算法Day 98】慈善赌神godweiyang教你算骰子点数概率!

    把 n 个骰子扔在地上,所有骰子朝上一面的点数之和为 s。输入 n,打印出 s 的所有可能的值出现的概率。

    godweiyang
  • iOS-自定义 UIButton-文字在左、图片在右(一)

    用户1890628

扫码关注云+社区

领取腾讯云代金券