检测窗口是否最大化(窗口覆盖或独占全屏)兼容 Win10/11
问题描述
在 Win10/11 上有很多 UWP 进程,检测窗口是否最大化将迎来新的挑战。这些窗口以其不能够使用 Win32 的 IsWindowVisible 获取窗口可见性为特征。此时,必须使用 DWM API 来判断窗口的可见性状态。
代码实现
下面的代码实现了一个工具类检测当前桌面是否被覆盖,以及覆盖窗口的信息。(此代码参考 CustomDesktop 开源项目,并做了预览桌面时的逃逸规则)
“Singleton.h” :
#pragma once
namespace cd
{
template<class T>
class Singleton
{
protected:
Singleton() = default;
virtual ~Singleton() = default;
public:
static T& GetInstance()
{
static T s_instance;
return s_instance;
}
};
#define DECL_SINGLETON(T) friend class Singleton<T>
#define DECL_SINGLETON_DEFAULT(T) \
DECL_SINGLETON(T); \
private: \
T() = default; \
~T() = default
}
“CheckCovered.h”:
#pragma once
#include "Singleton.h"
#include <thread>
#include <memory>
namespace cd
{
// 检测桌面是否被遮挡了
class CheckCovered final : public Singleton<CheckCovered>
{
DECL_SINGLETON(CheckCovered);
public:
bool IsReady() { return m_runThreadFlag; }
bool Init();
bool Uninit();
private:
CheckCovered();
~CheckCovered();
std::unique_ptr<std::thread> m_thread;
bool m_runThreadFlag = true;
bool m_isCovered = false;
HWND m_coveredByHwnd = NULL;
void CheckCoveredThread();
bool IsDesktopCovered();
};
}
“CheckCovered.cpp” :
#include "CheckCovered.h"
#ifdef _WIN64
#include <Dwmapi.h>
#endif
namespace cd
{
CheckCovered::CheckCovered()
{
Init();
}
CheckCovered::~CheckCovered()
{
Uninit();
}
bool CheckCovered::Init()
{
m_runThreadFlag = true;
/* 在主线程中执行函数,可以用来做 dllmain 中不能完成的初始化,通过自定义消息实现
// MYDLL_API void WINAPI ExecInMainThread(std::function<void()> function);
CD_API void WINAPI ExecInMainThread(std::function<void()> function)
{
PostMessage(g_global.m_fileListWnd, WM_EXEC_FUNCTION, reinterpret_cast<WPARAM>(
new decltype(function)(std::move(function))), NULL);
}
*/
ExecInMainThread([this]{ m_thread = std::make_unique<std::thread>(&CheckCovered::CheckCoveredThread, this); });
return true;
}
bool CheckCovered::Uninit()
{
m_runThreadFlag = false;
if (m_thread != nullptr && m_thread->joinable())
m_thread->join();
m_thread = nullptr;
return true;
}
void CheckCovered::CheckCoveredThread()
{
while (m_runThreadFlag)
{
if (IsDesktopCovered())
{
if (!m_isCovered)
{
m_isCovered = true;
//ExecInMainThread([]{ g_desktopCoveredEvent(); });
#ifdef _DEBUG
WCHAR windowName[100], className[100];
GetWindowTextW(m_coveredByHwnd, windowName, _countof(windowName));
GetClassNameW(m_coveredByHwnd, className, _countof(className));
_RPTFW2(_CRT_WARN, L"桌面被 %s (%s) 遮挡\n", windowName, className);
#endif
}
}
else
{
if (m_isCovered)
{
m_isCovered = false;
//ExecInMainThread([]{ g_desktopUncoveredEvent(); });
_RPTF0(_CRT_WARN, "桌面从被遮挡恢复\n");
}
}
for (int i = 0; i < 10; i++)
{
if (!m_runThreadFlag)
break;
Sleep(100);
}
}
}
bool CheckCovered::IsDesktopCovered()
{
m_coveredByHwnd = NULL;
// 对于 D3D 独占全屏的程序,不能用 IsZoomed 判断全屏
// TODO:兼容多屏幕
int screenWidth = GetSystemMetrics(SM_CXSCREEN);
int screenHeight = GetSystemMetrics(SM_CYSCREEN);
HWND hwnd = GetForegroundWindow();
if (hwnd != GLOBAL_YOUR_WINDOW) // GLOBAL_YOUR_WINDOW 是你要检测是否被全屏幕覆盖的窗口
{
RECT rect;
GetWindowRect(hwnd, &rect);
if (rect.left == 0 && rect.top == 0
&& rect.right == screenWidth && rect.bottom == screenHeight)
{
WCHAR wsClassName[MAX_PATH] = { 0 };
GetClassNameW(hwnd, wsClassName, MAX_PATH);
if (wcsstr(wsClassName, L"LivePreview") == NULL) { // 预览桌面窗口出现时,恢复动画播放
m_coveredByHwnd = hwnd;
return true;
}
else {
m_coveredByHwnd = nullptr;
return false;
}
}
}
EnumWindows([](HWND hwnd, LPARAM pCoveredByHwnd)->BOOL {
#ifdef _WIN64
// 对于 win10 app,不能用 IsWindowVisible 判断是否可见
DWORD cloaked = 0;
DwmGetWindowAttribute(hwnd, DWMWA_CLOAKED, &cloaked, sizeof(cloaked));
if (cloaked != 0)
return TRUE;
#endif
// 有最大化的窗口而且可见则被遮挡(最小化也是不可见)
if (IsZoomed(hwnd) && IsWindowVisible(hwnd))
{
*(HWND*)pCoveredByHwnd = hwnd;
return FALSE;
}
return TRUE;
}, (LPARAM)&m_coveredByHwnd);
if (m_coveredByHwnd != nullptr) {
WCHAR wsClassName[MAX_PATH] = { 0 };
GetClassNameW(m_coveredByHwnd, wsClassName, MAX_PATH);
if (wcsstr(wsClassName, L"LivePreview") == NULL) { // 预览桌面窗口出现时,恢复动画播放
return true;
}
else {
m_coveredByHwnd = nullptr;
return false;
}
}
return false;
}
}
这里有两个点要说一下,一是此代码需要完善多桌面的情况,二是此代码考虑了预览桌面时候会产生一个窗口覆盖全屏的情况(LivePreview),为了避免检测失效,应该排除此时的覆盖情况(代码中也已经初步实现了)。
关于优化
为了更高效地判定窗口覆盖情况(不使用循环遍历窗口),我们可以使用事件钩子(SetWinEventHook),并结合 EVENT_SYSTEM_FOREGROUND、EVENT_OBJECT_LOCATIONCHANGE 等相关事件的检测和过滤,构建一个窗口监视器和与之关联的窗口信息表(Map),通过在窗口创建、销毁、和修改大小/顺序时同步修改窗口信息表。在 EVENT_SYSTEM_FOREGROUND 事件时可以从最大化窗口的列表中找到 Z 序最高的最大化窗口,来判断是否有窗口覆盖桌面。
一个典型的代码示例是 TranslucentTB 项目的窗口处理代码:taskbarattributeworker.cpp。
TranslucentTB 右键选项菜单
为了在桌面窗口处于不同状态时能够切换不同的任务栏效果,TranslucentTB 检测了大量的事件:
TranslucentTB 的事件处理钩子
下面是一个对示例代码的简单修改(未经验证),其实需要像 TB 一样更多的代码逻辑才能完成。我们只使用了 EVENT_SYSTEM_FOREGROUND 事件。
#include "CheckCovered.h"
#ifdef _WIN64
#include <Dwmapi.h>
#pragma comment(lib, "Dwmapi.lib")
#endif
namespace cd
{
CheckCovered::CheckCovered()
{
Init();
}
CheckCovered::~CheckCovered()
{
Uninit();
}
bool CheckCovered::Init()
{
m_runThreadFlag = true;
ExecInMainThread([this]{ StartEventHook(); }); // 这里需要修改为实际的线程调用
return true;
}
bool CheckCovered::Uninit()
{
m_runThreadFlag = false;
if (m_thread != nullptr && m_thread->joinable())
m_thread->join();
m_thread = nullptr;
StopEventHook();
return true;
}
void CheckCovered::StartEventHook()
{
m_eventHook = SetWinEventHook(
EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND,
NULL, WinEventProc, 0, 0,
WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS);
}
void CheckCovered::StopEventHook()
{
if (m_eventHook != NULL)
{
UnhookWinEvent(m_eventHook);
m_eventHook = NULL;
}
}
void CALLBACK CheckCovered::WinEventProc(HWINEVENTHOOK hWinEventHook, DWORD event, HWND hwnd, LONG idObject, LONG idChild, DWORD dwEventThread, DWORD dwmsEventTime)
{
if (event == EVENT_SYSTEM_FOREGROUND)
{
auto& instance = CheckCovered::GetInstance();
if (instance.IsDesktopCovered(hwnd))
{
if (!instance.m_isCovered)
{
instance.m_isCovered = true;
#ifdef _DEBUG
WCHAR windowName[100], className[100];
GetWindowTextW(instance.m_coveredByHwnd, windowName, _countof(windowName));
GetClassNameW(instance.m_coveredByHwnd, className, _countof(className));
_RPTFW2(_CRT_WARN, L"桌面被 %s (%s) 遮挡\n", windowName, className);
#endif
}
}
else
{
if (instance.m_isCovered)
{
instance.m_isCovered = false;
_RPTF0(_CRT_WARN, "桌面从被遮挡恢复\n");
}
}
}
}
bool CheckCovered::IsDesktopCovered(HWND hwnd)
{
m_coveredByHwnd = NULL;
int screenWidth = GetSystemMetrics(SM_CXSCREEN);
int screenHeight = GetSystemMetrics(SM_CYSCREEN);
if (hwnd != GLOBAL_YOUR_WINDOW)
{
RECT rect;
GetWindowRect(hwnd, &rect);
if (rect.left == 0 && rect.top == 0 && rect.right == screenWidth && rect.bottom == screenHeight)
{
WCHAR wsClassName[MAX_PATH] = { 0 };
GetClassNameW(hwnd, wsClassName, MAX_PATH);
if (wcsstr(wsClassName, L"LivePreview") == NULL && !IsWindowFullyTransparent(hwnd))
{
m_coveredByHwnd = hwnd;
return true;
}
else
{
m_coveredByHwnd = nullptr;
return false;
}
}
}
EnumWindows([](HWND hwnd, LPARAM pCoveredByHwnd)->BOOL {
#ifdef _WIN64
DWORD cloaked = 0;
DwmGetWindowAttribute(hwnd, DWMWA_CLOAKED, &cloaked, sizeof(cloaked));
if (cloaked != 0)
return TRUE;
#endif
if (IsZoomed(hwnd) && IsWindowVisible(hwnd))
{
if (!CheckCovered::GetInstance().IsWindowFullyTransparent(hwnd))
{
*(HWND*)pCoveredByHwnd = hwnd;
return FALSE;
}
}
return TRUE;
}, (LPARAM)&m_coveredByHwnd);
if (m_coveredByHwnd != nullptr)
{
WCHAR wsClassName[MAX_PATH] = { 0 };
GetClassNameW(m_coveredByHwnd, wsClassName, MAX_PATH);
if (wcsstr(wsClassName, L"LivePreview") == NULL)
{
return true;
}
else
{
m_coveredByHwnd = nullptr;
return false;
}
}
return false;
}
bool CheckCovered::IsWindowFullyTransparent(HWND hwnd)
{
// 检查 WS_EX_LAYERED 样式和 GetLayeredWindowAttributes
LONG exStyle = GetWindowLong(hwnd, GWL_EXSTYLE);
if (exStyle & WS_EX_LAYERED)
{
BYTE alpha = 0;
DWORD flags = 0;
GetLayeredWindowAttributes(hwnd, NULL, &alpha, &flags);
if ((flags & LWA_ALPHA) && alpha == 0)
{
return true;
}
}
#ifdef _WIN64
// 使用 DwmGetWindowAttribute 检查 DWM 实现的透明
BOOL isCloaked = FALSE;
DwmGetWindowAttribute(hwnd, DWMWA_CLOAKED, &isCloaked, sizeof(isCloaked));
if (isCloaked)
{
return true;
}
BYTE opacity = 255;
HRESULT hr = DwmGetWindowAttribute(hwnd, DWMWA_OPACITY, &opacity, sizeof(opacity));
if (SUCCEEDED(hr) && opacity == 0)
{
return true;
}
#endif
return false;
}
}
本文发布于:2024.06.10,修改于:2024.07.10.