ホーム 上へ 進む

3.1 ウィンドウと Window Control とメッセージの処理

VCL のようなウィンドウシステム用のフレームワークにおいて最も基本的な部分はシステムが管理しているウィンドウと オブジェクトをどう結び付けるかという点です。

例えば TEdit のインスタンスは Windows の"Edit" ウィンドウに対応しています。VCL の利用者は TEdit のインスタンスをまるで "Edit" ウィンドウであるがごとく扱うことが出来ます。しかし、本当はこれは VCL が作り出した幻想であって、TEdit のインスタンスは VCL で定義されたオブジェクトであり、"Edit" ウィンドウは Windows が管理している別のものです。VCL では、この TEdit ようなオブジェクトを Window Control と呼びます(VCL では TEdit 等は TWinControl の派生クラスです)。この節では、Window Control をウィンドウに見せかけるための仕組みについて解説します。

3.1.1 リンク

Window Control は Object Pascal のオブジェクトに過ぎません。これをウィンドウに見せかけるために VCL は3つの仕掛けを設けています。

  1. Window Control のメソッドでウィンドウを操作できるようにする。
  2. ウィンドウハンドルから Window Control を得られるようにする。
  3. ウィンドウに来たメッセージを Window Control のメソッドで処理できるようにする。

1 はオブジェクトからウィンドウを操作する、つまりオブジェクトからウィンドウへ向いた結合です。一方 2 はウィンドウからオブジェクトを得る、つまりウィンドウからオブジェクトへの向きの結合です。3 は 2 を使って実現出来るのですが、VCL は高速化のために 別の仕掛け(Object Instance)を使います。

以上の3点を作成することを「リンク」を確立すると呼ぶことにします(この言葉は Windows++ という本から借用しました)。 Figure 3.1-1

図3.1-1 リンク

3.1.2 Window Control からウィンドウへの結合の作成

Window Control からウィンドウへの結合を作るのは比較的簡単です。要は、Window Control 内に、対応するウィンドウのウィンドウハンドルを置いておけばよいわけです。Window Control(TWindow Control の派生クラス)は CreateWindowHandle(Protected virtual) メソッドでウィンドウを作成します。CreateWindowHandle メソッドは Windows API の CreateWindowEx を呼び出します。この時、ウィンドウハンドルを、Window Control のメンバ FHandle にセットすれば良いわけです。VCL は CreateWindowEX の戻り値(ウィンドウハンドル)をセットすることもありますが、たいていの場合は、もっとうまい方法でウィンドウハンドルを取得しセットします(後述)。

こうして取得されたウィンドウ ハンドルは、Window Control のいろいろなメソッドで、Window Control に対応するウィンドウを指定するのに使われます。

3.1.3 ウィンドウから Window Control への結合の作成

ウィンドウから Window Control への結合の作成は「プロパティ」を使います。この「プロパティ」は Object Pascal の プロパティ とは別のもので、ウィンドウにユーザ情報を記憶させる仕組みの一つです。Windows の SetProp API でウィンドウに情報を名前を付けてセットできます。また GetProp API でウィンドウから情報を名前で検索して取り出すことが出来ます。 VCL は ウィンドウ生成時にSetProp API を使って、ウィンドウに Window Control のオブジェクト参照をセットします。 VCL の Control ユニット内で定義されている 公開関数 FindControl は、この情報を使ってウィンドウハンドルを Window Control のオブジェクト参照に変換します。

3.1.4 ウィンドウから Window Control へのメッセージの通知

ウィンドウから Window Control へメッセージの通知は、基本的には上述のウィンドウに「プロパティ」として記録された Window Control のオブジェクト参照を使えば出来ます。しかし、「プロパティ」は速度が比較的遅いので、VCL は別の技術を使ってウィンドウから Window Control へメッセージを送ります。

Windows の各ウィンドウはウィンドウプロシージャのアドレスを内部に持っています。ウィンドウにメッセージが到着すると、Windows はこのアドレスの指すメッセージ処理ハンドラにメッセージを渡します。

