メッセージの送信とポスト

アプリケーションによるウィンドウメッセージの発生

前回までは、おもにWindowsが発生させたメッセージをアプリケーション側で受け取ることを中心にやってきましたが、実際には、アプリケーションはメッセージを受け取るだけでなく、メッセージを発生させることもできるのです。今回は、アプリケーションが明示的にメッセージを発生させる仕組みについて説明していきます。また、メッセージのメカニズムについてもやや詳しく説明しましょう。

また、前回に、メッセージを発生させることをメッセージを「送信する」または「ポストする」ということを説明しましたが、この『送信』と『ポスト』は、厳密な意味で異なります。これについての説明もしていきます。

メッセージのポスト

まずは「メッセージをポストする」という動作について説明していきましょう。

スレッドのメッセージキュー

まず、『キュー (queue)』とはいったいどういうものか知らない方のために説明しておきましょう。

『キュー』というのは、「先入れ先出し(First In First Out:FIFO)方式のデータ構造」を表すものです。このデータ構造では、先に格納された要素が先に取り出されるので、『待ち行列』とも呼ばれています。

キューでは、最初の(先に入れられた)データを先頭、最後の(後に入れられた)データを末尾と呼び、キューの末尾に新しいデータを追加することを「ポストする」と呼びます。

ウィンドウを持つスレッドは、ウィンドウメッセージを格納するためのメッセージキューを持っています。通常、メッセージはいったんこのメッセージキューにためられて、スレッドがウィンドウメッセージ処理の機会を得たときに、順に処理されていくのです。

ウィンドウを所有しているスレッドは、ウィンドウに送られてくるいくつものメッセージを同時に処理することはできないので、メッセージをいったんキューにためておいて、先に送られてきたメッセージから順にキューから取り出してウィンドウプロシージャで処理していくのです。

メッセージキューは、ウィンドウごとに準備されているのではなく、スレッドごとに準備されています。そのスレッドが所有しているウィンドウにポストされたメッセージはすべて同じメッセージキューに格納されます。HSPはシングルスレッド(スレッドを1つしか持たない)アプリケーションですから、HSPアプリケーションの1つのプロセスで作られるすべてのウィンドウ(HSPオブジェクトやコントロールなども含めて)に対して、メッセージキューは1つだけ存在することになります。

メッセージ処理の仕組み

C言語などで書かれているGUIアプリケーションプログラムは、メッセージがキューに存在するかどうかを調べるために、GetMessageまたはPeekMessageというAPI関数を呼び出します。この2つの関数は、メッセージキューにメッセージが存在する場合には、キューの先頭からメッセージの情報を取り出して、それを呼び出し元に返します。メッセージキューにメッセージが存在しない場合には、GetMessage関数は、新しいメッセージを受け取るまでスレッドを停止させ、PeekMessage関数はそのまま次の処理を続行させます。

アプリケーションは、これらの関数呼び出しで、メッセージを受け取ったことを知ると、そのメッセージ情報を、ウィンドウプロシージャに渡すためのAPI関数(DispatchMessage関数)を呼び出します。これによって、メッセージはウィンドウプロシージャに送られ、メッセージ処理がされるのです。

HSP内部でも、私たちからは見えないところでこれらの関数が使われています。スクリプト中でawait命令が実行されるとき、内部ではPeekMessage関数によるメッセージの取得が行なわれています。また、stop命令やwait命令が実行されたときは内部でメッセージではGetMessage関数が呼び出されているのです。

したがって、メッセージキューに格納されているメッセージをウィンドウプロシージャで処理するためには、アプリケーションがGetMessage関数やPeekMessage関数を呼び出さなければならないことになります。つまり、HSPではstop命令やwait命令、await命令が実行されなければ、キューのメッセージを処理する機会が与えられないということです。

wait命令を実行すると、HSP内部ではGetMessage関数を呼び出す前に、ユーザータイマーと呼ばれるタイマーをセットします。このタイマーは、指定された時刻が経過するとウィンドウメッセージを発生させます。タイマーをセットした後にGetMessage関数を呼び出すことで、メッセージがあればメッセージ処理を、なければスレッドをいったん停止させます。その後、設定されていた時間が経過するとウィンドウにタイマーメッセージが送られ、スレッドが再び動き出して、処理が続行されるようになっているのです。

await命令は精密な時間処理を必要とするため、決められた時刻になるまで繰り返しPeekMessage関数を呼び出し続けるという処理が行なわれます。GetMessage関数とは異なり、スレッドは休止状態になることがないため、await命令によるループを作ると、CPUの空き時間をすべて奪って、CPU使用率100%という現象が起こってしまうのです。

