Unity在Windows操作系统下使用保存和打开文件的对话框

前言

        这里我仅说明使用CSharp的情况。其实网上已经有很多类似的文章,你在网上搜便能找到一堆。下面的代码其实也是我从网络上抄过来的。但是大多数的文章就仅仅是给代码然后不说为什么这样。如果只是满足当前的需求,那这样做也没什么问题。只是当这些代码出现了一些问题,或者突然有些需求要进行扩展的时候,这些如同黑盒子一样的代码就显得麻烦了。

        因为我很懒,所以我这里的代码说明也只到我现在使用的程度。

环境

    Windows10
    Unity 2022.3.8f1c1

正文

        要想在Windows操作系统下打开其保存和打开文件的对话框,我们必须要使用其Comdlg32.dll。如果你有搜索并看过这些方面的文章。你会发现文章中都会预先让你声明一个结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct FileOpenDialog
{
public int structSize; //结构的内存大小
public IntPtr dlgOwner; //设置对话框的句柄
public IntPtr instance; //根据flags标志的设置,确定instance是谁的句柄,不设置则忽略
public string filter; //调取文件的过滤方式
public string customFilter; //一个静态缓冲区 用来保存用户选择的筛选器模式
public int maxCustFilter; //缓冲区的大小
public int filterIndex; //指向的缓冲区包含定义过滤器的字符串对
public string file; //存储调取文件路径
public int maxFile; //存储调取文件路径的最大长度 至少256
public string fileTitle; //调取的文件名带拓展名
public int maxFileTitle; //调取文件名最大长度
public string initialDir; //最初目录
public string title; //打开窗口的名字
public int flags; //初始化对话框的一组位标志 参数类型和作用查阅官方API
public short fileOffset; //文件名前的长度
public short fileExtension; //拓展名前的长度
public string defExt; //默认的拓展名
public IntPtr custData; //传递给lpfnHook成员标识的钩子子程的应用程序定义的数据
public IntPtr hook; //指向钩子的指针。除非Flags成员包含OFN_ENABLEHOOK标志,否则该成员将被忽略。
public string templateName; //模块中由hInstance成员标识的对话框模板资源的名称
public IntPtr reservedPtr;
public int reservedInt;
public int flagsEx; //可用于初始化对话框的一组位标志
}

这个结构体是用来和Comdlg32.dll中的GetOpenFileNameGetSaveFileName进行信息传输的类。这个其实是OPENFILENAME(关于其信息和上述变量的意思,可查看微软的文档:OPENFILENAMEA 结构 (commdlg.h))的封装实现。

2025.2.27修正:将类改为结构体。这里并不能使用类来做操作,这样会导致在选取多个文件时结果错误,具体缘由我未能找到。我只能猜测是因为类和结构体在赋值方式上的不同导致的。

我们为了调用Comdlg32.dll中的GetOpenFileNameGetSaveFileName。我们需要写入下面的代码:

1
2
3
4
5
6
7
8
public class DialogShow
{
[DllImport("Comdlg32.dll", SetLastError = true, ThrowOnUnmappableChar = true, CharSet = CharSet.Auto)]
public static extern bool GetOpenFileName([In, Out] FileOpenDialog dialog);

[DllImport("Comdlg32.dll", SetLastError = true, ThrowOnUnmappableChar = true, CharSet = CharSet.Auto)]
public static extern bool GetSaveFileName([In, Out] FileOpenDialog dialog);
}

而在微软文档中也是有类似的声明(下面只显示GetOpenFileName文档的声明)

1
2
3
BOOL GetOpenFileNameA(
[in, out] LPOPENFILENAMEA unnamedParam1
);

虽然这里的函数名字为GetOpenFileNameA,但是其实就是GetOpenFileName。接下来我们就要进行具体的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 打开文件
public static string OpenFile(string title, string openPath = "")
{
FileOpenDialog dialog = new FileOpenDialog();
dialog.structSize = Marshal.SizeOf(dialog);
dialog.filter = "Json Files(*json文件)\0*.json\0";
dialog.file = new string(new char[256]);
dialog.maxFile = dialog.file.Length;
dialog.fileTitle = new string(new char[64]);
dialog.maxFileTitle = dialog.fileTitle.Length;
dialog.initialDir = string.IsNullOrEmpty(openPath) ? UnityEngine.Application.dataPath : openPath; //默认路径
dialog.title = title;
dialog.defExt = null; //显示文件的类型
//注意一下项目不一定要全选 但是0x00000008项不要缺少
dialog.flags =
0x00080000 | 0x00001000 | 0x00000800 | 0x00000200 |
0x00000008; //OFN_EXPLORER|OFN_FILEMUSTEXIST|OFN_PATHMUSTEXIST| OFN_ALLOWMULTISELECT|OFN_NOCHANGEDIR

if (DialogShow.GetOpenFileName(dialog))
{
return (dialog.file);
}
return "";
}

