walklang / uilib

A simply and powerful ui script framework library. via http://www.uilib.cn

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

WindowFromPoint捕获鼠标悬停的实际窗口

suhao opened this issue · comments

commented

大部分情况下,我们都可以使用WindowFromPoint来捕获鼠标当前悬停所在的窗口句柄。但是,总是有一些特殊情况,需要我们进行特殊处理。

  1. 获取组合控件或者嵌入式窗口的实际窗口
  2. 截图时获取窗口:我们可能需要使用到ChildWindowFromPointEx
  3. 获取透明窗口

如上各自问题的解决方案有:

  1. 侵入式场景方案一:利用框架层解决
  • 界面库提供一个统一的窗口层级管理系统,来记录窗口的zorder和区域
  • 实现z-order动态实时更新
  • 此时我们可以快速来处理如上的问题
  1. 非框架层或者非侵入式场景:利用窗口遍历
  • 系统级别:遍历桌面层级的所有窗口,记录z-order和区域,然后进行获取
  • 进程级别:GetWindowThreadProcessId进行进程过滤
  • 窗口级别:WindowFromPoint获取当前窗口,然后遍历获取同z-order级别窗口
  1. 侵入式场景方案二:在进程中利用窗口遍历来实现
  • 例如通过EnumThreadWindow来遍历,减少遍历的窗口数量

在实际应用场景下,可以根据要求来选取具体方案

commented

WindowFromPointEx.c

//
//	WindowFromPointEx.c
//
//  Copyright (c) 2002 by J Brown 
//  Freeware
//
//	HWND WindowFromPointEx(POINT pt)
//
//	Provides a better implementation of WindowFromPoint.
//  This function can return any window under the mouse,
//  including controls nested inside group-boxes, nested
//  dialogs etc.
//

#define STRICT
#define WIN32_LEAN_AND_MEAN

#include <windows.h>
#include <tchar.h>

static HWND  hwndGlobal;
static HWND  hwndButton;
static BOOL  g_fShowHidden;
static DWORD dwArea;

//
//	Callback function used with FindBestChild
//
static BOOL CALLBACK FindBestChildProc(HWND hwnd, LPARAM lParam)
{
	RECT  rect;
	DWORD a;
	POINT pt;

	pt.x = (short)LOWORD(lParam);
	pt.y = (short)HIWORD(lParam);

	GetWindowRect(hwnd, &rect);

	// Is the mouse inside this child window?
	if(PtInRect(&rect, pt))
	{
		// work out area of child window
		a = (rect.right-rect.left) * (rect.bottom-rect.top);
	
		// if this child window is smaller than the
		// current "best", then choose this one
		if(a < dwArea && (IsWindowVisible(hwnd) || g_fShowHidden == TRUE))
		{
			dwArea = a;
			hwndGlobal = hwnd;
		}
	}

	return TRUE;
}

//
//	The problem:
//
//	WindowFromPoint API is not very good. It cannot cope
//  with odd window arrangements, i.e. a group-box in a dialog
//  may contain a few check-boxes. These check-boxes are not
//  children of the groupbox, but are at the same "level" in the
//  window-hierachy. WindowFromPoint will just return the
//  first available window it finds which encompasses the mouse
//  (i.e. the group-box), but will NOT be able to detect the contents.
//
//	Solution:
//
//	We use WindowFromPoint to start us off, and then step back one
//  level (i.e. from the parent of what WindowFromPoint returned).
//
//	Once we have this window, we enumerate ALL children of this window
//  ourselves, and find the one that best fits under the mouse - 
//  the smallest window that fits, in fact.
//
//	I've tested this on alot of different apps, and it seems
//  to work flawlessly - in fact, I havn't found a situation yet
//  that this method doesn't work on.....we'll see!
//
//  Inputs:
//
//	hwndFound - window found with WindowFromPoint
//	pt        - coordinates of mouse, in screen coords
//				(i.e. same coords used with WindowFromPoint)
//
static HWND FindBestChild(HWND hwndFound, POINT pt)
{
	HWND  hwnd;
	DWORD dwStyle;

	dwArea = -1;	// Start off again

	hwndGlobal = 0;

	hwnd = GetParent(hwndFound);
	
	dwStyle = GetWindowLong(hwndFound, GWL_STYLE);

	// The original window might already be a top-level window,
	// so we don't want to start at *it's* parent
	if(hwnd == 0 || (dwStyle & WS_POPUP))
		hwnd = hwndFound;

	// Enumerate EVERY child window.
	//
	//	Note to reader:
	//
	//	You can get some real interesting effects if you set
	//  hwnd = GetDesktopWindow()
	//  fShowHidden = TRUE
	//  ...experiment!!
	//
	EnumChildWindows(hwnd, FindBestChildProc, MAKELPARAM(pt.x, pt.y));

	if(hwndGlobal == 0)
		hwndGlobal = hwnd;

	return hwndGlobal;
}

