QA@IT

他者が Form を ShowDialog/Show した瞬間に、自分の処理を動かしたい

6223 PV

既存の Windows Forms Application 用のプラグインのコードを Visual C# 2010 Express で 作っています。

先日の質問、
「MessageBox を表示中に、その MessageBox のインスタンスにアクセスしたい」
http://qa.atmarkit.co.jp/q/3120
と背景は同一です。

既存のアプリケーションがおそらく Form を ShowDialog/Show していて、画面上にその Form が出現するのですが、その出現した瞬間にプラグインの中で自分のコードを実行したいと考えています。
今はプラグインの中で定期的にポーリングして、親となる Form の OwnedForms の Length の値の増加を検査することで一応は実現できていますが、やはりどうしても最大でポーリング間隔の時間、平均でポーリング間隔の半分の時間だけ遅れてしまうので、遅れがないように Form が ShowDialog/Show されるタイミングで raise されるようなイベントがあればそのイベントを捉えたいと思います。そういうイベントはありますか?

以下に、コンテキストを説明する擬似コードを示します。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            Form2 f = new Form2();
            f.ShowDialog(this);
            //f.Show(this);
        }

        private void timer1_Tick(object sender, EventArgs e)
        {
            Console.WriteLine(this.OwnedForms.Length);
        }
    }
}

Form2 を new したり ShowDialog あるいは Show する箇所は、既存のコードが動いていてそこは私がいじることができません。button1 は説明のために付けたイベントハンドラーであり、実際にはどんなイベントで Form2 が new されたり ShowDialog/Show されているのかは不明です。
タイマー(timer1)をしかけることはでき、Form1 の OwnedForms などのプロパティーにアクセスしたり、Form1 のイベントハンドラーにアクセスすることはできます。プラグインのロード時に Form1 のプロパティーやメソッド/イベントハンドラーにアクセスすることもできます。

技術的な課題は、Form2 が ShowDialog/Show されるタイミングを捕まえることだけです。Form2 が出現してしまえば、Form2 のインスタンスには上記のように OwnedForms などを経由して自由にアクセスできます。よろしくお願いします。

  • 漠然とはわかるんですけど、これもう一個インスタンス(監視しているプラグイン、Form1から起動されるもの)がいるってことですよね?Form2のインスタンスにはアクセスできるけど、それも既存のコードまたは別のプラグインであって、Form2のコードも変更することはできない認識でいいですか?
    説明用のコードであればもう少しクラス名をわかりやすくしてもらえると助かります。
    -
  • コメントありがとうございます。質問は、プラグインの作り方の質問というよりも、.NET で Form が ShowDialog/Show される時にイベントは起きますか?のほうを意識しています。Form2 にイベントハンドラーを設定できれば Form2_Load などのイベントハンドラーを設定するだけで済みますが、Form2 のコードはいじれないという前提です。クラス名については今後、検討します。 -
  • プラグインはあまり詳細は書けないので雰囲気で説明しますと Form3 のようなクラスを開発して、プラグインとして Form3 という名称を登録すると、Form1 の起動時に Form3 を new および Show してくれて Form3 の Parent が Form1 になっているような感じです。 -
  • Form3 の Parent が Form1 で、Form1 の OwnedForms の中に Form2 があるから、Form3 から Form1/Form2 のインスタンスにアクセスすることは自由にできます。Form2 のインスタンスがいつ生成されるかというタイミングだけが分からない状況です。 -
  • .NET の Form に OwnedFormAdded のようなイベントがあって、Form3 のインスタンスから、
    this.Parent.OwnedFormAdded += new EventHandler(Parent_OwnedFormAdded);
    のようにしてイベントハンドラーを設定できたらありがたいので探しています。(this.Parent が Form1 を指します。)
    -
  • さきほど書いた「Form2 にイベントハンドラーを設定できれば Form2_Load などのイベントハンドラーを設定するだけで済みます」はできるのですが、それができるのは Form2 のインスタンスが生成された後のことなので、Form2 の生成タイミングが分からないのでジレンマになっています。 -

回答

