プロフィール

髭山髭人(ひげひと)

自分の書いた記事が、一人でも誰かの役に立てば...
活動信条の一つとして「貴方のメモは、誰かのヒント」というのがあります。

このサイトについて

本家HP packetroom.net から切り離した いわゆる技術メモ用のブログで、無料レンタルサーバーにて運用しています。広告表示はその義務なのでご容赦。
XREA さんには長年お世話になっています

C# WPF にて WM_SETTEXT の SendMessage で文字列受け渡し

概要とか

  • 環境とか
    C# / WPF / .NET 8.0

Win32 API SendMessage() で、シンプルなテキスト(文字列)を SendAppからRecvAppへ投げる

ざっくり内容

  • アプリ間で示し合わせておくもの

    • 受信側ウィンドウタイトル
      本例では "RecvWindow"
    • メッセージID
      本例では WM_SETTEXT (0x000C) で送信
  • WndProc 云々
    WPF だと、WindowsFormApplication 世代に存在した WndProc の override 手法が使えないとの事。
    なので、WPF用の処理とされている ↓こちらをベースとした
    Qiita - [WPF] ウインドウメッセージハンドラをフックする

    • ウィンドウハンドル取得タイミングメモ
      早い段階でフック処理を仕込みたい時は WindowInteropHelper.Handle より
      WindowInteropHelper.EnsureHandle() のほうが個人的にうれしかったので採用
      Microsoft Learn - WindowInteropHelper.EnsureHandle
  • メモリ上テキスト位置の やり取りロジック

    1. 【送】 / Marshal.StringToHGlobalUni() を使ってアンマネージドなメモリにテキストを突っ込む
    2. 【送】 / SendMessage() を使って WM_SETTEXT 区分で送信
      ※ 対象ウィンドウハンドル検出は FindWindow() を足がかりにした
    3. 【受】 / HwndSource.AddHook で仕込んでおいた WndProc 相当のメソッドにて WM_SETTEXT メッセージを受け取る
    4. 【受】 / 受け取った情報から Marshal.PtrToStringUni() を使ってテキスト回収
  • ログ表示機能
    本筋とは関係ないが、フォーム上にログを出したかったので xaml と連携するロジックをコード内に加えている

送信側 SendApp プロジェクト

TextBox に入力した文字列を Button クリックで SendMessage する。
まずはコードビハインド側から。

namespace SendApp
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            this.Title = "SendWindow";
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            TextBox textBox = (TextBox)FindName("TextBox01");
            // "RecvWindow" のウィンドウタイトルに向けて送るようにした (雑)
            MyWin32API.SendWindowMessage("RecvWindow", textBox.Text );
        }
    }
    public static class MyWin32API
    {
        [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        public static extern IntPtr FindWindow(string? lpClassName, string lpWindowName);

        [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Unicode)]
        public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, int wParam, IntPtr lParam);

        private const int WM_SETTEXT = 0x000C;
        public static void SendWindowMessage(string _targetWindowName , string _msgStr)
        {
            IntPtr _hWnd = FindWindow(null, _targetWindowName);
            if (_hWnd == IntPtr.Zero)
            {
                return;
            }
            IntPtr _shareAddr = Marshal.StringToHGlobalUni(_msgStr);
            int _wParam = 0;
            Log.Add($"(0x{_shareAddr.ToString("X")}) : {_msgStr}");
            var _result = SendMessage(_hWnd, WM_SETTEXT, _wParam, _shareAddr);
            Marshal.FreeHGlobal(_shareAddr);
        }
    }
    // ただのログ表示用
    public static class Log
    {
        private static ObservableCollection<string> _logCollection = new ObservableCollection<string>();
        public static ObservableCollection<string> LogCollection { get { return _logCollection; } }
        public static void Add(string _text) { LogCollection.Add(_text); }
    }
}

