DOBON.NET DOBON.NETプログラミング掲示板過去ログ

ShowInTaskbar = False でのメインウィンドウハンドルの取得方法について

環境/言語:[XP / C# / Framework 2.0]
分類:[.NET]

先日は"角の丸いフォーム"の件でお世話になりました、まことと申します。
今回も、引き続き開発中の拡大鏡プログラムに起因する質問をさせて下さい。


現在"プログラムの実行"をトリガーに、トグルスイッチの様な感じで起動⇔終了を
切り替えたいと考えております。

理論的には起動時に二重起動のチェックを行い、起動済みのプロセスからメインの
ウィンドウハンドルを取得し、WM_CLOSEでも送ってやれば…と思っていたのですが
表題に書いてあります様に、ShowInTaskbar を False とした場合の
Process.MainWindowHandle が常に 0 となってしまい、上手く行きません。

自分なりに調べてみた結果、Process を足掛かりにフォームのハンドルを取得する
事が出来ないと判断したのですが、まずこの判断は正しいのでしょうか?


更に FindWindow と GetClassName を使用して、同一クラス名を持つウィンドウの
ハンドルを取得しようと試みているのですが、自身のクラス名を探し出すと当り前
ですが自分自身を検出してしまい、起動すらしなくなります(苦笑)

そこで Application.Run の前にダミーのフォームをインスタンス化し、それから
クラス名を取得した後にダミーフォームを破棄してから FindWindow を呼び出して
いるのですが、スマートな方法では無い様な気がしてなりません。

尚 Form.Text は都合により空文字 or としていますので、ウィンドウタイトルで
検索する事出来ない物と考えております。

上記以外の方法で実行中の同一プログラムのウィンドウハンドルを取得する方法が
有りましたら教えて頂けないでしょうか。

以上 宜しくお願い致します。


static void Main()
{
    Application.SetCompatibleTextRenderingDefault(false);

    using (Form1 dummy = new Form1())
    {
        StringBuilder className = new StringBuilder(256);
        GetClassName(new HandleRef(null, dummy.Handle), className, className.Capacity);
    }

    IntPtr hWnd = FindWindow(className.ToString(), null);

    if (hWnd == IntPtr.Zero)
    {
        Application.Run(new Form1());
    }
    else
    {
        SendMessage(hWnd, WM_CLOSE, IntPtr.Zero, IntPtr.Zero);
    }
}
サンプルソースに一部誤りが有りましたので訂正いたします。

static void Main()
{
    Application.SetCompatibleTextRenderingDefault(false);

    StringBuilder className = new StringBuilder(256);

    using (Form1 dummy = new Form1())
    {
        GetClassName(new HandleRef(null, dummy.Handle), className, className.Capacity);
    }

    IntPtr hWnd = FindWindow(className.ToString(), null);

    if (hWnd == IntPtr.Zero)
    {
        Application.Run(new Form1());
    }
    else
    {
        SendMessage(hWnd, WM_CLOSE, IntPtr.Zero, IntPtr.Zero);
    }
}
EventWaitHandle を使って名前付きイベントで多重起動を通知すればいいかと思います。
先に起動した方は EventWaitHandle をワーカスレッドでタイムアウト無しの WaitOne。待機が完了したら Invoke を使って終了処理。
後に起動した方は EventWaitHandle を Set。
多重起動判定も EventWaitHandle 作成時にできますね(コンストラクタの引数 createNew が false を返したら 2 つ目のインスタンスが起動)。
■No23908に返信(Hongliangさんの記事)
> EventWaitHandle を使って名前付きイベントで多重起動を通知すればいいかと思います。
> 先に起動した方は EventWaitHandle をワーカスレッドでタイムアウト無しの WaitOne。待機が完了したら Invoke を使って終了処理。
> 後に起動した方は EventWaitHandle を Set。
> 多重起動判定も EventWaitHandle 作成時にできますね(コンストラクタの引数 createNew が false を返したら 2 つ目のインスタンスが起動)。

Hongliangさん、お返事ありがとうございます。

EventWaitHandle や 名前付きイベント と言う物を始めて知りまして、現在調べている
最中なのですが、厚かましいお願いを承知の上で、もし良ければお手隙の際で結構です
ので具体的なサンプルコードを提示しては頂けないでしょうか?

宜しくお願い致します。
■No23908に返信(Hongliangさんの記事)
> EventWaitHandle を使って名前付きイベントで多重起動を通知すればいいかと思います。


実業務が一段落しましたので EventWaitHandle について調べて見ました。

Hongliangさんに教えて頂いた内容を元にソースを記述してみたのですが、雰囲気的に
下記のような記述で合っているでしょうか?


public partial class Form1 : Form
{
    private EventWaitHandle ewh;

    public Form1()
    {
        InitializeComponent();

        bool createdNew;

        ewh = new EventWaitHandle(false, EventResetMode.AutoReset,
                                  "hogehoge", out createdNew);

        if (!createdNew)
        {
            bool ret = ewh.Set();
        }
        else
        {
            backgroundWorker1.RunWorkerAsync();
        }
    }

    private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
    {
        ewh.WaitOne();
        MessageBox.Show("他で起動されたから終了");
        e.Result = 0;
    }

    private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        ewh.Close();
        this.Close();
    }
}


