Windowメッセージでプロセス間通信をしてみよう(VC++からVC++へ)
「マルペケの徒然なるプログラムのお話」最初の記事はWindowメッセージによるプロセス間通信です。業務で必要になったのがきっかけで実装してみた所簡単に実現できたため、こちらで方法論をまとめてみる事にしました。
プロセス間通信とは?
プロセス間通信というのは実行しているアプリケーション(.exe)間でメッセージをやりとりする事を指します。これ実は色々方法があります。あり過ぎて困るので、今回はWindowメッセージに限定してみました。つまり今回の記事の技術はWindows OSのアプリ限定です。
Windowメッセージ
Windowsで何らかのアプリを起動すると、ウィンドウが出てきますよね。そのウィンドウはサイズを変更したり、ボタンをクリックするなどアクションを起こせます。そういうアクションがある度にアプリ内には「Windowメッセージ」というメッセージが発生しています。Windowsのアプリは基本その受け取ったメッセージを元に動いています。
このメッセージ、実は相手のアプリがわかれば他のアプリからも飛ばす事ができます。受け取り側でちゃんと準備しておけば、送信側のアプリでメッセージを送信する事で、受け取り側のアプリを操作する事ができるんですね。もちろん任意のデータも送れます。これが楽しいのです(^-^)
では早速方法を見て行きましょう。
受け取り側アプリ(VC++)
Windowsでアプリを作る方法も今は物凄く多義になり、解説するのも大変になってしまいました(^-^;。とりあえず、まずはオーソドックスにVisual StudioによるVC++で受け取り側のアプリを作ってみます。ただ冗長を避けるため、細かな所は端折らせて下さい。ゴメンナサイ。
それなりに最近のバージョンのVisual Studioで「Windowsデスクトップアプリケーション」のプロジェクトを立ち上げます:
このテンプレートは最低限のWindowsアプリの機能を提供してくれます。必要なのはその中にある「ウィンドウプロシージャ」というコード部分で、それがあるテンプレートなら別の物でも構いません。
プロジェクト作成後、wWinMain関数がある.cppの中にWndProc関数があるはずです。ここに内外から自分向けに飛ばされたWindowメッセージが入って来ますので、メッセージの種類を判断し、対応する振る舞いを記述します。デフォルトで既にいくつかのメッセージの振る舞いが記述されていますね:
caseにあるWM_COMMANDとかWM_PAINTがWindowメッセージです。これらはdefineされた表記で、実際は単なる整数です。今回はここに「WM_COPYDATA」というメッセージの処理を追加します。とりあえず空の受け口を追加しておきましょう:
...
case WM_COPYDATA:
break;
WM_COPYDATA
このメッセージはWindowsが用意してくれているシステムメッセージの一つで、外部から与えられた任意のデータの塊と共に飛んでくるメッセージです。任意データの塊は送信側が自由にサイズや値を決められます。その塊はWndProc関数の第4引数にあるlParamにCOPYDATASTRUCT構造体(のポインタ)として振ってきます:
struct COPYDATASTRUCT {
ULONG_PTR dwData; // 任意の値
DWORD cbData; // データサイズ
PVOID lpData; // データの塊へのポインタ
};
dwDataは送信側で自由に決められる32bitの符号無し整数値です。データの種類などを識別するのに使うのが一般的でしょうか。lpDataに送信側で設定した任意サイズのデータの塊へのポインタが渡されます。cbDataでそのデータのサイズが分かります。
受信したデータはコピーが必要
典型的な受信データの処理方法は例えば次のような感じです:
#include <string>
#define MYDATA_STRING 100
std::string receiveStr_g;
...
// WndProc内
case WM_COPYDATA:
{
COPYDATASTRUCT* cds = ( COPYDATASTRUCT* )lParam;
switch ( cds->dwData ) {
case MYDATA_STRING:
receiveStr_g = ( const char* )cds->lpData;
break;
}
InvalidateRect( 0, 0, true );
break;
}
送信側から任意の文字列が流れて来る場合の例です。
まず送られた文字列をコピーし保持するreceiveStr_gという文字列変数を用意します。送信元のデータの塊は一時的なバッファなので、WndProc関数を抜けるとすぐに消えてしまいます。そのため利用するには必ずコピーしなければいけません。
dwDataに送信側で決めた識別番号が入ってくる事を想定しMYDATA_STRINGというdefineを定義しておきます。値は任意で構いません。
WM_COPYDATAを受信したら、まずlParamをCOPYDATASTRUCT構造体のポインタにキャストします。これはそういうお約束なのでハードキャストしてしまって構いません。次にdwDataを判断し、それがMYDATA_STRINGの場合lpDataに文字列があるとし、const char*にキャストしてダイレクトにreciveStr_gに放り込みます。
InvalidateRect関数は再描画(WM_PAINT)を促す命令で、プロセス間通信とは関係ありません。次のテスト表示の為に追加しています。
受け取った文字列を画面にテスト表示
受け取った文字列はreceiveStr_gにコピーしておけば後は好きに扱えますので、ここではテスト表示するようにしてみましょう。WndProc関数内のWM_PAINTに文字列を表示するコードを追加します:
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
RECT r = { 0, 0, 500, 36 };
DrawTextA( hdc, receiveStr_g.c_str(), -1, &r, DT_LEFT );
EndPaint(hWnd, &ps);
}
break;
DrawText関数の第2引数に受け取った文字列を渡せば画面に表示されます。ただしWM_PAINTメッセージは画面をリサイズするとか何か再描画のきっかけが無いと発生しません。そのため先の所で再描画を強制するInvalidateRect関数をWM_COPYDATAメッセージを受け取った時に呼び出したというわけです。
さて、これで受け取り側の準備が出来ましたので、今度はWindowメッセージを送信するアプリを作ってみましょう。
送信側アプリ(VC++)
送信側はWindowメッセージを送信できる物であれば実は何でも構いません。今回はシンプルにVC++のコンソールアプリで作ってみます。
新規プロジェクトで「コンソールアプリ」を選択して環境を作って下さい:
起動すると超シンプルなmain関数の.cppが開いていると思いますので、ここにWM_COPYDATAメッセージを送信するスニペットコードを記述する事にしましょう。
FindWindow関数
送信するからにはWM_COPYDATAメッセージを受け取るアプリを必ず調べる必要があります。これはFindWindow関数で検索する事が可能です:
#include <Windows.h>
HWND hWnd = FindWindow( 0, L"MessageReceiver" );
第1引数にはWindowClass Name(ウィンドウクラス名)という文字列を渡します。これは特定のウィンドウの型を識別する文字列で、先の受け取り側アプリでも実は定義されているんですが、無くても検索が可能なので上では0を渡しています。
第2引数には起動中のウィンドウのキャプション名を指定します。僕は今回受け取り側を「MessageReceiver」という名前にしたため、そう指定していますが、皆さんは皆さんが作った受け取り側アプリのキャプション名を指定して下さい:
起動中の受け取り側アプリがあれば、hWndにそのウィンドウハンドル(識別ID)が返ります。無い場合は0になります。
SendMessage関数
有効なウィンドウハンドルが取れたら、SendMessage関数を使ってWM_COPYDATAメッセージをそのウィンドウに向けて送信します。今回は文字列を受け取れるように仕込んだので、適当な文字列を送信するようにしてみます:
#include <Windows.h>
#define MYDATA_STRING 100;
int main()
{
HWND hWnd = FindWindow( 0, L"MessageReceiver" );
if ( hWnd != 0 ) {
const char* msg = "Hello World!!";
COPYDATASTRUCT cds;
cds.dwData = MYDATA_STRING;
cds.lpData = (void*)msg;
cds.cbData = strlen( msg ) + 1;
SendMessage( hWnd, WM_COPYDATA, 0, (LPARAM)&cds );
}
return 0;
}
COPYDATASTRUCT構造体を作り、そのメンバを受け取り側の仕様に合わせて設定していきます。WM_COPYDATAメッセージに限って言えば、lpDataに代入する文字列バッファのポインタは一時的な物で構いません。SendMessage関数の内部でプロセス間メッセージで利用できるグローバルヒープにデータがコピーされるため、送信側でメモリを確保する必要はありません。文字列は終端文字分サイズがありますのでcbDataに渡すサイズは文字列サイズ+1です。
作ったCOPYDATASTRUCT構造体のポインタをSendMessage関数の第4引数にあるlParamに渡せば送信完了です。第3引数のwParamも実は利用可能ですが(任意の数値でOK)今回は使用していません。
送信してみよう!
さ、これで送受信がもう出来ます!簡単ですよね(^-^)
受け取り側アプリを実行し(Visual Studio上でデバッグ実行でもOK。ブレイクポイントで受信を確認できるので便利です)、送信側アプリも実行してみましょう。すると…、
送信側で指定した文字列が受け取り側で表示されました!!
もしうまく表示されない場合は、受け取り側のWM_COPYDATA内にブレークポイントを張ってみて、メッセージが飛んできているかをチェックしてみて下さい。飛んでいない場合は送信側でhWndが有効か、そしてSendMessage関数に渡している引数が正しくなっているかを確認してみて下さい。また、今回は受信側でASCIIな文字列を受け取るようにしています。送信側でUnicodeな文字列を送信すると表示がバグるのでご注意下さい。
どうでしたでしょうか?受け取り側も送信側も、少ないコードであっさりとプロセス間通信が出来てしまいましたよね。ポイントは送信側で受け取り側のウィンドウハンドルを正しく得る事(FindWindow関数)、そして受け取り側でCOPYDATASTRUCTの値をちゃんとコピーする事です。間違ってもlParamのポインタを保持して直接使用してはいけません。
今回は送受信のアプリいずれもVC++で作成しましたが、これはC# .NetでもVB .Netでも実は作れます。C# .Netを採用しているUnityでも勿論可能です(ただしWindows版のビルド及びWindows環境のUnity Editorのみ)。実際僕はUnity Editorを送信側とし、VC++で作成したアプリに受信部分を仕込んでやり取りする構成を取りました。
他の言語やUnity上での作り方も機を見て紹介できればと思います。ではまた(^-^)/