Windowメッセージでプロセス間通信をしてみよう(C#からC#へ)

 前章では送信側VC++と受信側VC++間でWindowメッセージによるプロセス間通信をしてみました:

 今回はそのC#.Net版です。

受信側アプリの作成(C#)

 Visual StudioからC#の「Windowsフォームアプリケーション」のプロジェクトを新規に作ります:

空のフォームができるので、受け取ったメッセージを表示するTextBoxを一つ配置しましょう。またメインウィンドウのキャプションを「MessageReceiverCS」とリネームしておきます:

TextBoxのNameは任意ですが、ここではReceiveMessageという名前にしておきます。

 C++版の時は受信側はウィンドウプロシージャ(WndProc関数)に飛んでくるWM_COPYDATAメッセージを処理したのでした。C#の場合、実はフォームがWndProc関数を仮想メソッドとして持っています。よって作成されているForm1クラスのWndProcメソッドをoverrideすると好きな処理を書く事が出来ます。WM_COPYDATAを受け取る窓口を実際に書いてみましょう:

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

    // WM_COPYDATAのメッセージID
    const int WM_COPYDATA = 0x004A;

    protected override void WndProc( ref Message m ) {
        base.WndProc( ref m );

        switch( m.Msg ) {
            case WM_COPYDATA:
                break;
        }
    }
}

 まず今回受け取るWM_COPYDATAメッセージはC#のシステムには用意されていないので、上のようにその値を定義してあげます。メッセージのIDは0x004Aと決まっているので、その値を代入しておきます。

 次にFormクラスが持っているWndProcをoverrideします。引数はMessage構造体です。今時のVisual Studioであれば自動スニペットしてくれるんじゃないでしょうか。フォームはデフォルトのメッセージも沢山処理しますので、最初にbase.WndProc()を呼んでその処理を促す必要があります。WM_COPYDATAを処理するのはその後です。

COPYDATASTRUCT構造体へキャスト

 Message構造体にはWindowメッセージの内容が一塊で降りてきます。WM_COPYDATAの場合LParamにCOPYDATASTRUCT構造体へのポインタ(IntPtr)が格納されています。まずはこれをCOPYDATASTRUCT構造体にキャストします。と言ってもCOPYDATASTRUCT構造体は用意されていないので自前で用意します:

using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Explicit)]
struct COPYDATASTRUCT32 {
    [FieldOffset( 0 )] public UInt32 dwData;
    [FieldOffset( 4 )] public UInt32 cbData;
    [FieldOffset( 8 )] public IntPtr lpData;
}

 一般にC#の構造体は変数の並びやサイズが保障されていません。C#側の都合の良いように並び替えやパディングが入ってしまうんですね。一方でWM_COPYDATAで振ってくるC++なCOPYDATASTRUCT構造体はサイズが厳密に定められています。その整合性を合わせる為、上のように[StructLayout(LayoutKind.Explicit)]という属性を付記します。これは変数の並びやサイズを厳密に定義しますよ~という意味になっています。
 各メンバには[FieldOffset( 0 )]でメモリ配置位置を指定します。括弧内の数字は構造体の先頭からのバイト数です。

 ここで大切な要点を。C++なCOPYDATASTRUCT構造体は実はアプリケーションがx86用(32bit)かx64用(64bit)かで第1メンバのdwDataのサイズが異なります。今回は32ビット版を想定しdwDataはUInt32、つまり4バイト(32bit)としました。送信側が64bitアプリの場合、ここをUInt64にする必要があります。

 上の構造体を定義したら、Marshal.PtrToStructureメソッドを通す事でCOPYDATASTRUCT構造体の値を得る事が出来ます:

protected override void WndProc( ref Message m ) {
    base.WndProc( ref m );

    switch( m.Msg ) {
        case WM_COPYDATA:
            // LParam -> COPYDATASTRUCT32に変換
            var cds = (COPYDATASTRUCT32)Marshal.PtrToStructure( m.LParam, typeof( COPYDATASTRUCT32 ) );
            break;
    }
}

m.LParamにはC++なCOPYDATASTRUCT構造体の塊がバイト配列として存在しています。Marshal.PtrToStructureメソッドはその名の通り、IntPtrの先にあるバイト配列を指定の構造体にキャストして格納してくれます。

