ホーム >プログラム >Delphi 6 ローテクTips >マウスカーソル下のウィンドウを取得

マウスカーソル下のウィンドウを取得したいと思いました。
大抵はWindowFromPoint API一発で事足りるのですが、実はWindowFromPointは無効(EnabledがFalse)なウィンドウを取得できません。
とうことで色々やってみた結果EnumChildWindows APIというややこしいものを使わざるを得ないことが分かりました。
意外と大変です。



まずWindowFromPointがだめなら同じようなAPIのChildWindowFromPointEx APIを使えば良いのではなかろうかと思いました。

var
  lh_Window,
  lh_CWindow : HWND;
  lpt_Pos,
  lpt_Client : TPoint;
begin
  //現在のマウスカーソルの位置を取得
  GetCursorPos(lpt_Pos);
  lh_Window  := WindowFromPoint(lpt_Pos);
  //マウスカーソルの位置をWindowFromPointで取得したウィンドウのクライアント座標に変換
  lpt_Client := lpt_Pos;
  Windows.ScreenToClient(lh_Window, lpt_Client);
  lh_CWindow := ChildWindowFromPointEx(lh_Window, lpt_Client, CWP_SKIPINVISIBLE or CWP_SKIPTRANSPARENT);

ChildWindowFromPointEx APIの第1引数にはWindowFormPointで取得した値を与えます。
第2引数はマウスカーソルの位置を第1引数に与えたウィンドウのクライアント座標に変換して与えます。
第3引数は非可視ウィンドウと透明ウィンドウを無視するように指定します。
無効ウィンドウは無視しないようにするのがポイントです。
これで無効ウィンドウも拾えるようになりました。

ChildWindowFromPointEx APIを使えばButton1が無効であってもButton1のハンドルを取得できます。

これで万事解決かと思いきや、例えば上図でPanel1が無効になっている場合はButton1の上にマウスカーソルがあってもlh_CWindowはPanel1のハンドルになってしまいます。
Form3が無効になっている場合も同じように、Button1の上にマウスカーソルがあってもButton1のハンドルではなくPanel1のハンドルになってしまいます。
更にマウスカーソルがフォームのタイトルバーやメニューなどの非クライアント領域にあるとChildWindowFromPointExは0を返してしまうので尚更うまくありません。

ということで子ウィンドウを列挙してくれるEnumChildWindowsを使い、列挙された子ウィンドウが可視でありマウスカーソルの位置あるかを判定することになります。
気をつける点は、EnumChildWindowsはZオーダーは手前のウィンドウから列挙しますが、親子関係は親ウィンドウから列挙することです。

この例だと
Panel2
Button4
Button3
Panel1
Button2
Button1
の順で列挙されます。

見た目どおりの
Button4
Button3
Panel2
Button2
Button1
Panel1
という順ではないので慣れないと結構ややこしいです。

上図ではForm3にはPanel1とPanel2の子ウィンドウがあります。
Panel1よりPanel2の方がZオーダーは手前になるのでまずPanel2から列挙されます。
Panel2にはButton3とButton4の子ウィンドウがあり、Button4の方がZオーダーで手前にあるのでButton4が列挙されその次にButton3が列挙されます。
これ以上Panel2には子ウィンドウはないのでPanel1が列挙され、Panel1にはButton1とButton2の子ウィンドウがあり、Button2の方が手前にあるのでButton2が列挙され次にButton1が列挙されます。

この辺りは結構ややこしいのでテストフォームを作って色々テストしてみることをお勧めします。

//APoint上にある可視ウィンドウを返す。
type
  T_WinPos = record
    Handle : HWND;  //調べる点の上にあるウィンドウ
    Point : TPoint; //この位置にある一番手前の見えているウィンドウを取得する
  end;
  P_WinPos = ^T_WinPos;

