Delphi Programming / Object Pascal
サイトのトップページへ リンクのページへ ロゴマーク
[掲載 2020年08月02日] [更新 2020年08月27日] Delphi 一般・その他
マウスカーソルのコントロール領域外への移動を検出
動作確認等 Windows 7 U64(SP1) + Delphi XE5(UP2) Pro VCL-32
[整理番号 - 0610]







マウスカーソルのコントロール領域外への移動を検出


マウスカーソルが、TMemo 等のコントロールの領域外に移動した場合、その移動を検出するためのサンプルです。これには OnMouseLeave イベントが利用できます。継承クラスの場合は、CM_MOUSELEAVE メッセージが利用できます。
本ページでは、対象のコントロールとして TMemo を使用しています。

マウスカーソルのフォーム上での位置が分かれば、マウスカーソルが、特定のコントロールの領域外へ移動したかの検出に利用できます。そのためには、マウスカーソルが対象のコントロールの領域内にあるか、領域外にあるかを知るための、フラグとしての変数が必要です。OnMouseLeave イベントを利用すればその必要がありません。






01_OnMouseLeave イベントによる領域外への移動検出


TControl の OnMouseLeave イベントを使用する例です。このイベントは名前の通り、コントロールの領域外にマウスカーソルが移動すると発生します。


図1
TMemo の外側にマウスカーソルが移動したらテキストを追加描画

リスト1
//=============================================================================
//  Memo1 の OnMouseLeave イベント処理
//  マウスカーソルがコントロールの領域外に移動すると発生するイベント
//  マウスがキャプチャされている状態 (マウスボタン押下状態 ) の時は発生しない
//  その場合、マウスのボタンを離すとこのイベントが発生する
//=============================================================================
procedure TForm1.Memo1MouseLeave(Sender: TObject);
begin
  Memo1.Lines.Add(InttoStr(Memo1.Lines.Count) + '  Leaved from Memo1 !!');
end;


[備考]
Delphi 7 以前のバージョンには OnMouseLeave イベントが実装されていません。その場合は、継承クラスを作成して次項のサンプルのようにメッセージを処理します。あるいは、コントロールをサブクラス化してメッセージを処理します。





02_CM_MOUSELEAVE メッセージによる領域外への移動検出


継承クラスの場合は、イベントよりも CM_MOUSELEAVE メッセージ、または WM_MOUSELEAVE メッセージを使用すると便利です。継承クラスでも、フォームに配置するコンポーネントであれば OnMouseLeave イベントが使用できます。


