[ クラス | フォーム | 配列、動的配列 | 文字列 | Delphi の格言 | IDE の小技 | エッセンス ]

up

エッセンス

ボタンはなぜボタンに見えるか

Windows でGUIアプリを作る場合、Window というオブジェクトだけがメッセージを受け取ることができて、それを処理することにより、すべてのプログラムはできている、とだいたい言えると思います。描画についてもそのとおりで、フォームがフォームらしく、ボタンがボタンらしく見えるのも、そのウィンドウが描画を要求するメッセージに応答して、GDI関数を実行して、描画している結果です。VCLプログラマはこのことをあまり意識することはありませんが、VCLのコードのどこかで、あるいは、Windows のコントロールのコードのどこかで、実際に描画コードを実行しているのです。例えば、Form を描画しているコードの実行を阻止したらどうなるでしょうか。試してみましょう。

 type
   TForm1 = class(TForm)
   private
     { Private 宣言 }
   public
     procedure WMPaint(var Msg:TWMPaint);message WM_PAINT;
     procedure WMEraseBkgnd(var Msg:TWMEraseBkgnd);message WM_ERASEBKGND;
   end;

 var
   Form1: TForm1;

 implementation

 {$R *.DFM}

 { TForm1 }

 procedure TForm1.WMEraseBkgnd(var Msg: TWMEraseBkgnd);
 begin
   //
 end;

 procedure TForm1.WMPaint(var Msg: TWMPaint);
 begin
   //
 end;

 end.

実行してみると、クライアント領域の描画が行なわれていない不思議な見かけになります。枠やタイトルバーなどの非クライアント領域の描画は、 WM_NCPAINT というメッセージの処理で行なわれているので、今回は阻止していないので正常に描かれます。メニューも非クライアント領域なので正常に描かれます。このように、WM_PAINT や WM_ERASEBKGND(背景を描く)が来たときだけ応答すれば、見かけを維持するためには十分です。これは、どのようなときに送られてくるかといえば、手前にあったウィンドウがなくなってしまったときや、最小化から復帰したときなどです。平行移動のときはウィンドウズが面倒を見てくれるので、WM_PAINT は送られてこないようです。また、マウスポインタの下になっていた部分の描画もウィンドウズが自動的に再描画するようです。またコードでも、意図的に再描画を行なうことができます。これに相当するのは、VCLの TControl の Update、Repaint、Refresh メソッドなどです。

標題についてなんですが、TButton は、Win32 の 'BUTTON' をカプセル化したものなのです。ですから、ボタンとしての外観は、VCLの中のコードではなく、ウィンドウズを構成するどれかのDLLのコードが描画しているわけです。マウスでボタンを押すとへこみますが、これは、WM_LBUTTONDOWN メッセージに応答して、へこんだ絵を描画しているのです。TStringGrid は、 TWinControl の派生クラスですが、ウィンドウズの標準コントロールではなくVCLのコードで作られていますので、描画もVCLのなかのコードで行なわれています。

では、TLabel や TPaintBox、TSpeedButton、TImage などのコントロールは、ウィンドウではないのにどうして自分を WM_PAINT に同期して描画できるのでしょうか。これらのコントロールは、TGraphicControl クラスの派生クラスであり、ウィンドウをつくりません。しかし、その Parent プロパティーは必ず TWinControl であり、ウィンドウです。VCLの TWinControl の派生クラスは、自分の上に乗っかっている TGraphicControl を知っているので、再描画矩形に含まれている位置にある子コントロールに WM_PAINT に相当するメッセージを転送するのです。上記で、TPaintBox を除くコントロールは、この転送メッセージに応答して、自分自身をそれらしく描画しているわけです。

TPaintBox と TImage の相違