ただし1点分からない事がありましたので、引き続き教えて頂けると幸いです。

EventWaitHandleのコンストラクタの説明(ヘルプ)に「(非)シグナル状態」と「スレッド
のブロック」について以下の様に記載が有ります。

> イベントの初期状態が非シグナル状態の場合、イベントを待機するスレッドはブロックされます。
> イベントの初期状態がシグナル状態で、ManualReset フラグが mode に指定されている場合、
> イベントを待機するスレッドはブロックされません。
> イベントの初期状態がシグナル状態で、mode が AutoReset である場合、イベントを待機している
> 最初のスレッドはすぐに解放されますが、その後イベントがリセットされ、後続のスレッドはブロックされます。

この部分がどうにもイメージし難いのですが、どなたかもう少し噛み砕いた形で教えて
頂けないでしょうか?

以上 宜しくお願い致します。
> Hongliangさんに教えて頂いた内容を元にソースを記述してみたのですが、雰囲気的に
> 下記のような記述で合っているでしょうか?
Form のコンストラクタでやるもんでもないと思いますが、そんな感じです。
// Main メソッドの中で Application.Run する前にイベントオブジェクト作るかな。


>>イベントの初期状態が非シグナル状態の場合、イベントを待機するスレッドはブロックされます。
>>イベントの初期状態がシグナル状態で、ManualReset フラグが mode に指定されている場合、
>>イベントを待機するスレッドはブロックされません。
>>イベントの初期状態がシグナル状態で、mode が AutoReset である場合、イベントを待機している
>>最初のスレッドはすぐに解放されますが、その後イベントがリセットされ、後続のスレッドはブロックされます。
>
> この部分がどうにもイメージし難いのですが、どなたかもう少し噛み砕いた形で教えて
> 頂けないでしょうか?

イベントオブジェクトを使ったイベント通知は、スレッドの停止と再開で実現されています。
通知してほしい側は一旦自分のスレッドを停止して通知を待ちます。
通知する側はイベントオブジェクトを介して停止しているスレッドを再開させます。
通知してほしい側はこのスレッドの再開そのものが通知と言うわけです。

さて、イベントオブジェクトにはシグナル状態と非シグナル状態があります。非シグナル状態は、待機することができる状態です。シグナル状態では待機できません(待機=自スレッドの停止です)。
まず、非シグナル状態のイベントオブジェクトに対して WaitOne を呼び出すことで、待機に入ります。このときの WaitOne のように、何らかの契機があるまで制御が返ってこないような処理を、「スレッドをブロックする」などと言います。
待機スレッドに通知したいタイミングで、通知者はイベントオブジェクトの Set を呼び出します。イベントオブジェクトはシグナル状態になり、このとき、イベントオブジェクトで待機していたスレッドが再開されます。つまり、Set は待機スレッドに再開通知を行うメソッドです。

シグナル状態になったイベントオブジェクトはそのままでは再待機することができません。非シグナル状態に戻す必要があります。ManualReset モードの場合、Reset を明示的に呼び出すことで非シグナル状態に戻します。AutoReset モードの場合、Set でシグナル状態になった後すぐ自動的に非シグナル状態に戻ります。
例えば、とあるイベントオブジェクトに対し WaitOne で待機しているスレッドが三つあるとします。
AutoReset モードの場合、Set によってシグナル状態になり、どれか一つのスレッドに通知されますが、すぐに非シグナル状態となるため残り二つのスレッドはブロックされたままです。
ManualReset の場合、Set によってシグナル状態となると、三つのスレッド全てでシグナルを受信しブロックが解除されることになります。
■No23981に返信(Hongliangさんの記事)

Hongliangさん、非常に分かりやすい説明を書いて下さって本当にありがとうございました。
仕様をきちんと把握できたお陰で、実務にも応用できそうです。

最後に実装したコードを"回答"として挙げ、本スレッドを"解決済み"とさせて頂きます。


static class Program
{
    public static EventWaitHandle evnt = null;

    [STAThread]
    static void Main()
    {
        bool createdNew;
        evnt = new EventWaitHandle(false, EventResetMode.ManualReset, "hogehoge", out createdNew);

        if (createdNew)
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }
        else
        {
            evnt.Set();
        };
    }
}

public partial class Form1 : Form
{
    private Thread WaitThread = null;

    public Form1()
    {
        InitializeComponent();
        WaitThread = new Thread(new ThreadStart(WaitOne));
        WaitThread.IsBackground = true;
        WaitThread.Priority = ThreadPriority.Lowest;
        WaitThread.Start();
    }

    public void WaitOne()
    {
        Program.evnt.WaitOne();
        this.Invoke((MethodInvoker)delegate
        {
            this.Close();
        });
    }
}
解決済み!

DOBON.NET | プログラミング道 | プログラミング掲示板