タスクトレイ常駐アプリの実装 Tips&Tricks(その1)

2013/11/3追記:続きの記事(その2)を書きました。
2013/12/8追記:続きの記事(その3・Alt+F4キー対策)を書きました。

DLWアクセスランプというユーティリティを作っている過程で、
C#(WindowsForm)でタスクトレイアイコン常駐型アプリケーションを実装するにあたり、注意しなければならない点・知っておいたほうがよいことがいろいろと見えてきました。
あまり需要がない気もしますが、ノウハウとして公開してみます。

起動時にフォームを表示せずトレイアイコンのみ表示させる場合の注意点

フォームを表示させずにトレイアイコンを表示する(DOBON.NET)などでさまざまなやり方が紹介されていますが、
引数なしでApplication.Run()を実行する方法は要注意です。

タイマイベントなどから表示更新といったUI操作ができない

引数なしでApplication.Run()を実行する方法は、
たしかに起動時のフォームを表示しないようにできますが、遅かれ早かれ
タイマイベントなど非UIスレッドで実行される処理からはUI操作ができない」
という問題にぶちあたります。

class Program
{
    [STAThread]
    static void Main(string[] args)
    {
        //通常のアプリケーション起動。Form1が起動時に表示される
        //Application.Run(new Form1());
        
        //(1)Formは生成するが、起動時パラメータでフォームを指定しない
        Form1 form = new Form1(); //Form1の中でNotifyIconを生成
        Application.Run();
        
        //(2)Formの代わりにComponentの継承クラスを生成、起動時パラメータを指定しない
        Component1 comp = new Component1(); //Component1の中でNotifyIconを生成
        Application.Run();
    }
}

このように実装している場合、タイマイベントなどの非UIスレッドからの操作では、NotifyIconのツールチップ表示やアイコン表示を書き換えることは可能ですが、フォームやダイアログの表示といった処理(UIスレッドへInvokeしなければならない処理)が実行できません。

(1)であればFormのInvoke()、(2)ならComponent内の適当なコントロールInvoke()を行えば、一見正しくUIスレッドへ処理を引き渡せるかのように思えます。
しかし、実際には(1)(2)とも、オブジェクトは生成したものの、実際に画面表示を行っていないので、コントロールのウィンドウハンドルが作成されていません。
で、ウィンドウハンドルが未作成だとInvoke()メソッドは例外System.InvalidOperationException「ウィンドウ ハンドルが作成される前、コントロールInvoke または BeginInvoke を呼び出せません。」をThrowして異常終了してしまい、必要なUI操作ができません。
※このあたりの問題は、http://igeta.cocolog-nifty.com/blog/2007/06/invoke.htmlの記事が詳しいです。

ウィンドウハンドルさえ確保できれば問題は解決する

普通にApplication.Run(form)で起動するなどして、いったんフォーム等を表示させればウィンドウハンドルが確保でき、Invokeも正しく動くようになります。
もっとも、Application.Run()もしくはform.Show()で表示した後すぐにHide()して非表示にするのも、一瞬ウィンドウが表示されてしまうことになってしまい美しくありません。
Opacityの設定、CreateParamsをオーバーライドするのもちょっとトリッキーです。

ウィンドウハンドルを確保するだけならもっと簡単な方法があります。
Control.Handle プロパティです。

Control.Handle プロパティ
コントロールのバインド先のウィンドウ ハンドルを取得します。


Handle プロパティの値は Windows HWND です。ハンドルがまだ作成されていない場合は、このプロパティを参照すると強制的にハンドルが作成されます。

Control.Handle Property (System.Windows.Forms) | Microsoft Docs

これを利用し、各コントロールのハンドルを無理やり確保してしまえばよいわけです。
FormであればForm側のコンストラクタのどこかに「IntPtr dummy = this.Handle;」と記述してもよいし、Mainメソッド内でFormを生成したときにハンドルを確保してもよいです。

    static void Main(string[] args)
    {   //(1の改良版)Formを生成し、ハンドルを確保するが、起動時パラメータでは指定しない
        Form1 form = new Form1(); //Form1の中でNotifyIconを生成
        IntPtr dummy = form.Handle; //【ハンドルを確保】
        Application.Run();
    }

