10/14の記事でWin9x系へのサポートについて言及した他プロセスのアドレス空間へのアクセスですが、一々実行時にプラットフォームを考えて処理を分岐するというのは死ぬほど面倒なものがあります。
そこで作ってみたのが、プラットフォームに応じて自動的に確保や読み書きをやってくれる共有メモリのラッパクラスです。飽くまでも扱いやすさのためのクラスですから、速度効率的にはそう良くないでしょう。virtualとか使ってるし。ポイントは透過的に扱えるという点なのでそんなのはまあ無視です。
ついでと言ってはなんですが、扱いやすさついでに、構造体・バイト配列・文字列・ポインタ(IntPtr)の読み書きも一元的に扱えるようにしましょう。一々「文字列を書き込むから、一旦Encoding.GetBytesでバイト配列に……うわこれも9x系はEncoding.Defaultで」とか「Marshall.PtrToStructureで……ああVSがあれば引数のインテリセンスが」とかなどの混乱無く、書き込むときはWriteのオーバーロードで、読み出すときはReadHogehogeで一発解決です。ついでに、大きめに領域を取って前半に構造体を、後半に関数などによって格納される文字列のバッファを割り当てるってなやり方のために、それぞれにアドレスオフセットを取るオーバーロードも作っておきます。このクラスを使えば確保の手間もないわけですから文字列バッファをまた別に確保しても良いんでけど、そこはまあ効率とかあるかも知れないので。
それでは共有メモリ管理クラス・SharedMemoryの解説に入りましょう。今回はじっくり解説してみます。
SharedMemoryは抽象クラス(abstract/MustInherit)で、このクラスのインスタンスを直接は作成しません。静的メソッドとしてAllocが用意されており、そこで動作プラットフォームに応じて、SharedMemoryにネストして定義されているクラスSharedMemoryNTまたはSharedMemory9xのインスタンスを作成しそれを返します。SharedMemory9xではAllocの引数のうちプロセスに関するものは使用されませんが、両方のプラットフォームから透過的に扱えるのが目標なので、プロセスに関する引数の省略を許しません。
SharedMemoryは書き込みに関するメソッドをオーバーロードで8つ、読み出しに関するメソッドをオーバーロードを含めて8つ用意しています。このうち読み書き4つずつは「型インスタンス・バイト配列・文字列・IntPtrで表現されるメモリ領域」のそれぞれの単純な読み書きを行い、仮想メソッド(virtual/Overridable)として宣言されています。残りの4つずつは書き込むアドレスのオフセットを指定できるオーバーロードになっており、こちらは抽象メソッド(abstract/MustOverride)で、派生クラスであるSharedMemoryNT/SharedMemory9xが実装を持ちます。ちなみに仮想メソッドの8つは、単純にオフセット0で対応するオーバーロードを呼び出しているだけです。一応、拡張性のため仮想メソッドで別のやり方も許容するようにしていますが、SharedMemoryNT/SharedMemory9xではそのままにしています。
確保した共有メモリが確実に解放されるように、SharedMemoryは解放手段として多少複雑な構成を取ります。まず、SharedMemoryはIDisposableインターフェイスを実装します。分かりやすさのために、Free()メソッドも定義し、実装はこちらで行います(C#では、Dispose()はそのままFree()を呼び出します(Dispose()そのものは明示的実装とし、外部から直接は見えない)。VB.NETではDispose()の別名定義としてFree()を実装します)。Free()は、まず抽象プロテクトメソッドであるFree(bool)を呼び出します。この引数のbool値はFree()から呼び出したかどうか(ファイナライザが呼び出した場合はfalse)を表します。派生クラスはこのFree(bool)をオーバーライドして共有メモリやその他のハンドルなどを解放します。Free()は更にDispose(bool)を呼び出し、SharedMemory抽象クラス側で必要な後処理を行います。
GetAddressメソッドは始めに確保した領域の先頭アドレスからオフセットを追加したアドレスを返すメソッドです。当然の事ながらオフセットを0未満にすると例外を投げます。
Addressプロパティは、確保したアドレスを利用者が取得するためのプロパティです。これを利用してプロセス越しのSendMessageをしたりするわけですね。
それから、IsNT静的プロパティは、そのままEnvironment.OSVersion.Platformを見て判断しているだけです。ところで.NET 2.0でもWin64NTとか追加されていないんですが(代わりにUnixなんてのが追加されている)どうするんでしょうね。というかWin64下でAPI呼び出しがどうなるのか知らないんですけど。DLLの名前とか。またThrowLastWin32ErrorメソッドはAPI呼び出しの失敗時に例外を送出するメソッドです。
しかし、ほとんどのメソッドでdisposedをチェックして例外を投げていますが、こういうのってメソッドにした方が良いんですかね? CheckDisposedとかそんなの。
続いてSharedMemoryNTクラスの解説です。NT系では他プロセスのメモリ空間を扱う場合、どのプロセスを扱うかと言う情報が必要なので、コンストラクタ引数に、確保するサイズの他にそのプロセスのハンドルまたはSystem.Diagnostics名前空間のProcessインスタンスを要求します。Processインスタンスを使って確保する場合、そのProcessインスタンスのClose/Disposeが呼ばれるとハンドルが無効になってしまうため、与えられたProcessインスタンスのIdプロパティを元に新しくProcessインスタンスを作成します。
読み書きを行うメソッドの内、抽象メソッドをそれぞれオーバーライドして動作を規定します。WriteProcessMemory/ReadProcessMemoryを使う他はそう目立ったところはないでしょう。
SharedMemory9xは更に簡単です。確保するとき以外は自プロセスのIntPtrへの操作と同じですからね。
ところで、WinFXではこの辺はどうなるんでしょうか。タスクバーからアイコンを消したいとか良くある要求だと思うんですが。そうでもないですか。
今回は作業量の関係上C#のコードのみとなっています。一応そのうちVB.NETのコードも書く予定ですが。いつものXMLドキュメント付きのですが、これってただ見づらいだけかなぁ……。
C#だけですが今回はいつもの@IT会議室から、マウスの乗っているTabコントロールを取得したいスレッドをモチーフに、TabControlを拡張したいと思います。
TabControlは便利なコントロールですが、如何せん少々不自由なところがあります。タブ周りに関するメソッドやイベントが不足しているのです。
まず、任意の位置に存在するタブを取得する、って事ができません。そして、その事に起因することが、マウス関係のイベントがタブ上で発生しても、それがどのタブで発生したのかってのが分かりません。タブ右クリックでコンテキストメニューを表示させようにも、はたと困ってしまいます。
TabControl.ContextMenuを使ったコンテキストメニューは、TabPageの中まで有効なことに注意です。TabPageに含まれるコントロールがContextMenuを持っていた場合はそちらが優先されますが、持っていない場合はTabControlに設定したコンテキストメニューが表示されます。厄介なことですね。
実はというかやっぱりというか、「任意の位置に存在するタブを取得する」方法は、コモンコントロールのタブコントロールのレベルでTCM_HITTESTメッセージとして存在します。MFCではCTabCtrl::HitTest関数としてきっちり実装されているのにどうして.NETで省略されたのかは謎ですが。TCM_HITTESTはついでにタブ内の位置も取得できます。タブ内の位置というのは、タブタイトルの文字列部分とかタブのアイコン部分とかのことです。
こいつを実装してやればいろいろできそうです。コンテキストメニューの表示なら全く問題ありません。が、例えばマウスカーソルがタブに入ったときにオーナードローでタブの背景色を変える、と言った場合どうするか。.NET的にはイベントですが、どうやって発生させましょうか。
WndProcの各種マウスメッセージを捕まえる手もありますが、ここはもっと単純にMouseMoveなどのイベントで判定させ、タブ絡みのイベントを発生させるという手段を執ることにします。欠点は派生クラスでOnMouseHogehogeをオーバーライドしたのにbase.OnMouseHogehogeを呼ばなかったときに発生しないことですが、まあそれくらいは目を瞑りましょう。
例で出したタブの背景色についてもちょっとコメントしましょう。TabPageクラスにあるBackColorは、タブではなくタブページの背景色なのでタブの背景色には使えません。とすると、色をおくところとして考えられるのは
このうち、TabPage.Tagに置くのは微妙かも知れません。後々汎用データを追加しなきゃならなくなったときに仕様変更部分が大きくなります。もっと大きな問題は、この変更をTabControlに通知しようがないと言うことです。タブ部分の描画はTabControlの責任ですから、どうやってかTabContrlに色が変わったことを伝えなければならないのですが。派生クラスのところで解説する手段を使えば不可能ではないのですが、それを行うのがTabControlでもTabPageでもない、恐らくはFormで行うことになると言うのがイヤな感じです。
派生クラスを作ったとしても、この問題はつきまといます。TabBackColorが変更したときにイベントを発生させることは問題ではありません。しかし、それをどうやってTabControlに捕捉させるか。TabControl.ControlAddedイベントでTabPage.TabBackColorChangedイベントをひっかけるくらいでしょうか。結局TabControl側の変更が必要になってしまって巧くありません。一つの解決として、TabBackColorが変更されたときにParentを調べ、そのInvalidateメソッドを呼ぶ、と言う手があります。Invalidateの範囲の制限のためにGetTabRectも可能ですから効果はばっちりですね。これはこれで解決ですが、Parentのメソッドを呼ぶってのはちょっと抵抗があります。
TabControl側で管理するのも一案です。これなら色を変更するメソッドでついでにInvalidateもできますからね。TabPageで持つべき情報をTabControlで管理するのは気持ち悪いですが、TabPageの種類(派生クラス)に縛られないで一元的に管理できるというのも魅力かも知れません。どのみち色の情報を必要とするのはTabControl.OnDrawItemなのですから、TabControlの変更は必須ですし。
オブジェクト指向的にはTabPageが持つのが自然に感じるでしょう。しかし描画のことを考えると逆にTabControl側が持つ方がすっきりするのです。なかなか難しいもんですね。
ところで気付いたことが後一つ。今まで何気なくSendMessageの返値をInt32で宣言していましたが、よく調べるとLRESULTはLONG_PTR型で、つまりIntPtrで取るのが正しいらしいです。
C#のソースはこちら。
コードしか見えない今更ながら、DDEML関数を.NETにマッピングしてみています。実は『DDEって何?』レベルからのスタートなんですが。一通り調べた感想は、まあ要するに規格化されたメッセージだなと。いやそのままですが。直接DDEメッセージを扱うのではなく、DDEML関数を通して扱うとなると実体が見えにくいので。しかし一番手間取るのがドキュメント部分だったり……。
さて今日は、前回のと対をなす、マウスフックを実装するクラスを紹介しましょう。
これもグローバルフックを使用しますが、なぜか.NETで可能なので。あ、でもNT系しか使えません。
コードを解説すると、基本的なところはというかほとんどKeyboardHookと同じです。ちょっとした技巧としてFieldOffset属性を使用して共有体もどきを作っているくらいでしょうか。
注意点として、キーボードフックと同じく簡単なクラスなので何かのメソッド内で宣言・設定をまとめてやってしまいがちですが、宣言をメソッド内でやってしまうと、そのメソッドが終了時にMouseHookオブジェクトへの参照が無くなってしまってGCの対象になってしまいます。複数インスタンスを使うクラスでもないですし、staticフィールドとして宣言しておくのが良いでしょう。繰り返しますがWindows 9x系では動きません。
Win32APIをマッピングする際に悩むのがどうコールバックをイベントにするか、というかイベントデータクラスをどう書くかです。私はできる限りIntPtrやビットを隠して、分かりやすいEventArgs派生クラスを作るように心がけていますが……面倒なことこの上ないです。
今回のコードもC#のみです。記事に直に載せたりもしませんので、直接csファイルをどうぞ。要望があればVB.NETにも移植しますけど。そう言えば昨日だったか、まさにぴたりと「VB.NET フック」でいらっしゃった方がいたようですが。
一応キーワードを播いておきましょう。C#による、SetWindowsHookExを使った、WH_MOUSE_LLの実装、が今回の主題でした。
06/04/13 追記。当ブログ二番目くらいに人気のグローバルフックですが、問い合わせがあった KeyboardHook の方はさっさと修正を加えたのですがこちらは放置しっぱなしでしたので、改めて改善版のコード を出しておきましょう。また、今回新たに MouseHookedEventArgs を EventArgs からではなく CancelEventArgs から派生させました。イベントハンドラで Cancel プロパティを true にしてやれば、そのときのマウスの動作をキャンセルできます。が、注意深く扱ってください。特にマウスは影響範囲が大きいですからね。
今日は、過去に書いたコードの中から、キーボードフックを実装するクラスを紹介しましょう。
.NETではフックに関して、「グローバルフックは使えない」と説明されています。DLLにグローバルフックで使用されるコールバック関数を実装しなければならないのに、.NETではそのDLLを作れないからです。
なんですが、何故かWH_KEYBOARD_LLやWH_MOUSE_LLはグローバルフックが可能なようなのです。理由は定かではありませんが。ひょっとしたら何らかの環境依存があるのかも知れません。ですから当然動作保証などできません。少なくとも私の環境では動いています。
コードそのものはそう大したことはしていません。SetWindowsHookExとその周辺を軽くラップした程度です。
テクニックとしては、直接SetWindowsHookExが要求するコールバックを外に見せるのではなく、コールバックメソッドそのものは内部で持って、その内部のコールバックメソッドからユーザが登録するイベントとして別のより認識しやすい形にしたデリゲート型のイベントを定義している事でしょうか。ああ、言葉で言うと本当に分かりづらいですね。こういう形にしたのには当然訳があって、一旦アンマネージドにデリゲートの参照を渡してしまうと、その後そのデリゲートに新たにハンドラを加えても(+=しても)、アンマネージド側はその後から加わったハンドラは認識も実行もできないんですね。それで、アンマネージドに渡すハンドラはクラスのプライベートメソッドに限定して、ユーザが使用するイベントにはもっとかみ砕いたイベントハンドラを用意しようと言うわけです。これならハンドラの追加も削除も自由自在。
注意点として、簡単なクラスなので何かのメソッド内で宣言・設定をまとめてやってしまいがちですが、宣言をメソッド内でやってしまうと、そのメソッドが終了時にKeyboardHookオブジェクトへの参照が無くなってしまってGCの対象になってしまいます。複数インスタンスを使うクラスでもないですし、staticフィールドとして宣言しておくのが良いでしょう。またWindows 9x系では動きません。
今回のコードはC#のみです。記事に直に載せたりもしませんので、直接csファイルをどうぞ。
2006/03/19 追記。ここのコメント欄で書いた通り、KeyboardHook.cs の内容が多少古くなっていましたので、ちょっとだけ手直ししたバージョンを公開します。以前のものも残しておくので見比べてみてください。
2006/04/13 追記。もうちょっと機能拡充すべくわずかに手を加えました。今回の変更点は、まず DefaultEvent 属性を付けたこと。これでフォームに D&D したあと KeyboardHook コンポーネントをダブルクリックするだけでイベントが記述されます。自分が VS 使わないものだからこの辺気付きませんでした(笑)。そして機能の拡張として、KeyboardHookedEventArgs を CancelEventArgs から派生させ、キーボードのイベントをキャンセルできるようにしました。
今回のネタは各所で話題のC# 3.0。今更とか言わないで。決してTextBoxの記事を書くのが面倒になった訳じゃないですじょ?
一つ一つこれはどういう機能であるという解説はこちらなどにやって頂くことにして、私の雑感など。
えー、クエリ表現ですか、正直これには今のところ余り興味が湧きません。私にとってSQLが余り身近でないこと、データの検索などの重要度が大きくないことが原因でしょう。こういうのをフレームワークのみならず言語として取り込むというのには言語の複雑性を増すという点、また基本的に異文化であるSQL構文を含むことで言語の統一性が失われると言う点で抵抗が強いです。
型推論がある程度強力になったのはどうでしょう。暗黙の型付け、匿名型、暗黙の型付け配列。いずれもそのままでは書くのが楽になっても読むのに苦労を強いられそうです。匿名型は、プロパティしか定義できないっぽい? なんか微妙。
ラムダ表現、オブジェクト/コレクション初期化子は基本的にただのシンタックスシュガーですから抵抗無く受け入れられそうです。ラムダ表現にはちょっと慣れが必要でしょうけど。
拡張メソッドは賛否分かれそうですね。文としては自然になりますしあってもいい機能だとは思いますが、もうちょっと文法を変えた方が良いような気もします。obj.StaticMethodではどこで定義されているメソッドなのか極めて分かりづらいですからね。obj.Class.StaticMethodって感じでも良いような。んー、利点が無くなるかな……。
C# Version 3.0 Specification Semtember 2005を読んで気になったのは、サンプルが軒並みGenericsを使っていることです。私は、List
ここ(hongliang.seesaa.net)で公開しているものについて、利用は自由に行って頂いて構いません。改変、再頒布もお好きになさって下さい。利用に対しこちらが何かを要求することはありません。
ただし、公開するものを使用、または参考したことによって何らかの損害等が生じた場合でも、私はいかなる責任も負いません。
あ、こんなのに使ったってコメントを頂ければ嬉しいです。
この広告は90日以上新しい記事の投稿がないブログに表示されております。