Win32apps|你选择一个文件夹不就可以了吗?

Win32apps|你选择一个文件夹不就可以了吗?

前言

Unity 打开文件对话框的接口,如 EditorUtility.OpenFilePanel 和 EditorUtility.OpenFilePanelWithFilters,它们都有一个缺点,就是不支持多选文件。但是实际开发中会遇到需要多选文件的需求,这时候就得自行开发接口了。这时候就有同学要问了:你选择一个文件夹不就可以了吗?试想unity 打开文件对话框,若一个文件夹下有 100 个文件,我只想一次性选择其中 10 个文件,选整个文件夹肯定是不行的,我又不想打开 10 次对话框——这就是需求背景。

资料搜集

要做一个新的需求,第一步肯定是搜集现有资料,看看有无前人踩过坑甚至实现了的。经过一番搜索,我找到了三个可能的方向:① 直接调用 Windows 的原生接口;② 调用 Windows.Forms 的接口;③ 通过其他脚本语言实现。

方案详解 方法一:Windows 原生接口

Win32 原生接口中有个 GetOpenFileName,支持多选文件,如何在 Unity C# 中使用该接口我也找到了一篇博客,写的非常详细:【Unity编辑器开发】工具开发之Windows单选或多选文件踩坑记录 - 陌冉 - 博客园 ()

大致思路就是构造一个和 Win32 中某个结构体字段以及布局完全一致的数据结构,用 DllImport 属性引入 Comdlg32.dll(该 dll 在目录 C:/Windows/System32 文件夹下)的 GetOpenFileName方法。

但是这个接口有个最大的坑点就是如果你选择了多个文件,它并不会返回这些文件的列表,而是返回它们的父文件夹。官方文档写的清清楚楚:

文档传送门:OPENFILENAMEA (commdlg.h) - Win32 apps | Microsoft Docs

无奈只能放弃该方法。

方法二:Windows.Forms 窗体接口

既然 win32 原生的行不通,那么就找其他的 dll。Windows 平台有个著名的应用类型叫 Windows 窗体应用,其中就提供了打开文件对话框的接口。顺着这个思路,我找到了一个开源项目:

gkngkc/UnityStandaloneFileBrowser: A native file browser for unity standalone platforms ()

在此只简单分析一下该项目的思路,具体实现查阅仓库即可。

该项目支持 Windows、Mac 和 Linux 三大平台。其中 Windows 平台是以第三方库 Ookii Dialogs 作为底层支持的,仅是对 Ookii 库做了一层封装,而 Ookii 依赖于 System.Windows.Forms开发学习,所以需要同时引入 Ookii.Dialogs.dll 和 System.Windows.Forms.dll。Mac 和 Linux 平台则引入了自定义的 StandaloneFileBrowser.jslib,并且没有任何说明,故无法考证底层用了什么方法。

Ookii Dialogs 的 GitHub 首页链接为:Ookii Dialogs ()

可以看一下 StandaloneFileBrowserWindows 这个类的部分实现:

public class StandaloneFileBrowserWindows : IStandaloneFileBrowser {
    [DllImport("user32.dll")]
    private static extern IntPtr GetActiveWindow();
    public string[] OpenFilePanel(string title, string directory, ExtensionFilter[] extensions, bool multiselect) {
        var fd = new VistaOpenFileDialog();
        fd.Title = title;
        // ...
        fd.Multiselect = multiselect;
        // ...
        var res = fd.ShowDialog(new WindowWrapper(GetActiveWindow()));
        var filenames = res == DialogResult.OK ? fd.FileNames : new string[0];
        fd.Dispose();
        return filenames;
    }
    // other methods
}

其中,user32.dll 是每台 Windows 电脑都会有的,在目录 C:\Windows\System32 下,用 DllImport 特性可直接导入。VistaOpenFileDialog 是 Ookii 库的函数,设置好参数然后调用 ShowDialog 即可。

方法三:通过 Python 实现

方法二虽然已有现成实现,但是不适合已上线的大项目,因为引入新的 dll 会拉长项目的编译时长,只为这一个小功能引入两个 dll 更是得不偿失。

但是前两个方法已经是代表了 C# 层的所有路子了:系统级 api,框架 api,第三方 api。所以,要想实现这个需求技能特效,必须另辟蹊径,比如——通过其他语言来实现。