//
//	Find window under specified point (screen coordinates)
//
HWND WindowFromPointEx(POINT pt, BOOL fShowHidden)
{
	HWND hWndPoint;
	
	g_fShowHidden = fShowHidden;

	//
	// First of all find the parent window under the mouse
	// We are working in SCREEN coordinates	
	// 
	hWndPoint = WindowFromPoint(pt);
	
	if(hWndPoint == 0)
		return 0;

	// WindowFromPoint is not too accurate. There is quite likely
	// another window under the mouse.
	hWndPoint = FindBestChild(hWndPoint, pt);
	
	//if we don't allow hidden windows, then return the parent
	if(!g_fShowHidden)
	{
		while(hWndPoint && !IsWindowVisible(hWndPoint))
			hWndPoint = GetParent(hWndPoint);
	}

	return hWndPoint;
}

commented

QQ截图时窗口自动识别的原理

新版的QQ在截图时加入了窗口自动识别的功能,能根据鼠标的位置自动画出下面窗口的轮廓。今天有人在论坛上问起这个问题,下面我们来探讨这个功能的实现原理。

首先我们要明白截图软件的基本原理,截图时实际上是新建了一个全屏窗口,然后将当前桌面的截图画在上面,大部分截图软件,包括QQ都是这么做的。根据鼠标位置获取下层窗口,有好几个类似的API可以用(WindowFromPoint, ChildWindowFromPoint, ChildWindowFromPointEx,RealChildWindowFromPoint)。

这里我们重点关注ChildWindowFromPointEx,因为我们知道截图时有个全屏窗口覆盖在上面,通过鼠标位置去取得窗口,肯定首先取到的是这个全屏窗口,所以我们要把这个窗口过滤掉,而只有ChildWindowFromPointEx这个API有窗口过滤功能。
HWND ChildWindowFromPointEx(

HWND hwndParent,     POINT pt,     UINT uFlags );

Parameters

hwndParent
[in] Handle to the parent window.
pt
[in] Specifies a POINT structure that defines the client coordinates (relative to hwndParent) of the point to be checked.
uFlags
[in] Specifies which child windows to skip. This parameter can be one or more of the following values.
CWP_ALL
Does not skip any child windows
CWP_SKIPINVISIBLE
Skips invisible child windows
CWP_SKIPDISABLED
Skips disabled child windows
CWP_SKIPTRANSPARENT
Skips transparent child windows

所以我们有理由相信QQ的全屏窗口用了WS_EX_LAYERED属性,然后QQ通过调用ChildWindowFromPointEx(hWndDesktop,ptCursor, CWP_SKIPINVISIBLE|CWP_SKIPTRANSPARENT), 这样就可以过滤掉不可见的和Layered窗口,然后通过递归调用该API,就可以获取里面的子窗口了。

为了验证我们猜想,怎么可以自己建立一个Layered Window,然后用QQ截图,可以看到QQ是无法识别该窗口的。

另外我们可以在启动QQ截图后,通过Windows键激活任务栏,然后改变通过任务栏最小化或是关闭某个包含在截图内的窗口,再继续截图就会发现QQ没法识别了。这也说明了QQ截图是实时通过ChildWindowFromPointEx来获取下层窗口的。这也算是QQ截图的一个Bug。

很多截图软件却没有上述问题,我想他们应该是在开始截图时保存了桌面上所有窗口的层次关系和所在区域,后面用的都是当时保存的信息来识别的,这样即使后面下面的窗口变化了,识别也不会受到影响。

另外,有些截图软件能够识别到比窗口粒度更小的元素,比如Toolbar控件上的每个Item,他们用的应该是MSAA(Microsoft Active Accessibility),标准控件一般都支持该接口。

看到有些人对通过枚举方式识别窗口的代码感兴趣 , 下面是我的代码:

class CSCWinFilter
{
public:
    static BOOL IsFilterWindow(HWND hWnd)
    {
        _ASSERTE(hWnd != NULL);
        DWORD dwProcessID = GetCurrentProcessId();
        if(hWnd != NULL && IsWindow(hWnd))
        {
            DWORD dwWinProcessId(0);
            GetWindowThreadProcessId(hWnd, &dwWinProcessId);
            if(dwProcessID == dwWinProcessId) 
            {
                return TRUE;
            }
        }
        
        return FALSE;
    }
    
    static DWORD GetIncludeStyle()
    {
        return WS_VISIBLE;
    }
    
