Form の this.Activate() について

たまに Windows フォームアプリケーションを書く機会があるのですが,この時にいつも悩むのが「起動時にウィンドウを前面に表示させる」方法でした.ぐぐってみると他のウィンドウからフォーカスを奪って前面に表示させるためには this.Activate() を使えば良いという情報がたくさんヒットするのですが,これをやってもどうもうまくいきません(フォーカスが奪えずに他のウィンドウに隠れてしまう場合がある).そんな訳で,ちょっと調べてみたメモを残しておきます.

強制的にフォーカスを奪う

this.Activate() を行った際に,その時点でフォーカスを握っているウィンドウがそれ(フォーカスを渡すこと)を拒否する事があるそうです.これを回避するためには,以下のようにして強制的にフォーカスを奪う必要があるそう.

using System;
using System.Runtime.InteropServices;
...

public partial class MainForm : Form {
    ...
    
    // 最前面に表示されているウィンドウから強制的にフォーカスを奪う
    private void ForceAtivate() {
        int foreground = GetWindowThreadProcessId(GetForegroundWindow(), IntPtr.Zero);
        int me = System.Threading.Thread.CurrentThread.ManagedThreadId;
        if (foreground == me) return;
    
        AttachThreadInput(me, foreground, true);
        this.Activate();
        AttachThreadInput(me, foreground, false);
    }
    
    [DllImport("user32.dll")]
    private extern static int GetWindowThreadProcessId(IntPtr hWnd, IntPtr ProcessId);

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

    [DllImport("user32.dll")]
    private extern static bool AttachThreadInput(int idAttach, int idAttachTo, bool fAttach);
}

しかし,this.Activate() の代わりに上記の this.ForceActivate() を使用してみましたが,症状は変わらず.どうやら,別の原因があるようです.

this.Activate() を行うタイミング

MSDN で Form.Activate() のページを読むと,以下のような解説が書かれていました.

アクティブなアプリケーションであるフォームをアクティブにした場合は、そのフォームが最前面に移動します。アクティブなアプリケーションではないフォームの場合は、そのフォームのウィンドウ キャプションが点滅します。このメソッドを有効にするには、フォームを表示できる状態にしておく必要があります。

http://msdn.microsoft.com/ja-jp/library/system.windows.forms.form.activate(VS.80).aspx

ここで気になったのが,「このメソッドを有効にするには、フォームを表示できる状態にしておく必要があります」と言う部分.私はいつも,Form のコンストラクタの最後に this.Activate() を記述していたのですが,どうやらこの時点で this.Activate() を実行するのが問題のようです.

Windowsフォームにおいては、アプリケーションの初期化処理はフォームのコンストラクタやLoadイベント・ハンドラ(例えば「Form1_Load」メソッド)で行うことが通常だ。しかしフォーム自体に関する初期化処理は、これらのタイミングで行えない場合がある。

これは、コンストラクタやLoadイベント・ハンドラが呼び出されるときには、まだフォームが表示されていないことに起因する。

.NET TIPSFWindowsƒtƒH[ƒ€‚Ì•\Ž¦’¼Œã‚ɏ‰Šú‰»ˆ—‚ðs‚¤‚ɂ́Hm2.0‚̂݁AC#AVBn - —IT

それでは,どこで this.Activate() を実行するかと言うと,上記の記事にあるように Shown イベントが発生した時点で行うのが良いようです.Shown イベントは .NetFramework 2.0 で導入された「フォームが表示された直後に一度だけ発生するイベント」だそうです.例えば,以下のようなメソッドを Form クラス内に定義し,これをメイン Form の Shown イベント・ハンドラに指定しておきます.

private void Form_Shown(object sender, EventArgs e) {
    this.Activate();
    // もしくは,前述した this.ForceAtivate();
}

正しいのかどうか自信がないのですが,今のところはこれでうまくいっているようです.嘘でした.全然うまくいきません.

取り合えず最前面に表示させる

邪道.上記の ForceActivate() でもフォーカスを渡してくれないケースがかなり存在しました(パッと見,そこは渡してくれてもいいのになぁと言うシチュエーションでもフォーカスを奪えない場合が結構ありました).そんな訳でフォーカスをきちんと奪うことはいったん諦め,「取り合えず最前面に表示させる」事を考えます.

最上位フォームとは、アクティブでない場合や前面のフォームではない場合でも、他のすべての (最上位でない) フォームの上に表示されるフォームのことです。最上位フォームは、常にデスクトップ ウィンドウの Z オーダーの最上位に表示されます。

http://msdn.microsoft.com/ja-jp/library/system.windows.forms.form.topmost%28VS.80%29.aspx

Shown イベントが発生した時点で一瞬だけ TopMost プロパティを true にすると,アクティブであってもなくても目的のウィンドウを最前面に表示させる事ができます.

private void Form_Shown(object sender, EventArgs e) {
    this.Activate();
    this.TopMost = true;
    this.TopMost = false;
}

問題はフォーカスが奪えていないため(上部のバーがフォーカスされていない事を表す灰色(クラシックモードのデフォルト時)になっている),ユーザによるフォーカスの移動が意図した結果にならない場合があります.具体的には,その時点でフォーカスを持っているウィンドウを最前面に表示させようとした場合,いったんフォーカスのないウィンドウにフォーカスを動かさなければなりません.