C#
.NET
WPF

【.NET】ウインドウを一時的に最前面に表示しフォーカスを奪う

はじめに

下記内容の続き、ウィンドウを最前面に表示しフォーカスが奪うに対応した経緯です。

お仕事で外部アプリケーションが出力するログを監視し、ある条件なら警告画面を前面に表示させる要望がありました。
前面ならTopMostにして最前面にすればいいと思ったのですが、指定のダイアログ画面や指定のボタンまたはラベルが表示されている場合には前面を解除したいということになり、タイマーで外部アプリケーションの表示状態を監視して前面表示を解除するようにする予定です。
【.NET】外部アプリケーションの画面上すべてのタイトル(キャプション)を取得する

環境

  • Windows 7(64bit)
  • Windows 7(32bit)

開発PCではWindows 7(64bit)を使用しています。
テスト環境では古め(5年前くらい)のPCでWindows 7(32bit)の2台となります。

一時的に最前面に表示

ウィンドウを常に最前面に表示するにはTopMostプロパティをtrueにすればいいだけです。

今回は常に最前面ではなく、条件によっては最前面でなくす必要があったので、タイマーを使って条件以外の場合に最前面にするという方法を取りました。

最初はWindows APIを使用して一時的に最前面(TopMostを有効後に解除)しました。
これで、ウィンドウが最前面にすることが出来ました。

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

// ウィンドウをアクティブにする
public static void SetActiveWindow(IntPtr hWnd)
{
    const int SWP_NOSIZE = 0x0001;
    const int SWP_NOMOVE = 0x0002;
    const int SWP_SHOWWINDOW = 0x0040;

    const int HWND_TOPMOST = -1;
    const int HWND_NOTOPMOST = -2;

    SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
    SetWindowPos(hWnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_SHOWWINDOW | SWP_NOMOVE | SWP_NOSIZE);
}

// WPFでウィンドウハンドルを取得
IntPtr handle = new WindowInteropHelper(this).Handle;
SetActiveWindow(handle)

フォーカスを奪えない

上述の方法でウィンドウが最前面になることは出来たのですが、警告画面にボタンのみある状態で最前面になった場合、Enterキーを押したら警告画面が閉じると思ったのですが、実際には直前にアクティブなアプリケーション側にフォーカスがある状態になっているのです。

例えば、メモ帳にフォーカスがある状態で警告画面を最前面にした場合、Enterキーを押すとメモ帳側に改行が入力されるのです。

ネットで調べていくと下記サイトを見つけました。
Form の this.Activate() について

これで警告画面が最前面に表示された際に、Enterキーを押すと警告画面を閉じることが出来ました。また、Windows APIも不要で済んだわけです。

// これを実行しないとフォーカスが奪えない
this.Activate();

// 最前面にした後に解除することで前面化させる
this.Topmost = true;
this.Topmost = false;

マウスでウィンドウをクリック

ところが開発PCでは正常に動作するものの、テスト環境のPCではフォーカスが奪えない状態になりました。
ネットの記事などを参考にWindows API(AttachThreadInputやSetForegroundWindowやBringWindowToTopなど)を駆使してみるのですが、一向に解決しないのです。

SetForegroundWindowはフォーカスを奪うことになっており、失敗する特定のケースがあります。
SetForegroundWindow関数は、指定されたウィンドウを作成したスレッドをフォアグラウンドに置き、ウィンドウをアクティブにします。 キーボード入力はウィンドウに向けられます
SetForeGroundWindow、SetActiveWindow、およびBringWindowToTopの違いは何ですか? あたかも同じことをするかのように見えます。
MSDNによると、SetForeGroundWindowはウィンドウをアクティブにし、キーボードフォーカスをウィンドウに表示します。 これは、プロセスがバックグラウンドであっても動作します。 SetActiveWindowはSetForeGroundWindowと同じことを行いますが、アプリケーションが最前面のアプリケーションでない場合は何も行いません。 最後に、BringWindowToTopはウィンドウを一番上に移動させ、キーボードのフォーカスを変更しません。
c# - Win32 APIを使用してC#でウィンドウを前面に表示

しかし、マウスで警告画面をクリックすればアクティブになるのは分かっていました。時間も無かったし無理矢理ではあるのですが、自動でマウスをクリックするという方向に舵を切りました。参考:ユーザ入出力>マウスの自動操作 - 緑のバイク

WindowsHandles.cs
using System.Runtime.InteropServices ;  // for DllImport, Marshal

[DllImport("user32.dll")]
extern static uint SendInput(
    uint       nInputs,   // INPUT 構造体の数(イベント数)
    INPUT[]    pInputs,   // INPUT 構造体
    int        cbSize     // INPUT 構造体のサイズ
    ) ;

