Win32(MFCも含む)用のNativeStandardControlsに3年ぶりくらいに機能追加です。
メニューのユーティリティを追加しました。
NativeMenuItem
こんな感じで使います。
[TestMethod]
public void SampleWindowMenu()
{
var app = new WindowsAppFriend(Process.Start(TargetPath.NativeControls));
var main = WindowControl.FromZTop(app);
var b0 = NativeMenuItem.GetMenuItem(main, "B0");
b0.Click();
var b01 = NativeMenuItem.GetPopupMenuItem(app, "B0-1");
b01.Click();
var b011 = NativeMenuItem.GetPopupMenuItem(app, "B0-1-2");
b011.Click();
}
[TestMethod]
public void SampleContextMenu()
{
var app = new WindowsAppFriend(Process.Start(TargetPath.NativeControls));
var main = WindowControl.FromZTop(app);
main.Click(MouseButtonType.Right, 100, 100);
var a00 = NativeMenuItem.GetPopupMenuItem(app, "A0-0");
a00.Click();
}
有効無効とIdも取得できます。WM_COMMANDを使ってメッセージを送ることも可能です。
[TestMethod]
public void SampleSendMessage()
{
var app = new WindowsAppFriend(Process.Start(TargetPath.NativeControls));
var main = WindowControl.FromZTop(app);
main.Click(MouseButtonType.Right, 5, 5);
var p0 = NativeMenuItem.GetPopupMenuItem(app, "A0-0");
if (p0.Enabled)
{
const int WM_COMMAND = 0x111;
main.SendMessage(WM_COMMAND, new IntPtr(p0.Id), IntPtr.Zero);
}
}
ちなみにFriendlyの提供するSendMessageは特殊で、対象のスレッドでSendMessageを実行させるという方式になっています。別スレッド(プロセス)からSendMessageすると想定外のタイミング(対象スレッドが特定のAPI使っている最中に割り込むとか)で割り込んでトラブルが発生する場合があるからです。ダイアログが出てくる場合などはSendMessageなのに非同期で実行できます。これは対象のプロセスにSendMessageを実行させる箇所を非同期にしています。その処理が完了することはasyncオブジェクトで監視することができます。
var async = new Async();
main.SendMessage(WM_COMMAND, new IntPtr(p0.Id), IntPtr.Zero, async);
var dlg = main.WaitForNextModal();
dlg.Close();
async.WaitForCompletion();
必要?
そうなんですよ。テスト自動化用なんでIDも送信先のウィンドウもわかってるんです。実行するにはなくても良いのです。
なんで作った?
SendMessageでWM_COMMANDを送るのは確実に動作して良いのですが、逆に言えばどんな時でも実行できてしまうというのもあります。メニューが存在してなかったり、無効だったりした場合でも実行できてしまうのです。実はネイティブアプリの場合は、ここは手動でやってもらったり、別口をDLL公開関数で作ってもらったりで対応してました。あと、この方針はキーマウスエミュレートと組み合わせて使うので去年まではあんまり推奨してなかったのですよね。でも、去年キーマウスエミュレートでも同期のとれる方法を思いついて、ちょくちょく使ってるのでこちらも頑張ってみるかという流れです。
おまけ
さっきのSendMessageはこんな感じで書きたいですよね。
p0.Execute();
それでこれをやるためにはメニューハンドルから送信先のウィンドウハンドルを引っ張ってくる必要があるのですが、(もちろん p0.Execute(main) とか引数付けたらいいんですけど、それはイマイチなのでやりたくない)でもちょっと調べた感じではメニューハンドルから送信先のウィンドウハンドルを逆引きするAPIはないようです。
それでも、@さんに聞いたところフックしてWM_INITMENUPOPUPを見張ればいいんじゃない?とのご意見をいただいたのでやってみました。(その他ご意見いただいた@さん、@さん、@さんもありがとうございます!)
Friendlyを使うとフックもこんなに簡単に書けちゃうんですよー。
[TestMethod]
public void GetLastPopupOwnerSample()
{
app.LoadAssembly(GetType().Assembly);
var hooker = app.Type<Hooker>()();
main.Click(MouseButtonType.Right, 5, 5);
IntPtr sendWnd = hooker.LastPopupOwner;
}
public class Hooker
{
const int WM_INITMENUPOPUP = 0x0117;
const int WH_CALLWNDPROC = 4;
delegate IntPtr HookProc(int code, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll")]
static extern IntPtr SetWindowsHookEx(int hookType, HookProc lpfn, IntPtr hMod, int dwThreadId);
[DllImport("user32.dll")]
static extern IntPtr CallNextHookEx(IntPtr hookHandle, int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("kernel32.dll")]
static extern int GetCurrentThreadId();
[StructLayout(LayoutKind.Sequential)]
struct CWPSTRUCT
{
public IntPtr wparam;
public IntPtr lparam;
public int message;
public IntPtr hwnd;
}
HookProc _traceProc;
IntPtr _idHook;
public IntPtr LastPopupOwner { get; set; }
public Hooker()
{
var threadId = GetCurrentThreadId();
_traceProc = WindowProcHook;
_idHook = SetWindowsHookEx(WH_CALLWNDPROC, _traceProc, IntPtr.Zero, threadId);
}
~Hooker() => GC.KeepAlive(_traceProc);
IntPtr WindowProcHook(int hookCode, IntPtr wParam, IntPtr lParam)
{
if (hookCode < 0)
{
return CallNextHookEx(_idHook, hookCode, wParam, lParam);
}
var msg = (CWPSTRUCT)Marshal.PtrToStructure(lParam, typeof(CWPSTRUCT));
if (msg.message == WM_INITMENUPOPUP)
{
LastPopupOwner = msg.hwnd;
}
return CallNextHookEx(_idHook, hookCode, wParam, lParam);
}
}
で取ることには成功したのですが、フック開始をユーザーに明示的にやらせるのかとか、あとやりたいことの割に仕掛けが大げさなので一旦ライブラリにはいれないことにしました。(やりたい人はこのコードを使ってね)もっとサクッとメニューハンドルから送信先のウィンドウハンドル取れたらいいんですけどねー。内部的には知ってるはずだよなー。なんかないかなー。