タスクトレイ常駐アプリの実装 Tips&Tricks(その3・Alt+F4キー対策)

C#(WindowsForm)のNotifyIconクラスを使用したタスクトレイ通知領域常駐アプリケーションでは、

  1. トレイアイコンを右クリックし、コンテキストメニューを表示する
  2. ALT+F4キーを押す

という操作を行うと、プログラムは起動したままトレイアイコンが消えてしまい、タスクマネージャーからプロセスを止める以外にアプリケーションを終了させることができなくなってしまいます。
この問題の回避策ですが、以前の記事で書いた方法をさらに改良してみたところ、よい感じの挙動になってくれたので、公開します。

原因と対策のおさらい

詳細は以前の記事を参照ください。
タスクトレイのNotifyIconは、擬似的なウィンドウ(フォーム)の管理下に置かれており、NotifyIconの右クリックメニューが表示されたタイミングでこの擬似ウィンドウがアクティブなフォアグラウンドウィンドウになります。
Alt+F4キーの動作(現在のウィンドウを閉じる)をアクティブになっている擬似ウィンドウが受け付けると、紐付いているNotifyIconもろとも消されてしまいます。
逆に言えば、擬似ウィンドウがアクティブでなければ、Alt+F4が押されてもNotifyIconが消えてしまうようなことはなくなります。

以前の記事では、

  • コンテキストメニューが閉じるタイミングで、デスクトップをアクティブに(擬似ウィンドウを非アクティブに)する
  • それでもタイミングによってはAlt+F4キーが効いてしまいトレイアイコンが消えてしまうので、NotifyIconのアイコン画像を設定しなおすことで、トレイアイコンを再表示・復旧させる

という方法を紹介しましたが、

  1. トレイアイコンが消えていないか都度チェックする必要がある
  2. トレイアイコンが消えてしまうことが避けられない、タスクバーからアイコン数が増減するのが見た目美しくない
  3. 強制的にデスクトップをアクティブにするので、ほかのウィンドウをクリックして右クリックメニューを閉じたときにもデスクトップにフォーカスが移ってしまい、クリックしたウィンドウがアクティブにならない
    ※2階層目のサブメニューを表示していると100%再現する

といった問題があり、やり方を再検討してみたところ、以下のようなソースコードに落ち着きました。

Alt+F4キー対策版ソースコード

class Program
{
  static NotifyIcon tray; //タスクトレイアイコン
  static ContextMenuStrip cms; //右クリックメニュー

  /// <summary>アクティブウィンドウ制御要否判定に使用</summary>
  [DllImport("user32")]
  static extern IntPtr GetForegroundWindow();
  /// <summary>デスクトップをアクティブにする際に使用</summary>
  [System.Runtime.InteropServices.DllImport("user32")]
  [return: MarshalAs(UnmanagedType.Bool)]
  static extern bool SetForegroundWindow(IntPtr hWnd);
  /// <summary>デスクトップをアクティブにする際に使用</summary>
  [System.Runtime.InteropServices.DllImport("user32")]
  static extern IntPtr GetDesktopWindow();

  static void main()
  {
    :(tray,cmsをいろいろ初期化)
    tray.ContextMenuStrip = cms;

    // Alt+F4対策#1:ContextMenuStrip表示中にAltキーが押されたら
    //               デスクトップをアクティブにする
    cms.PreviewKeyDown += (sender, e) =>
    {
      if (e.Alt)
      {
        SetForegroundWindow(GetDesktopWindow());
      }
    };
    // Alt+F4対策#2:メニューを閉じた段階でトレイの擬似ウィンドウがアクティブなら
    //               デスクトップをアクティブにする
    cms.Closing += (sender, e) =>
    {   
        //NotifyIconから、擬似ウィンドウ(NativeWindow)を取得
        FieldInfo fi = typeof(NotifyIcon).GetField("window", BindingFlags.NonPublic | BindingFlags.Instance);
        NativeWindow window = fi.GetValue(tray) as NativeWindow;
        //アクティブなウィンドウのハンドル=擬似ウィンドウのハンドルか判定
        IntPtr handle = GetForegroundWindow();
        if (handle == window.Handle)
        {
            SetForegroundWindow(GetDesktopWindow());
        }
    };
    :

NotifyIconの右クリックメニューはAltキーを押した時点で閉じられてしまうので、PreviewKeyDownイベントでいち早く検出してデスクトップにフォーカスを移しておけば、Altキーに続いてすぐF4キーが押されても問題ありません。上記の問題点1・問題点2の考慮も一切不要となります。
また問題点3についても、メニューを閉じられた際のアクティブなウィンドウを調べ、擬似ウィンドウ以外のほかのウィンドウがすでにアクティブになっているならフォーカス移動しない・・・という制御を入れることで、きれいに解決できます。

DLWアクセスランプVectorで公開している自作ソフトです)にも、Ver1.3.0でこの対処を組み込みました。問題なく良好に動作することを確認しています。