バイト配列を文字列に

 C++版では任意の文字列を送受信してみました。C#版でも同じ仕様のメッセージを受け取れるようにします。上で定義したCOPYDATASTRUCT32構造体のlpData(IntPtr型)に文字列(ASCII)がバイトデータとして格納されているはずなので、これをC#のstringに変換します:

string message = Marshal.PtrToStringAnsi( cds.lpData );

IntPtrが指す先がC++のASCIIな文字列(終端文字で終わっている文字列)である事が分かっている場合、Marshal.PtrToStringAnsiメソッドで一発でstring型に変換できます。便利(^-^)

 では受け取った文字列メッセージをフォーム内に表示するコードを作成しテストしてみましょう:

using System.Runtime.InteropServices;

...

protected override void WndProc( ref Message m ) {
    base.WndProc( ref m );

    switch( m.Msg ) {
        case WM_COPYDATA:
            // LParam -> COPYDATASTRUCT32に変換
            var cds = (COPYDATASTRUCT32)Marshal.PtrToStructure( m.LParam, typeof( COPYDATASTRUCT32 ) );
            var message = Marshal.PtrToStringAnsi( cds.lpData );
            ReceiveMessage.Text = message;
            break;
    }
}

 前回作成したC++版送信アプリでC#版の受信アプリのウィンドウハンドルを捕まえ(L"MessageRecieverCS"のウィンドウを検索)、メッセージを送信してみると…

はい、しっかりと文字列のメッセージを受信できました。