// 保存文件
public static string SaveFile(string fileName, string title, string openPath = "",string filter = "",string defExt = "")
{

FileOpenDialog pth = new FileOpenDialog();
pth.structSize = Marshal.SizeOf(pth);
pth.filter = string.IsNullOrEmpty(filter) ? "All Files\0*.*\0\0" : filter;//文件类型
pth.file = new string(new char[256]);
pth.maxFile = pth.file.Length;
pth.file = fileName;//保存文件的默认名字
//fileName就是默认值, pth.file = fileName这个语句需要放在 pth.maxFile = pth.file.Length语句后面,要不然会一直报错。
pth.fileTitle = new string(new char[64]);
pth.maxFileTitle = pth.fileTitle.Length;
pth.initialDir = string.IsNullOrEmpty(openPath) ? UnityEngine.Application.dataPath : openPath; //默认路径
pth.title = title;//
pth.defExt = defExt;
pth.flags = 0x00080000 | 0x00001000 | 0x00000800 | 0x00000200 | 0x00000008 | 0x00000002;
if (DialogShow.GetSaveFileName(pth))
{

return (pth.file);
}
return "";
}

其中关于其变量设置的说明在OPENFILENAMEA 结构 (commdlg.h)文档中都有。这其中特别是关于flags变量的赋值。其不同的赋值会导致我们所设定的变量值不同。具体的大家还是看文档吧。这里我特别说明一下filter变量,虽然你们看文档也能知道这点,但我还是额外说明一下。文档中对于其的部分描述:

1
每对中的第一个字符串是描述筛选器 (的显示字符串,例如,“文本文件”) ,第二个字符串指定筛选器模式 (例如 ".TXT" ,) 。 若要为单个显示字符串指定多个筛选器模式,请使用分号分隔模式 (例如 “.TXT;.DOC;”。BAK“) 。 模式字符串可以是有效文件名字符和星号 (*) 通配符的组合。 请勿在模式字符串中包含空格。

从此描述中,其实我们可传两个字符串。但是我们实际上将其声明为一个单独的string类型,所以这里我使用了\0来分隔字符串。关于\0,我在网上找到了这篇文章——关于字符串中‘\0‘的坑。 完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
using System;
using System.Runtime.InteropServices;
using UnityEngine;

