找回密码
 立即注册
查看: 501|回复: 0

工具杂谈 #1 Unity 多选文件对话框

[复制链接]
发表于 2021-12-7 16:11 | 显示全部楼层 |阅读模式
前言

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

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

方法一:Windows 原生接口


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

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

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


Microsoft-api

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

无奈只能放弃该方法。
方法二:Windows.Forms 窗体接口


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

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

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

该项目支持 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 (github.com)

可以看一下 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,框架 ai,第三方 api。所以,要想实现这个需求,必须另辟蹊径,比如——通过其他语言来实现。

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


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

tkinter 库打开多选文件对话框的代码很简单:
import tkinter as tkimport tkinter.filedialog as fdroot = tk.Tk()  # 初始化tkinterfiles = fd.askopenfilenames()  # 打卡文件对话框
但是在打开文件对话框的同时还会打开一个小小的白色窗口(Mac 平台则为黑色小窗口):

tk_dialog

这样的小窗看着很糟心,要去掉也很简单,只需调用 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# 传递参数给 python,仍是通过命令行方式。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    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_valfiles = 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 势必会拉长编译时间,影响的是整个项目组的时间;第三个方案则是剑走偏锋,用偏门的方法实现。

希望这篇文章对你有用
<hr>
最后附上完整实现的 GitHub 仓库链接:Dont-laugh/MultiFilePanel: Multi select file panel in Unity (github.com)

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-16 04:24 , Processed in 0.093139 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表