リスト2
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, 
  Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TplMemo = class(TMemo)
  protected
    procedure CMMouseleave(var Message: TMessage); message CM_MOUSELEAVE;
  end;

  TForm1 = class(TForm)
    Memo1: TMemo;
    procedure FormCreate(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
    plMemo1 : TplMemo;
  end;

var
  Form1: TForm1;

implementation

uses
  plChgCompo;

{$R *.dfm}

//=============================================================================
//  フォーム生成時の処理
x//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  Memo1.ScrollBars := ssVertical;
  // フォーム作成時にコンポーネントの入れ替え
  plMemo1 := TplMemo(ChangeComponent(Memo1, TplMemo));
end;

{ TplMemo }

//=============================================================================
//  TMemo の CM_MOUSELEAVE メッセージ処理
//  マウスカーソルがコントロールの領域外に移動すると送られてくるメッセージ
//  マウスがキャプチャされている状態 (マウスボタン押下) の時は送られてこない
///  その場合、マウスのボタンを離すとこのメッセージが送られてくる
//
//  Delphi 6 等には OnMouseLeave イベントがないのでこのメッセージを使用する
//=============================================================================
procedure TplMemo.CMMouseleave(var Message: TMessage);
begin
  inherited;
  Lines.Add(InttoStr(Lines.Count) + '  Leaved from ' + Name + ' !!');
end;

end.




03_SetCaptureControl によるマウスキャプチャの解除


OnMouseLeave イベントを使用するサンプルで、マウスをドラッグしてコントロールの領域外にマウスカーソルを移動してもイベントは発生しません。その状態でマウスのボタンを離すとイベントが発生します。CM_MOUSELEAVE メッセージを使用する方法も現象は同じです。

くのコントロールではマウスボタンを押下するとマウスキャプチャ (マウス動作の捕捉によるコントロールのキャプチャ) が行われます。つまり、ドラッグ中は、マウスはそのコントロール上にあるのと同じ状態になっています。実際に、マウスドラッグで Memo1 の領域外にマウスカーソルが移動しても、マウスカーソルkの形状は Memo1 の領域内にある時と同じです。
マウスドラッグでも、コントロールの領域外にマウスカーソルを移動してからマウスボタンを離すと、ドラッグ終了となり、マウスキャプチャが解除されてイベントが発生します。



図2
マウスドラッグで Memo1 の領域外に移動
  • OnMouseLeave イベントは発生しない
  • マウスキャプチャされているので、マウスカーソルの形状は Memo1 内にある時と同じ
図3
マウスボタンを離してドラッグ終了
  • OnMouseLeave イベントが発生して、Memo1 に文字列が追加される
  • マウスキャプチャが解除されるので、マウスカーソルの形状が変わる


マウスキャプチャを強制的に解除してしまえば上記の現象は発生しません。マウスキャプチャを解除するには SetCaptureControl 手続きの引数に nil を指定して実行します。その例です。
ReleaseCapture という Windows API の関数もありますが、SetCaptureControl 手続きを使用した方が無難です。SetCaptureControl 手続きは、内部で ReleaseCapture 関数を使用しています。



リスト3
//=============================================================================
//  Memo1 の OnMouseDown イベント処理
//=============================================================================
procedure TForm1.Memo1MouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  // マウスキャプチャを解除
  SetCaptureControl(nil);
end;

//=============================================================================
//  Memo1 の OnMouseLeave イベント処理
//  マウスカーソルがコントロールの領域外に移動すると発生するイベント
//  マウスがキャプチャされている状態 (マウスボタン押下状態 ) の時は発生しない
//  その場合、マウスのボタンを離すとこのイベントが発生する
//=============================================================================
procedure TForm1.Memo1MouseLeave(Sender: TObject);
begin
  Memo1.Lines.Add(InttoStr(Memo1.Lines.Count) + '  Leaved from Memo1 !!');
end;


継承クラスの場合は、MouseDown メソッド内でマウスキャプチャの解除ができます。



リスト4
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, 
  Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TplMemo = class(TMemo)
  protected
    procedure CMMouseleave(var Message: TMessage); message CM_MOUSELEAVE;
    procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X: Integer;
      Y: Integer); override;
  end;

  TForm1 = class(TForm)
    Memo1: TMemo;
    procedure FormCreate(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
    plMemo1 : TplMemo;
  end;

var
  Form1: TForm1;

implementation

uses
  plChgCompo;

{$R *.dfm}

//=============================================================================
//  フォーム生成時の処理
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  Memo1.ScrollBars := ssVertical;
  // フォーム作成時にコンポーネントの入れ替え
  plMemo1 := TplMemo(ChangeComponent(Memo1, TplMemo));
end;

{ TplMemo }

//=============================================================================
//  TMemo の MouseDown メソッド処理
//  マウスボタンを押下すると自動的に実行される (OnMouseDown イベント相当
//=============================================================================
procedure TplMemo.MouseDown(Button: TMouseButton; Shift: TShiftState; X,
  Y: Integer);
begin
  inherited;
  // マウスキャプチャを解除
  SetCaptureControl(nil);
end; 

//=============================================================================
//  TMemo の CM_MOUSELEAVE メッセージ処理
//  マウスカーソルがコントロールの領域外に移動すると送られてくるメッセージ
//  マウスがキャプチャされている状態 (マウスボタン押下) の時は送られてこない
///  その場合、マウスのボタンを離すとこのメッセージが送られてくる
//
//  Delphi 6 等には OnMouseLeave イベントがないのでこのメッセージを使用する
//=============================================================================
procedure TplMemo.CMMouseleave(var Message: TMessage);
begin
  inherited;
  Lines.Add(InttoStr(Lines.Count) + '  Leaved from ' + Name + ' !!');
end;

end.


マウスキャプチャを解除すると、マウスボタンを押した状態にしても、ドラッグ操作としては機能しなくなります。このサンプルでは、マウスドラッグによる Memo1 の文字列選択ができなくなります。
下記のページの最初のサンプルでも、OnMouseDown イベントの最後に SetCaptureControl(nil) を実行すると、マウスドラッグによるコントロールの移動ができなくなります。



[備考 1]
マウスのドラッグ操作は、ウィンドウとしてのフォームの移動やリサイズ等、Drag & Drop (ドラッグ・アント・ドロップ) によるオブジェクトの処理が必要な場合の操作です。文字列の選択や、画像の領域選択操作等でもマウスドラッグが使われます。

TMemo やウェブブラウザでマウスドラッグすると、テキスト等の選択操作になります。ファイルやフォルダを選択した状態でエクスプローラからドラッグしてマウスのボタンを離すと (Drag & Drop すると)、そのファイルやフォルダのコピーや移動、あるいはファイルの表示やプログラムの実行となります。ボタンの種類よっては、ボタンを離した時にメニューが表示されます。
これらのことから、単なるマウスカーソルの位置移動に、通常は、マウスドラッグ操作の状態にすることはありません。

マウスのドラッグ操作における OnMouseLeave イベントの挙動は、ドラッグが終了したらアクションが発生する Drag & Drop の挙動と同じと解釈することもできます。


[備考 2]
Delphi 4 / 5 / 6 / 7 では、マウスドラッグ操作でも、マウスカーソルがコントロールの領域外に移動すると CM_MOUSELEAVE メッセージが送信されてくることを確認しています。
同じマウスドラッグ操作でも、対象のコントロールの領域外からマウスカーソルが移動してきても、CM_MOUSEENTER メッセージを受信しません。Delphi 4 / 5 / 6 / 7 では、マウスドラッグ操作によるマウスカーソル移動の時は、CM_MOUSELEAVE メッセージも CM_MOUSEENTER メッセージも、キャプチャされているコントロールが受信します。

それらのメッセージ処理関係の VCL のコードを変更すれば、マウスドラッグ時の挙動を Delphi 7 以前と同じすることもできます。





04_マウスカーソルの領域外への移動を禁止


目的にもよりますが、マウスカーソルをコントロールの領域外に移動させたくないことがあります。そのような場合の参考です。コントロールの領域外にマウスカーソルを移動させない方法です。
Windows API の ClipCursor 関数を使用します。引数はスクリーン座標での矩形領域とします。マウスカーソルはこの矩形領域でクリップされます。クリップを解除するには引数を nil とします。

このサンプルでは、フォームのクライアント領域でクリップします。

コントロールの Drag & Drop (ドラッグ・アンド・ドロップ) 操作では、コントロールが特定の領域外に移動しないようにするために、SetCaptureControl 手続きを使用して、コントロールのキャプチャ (マウスキャプチャ) を解除することがあります。



図4
マウスカーソルのクリップ
  • Button1 クリックでクリップ開始
  • Button2 クリックでクリップ解除

リスト5
//=============================================================================
//  マウスカーソルの移動を指定した矩形領域内だけに限定
//  ここではフォームのクライアント領域内だけとする
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LPoint  : TPoint;
  LRect   : TRect;
begin
  // このフォームのクライアント領域をスクリーン座標で取得
  // 一般的には GetWindowRect 関数が使用可能
  LPoint := TPoint.Create(ClientRect.Left, ClientRect.Top);
  LRect  := TRect.Create(ClientToScreen(LPoint), ClientWidth, ClientHeight);

  // マウスカーソルの移動をクリップ
  ClipCursor(@LRect);
end;

//=============================================================================
//  マウスカーソルの移動のクリップを解除
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
begin
  ClipCursor(nil);
end;