読者です 読者をやめる 読者になる 読者になる

チリペヂィア

リンクフリー。サンプルコードなどは関連記事内でライセンスについて明示されない限り商用利用なども自由に行って構いませんが、自己責任でお願いします。またこれら日記内容の著作権自体は放棄していません。引用部分については引用元の権利に従ってください。

UnityでVisual Studio Expressのソリューションを開くスクリプト

(Unity4.3.4f1Win)

母艦組み直しで32bitXP→64bitWin8移行。思ったよりトラブルなく移行できたんですが、ついに「うにゃにゃ!?」ってなりました。

SyncMonoDevProjectでVisual Studio Expressが起動できない?

なぜか今までの設定ではExpressが起動できない。ここでひっかかったか。

今まで:UnityとMonoDevelop日本語対応状況再確認
http://d.hatena.ne.jp/tiri_tomato/20130725/1374713075

いろいろ調べてみると、要するにArgumentsの"$(File)"のところ、一部のエディタ*1を除き、そのまま文字列引数として投げられているようです。イミガワカラナイ…当然ファイル名として$(File)なんて無効。そんな変な起動オプションに対応するエディタもありません。もう少し状況を整理。

ひとつ、スクリプトファイルをダブルクリックした時の動作は今まで通り正常。
Sync〜メニューではなく、スクリプトファイルをダブルクリックで開く時は$(File)はきちんと作動する模様。しかしソリューションが開くわけじゃないので、入力補完は作動せず(でもこの仕様は今まで通り。やっぱりソリューション開く前にファイル単体開いちゃうとダメだった)。

ひとつ、Sync〜で$(File)が渡されることにより起動エラーが表示されたエディタとか

  • Start "" %1 (バッチファイル)
  • VS Express 2010
  • VS Express 2013 for Desktop

ひとつ、とくにエラー表示とかはないけどソリューションやファイルは開けず、ただ起動するだけのエディタ

  • XamarinStudio 4.2.3(最新Mono相当)

うーん「Sync〜」は基本的にデバッガをリンクする前提っぽい。ただUnityがそのへん対応しないツールでソリューション開こうとすると$Fileの置換機能が作動しない感じ。しかも外部ツールの設定は、Sync〜の*.slnオープンと*.csのダブルクリックの両方の設定を兼ねているのでややこしい。


…やはりしょうがないやうです。書きます。

まずコレ。

$(File)作動したりしなかったり対策バッチファイル

Unityのメニューから、Preferences>External Tools>External Script Editorに次のような*.batファイルを設定します。

if exist %1 start "" %1

見たまんま。バッチでexistするとファイルパスの確認ができるので、Sync〜によって変な引数が飛んできたらif文で抜けるよろし。これでcsダブルクリック時の動作はとりあえず確保。EditorAttachingもどうせ動かないのでチェックをfalseにしておきます。

で、こうするとSync〜でソリューションが開けないので、さらにソリューション開くメニュースクリプトを書きまする*2

ソリューションを開くEditorスクリプト

Windows専用で申し訳ない。*3

<仕様>

  • このcsを適当なEditorフォルダに保存するとSync〜メニューの下に、Open Solution Fileという名前のメニューが追加されます
  • Open Solution Fileをクリックするとソリューションファイル開きます。
    すでに開いている場合は、そのプロセスのエディタウィンドウに切り替えます。
  • いわゆる設定はプログラム冒頭に直接ハードコードしてるので、そのへんの変更は直接スクリプトを書き換えて下さい(外部ツールのパスとか、ウィンドウの検索条件とか)。
  • 本当は設定ファイルとか対応した方がカッコイイんですが疲れたので適当に書き散らかして逃げます。この問題はすぐに修正されそうな気がしないでもないし。
  • 参考にしたDobon様(他やまほどのDobon記事。ほとんどのAPIパートがコピペェ…)
    http://dobon.net/vb/dotnet/process/appactivate.html

<コード>

