酒井 法雄 SAKAI,Norio norio@int21.co.jp
| メモリを直接操作できる「お楽しみ」 |
|---|
Windows APIは強力だ.Visual Basicの限界を超えるためには,必須項目である.そして,単純にWindows APIを使えば何でもできてしまうように思いがちだ.しかし,これは正しくない.それは,あくまでもVisual Basicの言語仕様に合わせて使うからにほかならない.
このような問題で使えない,あるいは使いにくいものに,アドレスの操作がある.Basic言語は歴史的に危ないことをユーザーができにくい仕様になっていた.したがって,変数や関数のアドレスを直接いじることはあまり考慮されていなかった.
もっとも,DOS時代にはそうでもなかったのだが,Windowsになってからはこの制限が厳しくなった.というのも,マルチタスクをサポートしたWindowsではメモリは動的に移動してしまうため,メモリをロックして固定しないといけなかったのである.Visual Basicで使われるメモリも,モノによっては動的なものであり,その管理はVisual BasicおよびWindowsに任せておく必要があったのだ.
しかし,Visual Basicはバージョンを重ねるごとに「これができたらいいのに」という点が次々と仕様に盛り込まれてきた.そして,ついにVersion 5.0ではメモリを直接操作できるキーワードや関数が用意されるにいたった.
これは本当に進化なのかと考えると,高レベルな言語としてのVisual Basicには本来不要のものではないかという気がしないでもない.Windows APIなど泥臭いモノを使わなくてもAutomationだけで機能を拡張していくのがVisual Basicの本来の姿だといえなくもないからだ.実際,OSがサポートする基本的な機能はどんどんAutomationでVisual Basicから使えるようになってきているし,MicrosoftはCairoと呼ばれるWindowsの将来技術ではすべてをCOMベースでできるようにすると言明していたハズだ.
しかし,現実にはなかなかそうはなっていない以上,こうした下々の者がすべき泥臭い作業(?)も,Visual Basicからできるようになっていなくては困るわけだ.そういった意味では,Visual Basic 5.0は画期的なバージョンとなったともいえるのである.
こういったVisual Basic 5.0でできるようになった泥臭い処理の中でも,画期的に面白いのがサブクラス化である.カンタンに言えば,これはVisual Basicのイベントとして用意されていないイベントを,つまり通常は知ることのできなかったメッセージを知ることができるというものである.
とはいえ,アドレスを扱う,メモリを直接扱うといった処理は,便利というよりはWindowsのネイティブな部分に近づけることが嬉しいといった,お楽しみ的要素が強いのも確かである.もちろん,便利なことは便利なのだが,実はVisual Basic附属あるいは市販のActiveXコントロールがあれば,こんなものは使わない方が開発工数は少なくて済むのである.もちろん,APIなんてものも使わない方がよいに決まっている.
それでもあえてこのお楽しみを実践したいという方は,ここから先を読んでいただきたい.
| これは高度な「お楽しみ」だ |
|---|
お楽しみだと割り切ってしまえば,こちらとしても気がラクだ.なにせ仕事に役立つかどうかなんて話は二の次である.W. Stealride氏がよく書いている記事を読むときのように,「こりゃすげーや.でも,これって本当に役に立つの?」なんてことを考えてはいけないのだ.本当の目標はWindowsのHackなのである.
というわけで,Hackしようというからには,アナタにもそれなりの覚悟が必要である.次に,ここから先を読み進むための条件を提示しておこう.
これを満足できない,あるいはする気がない方には,この記事はムダである.
しかし,まじめな話,これら全部に当てはまったとしたら,それも問題である.私だったら,こんなアナタには,安心して仕事を任せられない.こういう人はシステムが動くことより,自分の興味のある一部のコードが動くことの方が重要になりがちだからだ.
というわけで,これは試金石だったのである.サブクラス化は確かに楽しいのだが,こういうことにハマってしまうと,今すべきことの本質を見失う恐れがあるので,十分に注意されたい.いや,そんなに真剣に悩んだり怒ったりしなくてもよいのだ.これは「お楽しみ」なのだから.
| VB5のアドレス関係キーワード |
|---|
いつまでも言葉遊びをしていてもしょうがない.さっそくVisual Basic 5.0で強化されたアドレス関係のキーワードを紹介しよう.次に一覧を示す.
このうち,正式にサポートされているのは,AddressOfだけである.他のものはヘルプにも出ていない.では,どうしてサポートされているのかわかるのだろうか.
これは,私がMSKKにいる怪しい友人から聞き出したわけではない.いや,そういうものがあるらしいという噂は聞いていた.しかしこれはオブジェクトブラウザに出ていることなのだ.といっても,普通にVisual Basicから[F2]キーを押してオブジェクトブラウザを開いて「VarPtr」を検索しても,何もでてこない.
オブジェクトブラウザのそのへんを右クリックし,出てきたドロップダウンメニューから「非表示メンバーの表示」を選ぶと,「HiddenModule」クラスのメンバーとして出てくるのである(図1).これはPCDNで,とっても難しい質問が出たときに登場する謎のお米屋さんこと穐谷さんから教えていただいたのである.この人は怪しい.おそらく,先に述べた条件をすべて満足しているに違いない.
余談はこれくらいにして(編注:前置きがネストしています),これらのうちから,今回の話の中心となるAddressOf演算子の機能について説明しよう.
| AddressOf演算子の必要性 |
|---|
AddressOf演算子は,それに続く関数のアドレスを得るためのものである.関数のアドレスなんかわかってもしようがない,むしろVarPtrやStrPtr, ObjPtrの方が使い途があるのではないかと思われる方もいらっしゃるだろう.たしかにその方が用途としてはわかりやすい.おそらく,Windows APIでハマったことのある方なら,そう思うだろう.
しかし,もうちょっと先までいってハマった方には,AddressOfは神様の贈り物のように感じるほど嬉しいものである.
Cなどでは,関数のアドレスが重要になることがある.たとえば,関数のアドレスを保持するポインタ配列を宣言し,なんらかの条件分岐によって呼び出す関数を変えたいといったとき,関数のアドレスをこのポインタ配列に代入しておけば,一発で目的の関数を呼び出すといったワザが使えるのである.
しかし,Visual Basicで使って嬉しいのはこういう場面ではない.Windows APIがらみで,どうしても必要になる場面があるのだ.それが,Callback関数と呼ばれるものである.
たとえば,次の例を見て欲しい.ここにCallbackProcという関数を作った.Funcという関数の引数として,「AddressOf CallbackProc」が指定されている.これは,CallbackProcという関数のアドレスを引数としてFunc関数を呼び出すということである.この結果として,なんらかのタイミングで自動的にCallbackProcが呼び出されるようになるというのが,コールバック関数である.
Func(AddressOf CallbackProc, …)
Public Function CallbackProc()
....
End Function
実は,こういったコールバック関数を使うWindows APIは結構あるのだ.たとえば,Win32 APIのEnumWindows関数は,図のような動きをする(図2).
つまり,そのとき存在するウィンドウの数だけ,Callback関数が呼び出されるのだ.このとき,引数としてひとつひとつのウィンドウのハンドルhWndが渡される.このhWndを解析して,たとえばタイトルを出すとか,クラスを得るなどしてリストボックスに追加するという処理をCallback関数に記述しておく.すると,すべてのウィンドウについての情報が得られるというわけだ.
実際にこれを使ったサンプルプログラムは,以前に本誌1997年6月号 「VisualBasic 5.0の新しい言語仕様:API編」でも紹介した通りだ.ここでは,コードおよび実行画面を再掲載しておく(リスト1-a・b,図3).
リスト1-a:enumwin.bas
Option Explicit
Declare Function EnumWindows Lib "user32" _
(ByVal lpEnumFunc As Long, lParam As Any) As Long
Declare Function GetWindowTextLength Lib "user32" _
Alias "GetWindowTextLengthA" (ByVal hwnd As Long) As Long
Declare Function GetWindowText Lib "user32" _
Alias "GetWindowTextA" _
(ByVal hwnd As Long, ByVal lpString As String, _
ByVal cch As Long) As Long
Declare Function IsWindowVisible Lib "user32" _
(ByVal hwnd As Long) As Long
Public Function EnumWinProc _
(ByVal hWndX As Long, lParam As Long) As Boolean
If frmMain.chkVisible.Value = 0 _
Or IsWindowVisible(hWndX) Then
frmMain.List1.AddItem GetWinText(hWndX)
frmMain.List1.ItemData(frmMain.List1.NewIndex) = hWndX
End If
EnumWinProc = True
End Function
Public Function GetWinText(hWndTarget As Long) As String
Dim l As Long
Dim s As String
Dim dum As Boolean
l = GetWindowTextLength(hWndTarget) + 1
s = String(l, 0)
dum = GetWindowText(hWndTarget, s, l)
GetWinText = s
End Function
|
リスト1-b:enumwin.frm
Private Sub chkVisible_Click() Command1.Value = True End Sub Private Sub Command1_Click() Dim dumb As Boolean List1.Clear dumb = EnumWindows(AddressOf EnumWinProc, 0&) lblTotal.Caption = "Total:" & CStr(List1.ListCount) & " Windows" End Sub Private Sub Form_Load() Command1.Value = True End Sub Private Sub List1_Click() lblhWnd.Caption = "hWnd:" & Hex$(List1.ItemData(List1.ListIndex)) End Sub Private Sub List1_DblClick() AppActivate Mid(List1.Text, InStr(List1.Text, ":") + 1), True End Sub |
こうした,Enum系の関数には,次のようなものがある.
Win32 Enum*関数
| EnumWindows |
| EnumPropsEx |
| EnumCalendarInfo |
| EnumDateFormats |
| EnumSystemLocales |
| EnumMetafile |
| EnumThreadWindows |
| EnumResourceLanguages |
また,プリンタがAbortしたときに実行されるCallback関数も,アドレス指定をしておくなど,結構いろいろな場面で使われるのである.そして,ここでのテーマであるサブクラス化でも使われるのだ.
| AddressOf を使うための注意 |
|---|
サブクラスの話に入る前に,AddressOfを使う上での注意事項を述べておこう.
通常のWindows API関数からコールバックされる関数をVisual Basic内に記述するときに必要なのは,1〜3までの条件である.フォームやクラスモジュールには書くことができないので注意してほしい.AddressOf演算子自体を使う場所は,他のモジュールでもかまわない.
つまりは,先ほどの例にあったような書き方をすればよいというわけだ.
| Visual Basicで知ることのできないイベント |
|---|
Windowsはメッセージベースで動いている.メッセージ自体は,WM_XXXというような定数で表わされるものだ.画面に再描画が必要になったとき,ボタンが押されたときなど,そのウィンドウに関連するイベントが発生すると,Windowsのシステムからメッセージが送られてくる.各ウィンドウは,このメッセージを受け取って,適切な処理を行なうことになる.
Visual Basicでは,このメッセージをイベントという形でカプセル化して表現している.実際,Visual Basicのイベントには多くの種類があり,オブジェクトによってその内容も異なっている.しかしながら,これはWindowsから送られてくるすべてのメッセージに対応するものではない.ありがちな処理にのみ対応したイベントが用意されているのだ.
これは,メッセージの種類だけイベントがあったら,とてもプログラミングが難しくなってしまうからである.
Visual Basicでは,あるときにはそのメッセージに対応した処理を自動的に行ない,またあるときには処理をしたあとでイベントを発生させることもある.この動きはプロパティによって異なることもある.たとえば,AutoRedrawプロパティをTrueにすると,WM_PAINTメッセージに対応するPaintイベントは発生しなくなり,Visual Basicが自動的に再描画を行なうようになる.
ところが,メッセージによっては,完全にVisual Basicで無視されてしまうものもある.これは,対応する処理をVisual Basicが自動的に行なってくれるわけでもなく,イベントとしてプログラマが知ることもできないということだ.
こういった処理は意外に多い.たとえば,システム時刻が変更されたとき,デフォルトプリンタが変更されたとき,その他システム設定が変更されたとき,バッテリが少なくなってきたときといったイベントは,Visual Basicから知ることはできない.あるいは,Windows APIを積極的に利用し,システムメニューに独自のメニューを追加したとき,それに対応するイベントを知ることができないといったこともある.
このようなイベントのうちでも,システム系のイベントを知りたいときには,Visual Basic 5.0に附属のSysInfo ActiveXコントロールなどのカスタムコントロールを使うことができる.これは比較的カンタンに使うことができる.
また,もっと汎用なものとして,文化オリエントから販売されているSpyWorks/OCXなどを使うという手もある.SpyWorks/OCXを使いこなすには,Windowsのメッセージやそれに付随するAPIに関するかなりの知識が要求される.
| サブクラス化とは |
|---|
実は,SpyWorks/OCXのやっていることこそ,今回のテーマであるサブクラス化なのである.Visual Basic 5.0では,AddressOf演算子を使ってSpyWorks/OCXのようなサブクラス化が可能になる.
では,サブクラス化とは一体なんなのだろうか? これを理解するためには,もう少々Windowsでのメッセージの流れについて知る必要がある.
図4は,WindowsでのVisual Basicアプリケーションを使ったときの通常のメッセージの流れを示したものだ.先に述べたとおり,システムからVisual BasicにWM_XXXメッセージが送られてくる.この送られる先が,デフォルトウィンドウプロシージャと呼ばれる関数である.
デフォルトウィンドウプロシージャには,実に多くのメッセージ送られてくる.Visual Basicではここで何のメッセージであるかを調べ,適切な処理を行なう.場合によっては,Visual Basicで書かれたイベントを呼び出すかもしれないし,何もしないかもしれない.このように,デフォルトウィンドウプロシージャがメッセージ処理の起点となっているのである.知りたいメッセージが送られてきても,デフォルトウィンドウプロシージャで握りつぶされてしまったら,おしまいなのだ.
そこで,本来ならメッセージが最初に処理される送り先であるデフォルトウィンドウプロシージャを,別のプロシージャに変えてしまおうというのが,サブクラス化と呼ばれる手法なのである.図5は,サブクラス化したときのメッセージの流れである.
この図5からも分かるように,デフォルトウィンドウプロシージャに送られるメッセージを事前に「かっぱらって」きて,自前のウィンドウプロシージャで処理をして,何ごともなかったかのように,本来送られるはずだったところに渡してやるというものだ.こうすれば,Visual Basicに握りつぶされる情報を,あらかじめ取得して適切な処理をしてやることができるというワケである.
このような処理をフック(Hook)と呼ぶことがあるが,サブクラス化はフックとは違う.サブクラス化はあくまでも特定のウィンドウからの「かっぱらい」行為である.そのウィンドウプロシージャのクラスと同じことをやるものを別途作って,そちらに処理をさせてしまうからサブクラスなのである.
実はWindowsには,システム上を飛び交う不特定のウィンドウに対するメッセージをフィルタリングする機能がある.これをフックと呼んでおり,サブクラス化とは区別している.
フック,SetWindowHookExなどのWindows APIで実現可能であり,AddressOf演算子を使えばVisual Basicからでも理論的には実現できる.しかし,Windows上を飛び交うメッセージの量といったら半端ではない.それをフックして処理すると,当然ながらパフォーマンスは低下する.したがって,このような処理をVisual Basicのような処理速度の遅いツールで行なうのは感心しない.
| サブクラス化の手順 |
|---|
サブクラス化の概要がわかったところで,実際の手順を述べよう.
ここでは,もちろんWindows APIを使うことになる.ここで使うのは,SetWindowLongとCallWindowProcのふたつの関数だ.それぞれの概要を表に示す.
表1:SetWindowLong関数
| 構文 | ||
| LONG SetWindowLong(hwnd, nIndex, lNewLong) | ||
| HWND hwnd; /* ウィンドウのハンドル */ | ||
| int nIndex; /* 設定する値のオフセット */ | ||
| LONG lNewLong; /* 新しい値 */ | ||
| 解説 | ||
| SetWindowLong関数は指定されたウィンドウの属性を変更する.また,この関数は指定されたウィンドウの補足ウィンドウメモリ内の指定されたオフセットに32ビット(long)値を設定する | ||
| パラメータ | 説明 | |
| hwnd | ウィンドウを識別する.間接的には,このウィンドウが属するクラスも識別する | |
| 次に示す値はhwndパラメータがダイアログボックスを識別するときに有効である | ||
| 値 | 動作 | |
| DWL_DLGPROC | ダイアログボックスプロシージャの新しいアドレスを設定 | |
| DWL_MSGRESULT | ダイアログボックスプロシージャ内でメッセージが処理する戻り値を設定 | |
| DWL_USER | ハンドルやポインタなどのアプリケーションに対してプライベートな新しい追加情報を設定 | |
| lNewLong | 置き換える値を設定 | |
| nIndex | 変更する値の0から始まるオフセットを指定する.有効な値の範囲は0〜(ウィンドウメモリのバイト数−4.たとえば12バイト以上の補足ウィンドウメモリが指定されたときは8という値が3番目の32ビット整数へのインデックスとなる).または次に示す値のいずれかを指定する | |
| 値 | 動作 | |
| GWL_EXSTYLE | 拡張ウィンドウ スタイルを設定 | |
| GWL_STYLE | 新しいウィンドウ スタイルを設定 | |
| GWL_WNDPROC | ウィンドウ プロシージャ用の新しいアドレスを設定 | |
| GWL_HINSTANCE | 新しいアプリケーション インターフェイス ハンドルを設定 | |
| GWL_ID | ウィンドウの新しいIDを設定 | |
| GWL_USERDATA | ウィンドウに関連付けられている32ビット値を設定.各ウィンドウは,ウィンドウを作成したアプリケーションが使用することを示す,対応する32ビット値をもつ | |
表2:CallWindowProc関数
| 構文 | |
| LRESULT CallWindowProc(wndprcPrev, hwnd, uMsg, wParam, lParam) | |
| WNDPROC wndprcPrev; /* 以前のプロシージャのアドレス */ | |
| HWND hwnd; /* ウィンドウのハンドル */ | |
| UINT uMsg; /* メッセージ */ | |
| WPARAM wParam; /* 第1メッセージ パラメータ */ | |
| LPARAM lParam; /* 第2メッセージ パラメータ */ | |
| 解説 | |
| CallWindowProc関数は指定されたウィンドウプロシージャにメッセージ情報を渡す | |
| パラメータ | 説明 |
| wndprcPrev | 以前のウィンドウプロシージャをさすポインタ |
| hwnd | メッセージを受け取るウィンドウプロシージャを識別する |
| uMsg | メッセージを指定する |
| wParam | メッセージにより異なる追加情報を指定する.このパラメータの内容はuMsgパラメータの値による |
| lParam | メッセージにより異なる追加情報を指定する.このパラメータの内容はuMsgパラメータの値による |
| 戻り値 | |
| メッセージ処理の結果を示す.戻り値の意味は送られたメッセージにより異なる | |
SetWindowLong,CallWindowProcのプロトタイプは次のとおりである(リスト2).
ここではサブクラス化するのに使うから,nIndexはGWL_WNDPROCである.
リスト2:プロトタイプSetWindowLong・CallWindowProc
Declare Function SetWindowLong Lib "user32" _ Alias "SetWindowLongA" _ (ByVal hwnd As Long, ByVal nIndex As Long, _ ByVal dwNewLong As Long) As Long Public Const GWL_WNDPROC = (-4) Declare Function CallWindowProc Lib "user32" _ Alias "CallWindowProcA" _ (ByVal lpPrevWndFunc As Long, ByVal hwnd As Long, _ ByVal msg As Long, ByVal wParam As Long, lParam As Long) As Long |
では,サブクラス化を実現する手順を次に述べよう.
サブクラス化の開始
SetWindowLongは,指定されたウィンドウについての属性を変更するものだ.このとき,
この第3パラメータで,AddressOfが使われることになる.つまり,Visual Basicの標準モジュールに記述した自前のウィンドウプロシージャに,「かっぱらった」メッセージを送ってやるわけだ.
この関数の戻り値には,古いウィンドウプロシージャのアドレスが入ってくる.これは適当な変数に保存しておき,サブクラス化を終了するときに,再びSetWindowLong関数を使って元に戻してやる必要がある.
このように,一般的にはSetWindowLong関数を次のような形で実行して,サブクラス化が開始される.
m_wndprcNext = SetWindowLong _ (hWnd, GWL_WNDPROC, AddressOf WindowProc)
自前のウィンドウプロシージャの記述
これ以後,このアプリケーションに送られてくるメッセージは,自前のデフォルトウィンドウプロシージャに送られてくることになる.
このウィンドウプロシージャは,関数名はともかくとして,引数や戻り値は次のような形のなっていないといけない.
Public Function WindowProc _ (ByVal hWnd As Long, _ ByVal uMsg As Long, _ ByVal wParam As Long, _ ByVal lParam As Long) As Long
あとは,この自前の関数の中で適当な処理をしてやればよい.
しかし,このままこの関数を終了してはいけない.これはあくまでもVisual BasicからVBプログラマに教えてくれないメッセージを知ろうという趣旨であるから,それ以外のものは今まで通りにVisual Basicに処理させたい.もちろん,モノによってはVisual Basicに処理を渡す必要がないものもあるかもしれないが,そうでないものの方が多いわけだ.したがって,ここからデフォルトウィンドウプロシージャを呼び出してやる必要があるのだ.
その作業をしてくれるのが,CallWindowProc関数である.ここからもわかる通り,先ほどの自前で作ったウィンドウプロシージャに渡された内容を,そのまま引数として渡してやればよい.もちろん必要によっては内容を変更してやってもよいだろう.
したがって,一般的にウィンドウプロシージャは次のような形になる(リスト3).
リスト3:ウィンドウプロシージャ
Public Function WindowProc _
(ByVal hWnd As Long, ByVal uMsg As Long, _
ByVal wParam As Long, ByVal lParam As Long) As Long
Select Case uMsg
Case WM_XXX
....
Case WM_YYY
....
Case WM_ZZZZ
....
End Select
WindowProc = CallWindowProc _
(m_wndprcNext, hWnd, uMsg, wParam, ByVal lParam)
End Function
|
サブクラス化の終了
これで,サブクラス化が行なわれるようになったわけだ.
しかし,アプリケーションを終了するときなどには,このサブクラス化を解除して元に戻してやらないと,いろいろおかしなことが起きる.
元に戻すのはカンタンだ.サブクラス化を開始したときに得ていたオリジナルのウィンドウプロシージャのアドレスを指定して,SetWindowLongを呼び出してやればよいのである.
SetWindowLong hWnd, GWL_WNDPROC, m_wndprcNext
|
サブクラス化の応用例1: MCIコマンドの終了通知を得る |
|---|
では,実際にサブクラス化を行なってみよう.
以前の記事では,システム関係のメッセージを得るというものを作ってみた.これは単純で解説用としてはよかったものの,実際には先に述べたSysInfo ActiveXコントロールで実現できるものであるため,あまり面白みはない.
そこで,ここでは別項で紹介した高レベルMCIコマンドの課題として残しておいた,コマンド終了の通知をするnotifyの実現をしてみよう.
ここでは,「マルチメディア自由自在! 高レベルMCIを使う」で紹介したmcitestサンプルプログラムを拡張し,通知イベントの機能を付加する.このプログラムのMCI部分の内容については,該当記事をごらんいただきたい.
通知されるウィンドウの指定
MCIコマンド文字列にnotifyオプション文字列を付加すると,その命令の実行が終了したとき,特定のウィンドウに対して終了イベントが通知される.このウィンドウを指定するには,mciSendString関数の第4パラメータに該当するウィンドウのハンドルhWndを指定してやればよい.
そこで,mSendString関数に,第3パラメータとしてhWndを追加した(リスト4).
リスト4:hWnd
Public Function mSendString _
(s As String, ErrString As String, hWnd As Long) As Long
Dim r As Long
If s <> "" Then ' コマンド文字列が指定されている
r = mciSendString(s, szCmds, LENCMDS, hWnd)
以下略
End Function
|
サブクラス化の開始
デフォルトウィンドウプロシージャのアドレスは,標準モジュールにPrivateとして宣言しておく.
Private lpOrg As Long
Notifyチェックボックスがチェックされると,サブクラス化を開始するSubClassメソッドを呼び出す.この関数では,指定されたウィンドウをWindowProcにサブクラス化する(リスト5).
リスト5:SubClassメソッドを呼び出す
' Start SubClassing Public Sub SubClass(hWndS As Long) lpOrg = SetWindowLong _ (hWndS, GWL_WNDPROC, AddressOf WindowProc) End Sub |
ウィンドウプロシージャ
独自のウィンドウプロシージャでは,システムから送られてくるMM_MCINOTIFYメッセージをもつ.これがコマンドの終了メッセージだが,さらに細かい情報はwParamの値などで知ることができる.たとえば,複数のコマンドが実行され,途中のnotifyは送られなかったといった情報などがある.
ここでは,このような詳細な状況調査はせず,単純に通知メッセージがきたら,フォーム上にあるピクチャーボックスの色を緑色にしている.
この後,すべてのメッセージについて,オリジナルのウィンドウプロシージャにディスパッチしている(リスト6).
リスト6:独自のウィンドウプロシージャ
' SubClassing WindowProc
Public Function WindowProc _
(ByVal hWnd As Long, ByVal uMsg As Long, _
ByVal wParam As Long, lParam As Long) As Long
Dim duml As Long
Select Case uMsg
Case MM_MCINOTIFY
frmMCITest2.picNotify.BackColor = RGB(0, 255, 0)
End Select
WindowProc = _
CallWindowProc(lpOrg, hWnd, uMsg, wParam, lParam)
End Function
|
サブクラス化の終了
Notifyチェックボックスがチェックが外されると,サブクラス化を終了するUnSubClassメソッドを呼び出す(リスト7).
リスト7:UnSubClassメソッドを呼び出す
' Stop SubClassing
Public Sub UnSubClass(hWndS As Long)
Dim lpX As Long
If lpOrg <> 0 Then
lpX = SetWindowLong(hWndS, GWL_WNDPROC, lpOrg)
lpOrg = 0
End If
End Sub
|
これでサブクラス化のコードはOKだ.このプログラムの全ソースと実行結果を次に示す(リスト8-a・b,図6).
この例では,CD Audioデバイスをx1というエイリアスでオープンし,CDドライブのドアを閉じる命令が書いてある.ボタンを押すと,Notifyピクチャーボックスは赤になる.
通知を示すnotifyが指定されているので,ドアが閉じられてCDの情報が読み込まれて再生可能状態になると,このウィンドウに通知メッセージが届き,サブクラス化されたウィンドウプロシージャ内のコードで,Notifyピクチャーボックスは緑になる.
この他にも,演奏が終了したかといった情報など,時間がかかるMCIコマンドの通知には,この機能が使えるととても便利である.
|
サブクラス化の応用例2: システムメニューに独自メニューを追加する |
|---|
ここでは,システムメニューに独自のメニューを追加し,それに対応するイベントを取得してみることにしよう.具体的には,「Incriment」メニューを追加し,これが選ばれるたびに,ラベルに表示された数値がインクリメントされていくというものだ.
順を追って説明していこう.
システムメニューへの独自メニューの追加
システムメニューに限らず,メニュー項目の追加には,Windows APIのAppendMenu関数を使う.
このとき,どのメニューに対してメニュー項目を追加するかを指定するため,システムメニューのハンドルを得るWindows API GetSystemMenu関数を使う.それぞれの関数の概要を次に示す.また,Visual Basicでの宣言を示す(表3・4,リスト9).
表3:AppendMenu
| 構文 | |||
| BOOL AppendMenu(hmenu, fuFlags, idNewItem, lpszNewItem) | |||
| HMENU hmenu; /* メニューのハンドル */ | |||
| UINT fuFlags; /* メニュー項目フラグ */ | |||
| UINT idNewItem; /* メニュー項目ID、 またはポップアップ メニュー ハンドル */ | |||
| LPCTSTR lpszNewItem; /* メニュー項目の内容を指定 */ | |||
| 解説 | |||
| AppendMenu関数はメニューの末尾に新しいメニュー項目を追加する.アプリケーションはこの関数を使ってそのメニュー項目の内容,表示,動作を指定できる | |||
| パラメータ | 説明 | ||
| hmenu | 変更するメニューを識別する | ||
| fuFlags | 新しいメニュー項目の表示および動作を制御するフラグを指定.このパラメータは以下の値の組み合わせである | ||
| MF_UNCHECKED | 項目の隣りにチェックマークを付けない(デフォルト).アプリケーションがチェックマークビットマップ(SetMenuItemBitmaps関数参照)を提供しているときにこのフラグを設定すると,メニュー項目の隣りにチェックオフの状態を示すビットマップが表示される | ||
| idNewItem | 新しいメニュー項目のIDを指定する.あるいはfuFlagsパラメータがMF_POPUPに設定されているときは,ポップアップメニューのメニューハンドルを指定 | ||
| lpszNewItem | 新しいメニュー項目の内容を指定する.lpszNewItemパラメータの意味は,fuFlagsパラメータがMF_BITMAPフラグ,MF_OWNERDRAWフラグ,またはMF_STRINGフラグのいずれを含むかにより次のように異なる | ||
| 値 | 意味 | ||
| MF_BITMAP | ビットマップハンドルを含む | ||
| MF_OWNERDRAW | アプリケーションが供給する32ビットの値を含む.この32ビット値はアプリケーションがメニュー項目に関連する付加的なデータを管理するために使われる.この値はメニューが作成されたときや表示が更新されたときに送られるWM_MEASUREメッセージまたはWM_DRAWITEMメッセージのlparamパラメータが指す構造体のitemDataメンバにある | ||
| MF_STRING | NULLで終わる文字列を指すポインタを含む | ||
| 戻り値 | |||
| 関数が正常に終了した場合はTRUEを返す.それ以外の場合はFALSEを返す.拡張エラー情報を取得するにはGetLastError関数を要使用 | |||
表4:HMENU GetSystemMenu関数
| 構文 | |
| HMENU GetSystemMenu(hwnd, fRevert) | |
| WNDPROC wndprcPrev; /* 以前のプロシージャのアドレス */ | |
| HWND hwnd; /* コントロール メニューを所有するウィンドウのハンドル */ | |
| BOOL fRevert; /* リセットフラグ */ | |
| 解説 | |
| GetSystemMenu関数は,アプリケーションがコントロールメニュー(システムメニュー)にアクセスしてコピーおよび修正することを可能にする | |
| パラメータ | 説明 |
| hwnd | コントロールメニューのコピーを所有するウィンドウを識別する |
| fRevert | 関数の動作を指定する.このパラメータがFALSEのとき,GetSystemMenu関数は現在使用中のコントロールメニューのコピーのハンドルを返す.このコピーは最初はコントロールメニューと同じものであるが修正が可能 このパラメータがTRUEのときにはGetSystemMenu関数はコントロールメニューをリセットしてWindowsのデフォルトの状態にする.このとき以前のコントロールメニューがある場合には破棄される |
| 戻り値 | |
| fRevertパラメータがFALSEの場合,コントロールメニューのコピーのハンドルを返す | |
| fRevertパラメータがTRUEの場合,NULLを返す | |
| 備考 | |
| GetSystemMenu関数を使ってコントロールメニューの独自のコピーを作成しないウィンドウには,標準のコントロールメニューが設定される | |
| コントロールメニューは初期状態でSC_CLOSE,SC_MOVE,SC_SIZEなどのID値をともなう項目をもつ | |
| コントロールメニューのメニュー項目はWM_SYSCOMMANDメッセージを送る | |
| 定義済みのコントロールメニュー項目はすべて0xF000よりも大きいID番号をもつ.このためアプリケーションでコントロールメニューにコマンドを追加するときは0xF000よりも小さいID番号を使用しなければならない | |
| Windowsは状況に応じて標準コントロールメニューの項目を自動的に灰色表示にする.アプリケーションはメニューが表示される前に送られてくるWM_INITMENUメッセージに応答することにより,チェックマーク表示や灰色表示を必要に応じて実行できる | |
リスト9:GetSystemMenuのVisual Basicでの宣言
Declare Function AppendMenu Lib "user32" _ Alias "AppendMenuA" _ (ByVal hMenu As Long, ByVal wFlags As Long, _ ByVal wIDNewItem As Long, ByVal lpNewItem As String) As Long Declare Function GetSystemMenu Lib "user32" _ (ByVal hWnd As Long, ByVal bRevert As Long) As Long |
ここでは,このAPIの使い方を紹介するのは本筋ではないので,単純に実例を示すのみとする.Form_Loadイベントで,セパレータと「Incriment」のメニューを追加している.後からどのメニューが選ばれたかを知るためのIDとして,定数IDM_INCRを定義し,指定している.
Public Const IDM_INCR As Long = 1000 Private Sub Form_Load() AppendMenu GetSystemMenu _ (hWnd, 0), MF_SEPARATOR, 0&, 0& AppendMenu GetSystemMenu _ (hWnd, 0), MF_STRING, IDM_INCR, _ "&Incriment" SubClass End Sub
これだけでシステムメニューにメニュー項目が追加されるが,Visual Basicからはこのメニューが選ばれたかどうかを知るイベントはない.そこで,サブクラス化である.
サブクラス化の開始
Form_Loadイベントの最後では,フォームモジュール内にあるサブクラス化を開始するSubClassプロシージャが呼び出される.実際の処理は次のようにSetWindowLong関数を呼び出している.このとき,デフォルトウィンドウプロシージャのアドレスを,m_wndprcNextとして保存しておく.
Public m_wndprcNext As Long
Private Sub SubClass()
m_wndprcNext = _
SetWindowLong _
(hWnd, GWL_WNDPROC, _
AddressOf WindowProc)
End Sub
ウィンドウプロシージャの処理
自前で作ったウィンドウプロシージャでは,次の処理をする.
グレイ表示をするためには,Windows API EnableMenuItem関数を使う.次に概要および,Visual Basicでの宣言および関連する定数の定義を示す(表5・リスト10).また実際のコードは,リスト11のように書いた.
表5:EnableMenuItem関数
| 構文 | ||
| BOOL EnableMenuItem(hmenu, uItem, fuFlags) | ||
| HMENU hmenu; /* メニューのハンドル */ | ||
| UINT uItem; /* 使用可能、 使用不能、 または灰色表示するメニュー項目 */ | ||
| UINT fuFlags; /* メニュー項目フラグ */ | ||
| 解説 | ||
| EnableMenuItem関数はメニュー項目を使用可能,使用不能,または灰色表示にする | ||
| パラメータ | 説明 | |
| hmenu | メニューを識別する | |
| uItem | fuFlagsパラメータによって決定される使用可能,使用不能,または灰色表示にするメニュー項目を指定する.このパラメータにはメニューバー項目またはポップアップメニュー項目を指定しする | |
| fuFlags | uItemパラメータの解釈を制御し,メニュー項目が使用可能,使用不能,灰色表示のいずれであるかを示すフラグを指定する.このパラメータにはMF_BYCOMMAND,MF_BYPOSITIONのいずれかと,MF_ENABLED,MF_DISABLED,MF_GRAYEDのいずれかを組み合わせたものを指定しなければならない | |
| 値 | 意味 | |
| MF_BYCOMMAND | uItemパラメータがメニュー項目識別子を指定することを示す.MF_BYCOMMANDフラグもMF_BYPOSITIONも指定されない場合,MF_BYCOMMANDフラグがデフォルトの値になる | |
| MF_BYPOSITION | uItemパラメータがメニュー項目の0からの相対位置を指定することを示す | |
| MF_DISABLED | メニュー項目が灰色表示にはされていないが使用不能にされているので選択できないことを示す | |
| MF_ENABLED | メニュー項目が使用可能にされ選択できるように灰色表示から復元されたことを示す | |
| MF_GRAYED | メニュー項目が使用不能にされ選択できないように灰色表示されていることを示す | |
| 戻り値 | ||
| メニュー項目の以前の状態を示す値(MF_DISABLED,lMF_ENABLED,またはMF_GRAYEDのいずれか)を返す.メニュー項目が存在しない場合は0xFFFFFFFFを返す | ||
| 備考 | ||
| メニューバーへの入力の可能,不能について詳しくはWM_SYSCOMMANDメッセージの説明を参照 | ||
| メニュー項目の状態(使用可能,使用不能,灰色表示)は,CreateMenu,InsertMenu,ModifyMenu,およびLoadMenuIndirectの各関数を使用して設定することもできる | ||
| MF_BYPOSITIONフラグを使うときには,アプリケーションで正しいメニューハンドルを指定する必要がある.メニューバーのメニューハンドルが指定された場合,トップレベルメニューの項目(メニューバー内の項目)が対象になる.ポップアップ内の項目,または位置的にネストされたポップアップメニュー内の項目の状態を設定するときにはポップアップメニューのハンドルを指定 | ||
| アプリケーションでMF_BYCOMMANDフラグを指定すると,Windowsは指定されたメニューハンドルにより識別されるメニューよりも下位のすべてのポップアップメニューをチェックする.したがって重複するメニュー項目が存在しないかぎりメニューバーのメニューハンドルを指定するだけで十分である | ||
リスト10:EnableMenuIgtemの宣言
Declare Function EnableMenuItem Lib "user32" _ (ByVal hMenu As Long, ByVal wIDEnableItem As Long, _ ByVal wEnable As Long) As Long Public Const MF_SEPARATOR = &H800& Public Const MF_GRAYED = &H1& Public Const MF_STRING = &H0& Public Const WM_SYSCOMMAND = &H112 Public Const WM_INITMENUPOPUP = &H117 |
リスト11:実際のコード
Public Function WindowProc _
(ByVal hWnd As Long, ByVal uMsg As Long, _
ByVal wParam As Long, ByVal lParam As Long) As Long
Dim hSysMenu As Long
Static c As Integer
Select Case uMsg
Case WM_INITMENUPOPUP
If lParam \ 65535 And &HFFFF& Then 'HIWORD of lparam
hSysMenu = GetSystemMenu(hWnd, 0)
If wParam = hSysMenu Then
EnableMenuItem _
hSysMenu, IDM_INCR, ByVal IIf _
(Form1.WindowState = vbMinimized, MF_GRAYED, 0)
End If
End If
Case WM_SYSCOMMAND
Select Case wParam
Case IDM_INCR
c = c + 1
Form1.lblCount.Caption = CStr(c) & "回目"
End Select
End Select
WindowProc = CallWindowProc _
(m_wndprcNext, hWnd, uMsg, wParam, ByVal lParam)
End Function
|
サブクラス化の終了
Unloadイベント時には,サブクラス化を終了するUnSubClassを呼び出し,オリジナルのウィンドウプロシージャに戻してやる.
Private Sub Form_Unload(Cancel As Integer) UnSubClass End Sub Private Sub UnSubClass() SetWindowLong _ hWnd, GWL_WNDPROC, m_wndprcNext End Sub
実行例
では,実際にこのプログラムを動かしてみよう.
プログラムを実行してシステムメニューを開くと,図7のように「Incriment」メニューアイテムが増えていることが分かる.
このメニューを選ぶと,図8のように選んだ回数がラベルに表示される.
アイコン化されているときにシステムメニューの「Incriment」はグレイ表示されることもわかるだろう.
このプログラムのコードを次に示す(リスト12-a・b).
図7:Incrimentメニュー
|
図8:ラベル表示される
|
このようにサブクラス化を使えば,通常はVisual Basicでは実現できないシステムメニュー項目もカスタマイズすることが可能になったのである.
| デバッグ時の注意 |
|---|
サブクラス化は,ウィンドウのメッセージを「かっぱらう」危険で野蛮な処理である.したがって,Visual Basic環境では,特にデバッグ時にいろいろな不整合が起きることがある.
次に代表的な注意点を挙げる.
ウィンドウプロシージャ内で停止するべからず
この手のプログラムを作れば,当然デバッグが必要になるだろう.しかも,デバッグの場所はいきおいウィンドウプロシージャ内ということになりがちだ.したがって,ウィンドウプロシージャ内にブレークポイントを指定するなどして,停止させたくなることだろう.
ところが,ウィンドウプロシージャ内の,Select Case文やCase文より外側で停止したら最後である.というのも,Visual Basicで行なうべき処理をサブクラス化しているわけだから,そこからステップ実行させたり,継続実行させようとしても,いうことを聞いてくれないのだ.しかも,ブレークポイントを解除することもできない.プログラムの停止はおろか,[ALT]-[F4]での終了すらできない.
やろうとしたことは,すべてサブクラス化されてしまうので,メッセージキューに溜まるばかりなのである.
結局のところ,タスクバーなどからVisual Basicのアイコンを右クリックするなどして,Visual Basic自体を終了させなくてはならない.保存していないコードはすべておジャンである.十分に注意されたい.
アプリケーションの終了は,アプリケーションから
先に述べたように,サブクラス化されたウィンドウをそのまま終了しようとすると,アプリケーションエラーが発生する.ならば,UnloadイベントでUnSubClassすればよいということになるのだが,往々にしてVisual Basicのメニューやツールバーからアプリケーションを終了させてしまうことがある.
れをやると,サブクラス化されたままアプリケーションが終了することになるから,やはりアプリケーションエラーになってしまう.保存していないコードはすべておジャンである.十分に注意されたい.
| 複数インスタンス時の問題 |
|---|
サブクラス化はできた.実際に動かしてみると,Visual Basicの限界を超えたという感が得られて,なかなか嬉しい.
しかし,これですべてがうまくいったわけではない.
次の例を見て欲しい.図9は,先ほどのプログラムをちょっとだけ改造して,ボタンを押すとフォームの新しいインスタンスを起動できるようにしたものだ.具体的には,次のようなコードが付加されている.
Private Sub Command1_Click() Dim frm1 As New Form1 frm1.Show End Sub
実際にこのコードを実行してみると,ふたつめのインスタンスから「Incriment」メニューを実行しても,図10のようにひとつめのインスタンスがインクリメントされてしまう.
しかも,このプログラムを終了しようとすると,図11のようにアプリケーションエラーになってしまう.これはどうしたことだろう?
ここで,サブクラス化の手法をもう一度考えてみてほしい.サブクラス化とは,特定のウィンドウへのメッセージを横取りすることである.ここでは,ふたつのウィンドウがある.ところが,自前のウィンドウプロシージャは,共通の標準モジュールにある.ここからどのようにしてインクリメントしていたかといえば,次のコードだった.
Form1.lblCount.Caption = CStr(c) & "回目"
新しくインスタンシングしたフォームからもこのコードが呼び出されるのだから,当然インクリメントされるのは元となるフォームだけなのである.
さらに悪いことに,オリジナルのウィンドウプロシージャのアドレスを保存しているm_wndprcNextは,標準モジュールにたったひとつだけあるわけだから,サブクラス化から回復させるときに,正常なアドレスに戻すことができない.このため,アプリケーションエラーになってしまうわけだ.
| ActiveXコントロールでの問題 |
|---|
ここではフォームのマルチインスタンス時の問題として扱ったが,この問題はサブクラス化をするActiveXコントロールを作ったときにも起きる.
実はActiveXコントロールでは,それ以前の問題もある.
先ほどの例のように,標準モジュールからForm内のメンバーにアクセスはできるが,同じプロジェクト内にあっても標準モジュールからUserControlモジュールのメンバーにはアクセスできないのだ.これでは,UserControlモジュールからRaiseEventメソッドを実行して,コンテナ側に通知することができない.
そこで,標準モジュール内にCtlObjなどの変数をObject型として宣言し,UserControlモジュールのInitialize時に自分自身をSetしてやる.こうしておけば,標準モジュールからCtlObj.FireEventのようなコードでUserControl内のメソッドを呼び出すことができる.このメソッド内でRaiseEventしてやれば,コンテナにイベントを通知できるというワケだ.
こんな面倒なことをして,やっとフォームの例と同じ土俵に上がれるわけである.
さて,これからが問題だ.
たとえば,複数のフォームに配置したActiveXコントロールであっても,やはり標準モジュールは共通のものであるから,先ほどのフォームとまったく同じ問題が発生するのである.
| インスタンスを区別する手法 |
|---|
マルチインスタンス時の問題点は,次のふたつである.
したがって,これを回避するためには,次の項目を満足すればよい.
次に,それぞれについて回避策を述べよう.
ウィンドウプロシージャ側で呼び出し元のインスタンスを区別できる
ウィンドウプロシージャ側でインスタンスを区別するための材料は限定されている.すなわち,ウィンドウプロシージャの引数がすべてなのである.実際のところ,対象となるウィンドウのハンドルhWndで区別するしかない.
つまり,hWndを元にして,インスタンスの区別,というよりは該当するFormあるいはUserControlのインスタンス内にあるメソッドを呼び出せればOKだ.つまり,hWndからVisual Basicでインスタンスを区別するオブジェクト型(Form型やObject型)に格納されたオブジェクトを得られればよいということになる.
これには,hWndからオブジェクトを得られるように対応した,配列やコレクションオブジェクトを用意する方法がある.しかし,検索速度の点で難がある.メッセージを煩雑に処理する必要があるウィンドウプロシージャでは,このような参照はなるべく避けて,スマートに処理できるようになっていた方がよい.
そこで,ここではウィンドウ情報の入っているエリアの空きを使って,ここにオブジェクトの情報を格納してしまおう.
具体的には,Windows API GetWindowLong/SetWindowLong関数で,GWL_USERDATAを指定して,ユーザーデータ領域を使う方法だ.つまり,ウィンドウごとにある余っているユーザーデータ領域にオブジェクトへの参照を示すポインタを格納しておくのだ.こうしておけば,hWndからオブジェクトを参照できるようになる.
したがって,標準モジュールにあるウィンドウプロシージャはこの判断をして,目的のオブジェクト中にあるメソッドを呼び出すだけになる.そのメソッド内で,本来のウィンドウプロシージャのコードが走るようにすればよいというわけだ.
デフォルトウィンドウプロシージャのアドレスは,インスタンスごとに管理する
従来は標準モジュール内でPublic,すなわち事実上グローバル変数として使われていたm_wndprcNextを,フォームモジュールごとに用意してやればよい.ひとつめの問題の解決策と合わせて使えば,これはPrivateな変数として用意すればよいだろう.
| hWndからオブジェクトを参照するコード |
|---|
次に,これらの解決策の具体的なコードを書いてゆこう.
ウィンドウプロシージャアドレスの宣言
オリジナルのウィンドウプロシージャのアドレスは,フォームのPrivate変数として用意する.
Private m_wndprcNext As Long
サブクラス化の開始と終了
サブクラス化を開始するとき,念のためUnSubClassをしておく.自分自身のオブジェクトを示すポインタMeをObjPtr関数を使って得て,SetWindowLong関数でユーザーデータ領域に書き込んでおく.
サブクラス化した独自のウィンドウプロシージャは,標準モジュールにあるfrmProc関数である.
UnSubClassには若干のチェックが入っているが,基本的にはユーザーデータエリアをクリアすること以外に違いはない(リスト13).
リスト13:
Public Const GWL_USERDATA = (-21)
Private Sub SubClass()
UnSubClass
Debug.Assert GetWindowLong _
l(hWnd, GWL_USERDATA) = 0
SetWindowLong hWnd, GWL_USERDATA, ObjPtr(Me)
m_wndprcNext = SetWindowLong _
(hWnd, GWL_WNDPROC, AddressOf frmProc)
End Sub
Private Sub UnSubClass()
If m_wndprcNext Then
SetWindowLong hWnd,_
GWL_WNDPROC, m_wndprcNext
SetWindowLong hWnd, GWL_USERDATA, 0&
m_wndprcNext = 0
End If
End Sub
|
独自のウィンドウプロシージャ
標準モジュールにあるfrmProc関数では,GetWindowLong関数(表7)でユーザーエリアからオブジェクトのポインタを得る.これをWindows API RtlMoveMemory関数を使ってForm1型オブジェクトfrmにオブジェクトポインタとしてコピーする.
こうして得られたfrmオブジェクトのWindowProcメソッドを呼び出す.呼び出した後には,Form1型オブジェクト変数frmを初期化しておく(リスト14).
リスト14:
Declare Function GetWindowLong Lib "user32" _
Alias "GetWindowLongA" (ByVal hWnd As Long, _
ByVal nIndex As Long) As Long
Declare Sub CopyMemory Lib "kernel32" _
Alias "RtlMoveMemory" _
(pDest As Any, pSource As Any, ByVal ByteLen As Long)
Public Function frmProc _
(ByVal hWnd As Long, ByVal uMsg As Long, _
ByVal wParam As Long, ByVal lParam As Long) As Long
Dim frm As Form1
Dim pObj As Long
pObj = GetWindowLong(hWnd, GWL_USERDATA)
CopyMemory frm, pObj, 4
frmProc = frm.WindowProc(hWnd, uMsg, wParam, lParam)
CopyMemory frm, 0&, 4
End Function
|
フォームモジュールにあるWindowProcでは,以前とまったく同じ処理を書けばよい.ただし,このプロシージャはプロジェクト内での参照可能にすべきものだから,PublicではなくFriendとして作成する.これは,ActiveXコントロールとしたときを考えていただければわかるだろう.
Friend Function WindowProc _ (ByVal hWnd As Long,_ ByVal uMsg As Long,_ ByVal wParam As Long, _ ByVal lParam As Long) As Long
表7:GetWindowLong関数
| 構文 | ||
| LONG GetWindowLong(hwnd, nIndex) | ||
| HWND hwnd; /* ウィンドウのハンドル */ | ||
| int nIndex; /* 取得する値のオフセット */ | ||
| 解説 | ||
| GetWindowLong関数は指定されたウィンドウに関する情報を取得する.指定されたウィンドウの補足ウィンドウメモリにある指定されたオフセット位置の32ビット(long)値も取得する | ||
| パラメータ | 説明 | |
| hwnd | ウィンドウおよび間接的にはウィンドウに属するクラスを識別する hwndパラメータがダイアログボックスを識別するときには次の値も利用可能 | |
| 値 | 意味 | |
| DWL_DLGPROC | ダイアログボックスプロシージャのアドレスを指定 | |
| DWL_MSGRESULT | ダイアログボックスプロシージャ内で処理されるメッセージの戻り値を指定 | |
| DWL_USER | ハンドルやポインタなどアプリケーション固有の補足情報を指定 | |
| nIndex | 取得する値の0から始まるオフセットを指定する.有効な値の範囲は0〜(補足ウィンドウメモリのバイト数−4.たとえば12バイト以上の補足ウィンドウメモリが指定されたときは8という値が3番目のlong整数へのインデックスとなる).または次の値のいずれかになる | |
| 値 | 意味 | |
| GWL_EXSTYLE | 拡張ウィンドウスタイルを取得 | |
| GWL_STYLE | ウィンドウスタイルを取得 | |
| GWL_WNDPROC | ウィンドウプロシージャのアドレスを取得 | |
| GWL_HINSTANCE | アプリケーションインスタンスのハンドルを取得 | |
| GWL_HWNDPARENT | 親ウィンドウのハンドルを取得 | |
| GWL_ID | ウィンドウの識別子を取得 | |
| GWL_USERDATA | ウィンドウに対応するlong値を取得.各ウィンドウはそれぞれアプリケーションがそのウィンドウを作成するときに使用を意図するlong値をもっている | |
| 戻り値 | ||
| 関数が正常に終了した場合は要求された32ビット値を返す.それ以外の場合は0を返す.拡張エラー情報を取得するにはGetLastError関数を使う | ||
| 備考 | ||
| RegisterClass関数とともに使用されるWNDCLASS構造体のcbWndExtraメンバに0以外の値を指定して補足ウィンドウメモリを予約すること | ||
このようにして作成した,マルチインスタンスに対応したサブクラス化プログラムのリストおよび実行例を示す(リスト15-a・b,図12).
図12:マルチインスタンスに対応したサブクラス化プログラム
リスト15-a:SubClass.frm (マルチインスタンス対応版)
リスト15-b:SubClass.bas(マルチインスタンス対応版)
| AddressOfを使いこなすと |
|---|
さて,ここまですべてを理解できた方であれば,ここから先はもっと楽しいことが待っている.それは,COMにおけるVtableのオーバーライドだったり,さらに意表をつくようなワザの応酬かもしれない.
しかし,最初に述べたように,これで本当に生産性が上がるのかというと,やはり疑問である点も多い.こんなに難しいことをしなくても,買ってきたOCXでやればよいのだし,その方がおかしなことが起きないだろう.
また,最初からVisualC++などのツールを使った方が早いという場面も多そうだ.
しかし,これはこれでよいのだ.何といっても,Windowsの中身をいじっているぞと実感できるHackingは楽しいのだ.
|
サンプルプログラムのダウンロード (ダウンロード後ディレクトリ付きで解凍してください) |