Componentを用いる場合(前述(2))も要領は一緒で、Component内の適当なコントロールでハンドルを確保すればOKです。
これでInvoke()にまつわる問題から解放されます。

タスクトレイ常駐アプリでは、FormやComponentの生成は必ずしも必須ではない?

そもそもNotifyIconを使用するにあたっては、必ずしもNotifyIconをFormに配置する必要はありません。
(Componentを作成しその中に配置すればOKらしい)
前項で見たとおり、ウィンドウハンドルによるInvokeの問題は、Form/Componentとも同様に発生します。

さらに一歩進めて、FormだけでなくComponentも用いずに、
MainメソッドのあるクラスでNotifyIconを生成してみたら、どうなるでしょうか?

class Program
{
    internal static NotifyIcon icon;
    internal static ContextMenuStrip cms;
    [STAThread]
    static void Main(string[] args)
    {
        //(3)Formではなく、表示する通知アイコン/アイコンのメニューを直接生成する
        icon = new NotifyIcon();
        cms = new ContextMenuStrip();
        icon.ContextMenuStrip = cms; //右クリックメニューを設定する
        (アイコンに関する各種設定、イベント設定をいろいろ行う)
        Application.Run();
    }
}
(そのほか必要なメソッド等も定義)

・・・・実は、NotifyIconを表示するだけならこれでも動きます。
Invokeが必要な操作についても、NotifyIconにはInvokeメソッドがありませんが(Controlを継承していないので)、適当なコントロール部品のInvokeメソッドを使用すれば、UI操作も問題なく行えます。
NotifyIconを利用するプログラムならたいてい右クリックメニューを仕込むことになるので、ContextMenuStrip/ContextMenuあたりを使えばよいでしょう。

ただし、通知アイコンの右クリックを一度も行っていない状態でInvokeが呼び出されてしまうと、ContextMenuStripが表示されていない=ハンドルが確保されていない、ということで、Form/Componentの場合と同様に、例外System.InvalidOperationExceptionになってしまいます。
これもControl.Handleプロパティでウィンドウハンドルを確保してしまえば解決します。

    internal static NotifyIcon icon;
    internal static ContextMenuStrip cms;
    [STAThread]
    static void Main(string[] args)
    {
        //(3の改良版)表示する通知アイコン/アイコンのメニューを直接生成、ハンドル確保
        NotifyIcon icon = new NotifyIcon();
        ContextMenuStrip cms = new ContextMenuStrip();
        icon.ContextMenuStrip = cms;
        IntPtr dummy = cms.Handle; //ハンドルを確保
        (アイコンに関する各種設定、イベント設定)
        Application.Run();
    }

    private static Form1 form;
    //非UIスレッドから呼び出されるフォーム表示処理
    internal static void ShowForm1()
    {
        cms.BeginInvoke((MethodInvoker)(() =>
        {
            if (form == null || form.IsDisposed)
            {
                form = new Form1();
                form.Show();
            }
            form.WindowState = FormWindowState.Normal;
            form.Activate();
        }
    }

Invokeによりフォームを表示する、既に表示中ならフォーカスする処理のサンプルソースです。
手持ちのWindowsXPWindows7(32/64)環境、ならびに報告いただいたところではWindowsVista(32/64)・Windows8.1環境とも、特に支障なく動いてています。
タスクトレイ常駐アプリケーションの実装にあたっては、
ウィンドウハンドルさえ確保できていれば必ずしもFormやComponentを生成する必要はなく、NotifyIcon・ContextMenuStripなどアプリケーションで必要なコントロールだけ生成すれば足りると思われます。

※ContextMenuStripをInvokeの踏み台にするのもちょっと違和感がありますが・・・・ダミーのメインフォームを作成して踏み台にするよりはマシかと思われます。

11/3追記:その2に続きます。