// このプログラムはUser32.dll使いまくりなのでWindows専用です。
using System.Runtime.InteropServices;
using UnityEngine;
using UnityEditor;

public class OpenCSSolution
{
    // 開きたい外部ツールのパス。
    private static readonly System.String openPath = "C:\\Program Files (x86)\\Microsoft Visual Studio 10.0\\Common7\\IDE\\VCSExpress.exe";
    // (↑わかる人だけ:バッチファイルとかShellあるいは関連付けやその他ランチャーを経由して開くのはヤメテ!/それやるとC#から直接プロセスが見えない=多重起動検知が面倒くさい)
    
    // 検索するウィンドウのタイトルの一部。とりあえずウィンドウタイトルが、以下の文字列に完全一致する部分を持つウィンドウを探知する。
    private static readonly System.String partOfTitle = "Microsoft Visual C# 2010 Express";

    // 子ウィンドウの列挙を含めるかどうか。true=含める/false=含めない。デフォでは含めないけど、見つからないのならtrueにすると見つかるかも?
    private static readonly bool includeChildWnd = false;

    // ソリューションファイルが同じディレクトリに複数見つかる時の選別モード。SeeAlso:SolutionPathSelectMode
    private static readonly SolutionPathSelectMode solutionFileSelectMode = SolutionPathSelectMode.Longest;

    /// <summary>
    /// ソリューションファイルが同じディレクトリに複数見つかる時の選別モード。ようするに[Project].slnか、[Project]-csharp.slnか選択。
    /// </summary>
    private enum SolutionPathSelectMode
    {
        /// <summary>
        /// もっとも短いファイル名のものを選択。例えば[Project].slnと[Project]-csharp.slnでは[Project].slnが選択される。
        /// </summary>
        Shortest = 0,

        /// <summary>
        /// もっとも長いファイル名のものを選択。例えば[Project].slnと[Project]-csharp.slnでは[Project]-csharp.slnが選択される。
        /// </summary>
        Longest = 1
    }

    private class ProcessWindowDetector
    {
        public ProcessWindowDetector(System.Diagnostics.Process proc)
        {
            EnumWindows(EnumWindowCallBack, proc.Id);
        }

        public System.IntPtr detectedWindowHandle { get { return m_detectedWindowHandle; } }
        public System.String detectedWindowText { get { return m_detectedWindowText; } }
        
        private delegate bool EnumWindowsDelegate(System.IntPtr hWnd, int lparam);

        [DllImport("user32.dll")]
        private static extern System.IntPtr GetWindowLong(System.IntPtr hWnd, int nIndex);
       
        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        private extern static bool EnumWindows(EnumWindowsDelegate lpEnumFunc, int lparam);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern int GetWindowText(System.IntPtr hWnd,
            System.Text.StringBuilder lpString, int nMaxCount);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern int GetWindowTextLength(System.IntPtr hWnd);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern int GetClassName(System.IntPtr hWnd,
            System.Text.StringBuilder lpClassName, int nMaxCount);

        [DllImport("user32.dll", SetLastError = true)]
        private static extern uint GetWindowThreadProcessId(System.IntPtr hWnd, out int lpdwProcessId);

        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool SetForegroundWindow(System.IntPtr hWnd);

        [DllImport("user32.dll")]
        private static extern System.IntPtr GetForegroundWindow();