下記は xaml側 (抜粋)
上記の Log クラスと連携し、フォーム上でログ表示が行われる。
あと入力欄と送信ボタン的なヤツ

    <Grid>
        <DockPanel LastChildFill="True">
            <StackPanel DockPanel.Dock="Top">
                <TextBox Text="DefaultText" x:Name="TextBox01"></TextBox>
                <Button Click="Button_Click">送信</Button>
            </StackPanel>
            <ScrollViewer CanContentScroll="True">
                <ItemsControl ItemsSource="{Binding Path=(local:Log.LogCollection)}"/>
            </ScrollViewer>
        </DockPanel>
    </Grid>

受信側 RecvApp プロジェクト

コードビハインド & xaml 抜粋を掲載
送信側と似た構成です。
( Log クラスと連携し、フォーム上でログを表示する機能付き )


namespace RecvApp
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            // 送信側は、このウィンドウタイトルで探しに来る ( InitializeComponent() 後に指定 )
            this.Title = "RecvWindow";
            var hWnd = new WindowInteropHelper(this).EnsureHandle();
            var _source = HwndSource.FromHwnd(hWnd);
            _source.AddHook(new HwndSourceHook(MyWin32API.WndProc));
        }
    }

    public static class MyWin32API
    {
        private const int WM_SETTEXT = 0x000C;
        public static IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam , ref bool handled)
        {
            if (msg == WM_SETTEXT)
            {
                if (lParam != IntPtr.Zero)
                {
                    object? _recvObj = Marshal.PtrToStringUni(lParam);
                    if(_recvObj != null)
                    {
                        string _recvStr = (string)_recvObj;
                        Log.Add($"(0x{lParam.ToString("X")}) : {_recvStr}");
                    }
                }
                handled = true; // true にして正規処理を蹴る
                // なんとなく 1 を返して、正常にメッセージ処理が行われたことにする
                return new IntPtr(1);
            }
            return IntPtr.Zero;
        }
    }
    // ただのログ表示用
    public static class Log
    {
        private static ObservableCollection<string> _logCollection = new ObservableCollection<string>();
        public static ObservableCollection<string> LogCollection { get { return _logCollection; } }
        public static void Add(string _text) { LogCollection.Add(_text); }
    }
}
    <Grid>
        <DockPanel LastChildFill="True">
            <ScrollViewer CanContentScroll="True">
                <ItemsControl ItemsSource="{Binding Path=(local:Log.LogCollection)}"/>
            </ScrollViewer>
        </DockPanel>
    </Grid>
  • ちょいメモ
    WndProc 内で以下のようにしている
    handled = true;
    return new IntPtr(1);

    本来 WM_SETTEXT はウィンドウタイトルやテキストを扱うコントロール系の値を書き換えるものらしいので、
    ここで handled = true をしておかないと、アプリのウィンドウタイトルが書き変わってしまう。
    また、「相手側で処理が成功しましたよ」と成功を送信側へアピールするために、
    成功扱い1 をなんとなくで返すようにした。

動作スクショ

  • 双方でアドレスが異なってるんだけども
    どうも SendMessage → WndProc の双方で (送った|受け取った) lParam 値が異なる模様。
    「アドレス相当の値が相互で同じになる」と、なぜか自分は思い込んでたのですが、
    開発者側がざっくり確認できる値としては、受信側で都合よく管理・変換された値になっているっぽい?

    ちなみに送信側はボタンを押すたびにアドレスが露骨に変わっていた

  • スクショ内容の簡易解説
    SendApp 側 exe から何度か文字列を送信したのち、
    "ABCDEFGHIKJLMN" 文字列を送ってる(受信している)さなかに受信側 RecvApp をブレークポイントで止めておき、
    lParamMarshal.PtrToStringUni() の値を確認しているところ。

    以下のSSでは lParam の値を 0x0039DfE8 として受け止めており、
    この値と同じ場所をメモリアドレス上で確認すると、 それっぽい文字列値が格納されていることが確認できた ( 右上赤線 )
    Microsoft Learn - Visual Studio デバッガーで [メモリ] ウィンドウを使用する