在 Unity 中实现 Windows 选择文件

🥦实现思路

在 Windows 上实现存档的导入功能, 可以直接调用系统的文件管理器, 选择我们指定类型的文件, 之后代码中进行处理即可

🌵调用文件管理器

系统提供了 GetOpenFileName 函数, 可以调用 Windows 文件管理器, 以下是此函数的使用步骤

新建数据类

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)]
internal class OpenFileName
{
public int structSize;
public IntPtr dlgOwner;
public IntPtr instance;
public string filter;
public string customFilter;
public int maxCustFilter;
public int filterIndex;
public IntPtr file; // 新风格不能使用 string 存储返回值, 这里使用 IntPtr 存储返回值
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;
}

引入 DLL

1
2
3
4
5
/// <summary>
/// 打开文件对话框
/// </summary>
[DllImport("ComDLG32.dll", SetLastError = true, ThrowOnUnmappableChar = true, CharSet = CharSet.Auto)]
internal static extern bool GetOpenFileName([In, Out] OpenFileName openFileName);

准备数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var openFileName = new OpenFileName();

openFileName.structSize = Marshal.SizeOf(openFileName);
openFileName.filter = filter;
openFileName.fileTitle = new string(new char[64]);
openFileName.maxFileTitle = openFileName.fileTitle.Length;
openFileName.initialDir = defaultFolder;
openFileName.title = windowTitle;
openFileName.templateName = string.Empty;
openFileName.flags = 0x00000008 | 0x00000200 | 0x00000800 | 0x00001000 | 0x00080000;

// 分配 2KB 缓冲区
const int BUFFER_SIZE = 2048;
var fileBuffer = Marshal.AllocHGlobal(BUFFER_SIZE * Marshal.SystemDefaultCharSize);

// 清空缓冲区,防止残留数据影响
for (var index = 0; index < BUFFER_SIZE; index++)
{
Marshal.WriteByte(fileBuffer, index, 0);
}

// 赋值缓冲区
openFileName.file = fileBuffer;
openFileName.maxFile = BUFFER_SIZE;

打开管理器

1
2
3
4
5
6
// 选取文件
if (GetOpenFileName(openFileName) == false)
{
selectData = null;
return false;
}

处理返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
selectedFilesList.Clear();

var pointer = fileBuffer;

while (true)
{
var file = Marshal.PtrToStringAuto(pointer);

if (string.IsNullOrEmpty(file))
{
break;
}

selectedFilesList.Add(file);

pointer += (file.Length + 1) * Marshal.SystemDefaultCharSize;
}

selectData = selectedFilesList;
return selectedFilesList.Count > 0;

释放内存

1
2
// 释放分配的缓冲区内存
Marshal.FreeHGlobal(fileBuffer);

🦄总结

  1. 强烈建议使用新风格的文件管理器, 虽然旧风格的文件管理器返回的值用 空格 来分割, 处理起来更简单, 但是界面巨丑, 操作巨反人类
  2. 使用新风格的文件管理器时要注意, 一定不要使用 string 类型存储返回值, 因为返回值使用 \0 来分割, 而 C# 中的字符串认为 \0 是字符串的结束, 因此后面的信息就丢失了
  3. 记得及时释放申请的缓冲区内存

👀完整代码

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
// ReSharper disable NotAccessedField.Global

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;

namespace Kuroha.Utility
{
public class FileUtilWindows
{
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
internal class OpenFileName
{
public int structSize;
public IntPtr dlgOwner;
public IntPtr instance;
public string filter;
public string customFilter;
public int maxCustFilter;
public int filterIndex;
public IntPtr file; // 新风格不能使用 string 存储返回值, 这里使用 IntPtr 存储返回值
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>
[DllImport("ComDLG32.dll", SetLastError = true, ThrowOnUnmappableChar = true, CharSet = CharSet.Auto)]
internal static extern bool GetOpenFileName([In, Out] OpenFileName openFileName);

private static readonly List<string> selectedFilesList = new List<string>();

/// <summary>
/// 选择多个文件并返回
/// </summary>
/// <param name="windowTitle">弹窗标题</param>
/// <param name="defaultFolder">默认打开的文件夹</param>
/// <param name="filter">文件后缀筛选</param>
/// <param name="selectData">返回选择情况: 多个文件时, 首个数据为文件夹, 后续数据依次为文件名</param>
/// <returns>未选择时返回 false, 选择文件时返回 true</returns>
public static bool SelectFiles(string windowTitle, string defaultFolder, string filter, out List<string> selectData)
{
var openFileName = new OpenFileName();

openFileName.structSize = Marshal.SizeOf(openFileName);
openFileName.filter = filter;
openFileName.fileTitle = new string(new char[64]);
openFileName.maxFileTitle = openFileName.fileTitle.Length;
openFileName.initialDir = defaultFolder;
openFileName.title = windowTitle;
openFileName.templateName = string.Empty;
openFileName.flags = 0x00000008 | 0x00000200 | 0x00000800 | 0x00001000 | 0x00080000;

// 分配 2KB 缓冲区
const int BUFFER_SIZE = 2048;
var fileBuffer = Marshal.AllocHGlobal(BUFFER_SIZE * Marshal.SystemDefaultCharSize);

// 清空缓冲区,防止残留数据影响
for (var index = 0; index < BUFFER_SIZE; index++)
{
Marshal.WriteByte(fileBuffer, index, 0);
}

// 赋值缓冲区
openFileName.file = fileBuffer;
openFileName.maxFile = BUFFER_SIZE;

try
{
// 选取文件
if (GetOpenFileName(openFileName) == false)
{
selectData = null;
return false;
}

#region 处理返回值

selectedFilesList.Clear();

var pointer = fileBuffer;

while (true)
{
var file = Marshal.PtrToStringAuto(pointer);

if (string.IsNullOrEmpty(file))
{
break;
}

selectedFilesList.Add(file);

pointer += (file.Length + 1) * Marshal.SystemDefaultCharSize;
}

selectData = selectedFilesList;
return selectedFilesList.Count > 0;

#endregion
}
finally
{
// 释放分配的缓冲区内存
Marshal.FreeHGlobal(fileBuffer);
}
}
}
}