送信側アプリの作成(C#)

 さて次はWindowメッセージを送信するアプリをC#で作ってみます。C#にはWindowメッセージを送信する方法が提供されていないため、Win32 APIを直接叩きます。つまりC++の実装と大して変わりません。

 送信用のプロジェクトを新規に作成しましょう:

 送信するメッセージを書き込むTextBox(SendMessage)と、送信を実行するButton(SendBtn)を配置します:

上のSendボタンをダブルクリックしてボタン押し下げのハンドラを記述する個所を自動生成してもらったら、準備完了です。

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

    private void SendBtn_Click( object sender, EventArgs e ) {
        // ここでメッセージを送信する
    }

受信側Windowを検索する

 受信するウィンドウを検索する方法はいくつかあります。ここではProcessクラスの機能を利用して起動中のウィンドウを列挙し、そこから指定のキャプション名を持ったウィンドウを見つける方法を取ってみます。

 起動中アプリのウィンドウを列挙するにはProcess.GetProcessesメソッドを使います。実際にコードにするとこんな感じです:

private void SendBtn_Click( object sender, EventArgs e ) {
    // 指定のウィンドウを検索
    var windowHandle = new IntPtr( 0 );
    foreach ( var process in Process.GetProcesses() ) {
        if ( process.MainWindowTitle == "MessageReceiverCS" ) {
            windowHandle = process.MainWindowHandle;
            break;
        }
    }
    if ( ((int)windowHandle) != 0 ) {
        // メッセージ送信
    }
}

 Process.GetProcesses()と呼び出すと現在稼働中のプロセス(Process型)が列挙されてくるので、それをforeachでイテレーションします。各processからそのメインウィンドウのタイトル名をMainWindowTitleプロパティで得る事が出来ますので、受信側のタイトル名をそれで調べます。一致する物があったら、MainWindowHandleプロパティでそのウィンドウのウィンドウハンドル(IntPtr型)を得る事が出来ます。

COPYDATASTRUCT構造体を作成

 送信データの元であるCOPYDATASTRUCT構造体を次に作成します。これは受信側で定義した物をそのまま流用します。

 送信するのは任意の長さの文字列です。C#の文字列はstring型ですが、これをそのまま送信する事は出来ません。COPYDATASTRUCT.lpDataはIntPtr型なのでstring内の文字列をバイト配列にキャストする必要があります:

if ( ((int)windowHandle) != 0 ) {
    // 送信データを作成
    string message = SendMessage.Text;

    var cds = new COPYDATASTRUCT32();
    cds.dwData = 0;     // 任意の数値
    cds.lpData = Marshal.StringToHGlobalAnsi( message );  // 文字列をキャスト
    cds.cbData = (uint)message.Length + 1;

    Marshal.FreeHGlobal( cds.lpData );  // メモリを解放
}

 文字列をASCIIな文字列としてバイト配列化する方法はいくつかありますが、上ではMarshal.StringToHGlobalAnsiメソッドを通しています。これはマネージドなstring型の文字列をアンマネージドな(C++で扱える)バイト配列に変換してくれます。その時についでにAnciな文字列にも変えてくれます。戻り値はバイト配列のIntPtr型なので、これをCOPYDATASTRUCT.lpDataに指定すればOKです。cbDataにデータサイズ(文字数+1)を渡すのもお忘れなく。
 Marshalで確保したメモリは使用後にFreeHGlobalメソッドで開放しないとリークしてしまうのでご注意ください。

SendMessage APIを呼んでメッセージを送信

 COPYDATASTRUCT構造体を作成したら、後はWin32 APIであるSendMessage関数を呼ぶだけです。これはシステム内のUser32.dllに定義してあるSendMessage関数をダイレクトに利用します。

 C#でDLLを使うには宣言を記述する必要があります。今回だとこんな感じです:

[DllImport( "User32.dll", EntryPoint = "SendMessage" )]
static extern Int32 sendMessage( Int32 hWnd, Int32 Msg, Int32 wParam, ref COPYDATASTRUCT32 lParam );

DllImportの第1引数にDLLの名前を直指定し、EntryPointにDLL内で公開されている関数名を指定します。

 これを記述すればDLL内の関数を呼び出せます。実際にコールしてメッセージを飛ばしてみましょう:

if ( ((int)windowHandle) != 0 ) {
    // 送信データを作成
    string message = SendMessage.Text;

    var cds = new COPYDATASTRUCT32();
    cds.dwData = 0;     // 任意の数値
    cds.lpData = Marshal.StringToHGlobalAnsi( message ); ;
    cds.cbData = (uint)message.Length + 1;

    // メッセージを送信
    sendMessage( (int)windowHandle, WM_COPYDATA, 0, ref cds );

    Marshal.FreeHGlobal( cds.lpData );  // メモリを解放
}

 先に作成した受信側アプリを立ち上げた状態で、送信側アプリを実行し、TextBoxに適当な文字列をいれて送信してみると…、

送受信成功です!左側が送信側アプリ、右側が受信側アプリで、同じ文字列が無事表示できています!

 送信側アプリの送信ボタン押し下げ時のコード全体を掲載しておきますね:

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

    // WM_COPYDATAのメッセージID
    const int WM_COPYDATA = 0x004A;

    [StructLayout( LayoutKind.Explicit )]
    struct COPYDATASTRUCT32 {
        [FieldOffset( 0 )] public UInt32 dwData;
        [FieldOffset( 4 )] public UInt32 cbData;
        [FieldOffset( 8 )] public IntPtr lpData;
    }

    [DllImport( "User32.dll", EntryPoint = "SendMessage" )]
    static extern Int32 sendMessage( Int32 hWnd, Int32 Msg, Int32 wParam, ref COPYDATASTRUCT32 lParam );

    private void SendBtn_Click( object sender, EventArgs e ) {
        // 指定のウィンドウを検索
        var windowHandle = new IntPtr( 0 );
        foreach ( var process in Process.GetProcesses() ) {
            if ( process.MainWindowTitle == "MessageReceiverCS" ) {
                windowHandle = process.MainWindowHandle;
                break;
            }
        }
        if ( ((int)windowHandle) != 0 ) {
            // 送信データを作成
            string message = SendMessage.Text;

            var cds = new COPYDATASTRUCT32();
            cds.dwData = 0;     // 任意の数値
            cds.lpData = Marshal.StringToHGlobalAnsi( message ); ;
            cds.cbData = (uint)message.Length + 1;

            sendMessage( (int)windowHandle, WM_COPYDATA, 0, ref cds );

            Marshal.FreeHGlobal( cds.lpData );  // メモリを解放
        }
    }
}


 C#版のWindowメッセージ送受信アプリができました。説明は少し冗長でしたが(^-^;、実際のコード量はとても少ないですよね。これでメッセージをやり取りできるのですからお得です。

 これでC++版、C#版の2つの言語で実現でき、それぞれでメッセージをやり取りする事も可能になりました。C#.NetはUnityでも採用されているのでUnityからメッセージを送信する事も勿論できます。ただ上の方法でUnityでウィンドウメッセージを受信する事は出来ません。理由はUnityはFormクラスを持たないからです。んじゃどうするかですが、すみません、それについて僕もまだ調べがついていません(^-^;;。分かり次第また紹介できればなと思います。

 ではまた(^-^)/

いいなと思ったら応援しよう!