我第一个想到的就是 Python,因为它有以下优点:一是脚本语言,很容易与其他语言结合;二是 Python 库非常丰富,你总能找到可以满足你需求的库。

Python 脚本

我决定使用的库为 tkinter,当然你大可使用其他的库。tkinter 的 simpledialog 是处理文件对话框的模块,官方文档地址:

tkinter 库打开多选文件对话框的代码很简单:

import tkinter as tk
import tkinter.filedialog as fd
root = tk.Tk()  # 初始化tkinter
files = fd.askopenfilenames()  # 打卡文件对话框

但是在打开文件对话框的同时还会打开一个小小的白色窗口(Mac 平台则为黑色小窗口):

这样的小窗看着很糟心,要去掉也很简单,只需调用 withdraw 接口即可:

root = tk.Tk()
root.withdraw()

函数 askopenfilenames 有一些参数,含义如下:

参数名含义

parent

该窗口的父级,将会把当前窗口放在该它的前面

title

对话框的标题,默认是“打开”

initialdir

默认打开的目录

initialfile

默认打开的文件

filetypes

筛选扩展名,是 (label, pattern) 形式的元组序列,“*”表示所有文件类型

multiple

是否多选文件,默认就是 true

C# 层的封装

众所周知,一个 Python 脚本可以通过命令行运行,如 python test.py。而在 C# 我们可以通过以下代码来调用命令行:

using (var p = new System.Diagnostics.Process())
{
    p.StartInfo.FileName = "cmd.exe";          // 运行cmd控制台
    p.StartInfo.CreateNoWindow = true;         // 不显示窗口
    p.StartInfo.UseShellExecute = false;       // 不使用操作系统外壳程序启动进程
    p.StartInfo.RedirectStandardInput = true;  // 重定向输入流,否则不能写入命令
    p.Start();
    p.StandardInput.Flush();
    p.StandardInput.WriteLine("Your command");
    p.StandardInput.Close();
    p.WaitForExit();
}

我们只需要调整输入的命令为调用写好的 Python 脚本,然后获取命令行的输出即可。

完善细节

好,现在大致的流程我们都清楚了,只需要丰富一点点细节就 OK。

解析命令行参数

首先我们需要从 C# 传递参数给 pythonunity 打开文件对话框,仍是通过命令行方式。python 获取命令行参数的接口是 sys.argv,它返回一个列表。

在此规定参数格式,假设我们要调用 python 脚本的完整命令行是:

python MultiFilePanel.py -title "打开源文件" -fileTypes "Source Code,*.cs;All Files,*" -directory "D:/test"

注意,为了支持扩展名中可保护空格,我们需要用双引号把整个 -fileTypes 参数值括起来。

解析命令行参数的代码如下:

# 第1个参数是python文件名,所以我们从第2个参数开始遍历
for i in range(1, len(sys.argv), 2):
    arg_name = sys.argv[i]
    arg_val = sys.argv[i + 1]
    if arg_name == '-title':
        title = arg_val
    elif arg_name == '-fileTypes':
        file_types = get_file_types(arg_val)
    elif arg_name == '-directory':
        directory = arg_val
files = fd.askopenfilenames(title=title, filetypes=file_types,initialdir=directory)
print(files)  # 输出返回值到命令行

此处省略 get_file_types 的实现,该函数功能仅是把命令行的参数转化成元组列表,详情可查阅文本给出的仓库。

解析返回值

python 层将选择的文件列表输出到了命令行,在 C# 层就需要读取 python 的输出。

我们先要调整 Process.StartInfo 的参数,才能够读取命令行的输出结果:

p.StartInfo.RedirectStandardOutput = true; // 重定向输出流,否则不能读取输出
...
string output = p.StandardOutput.ReadToEnd();

然后就可以对 output 进行操作,以获得选择的文件列表。

总结

本文探讨了在 Unity 中实现多选文件对话框的三个方案:第一个方案不能满足我们的需求;第二个方案效果最佳,但是需要在项目里导入新的 dll,这样的做法在已上线的项目里是不可取的,因为引入 dll 势必会拉长编译时间,影响的是整个项目组的时间;第三个方案则是剑走偏锋,用偏门的方法实现。

希望这篇文章对你有用

最后附上完整实现的 GitHub 仓库链接:Dont-laugh/MultiFilePanel: Multi select file panel in Unity ()

文章来源:https://blog.csdn.net/Wavelet_Peng/article/details/121738621