ja ja en ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja en ja ja ja ja ja ja ja ja ja ja en ja es gu ja fr en ja zh mk ja el ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja pl nl ja nb ja ja ja ja ja en ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja jbo en en ja nl nb en ko it en it it it en fr it ja ja en la fi ja it it it pt it de en ja en en it fr it en it nb it ja ja ja ja en ja ja ja ja ja ja ja ja ja
C#で他アプリケーションを操作するための基礎知識 - Sanwa Systems Tech Blog
メッセージのタイプは「マウスの左ボタンが押された(WM_LBUTTONDOWN)」だよ
対象のボタンのウィンドウハンドルは 0x00022012 だよ
一緒に押されていたキー(マウスの右ボタン、キーボードのSHIFTキーなど)はなかったよ
クリックされた場所の座標は(8, 11)だよ
メッセージを受け取ったプログラムはこれを解釈し、「Windowsから伝えられたメッセージはWM_LBUTTONDOWN、ウィンドウハンドルは0x00022012だったな。つまり"5"のボタン*2がクリックされたわけだから、それに応じた処理をしよう……」と考えるわけです。普段C#でプログラムを書いている人はこんなことを意識しませんが、ここら辺は .NET Framework がよろしくやって隠蔽してくれています。
メッセージは、作れる!
通常、「マウスがクリックされた」「キーが押された」といったメッセージはユーザーの操作に応じてWindowsがプログラムに対して発行されるものです。しかし、メッセージは自分で他のプログラムへ送信することもできるのです。これを使えば、自分のプログラムから他のアプリケーションの画面を「クリックしたことにする」「キー操作したことにする」といったこともできるようになります。これが冒頭で触れた「他アプリのコントロール」「自動操作」だということになります。
メッセージには4つのパラメータが含まれています。他のウィンドウにメッセージを送る関数の一つ、SendMessage関数の宣言をMSDNから引用してみましょう。
LRESULT SendMessage( HWND hWnd, // 送信先ウィンドウのハンドル UINT Msg, // メッセージ WPARAM wParam, // メッセージの最初のパラメータ LPARAM lParam // メッセージの 2 番目のパラメータ );
これはC++のアンマネージドコードなので、見慣れない型がたくさん並んでいて逃げ出したくなるかもしれません。型については
プラットフォーム呼び出しのデータ型
Windows Data Types (Windows)
を参考にして、さらに必要な宣言を加えてC#に書き換えるとこうなります。 *3
[DllImport("user32.dll")] extern long SendMessage( IntPtr hWnd, // 送信先ウィンドウのハンドル uint Msg, // メッセージ uint wParam, // メッセージの最初のパラメータ uint lParam // メッセージの 2 番目のパラメータ );
だいぶ親しみが持てる形になりました。4つの引数を順に説明してゆきます。
これは最初に説明しました。ハンドルが0x00022012であれば、new IntPtr(0x00022012)のように書けます。既存のウィンドウからウィンドウハンドルを取得する方法については後述します。
Windowsには様々なメッセージが定義されています。ここにはそのメッセージを示す整数を渡します。よく使いそうなものを挙げるとこんな感じです。
キーボードのキーが押された
キーボードのキーが離された
WM_LBUTTONDOWN
マウスのボタンが押された
WM_LBUTTONUP
マウスのボタンが離された
完全な一覧はAbout Messages and Message Queues (Windows)にあります。マウス・キー操作はWM系を参照すれば良いでしょう。C++であればwinuser.hというヘッダファイルをインクルードすれば一挙に定数がインポートできるのですが、C#にはそのようなものはないので自前で定数を定義して使うことになります(別にハードコードしても構いませんが)。
なお、受け取ったメッセージをどのように処理するかはアプリケーションの自由です。また、メッセージとはいえども実体はただの整数値ですから、アプリケーション単位で好きな値を決めて独自のメッセージを定義することもできます。同時起動した同じアプリ同士、あるいは異なるアプリ同士*4で連携を行う場合などに使われています。詳しくはWM_USERで検索してみてください。
メッセージの内容をさらに詳細に示すためのパラメータです。「最初の」とか「2番目の」とか曖昧な表現になっているのは、メッセージの種別によって担うべき情報が異なるからです。たとえばWM_LBUTTONDOWNでは、
最初のパラメータ……同時に押された他のキーやボタンを示す
2番目のパラーメータ……クリックされた場所の座標を示す(下位2バイトがX座標、上位2バイトがY座標)
となっています。またWM_KEYDOWNでは、
最初のパラメータ……押されたキーの仮想キーコードを示す
2番目のパラーメータ……オートリピート中であるかどうかなどの補助情報
が格納されます。一部のメッセージでは常に固定値となっているものもあります。こういった情報はメッセージ名で検索すると容易に見つけることができます。
電卓を例にしたサンプル
ここまでの話を具体的に試すためのサンプルとして、起動中の電卓の「5」ボタンを押すコードを書いてみます。こんな手順になります。
電卓のウィンドウハンドルを検索する(これで親となるウィンドウのウィンドウハンドルが分かる)
ウィンドウの親子構造をたどり(あるいは再帰的に子ウィンドウを探し)目的のボタンを見つける
見つけたボタンのウィンドウハンドルにWM_LBUTTONDOWNを送る(これでマウスのボタンを押したことになる)
見つけたボタンのウィンドウハンドルにWM_LBUTTONUPを送る(これでマウスのボタンを離した=クリックが完了したことになる)
目当てのウィンドウを見つけるのはWebでスクレイピングを行うのと似ています。ラベルとクラスだけで特定できれば楽なのですが、そうはいかないケースも多々あります。たとえば電卓の場合、ボタンのラベルは「文字」ではなく「絵」(グラフィック)で描画されていますので、ラベルは全てのボタンでブランクになっています。今回は強引に「全てのボタンを列挙し、その10番目を選ぶ」という方法で行ってみます。
WindowsのAPI、FindWindowEx関数などを使って探すのですが、冗長になるためこの記事の最後に完全なコードを示します。今はあらかじめ分かっていることにします。
class Program { public const int WM_LBUTTONDOWN = 0x201; public const int WM_LBUTTONUP = 0x202; public const int MK_LBUTTON = 0x0001; [DllImport("user32.dll")] public static extern int SendMessage(IntPtr hWnd, uint Msg, uint wParam, uint lParam); static void Main(string[] args) { // 電卓のトップウィンドウのウィンドウハンドル(※見つかることを前提としている) var mainWindowHandle = Process.GetProcessesByName("calc")[0].MainWindowHandle; // 対象のボタンを探す(これでボタンのハンドルが取得できる) var hWnd = FindTargetButton(mainWindowHandle); // マウスを押してから放す SendMessage(hWnd, WM_LBUTTONDOWN, MK_LBUTTON, 0x000A000A); SendMessage(hWnd, WM_LBUTTONUP, 0x00000000, 0x000A000A); } public static IntPtr FindTargetButton(IntPtr hTopWindow) { /* ... */ } }
というわけで、全体の流れを復習するとこんな感じです。
対象のアプリを事前に手動操作してどんなメッセージを送るべきかを確認しておく
対象のアプリのウィンドウ構造を確認しておく
対象のアプリの操作対象となるウィンドウをどのように探せば良いかを確認しておく
対象のアプリのウィンドウを検索してウィンドウハンドルを取得する
決まった順にメッセージを飛ばしてマウスやキー操作をエミュレートする(必要があれば適宜ウェイトを入れる)
Windowsのベースとなる仕組みを使っているため、非常に煩雑ではありますが、何か特殊な対策を行っていない限り原理的にはどのようなアプリケーションも制御できることになります。使えるものは全部使って良い自動化ライフを送りましょう。それでは。
class Program { public const int WM_LBUTTONDOWN = 0x201; public const int WM_LBUTTONUP = 0x202; public const int MK_LBUTTON = 0x0001; public static int GWL_STYLE = -16; [DllImport("user32.dll")] public static extern int SendMessage(IntPtr hWnd, uint Msg, uint wParam, uint lParam); [DllImport("user32.dll")] public static extern IntPtr FindWindowEx(IntPtr hWnd, IntPtr hwndChildAfter, string lpszClass, string lpszWindow); [DllImport("user32")] public static extern int GetWindowLong(IntPtr hWnd, int nIndex); [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] public static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount); [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] public static extern int GetWindowTextLength(IntPtr hWnd); [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); static void Main(string[] args) { // 電卓のトップウィンドウのウィンドウハンドル(※見つかることを前提としている) var mainWindowHandle = Process.GetProcessesByName("calc")[0].MainWindowHandle; // 対象のボタンを探す var hWnd = FindTargetButton(GetWindow(mainWindowHandle)); // マウスを押して放す SendMessage(hWnd, WM_LBUTTONDOWN, MK_LBUTTON, 0x000A000A); SendMessage(hWnd, WM_LBUTTONUP, 0x00000000, 0x000A000A); } // 全てのボタンを列挙し、その10番目のボタンのウィンドウハンドルを返す public static IntPtr FindTargetButton(Window top) { var all = GetAllChildWindows(top, new List ()); return all.Where(x => x.ClassName == "Button").Skip(9).First().hWnd; } // 指定したウィンドウの全ての子孫ウィンドウを取得し、リストに追加する public static List  GetAllChildWindows(Window parent, List  dest) { dest.Add(parent); EnumChildWindows(parent.hWnd).ToList().ForEach(x => GetAllChildWindows(x, dest)); return dest; } // 与えた親ウィンドウの直下にある子ウィンドウを列挙する(孫ウィンドウは見つけてくれない) public static IEnumerable  EnumChildWindows(IntPtr hParentWindow) { IntPtr hWnd = IntPtr.Zero; while ((hWnd = FindWindowEx(hParentWindow, hWnd, null, null)) != IntPtr.Zero) { yield return GetWindow(hWnd); } } // ウィンドウハンドルを渡すと、ウィンドウテキスト(ラベルなど)、クラス、スタイルを取得してWindowsクラスに格納して返す public static Window GetWindow(IntPtr hWnd) { int textLen = GetWindowTextLength(hWnd); string windowText = null; if (0 < textLen) { //ウィンドウのタイトルを取得する StringBuilder windowTextBuffer = new StringBuilder(textLen + 1); GetWindowText(hWnd, windowTextBuffer, windowTextBuffer.Capacity); windowText = windowTextBuffer.ToString(); } //ウィンドウのクラス名を取得する StringBuilder classNameBuffer = new StringBuilder(256); GetClassName(hWnd, classNameBuffer, classNameBuffer.Capacity); // スタイルを取得する int style = GetWindowLong(hWnd, GWL_STYLE); return new Window() { hWnd = hWnd, Title = windowText, ClassName = classNameBuffer.ToString(), Style = style }; } } class Window { public string ClassName; public string Title; public IntPtr hWnd; public int Style; }
*1:この一瞬後、プログラムはさらに「マウスの左ボタンが離されたよ」というメッセージも受け取ることになりますが、話が面倒になるので省略します
*2:どのウィンドウハンドルがどのボタンを示すかの対応表はプログラムが自分で覚えておく必要があります
*3:最後のlParamは本当はint型になるはずですが、パラメーターを作るのが面倒なのでuint型にしています。どのみちビット列の解釈の違いでしかないので問題にはなりません
*4:つまりプロセスが異なるのでメモリも共有できない状態
nurenezumi 2015-11-25 17:10
CloudWatchの通知をSlackに流す
こんにちは。AWS担当のwakです。今回はAWS監視の話をします。 …
« CrystalReportsの印字位置をC#側で直接指… EPPlusで作成したOpenXmlの構成 »