UnityでのWindowsのコモンダイアログを利用したファイル選択

# 概要
Unity から Windows のコモンダイアログ Comdlg32.dll の GetOpenFileName() を呼び出した方法のメモです。


# 背景
Unity上でファイル選択をする場合、[Unity Simple File Browser](https://github.com/yasirkula/UnitySimpleFileBrowser)[^1] やその他アセットを利用する方法があります。
あるいは [Mono の System.Windows.Forms. OpenFileDialog を利用する方法](https://qiita.com/koukiwf/items/47b05e3df41928959ff9)[^2]もありますが、DLLのコピーが必要です。

また、どちらもWindowsのダイアログそのものではないため使い勝手が異なります。 
そこで、Windowsのダイアログを利用する方法を記しておきます。  

# 方法
[Stack Overflow の投稿](https://stackoverflow.com/questions/9088227/using-getopenfilename-instead-of-openfiledialog)[^3] などを参考に、下記のファイル OpenFileName.cs を用意しました。

```cs:OpenFileName.cs
using System;
using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public class OpenFileName
{
    public int structSize = 0;
    public IntPtr dlgOwner = IntPtr.Zero;
    public IntPtr instance = IntPtr.Zero;
    public string filter = null;
    public string customFilter = null;
    public int maxCustFilter = 0;
    public int filterIndex = 0;
    public string file = null;
    public int maxFile = 0;
    public string fileTitle = null;
    public int maxFileTitle = 0;
    public string initialDir = null;
    public string title = null;
    public int flags = 0;
    public short fileOffset = 0;
    public short fileExtension = 0;
    public string defExt = null;
    public IntPtr custData = IntPtr.Zero;
    public IntPtr hook = IntPtr.Zero;
    public string templateName = null;
    public IntPtr reservedPtr = IntPtr.Zero;
    public int reservedInt = 0;
    public int flagsEx = 0;

    public static readonly int OFN_ALLOWMULTISELECT = 0x00000200;
    public static readonly int OFN_CREATEPROMPT = 0x00000200;
    public static readonly int OFN_DONTADDTORECENT = 0x02000000;
    public static readonly int OFN_ENABLEHOOK = 0x00000020;
    public static readonly int OFN_ENABLEINCLUDENOTIFY = 0x00400000;
    public static readonly int OFN_ENABLESIZING = 0x00800000;
    public static readonly int OFN_ENABLETEMPLATE = 0x00000040;
    public static readonly int OFN_ENABLETEMPLATEHANDLE = 0x00000080;
    public static readonly int OFN_EXPLORER = 0x00080000;
    public static readonly int OFN_EXTENSIONDIFFERENT = 0x00000400;
    public static readonly int OFN_FILEMUSTEXIST = 0x00001000;
    public static readonly int OFN_FORCESHOWHIDDEN = 0x10000000;
    public static readonly int OFN_HIDEREADONLY = 0x00000004;
    public static readonly int OFN_LONGNAMES = 0x00200000;
    public static readonly int OFN_NOCHANGEDIR = 0x00000008;
    public static readonly int OFN_NODEREFERENCELINKS = 0x00100000;
    public static readonly int OFN_NOLONGNAMES = 0x00040000;
    public static readonly int OFN_NONETWORKBUTTON = 0x00020000;
    public static readonly int OFN_NOREADONLYRETURN = 0x00008000;
    public static readonly int OFN_NOTESTFILECREATE = 0x00010000;
    public static readonly int OFN_NOVALIDATE = 0x00000100;
    public static readonly int OFN_OVERWRITEPROMPT = 0x00000002;
    public static readonly int OFN_PATHMUSTEXIST = 0x00000800;
    public static readonly int OFN_READONLY = 0x00000001;
    public static readonly int OFN_SHAREAWARE = 0x00004000;
    public static readonly int OFN_SHOWHELP = 0x00000010;

    public OpenFileName()
    {
        this.structSize = Marshal.SizeOf(this);
        this.filter = "All Files\0*.*\0\0";
        this.file = new string('\0', 4096);
        this.maxFile = this.file.Length;
        this.fileTitle = new string('\0', 256);
        this.maxFileTitle = this.fileTitle.Length;
        this.title = "Open";
        this.flags = OFN_EXPLORER
            | OFN_FILEMUSTEXIST
            | OFN_PATHMUSTEXIST
            | OFN_NOCHANGEDIR;
    }

    [DllImport("Comdlg32.dll", SetLastError = true, ThrowOnUnmappableChar = true, CharSet = CharSet.Auto)]
    public static extern bool GetOpenFileName([In, Out] OpenFileName lpOfn);

    /// <summary>
    /// このメソッドでダイアログが開く
    /// </summary>
    /// <returns>ファイルが選択されればそのパス、非選択ならnullを返す</returns>
    public static string ShowDialog()
    {
        OpenFileName ofn = new OpenFileName();
        if (GetOpenFileName(ofn))
        {
            return ofn.file;
        }
        return null;
    }
}
```

上記を用意した後、例えば下記のようなスクリプトを適当なオブジェクトにアタッチすることで試せます。

```cs:TestScene.cs
using UnityEngine;

public class TestScene : MonoBehaviour {
    private void OnGUI()
    {
        if (GUI.Button(new Rect(100, 100, 200, 40), "Open"))
        {
            string path = OpenFileName.ShowDialog();
            Debug.Log(path);
        }
    }
}
```


# 制限事項
Windows 専用となります。
この実装例では単一ファイルの選択のみ可能です。
ダイアログが開いている間、Unity側の動作は停止します。

このコードは Windows 10 64ビットでのみテストしました。

Unityエディタ上で再生してダイアログを開き、閉じる前にUnityエディタをアクティブにすると、カレントディレクトリが変更されたと怒られエディタを強制終了させられました。

# 補足（2019/2/2追記）
- flags に設定する `OFN_*****` の内容についてはMSDNや[それを訳していただいた記事](http://hp.vector.co.jp/authors/VA023539/tips/dialog/004.htm)[^4]が参考になります。
- ここで載せているのはとても簡易的なものです。実用するなら有料アセットも探した方が良いかと思います。無料でネイティブダイアログを出せるものだと[UnityStandaloneFileBrowser](https://github.com/gkngkc/UnityStandaloneFileBrowser)[^5]というものがありました。Mac、Linux、WebGLにも対応していて良さそうです！（私自身はそう試せていませんが）

# 参考資料
[^1]: yasirkula. [Unity Simple File Browser](https://github.com/yasirkula/UnitySimpleFileBrowser), GitHub. (accessed 2019-01-29)
[^2]: @koukiwf. [UnityでOpenFileDialog（外部ファイルの読み込み）](https://qiita.com/koukiwf/items/47b05e3df41928959ff9), Qiita. (accessed 2019-01-29)
[^3]: [Arie](https://stackoverflow.com/users/891715/arie). [Using GetOpenFileName instead of OpenFileDialog](https://stackoverflow.com/questions/9088227/using-getopenfilename-instead-of-openfiledialog), Stack Overflow. (accessed 2019-01-29)
[^4]: すなのかたまり. [OPENFILENAME構造体](http://hp.vector.co.jp/authors/VA023539/tips/dialog/004.htm), Tips of VC++. (accessed 2019-02-02)
[^5]: gkngkc. [UnityStandaloneFileBrowser](https://github.com/gkngkc/UnityStandaloneFileBrowser), GitHub. (accessed 2019-02-02)