    static DWORD GetExcludeStyleEx()
    {
        return  WS_EX_TRANSPARENT;
    }
    
    static BOOL IsTargetPopupWindow()
    {
        return FALSE;
    }
};

class CSCWinInfo
{
public:
    HWND m_hWnd;    
    CRect m_rtWin;    //window rect
                    
    INT m_nLevel;    // 1 - pop up window  ;  2N - child window
};

//pop up win 1 (level 1).. first Z order
//        child11 (level 2)
//        child12 (level 2)
//                chilld121 (level 3)
//                chilld122 (level 3)
//                
//        child3 (level 2)
//pop up win2
//        child21 (level 2)
//        child21 (level 2)
// .
// .
//pop up winN . last Z order


template<typename CWinFilterTraits = CSCWinFilter>
class CSCWinSpy:  public CHYSingleton<CSCWinSpy>
{
public:
    BOOL SnapshotAllWinRect()
    {
        ClearData();

        // cache current window Z order when call this function
        EnumWindows(EnumWindowsSnapshotProc, 1); 
        
        return TRUE;
    }
    
    //get from current Z order of desktop
    HWND GetHWNDByPoint(CPoint pt)
    {
        m_hWndTarget = NULL;
        
        EnumWindows(EnumWindowsRealTimeProc, MAKELPARAM(pt.x, pt.y));
        
        return m_hWndTarget;
    }
    
    CRect GetWinRectByPoint(CPoint ptHit, BOOL bGetInRealTime = FALSE)
    {
        CRect rtRect(0, 0, 0, 0);
        if(bGetInRealTime) //get from current Z order
        {
            HWND hWndTarget = GetHWNDByPoint(ptHit);
            if(hWndTarget != NULL )
            {
                GetWindowRect(hWndTarget, &rtRect);
            }
        }
        else //get from snapshot cache
        {
            GetRectByPointFromSnapshot(ptHit, rtRect);
        }
        
        return rtRect;
    }
    
protected:
    static BOOL CALLBACK EnumWindowsRealTimeProc(HWND hwnd, LPARAM lParam)
    {
        if(!PtInWinRect(hwnd, CPoint(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)))) return TRUE;
        
        if(ShouldWinBeFiltered(hwnd))  return TRUE;
        
        m_hWndTarget = hwnd;
        
        if(CWinFilterTraits::IsTargetPopupWindow()) return FALSE; //this is the target window, exit search
        
        EnumChildWindows(hwnd, EnumChildRealTimeProc, lParam);
        
        return FALSE;
    }
    
    static BOOL CALLBACK EnumChildRealTimeProc(HWND hwnd, LPARAM lParam)
    {
        if(!PtInWinRect(hwnd, CPoint(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)))) return TRUE;
        
        if(ShouldWinBeFiltered(hwnd)) return TRUE;
        
        m_hWndTarget = hwnd;
        EnumChildWindows(hwnd, EnumChildRealTimeProc, lParam);
        
        return FALSE;
    }
    
protected:
    static BOOL CALLBACK EnumWindowsSnapshotProc(HWND hwnd, LPARAM lParam)
    {
        INT nLevel = lParam;
        if(ShouldWinBeFiltered(hwnd))  return TRUE;
        
        SaveSnapshotWindow(hwnd, nLevel);
        
        if(!CWinFilterTraits::IsTargetPopupWindow())
        {
            ++nLevel;
            EnumChildWindows(hwnd, EnumChildSnapshotProc, nLevel);
        }
        
        return TRUE;
    }
    
    static BOOL CALLBACK EnumChildSnapshotProc(HWND hwnd, LPARAM lParam)
    {
        INT nLevel = lParam;
        
        if(ShouldWinBeFiltered(hwnd)) return TRUE;
        
        SaveSnapshotWindow(hwnd, nLevel);
        
        ++nLevel;
        EnumChildWindows(hwnd, EnumChildSnapshotProc, nLevel);
        
        return TRUE;
    }
    