Windows はメッセージ処理ハンドラが普通のプロシージャであると思っていますが、我々(VCL)が欲しいのは、オブジェクトのメソッドの呼び出しです。これを実現するには、いったんウィンドウからのメッセージを単純なプロシージャで受け、そこからオブジェクトのメソッドを呼び出す仕掛けが必要です。VCL では小さなコード片を使ってこれを実現しています。Borland ではこれを Windows の Procedure Instance に引っ掛けて Object Instance と呼んでいるようです。

Object Instance は、わずか 13 バイトのコード片で、Forms ユニット内で管理されています(Delphi 6 では Classes ユニットで管理されるようになりました)。Forms ユニット内には(Delphi 6 ではClassesユニット内には) MakeObjectInstance と FreeObjectInstance という関数があり、MakeObjectInstance にメソッドポインタを渡すと Object Instance を作成してくれます。また FreeObjectInstance は Object Instance を削除してくれます。

Object Instance は2個の補助コードとともに動作します(下図参照)。

Figure 3.1-2

図3.1-2 Object Instance

Object Instance 内にはメソッドポインタが格納されています。Object Instance が呼び出されると、Object Instance は補助コードを Near Call します。補助コードは戻り値を POP するため、 ECX レジスターには メソッドポインタへのポインタが入ります。補助コードは function StdWndProc を呼び出します。 StdWndProc は ECX の指すメソッドポインタを見て、メソッドポインタの示すオブジェクトのメソッドを呼び出します。

この Object Instance をウィンドウのウィンドウプロシージャとしてウィンドウにセットすれば、ウィンドウから Window Control のメソッドへ直接メッセージを渡すことが可能になります。

以上で、基本的な仕掛けの説明は終わりです。次ではさらに、ウィンドウが生成される過程で、リンクがどのように設定されてゆくかを説明します。

3.1.5 ウィンドウの作成の第一段階

ウィンドウの作成は二段階で行われます。第一段階は Window Control のコンストラクタで、第二段階は CreateHandle メソッドです。このことから、判るように、ウィンドウと Window Control の寿命は同じでは有りません。Window Control の方が長いのです。クラスライブラリの中には、これを出来る限り一致させるようになっているものもありますが、 VCL はそうでは有りません。VCL では Window Control の寿命の中で、ウィンドウが何度も作り直されたりすることもあります。これは、ウィンドウの属性の変更の自由度を本来のウィンドウのものより高くするためです。

第一段階のコンストラクタでは Object Instance が作られます。TApplication など、ごくわずかな例外を除いて、Window Control の MainWndProc メソッドにジャンプする Object Instance が作られ、Window Control の メンバ FObjectInstance にセットされます。TApplication は Application.WndProc にセットされますが、これに関しては別の節であらためて説明します。

3.1.6 ウィンドウの作成の第2段階

第二段階の CreateHandle メソッドは HandleNeeded メソッド等から呼び出されます。CreateHandle は CreateWnd を呼び出し、CreateWnd は CreateParams と CreateWindowHandle を呼び出します。CreateHandle, CreateWnd, CreateParams, CreateWindowHandle はいずれも Virtual なメソッドなので、Window Control のクラス毎に微妙に動作が違いますが、基本的な動作は以下の通りです。

CreateHandle

ウィンドウを作成し(CreateWnd)、ウィンドウの「プロパティ」にウィンドウコントロールのオブジェクト参照をセットします。

CreateWnd

ウィンドウの作成のためのパラメータを作り(CreateParams)、ウィンドウ作成のためのさまざまなセットアップを行った後、ウィンドウを作成します(CreateWindowHandle)。

CreateParams

ウィンドウクラスのパラメータと CreateWindowEx のパラメータをセットアップします。

CreateWindowHandle

CreateWindowEx を呼び出してウィンドウを作成し、得られたウィンドウハンドルを Window Control のメンバ変数 FHandle にセットします。

Figure 3.1-3

図3.1-3 ウィンドウの作成の第二段階

3.1.7 CreateParams メソッド

