Process.Start()で起動したプロセスのメインウィンドウのハンドルが取得できない場合の対処法


 現在C#にて、以下のような仕様でプロセス間通信を行うアプリケーションを作っているのですが、プロセスAから起動したプロセスBのメインウィンドウのハンドルが取得できないという問題に遭遇しました。

  • ユーザーの操作でプロセスAがプロセスBを起動。
  • プロセスAはプロセスBのメインウィンドウにWM_COPYDATAを用いてデータを送信。
  • プロセスBはプロセスAから受けた指示に従って各種動作を行う。

 一番最初に書いたコードは、次のコードです。
このコードを実行すると、プロセスは起動できるのですがWaitForInputIdle()で起動したプロセスのメッセージループ突入まで待機しているにも関わらず、メインウィンドウのハンドルが取得できずに0が返って来ることがあります。

            //プロセスを生成する。
            Process p = new Process();
            p.StartInfo.FileName = appPath;
            p.StartInfo.Arguments = args;
            p.StartInfo.UseShellExecute = false;
 
            p.Start();
 
            //念のため待つ。
            p.WaitForInputIdle();

            // ウィンドウのハンドルを取得するのだが、
// IEや.NETアプリを立ち上げるとゼロになってしまう場合がある。
            IntPtr hMainWindow = p.MainWindowHandle;

 notepad.exeなどのシンプルなアプリケーションの場合には正しくウィンドウハンドルを取得できるのですが、Internet Explorerや自作の.NETアプリケーションの場合には0が返ってきます。

 どうやら挙動を詳しく調べてみると、一部のアプリケーションを起動した場合にはWaitForInputIdle()がきちんとWaitしてくれていないようで、メインウィンドウが生成される前にMainWindowHandleを取得してしまいゼロが取得されるようでした。

 おそらく一部のアプリケーションでは、ウィンドウ生成後メッセージループ突入という組み方がされていないためこのような現象が起こるのでは、と推測します。

 そこで、以下のようなコードを作成し再びテストを行ったところ、InternetExplorerや.NETアプリケーションを起動してもウィンドウハンドルを取得することができるようになりました。

            //プロセスを生成する。
            Process p = new Process();
            p.StartInfo.FileName = appPath;
            p.StartInfo.Arguments = args;
            p.StartInfo.UseShellExecute = false;
 
            p.Start();
 
            //念のため待つ。
            p.WaitForInputIdle();
 
            //ウィンドウハンドルが取得できるか、
//生成したプロセスが終了するまで待ってみる。
            while (p.MainWindowHandle == IntPtr.Zero && 
p.HasExited == false)
            {
                System.Threading.Thread.Sleep(1);
                p.Refresh();
            }
 
            // hMainWindow != IntPtr.Zeroの場合にはウィンドウハンドルを
//取得できたと思われる。
            IntPtr hMainWindow = p.MainWindowHandle;

 このコードでは、WaitForInputIdle();で念のためメッセージループの開始を待った後に、さらに、本当にウィンドウハンドルが取得できたかどうかをチェックし、取得できるまでループを回す、という事を行っています。

 その際に、起動したプロセスが異常終了した時の事も考慮し、プロセスが終了してしまった場合にはループを抜けるようにしています。

 今のところはこのやり方で、メインウィンドウのハンドルは取得できています。

 ただしこのコードにはタイムアウト処理が入っていません。もし起動したプロセスがなんらかの原因でウィンドウの生成に失敗した場合、起動元のプロセスがフリーズしてしまいますので、実際にはタイムアウト処理も入れた方が良いでしょう。

 さらに.NET Frameworkのドキュメントによりますと、Windows Formで生成したフォームのウィンドウハンドルはフォームの有効期間中に不変であるという保証はないそうです。(ShowInTaskbarプロパティを操作すると変わったりするようです) 従って、ウィンドウハンドルを取得しそこへウィンドウメッセージを送るという方式でプロセス間通信を実現する場合には、取得したメインウィンドウのハンドルを保持しておくのではなく、メッセージ送信前に取得しなおしておいたほうが確実かもしれません。