12
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Unity上での自ウィンドウハンドル取得方法5種

Posted at

概要

Unity上で、自分自身のウィンドウハンドルを取得する方法を5通り試しました。
結論としてはこの下の「方法 2」でどうかと考えています。
(製品にするなら「方法 5」が本命。)

背景

(Windowsでの話ですが)ウィンドウハンドルを取得できれば、Windows API を利用してリサイズや透明化など、色々なことができます。
ですが、Unityで作成したアプリが自分自身のウィンドウハンドルを取得する方法は元々は用意されていません。

そこでスクリプトからWindows APIを呼び出すことによる取得方法をいくつか試しました。

取得方法

方法 1 : シンプルにGetActiveWindow()

Windows API の GetActiveWindow() を用いると、アクティブなウィンドウのハンドルを取得できます。
Start()の中など起動直後に取得すれば、おおよそ自分のウィンドウがアクティブなはずなので大抵は動作しますが、そうは言っても起動は多少時間がかかるため、その間にデスクトップや他のウィンドウにフォーカスを移されて失敗することがよくあります。

WindowHandle.cs
using System;
using System.Runtime.InteropServices;
using UnityEngine;

public class WindowHandle : MonoBehaviour {
    [DllImport("user32.dll")]
    public static extern IntPtr GetActiveWindow();

    IntPtr hWnd;    // 自ウィンドウのハンドルをここに取得したい

    void Start () {
        hWnd = GetActiveWindow();
    }
}

利点

手軽

問題点

割と失敗する

方法 2 : GetActiveWindow() + 監視

そこで OnApplicationFocus() を用いて、自分がフォアグラウンドになったときに GetActiveWindow() を監視することを考えます。
自分がフォアグラウンドになった瞬間ならば、ほぼ GetActiveWindow() で自ウィンドウを取得できるだろうという考えです。

ところが考え方はそれで良さそうなところ、なぜか起動時に自分にフォーカスがなかった場合、一度アクティブにしても OnApplicationFocus() が呼ばれませんでした。二回目以降にアクティブにすると OnApplicationFocus() が呼ばれます。

そこで補助として、Input.anyKey の反応を見て何かマウスボタンやキー押下があれば、その時点で GetActiveWindow() を見るものも追加してみます。
どちらかというと[Alt]+[Tab]で切り替えるよりはウィンドウ内をクリックする人が多いと考えられるため、クリックした時点で反応すれば多少自然になります。

さて、これでも動作はしそうですが、毎フレーム Input.anyKey に反応があればウィンドウハンドルを取り直すのでは無駄が多そうですので、フラグを設けてどうやら確実に自ウィンドウがとれていれば以降は監視しないようにもしてみます。

以上をまとめたものが次のコードです。方法1に要素が追加された形となります。

WindowHandle.cs
using System;
using System.Runtime.InteropServices;
using UnityEngine;

public class WindowHandle : MonoBehaviour {
    [DllImport("user32.dll")]
    public static extern IntPtr GetActiveWindow();

    IntPtr hWnd;    // 自ウィンドウのハンドルをここに取得したい

    bool isHandleConclusive = false;    // ハンドルが確かならtrueとするフラグ

    void Start () {
        hWnd = GetActiveWindow();
        isHandleConclusive = false;
	}

    private void Update()
    {
        if (!isHandleConclusive)
        {
            if (Input.anyKey)
            {
                RetakeMyWindowHandle();
            }
        }
    }

    /// <summary>
    /// 別アプリとの間でフォーカスが移ったら呼ばれます
    /// </summary>
    /// <param name="focus">こちらにフォーカスがあたればtrue。外れた際はfalse</param>
    private void OnApplicationFocus(bool focus)
    {
        if (!isHandleConclusive)
        {
            if (focus)
            {
                RetakeMyWindowHandle();
            }
        }
    }

    /// <summary>
    /// 現在のアクティブウィンドウを自ウィンドウとしてハンドル取り直し
    /// </summary>
    private void RetakeMyWindowHandle()
    {
        IntPtr activeHWnd = GetActiveWindow();
        if (activeHWnd == IntPtr.Zero) {
            // アクティブウィンドウハンドルが取得できていなさそうなら、何もしない
        }
        else if (hWnd == activeHWnd)
        {
            // 前に取得したハンドルと今のアクティブウィンドウのハンドルが一致すれば
            // 自ウィンドウが確実に取得できているだろうとしてフラグを立てる
            isHandleConclusive = true;
        }
        else
        {
            // 前に取得したハンドルが今のアクティブウィンドウのハンドルと一致しなければ
            // 今のハンドルを有効とする

            // もし既にhWndを使って何かしてあれば、ここで戻す

            // 今アクティブなウィンドウを自ウィンドウとする
            hWnd = activeHWnd;
        }
    }
}

