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
- ウィンドウハンドル取得タイミングメモ
-
メモリ上テキスト位置の やり取りロジック
- 【送】 / Marshal.StringToHGlobalUni() を使ってアンマネージドなメモリにテキストを突っ込む
- 【送】 /
SendMessage()
を使ってWM_SETTEXT
区分で送信
※ 対象ウィンドウハンドル検出はFindWindow()
を足がかりにした - 【受】 /
HwndSource.AddHook
で仕込んでおいた WndProc 相当のメソッドにてWM_SETTEXT
メッセージを受け取る - 【受】 / 受け取った情報から 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 をブレークポイントで止めておき、
lParam
やMarshal.PtrToStringUni()
の値を確認しているところ。以下のSSでは
lParam
の値を0x0039DfE8
として受け止めており、
この値と同じ場所をメモリアドレス上で確認すると、 それっぽい文字列値が格納されていることが確認できた ( 右上赤線 )
Microsoft Learn - Visual Studio デバッガーで [メモリ] ウィンドウを使用する