Visual Basic 6.0 中級講座
VB6対応

 

Visual Basic 中学校 > VB6 中級講座 >

9.サブクラス化

 

今回は「サブクラス化」というパワフルな技を紹介します。「サブクラス化」を使うと普通にVBを使っていてはできないようないろいろなことができるようになります。今回は「右クリック禁止」処理を通して サブクラス化を説明します。

この回の要約

・Windowsはメッセージを使ってイベントをアプリケーションに通知している。

・メッセージを自分で直接処理すれば普通VBにはできないような処理ができる。

・メッセージを自分で処理するのは危険→知識のない人は要注意!

・メッセージを処理するにはWindowプロシージャを作る。

・Windowプロシージャになるには標準モジュール上でPublic Functionで宣言し、既定の引数と戻り値(の型)を装備する必要がある。

・Windowプロシージャを指定するにはSetWindowLong関数を指定する。


この回の使えるサンプル

・ 指定したテキストボックスでの右クリックを禁止する→サンプルA

 

1.メッセージ

 

Windowsでは各アプリケーションおよびシステム(Windows)は「ッセージ」という仕組みを媒介としてお互いの情報を共有しています。

たとえば、みなさんが作成したVBのアプリケーションでボタンがマウスでクリックされるとClickイベントが発生しますが、マウスでクリックされてからClickイベントが発生するまでの仕組みは次のようになっています。

まず、ボタンがクリックされたことを検知するのはVBで作られたアプリケーションではなくWindowsです。その証拠に普通の人はボタンがクリックされたかどうか判断するプログラムを書いたことはないでしょう。Windowsが自動でやってくれているからです。 Windowsはボタンがクリックされたことを検知するとアプリケーションにそのことを通知します。このような通知 がメッセージです。

メッセージの種類は無数にあって、ボタンがクリックしたことを知らせるメッセージは WM_LBUTTONUP です(正確にはこのメッセージはマウスの左ボタンが開放されたことを表します)。VBで作ったアプリケーションの場合はWM_LBUTTONUPを受信すると自動的に該当するボタンのClickイベントが呼び出されます。

Visual Studioに付属しているSpy++というツールを使用すると実際のメッセージの様子をリアルタイムで見ることができます。

今、「自動的に」と書きましたが、これはVBが処理してくれるので自分ではこの部分を書くことはないと言う意味です。確かに 「WM_LBUTTONCLICKメッセージが送られてきたらClickイベントを呼べ」などというコードを書いたことのある人はあまりいないでしょう。このようにWindowsからメッセージを受け取って何かの処理をする部分のことを「Windowプロシージャ」と言います(「Windowsプロシージャ」ではありませんよ。念のため)。

VB以外の言語ではこのWindowプロシージャを自分で書かなければいけない場合もありますが、VBではWindowプロシージャ は自動的に生成されるので通常は自分で書く必要はありません。Windowプロシージャは各ウィンドウ(テキストボックスなど多くのコントロールはウィンドウです)が1つずつ持っています。

 

このようなWindowsの仕組みを利用するといくつかのとても強力なプログラミングを行うことができます。

まず、VBであっても特別な方法を使ってWindowプロシージャを自分で記述することができます。Windowプロシージャを自分で書くと、すべてのメッセージに対して独自の処理を行うことができ、本来VBでは検知できないようなイベントを処理したりすることができたり、逆に処理されるべきメッセージを処理しないようにしたりすることができるようになります。

この技術のことを「サブクラス化」と呼びます。サブクラス化の例はすぐ後で紹介します。

もう1つの強力な技術として「フック」が挙げられます。フックとは特定のメッセージが発せられたことを通知してもらう仕組みのことです。たとえば、キーボード関連のメッセージによる通知を受け取るためのキーボードフックや、マウス関連のマウスフックなどいくつかの種類があります。こう書くとサブクラス化の機能縮小版のようにも思えてしまいますが実際にはちょっと違います。

まず、サブクラス化とは異なり生のメッセージをそのまま処理するわけではありません。また、自分以外のアプリケーションに向けて発せられたメッセージを取得することができます。

フックを行うにはAPI関数のSetWindowsHookExを使用します。例としては初歩的なものではありますがテクニック3 メッセージボックスを使い倒すで紹介していますので興味がある方は参照して下さい。

 

なお、サブクラス化もフックも必要がなくなった時点で所定の関数を呼び出して解除する必要があります。解除しないままプログラムを終了させてしまうと、Windowsにとっては登録されているメッセージの送り先が突然見つからなくなってしまうという状態なってしまし、VBはいきなり強制終了させられてしまいます。