TPaintBox は、親の TWinControl の派生クラスの特定の矩形領域の描画機能をカプセル化したものです。それ自身は、なにも描画しませんが、OnPaint イベントにユーザが描画コードを書くと、ボタンがいつもボタンとして描画されているのと同様に、永続的な描画ができます。OnPaint イベントではなく、たとえば Button1Click イベントでも描画できますが、この描画による絵は、次の WM_PAINT メッセージが来たときに消えてしまいます。このような、非同期の描画は、 WM_PAINT とは何の関係もありません。たんに、相当するピクセルの色を変更しているだけです。これに対して、TImage.Canvas に描いた絵は消えませんね。中村さんによると TImage は、それが保持しているビットマップの「虫眼鏡つきの覗き窓」だそうです。TImage は、自分自身への描画要求に対して、それが保持しているビットマップを表示するのです。この表示は、プログラマがコードを書かなくても WM_PAINT に自動的に同期して行なわれます。つまり、予め表示する絵のデータを持っているわけですね。TPaintBox では絵としての描画をもっているのではなく、OnPaint でイベントが起こるたびにプログラマが書いた描画関数を実行しているわけです。

このように考えると、GUIアプリケーションは、デスクトップに自分自身を描画するために、膨大なコードが実行されていることになりますね。VCLをつかって、メモ帳をつくったとして、そのために1000行のコードを書いたとしても、じつはその外観を維持するためにその何倍もの(他人が書いた)コードが実行されているわけですね。 [delphi-ml:60058]

top

Button1 を押すと何故 Button1Click が呼び出されるか

Delphi の IDE で Form1 に Button1 を配置して、実行時に Button1 をマウスでクリックすると Button1Click( ) が実行されます。これは、どんな仕組みなのでしょうか? わたしなりに D3.1 のソースを見て、調べてみましたのでここに書かせてください。

コンポーネントパレットの Standard や Win32 のページにあるほとんどのコンポーネントは、Win32 がOSとして供給している標準コントロールやコモンコントロールの機能を ObjectPascal のコードで包み込んで使いやすくした物です。これらは、メニューや TImageList や TLabel などを除くと、ほとんど子ウィンドウとしてのウィンドウ(コントロール)を作ります。ウィンドウにはウィンドウ関数というOSから呼び出される関数があって、これにコードを書くとことによりメッセージの処理が行われ、ボタンはボタンらしく、リストボックスはリストボックスらしく動作します。OSから供給されているコントロールはこのウィンドウ関数が公開されていなくて、ユーザはそれに直接コードを書くことはできません。しかし、コントロールに何か重要な出来事が起こると、標準コントロールの場合は親ウィンドウに WM_COMMAND、コモンコントロールの場合は WM_NOTIFY を送って、そのことを知らせます。スクロールバーとトラックバーは例外で、親ウィンドウに WM_VSCROLL か WM_HSCROLL メッセージを送ります。また、コードでコントロールを操作したいときは、各コントロールにそれぞれ定義されているメッセージを SendMessage( ) で送信する事により遠隔操作します。

さて、VCLを使わないで MainWindow と Button1 を作ることができますが、この場合は、Button1 を押すと、MainWindow のウィンドウ関数に WM_COMMAND が送られてきます。

 WM_COMMAND 

 wNotifyCode = HIWORD(wParam); // notification code 
 wID = LOWORD(wParam);         // item, control, or accelerator identifier 
 hwndCtl = (HWND) lParam;      // handle of control 

このとき、wNotifyCode には

 { Button Notification Codes }
 const
   BN_CLICKED   = 0;
   BN_DBLCLK    = 5;
   BN_SETFOCUS  = 6;
   BN_KILLFOCUS = 7
  