CreateParams メソッドは CreateWnd メソッドから呼び出され、ウィンドウの作成に必要な大部分のパラメータを設定します。パラメータは二群に分かれていて、一つはウィンドウクラスを登録するためのもの、もう一つは CreateWindowEx のパラメータです。

Figure 3.1-4

図3.1-4 CreateParams が設定する TCreateParams レコードの内容

CreateParams は以下のようにパラメータを設定します。但し、これはあくまでデフォルト値であり、Window Control(TWinControl の派生クラス)は必要に応じて CreateParams を Override してこれらを適宜変更しています。以下の設定は新たに Window Control を設計する時の参考としてください。

パラメータ名設定内容
ウィンドウのキャプションCaption プロパティ が設定される。
ウィンドウのスタイルWS_CHILD or WS_CLIPSIBLINGSが基本。
Window Control が他のコントロールのコンテナならば WS_WS_CLIPCHILDREN、
Enable プロパティ = FALSE なら WS_DISABLED、TabStop プロパティ = True なら WS_TABSTOP が加わる。
ウィンドウの拡張スタイル0がセットされる
ウィンドウの位置Left, Top プロパティ
ウィンドウの幅/高さWidth/Height プロパティ
親ウィンドウのウィンドウハンドルParent プロパティ の示すウィンドウのウィンドウハンドルをセット
Window Creation Data へのポインタNil
クラススタイルCS_VREDRAW + CS_HREDRAW + CS_DBLCLKS
ウィンドウプロシージャへのポインタDefWindowProc
クラスの拡張バイト数0
ウィンドウの拡張バイト数0
クラスアイコンのハンドル0
クラスカーソルのハンドルLoadCursor(0, IDC_ARROW)
クラスブラシのハンドル0
クラスメニューの名前Nil
クラス名Window Controlのクラス名をそのまま用いる

Window Control が TEdit のような既存のウィンドウクラスを用いる場合は、上記のウィンドウクラスのパラメータの部分は、既存のクラスのものに置き換わります。例えば、TCustomEdit の場合、TCustomEdit の CreateParams メソッドは GetClassInfo API で "EDIT" クラスのウィンドウクラスレコードの内容を読み取り、上記のウィンドウクラスのパラメータ部分にコピーします。これはいわゆる「スーパークラス化」という手法で、既存のクラスを流用しつつ新しいクラスを作るのに使われる手法です。

CreateParams メソッドは、以上のようにウィンドウの基本的な属性を設定するので、既存の Window Control を継承して新しいコントロールを作成する時に、Override する第一候補です。例えば TEdit の表示を右寄せに変えたコントロールを CreateParams を Override するだけで簡単に作成出来ます。

3.1.8 CreateWnd メソッド

次に説明するのは CreateWnd です。CreateWnd は CreateParams を呼んでパラメータを設定した後、ウィンドウを作成するための準備作業を行います。

まず、CreateWnd は後述のサブクラス化に備えるため、ウィンドウクラスのパラメータの「ウィンドウプロシージャへのポインタ」を Window Control のメンバ変数 FDefWndProc にセーブします。そして、ウィンドウクラスパラメータの「ウィンドウプロシージャへのポインタ」に InitWndProc というプロシージャのポインタをセットします。InitWndProc の機能については後述します。

次に、CreateWnd は CreateParams が作成したクラスを登録します。クラス名は必ずその Window Control のクラス名と同じになります。以前に同名のクラスが登録済で有った場合で、ウィンドウプロシージャが InitWndProc で無い場合は、それを削除してから改めて登録し直します(InitWndProc で無いケースがあるとは思えませんが、おそらくこれは万が一のためのガードルーチンです)。

最後に、CreationControl というグローバル変数に自分自身のオブジェクト参照をセットして、CreateWindowHandle を呼び出します。CreationControl は後で InitWndProc が使います。 CreateWindowHandle は CreateParams と CreateWnd が設定したパラメータで CreateWindowEx を呼び出し、取得したウィンドウハンドルを Window Control のメンバ変数 FHandle にセットします。

3.1.9 InitWndProc

