Windows编程系列:设置资源管理器背景

偶然的机会,在github上发现了一个有趣且优秀的项目,https://github.com/Maplespe/explorerTool

这里学习了一下,并顺带学习了一下涉及的相关知识点。不得不感叹作者的厉害之处,能想到这种方法。

 

主要实现原理:

1、创建一个DLL,通过远线程注入的方式,注入资源管理器进程

2、在DLL被加载时时,对api进行hook,包括CreateWindowExW,DestroyWindow, BeginPaint, FillRect, CreateCompatibleDC等函数

3、通过ApiHook,在调用CreateWindows函数创建窗口时,如果是资源管理器的内容区域窗口,就将窗口记录下来,

 说明:不同版本的Windows版本窗口层级有变化,需要针对不同版本做不同处理

 

4、通过ApiHook,hook FillRect函数,在系统绘制完成之后 ,调用AlphaBlend函数,绘制背景图像

 

最终实现效果如上图所示,其它区域都不支持透明,所以没办法让背景全部透过去。

以前想过自己开发一个资源管理器,以便对UI进行完全控制。 但是资源管理器是一个大工程,时间有限,一直没有付诸行动。

从XP以后,再也没有实现过资源管理器背景,这种方法虽然是通过“外挂”的形式实现,但至少也弥补了我心中的一点遗憾。再次感谢原作者。

 

这个项目主要涉及以下几个知识点

1、GDI绘图(资源管理器的窗口是使用GDI技术进行绘制的,在user32.dll中)

2、ApiHook

3、DLL编程及DLL注入

 

下面会详细介绍这个功能是如何实现的。

 

ApiHook

关于ApiHook,可以参考前面的文章

https://www.cnblogs.com/zhaotianff/p/18073138,这篇文章中介绍了minHook库的使用。

 

GDI绘图

GDI绘图可以参考这两篇文章

https://www.cnblogs.com/zhaotianff/p/18108747

https://www.cnblogs.com/zhaotianff/p/18120634

掌握这部分理论,后面的内容就可以理解了。

 

DLL方面

如果在DLL编程还没有了解过,也可以参考 https://www.cnblogs.com/zhaotianff/p/15141399.html

在dllmain中调用apihook,示例代码如下:

 1 BOOL APIENTRY DllMain( HMODULE hModule,
 2                        DWORD  ul_reason_for_call,
 3                        LPVOID lpReserved
 4                      )
 5 {
 6     switch (ul_reason_for_call)
 7     {
 8     case DLL_PROCESS_ATTACH:
 9         if (NULL == g_hShellHookDllModule)
10         {
11         
12             //省略
13             //获取当前进程名并判断是否为explorer.exe
14             if (lstrcmp(szModuleName, L"explorer.exe") == 0)
15             {
16                 //开始hook
17                 StartHook();
18             }
19         }
20         break;
21     case DLL_THREAD_ATTACH:
22     case DLL_THREAD_DETACH:
23     case DLL_PROCESS_DETACH:
24         break;
25     }
26     return TRUE;
27 }

 

StartHook函数内部对api进行hook

 1 VOID StartHook()
 2 {
 3     //省略初始化代码
 4 
 5     if (MH_Initialize() != MH_OK)
 6         return;
 7 
 8     //调用minHook库中的函数对api进行hook
 9     MH_CreateHookEx(&CreateWindowExW, &MyCreateWindowExW, &InitCreateWindowExW);
10     MH_CreateHookEx(&DestroyWindow, &MyDestroyWindow, &InitDestroyWindow);
11     MH_CreateHookEx(&BeginPaint, &MyBeginPaint, &InitBeginPaint);
12     MH_CreateHookEx(&FillRect, &MyFillRect, &InitFillRect);
13     MH_CreateHookEx(&CreateCompatibleDC, &MyCreateCompatibleDC, &InitCreateCompatibleDC);
14 
15     MH_EnableHook(MH_ALL_HOOKS);
16 }

 