ただし、HSP ver2.6ではawait命令の仕様が変更され、第2パラメータの値に応じてSleep関数によるスレッドの停止を行なうようになっています。

PostMessage関数によるメッセージのポスト

メッセージのポストというのは、スレッドのメッセージキューにメッセージをポストする、ということです。アプリケーションから明示的にメッセージをポストするには、PostMessage関数を呼び出します。

BOOL PostMessageA(
    HWND   hWnd,    // ウィンドウハンドル
    UINT   Msg,     // メッセージコード
    WPARAM wParam,  // wParamパラメータ
    LPARAM lParam   // lParamパラメータ
);

この関数を呼び出すと、引数で指定されたメッセージ情報が、メッセージキューにポストされます。そして、メッセージ処理の機会が与えられた時点で(GetMessage関数やPeekMessage関数が呼び出されたときに)、ウィンドウプロシージャに送られてメッセージ処理がなされます。

次の図は、PostMessage関数によりポストされたメッセージの処理のメカニズムを表すイメージ図です。

ポストされたメッセージはキューの最後に追加されます。メッセージ処理の機会が与えられると、キューの先頭にあるメッセージから順番に処理されていく様子がわかります。

メッセージの送信

次に、「メッセージを送信する(送る)」といったときの動作について説明しましょう。

メッセージ送信

「メッセージのポスト」は、通常、ウィンドウに何らかの通知をしたい場合のみに用いるものです。この場合、メッセージをポストしたら、そのメッセージに対する処理が行なわれたかどうかに関わらず、次の処理を続行します。

ところが、「メッセージの送信」は、ウィンドウに今すぐに何かをさせたいときや、ウィンドウに何かを要求して、すぐにその結果を知りたいときに使われます。メッセージを送信すると、そのメッセージが処理されるまで次の動作には移りません。対象となるウィンドウによるメッセージ処理がなされて、その結果がわかって初めて、次の処理を行なうのです。


ウィンドウにメッセージを送信するには、SendMessage関数を呼び出します

LRESULT SendMessageA(
    HWND   hWnd,    // ウィンドウハンドル
    UINT   Msg,     // メッセージコード
    WPARAM wParam,  // wParamパラメータ
    LPARAM lParam   // lParamパラメータ
);

それぞれのパラメータはPostMessage関数と変わりません。これらのパラメータはそのままウィンドウプロシージャに渡されることになります。


さて、SendMessage関数のメカニズムは、対象となるウィンドウを所有しているスレッドが、呼び出しスレッドと同じであるかどうかによって異なってきます。次はこれについて説明します。

SendMessage関数による同スレッド内のメッセージ送信

まずは、呼び出し側スレッドとが所有しているウィンドウに対してSendMessage関数を呼び出したときの動作について説明しましょう。

同じスレッドのウィンドウに対してSendMessage関数を呼び出すと、SendMessage関数は内部で直接ウィンドウプロシージャを呼び出します。これは、ちょうど呼び出し側コードから直接ウィンドウプロシージャを呼び出すのと同じになると考えることができます。

上の図にある通り、SendMessage関数に渡した引数はそのままウィンドウプロシージャに渡され、ウィンドウプロシージャが返した戻り値はそのままSendMessage関数の戻り値として呼び出し側に返されることになります。

SendMessage関数による異なるスレッド間のメッセージ送信

呼び出し側スレッドとは別のスレッドが所有しているウィンドウに対してSendMessage関数を呼び出した場合の動作を説明しましょう。これは、例えば同じアプリケーションの別のプロセスのウィンドウや、別のアプリケーションのウィンドウに対してメッセージを送信した場合に起こります。

Windowsでは、「ウィンドウプロシージャは、常にそのウィンドウを所有しているスレッドによって処理されるべき」という考えに基づいて、ウィンドウメッセージ処理を行なっています。したがって、所有スレッド以外のスレッドがウィンドウプロシージャの処理をしなくても済むように、次のようなメカニズムが働いているのです。

呼び出し側のスレッドでSendMessage関数が呼び出されると、送信先のウィンドウを所有しているスレッドのメッセージキューにいったんメッセージが格納されます。すると、その時点で呼び出し側のスレッドが休止状態となります。この休止状態では、このスレッドにCPU時間が割り当てられることはないので、他のスレッドに影響することはありません。

メッセージを受け取った方のスレッドは、その時点ではまだ別の処理をしているかもしれません。この場合、その処理が終わるまで待つことになります。受け取り側がメッセージを処理することができるようになったとき、すなわち、GetMessage関数やPeekMessage関数が呼び出されたときに、送信されたメッセージに対しての処理がウィンドウプロシージャで行なわれます。このとき、送信されたメッセージは、他のPostMessage関数などでポストされたメッセージよりも優先的に処理されることになります。(このメッセージ処理を待っているスレッド(呼び出し側)を止めてしまっているのですから、このメッセージを優先的に処理するのは当然ですね。)