VCL は InitWndProc 内で、作成されたウィンドウのサブクラス化を行います。なぜ、このようなルーチンを使ってサブクラス化を行うのでしょうか? 既にコンストラクタで、Object Instance のポインタは判っています。単純に考えると、ウィンドウクラスのパラメータ内の「ウィンドウプロシージャへのポインタ」に Object Instance を代入してクラスを登録し、ウィンドウを作成すればよいように思えます。しかしこの方式には少し問題があります。

最初からObject Instance を使う場合、ウィンドウの生成直後から全てのメッセージが Object Instance に設定されたメソッドに飛んできます。CreateWindowEx API が終了する前にもウィンドウには多数のメッセージが来るので、Window Control のメッセージハンドラはそれも処理しなくてはなりません。しかし、この段階では Window Control 内の FHandle にウィンドウハンドルがセットされていないので、メッセージハンドラはメッセージ内のウィンドウハンドルを利用しなくてはなりません。これではもし、CreatWindowEx から戻る前に、メッセージ処理の中でコントロールの FHandle を使うメソッドが呼び出されると困ったことになってしまいます。対処として、メッセージハンドラが最初にメッセージを受けた時に FHandle がセットされているかをチェックして、セットされていなければ FHandle をセットするようにすれば問題はなくなります。しかしこうすると、メッセージハンドラは全てのメッセージで FHandle をチェックするようになってしまいます。これは無駄以外の何者でもありません。

VCL ではこの問題を InitWndProc を使用することで巧妙に避けています。VCL の方式では、ウィンドウが作成されると最初のメッセージは InitWndProc に来ます。InitWndProc はグローバル変数 CreationConrol を見て、作成されたウィンドウに対応する Window Control のインスタンスのオブジェクト参照を得ます。IntiWndProc は Window Control の インスタンスの FHandle にウィンドウハンドルを代入し、ウィンドウのウィンドウプロシージャを Window Control のインスタンスの FObjectInstance の値に SetWindowLong API を使って変更します(下のコード参照)。つまり、ウィンドウをサブクラス化します。

SetWindowLong(HWindow, GWL_WNDPROC,
    Longint(CreationControl.FObjectInstance));
その後、インスタンスのオブジェクト参照をウィンドウの「プロパティ」に登録し、CreationControl をクリアした後、FObjectInstance の示すウィンドウプロシージャにジャンプします。

要するに、ウィンドウのメッセージは最初のメッセージだけが、InitWndProc で処理され、2番目のメッセージからは FObjectInstance の指すメソッドに直行するようにすることで、前述の不要なオーバヘッドから逃れています(下図参照)。

Figure 3.1-5

図3.1-5 InitWndProc の役割

お気づきかと思いますが、ウィンドウの「プロパティ」に Window Control のオブジェクト参照を設定することと、 FHandle にウィンドウハンドルを設定することは、それぞれ、CreateHandle, CreateWindowHandle でも行っています。重複して行っているのは、ウィンドウを作成した後、つまり、CreateWindowEx API が制御を戻してきた後にサブクラス化するケースがあるからです。これは、CreateWindowEx が制御を戻す前のメッセージを Window Control で処理しなくてもいいケースで使われます。例えば、MDI フレームウィンドウが MDI クライアントウィンドウを作る時、この方法が使われます。

3.1.10 メッセージの流れ

3.1.9 までで、ウィンドウのメッセージがどうやって Window Control のメソッドに渡されるかが判りました。ここではさらに、メッセージが Window Control でどのように処理されるか、その概略を説明します。メッセージの流れの概略は下図のようになります。

Figure 3.1-6

図3.1-6 メッセージの流れ

いくつかの例外を除いてほとんどの Window Control はメッセージを最初に受け取るメソッドを TWinControl.MainWndProc に設定します。MainWndProc はさらに、WndProc を呼び出します。MainWndProc は非常に小さなルーチンで、以下のようなコードになっています(この程度のコードなら引用しても構わないでしょう)。

procedure TWinControl.MainWndProc(var Message: TMessage);
begin
  try
    try
      WndProc(Message);
    finally
      FreeDeviceContexts;
      FreeMemoryContexts;
    end;
  except
    Application.HandleException(Self);
  end;