        [DllImport("user32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool BringWindowToTop(System.IntPtr hWnd);

        [DllImport("user32.dll")]
        static extern System.IntPtr SetFocus(System.IntPtr hWnd);

        [DllImport("user32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool SetWindowPos(System.IntPtr hWnd,
            int hWndInsertAfter, int x, int y, int cx, int cy, int uFlags);

        private const int SWP_NOSIZE = 0x0001;
        private const int SWP_NOMOVE = 0x0002;
        private const int SWP_NOZORDER = 0x0004;
        private const int SWP_SHOWWINDOW = 0x0040;
        private const int SWP_ASYNCWINDOWPOS = 0x4000;
        private const int HWND_TOP = 0;
        private const int HWND_BOTTOM = 1;
        private const int HWND_TOPMOST = -1;
        private const int HWND_NOTOPMOST = -2;

        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool ShowWindow(System.IntPtr hWnd, int nCmdShow);
        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool ShowWindowAsync(System.IntPtr hWnd, int nCmdShow);

        private const int SW_SHOWNORMAL = 1;
        private const int SW_SHOW = 5;
        private const int SW_RESTORE = 9;

        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool IsIconic(System.IntPtr hWnd);

        [DllImport("kernel32.dll")]
        private static extern uint GetCurrentThreadId();

        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool AttachThreadInput(
            uint idAttach, uint idAttachTo, bool fAttach);

        [DllImport("user32.dll", EntryPoint = "SystemParametersInfo",
            SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool SystemParametersInfoGet(
            uint action, uint param, ref uint vparam, uint init);

        [DllImport("user32.dll", EntryPoint = "SystemParametersInfo",
            SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool SystemParametersInfoSet(
            uint action, uint param, uint vparam, uint init);

        private const uint SPI_GETFOREGROUNDLOCKTIMEOUT = 0x2000;
        private const uint SPI_SETFOREGROUNDLOCKTIMEOUT = 0x2001;
        private const uint SPIF_UPDATEINIFILE = 0x01;
        private const uint SPIF_SENDCHANGE = 0x02;

        private static System.IntPtr GetParentWnd(System.IntPtr hWnd) { return GetWindowLong(hWnd, -8); }

        public static void ActiveWindow(System.IntPtr hWnd)
        {
            if (hWnd == System.IntPtr.Zero)
            {
                return;
            }

            //ウィンドウが最小化されている場合は元に戻す
            if (IsIconic(hWnd))
            {
                ShowWindowAsync(hWnd, SW_RESTORE);
            }

            //AttachThreadInputの準備
            //フォアグラウンドウィンドウのハンドルを取得
            System.IntPtr forehWnd = GetForegroundWindow();
            if (forehWnd == hWnd)
            {
                return;
            }
            //フォアグラウンドのスレッドIDを取得
            int procId = 0;
            uint foreThread = GetWindowThreadProcessId(forehWnd, out procId);
            //自分のスレッドIDを収得
            uint thisThread = GetCurrentThreadId();

            uint timeout = 200000;
            if (foreThread != thisThread)
            {
                //ForegroundLockTimeoutの現在の設定を取得
                //Visual Studio 2010, 2012起動後は、レジストリと違う値を返す
                SystemParametersInfoGet(SPI_GETFOREGROUNDLOCKTIMEOUT, 0, ref timeout, 0);
                //レジストリから取得する場合
                //timeout = (uint)Microsoft.Win32.Registry.GetValue(
                //    @"HKEY_CURRENT_USER\Control Panel\Desktop",
                //    "ForegroundLockTimeout", 200000);

                //ForegroundLockTimeoutの値を0にする
                //(SPIF_UPDATEINIFILE | SPIF_SENDCHANGE)を使いたいが、
                //  timeoutがレジストリと違う値だと戻せなくなるので使わない
                SystemParametersInfoSet(SPI_SETFOREGROUNDLOCKTIMEOUT, 0, 0, 0);

                //入力処理機構にアタッチする
                AttachThreadInput(thisThread, foreThread, true);
            }

            //ウィンドウをフォアグラウンドにする処理
            SetForegroundWindow(hWnd);
            SetWindowPos(hWnd, HWND_TOP, 0, 0, 0, 0,
                SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW | SWP_ASYNCWINDOWPOS);
            BringWindowToTop(hWnd);
            ShowWindowAsync(hWnd, SW_SHOW);
            SetFocus(hWnd);

            if (foreThread != thisThread)
            {
                //ForegroundLockTimeoutの値を元に戻す
                //ここでも(SPIF_UPDATEINIFILE | SPIF_SENDCHANGE)は使わない
                SystemParametersInfoSet(SPI_SETFOREGROUNDLOCKTIMEOUT, 0, timeout, 0);

                //デタッチ
                AttachThreadInput(thisThread, foreThread, false);
            }
        }

        private System.IntPtr m_detectedWindowHandle = System.IntPtr.Zero;
        private System.String m_detectedWindowText = System.String.Empty;

        private bool EnumWindowCallBack(System.IntPtr hWnd, int lparam)
        {
            // process id check
            int procId = 0;
            GetWindowThreadProcessId(hWnd, out procId);
            if (lparam != procId) return true;

            // parent window check
            if (includeChildWnd == false)
            {
                if (GetParentWnd(hWnd) != System.IntPtr.Zero) return true;
            }

            // get window title
            System.String wndText = System.String.Empty;
            int textLen = GetWindowTextLength(hWnd);
            if (0 < textLen)
            {
                System.Text.StringBuilder tsb = new System.Text.StringBuilder(textLen + 1);
                GetWindowText(hWnd, tsb, tsb.Capacity);
                wndText = tsb.ToString();
            }

            // window title check
            if (System.String.IsNullOrEmpty(partOfTitle) == false)
            {
                if (wndText.IndexOf(partOfTitle) < 0) return true;
            }

            m_detectedWindowText = wndText;
            m_detectedWindowHandle = hWnd;

            return false;
        }

    }

    static System.String lastOpenPath = System.String.Empty;
    static System.Diagnostics.Process lastStartedProcess = null;

    [MenuItem("Assets/Open Solution File")]
    static void OpenSolutionFile()
    {
        System.String solutionPath = Application.dataPath;
        solutionPath = solutionPath.Remove(solutionPath.Length - "Assets".Length);
        System.String[] files = System.IO.Directory.GetFiles(solutionPath, "*.sln", System.IO.SearchOption.TopDirectoryOnly);
        
        System.String targetPath = null;
        if (solutionFileSelectMode == SolutionPathSelectMode.Shortest)
        {
            if (files.Length <= 0) targetPath = System.String.Empty;
            else
            {
                foreach (System.String file in files)
                {
                    if ((targetPath == null) || (file.Length < targetPath.Length)) targetPath = file;
                }
            }
        }
        else
        {
            targetPath = System.String.Empty;
            foreach (System.String file in files) if (targetPath.Length < file.Length) targetPath = file;
        }
        
        if (System.IO.File.Exists(targetPath))
        {
            if ((lastStartedProcess != null) && (lastStartedProcess.HasExited == false))
            {
                ProcessWindowDetector procWnd = new ProcessWindowDetector(lastStartedProcess);

                if (lastOpenPath == targetPath)
                {
                    if (procWnd.detectedWindowHandle != System.IntPtr.Zero)
                    {
                        ProcessWindowDetector.ActiveWindow(procWnd.detectedWindowHandle);
                    }
                    else
                    {
                        EditorUtility.DisplayDialog("エディターウィンドウの切り替えに失敗",
                            "エディターウィンドウが発見できません。\nエディターウィンドウの検索設定を再設定してください。", "OK");
                    }
                    return;
                }

                if (EditorUtility.DisplayDialog("エディタを開き直しますか?", "変更は破棄される可能性があります。", "変更を破棄して再オープン", "キャンセル") == false) return;
                if (lastStartedProcess.CloseMainWindow() == false)
                {
                    lastStartedProcess.Close();
                }
                lastStartedProcess.WaitForExit(5000);
            }

            lastStartedProcess = System.Diagnostics.Process.Start(openPath, lastOpenPath = targetPath);
        }
    }
}

*1:標準のMonoとか有償版のVSとか

*2:$(File)が問題だからスクリプト書くほどでもないような気もするんだけどなぁ。Unityの設定側でSyncとダブルクリックを分離可能にして$(Project)とかにすれば良いような気も。

*3:プロセス間でのGUIアクティブ化処理、OSの違いを超えて一般化とかはさすがに難しいんだろうなぁ〜。