利点

100%確実とは言えないかもしれないが、ほぼ実用上は十分そう。

問題点

  • 対処療法的でスマートさに欠ける
  • 最初に取得失敗した場合、クリックしなければ正しくならなかったり、それまで他のウィンドウのハンドルを持ってしまっていたりする。
  • 最初に別ウィンドウのハンドルを取得していて、それに対して何か行っていた場合、戻す処理が必要
    • 確実そうだと判断できるまで処理には使わない、ということでも対応可能

方法 3 : ウィンドウタイトルによる取得

Windows API の FindWindow(クラス名, タイトル) を用いると、クラス名またはウィンドウタイトルが一致するウィンドウのハンドルを取得できます。
クラス名ではUnityのアプリがどれも同じになってしまいますので、タイトルからの取得の方が良いでしょう。

WindowHandle.cs
using System;
using System.Runtime.InteropServices;
using UnityEngine;

public class WindowHandle : MonoBehaviour {
    [DllImport("user32.dll")]
    public static extern IntPtr FindWindow(string lpszClass, string lpszTitle);

    IntPtr hWnd;    // 自ウィンドウのハンドルをここに取得したい

    void Start () {
        // Player設定で入れた Product Name がタイトルになる
        hWnd = FindWindow(null, Application.productName);
    }
}

利点

特徴的なタイトルならば、ほぼ確実にウィンドウハンドルを取得できます。

問題点

同じアプリケーションを複数起動することがあるようだと、困ります。
取得直後に Windows API の SetWindowText() を用いてウィンドウタイトルを書き換えてしまう、ということも考えられますが、まとめて複数起動されるとタイミングが合わない可能性もあるかもしれません。
またウィンドウタイトルは利用者にも見えるものですのであまり変に書き換えるのは良くないかと思われます。

方法 4 : プロセスIDを基準とした取得

下記のような流れとなります。(コードは複雑になるため省略。)

  1. 自分のプロセスIDは var process = System.Diagnostics.Process.GetCurrentProcess() でプロセスを取得した後 process.Id で得られる。
  2. そして、Windows API の EnumWindows() でウィンドウ一覧を取得する。
  3. それぞれのウィンドウに対して、Windows API の GetWindowThreadProcessId() を使って対応するプロセスIDを取得し、自分のプロセスIDと一致すれば、それが自ウィンドウだとする。

利点

確率的ではなく、確実に対象のウィンドウハンドルを取得できます。

問題点

  • ウィンドウを一通り取得して調べるため、GetActiveWindow() よりパフォーマンスが落ちる
  • EnumWindows() を使う場合コールバック関数が必要でコーディングに手間がかかる
  • なぜか IL2CPP かつ x86 のときは System.Diagnostics.Process 周りが失敗するなど、うまく動作しなかった

方法 5 : 起動ファイルを別に用意する方法

Unityでビルドしたスタンドアローンアプリケーションを、別のアプリケーションに埋め込むことができます。1
この場合、別途自分で作成したexeファイルが親のウィンドウとなるため、そのウィンドウハンドルを取得したり、子であるUnityのウィンドウハンドルを取得することは容易です。
こちら2にその話やサンプルプロジェクトへのリンクがありますので深くは解説しませんが、この方法ならばヒューリスティックではなく確実にウィンドウハンドルを取得できます。

利点

確率的にではなく、確実に対象のウィンドウハンドルを取得できます。
またこれならばウィンドウハンドルを使わなくとも、親となるウィンドウは自分で作成するため様々なことができます。

問題点

別途exeファイルを用意するため、Unity外のプロジェクトが必要となります。

まとめ

結局何が良いの?

私としては今のところ、Unityで完結させるなら方法2の GetActiveWindow()+監視 が扱いやすく、製品などできちんと作るならば方法5が最善かと考えています。

参考資料

  1. Unity Manual. https://docs.unity3d.com/ja/2018.1/Manual/CommandLineArguments.html (accessed 2019-01-30).

  2. Embed Unity3D app inside WPF application. https://stackoverflow.com/questions/44059182/embed-unity3d-app-inside-wpf-application Stack Overflow. (accessed 2019-01-30).

12
6
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?