end;

MainWndProc の役割は上のコードからも明らかなように、以下の3点です。

  1. WndProc メソッドを呼び出してメッセージの処理を行う。
  2. メッセージ毎に、処理終了後、全てのコントロールの デバイスコンテキスト (より正確には GetDC API で得た DC で Canvas がロックされていないもの)を解放し、全てのビットマップ(TBitmap)の Device Context(Memory Device Context) を破壊し、Windows 95 系列の Windows における貴重な GDI リソースの無駄遣いを防止します
  3. メッセージ処理内で捕捉されなかった例外を Application 変数の HandleException メソッドに渡す。

Window Control の WndProc メソッドは Virtual なので、Window Control の種類によって処理内容が異なります。また、フォーカス関連メッセージの処理や、非Window Controlに渡すメッセージを生成するなどさまざまな処理が行われますが、ここでは最も基本的な部分だけを説明します。メッセージ毎のさまざまな処理内容は、3.2 以降で説明します。

WndProc はいくつかの処理の後、ほとんどの場合 TControl.WndProc にメッセージを渡します。TControl.WndProc はさらにいくつかの重要な処理を行った後、TObject の Dispatch メソッドを呼び出します。

TObject の Diaptch メソッドは「メッセージ処理メソッド」を呼び出す特殊なメソッドです。「メッセージ処理メソッド」とは、message 指令でメッセージインデックスを指定されたメソッドのことです。「メッセージ処理メソッド」は dynamic なメソッドで、Dispatch は指定されたメッセージインデックス(メッセージ番号)でオブジェクトに「メッセージ処理メソッド」が有るかを探し、もしあれば、その「メッセージ処理メソッド」を呼び出します。もし無ければ DefaultHandler という Virtual なメソッドを呼び出します。

これで末端のメッセージ処理ルーチンまでメッセージが届いたわけですが、まだ、デフォルトのメッセージ処理ルーチンが(FDefWndProc)がどのように呼び出されるかを説明していません。この説明をする前に、「メッセージ処理メソッド」のInherited の意味を確認しておきます。

「メッセージ処理メソッド」内で使われる Inherited は特別な意味があります。通常 Inherited にはメソッド名を指定しますが、「メッセージ処理メソッド」内の Inherited はメソッド名が指定され無い場合、上位から継承した、同じメッセージインデックスのメッセージを処理する「メッセージ処理メソッド」を表します。そして、もし上位にメソッドが無い場合は DefaultHandler を呼び出します。DefaultHandler は TObject で定義されている virtual なメソッドで、TObject.DefaultHandler は何もしません。DefaultHandler メソッドは TWinControl で Override されています。いくつかの Window Control では DefaultHandler が Override されていますが、その DefaultHandler は上位のDefaultHandler を呼びだすように実装されています。

「メッセージ処理メソッド」は、上位の処理メソッドを呼び出す場合、Inherited で呼び出します。「メッセージ処理メソッド」が意図的に上位の処理にメッセージを渡したくない時を除き、メッセージはより上位の処理に渡されてゆきます。そして最後には該当する「メッセージ処理メソッド」が無くなり、DefaultHandler メソッドが呼び出されます。

通常 Default Handler は上位から継承した DefaultHandler メソッドを呼び出すため、最後には、TWinControl.DefaultHandler が呼び出され、これが FDefWndProc(DefWndProc プロパティ) を呼び出します。

Figure 3.1-7

図3.1-7 メッセージ処理メソッドのメッセージの流れ

「メッセージ処理メソッド」が意図的にデフォルトのメッセージ処理ハンドラ(DefWndProc プロパティ)や上位の継承した「メッセージ処理メソッド」にメッセージを渡したくない時は、処理を Exit; で終了させるか、Inherited を呼び出さないようにします。

3.1.11 メッセージ処理メソッドを書く時の注意事項