public class SelfOpenFile
{
/// <summary>
/// 文件类
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct FileOpenDialog
{
public int structSize;
public IntPtr dlgOwner;
public IntPtr instance;
public String filter;
public String customFilter;
public int maxCustFilter;
public int filterIndex;
public String file;
public int maxFile;
public String fileTitle;
public int maxFileTitle;
public String initialDir;
public String title;
public int flags;
public short fileOffset;
public short fileExtension;
public String defExt;
public IntPtr custData;
public IntPtr hook;
public String templateName;
public IntPtr reservedPtr;
public int reservedInt;
public int flagsEx;
}

/// <summary>
/// 文件夹类
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct OpenDialogDir
{
public IntPtr hwndOwner;
public IntPtr pidlRoot;
public String pszDisplayName;
public String lpszTitle;
public UInt32 ulFlags;
public IntPtr lpfn;
public IntPtr lParam;
public int iImage;
}


public class DialogShow
{
[DllImport("Comdlg32.dll", SetLastError = true, ThrowOnUnmappableChar = true, CharSet = CharSet.Auto)]
public static extern bool GetOpenFileName([In, Out] FileOpenDialog dialog);

[DllImport("Comdlg32.dll", SetLastError = true, ThrowOnUnmappableChar = true, CharSet = CharSet.Auto)]
public static extern bool GetSaveFileName([In, Out] FileOpenDialog dialog);

[DllImport("shell32.dll", SetLastError = true, ThrowOnUnmappableChar = true, CharSet = CharSet.Auto)]
public static extern IntPtr SHBrowseForFolder([In, Out] OpenDialogDir ofn);

[DllImport("shell32.dll", SetLastError = true, ThrowOnUnmappableChar = true, CharSet = CharSet.Auto)]
public static extern bool SHGetPathFromIDList([In] IntPtr pidl, [In, Out] char[] fileName);
}

public static string OpenFile(string title, string openPath = "")
{
FileOpenDialog dialog = new FileOpenDialog();
dialog.structSize = Marshal.SizeOf(dialog);
dialog.filter = Getfilter();
dialog.file = new string(new char[256]);
dialog.maxFile = dialog.file.Length;
dialog.fileTitle = new string(new char[64]);
dialog.maxFileTitle = dialog.fileTitle.Length;
dialog.initialDir = string.IsNullOrEmpty(openPath) ? UnityEngine.Application.dataPath : openPath; //默认路径
dialog.title = title;
dialog.defExt = null; //显示文件的类型
//注意一下项目不一定要全选 但是0x00000008项不要缺少
dialog.flags =
0x00080000 | 0x00001000 | 0x00000800 | 0x00000200 |
0x00000008; //OFN_EXPLORER|OFN_FILEMUSTEXIST|OFN_PATHMUSTEXIST| OFN_ALLOWMULTISELECT|OFN_NOCHANGEDIR

if (DialogShow.GetOpenFileName(dialog))
{
return (dialog.file);
}
return "";
}

/// <summary>
/// 保存文件
/// </summary>
public static string SaveFile(string fileName, string title, string openPath = "",string filter = "",string defExt = "")
{

FileOpenDialog pth = new FileOpenDialog();
pth.structSize = Marshal.SizeOf(pth);
pth.filter = string.IsNullOrEmpty(filter) ? "All Files\0*.*\0\0" : filter;//文件类型
pth.file = new string(new char[256]);
pth.maxFile = pth.file.Length;
pth.file = fileName;//保存文件的默认名字
//fileName就是默认值, pth.file = fileName这个语句需要放在 pth.maxFile = pth.file.Length语句后面,要不然会一直报错。
pth.fileTitle = new string(new char[64]);
pth.maxFileTitle = pth.fileTitle.Length;
pth.initialDir = string.IsNullOrEmpty(openPath) ? UnityEngine.Application.dataPath : openPath; //默认路径
pth.title = title;//
pth.defExt = defExt;
pth.flags = 0x00080000 | 0x00001000 | 0x00000800 | 0x00000200 | 0x00000008 | 0x00000002;
if (DialogShow.GetSaveFileName(pth))
{

return (pth.file);
}
return "";
}

public static string ChooseDictionary(string title, string openPath = "")
{
OpenDialogDir openDir = new OpenDialogDir();
openDir.pszDisplayName = new string(new char[2000]);
openDir.lpszTitle = title;
openDir.ulFlags = 1;// BIF_NEWDIALOGSTYLE | BIF_EDITBOX;
IntPtr pidl = DialogShow.SHBrowseForFolder(openDir);

char[] path = new char[2000];
for (int i = 0; i < 2000; i++)
path[i] = '\0';
if (DialogShow.SHGetPathFromIDList(pidl, path))
{
string str = new string(path);
string DirPath = str.Substring(0, str.IndexOf('\0'));
//Debug.LogError("路径" + DirPath);
return DirPath;
}

return "";
}

static string Getfilter()
{
string filter = "Json Files(*json文件)\0*.json\0";

return filter;
}
}

吐槽

        这篇文章好像有点水,不过很多东西在微软的文章中也说的够详细了,想要直接使用的人也就只要赋值完整的代码用就好了。所以我这不算水吧(手动狗头)。

参考文章

  1. OPENFILENAMEA 结构 (commdlg.h):https://learn.microsoft.com/zh-cn/windows/win32/api/commdlg/ns-commdlg-openfilenamea
  2. GetOpenFileName文档:https://learn.microsoft.com/zh-cn/windows/win32/api/commdlg/nf-commdlg-getopenfilenamea
  3. 关于字符串中‘\0‘的坑:https://blog.csdn.net/qq_37286579/article/details/129926484