ひつじTips

技術系いろいろつまみ食います。

[Unity]アプリのウインドウハンドルを確実に取得する方法

このQiita記事にめっちゃちゃんとまとまっていて(@kiruroboさん,素晴らしい記事本当にありがとうございます!!!),本エントリで紹介する方法はほぼこの「方法4」ではあるんですが,この「方法4」ではうまくいかないところがあったのでなんとかしましたよ,というのが本エントリになります.

qiita.com

諸事情により,アプリがバックグラウンドにいた場合でもウインドウハンドルを取得したく,その場合 GetActiveWindow() 関数を使用するQiita記事内の「方法1」「方法2」が使えません.

「方法3」でもいいのですが,ちょっとQiita記事内でも挙げられてる「問題点」が厳しくパス,「方法5」もちょっとめんどくてパス,というので,「方法4」たどり着きました.

Qiita記事には「方法4」の実装まで書かれていなかったので,具体的な実装を明記しつつ,問題点とそれの回避方法までまとめます.

無駄に長いので,答えが欲しい方は「4. 実装」の方にとんでください~

方針

ざっくり方針は,アプリのプロセスIDを取得し,ウインドウを全舐めして一致するプロセスIDを探す,という@kiruroboさんQiita記事の「方法4」と同じ方法です.

もうちょい具体的には以下のような感じ.

  1. System.Diagnostics.Process.GetCurrentProcess() で,自分のプロセスIDを取得する
  2. GetWindow() で,Zオーダ最前のウインドウハンドルを取得する
  3. GetWindowThreadProcessId() で,取得したウインドウハンドルのプロセスIDを取得する
  4. 自分のプロセスIDと一致していればそのウインドウハンドルが自分のもの,
    そうでなければ GetWindow() で次のZオーダのウインドウハンドルを取得する
  5. 3~4を繰り返す


Qiita記事の「方法4」では EnumWindows() を使ってウインドウを全舐めするのですが,記事内で書かれている通りコールバック関数を用意するなどちょっと実装がめんどいです...

調べてみると GetWindow() というWinAPIを使ってウインドウすべてを舐める方法があったので,そちらを採用しました.

yu-hr.hatenadiary.org

問題

ただ,この方針で実装を進めると問題に出くわしました.

それは,プロセスIDと一致するウインドウが3つ見つかる というものです.ど,どれが本物なんだ~~~ 😵😵😵

@kiruroboさんQiita記事には言及ないので,自分の環境の問題だったり,Unityのバージョンやビルド設定などの問題の可能性もあるのですが...)

確認のために,以下のようなスクリプトをつけてビルド→実行してみます.

    void Update()
    {
        if(Input.GetKeyDown(KeyCode.Space))
        {
            var thisProcess = System.Diagnostics.Process.GetCurrentProcess();
            ShowWindowHandles(thisProcess.Id);
        }
    }

    private void ShowWindowHandles(int targetProcessId)
    {
        Debug.Log(String.Format("{0}: {1}", "Active", User32.GetActiveWindow()));

        var cnt = 0;
        var hWnd = User32.GetTopWindow(IntPtr.Zero);
        while(hWnd != IntPtr.Zero)
        {
            int processId;
            User32.GetWindowThreadProcessId(hWnd, out processId);
            if(processId == targetProcessId)
            {
                Debug.Log(String.Format("{0}: {1}", cnt++, hWnd));
            }
            hWnd = User32.GetWindow(hWnd, 2);
        }
    }

    private struct User32
    {
        [DllImport("user32.dll")]
        public static extern IntPtr GetActiveWindow();

        [DllImport("user32.dll")]
        public static extern IntPtr GetTopWindow(IntPtr hWnd);

        [DllImport("user32.dll")]
        public static extern IntPtr GetWindow(IntPtr hWnd, uint wCmd);

        [DllImport("user32.dll")]
        public static extern int GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);

        [DllImport("user32.dll")]
        public static extern int GetWindowLong(IntPtr hWnd, int nIndex);
    }


Spaceを押したときに,まずActiveWindowのウインドウハンドルを表示し,そのあと全ウインドウを舐めてプロセスIDが一致したもののウインドウハンドルを表示するだけです.

Spaceを押している=Activeになっているはずなので,ActiveWindowのウインドウハンドルと,プロセスIDの一致したウインドウハンドルが一致することが期待されます.

が,結果は以下のようになります...

f:id:mu-777:20200315170909p:plain

プロセスIDと一致するウインドウハンドルが3つ(4005426,7478442,2955134)いることがわかります.一応その中にActiveWindowと同じウインドウハンドル(2955134)がいるので,そいつが欲しいウインドウハンドルではあるのですが...