function EnumChildProc(hHandle: HWND; pWinPos: Pointer): BOOL; stdcall;
var
  lh_Parent : HWND;
  lrc_Rect  : TRect;
begin
  Result := True;
  GetWindowRect(hHandle, lrc_Rect);
  if  (IsWindowVisible(hHandle))
  and (PtInRect(lrc_Rect, T_WinPos(pWinPos^).Point))
  then begin
    lh_Parent := GetAncestor(hHandle, GA_PARENT);
    if (lh_Parent = T_WinPos(pWinPos^).Handle) then begin
      //今までの処理でカーソル下にあると判断されたウィンドウの子ウィンドウだった
      //子ウィンドウは親ウィンドウより手前なのでT_WinPos(pWinPos^).Handleの値を書き換える
      //このウィンドウにも子ウィンドウがあるかもしれないので処理を続ける
      T_WinPos(pWinPos^).Handle:= hHandle;
    end else begin
      //親ウィンドウが今までの処理でカーソル下にあると判断されたウィンドウではないので処理を抜ける
      //EnumChildWindowsは手前のウィンドウから列挙するのでこれ以降列挙されるカーソル下のウィンドウが
      //あったとしてもそれはすべて一番手前にはないことが確定したため

      Result := False;
    end;
  end;
end;
function gfnhWindowGet(APoint : TPoint) : HWND;
//APoint上にあり一番手前にある見えているウィンドウのハンドルを返す。
var
  l_WinPos   : T_WinPos;
  lh_Window,
  lh_CWindow : HWND;
  lpt_Client : TPoint;
begin
  //WindowFromPointはAPointが無効ウィンドウ上にある場合はトップレベルウィンドウのハンドルを返す。
  lh_Window  := WindowFromPoint(APoint);
  lpt_Client := APoint;
  Windows.ScreenToClient(lh_Window, lpt_Client);
  lh_CWindow := ChildWindowFromPointEx(lh_Window, lpt_Client, CWP_SKIPINVISIBLE or CWP_SKIPTRANSPARENT);
  if (lh_Window = lh_CWindow) then begin
    //WindowFromPointとChildWindowFromPointExの戻り値が同じならそれで良い。
    Result := lh_Window;
  end else begin
    FillChar(l_WinPos, SizeOf(l_WinPos), 0);
    l_WinPos.Handle := lh_Window;
    l_WinPos.Point  := APoint;
    EnumChildWindows(lh_Window, @EnumChildProc, LPARAM(@l_WinPos));
    Result := l_WinPos.Handle;
  end;
end;

まずWindowFromPointとChildWindowFromPointExで一番手前のウィンドウを取得します。
二つの値を比較して同じならその値を返します。

  lh_Window  := WindowFromPoint(APoint);
  lpt_Client := APoint;
  Windows.ScreenToClient(lh_Window, lpt_Client);
  lh_CWindow := ChildWindowFromPointEx(lh_Window, lpt_Client, CWP_SKIPINVISIBLE or CWP_SKIPTRANSPARENT);
  if (lh_Window = lh_CWindow) then begin
    //WindowFromPointとChildWindowFromPointExの戻り値が同じならそれで良い
    Result := lh_Window;

違っていたらEnumChildWindowsの出番になります。

  if (lh_Window = lh_CWindow) then begin
    //WindowFromPointとChildWindowFromPointExの戻り値が同じならそれで良い
    Result := lh_Window;
  end else begin
    FillChar(l_WinPos, SizeOf(l_WinPos), 0);
    l_WinPos.Handle := lh_Window;
    l_WinPos.Point  := APoint;
    EnumChildWindows(lh_Window, @EnumChildProc, LPARAM(@l_WinPos));
    Result := l_WinPos.Handle;
  end;