3.1.10 でメッセージ処理メソッドの処理の流れを説明しましたが、図3.1.7 から明らかなように、メッセージ処理メソッドの処理の順番は、まずメッセージを受け取ったコントロールのメッセージ処理メソッド、次にその継承元のメッセージ処理メソッドという具合に数珠つなぎに呼び出され、最後にはコントロール の DefWndProc に渡るという順で処理されます。この順番を変えることは出来ません。もちろんメッセージ処理メソッドの中で

procedure TMyControl.WNChar(var msg: TWMChar);
begin
  Inherited;
  {処理}
end;

という形で書けば、継承先での処理を後回しにすることは出来ますが、この場合、コントロールの DefWndPorc より前に処理を挟み込むことが出来ません。

例えば、エディットコントロールの文字処理を変更する場合を考えてみましょう。エディットコントロールの DefWndProc には Windows の標準の Edit コントロールのメッセージ処理へのポインタが入っており、TWinContorl の WMChar メソッドには OnKeyPress イベントハンドラを呼び出す処理が入っています。OnKeyPress イベントはコントロールの他の処理に先立って呼び出されなければならないため、処理を追加するには、TWinControl.WMChar と DefWndProc(Edit コントロールのデフォルトの処理)の間に新しい処理を挟まなければなりません。これはメッセージ処理メソッドの override では不可能です。

DefaultHandler を Override して処理を挟み込むは可能ですが、DefaultHandler には全てのメッセージが集まってきますから、伝統的な Case で分岐するメッセージ処理を書かなくてはならず不便です。

このような時のために、TWinControl.WMChar メソッドの処理は、DoKeyPress メソッドと KeyPress メソッドを呼び出す構造になっています。KeyPress メソッドは dynamic なので、これを Override することにより、継承先のコントロールが簡単に WM_CHAR メッセージの処理に介入できるようになっています。VCL が標準で用意しているこのような処理に具体的にどのようにして処理は追加すべきかはメッセージの種類によって異なるので、別の節で個別に扱います。

以上の説明で明らかとは思いますが、もしあなたが新しいコントロールを書き、そのコントロールが、既存のメッセージ処理を変更するなら、メッセージ処理メソッドを 書く前に、メッセージ処理の Override 用メソッドが用意されていないかをソースコードをよく見て確かめる必要があります。また、コントロールに新たなメッセージ処理メソッドを新設する場合は、必要に応じて上記の KeyPress の様な Override 用のメソッドを用意し、メッセージ処理メソッドからそれを呼び出す構造にしてください。そうすれば、継承先でメッセージ処理を変更しやすくなります。

3.1.12 まとめ

3.1.1 から 3.1.11 で、Window Control を Windows のウィンドウに見せかける仕組みを説明しました。

Window Control とウィンドウ間の三個の結合
  1. Window Control のインスタンス内のウィンドウハンドル
  2. ウィンドウの「プロパティ」にセットされた Window Control のオブジェクト参照
  3. ウィンドウのウィンドウプロシージャに設定された Object Instance)
によって Window Control はウィンドウであるかのように動作することが出来ます。
メッセージは TWinControl.MainWndProc で処理されますが、MainWndProcはメッセージ処理毎に 全てのコントロールのデバイスコンテキスト (GetDC API で得たCanvas がロックされていないもの)を解放し、全てのビットマップ(TBitmap)の デバイスコンテキスト(Memory Device Context) を破壊します。こうすることでデバイスコンテキストの数が必要最小限に保たれ、Windows 95 系列の Windows における貴重な GDI リソースの無駄遣いを防止します
また、全ての補足されなかった例外が、 Application.HandleException に渡されます。
メッセージは WndProc を経由して「メッセージ処理メソッド」に渡されます。その後、必要ならば、DefaultHandler メソッドを経由して、デフォルトのメッセージ処理ハンドラが呼び出されます。
コントロールのメッセージ処理を継承先で変更する場合は、メッセージ処理を継承するための専用メソッドが用意されている場合があります。また新規にコントロールを作る場合、必要に応じてメッセージ処理継承用のメソッドを用意すべきです。

ホーム 上へ 進む

inserted by FC2 system