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

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

前回までは、主にWindowsが発生させたメッセージをHSPアプリケーション側で受け取り、それに対する処理をすることを中心にやってきました。しかし実際には、アプリケーションはメッセージを受け取るだけでなく、メッセージを発生させることもできるのです。今回は、アプリケーションが明示的にメッセージを発生させる仕組みについて説明していきます。

また、メッセージを発生させることをメッセージを「送信する」または「ポストする」と呼ぶということを以前に説明しましたが、この『送信』と『ポスト』は、実際には異なります。これについての説明も、メッセージのメカニズムとともにしていきます。

メッセージのポスト

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

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

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

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

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

ウィンドウを所有しているスレッドは、ウィンドウメッセージを格納するためのメッセージキューと呼ばれるキューを持っています。通常、メッセージはいったんこのメッセージキューにためられて、スレッドがウィンドウメッセージ処理の機会を得たときに、先頭から順に処理されていきます。スレッドはウィンドウに送られてくる複数のメッセージを同時に処理することができないので、メッセージをいったんキューにためておき、先に送られてきたメッセージから順にキューから取り出してウィンドウプロシージャで処理していくのです。

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

メッセージ処理の仕組み

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

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

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

以上のことから、メッセージキューに格納されているメッセージをウィンドウプロシージャで処理するためには、アプリケーションがGetMessage関数やPeekMessage関数を呼び出さなければならないことになります。つまり、HSPではstop命令やwait命令、await命令が実行されなければ、キューのメッセージを処理する機会が与えられないということです。適切なメッセージ処理のためにも、HSPプログラム中ではこれらの命令を実行させる必要があるということに注意しなければなりません。

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

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

BOOL PostMessageA(
    HWND   hWnd,    // window handle
    UINT   Msg,     // message code
    WPARAM wParam,  // wParam parameter
    LPARAM lParam   // lParam parameter
);

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

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

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

メッセージの送信

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

メッセージ送信

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

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


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

LRESULT SendMessageA(
    HWND   hWnd,    // window handle
    UINT   Msg,     // message code
    WPARAM wParam,  // wParam parameter
    LPARAM lParam   // lParam parameter
);

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


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

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

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

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

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

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

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

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

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

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

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

送信とポストの比較

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

実際に、自分自身のウィンドウに向けてメッセージを発生させてみて、それをHSPのメッセージ処理の機能で取得するということを行います。どの段階でメッセージ処理が行われているかを調べることによって、いつウィンドウプロシージャが呼び出されているのかということを知ろうというわけです。


ここでは、自分自身のウィンドウに送るメッセージコードとしてWM_USER以降で定義できる値を使用しています。Windowsでは、アプリケーションが独自のメッセージを使いたいときに、メッセージコード0x0400から0xBFFFまでの範囲で自由にアプリケーション独自のメッセージを定義してよいことになっています。WM_USER

#define WM_USER         0x0400

という形で定義された定数名で、通常はWM_USER+XXは0以上の整数)という値を別の定数名として定義して、それを使用します。

Windows 95/98/Meでは、ダイアログ制御メッセージとの衝突を避けるためにWM_USER+0x0100(すなわち0x0500)より大きい値を定義しなければならない、となっています。現在のところ0x04000x0500の範囲のメッセージコードでうまくいかない例を聞いたことはないですが、すべての環境でうまく動作させるため、とりあえずそのような値を使用した方がよいでしょう。

PostMessageの場合

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

#uselib "user32.dll"
#func PostMessage "PostMessageA" int,int,int,int

#define  WM_USER         0x0400
#define  MYWM_TESTMSG    (WM_USER + 0x101)

; メッセージ処理の設定
oncmd gosub *OnReceiveMessage, MYWM_TESTMSG

; PostMessage 関数の呼び出し (1)
mes "メッセージ1をポストします。"
PostMessage hwnd, MYWM_TESTMSG, 1, 100
mes "メッセージ1をポストしました。"

wait 10

; PostMessage 関数の呼び出し (2)
mes "メッセージ2をポストします。"
PostMessage hwnd, MYWM_TESTMSG, 2, 200
mes "メッセージ2をポストしました。"

stop

*OnReceiveMessage
; ======== メッセージ処理 ========
mes "メッセージ処理 wParam=" + wparam + " lParam=" + lparam
return

このスクリプトを実行させると、ウィンドウには

メッセージ1をポストします。
メッセージ1をポストしました。
メッセージ処理 wParam=1 lParam=100
メッセージ2をポストします。
メッセージ2をポストしました。
メッセージ処理 wParam=2 lParam=200

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

SendMessageの場合

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

#uselib "user32.dll"
#func SendMessage "SendMessageA" int,int,int,int

#define  WM_USER         0x0400
#define  MYWM_TESTMSG    (WM_USER + 0x101)

; メッセージ処理の設定
oncmd gosub *OnReceiveMessage, MYWM_TESTMSG

; SendMessage 関数の呼び出し (1)
mes "メッセージ1を送信します。"
SendMessage hwnd, MYWM_TESTMSG, 1, 100
mes "メッセージ1を送信しました。"

wait 10

; SendMessage 関数の呼び出し (2)
mes "メッセージ2を送信します。"
SendMessage hwnd, MYWM_TESTMSG, 2, 200
mes "メッセージ2を送信しました。"

stop

*OnReceiveMessage
; ======== メッセージ処理 ========
mes "メッセージ処理 wParam=" + wparam + " lParam=" + lparam
return

このスクリプトを実行させると、ウィンドウには

メッセージ1を送信します。
メッセージ処理 wParam=1 lParam=100
メッセージ1を送信しました。
メッセージ2を送信します。
メッセージ処理 wParam=2 lParam=200
メッセージ2を送信しました。

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

メッセージ送信命令sendmsg

HSP3では、SendMessage関数を呼び出すための命令であるsendmsg命令が標準命令として実装されています。

sendmsg n1, n2, n3, n4
n1:ウィンドウハンドル
n2:メッセージコード
n3:wParamパラメータ
n4:lParamパラメータ

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

#define  WM_USER         0x0400
#define  MYWM_TESTMSG    (WM_USER + 0x101)

; メッセージ処理の設定
oncmd gosub *OnReceiveMessage, MYWM_TESTMSG

; SendMessage 関数の呼び出し
mes "メッセージを送信します。"
sendmsg hwnd, MYWM_TESTMSG, 1, 100
mes "メッセージを送信しました。"
stop

*OnReceiveMessage
; ======== メッセージ処理 ========
mes "メッセージ処理 wParam=" + wparam + " lParam=" + lparam
return