以下のようなForm ChildPluginForm (Form2と言われていたもの相当)を別プロジェクトのForm ( ParentAppForm )から呼び出してみました。
(面倒だったのでプロジェクト参照してインスタンス作って Show() しているだけですので実際の方式とは違うとは思います。)
実際には winuser.h から WM_xxx引っこ抜いてソースに自動変換したのでWMsgToStringはもっと大量です。

たしかにWM_CREATEとWM_NCCREATEはインスタンス作る時しか出ませんね(当たり前といえば当たり前ですかね。)

WM_ACTIVATEとWM_NCACTIVATEはたしかにShowするたびに発生しているような気がします。

たとえば Visual C# のデバッガー上で動かしていて、Visual C# の IDE のウィンドウが前面に出ていると、拾えなかったです。

上記の状況が作れなかったので、拾えない状況はわかりませんでした。

ところで申し訳ないですが現状どこから何をしたいのかわからなくなってしまいました。 (具体的には自分でWndProcのオーバーロードではなくローカルフックを進めた理由がわからなくなりました)。
→ (書いている途中で思い出しました)

とりあえずForm1, Form2という名前ではなんだかわからないので ParentAppFormChildPluginForm という名前で説明させてもらいます。
途中で思い出しましたが、監視するプラグインがいてそれを今実装してるんでしたね。これはMonitorPluginForm とさせていただきます。

  • ゴール: MonitorPluginForm から ChildPluginForm が (ParentAppForm によって) Show ShowDialog されたことを検知したい
    • 今の流れは私がWM_CREATEWM_ACTIVATE あたりを拾えばよいといったので WM_ACTIVATEだが、アクティブになったことを検知したい?
    • WM_SHOWWINDOWは不要?
  • 前提: ChildPluginForm のコードは変更できない(ソースも見れない?)
  • 前提: ChildPluginFormParentAppForm から呼び出されるプラグインのようなもの。
  • 前提: ParentAppForm はプラグインの親となるアプリケーション
  • 前提: ParentAppForm は変更できない(ソースも見れない?)
  • 前提: MonitorPluginForm が現在コードを作成しているオブジェクト
  • 前提: MonitorPluginForm はコード編集できる
  • 前提: MonitorPluginFormParentAppForm から呼び出されるプラグインのようなもの。
  • 前提: ParentAppForm のインスタンスには MonitorPluginForm.Parent でアクセスできる?
  • 前提: ChildPluginForm のインスタンスには ParentAppForm 経由で ParentAppForm.OwnedForms から探せばアクセスできる

ローカルフックで同一プロセスのメッセージを拾えばいけるかも。
デバッグ中に ChildPluginForm がWM_ACTIVATEが発行されていないことがあるように思う ←今ここ

という状況でしたっけ?

回答書いてる途中でMonitorPluginFormの存在思い出したので以下のコードは前提とちょっと違いますが(対象のコードをいじってるので)、関連ありそうなメッセージを拾って判断するしかないでしょうね。

デバッグ中の件は本当に前にでる必要がなくなって、WindowsがWM_ACTIVATEを発行していないのかもしれません。ちらっとでもウィンドウが見えるとかタスクバーがハイライトされるのに拾えないなら不思議ですね。
無視した場合でもメッセージは発行されますので、それ以外だと自分より先に処理されて自分まで通達が来ていない場合も考えられますね。

namespace ChildPlugin
{
    public partial class ChildPluginForm : Form
    {
        bool seekWM = true;
        StringBuilder sb = new StringBuilder(10240);

        public ChildPluginForm(){
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e){
            seekWM = false;
        }

        protected override void WndProc(ref Message m){
            if (seekWM)
            {
                var msgString = WMsgToString(m.Msg);
                if (msgString == null) msgString = m.Msg.ToString("x9");

                sb.AppendLine(m.HWnd.ToString("X9") + "  " + msgString);
            }

            base.WndProc(ref m);
        }

        private void ChildPluginForm_Paint(object sender, PaintEventArgs e){
            var s = sb.ToString();
            if (s != textBox1.Text)
            {
                textBox1.Text = "";
                textBox1.AppendText(s);
                // textBox1.ScrollToCaret();
            }
        }