WindowFromPointで取得したウィンドウハンドルをl_WinPos.Handleにセットしておきます。
調べる点の値をl_WinPos.Pointにセットします。
EnumChildWindowsの第3引数にl_WinPosのアドレスを渡します。
この第3引数を通して必要な値の受け渡しをしているのでややこしくなっていますが、面倒ならウィンドウハンドルとTPointの二つの変数をグローバルに宣言して使ってもよいでしょう。

  GetWindowRect(hHandle, lrc_Rect);                   //列挙された子ウィンドウのRectを取得
  if  (IsWindowVisible(hHandle))                     //可視ウィンドウ
  and (PtInRect(lrc_Rect, T_WinPos(pWinPos^).Point)) //点は子ウィンドウのRect内にある
  then begin
    lh_Parent := GetAncestor(hHandle, GA_PARENT); //親ウィンドウを取得
    if (lh_Parent = T_WinPos(pWinPos^).Handle) then begin
      //今までの処理でカーソル下にあると判断されたウィンドウの子ウィンドウだった
      //Handleを書き換える
      //子ウィンドウがあるかもしれないので処理を続ける
      T_WinPos(pWinPos^).Handle := hHandle;
    end else begin
      //親ウィンドウが今までの処理でカーソル下にあると判断されたウィンドウでなければ処理を抜ける
      Result := False;
    end;
この図でForm3が無効(EnabledがFalse)になっている場合を想定しておさらいします。
EnumChildWindowsで列挙されるのは
Panel2
Button4
Button3
Panel1
Button2
Button1
の順になります。
  1. WindowFromPointで取得したウィンドウハンドルをl_WinPos.HandleにセットしてEnumChildWindowsを呼びます。
    WindowFromPointはフォームが無効になっている場合はそのフォームのウィンドウハンドルを返すのでこの値はForm3のハンドルになります。
        l_WinPos.Handle := lh_Window;
        l_WinPos.Point  := APoint;
        EnumChildWindows(lh_Window, @EnumChildProc, LPARAM(@l_WinPos));
  2. Form3にはPanel1とPanel2の二つの子ウィンドウがあります。
    Panel1よりPanel2の方が手前にあるのでまずPanel2のハンドルが列挙されます。
  3. Panel2はマウスカーソルの下にあるので比較処理に入ります。
    Panel2の親ウィンドウを取得します。
    Panel2の親ウィンドウはForm3です。
    この時l_WinPos.HandleにはForm3のハンドルがセットされています。
    l_WinPos.Handle値(Form3のハンドル)と今取得したPanel2の親ウィンドウのハンドル(Form3のハンドル)は同じなのでPanel2は今までの処理でマウスカーソル下にある一番手前のウィンドウ(この場合はForm3)の子ウィンドウであると判断できます。
    当然親ウィンドウより子ウィンドウのほうが手前になるのでl_WinPos.Handleの値をPanel2のハンドルにセットし直します。
    Panel2に子ウィンドウがあるかも知れないのでこのまま列挙を続けます。
  4. 次にPanel2の子ウィンドウで一番手前にあるButton4のハンドルが列挙されます。
    Button4はマウスカーソルの下にはないので次の列挙に進みます。
  5. Button4の次はButton3のハンドルが列挙されます。
    Button3はマウスカーソルの下にあるので比較処理に入ります。
    Button3の親ウィンドウを取得します。
    Button3の親ウィンドウはPanel2です。
    この時l_WinPos.HandleにはPanel2のハンドルがセットされています。
    l_WinPos.Handle値(Panel2のハンドル)と今取得したButton3の親ウィンドウのハンドル(Panel2のハンドル)は一緒なのでl_WinPos.Handleの値をButton3のハンドルにセットし直します。
    Button3に子ウィンドウがあるかも知れないのでこのまま列挙を続けます。
  6. その後Panel1、Button2、Button1と列挙が続きますがいずれもマウスカーソル下にはないので比較処理には入らず列挙が終了します。
  7. 最終的にl_WinPos.HandleにはButton3のハンドルがセットされているのでマウスカーソル下の見えている一番手前のウィンドウはButton3ということになります。