のうち、BN_CLICKED = 0 が設定されており、 wID には Button1 のID(16ビットの数値)、hwndCtl には Button1 のウィンドウハンドルが設定されています。 MainWindow のウィンドウ関数ではこの WM_COMMAND を受け取って Button1 がクリックされたことを知り、その処理コードを実行します。と、いうふうに、事態は単純です。しかし、VCL の場合は、簡単にウィンドウを作ったり、イベントハンドラが書けたりするようになっていて非常に便利ですが、そのかわり VCLは裏で大きな仕事をしています。Form1 の上の Button1 をクリックすると、 Win32 の標準コンロールの動作として、親である Form1 ウィンドウに、上記と同じように WM_COMMAND を送ります。VCLでは、ウィンドウ関数をインスタンス化するためにウィンドウ関数が MakeObjectInstance() 関数により MainWindowProc() になっていて、これに TMessage の形で届けられます。

 { Generic window message record }

  PMessage = ^TMessage;
  TMessage = record
    Msg: Cardinal;
    case Integer of
      0: (
        WParam: Longint;
        LParam: Longint;
        Result: Longint);
      1: (
        WParamLo: Word;
        WParamHi: Word;
        LParamLo: Word;
        LParamHi: Word;
        ResultLo: Word;
        ResultHi: Word);
  end;

Form1 は MainWndProc(var Message: TMessage); で受け取って、自分の WindowProc() に WindowProc(Message); として渡します。 WindowProc は、デフォルトでは WndProc なので、WndProc で受け取り、自分自身に Dispatch(Message) します。ここで、ObjectPascal のクラスインスタンスにメッセージが渡った訳です。そして、procedure WMCommand(var Message: TWMCommand); message WM_COMMAND; でこれを捕獲します。

 TWMCommand = record
     Msg: Cardinal;    // 今の場合は WM_COMMAND
     ItemID: Word;     // IDE が自動的に付けたID
     NotifyCode: Word; // BN_CLICKED = 0;
     Ctl: HWND;        // Button1 のハンドル
     Result: Longint;  // 処理結果(通常ゼロ)
   end;

次にここで Message.Ctl を参照して FindControl( ) によりそのハンドルを持つ TWinControl を探し、見つかったらそのコントロールの Perform( ) により Perform(Msg + CN_BASE, WParam, LParam); として、ようやく Button1 の WindowProc()に CN_COMMAND が渡されます。

 const
   CN_BASE              = $BC00;
   CN_COMMAND           = CN_BASE + WM_COMMAND;

そして同じように、Button1 の WndProc() → Dispatch() → procedure CNCommand(var Message: TWMCommand); message CN_COMMAND; としてクラスインスタンスの中で捕獲されます。このメソッドの中で Message.NotifyCode が BN_CLICKED であるならば Click メソッドが呼ばれ、ついにイベントハンドラ OnClick が ( TControl.Click から )呼び出されます。

まとめると、VCLではコントロールから親ウィンドウに SendMessage() で送られる WM_COMMAND は、親ウィンドウが CN_COMMAND として送り返すことにより、クラスインスタンスで捕獲して、イベントハンドラを呼び出すことができるようになっています。他のコントロールの場合も同様です。

VCLでは上記のようにメッセージの流れを追跡してゆくと非常にややこしいことになりますが、そのことを意識しないようにうまく隠蔽することにより、とても使いやすいクラスライブラリとなっています。しかし、VCLはメッセージ処理の全てをイベントやプロパティー、メソッドとして公開しているわけではありません。 ちょっと便利な機能を付け足すには、やはりメッセージ処理周りの知識は必要だと思うのです。[delphi-ml:61329]

top

TApplication.ProcessMessages とは何をするものか

D5のヘルプには、「ProcessMessages メソッドは,Windows がメッセージキューを処理できるようアプリケーションの実行を一時的に停止します。」とあります。これは、誤解されそうな文章ですね。ソースを見る限りでは、上記文章で<Windows が>というのは、<このスレッドが>にすべきです。このメソッドを呼び出した時点で、ProcessMessages は何をするのかというと PeekMessage() をループで呼び出して、SendMessage() で送られたメッセージの処理、およびメッセージキューを見て空になるまで

 TranslateMessage(Msg);
 DispatchMessage(Msg);