私もフック中にいつもののりで停止ボタンを押してプログラムの実行を停止させてしまうことがよくありますが、これをやってしまうとプログラムを保存する間もなくVBごと終了してしまいます。

また、Windowプロシージャやフックプロシージャの内部でどのように処理を書くべきかはMSDNライブラリなどを熟読してよく理解して置いてください。これらのプロシージャで処理を誤るとやはりアプリケーションが強制終了してしまう危険がありますし、ひどいときにはシステム全体に影響がおよぶ可能性もあります。

 

2.右クリック禁止

 

では、サブクラス化を利用してマウスの右クリックを禁止にするアプリケーションを作成しましょう。具体的にはテキストボックス上で右クリックができないようにします。VBで普通に作るとテキストボックス上では右クリックによってコピーや貼り付けが行えるのですがこれをできなくしてしまおうということです。

方法としてはWindowプロシージャで右クリックを表すメッセージ WM_CONTEXTMENU を受け取ったら何も処理を行わないようにしてしまいます。

まず、Windowプロシージャを作りましょう。Windowプロシージャの名前は何でもいいのですが2つの決まりを守らなければなりません。1つ目は「標準モジュール上でPublic Functionで宣言して作る」こと。2つ目は「引数」です。具体的には標準モジュールに次のような関数を作ってください。この関数が空のWindowプロシージャになります。