今度はButton4とButton3の上にマウスカーソルが乗っているこの図でForm3が無効(EnabledがFalse)になっている場合を想定しておさらいします。
EnumChildWindowsで列挙されるのは同じく
Panel2
Button4
Button3
Panel1
Button2
Button1
の順になります。
  1. WindowFromPointで取得したウィンドウハンドルをl_WinPos.HandleにセットしてEnumChildWindowsを呼びます。
    WindowFromPointはフォームが無効になっている場合はそのフォームのウィンドウハンドルを返すのでこの値はForm3のハンドルになります。
        l_WinPos.Handle := lh_Window;
        l_WinPos.Point  := APoint;
        EnumChildWindows(lh_Window, @EnumChildProc, LPARAM(@l_WinPos));
  2. Form3にはPanel1とPanel2の二つの子ウィンドウがあります。
    Panel1よりPanel2の方が手前にあるのでまずPanel2のハンドルが列挙されます。
  3. Panel2はマウスカーソルの下にあるので比較処理に入ります。
    Panel2の親ウィンドウを取得します。
    Panel2の親ウィンドウはForm3です。
    この時l_WinPos.HandleにはForm3のハンドルがセットされています。
    l_WinPos.Handle値(Form3のハンドル)と今取得したPanel2の親ウィンドウのハンドル(Form3のハンドル)は同じなのでPanel2は今までの処理でマウスカーソル下にある一番手前のウィンドウ(この場合はForm3)の子ウィンドウであると判断できます。
    当然親ウィンドウより子ウィンドウのほうが手前になるのでl_WinPos.Handleの値をPanel2のハンドルにセットし直します。
    Panel2に子ウィンドウがあるかも知れないのでこのまま列挙を続けます。

    ここまでは先の例と同じです。
  4. 次にPanel2の子ウィンドウで一番手前にあるButton4のハンドルが列挙されます。
    Button4はマウスカーソルの下にあるので比較処理に入ります。
    Button4の親ウィンドウを取得します。
    Button4の親ウィンドウはPanel2です。
    この時l_WinPos.HandleにはPanel2のハンドルがセットされています。
    l_WinPos.Handle値(Panel2のハンドル)と今取得したButton4の親ウィンドウのハンドル(Panel2のハンドル)は一緒なのでl_WinPos.Handleの値をButton4のハンドルにセットし直します。
    Button4に子ウィンドウがあるかも知れないのでこのまま列挙を続けます。
  5. Button4の次に列挙されるのはButton3のハンドルになります。
    Button3はマウスカーソルの下ににあるので比較処理に入ります。
    Button3の親ウィンドウを取得します。
    Button3の親ウィンドウはPanel2です。
    この時l_WinPos.HandleにはButton4のハンドルがセットされています。
    l_WinPos.Handle値(Panel4のハンドル)と今取得したButton3の親ウィンドウのハンドル(Panel2のハンドル)は一緒ではないのでButton3は今までの処理でマウスカーソル下にある一番手前のウィンドウ(この場合はButton4)の子ウィンドウではないことが分かります。
    この時点でマウスカーソル下の一番手前にある見えているウィンドウはButton4であることが確定します。
    というのもEnumChildWindowsは手前のウィンドウから列挙されるので最初にカーソル下にあると判断されたウィンドウより手前にあるのはそのウィンドウの子ウィンドウだけであるからです。

    まとめると、最初にカーソル下にあると判断されたのはForm3。
    次にカーソル下にあると判断されたのはPanel2でPanel2はForm3の子ウィンドウなのでForm3よりは手前にあります。
    次にカーソル下にあると判断されたのはButton4でButton4はPanel2の子ウィンドウなのでPanel2より手前にあります。
    次にカーソル下にあると判断されたのはButton3ですがButton3はButton4の子ウィンドウではないのでButton4よりは奥にあることになります。