を繰り返します。これらの一回の実行は TApplication.ProcessMessage( ) メソッドによって行なわれ、 TApplication.ProcessMessages はこれを while ループで呼び出しているだけです。

TApplication.ProcessMessages を実行ループにいれると「ビジーループ」を回避できるか

これは、はせがわさんの回答にありましたように、PeekMessage() を繰り返しているわけですから、ビジーループを回避できません。言い換えると、TApplication.ProcessMessages を実行ループにいれても、他のスレッドにタイムスライスを与えるような効果はありません。ビジーループを避けるためには Sleep() と一緒に使うべきですね。「ビジーループ」については、[delphi-ml:65484] [delphi-ml:65486] [delphi-ml:65488] でのわたしと高岡さんのやり取りがあります。

TApplication.HandleMessage では

TApplication.HandleMessage を実行ループにいれると「ビジーループ」を回避できるのですが、これは WaitMessage() API を実行するからです。しかし、これはメッセージキューにメッセージが入るまで復帰しませんから、本来のループ処理も停止してしまいます。これは、プログラマの本意ではないでしょうから、ビジーループを回避するために TApplication.ProcessMessages のかわりに HandleMessage をつかうことはできません。これをすると、本来の処理ができなくなります。

TApplication.ProcessMessages を実行ループにいれるときの注意事項

長い繰り返し処理中でも再描画や中断のためにメッセージに応答しなければならないときには、TApplication.ProcessMessages を頻繁に実行しなければなりません。かつ、ビジーループを避けるためには Sleep() とペアで使うことも必要です。そのほかに、 (1) いま実行中のループ処理が、このメッセージ処理によって再び実行されないように(再入が起こらないように)、関係するボタンやメニュー項目の Enabled を false にするなどの対策が必要です。 (2) いま実行中のループ処理中にアプリケーションを終了されては困るときは、終了できないように関連するメニュー項目、ボタン、Xボタンを使用不可にするなどの対策が必要です。

結局

TApplication.ProcessMessages はその時点で溜まっているメッセージを処理しましょう、ということですから、そのメッセージ処理にはあらゆる内容を想定しなければなりません。再入防止は最低限必要ですし、そのほか、ユーザはログアウトしたり、シャットダウンしたりするかもしれません。このように、本来の処理の途中でメッセージ処理を許すような場合は細心の注意が必要であり、TApplication.ProcessMessages はまさしくそのようなメソッドです。

アプリケーションに関連するあらゆるメッセージ処理は、メッセージループによって、メッセージを配送したり、実行タイミングを決められたりしています。これは、SendMessage() で送られたメッセージの場合も同様です。TApplication.ProcessMessages をループで実行するコードは、本来のメッセージループから制御を奪って、そのコードでメッセージループの代わりをするのと同じです。すべてのイベントハンドラの復帰もその中に帰って来ます。

私見では、Button1Click のイベントハンドラの最初のコードに Application.ProcessMessages を書くことは意味がありません。なぜなら、 Button1 がクリックされる直前までメッセージループが回っており、その結果、Button1 が親である Form1 に WM_COMMAND を送った結果が実行され、 OnClick イベントハンドラが実行されるからです。その仕組みについては、「Button1 を押すと、なぜButton1Click が呼び出されるのか」[delphi-ml:61329]に書きましたが、文章にすると長いのですが実行時間は多分 1ms 以下でしょう。ですから、Button1 を押して、OnClick の Application.ProcessMessages が実行される時点までの 1ms 以下の時間内に溜まったメッセージを処理することにどんな意味があるのでしょう?これに意味があるとすると、すべてのイベントハンドラに入れる必要があります。

溜まったかもしれないメッセージを処理するために Application.ProcessMessages を呼び出すのではなく、メッセージをためないように長い処理などでそれを呼び出すべきではないでしょうか。 [delphi-ml:65937]

top

[ クラス | フォーム | 配列、動的配列 | 文字列 | Delphi の格言 | IDE の小技 | エッセンス ]


up

更新日:2005-02-12