新版的QQ在截圖時加入了窗口自動識別的功能,能根據(jù)鼠標(biāo)的位置自動畫出下面窗口的輪廓。今天有人在論壇上問起這個問題,下面我們來探討這個功能的實(shí)現(xiàn)原理。
首先我們要明白截圖軟件的基本原理,截圖時實(shí)際上是新建了一個全屏窗口,然后將當(dāng)前桌面的截圖畫在上面,大部分截圖軟件,包括QQ都是這么做的。根據(jù)鼠標(biāo)位置獲取下層窗口,有好幾個類似的API可以用(WindowFromPoint, ChildWindowFromPoint, ChildWindowFromPointEx,RealChildWindowFromPoint)。
這里我們重點(diǎn)關(guān)注ChildWindowFromPointEx,因?yàn)槲覀冎澜貓D時有個全屏窗口覆蓋在上面,通過鼠標(biāo)位置去取得窗口,肯定首先取到的是這個全屏窗口,所以我們要把這個窗口過濾掉,而只有ChildWindowFromPointEx這個API有窗口過濾功能。
所以我們有理由相信QQ的全屏窗口用了WS_EX_LAYERED屬性,然后QQ通過調(diào)用ChildWindowFromPointEx(hWndDesktop,ptCursor, CWP_SKIPINVISIBLE|CWP_SKIPTRANSPARENT), 這樣就可以過濾掉不可見的和Layered窗口,然后通過遞歸調(diào)用該API,就可以獲取里面的子窗口了。
另外我們可以在啟動QQ截圖后,通過Windows鍵激活任務(wù)欄,然后改變通過任務(wù)欄最小化或是關(guān)閉某個包含在截圖內(nèi)的窗口,再繼續(xù)截圖就會發(fā)現(xiàn)QQ沒法識別了。這也說明了QQ截圖是實(shí)時通過ChildWindowFromPointEx來獲取下層窗口的。這也算是QQ截圖的一個Bug。
很多截圖軟件卻沒有上述問題,我想他們應(yīng)該是在開始截圖時保存了桌面上所有窗口的層次關(guān)系和所在區(qū)域,后面用的都是當(dāng)時保存的信息來識別的,這樣即使后面下面的窗口變化了,識別也不會受到影響。
另外,有些截圖軟件能夠識別到比窗口粒度更小的元素,比如Toolbar控件上的每個Item,他們用的應(yīng)該是MSAA(Microsoft Active Accessibility),標(biāo)準(zhǔn)控件一般都支持該接口。
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 ; 2
N - 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;
CRect rtSelect = CSCWinSpy<CSCWinFilter>::GetInstance()->GetWinRectByPoint(pt, FALSE);