The Delphi Bug List

Entry No.
509
VCL - 一般 - コントロール - TControl
TControl.WM_CancelMode はマウス状態を正しく扱わない
1.02 2.01 3.0 3.01 3.02 4.0 4.01 4.02 4.03 5.0 5.01 6.0 6.01 6.02 Kylix 1.0
Gotcha Gotcha Gotcha Gotcha Gotcha Gotcha Gotcha Gotcha Gotcha Gotcha Gotcha Gotcha Gotcha Gotcha N/A
解説
Reported by Max Nilson
標準の WM_CANCELMODE メッセージの処理は、Windows がマウスアップメッセージを扱う場合の欠陥を修正しようと試みていますが、それに失敗しています。
以下の Delphi 2.0 および Delphi 3.02 の Controls.pas からの抜粋を良く考えてください:
procedure TControl.WMCancelMode(var Message: TWMCancelMode);
begin
  inherited;
  if MouseCapture then
  begin
    MouseCapture := False;
    if csLButtonDown in ControlState then Perform(WM_LBUTTONUP, 0,
      $FFFFFFFF);
  end;
end;
ここで Delphi プログラマが試みていることは、新しいウィンドウを生成するマウスダウンに対応するマウスアップが、元のウィンドウではなく、新しいウィンドウに送られてしまうという Windows の問題を修正することです。
残念ながらこの作者は、DefWindowProc が WM_CANCELMODE を受け取ると全てのマウスキャプチャを開放するという文書化された振る舞いを知らなかったのです。そのため、この判断が派生コントロールで直接発生した場合、常に失敗します。これは、任意のアプリケーションでこの begin end ブロック内にブレークポイントを設定して実行することで確認できます。たとえドロップダウンや MDI ウィンドウなどであっても、決してブレークポイントに止まることはありません。
Delphi 関係者の誰かが問題に気づいたのでしょう。Delphi 4.02 では、コードが以下のように修正されています:
procedure TControl.WMCancelMode(var Message: TWMCancelMode);
begin
  inherited;
  if MouseCapture then
  begin
    MouseCapture := False;
    if csLButtonDown in ControlState then Perform(WM_LBUTTONUP, 0,
      Integer($FFFFFFFF));
  end
  else
    Exclude(FControlState, csLButtonDown);
end;
この修正により ControlState は正しく保守されます(これは良いことです)。しかし彼らは、この問題を発生させている本当のバグを完全に見逃しています!
私はこれは gotcha だと思います。なぜなら、正しいモード処理を必要とするコードを扱う人はどのみち WM_CANCELMODE を処理する必要があるためですが、マウスアップが期待通りに届かない理由を発見するまで全てのプログラマに混乱を引き起こすことは、やや予想外だからです。
Latest update of this entry: 1998-12-09
本家 The Delphi Bug List のエントリーはこちら
The Delphi Bug List 日本語訳 へ