protected:
    static BOOL PtInWinRect(HWND hWnd, CPoint pt)
    {
        CRect rtWin(0, 0, 0, 0);
        GetWindowRect(hWnd, &rtWin);
        return PtInRect(&rtWin, pt);
    }
    
    static BOOL ShouldWinBeFiltered(HWND hWnd)
    {
        if(CWinFilterTraits::IsFilterWindow(hWnd)) return TRUE;
        
        DWORD dwStyle = GetWindowLong(hWnd, GWL_STYLE);
        DWORD dwStyleMust = CWinFilterTraits::GetIncludeStyle();
        if((dwStyle & dwStyleMust) != dwStyleMust) return TRUE;
        
        DWORD dwStyleEx = GetWindowLong(hWnd, GWL_EXSTYLE);
        DWORD dwStyleMustNot = CWinFilterTraits::GetExcludeStyleEx();
        if((dwStyleMustNot & dwStyleEx) != 0) return TRUE;
        
        return FALSE;
    }
    
    //find the first window that level is biggest
    static BOOL  GetRectByPointFromSnapshot(CPoint ptHit, CRect& rtRet)
    {
        int nCount = m_arSnapshot.size();
        _ASSERTE(nCount > 0);
        CSCWinInfo* pInfo = NULL;
        CSCWinInfo* pTarget = NULL; 
        
        for(int i=0; i<nCount; ++i)
        {
            pInfo = m_arSnapshot[i];
            _ASSERTE(pInfo != NULL);
            
            //target window is found 
            //and level is not increasing, 
            //that is checking its sibling or parent window, exit search
            if(pTarget != NULL
                && pInfo->m_nLevel <= pTarget->m_nLevel)
            {
                break;
            }
            
            if(PtInRect(&pInfo->m_rtWin, ptHit))
            {
                if(pTarget == NULL)
                {
                    pTarget = pInfo;
                }
                else
                {
                    if( pInfo->m_nLevel > pTarget->m_nLevel)
                    {
                        pTarget = pInfo;
                    }
                }
            }
        }
        
        if(pTarget != NULL)
        {
#ifdef _DEBUG
            if(pTarget != NULL)
            {
                HWND hWnd = pTarget->m_hWnd;
                TCHAR szText[128] = {0};
                _sntprintf(szText, 127, _T("GetRectByPointFromSnapshot: pt(%d, %d), hWnd(%x)"),
                    ptHit.x, ptHit.y, (UINT)(pInfo->m_hWnd));
                OutputDebugString(szText);
            }
#endif

            rtRet.CopyRect(&pTarget->m_rtWin);
            return TRUE;
        }
        
        return FALSE;
    }
    
    static VOID SaveSnapshotWindow(HWND hWnd, INT nLevel)
    {
        _ASSERTE(hWnd != NULL && IsWindow(hWnd));
        CRect rtWin(0, 0, 0, 0);
        GetWindowRect(hWnd, &rtWin);
        if(rtWin.IsRectEmpty()) return;
        
        CSCWinInfo* pInfo = new CSCWinInfo;
        if(pInfo == NULL) return;
        
        pInfo->m_hWnd = hWnd;
        pInfo->m_nLevel = nLevel;
        pInfo->m_rtWin = rtWin;
        
        m_arSnapshot.push_back(pInfo);
    }
    
    static VOID ClearData()
    {
        int nCount = m_arSnapshot.size();
        for(int i=0; i<nCount; ++i)
        {
            delete m_arSnapshot[i];
        }
        
        m_arSnapshot.clear();
    }
    
protected:
    friend class CHYSingleton<CSCWinSpy>;

    CSCWinSpy() { NULL; }
    ~CSCWinSpy() {    ClearData(); }
    
    static HWND m_hWndTarget;
    static std::vector<CSCWinInfo*> m_arSnapshot;
};

template<typename T> HWND CSCWinSpy<T>::m_hWndTarget = NULL;
template<typename T> std::vector<CSCWinInfo*> CSCWinSpy<T>::m_arSnapshot;

这样使用, 在截图开始时保存所有桌面窗口层次:

CSCWinSpy<CSCWinFilter>::GetInstance()->SnapshotAllWinRect();

然后就可以这样查询某个位置的最上层窗口了:

CRect rtSelect = CSCWinSpy<CSCWinFilter>::GetInstance()->GetWinRectByPoint(pt, FALSE);

Feedback:

  1. 更正下,用工具看了下QQ截图时的全屏窗口,发现它没有WS_EX_LAYERED属性,也没有WS_DISABLED属性,所以QQ截图在获取最外层Popup窗口时应该不是用ChildWindowFromPointEx获取的。 而应该是通过Z-Order从上到下枚举(EnumWindows)所有可见并且非Transparent的Popup窗口, 这样他们可以过滤掉自己的全屏窗口,找到第一个鼠标所在位置的Popup窗口,然后再用ChildWindowFromPointEx获取里面的子窗口。
  2. 个人感觉QQ应该不是用枚举窗口做的,用SPY++可以发现,QQ的遮罩层会定时收到WM_ENABLE的消息,先FALSE然后在TRUE,比较像先把自己窗口ENABLE,然后抓取其他窗口,然后再恢复,但是使用WindowFromPoint却发现无法穿透ENABLE的窗口,目前还没找方法来实现,你说的枚举我回头试下,感谢你的分享