Bootstrap

检测窗口是否最大化兼容 Win10/11

检测窗口是否最大化(窗口覆盖或独占全屏)兼容 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 检测了大量的事件:

这是一张显示 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.

;