在窗口创建时,判断是否是资源管理器窗口,并进行记录

 1 HWND MyCreateWindowExW(DWORD dwExStyle, LPCWSTR lpClassName, LPCWSTR lpWindowName, DWORD dwStyle, int X, int Y, int nWidth, int nHeight, HWND hWndParent, HMENU hMenu, HINSTANCE hInstance, LPVOID lpParam)
 2 {
 3      HWND hwnd = InitCreateWindowExW(dwExStyle, lpClassName, lpWindowName,
 4          dwStyle, X, Y, nWidth, nHeight, hWndParent,
 5          hMenu, hInstance, lpParam);
 6 
 7      TCHAR szClassName[260]{};
 8      TCHAR szParentClassName[260]{};
 9      GetClassName(hwnd, szClassName, 260);
10      GetClassName(hWndParent, szParentClassName, 260);
11 
12      //通过窗口层级判断是否是内容区域句柄
13      if (lstrcmp(szClassName, L"DirectUIHWND") == 0
14          && lstrcmp(szParentClassName, L"SHELLDLL_DefView") == 0)
15      {
16          HWND hWndGranp = GetParent(hWndParent);
17          TCHAR szGranpClassName[260]{};
18          GetClassName(hWndGranp, szGranpClassName, 260);
19 
20          if (lstrcmp(szGranpClassName, L"ShellTabWindowClass") == 0)
21          {
22              ShellWindow shellwindow;
23              shellwindow.hwnd = hwnd;
24  
25              //以当前线程ID为Key将窗口句柄记录下来
26              g_mapShellWindow[GetCurrentThreadId()] = shellwindow;
27          }
28      }
29 
30      return hwnd;
31 }

 

这里我们创建了一个记录句柄和HDC的数据结构,它的作用是将窗口句柄和HDC成对保存起来。

定义如下:

1 typedef struct _ShellWindow
2 {
3     HWND hwnd;
4     HDC hdc;
5     SIZE size;
6 }ShellWindow,*PShellWindow;

 

在窗口被销毁时,从记录的窗口列表中移除窗口

 1 BOOL MyDestroyWindow(HWND hwnd)
 2 {
 3     std::unordered_map<DWORD, ShellWindow>::iterator iter = g_mapShellWindow.find(GetCurrentThreadId());
 4 
 5     if (iter != g_mapShellWindow.end() && hwnd == iter->second.hwnd)
 6     {
 7         g_mapShellWindow.erase(iter);
 8     }
 9 
10     return InitDestroyWindow(hwnd);
11 }

 

BeginPaint

BeginPaint是获取客户区窗口DC的一种方式,这部分涉及GDI绘图,可以参考前面的文章。

获取HDC后,将HDC记录到窗口对应的HDC里

 1 HDC MyBeginPaint(HWND hWnd, LPPAINTSTRUCT lpPaint)
 2 {
 3     HDC hdc =  InitBeginPaint(hWnd, lpPaint);
 4 
 5     auto iter = g_mapShellWindow.find(GetCurrentThreadId());
 6 
 7     if (iter != g_mapShellWindow.end() && hWnd == iter->second.hwnd)
 8     {
 9         iter->second.hdc = hdc;
10     }
11 
12     return hdc;
13 }

 

CreateCompitableDC

CreateCompatibleDC 函数创建与指定设备兼容的内存设备上下文 (DC) 。

它会在BeginPaint之后调用,创建与指定设备兼容的DC,我们将新创建出来的DC替换原来的DC,并记录下来

HDC MyCreateCompatibleDC(HDC hDc)
{
    HDC retHdc =  InitCreateCompatibleDC(hDc);

    auto iter = g_mapShellWindow.find(GetCurrentThreadId());
    if (iter != g_mapShellWindow.end() && iter->second.hdc == hDc)
    {
        iter->second.hdc = retHdc;
    }
    return retHdc;
}

 

FillRect

有了前面的拿到的HDC,我们可以在系统对窗口内容区域进行填充完成后,将背景绘制到窗口上。

AlphaBlend 函数的功能是显示具有透明或半透明像素的位图。支持透明。

 1 int MyFillRect(HDC hDC, RECT* lprc, HBRUSH hbr)
 2 {
 3    
 4         //省略
 5         BLENDFUNCTION bf = { AC_SRC_OVER, 0, img->opactiy, AC_SRC_ALPHA };
 6         //绘制背景
 7         AlphaBlend(hDC, pos.x, pos.y, dstSize.cx, dstSize.cy, img->hdc, 0, 0, img->size.cx, img->size.cy, bf);
 8     }
 9     return ret;
10 }

 

到这里基本已经实现全部功能了,在DLL释放时,再释放申请的资源就可以了。

 

还有一些细节方面,没有进行详细说明,可以自行阅读源码了解。

 

示例代码

完整的实现代码可以参考原作者的项目地址:https://github.com/Maplespe/explorerTool

也可以参考我仓库里的代码:https://github.com/zhaotianff/WindowsX/tree/main/WindowsX/WindowsX.Core.ShellHook

原作者的最新版本已经改为COM实现了,我这里还是使用的远线程注入的方式实现。

posted @ 2024-04-22 23:14  zhaotianff  阅读(42)  评论(0编辑  收藏  举报