Open suhao opened 2 years ago
//
// 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;
}
新版的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:
大部分情况下,我们都可以使用WindowFromPoint来捕获鼠标当前悬停所在的窗口句柄。但是,总是有一些特殊情况,需要我们进行特殊处理。
如上各自问题的解决方案有:
在实际应用场景下,可以根据要求来选取具体方案