Public Function WindowProc(ByVal hWnd As Long, ByVal uMsg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long

End Function

当たり前ですがこれを書いただけで自動的にこれがWindowプロシージャになるわけではありません。この関数をWindowプロシージャにするためにはこの関数をWindowプロシージャとして指定する必要があります。その方法はもう少し後で説明します。

ところで、この関数をこのままからの状態でWindowプロシージャに指定するととてもやばいことになります。なぜならこの関数は何もしないからです。先ほど説明したようにWindowプロシージャはWindowsからメッセージを受け取って適切なイベントを発生させる役割があるのです。しかし、この関数は何もしていません。

それにしても右クリックを禁止したいだけなのに、他のあらゆるメッセージも処理しなければならないとは大変です。そこで便利な関数が用意されていてその関数を使うと 自分に必要ないメッセージの処理をもともとのWindowプロシージャにまわすことができます。

つまり、WM_CONTEXTMENU以外のメッセージが来た場合にはもともとのWindowプロシージャに任せることができるわけです。

その部分まで記述するとWindowプロシージャは次のようになります。

Public Declare Function CallWindowProc Lib "user32" Alias "CallWindowProcA" (ByVal lpPrevWndFunc As Long, ByVal hWnd As Long, ByVal Msg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
Public Function WindowProc(ByVal hWnd As Long, ByVal uMsg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long

    Call CallWindowProc(DefaultProc, hWnd, uMsg, wParam, lParam)

End Function

ここで追加されているCallWindowProc関数が元々のWindowプロシージャを呼び出しています。しかし、このままでは変数が宣言されていないなどまだまだ不十分です。

そこで次は一挙に完成版をお見せしましょう。

繰り返しになりますが、以下のコードは「標準モジュール」に記述してください。フォームに記述したのでは動作しません。

'□API関数
Public Declare Function SetWindowLong Lib "user32" Alias "SetWindowLongA" (ByVal hWnd As Long, ByVal nIndex As Long, ByVal dwNewLong As Long) As Long
Public Declare Function CallWindowProc Lib "user32" Alias "CallWindowProcA" (ByVal lpPrevWndFunc As Long, ByVal hWnd As Long, ByVal Msg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long

'□SetWindowLongで使用
Private Const GWL_WNDPROC = -4

'□メッセージ
Private Const WM_CONTEXTMENU = &H7B '右クリック

'□コレクション すべてウィンドウハンドルがキー
Dim colDProc As Collection '現在サブクラス化されているコントロールの元のWindowsProcのアドレス
'■WindowProc
'■機能:メッセージを横取りする。
'■備考:この関数はコールバック関数なので定義を変えてはいけない!

Public Function WindowProc(ByVal hWnd As Long, ByVal uMsg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long

    Dim DefaultProc As Long

    Select Case uMsg

        Case WM_CONTEXTMENU '右クリック
            Exit Function

    End Select

CONTINUE:
    '引当のWindowProcへメッセージを回す。
    DefaultProc = colDProc(CStr(hWnd))
    WindowProc = CallWindowProc(DefaultProc, hWnd, uMsg, wParam, lParam)

End Function
'■BeginSubClass
'■機能:サブクラス化を開始する。

Public Sub BeginSubClass(oControl As Control)

    Static bAlready As Boolean
    Dim DefaultProc As Long

    If Not bAlready Then
        Set colDProc = New Collection
        bAlready = True
    End If

    'サブクラス化実行
    DefaultProc = SetWindowLong(oControl.hWnd, GWL_WNDPROC, AddressOf WindowProc)

    '元のWindowProcのアドレスを保存
    colDProc.Add DefaultProc, CStr(oControl.hWnd)

End Sub
'■EndSubClass
'■機能:サブクラス化を終了します。

Public Sub EndSubClass(oControl As Control)

    Dim Ret As Long
    Dim DefaultProc As Long

    'WindowProcのアドレスを元に戻す。
    DefaultProc = colDProc(CStr(oControl.hWnd))
    Ret = SetWindowLong(oControl.hWnd, GWL_WNDPROC, DefaultProc)
    colDProc.Remove CStr(oControl.hWnd)

End Sub

■サンプルA

いつも私の方針では解説している事柄を明確にするために余分な部分は削った例を紹介することが多いのですが今回は実用的な部分も含めて紹介しています。

このサンプルを動作させるにはフォームにコマンドボタンを2つとテキストボックスを1つ貼り付けて次のようにプログラムしてください。

Private Sub Command1_Click()

    Call BeginSubClass(Text1)

End Sub
Private Sub Command2_Click()

    Call EndSubClass(Text1)

End Sub

実行の際はプログラムを終了させる前に必ずサブクラス化を解除するためにCommand2をクリックしてください。危険です!!

 

3.解説

 

さて、このサンプルには3つの関数があります。

1つめのWindowProc関数は先ほどから紹介しているWindowプロシージャです。2つ目のBeginSubClass関数はサブクラス化を開始します。3つ目の関数はサブクラス化を終了させます。

まず、BeginSubClass関数から説明しましょう。今回はテキストボックスの右クリック禁止が目的なので複数のテキストボックスをサブクラス化するためにサブクラス化されているテキストボックスを記憶する仕組みが必要です。また、サブクラス化を解除するためにもともとのWindowプロシージャを覚えておく必要もあります。

そのためサブクラス化を実行する関数SetWindowLongを呼び出した後に元々のWindowプロシージャを対象となっているコントロールのハンドルをキーにしてコレクションcolDProcに追加します。元々のWindowプロシージャはSetWindowLong関数の戻り値を見ればわかります。なお、この場合Windowプロシージャはメモリ上のアドレスとして表現されています(このあたりが分からない人は今回はあまり気にしないでください)。

サブクラス化を実行する関数SetWindowLongは3つの引数をとります。順に「対象となるWindow(この場合はコントロール)のハンドル」、「定数GWL_WNDPROC」、「新しいWindowプロシージャ」を指定します。VBでは第1引数のハンドルとはhWndプロパティのことです。そのためこのhWndプロパティがないコントロール(たとえLabelImage)などについてはサブクラス化はできません。 これは別にマイクロソフトが怠慢でhWndプロパティを用意していないのではなく、LabelImageがウィンドウではないことからくる当然の区別なのです。

第2引数のGWL_WNDPROCはこれからWindowプロシージャの付け替えをするというフラグです。このSetWindowLong関数自体はサブクラス化以外にもいろいろな使い道があるのでこのような指定が必要なのです。GWL_WNDPROCは宣言部で宣言されています。3つ目の引数は新しいWindowプロシージャです。先ほど書いたようにプロシージャの指定は「アドレス」で行います。VBではプロシージャのアドレスを取り出すにはAddressOf関数(演算子)を使用します。このAddressOfはVB5から導入された関数(演算子)なのでVB4以前ではサブクラス化はできません。

SetWindowLong関数の戻り値は元々のWindowプロシージャ(のアドレス)を表しています。必ず受け取っておいてください。

次にWindowプロシージャであるWindowProc関数を説明します。サブクラス化されている間はそのテキストボックスがメッセージを受信するたびに必ずこのWindowProc関数が自動でWindowsから呼び出されます。

送られてくるメッセージは第2引数uMsgで判断できます。今回はWM_CONTEXTMENUメッセージを処理したいのでSelect Caseでそれ以外のメッセージは処理しません。関数の一番最後でCallWindowProc関数が呼び出されているのに注意してください。 何も処理されなかったメッセージはこのCallWindowProc関数で元々のWindowプロシージャに送られるのです。元々のWindowプロシージャはVBが自動で生成したものなのであらゆるメッセージにうまく対応してくれます。

このCallWindowProc関数の第1引数は解説が必要でしょう。第1引数はもともとのWindowプロシージャ(のアドレス)を指定するのですが、ここではサブクラス化を開始したときにコレクションに追加しておいたアドレスを取り出してからこの関数に渡しています。コレクションはコントロールのハンドルをキーにしているのでうまい具合にアドレスを取り出すことができます。現在メッセージを受信しているコントロールのハンドルはWindowプロシージャの第1引数hWndで分かります。

最後にEndSubClass関数です。この関数はサブクラス化を終了させるのですが「サブクラス化を終了させる」とは元々のWindowプロシージャを新しいWindowプロシージャに指定することを指します。そこでもう一度SetWindowLong関数を呼び出しているわけです。元々のWindowプロシージャ(のアドレス)はコレクションに追加してあるので取り出すことは容易です。それが終わるとコレクションからそのコントロールの項目を削除します。

どうでしょうか?

 

4.補助的な解説

 

以上の解説は流れを意識して書いてあるので、それ以外の部分でたりないと思う部分を補足しておきます。

まず、WindowProc関数の第3引数wParamと第4引数lParamですが、この2つの引数は受信するメッセージによって意味が変わります。VBでのイベントプロシージャの引数と同じですね。VBでは同じイベントプロシージャと言ってもたとえばMouseDownイベントとKeyDownイベントでは引数が違うでしょう。それと同じです。したがってこの2つの引数の意味を知るには個々のメッセージに関する知識が必要になります。

WindowProc関数内でWM_CONTEXTMENUメッセージを判定するのにIfではなくSelect Caseと使っているのは拡張性を考えてのことです。1つのメッセージを処理すればいいのであればIfで十分なのですが、多分将来このサンプルを基にして(コピーして)他のメッセージを処理したくなる日が来るでしょう。複数のメッセージを判定するにはSelect Caseの方が便利です。

さらに、CONTINUE: というラベル(念のために書きますが、「ラベル」はGoto文で使用します。ラベル自体には何の意味もありません。)ですが、実はこのサンプルではこのCONTINUE:はあってもなくても同じです。これも拡張性を考えての処理なのです。というのはメッセージの処理には 次の3種類があるからです。

1つ目は「すべて自分で処理する」です。今回の例はこれにあたります。すべて自分で処理するのでCallWindowProc関数を呼び出す必要はありません。そのため今回はSelect CaseCase節にExit Functionがあるのです。2つ目は「処理しない」です。この場合はなにもしないでCallWindowProc関数を呼び出します。そして、今回は登場しなかった3つ目が「自分で処理を追加して、後は元々のWindowProcにまわす」というパターンです。この場合はSelect CaseCase節に処理を書いた後でCallWindowProc関数を呼び出します。そこでCallWindowProc関数の前にCONTINUE:を挿入してExit Functionの代わりにGoto CONTINUE と書くことでいつでもCallWindowProc関数が呼び出せるようにしてあるのです。

BeginSubClass関数のbAlreadyについて。この変数はサブクラス化が初めてか既になされているかを判断します。始めてのサブクラス化の場合にはコレクションをNewで実体化する必要があるからです。また「初めてか」「初めてじゃないか」は記憶しておく必要があるのでこの変数はDimではなくStaticで宣言されているのです。

「コレクション」についても説明が必要でしょうか?コレクションは「配列」と拡張した大変便利な機能です。詳しくは初級編第23回をご覧ください。

 

5.Spy++

 

アプリケーションとWindowsがメッセージを使ってどのように通信しているかはSpy++というツールを使うと分かります。このツールを使うとたとえばマウスの真ん中のボタンをぐりぐりした時にはどんなメッセージが送られるのかが分かります。このようにしてあなたのアプリケーションに新しいメッセージに対する処理を埋め込むことができるようになるわけです。

Spy++がどうやったら入手できるのかよくは知りませんが前年ながらフリーウェアでないことは確かです。私の場合はVisual Studio 6.0のEnterpriseエディションを買ったらついていました。

 

6.最後に

 

「右クリックを禁止するだけでなんと面倒くさい!」と思われたでしょうか?確かにこれだけのためでしたらそう考えるのも無理はありません。けれどこの手法をマスターすれば今後はあらゆるメッセージに対応可能なのです。今回紹介しているサンプルをコピーして処理したいメッセージに関するコードを追加する作業はそれほど大変ではないでしょう。

Windowsから送られてくるメッセージの詳細はMSDNライブラリに記されています。リンクが貼ってありますので興味のある方は訪れてみてください。でも、英語の解説なのでちょっとつらいです。