[StructLayout(LayoutKind.Sequential)]  // アンマネージ DLL 対応用 struct 記述宣言
struct INPUT
{ 
    public int        type ;  // 0 = INPUT_MOUSE(デフォルト), 1 = INPUT_KEYBOARD
    public MOUSEINPUT mi   ;
    // Note: struct の場合、デフォルト(パラメータなしの)コンストラクタは、
    //       言語側で定義済みで、フィールドを 0 に初期化する。
}

[StructLayout(LayoutKind.Sequential)]  // アンマネージ DLL 対応用 struct 記述宣言
struct MOUSEINPUT
{
    public int    dx ;
    public int    dy ;
    public int    mouseData ;  // amount of wheel movement
    public int    dwFlags   ;
    public int    time      ;  // time stamp for the event
    public IntPtr dwExtraInfo ;
    // Note: struct の場合、デフォルト(パラメータなしの)コンストラクタは、
    //       言語側で定義済みで、フィールドを 0 に初期化する。
}

// アクティブなプロセス名を取得する
public static string GetActiveProcessName()
{
    // 現在アクティブなプロセスIDとプロセス名を取得
    int processId;
    GetWindowThreadProcessId(GetForegroundWindow(), out processId);

    return Process.GetProcessById(processId).ProcessName;
}

// ウィンドウをアクティブにする
public static void ForceActive(int x, int y)
{
    const int MOUSEEVENTF_LEFTDOWN = 0x2;
    const int MOUSEEVENTF_LEFTUP = 0x4;

    // 現在のマウス位置を取得
    Point pt = new Point(0,0);
    GetCursorPos(out pt);

    // 指定座標にマウス移動
    SetCursorPos(x, y);
    // クリック
    INPUT[] input = new INPUT[2];
    input[0].mi.dwFlags = MOUSEEVENTF_LEFTDOWN;     //左ボタン Down
    input[1].mi.dwFlags = MOUSEEVENTF_LEFTUP;       //左ボタン Up
    SendInput(2, input, Marshal.SizeOf(input[0]));

    // マウス位置を元に戻す
    SetCursorPos(pt.X, pt.Y);
}

※MOUSE動作のイベントのみであればdx,dyフィールドの設定は不要とのことで削除しました。また、SetCursorPos(x, y)を使わなくても、MOUSEEVENTF_MOVEを使えばdx,dyフィールドでマウス移動ができるとのこと、注意としてマウスロケーションをスクリーン座標から32bit座標に変換する必要がある。

// マウスカーソルの移動 例
input[0].mi.dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE;
input[0].mi.dx = x * (65535 / Screen.PrimaryScreen.Bounds.Width);
input[0].mi.dy = y * (65535 / Screen.PrimaryScreen.Bounds.Height);

WPF側で警告画面の表示位置から少しずらした(+10)ところでマウスを自動クリックさせてフォーカスを奪うようにしました。(マウス位置は保持してクリック後に元に戻します)
自身がアクティブの場合は何もしないようにしています。そうしないとタイマーで実行しているので警告画面の位置を移動したりサイズを変更しようとしてもマウス位置が意図しない方向になり変な動作をしてしまうのです。

// private string _appProcessName = Process.GetCurrentProcess().ProcessName;
// 自身がアクティブなら何もしない
string processName = WindowsHandles.GetActiveProcessName();
if (processName == _appProcessName) return;

// これを実行しないとフォーカスが奪えない
this.Activate();

// 最前面にした後に解除することで前面化させる
this.Topmost = true;
this.Topmost = false;

// PCによってはフォーカスが奪えないため、マウスクリックでアクティブ化する。
WindowsHandles.ForceActive((int)this.Left + 10, (int)this.Top + 10);

この方法ならフォーカスを奪うことができ、目的は達成できました。

リベンジ【2018/01/18追記】

マウスでウィンドウをクリックする方法は、一時的なら問題ないがタイマーの間隔が短いと別アプリケーションをマウスで移動やサイズ変更などさせようとすると、マウスが変な動作をしてしまい正常な動作ができなくなる問題が発生する。

再度、Windows APIを駆使して実現を試みる。先ずはフォーカスを奪えない原因を知ることから始める。

Win95/NT4までは、SetForegroundWindowだけでフォアグラウンドウィンドウを
切り替えられましたが、Win98/2000移行では、切り替え可能なプロセスは、
システムにより制限されるようになっています。
メニュー選択や文字の入力中に、勝手にウィンドウが切り替えられてしまい、
作業中の処理が中断してしまうことを防ぐための仕様変更だそうです
SetForegroundWindow で指定したウィンドウがアクティブにならない場合には?