3つのうちの正解を引くためにはどうすればよいのか..

(ちなみに,この問題は EnumWindows() をつかってウインドウを舐めるときも発生することは確認してます)

問題の解決

3つのウインドウの情報をWinAPIの GetWindowLong() 関数を使って引き抜いてみます.(GetWindowLong() の詳細な説明は割愛します)

上記のスクリプトShowWindowHandles() 関数内のプロセスIDに一致したところの処理を以下のように変えてみます.

            if(processId == targetProcessId)
            {
-                Debug.Log(String.Format("{0}: {1}", cnt++, hWnd));
+                var style = User32.GetWindowLong(hWnd, -16);
+                var exStyle = User32.GetWindowLong(hWnd, -20);
+                var parent = User32.GetWindowLong(hWnd, -8);
+                Debug.Log(String.Format("{0}: {1}\n  Style: {2:X}, ExStyle: {3:X}, Parent: {4:X}",
+                                        cnt++, hWnd, style, exStyle, parent));
            }


すると,以下のように表示されます.

f:id:mu-777:20200315173226p:plain

ActiveWindowのウインドウハンドルと同じやつ(3番目のやつ)だけ,StyleとExStyleの値がちょっと違うことがわかります!!

Style と表示されてる「WindowStyle」,ExStyle と表示されてる「ExtendWindowStyle」の詳細は以下です.

docs.microsoft.com

docs.microsoft.com

「WindowStyle」の値から,ActiveWindowのウインドウハンドルと同じ3つ目に見つかったウインドウハンドルのウインドウは,上の桁から順に,WS_VISIBLE,WS_CLIPSIBLINGS,WS_CAPTION,WS_SYSMENU | WS_MINIMIZEBOX と思われます(ここの詳細は未検証なので間違ってるかも)

注目すべきは,1桁目の「WS_VISIBLE」フラグです.残りの2つの「WindowStyle」の1桁目は「8」で,これは「WS_POPUP」を意味します.

これより,ウインドウを全舐めしてプロセスIDが一致するもののうち,「WS_VISIBLE」なウインドウハンドルが実際に欲しいウインドウハンドルである ことがわかります!

(実際,残り2つのウインドウは何なのか..は追ってません.マウスのクリックとかを見張るひとかな..とか適当に邪推します.もーーし万が一仮にそれが正解であれば,ウインドウハンドルを使ってやりたいこと次第では,残り2つにも処理を実行する必要があるかもです)

実装

あらためてまとめると,以下のような関数でUnityのアプリケーションのウインドウハンドルを取得することができます.

    public static IntPtr GetSelfWindowHandle()
    {
        var wsVisible = 0x10000000;
        var thisProcess = System.Diagnostics.Process.GetCurrentProcess();
        var hWnd = User32.GetTopWindow(IntPtr.Zero);

        while(hWnd != IntPtr.Zero)
        {
            int processId;
            User32.GetWindowThreadProcessId(hWnd, out processId);
            if(processId == thisProcess.Id)
            {
                if((User32.GetWindowLong(hWnd, -16) & wsVisible) != 0)
                {
                    return hWnd;
                }
            }
            hWnd = User32.GetWindow(hWnd, 2);
        }
        return IntPtr.Zero;
    }

    private struct User32
    {
        [DllImport("user32.dll")]
        public static extern IntPtr GetTopWindow(IntPtr hWnd);

        [DllImport("user32.dll")]
        public static extern IntPtr GetWindow(IntPtr hWnd, uint wCmd);

        [DllImport("user32.dll")]
        public static extern int GetWindowLong(IntPtr hWnd, int nIndex);

        [DllImport("user32.dll")]
        public static extern int GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);
    }


一応,いままでと同じようにSpaceを押すとDebug.Logするようにしてみて,GetActiveWindow() で取得できるウインドウハンドルと比較してみます.

    void Update()
    {
        if(Input.GetKeyDown(KeyCode.Space))
        {
            Debug.Log(String.Format("{0}: {1}", "Active", User32.GetActiveWindow()));
            Debug.Log(String.Format("{0}: {1}", "GetSelf", GetSelfWindowHandle()));
        }
    }


これを実行してみると,以下のようなコンソール結果が得られ,ちゃんと一致してることがわかります!

f:id:mu-777:20200315182400p:plain


やったぜ!!

感想

C#いろいろできて便利ではあるけど,めんどいといえばめんどいよなぁ..

てかなぜそもそもネイティブDLL経由ではなく,C#でウインドウハンドルを取得する方法がないのか...