        private static string WMsgToString(int msg){
            var msgString = "";
            switch (msg){

                case 32768:
                    msgString = "WM_APP";
                    break;
                case 6:
                    msgString = "WM_ACTIVATE";
                    break;
                case 134:
                    msgString = "WM_NCACTIVATE";
                    break;
                case 28:
                    msgString = "WM_ACTIVATEAPP";
                    break;
                case 3:
                    msgString = "WM_MOVE";
                    break;
                case 534:
                    msgString = "WM_MOVING";
                    break;
                case 130:
                    msgString = "WM_NCDESTROY";
                    break;
                case 1:
                    msgString = "WM_CREATE";
                    break;
                case 129:
                    msgString = "WM_NCCREATE";
                    break;

                case 24:
                    msgString = "WM_SHOWWINDOW";
                    break;

                default:
                    return null;
            }

            return msgString;
       }
}
編集 履歴 (2)
  • 最終目標は ChildPluginForm が出現したら、間を置かずにそのフォームにアクセスして、テキストボックスなどを読み取ったり書き込んだり、ボタンをクリックしたりということをしたいです。フォームにアクセスすることはできていて、残る課題は間を置かずにそのような処理を開始するということだけです。 -
  • その目的のために捉えるイベントはなんでもいいです。たまにイベントを捉えられなくても、あるいは誤認識して多めに捉えてしまっても、多少の誤差は構いません。間隔の長いポーリングを併用すれば、最悪の場合でも動き(反応)が遅いだけで済み、致命的な処理の誤りにはならないので。 -
  • 自分でウィンドウメッセージを捉えるプログラムを書かなくても、ParentAppForm(あるいはその子ウィンドウ)を Spy++ でメッセージをトレースすれば、期待するイベントが起きているのかどうかが判断できると考えています。 -
  • そうやって調べてみると前回書いたように拾えないことがあり、自分でウィンドウメッセージを捉えるプログラムを ChildPluginForm の中でローカルフックとして書いても同様に拾えませんでした。 -
  • ParentAppForm と ChildPluginForm のコードは変更できないし、ソースも見られません。
    ChildPluginForm の中でローカルフックを書いても、フックの対象は ParentAppForm(やその子ウィンドウ)や ChildPluginForm 自身でも選べます。
    -
  • WndProc のオーバーライドはコードをいじることが可能な ChildPluginForm を対象とすることしかできないと思いますので、たぶん今回の目的には使えないと考えています。 -
  • 名称を間違えました。直近の3投稿を破棄して書き直します。 -
  • そうやって調べてみると前回書いたように拾えないことがあり、自分でウィンドウメッセージを捉えるプログラムを MonitorPluginForm の中でローカルフックとして書いても同様に拾えませんでした。 -
  • ParentAppForm と ChildPluginForm のコードは変更できないし、ソースも見られません。 MonitorPluginForm の中でローカルフックを書いても、フックの対象は ParentAppForm(やその子ウィンドウ)や MonitorPluginForm 自身でも選べます。 -
  • WndProc のオーバーライドはコードをいじることが可能な MonitorPluginForm を対象とすることしかできないと思いますので、たぶん今回の目的には使えないと考えています。 -
  • 3投稿の書き直しを完了しました。 -
  • ChildPluginForm はプラグインではなく、親フォームから出される子フォームにすぎません。
    MonitorPluginForm からは MonitorPluginForm.Parent にも ParentAppForm にも自由にアクセスできます。
    -
  • WM_ACTIVATE が拾えないケースで IDE を使わずにできる再現方法を書きます。私の前回の MessageBox の質問のサンプルコードのようにタイマーで Form2 を Show/ShowDialog するようにします。そのタイマーが発動する前に画面上でメモ帳やIE などのアプリケーションをクリックして前面に出し、タイマーが発動するのを待ちます。 -
  • 先日書いた IDE というのは IDE ということにあまり意味はなく、メモ帳や IE などの別アプリケーションという意味にすぎませんでした。 -
  • ちなみに ParentAppForm が MessageBox を出す場合は、ParentAppForm が後面にあっても WM_ACTIVATE が発生するようです。Form の Show/ShowDialog だと発生しないことがあります。 -
  • なるほど、Childの方はプラグインではないんですね。ParentとChildのExeプロジェクトとMonitorのクラスライブラリプロジェクトの2つの構成が近いということですかね。
    MonitorPluginFormから(Parentプロパティだけでなく)ParentAppFormにも自由にアクセスできるというのは、インスタンスとして渡されているということでいいですか?
    -
  • 拾えないタイマーの方法はまだ試していませんが、たとえばBringToFrontなんかで前に来た場合もActiveにはならないので、WM_ACTIVATEでは拾えないかも知れませんね。時間のある時に確認してみます。 -
  • MonitorPluginForm には ParentAppForm のインスタンスは渡されませんが、MonitorPluginForm の中のコンテキストからは this.Parent == ParentAppForm となって見えます。これによりParentAppForm のインスタンスにアクセスできています。 -
  • 了解です。MonitorのParentプロパティからのみParentAppFormにアクセスできるのですね。 -

Form2のコードはいじれない前提で話します。

Form2が独立したクラスライブラリなどで、継承可能であれば継承してWndProcを上書きすればいろいろできそうな気はします。
そして ObserverパターンのようにForm2に状態を通知してもらう機能も拡張すればどうでしょう。


イベントだとピッタリのものはなさそうに思います。
Deactivateなどもプラグインの起動のトリガがユーザーの操作でなければ発生しないでしょうし不完全ですね。

やってみないとわかりませんが、Win32APIを使ってローカルフックでも可能かもしれません。
WM_CREATEだと拾いすぎるので、WM_ACTIVATEで知らないハンドルなら処理するという形でしょうか。

どれぐらいの遅延を気にしているかはわかりませんが、タイマーでもそんなには悪くないと思いますけどね。
フックしてウィンドウメッセージ捌くのとタイマーで定期的に呼ばれてプロパティ見るののどちらがコストが高いかはやってみないとわかりませんが。

編集 履歴 (1)
  • ご回答・コメントありがとうございます。開発しているコードでは、これ以外の箇所でもいろいろポーリングを多用していて、CPU負荷が100%よりも低くなるようにポーリング間隔を調整しているような感じです。そのため一箇所でもポーリングをやめてイベント駆動にしたいと思っています。 -
  • Form の出現を検知するためのポーリングなら、負荷も軽いためポーリング間隔を数十msと短くすることもできますが、技術的な興味もあり、可能ならばイベントで処理したいと思っています。 -
  • たしかにローカルフックでできそうな感じですね。グローバルフックではないので、C# だけでもコーディングできるのかなと思います。とはいえローカルフックでもかなり大変そうですね。 -
  • C# でローカルフックをおこなうサンプルとして、つぎの2つのページを参考にし、ローカルフックは実現できました。 -
  • 方法 : WndProcHooker クラスを使用する
    http://msdn.microsoft.com/ja-jp/library/ms229658(v=vs.80).aspx

    方法 : プラットフォーム呼び出しにヘルパー クラスを使用する
    http://msdn.microsoft.com/ja-jp/library/ms229683(v=vs.80).aspx
    -
  • 両者とも .NET Compact Framework を対象としたサンプルらしいのですが、coredll.dll と書いてある箇所を user32.dll に書き換えるだけで動かせました。 -
  • フックするウィンドウメッセージとして WM_CREATE を指定すると、なぜか全く拾えませんでした。WM_ACTIVATE を指定すると、ほぼ期待どおりに拾えるのですが、メインのウィンドウが前面に出ていない時は拾えないみたいです。 -
  • たとえば Visual C# のデバッガー上で動かしていて、Visual C# の IDE のウィンドウが前面に出ていると、拾えなかったです。
    それ以前に WM_CREATE が拾えないのもちょっと変な感じがするので、私のフックの使いかたがなにか間違っているのかもしれません。もうちょっと調べてみます。
    -
ウォッチ

この質問への回答やコメントをメールでお知らせします。