「SystemParametersInfo 関数」のSPI_GETFOREGROUNDLOCKTIMEOUTの説明によると、「ユーザーが何かを入力した後、システムは一定の時間にわたって、アプリケーションが自らをフォアグラウンドにすることを禁止します」ということです。よって、この時間(ForegroundLockTimeout)を0にするという対策も考えられます。
ForegroundLockTimeoutの値を0にする - 外部アプリケーションのウィンドウをアクティブにする

C#でAttachThreadInputSystemParametersInfoを使う下記サイトを参考にしてみた。
[C#]フォームをフォアグラウンドまたはアクティブにする

WindowsHandles.cs
[DllImport("user32.dll")]
private static extern bool IsIconic(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern bool ShowWindowAsync(IntPtr hWnd,int nCmdShow);
[DllImport("user32.dll")]
private static extern int GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);
[DllImport("user32.dll")]
private static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll", SetLastError = true)]
private static extern bool SystemParametersInfo(uint uiAction, uint uiParam, IntPtr pvParam, uint fWinIni);
[DllImport("user32.dll")]
private static extern bool AttachThreadInput(int idAttach, int idAttachTo, bool fAttach);

// ウィンドウを強制的にアクティブにする
public static void ForceActive(IntPtr handle)
{
    const uint SPI_GETFOREGROUNDLOCKTIMEOUT = 0x2000;
    const uint SPI_SETFOREGROUNDLOCKTIMEOUT = 0x2001;
    const int SPIF_SENDCHANGE = 0x2;
    const int SW_RESTORE = 9;

    IntPtr dummy = IntPtr.Zero;
    IntPtr timeout = IntPtr.Zero;

    // 最小化状態なら元に戻す
    if (IsIconic(handle))
        ShowWindowAsync(handle, SW_RESTORE);

    int processId;
    // フォアグラウンドウィンドウを作成したスレッドのIDを取得         
    int foregroundID = GetWindowThreadProcessId(GetForegroundWindow(), out processId); 
    // 目的のウィンドウを作成したスレッドのIDを取得
    int targetID = GetWindowThreadProcessId(handle, out processId);

    // スレッドのインプット状態を結び付ける   
    AttachThreadInput(targetID, foregroundID, true);
    // 現在の設定を timeout に保存
    SystemParametersInfo(SPI_GETFOREGROUNDLOCKTIMEOUT, 0, timeout, 0);
    // ウィンドウの切り替え時間を 0ms にする
    SystemParametersInfo(SPI_SETFOREGROUNDLOCKTIMEOUT, 0, dummy, SPIF_SENDCHANGE);

    // ウィンドウをフォアグラウンドに持ってくる
    SetForegroundWindow(handle);

    // 設定を元に戻す
    SystemParametersInfo(SPI_SETFOREGROUNDLOCKTIMEOUT, 0, timeout, SPIF_SENDCHANGE);
    // スレッドのインプット状態を切り離す
    AttachThreadInput(targetID, foregroundID, false);
}

この方法でフォーカスを奪うことができるようになったのですが、SystemParametersInfoで設定変えるので好ましいとは思えなかったので、試しにコメントアウトしてみたところ、それでもフォーカスを奪うこと出来ました。

WindowsHandles.cs
[DllImport("user32.dll")]
private static extern bool IsIconic(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern bool ShowWindowAsync(IntPtr hWnd,int nCmdShow);
[DllImport("user32.dll")]
private static extern int GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);
[DllImport("user32.dll")]
private static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern bool AttachThreadInput(int idAttach, int idAttachTo, bool fAttach);

// ウィンドウを強制的にアクティブにする
public static void ForceActive(IntPtr handle)
{
    const int SW_RESTORE = 9;

    // 最小化状態なら元に戻す
    if (IsIconic(handle))
        ShowWindowAsync(handle, SW_RESTORE);

    int processId;
    // フォアグラウンドウィンドウを作成したスレッドのIDを取得         
    int foregroundID = GetWindowThreadProcessId(GetForegroundWindow(), out processId); 
    // 目的のウィンドウを作成したスレッドのIDを取得
    int targetID = GetWindowThreadProcessId(handle, out processId);

    // スレッドのインプット状態を結び付ける   
    AttachThreadInput(targetID, foregroundID, true);
    // ウィンドウをフォアグラウンドに持ってくる
    SetForegroundWindow(handle);
    // スレッドのインプット状態を切り離す
    AttachThreadInput(targetID, foregroundID, false);
}

呼び出し側です。WPFで作成した画面はタスクトレイで常駐しており、インスタンスを使いまわしています。Topmostプロパティの設定を再度行っているのは、最初は最前面に表示されたのですが閉じた後の2回目以降が最前面にならなかったためです。

画面を使いまわすと例外エラーが発生したので、下記サイトにて対応しました。
Visual Studio / WPF > Form > 閉じたウィンドウを再度ShowModal()したとき > Error:System.InvalidOperationException: 'Window が閉じた後で、Visibility の設定や、Show、ShowDialog、およびWindowInteropHelper.EnsureHandl の呼び出しを行うことはできません。 > 対処

/// <summary>
/// ウィンドウを前面化する
/// </summary>
public void SetActiveWindow()
{
    // 自身をアクティブにする
    this.Activate();

    // 表示の最初は最前面とする
    this.Topmost = true;
    // 最前面にした後に解除することで前面化させる
    this.Topmost = false;

    // 強制的にフォーカスを奪う
    var helper = new System.Windows.Interop.WindowInteropHelper(this);
    WindowsHandles.ForceActive(helper.Handle);

    // 再度、再設定する
    // 表示の最初は最前面とする
    this.Topmost = true;
    // 最前面にした後に解除することで前面化させる
    this.Topmost = false;

    // アクティブイベントを呼ぶ
    Window_Activated(null, EventArgs.Empty);
}

これでマウスの自動クリックを使うことなくフォーカスを奪うことができ、目的は達成できました。

再リベンジ【2018/01/19追記】

最前面に表示について、2回目以降だと最前面にならず背面に隠れてしまう。Topmostプロパティを最前面にした後に解除することで前面化させている1 、昨日テストした時点では確認画面が最前面に表示されたままで他アプリケーションに制御が移れば背面に隠れる想定通りの動作だったのに、今日実行したら駄目でした。Windows制御は難しいね。

確かに理屈的には他アプリケーションが表示されたら確認画面が背面になる、よって最前面表示されたらタイマーで1秒後に最前面を解除するように修正した。またTopmostプロパティではなくWindows APIのSetWindowPosを使うようにした。

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

// ウィンドウの最前面/解除
public static void SetTopMostWindow(IntPtr handle, bool isTopMost)
{
    const int SWP_NOSIZE = 0x0001;
    const int SWP_NOMOVE = 0x0002;
    const int SWP_SHOWWINDOW = 0x0040;
    const int HWND_TOPMOST = -1;
    const int HWND_NOTOPMOST = -2;

    if (isTopMost)
    {
        // 最前面
        SetWindowPos(handle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
    }
    else
    {
        // 最前面解除
        SetWindowPos(handle, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_SHOWWINDOW | SWP_NOMOVE | SWP_NOSIZE);
    }
}

警告画面なので最初は最前面表示し、タイマーを使い設定したミリ秒後に背面にする。

/// <summary>
/// ウィンドウを前面化する
/// </summary>
/// <param name="isTopMostOnly">最前面化設定</param>
public void SetActiveWindow(bool isTopMostOnly)
{
    // 自身をアクティブにする
    this.Activate();

    var helper = new System.Windows.Interop.WindowInteropHelper(this);
    // 表示の最初は最前面とする
    WindowsHandles.SetTopMostWindow(helper.Handle, true);
    if (!isTopMostOnly)
        // 最前面にした後に解除することで前面化させる
        WindowsHandles.SetTopMostWindow(helper.Handle, false);

    // 強制的にフォーカスを奪う
    WindowsHandles.ForceActive(helper.Handle);

    // 背面に隠れることがあるため、再度繰り返す
    for(int i = 0; i < 2; i++)
    {
        // 表示の最初は最前面とする
        WindowsHandles.SetTopMostWindow(helper.Handle, true);
        if (!isTopMostOnly)
            // 最前面にした後に解除することで前面化させる
            WindowsHandles.SetTopMostWindow(helper.Handle, false);
    }

    // アクティブイベントを呼ぶ
    Window_Activated(null, EventArgs.Empty);
}

もう1つ、再現性は不明だがまれにタスクバーで点滅してフォーカスをが奪えないことがあった。このため、SystemParametersInfo を復活させるようにしました。このくらいしか対処できぬ。

最後に

小手先な方法ではなく、きちっとした方法で解決できれば良かったんですけどね。レジストリは絶対いじりたくないし、非推奨な方法でもやりたくない。出来ればWindows APIもあまり使いたくなかったんですよね。マウスの自動クリックでWindows APIが必要になるとは思わなかった。

【2018/01/18追記】
やはり、マウスの自動クリックという小手先な方法は使わない方がいい。今回は時間を頂いたので取り組むことが出来たのですが、この解決に到るまで結構かかってしまいました。

【2018/01/19追記】
結局、Windows API使いまくりになってしまった。


  1. TopmostプロパティがTrueのままだと常に最前面になるため、Topmostプロパティをfalseにすることで最前面が解除され、他アプリケーションに制御が移れば背面に隠れるようになる。