编程实现标题栏窗口摇动——显示桌面的未公开细节研究

本文主要是介绍编程实现标题栏窗口摇动——显示桌面的未公开细节研究,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

前言

一、“窗口摇动”功能内部原理

二、explorer.exe 中的 “窗口抖动” 实现

三、“切换到桌面” 功能所扩展的内部细节

四、概念验证

五、进一步研究如何自定义保留窗口列表


原文出处链接:[https://blog.csdn.net/qq_59075481/article/details/139204241]

本文包含微软未记录的功能和特性。遵循本文中的建议,您将自行承担风险。本文中介绍的方法可能依赖于内部实现,将来可能不再适用。

前言

如果您在 Windows 11(或 Windows 10)系统上抓住窗口标题并用鼠标摇动它,屏幕上所有其他打开的窗口都将最小化到任务栏。然后,如果您再次摇动同一个窗口,它们将返回到屏幕上。

此功能称为“窗口摇动” (TitleBar Window Shake)或 Aero Shake,自 Windows 7 起可用。微软曾经计划在 Windows 10 测试版上取消这个特性,但最终没有实施。

要在 Windows 11 上启用(或禁用)它,请转到“开始”->“设置”->“系统”->“多任务处理”。它将以名为“标题栏窗口摇动”的复选框的形式显示。

在 Windows 10 上,相同的选项是同一多任务设置屏幕中“捕捉窗口”功能的一部分。

问题是,我们能否在代码中以编程方式执行“窗口摇动”功能的结果?

Rbmm 做了一些逆向工程来回答这个问题。

顺便说一句,答案是肯定的。

如果你不想了解其工作原理而只想查看代码,请跳转到 “概念验证”。

一、“窗口摇动”功能内部原理

一切都从 uxtheme.dll 模块中的 ThemePreWndProc 函数开始。

uxtheme.dll 被映射到大多数 GUI 进程并提供熟悉的 Windows 操作系统视觉样式。

当该窗口过程收到 WM_MOVING 消息时,它会调用 SHRegGetBoolUSValueW 函数来获取系统注册表设置:

SHRegGetBoolUSValueW(L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced",L"DisallowShaking",FALSE,TRUE
);

该检查的结果用于确定是否继续,有很多开发者利用该项关闭这个功能。

如果以下任何注册表项中的 DisallowShaking 值设置为非零,则不会执行“窗口摇动”选项:
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced

HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced

否则它将调用 CShakeWnd::FromHwnd,并且如果返回 0,它将调用 CShakeWnd::Attach,这将创建 CShakeWnd 类并将其附加到使用中的窗口句柄 HWND:

SetProp(hWnd, hAtom, this);

还有一个 uxtheme.dll 导出的 API (仅按照序号 #86 访问)。它具有以下声明:

BOOL WINAPI IsValidShakeWindow(HWND hWnd);

CShakeWnd 类具有以下成员函数:

CShakeWnd::AddReference
CShakeWnd::Attach
CShakeWnd::CShakeWnd
CShakeWnd::Detach
CShakeWnd::FromHwnd
CShakeWnd::FromHwndAddRef
CShakeWnd::GetMsgProc
CShakeWnd::HandleEscapeActions
CShakeWnd::IsGoodAngle
CShakeWnd::IsGoodLength
CShakeWnd::IsGoodRatio
CShakeWnd::MessageToShell
CShakeWnd::ReleaseReference
CShakeWnd::Shift
CShakeWnd::_cObj
CShakeWnd::_st

经过一些内部处理后,代码最终会调用:

static LRESULT CShakeWnd::MessageToShell(HWND hWnd);

该函数在内部查找具有 “Shell_TrayWnd” 窗口类 的窗口:

if(HWND hwndTray = FindWindowW(L"Shell_TrayWnd", 0))
{//...
}

然后执行以下操作:

ULONG dwProcessId;if(GetWindowThreadProcessId(hwndTray, &dwProcessId))
{AllowSetForegroundWindow(dwProcessId);//...
}

最后:

//hWnd = window handle that was shaken
PostMessageW(hwndTray, 0x4F2, 0, (LPARAM)hWnd);

这样就向 hwndTray 发送了一条私有消息 0x4F2。

0x4F2 消息是 WM_USER 范围内的属于一个 Windows 资源管理器 窗口类的私有消息。 

所有进一步的处理均在 explorer.exe 进程内完成。

CShakeWnd::MessageToShell 本身的调用来自:

LRESULT OnPreWindowMovingShakeHandler(CShakeWnd *, THEME_MSG *)

从 CShakeWnd::HandleEscapeAction 开始看:

; void CShakeWnd::HandleEscapeAction(CShakeWnd::_ESC_ACTION)
;
; rcx = this
; rdx = _ESC_ACTIONmov         qword ptr [rsp+8],rbxpush        rdisub         rsp,20hmov         rbx,rcxmov         edi,edxmov         rcx,qword ptr [rcx+58h]test        rcx,rcxjne         lbl_1cmp         edx,1jne         lbl_2call        qword ptr [GetCurrentThreadId]nop         dword ptr [rax+rax]xor         r8d,r8dlea         rdx,[CShakeWnd::GetMsgProc]mov         r9d,eaxlea         ecx,[rdi+2]call        qword ptr [SetWindowsHookExW]nop         dword ptr [rax+rax]mov         qword ptr [rbx+58h],raxjmp         lbl_2lbl_1:call        qword ptr [UnhookWindowsHookEx]nop         dword ptr [rax+rax]and         qword ptr [rbx+58h],0cmp         edi,2jne         lbl_2mov         rcx,qword ptr [rbx+18h]call        CShakeWnd::MessageToShelllbl_2:mov         rbx,qword ptr [rsp+30h]add         rsp,20hpop         rdiret

OnPreWindowMovingShakeHandler 调用 CShakeWnd::IsGoodLength、CShakeWnd::IsGoodAngle、CShakeWnd::IsGoodRatio 来确定将哪些鼠标移动可以解释为摇动。

二、explorer.exe 中的 “窗口抖动” 实现

在 explorer.exe 内部,当它收到 0x4F2 私有消息时,它会调用:

//'hWnd' = window handle for the window that was shaken
LRESULT CTray::_ShakeTriggered(enum SHOWDESKTOPTRIGGER, HWND hWnd)

它在内部调用 IsValidShakeWindow(hWnd),然后调用 IsShakeEnabled。它的汇编代码如下:

; int IsShakeEnabled(void)push        rbxsub         rsp,20hlea         rcx,[POLID_NoWindowMinimizingShortcuts]       ; {4BF2C693-2A9A-48F8-89A6-0A778F7B1D7D}call        qword ptr [SHWindowsPolicy]nop         dword ptr [rax+rax]test        eax,eaxjne         lbl_10lea         ebx,[rax+1]mov         dl,bllea         rcx,['wil::Feature<__WilFeatureTraits_Feature_AeroShakeEnabled>::GetImpl'::'2'::impl]call        'wil::details::FeatureImpl<__WilFeatureTraits_Feature_AeroShakeEnabled>::ReportUsage'mov         r9d,ebxlea         rdx,L"DisallowShaking"xor         r8d,r8dlea         rcx,L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced"call        qword ptr [SHRegGetBoolUSValueW]nop         dword ptr [rax+rax]test        eax,eaxjne         lbl_10call        IsShakedAllowedInSkutest        eax,eaxjne         lbl_11lbl_10:xor         ebx,ebxlbl_11:mov         eax,ebxadd         rsp,20hpop         rbxret

IsShakedAllowedInSku 是一个巨大的函数,除了“DisallowShaking” 注册表设置已经指定的检查之外,它进一步确定是否可以使用“窗口摇动”功能。

如果 IsShakeEnabled 函数返回成功,代码将调用私有函数 CTray::_MinimizeAll :

int CTray::_MinimizeAll(BOOL , HANDLE hEvent)

这反过来又调用 TrayUI::MinimizeAll:

virtual BOOL TrayUI::MinimizeAll(BOOL b, HANDLE hEvent)
{SendMessageW(hwnd, 0x435, (WPARAM)hEvent, b);
}

该函数向具有 “MSTaskSwWClass” 窗口类和 “运行中的应用程序”(Running applications)标题的窗口发送另一条私有消息 0x435。

然后调用以下函数:

int CTaskBand::_MinimizeAll(HEVENT hEvent, BOOL b)

该线程最小化所有窗口,并在结束时向 CTaskBand::_MinimizeAll 发送先前传递的 hEvent 信号。

此外,根据传递给 CTaskBand::_MinimizeAll 的布尔变量,它可以向 Shell 桌面窗口发送私有消息(通过调用 GetShellWindow 函数返回)。结果,它将调用内部函数 CDesktopBrowser::_Raise(BOOL),调用树如下:

CDesktopBrowser::_WndProcBS
  CDesktopBrowser::_OnRaise
    CDesktopBrowser::_Raise
      GetLastActivePopup
      CDesktopBrowser::_EnsureRaisedDesktop
        SHCreateWorkerWindowW
        SetPropW
        GlobalAddAtomW
        SetPropW
        GlobalDeleteAtom
        SetPropW
        SetPropW
        DwmSetWindowAttribute
        CZOrderManager::InsertWindowInZOrder
        SHRestricted
        RegisterDragDrop
        CDesktopBrowser::_ShowOrHideDesktopIconsBasedOnDesktopVisibility
        __security_check_cookie
        ret
      LockWindowUpdate
      CDesktopBrowser::_SwapParents
      GetWindowRect
      SetWindowPos
      SetForegroundWindow
      LockWindowUpdate
      SetFocus
      __security_check_cookie
      ret
    PostMessageW
  __security_check_cookie
  ret

三、“切换到桌面” 功能所扩展的内部细节

我们知道,当按下快捷键 “Windows + D” 或者鼠标点击任务栏右侧的 “显示桌面” 按钮时,操作将最小化全部的桌面窗口。

对于那些没有 Windows/Start 键的人来说,还有一种方法......

运行...对话框或资源管理器地址栏中,命令:

Shell:::{3080F90D-D7AD-11D9-BD98-0000947B0257}

执行“显示桌面”命令。因此,如果创建一个新的快捷方式并将以下内容粘贴为目标:

explorer "Shell:::{3080F90D-D7AD-11D9-BD98-0000947B0257}"

Explorer将在创建时充实路径和扩展)

并指定一个快捷键组合,你将有一个显示桌面的键盘快捷键。

此外,还可以创建一个名为 Show Desktop.scf(确保文件扩展名确实是 .scf)的文件,其内容如下:

[Shell]
Command=2
IconFile=explorer.exe,3
[Taskbar]
Command=ToggleDesktop

然后,可以通过右键单击文件 →发送到 →桌面(创建快捷方式)来创建此文件的快捷方式。然后打开快捷方式的文件属性(右键单击 →属性),在快捷方式选项卡上,通过单击此字段并执行所需的组合键来为其指定快捷键。

“切换到桌面” 和 “窗口摇动” 的唯一不同点是触发方式不太一样,现在我将不得不解释上面没有展开的一些细节。

首先,无论是 “窗口摇动” 还是 “切换到桌面”(Toggle Desktop),它们都具有可恢复性,当再次重复操作时(重复摇动窗口或者再次输入快捷键等),原本被 “最小化” 的窗口将逐一恢复(动画)。

这是如何实现的?这就要涉及到 ZOrder 了,每一个窗口都有它的 Z-序,上面的操作并不是通过普通的广播窗口最小化消息来实现的,而是直接操作了 ZOrderList,将指定的窗口(如桌面窗口)插入到 可见 Z 序 的最前面。这就好比将一张纸张直接放到了首页一样。

这也就是为什么我们不能够第一时间在应用的窗口接收到 WM_WINDOWPOSCHANGING 消息的原因(在接收到消息之前就可以看到桌面已经跑到最前面了,应用窗口最小化只是附带的状态)。

而对于快捷键 “Windows + M” 则处理有所不同,它是将所有窗口都直接最小化,而不保留可恢复的显示顺序。

先来谈谈可恢复式的状态切换:

除了要必须要发送的私有消息 0x435 以外,还需要发送私有消息 0x434 和 0x8B。

首先发送一条 0x434 消息,其 wParam 传递 hEvent ,lParam 为 0。然后,再紧接着发送一条 0x435 消息,并指定 lParam 为 1(TRUE,当 lParam 传递 FALSE 时,就会忽略后面的 0x8B 消息,那么就是彻底的最小化操作了),这将要求传递后面需要恢复的窗口顺序链表和前台窗口句柄。

最后发送一条或者两条 0x8B 消息,wParam 表示前台活动窗口句柄(焦点窗口),lParam 很可能是内部结构体的指针(包含窗口顺序信息)。

发送第一次 0x8B 消息,将置后所有窗口;发送第二次 0x8B 消息,将恢复窗口顺序。

下图展示了实际环境中先后两次按下 “显示桌面” 按钮的消息记录,其中 SHELLHOOK 消息属于背景噪声:

然后,我们再来谈谈最小化所有窗口的操作:

其实就是砍掉 0x434 消息,然后直接指定 0x435 消息的 lParam 为 0:

四、概念验证

如果我们消除所有系统注册表检查,最重要的是调用 IsShakedAllowedInSku,我们可以创建以下 POC 函数,它将最小化所有打开的窗口,并且无法在重复操作中恢复:

#include <iostream>
#include <Windows.h>
#include <cassert>#define REBARCLASSNAMEW L"ReBarWindow32"void MinimizeAllWindows()
{if (HWND hwnd = FindWindowW(L"Shell_TrayWnd", 0)){ULONG dwProcessId;if (GetWindowThreadProcessId(hwnd, &dwProcessId)){AllowSetForegroundWindow(dwProcessId);HWND hWndChild = 0;while (hWndChild = FindWindowExW(hwnd,hWndChild,REBARCLASSNAMEW,0)){if (HWND hwndTask = FindWindowEx(hWndChild,0,L"MSTaskSwWClass",0 /*name*/)){if (HANDLE hProcess = OpenProcess(PROCESS_DUP_HANDLE,FALSE,dwProcessId)){if (HANDLE hEvent = CreateEvent(0, 0, 0, 0)){HANDLE hRemoteEvent;if (DuplicateHandle(GetCurrentProcess(),hEvent,hProcess,&hRemoteEvent,0,0,DUPLICATE_SAME_ACCESS)){if (PostMessageW(hwndTask,0x435,(WPARAM)hRemoteEvent,0)){ULONG n;while (1 == (n =MsgWaitForMultipleObjectsEx(1,&hEvent,2000,QS_ALLINPUT,MWMO_INPUTAVAILABLE))){MSG msg;while (PeekMessageW(&msg, 0, 0, 0, PM_REMOVE)){DispatchMessageW(&msg);}}if (WAIT_OBJECT_0 != n){assert(false);}}DuplicateHandle(hProcess,hRemoteEvent,0,0,0,0,DUPLICATE_CLOSE_SOURCE);}CloseHandle(hEvent);}CloseHandle(hProcess);}break;}}}}
}int main()
{MinimizeAllWindows();return 0;
}

紧接着是可以恢复的操作(例如 显示桌面):

#include <iostream>
#include <Windows.h>
#include <cassert>#define REBARCLASSNAMEW L"ReBarWindow32"void ToggleAllWindows(_In_ BOOL bDialogsToo, _In_opt_ HWND hwndMy = 0)
{if (HWND hwnd = FindWindowW(L"Shell_TrayWnd", 0)){ULONG dwProcessId;if (GetWindowThreadProcessId(hwnd, &dwProcessId)){AllowSetForegroundWindow(dwProcessId);HWND hWndChild = 0;while (hWndChild = FindWindowExW(hwnd,hWndChild,REBARCLASSNAMEW,0)){if (HWND hwndTask = FindWindowEx(hWndChild,0,L"MSTaskSwWClass",0 /*name*/)){if (HANDLE hProcess = OpenProcess(PROCESS_DUP_HANDLE,FALSE,dwProcessId)){if (HANDLE hEvent = CreateEvent(0, 0, 0, 0)){HANDLE hRemoteEvent;if (DuplicateHandle(GetCurrentProcess(),hEvent,hProcess,&hRemoteEvent,0,0,DUPLICATE_SAME_ACCESS)){// 新增 1 PostMessageW(hwndTask,0x434,(WPARAM)hRemoteEvent,           // 在显示桌面时触发它0);// 原始的if (PostMessageW(hwndTask,          // 只传入 0x435 私有消息,则 Shake Window 时最小化的效果0x435,(WPARAM)hRemoteEvent,(LPARAM)bDialogsToo))   // bDialogsToo 为 1 ,则需要提供窗口列表以供恢复时使用;// 如果为 0, 则类似 Win + M ,直接最小化所有窗口而不会在再此调用时恢复{// 新增 2 PostMessageW(hwndTask,0x8B,(WPARAM)hwndMy,     // wParam 为显示桌面前前台窗口的句柄。// 猜测 Shell 会保存 ZOrder 小于它的所有窗口层次(LPARAM)0x03D8F8E0);  // 控制 ZOrderList 的保存和恢复,// 每次建立桌面 wParam 和 lParam 都会变// 如果传入无效的值,将默认再次回到桌面ULONG n;while (1 == (n =MsgWaitForMultipleObjectsEx(1,&hEvent,2000,QS_ALLINPUT,MWMO_INPUTAVAILABLE))){MSG msg;while (PeekMessageW(&msg, 0, 0, 0, PM_REMOVE)){DispatchMessageW(&msg);}}if (WAIT_OBJECT_0 != n){assert(false);}}DuplicateHandle(hProcess,hRemoteEvent,0,0,0,0,DUPLICATE_CLOSE_SOURCE);}CloseHandle(hEvent);}CloseHandle(hProcess);}break;}}}}
}int main()
{Sleep(300);ToggleAllWindows(TRUE, (HWND)0x140E12);  // 窗口句柄system("pause");ToggleAllWindows(FALSE);system("pause");return 0;return 0;
}

“窗口摇动”是 Windows 资源管理器的一个便捷功能,如果您想最小化除一个窗口之外的所有窗口,这个功能会非常方便。

五、进一步研究如何自定义保留窗口列表

Rbmm 并没有提供完整的方案和代码,甚至上面的概念验证也是我修正的一部分,未来将在这里研究清楚 0x8B 的 lParam 以及 CZOrderManager 接口的更多细节。在该理论出现之前,我们使用下面的方法保留某个窗口的显示:

使用计时器或者事件 SetWinEventHook 来确定何时发生最小化,并使用 SetWindowsPos 恢复窗口的焦点,或者提前将窗口设置为桌面的子窗口或者给窗口添加 WS_EX_TOPMOST 扩展属性。但是在运用于 “显示桌面” 这种非消息传递引起的窗口层次变化上,有着缺陷。

未来,我保守估计将有下面的新方案:

方案一:检测 0x435 消息,并在消息处理前将我们的窗口设置为桌面的子窗口或者 WS_EX_TOPMOST,这将避免最小化,然后再再次按下 Win + D 时,恢复窗口设置。

方案二:需要研究清楚 0x8B 消息的 wParam 和 lParam ,它决定了Shell 最小化时要保留的窗口列表以及最小化设置。


原文出处链接:[https://blog.csdn.net/qq_59075481/article/details/139204241]。

本文发布于:2024.05.25,更新于:2024.05.26。

这篇关于编程实现标题栏窗口摇动——显示桌面的未公开细节研究的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1004058

相关文章

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time

Linux 网络编程 --- 应用层

一、自定义协议和序列化反序列化 代码: 序列化反序列化实现网络版本计算器 二、HTTP协议 1、谈两个简单的预备知识 https://www.baidu.com/ --- 域名 --- 域名解析 --- IP地址 http的端口号为80端口,https的端口号为443 url为统一资源定位符。CSDNhttps://mp.csdn.net/mp_blog/creation/editor

【Python编程】Linux创建虚拟环境并配置与notebook相连接

1.创建 使用 venv 创建虚拟环境。例如,在当前目录下创建一个名为 myenv 的虚拟环境: python3 -m venv myenv 2.激活 激活虚拟环境使其成为当前终端会话的活动环境。运行: source myenv/bin/activate 3.与notebook连接 在虚拟环境中,使用 pip 安装 Jupyter 和 ipykernel: pip instal

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

C#实战|大乐透选号器[6]:实现实时显示已选择的红蓝球数量

哈喽,你好啊,我是雷工。 关于大乐透选号器在前面已经记录了5篇笔记,这是第6篇; 接下来实现实时显示当前选中红球数量,蓝球数量; 以下为练习笔记。 01 效果演示 当选择和取消选择红球或蓝球时,在对应的位置显示实时已选择的红球、蓝球的数量; 02 标签名称 分别设置Label标签名称为:lblRedCount、lblBlueCount

Kubernetes PodSecurityPolicy:PSP能实现的5种主要安全策略

Kubernetes PodSecurityPolicy:PSP能实现的5种主要安全策略 1. 特权模式限制2. 宿主机资源隔离3. 用户和组管理4. 权限提升控制5. SELinux配置 💖The Begin💖点点关注,收藏不迷路💖 Kubernetes的PodSecurityPolicy(PSP)是一个关键的安全特性,它在Pod创建之前实施安全策略,确保P

工厂ERP管理系统实现源码(JAVA)

工厂进销存管理系统是一个集采购管理、仓库管理、生产管理和销售管理于一体的综合解决方案。该系统旨在帮助企业优化流程、提高效率、降低成本,并实时掌握各环节的运营状况。 在采购管理方面,系统能够处理采购订单、供应商管理和采购入库等流程,确保采购过程的透明和高效。仓库管理方面,实现库存的精准管理,包括入库、出库、盘点等操作,确保库存数据的准确性和实时性。 生产管理模块则涵盖了生产计划制定、物料需求计划、