ウィンドウプロシージャでの処理が終わって戻り値が返されると、休止状態にあった呼び出し側スレッドは再び動き出します。そして、受け取った戻り値をSendMessage関数の戻り値として返すのです。

送信とポストの比較

さて、SendMessage関数とPostMessage関数の違いを理解するのに、1つサンプルを作成し、実行してみましょう。

実際に、自分自身のウィンドウに向けてメッセージを発生させてみて、それをhsgetmsg.dllの機能で取得するということを行ないます。

hsgetmsg.dllでは、サブクラス化によって、ウィンドウプロシージャのなかでメッセージの保存を行ない、後で呼び出されるメッセージ取得命令によってその情報を取得できると説明しました。そこで、いつメッセージが保存されたかを調べることによって、いつウィンドウプロシージャが呼び出されているのかということを知ろうというわけです。

スクリプト中で行なうことは以下のことです。

  1. メッセージを発生させる(SendMessageまたはPostMessage
  2. メッセージが送られているか調べる
  3. wait(またはawait)を実行する
  4. メッセージが送られているか調べる

ここでなぜwait命令やawait命令を実行しなければならないのかについては、先ほどの説明からも分かることでしょう。これらの命令では、GetMessage関数やPeekMessage関数が呼び出され、キューに格納されているメッセージを処理する機会が与えられるからです。

SendMessageの場合

まずはSendMessage関数の場合を調べてみましょう。

    #include "llmod.as"
    #include "hsgetmsg.as"

    ; 取得するメッセージの設定
    set_subclass : hwnd = stat       ; ウィンドウハンドル
    set_message $400                 ; WM_USER

    ; SendMessage 関数の呼び出し
    mes "メッセージを送信します"
    pm = hwnd, $400, 0, 0
    dllproc "SendMessageA", pm, 4, D_USER

    ; wait 前のメッセージの取得
    get_message
    if (msgval != 0) & (msgval.1 == $400) {
        mes "wait 実行前にメッセージを取得しました"
    }

    ; wait 実行(GetMessage 関数の呼び出し)
    mes "wait を実行"
    wait 10

    ; wait 後のメッセージの取得
    get_message
    if (msgval != 0) & (msgval.1 == $400) {
        mes "wait 実行後にメッセージを取得しました"
    }

    stop

このスクリプトを実行させて分かることは、SendMessage関数を使った場合では、wait命令を実行する前に、すでにメッセージが取得されてしまっているということです。つまり、SendMessage関数を呼び出した瞬間、ウィンドウプロシージャでの処理が行なわれたということです。

PostMessageの場合

次にPostMessage関数の場合を調べてみます。上のスクリプトで、呼び出す関数をPostMessageに変えただけのものです。

    #include "llmod.as"
    #include "hsgetmsg.as"

    ; 取得するメッセージの設定
    set_subclass : hwnd = stat       ; ウィンドウハンドル
    set_message $400                 ; WM_USER

    ; PostMessage 関数の呼び出し
    mes "メッセージをポストします"
    pm = hwnd, $400, 0, 0
    dllproc "PostMessageA", pm, 4, D_USER

    ; wait 前のメッセージの取得
    get_message
    if (msgval != 0) & (msgval.1 == $400) {
        mes "wait 実行前にメッセージを取得しました"
    }

    ; wait 実行(GetMessage 関数の呼び出し)
    mes "wait を実行"
    wait 10

    ; wait 後のメッセージの取得
    get_message
    if (msgval != 0) & (msgval.1 == $400) {
        mes "wait 実行後にメッセージを取得しました"
    }

    stop

このスクリプトを実行させて分かることは、PostMessage関数を使った場合では、wait命令を実行した後ににならないとメッセージが取得されないということです。つまり、wait命令を実行したときにGetMessage関数によってメッセージ処理の機会が与えられ、メッセージキューにポストされていたメッセージが取り出されて、ウィンドウプロシージャでの処理が行なわれたのです。

llmodモジュール定義命令sendmsg

llmodモジュールでは、SendMessage関数を呼び出すためのモジュール命令であるsendmsg命令があらかじめ定義されています。

sendmsg v1
v1 : メッセージパラメータを格納した数値型配列変数
v1.0:ウィンドウハンドル
v1.1:メッセージコード
v1.2wParamパラメータ
v1.3lParamパラメータ

この命令を実行すると、SendMessage関数が呼び出されます。関数の戻り値は、グローバル変数dllretとシステム変数statの両方に格納されます。