【LibUIDK界面库系列文章】制作个性化桌面图标

作者:刘树伟
日期:2014-04-10

一、前言
“暴风看电影”的桌面图标,比其它图标都大一号,并且鼠标移动到图标上后,图标还会动起来。“迅雷看看高清影视”也是一个动画图标,鼠标移动到图标上,图标就会动态显示。本篇文章,我们就讨论如何自定义桌面图标。

二、分析
自定义桌面图标,大致有这么几种方式:
1. 子类化桌面,然后自绘图标;
2. Hook 绘制图标的API,然后替换图标、位置、大小等参数
3. 实现IShellIconOverlayIdentifiers接口,在已有的图标上,再覆盖一个新的图标。
4. 创建一个UpdateLayeredWindow过的窗口,在窗口上显示图标,并且窗口的所有消息都转发到桌面(假图标)。

我们逐一分析一下各种方法的优缺点。
1. 子类化:
这是最容易想到,也较容易实现的一种方式。只需要得到桌面窗口句柄,然后子类化桌面,处理WM_PAINT消息后,绘制我们自己的图标即可。难点是处理刷新问题,还有拖动图标时的显示问题,以避免原图标原形毕露。

2. Hook 绘制图标的API:
这应该是最完美的一种实现,只要Hook到绘制图标的API后,就可以直接向HDC参数中绘制我们想要的效果,并且不用考虑任何刷新问题,因为需要刷新的时候,系统会自动调用我们Hook的API的。并且也不用考虑拖动问题。

3. IShellIconOverlayIdentifiers:
IShellIconOverlayIdentifiers是微软提供的一个COM组件,用来为已经有图标上,再覆盖一个新的小图标。一些需要同步的软件,常常会使用这种方法,在图标上覆盖一个小图标,用来表示等待同步、正在同步及同步完成等状态。这是修改图标外观比较标准的一种方式,但缺点是,覆盖的小图标的大小和位置,无法自定义,它总是小于原图标。往往达不到我们的要求。优点是,不用考虑刷新和拖动图标的问题。

4. 假图标方式:
使用一个UpdateLayeredWindow过的窗口,窗口上绘制要显示的图标,然后根据图标的位置,来移动窗口。还需要把所有发往窗口的消息,全部转发到桌面上。这应该是所有方法中,最差的一种。“迅雷看看高清影视”似乎使用的就是这种方式。

注,在测试Hook API修改桌面图标时,得到以下一些成果:
1. 如果只想改变桌面图标下面的文字,可以通过Hook DrawTextW来完成,而不是常用的ExtTextOutW。
2. 任务栏托盘中的某些图标,是通过DrawIconEx来绘制的,比如QQ图标,如果想修改这些图标,可以Hook DrawIconEx。
3. XP及XP以上系统,支持Alpha任务栏,如果想在任务栏中加上自己的图标,需要先绘制父窗口部分,可以调用DrawThemeParentBackground。也可以Hook这个API,做一些事情。
4. XP及XP以上系统,如果想修改图标高亮及选中状态后的背景,可以通过Hook DrawThemeBackground实现。
5. 在Win7下(其它系统未测试),当文字输出区域在“我的电脑”等系统图标区域内时,字体无阴影,只有在Item之外输出,才有阴影。在普通图标区域,无此问题。但是后来不知道经过什么设置,普通图标也有这个问题,规律未掌握。
6. 在Win7下(其它系统未测试),当桌面背景切换为为纯色时,文字带阴影,但只要一重启桌面,文字就不带阴影了,并且如果是深色桌面背景,那么文字就是白色,如果是浅色背景,文字就是黑色。这时,如果切换一下桌面背景,文字又带阴影了。
7. 当使用DrawShadowText自己绘制图标文字时,当切换桌面背景时,字体就花了,需要进一步研究原因。

由于目前没有找到桌面是使用哪个API来绘制的图标,所以我们暂时放弃使用Hook方式,而是使用子类化,来自定义桌面图标。过程中,需要接合Hook API的方法。

三、通过子类化桌面,自定义桌面图标
3.1 得到桌面句柄
子类化桌面,需要首先把DLL注入桌面,所以,我们的功能,一定是以DLL形式封装的。关于如何把DLL注入桌面,及注入后如何调试,请参考《让DLL随系统自启动.txt》。这里,需要制作一个COM形式的DLL工程,假设工程名为:IUIDesktopIcon。

当桌面进程加载IUIDesktopIcon.dll时,我们需要得到桌面句柄,然后子类化它。但由于这时,桌面可能还未启动,所以句柄为NULL,为此,我们需要启动一个新的线程,每隔一小段时间,就去查询桌面句柄是否有效,直到有效后,子类化它。代码如下:

#define GETPROP_OLDWNDPROC     _T("IUI_OLD_WNDPROC")

LRESULT APIENTRY DesktopList_WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
 // Call the old window proc
 WNDPROC oldWndProc = (WNDPROC)GetProp(hwnd, GETPROP_OLDWNDPROC);

 if (oldWndProc != NULL)
  return CallWindowProc(oldWndProc, hwnd, uMsg, wParam, lParam);

 ASSERT(FALSE);
 return 0;
}

DWORD WINAPI GetDesktopListHWND(LPVOID lpParameter)
{
 while (true)
 {
  HWND hWndDesktop = GetDesktopListWindow(); // 见《得到桌面ListView句柄.txt》
  if (hWndDesktop != NULL)
  {
   WNDPROC procOldDesktopList = (WNDPROC)::SetWindowLongPtr(hWndDesktop, GWLP_WNDPROC, (LONG_PTR)DesktopList_WndProc);
   SetProp(hWndDesktop, GETPROP_OLDWNDPROC, procOldDesktopList); // 保存旧的窗口过程。

   break;
  }

  Sleep(1);
 }

 return 0;
}

typedef unsigned (__stdcall *PTHREAD_START)(void *);
#define chBEGINTHREADEX(psa, cbStackSize, pfnStartAddr, pvParam, dwCreateFlags, pdwThreadID) \
 ((HANDLE)_beginthreadex((void *)(psa), (unsigned)(cbStackSize), (PTHREAD_START)(pfnStartAddr), (void *)(pvParam), (unsigned)(dwCreateFlags), (unsigned *)(pdwThreadID)))

BOOL CIUIDesktopIconApp::InitInstance()
{
 DWORD dwThreadID = 0;
 HANDLE hThread = chBEGINTHREADEX(NULL, 0, GetDesktopListHWND, NULL, 0, &dwThreadID);
 if (hThread != NULL)
 {
  // Don't call WaitForSingleObject, it will cause lock.
  // WaitForSingleObject(hThread, INFINITE);
 }
 CloseHandle(hThread);

 return CWinApp::InitInstance();
}

如果你觉得这种方式不标准,也不安全(在DLL加载时做过多事情,可能导致崩溃或死锁等不可预知的问题),可以实现IShellIconOverlayIdentifiers组件,实现它的三个导出接口,当系统第一次调用这些接口时,正是桌面已经有效,并且开始绘制第一个非系统图标时(如果桌面只有“我的电脑”、“回收站”这些系统图标时,不会调用IShellIconOverlayIdentifiers接口),这个时机恰恰好,最终,我们就是采用IShellIconOverlayIdentifiers来制作,详细情况,可以参考《制作Overlay图标.txt》。

3.2 自绘桌面图标
子类化桌面后,可以响应WM_PAINT消息,让桌面先默认画一遍,然后找到自己的图标,得到图标的位置后,绘制一个自己的图标。

自定义的桌面图标大致有以下这几种表现形式:
a. 和其它普通桌面图标类似,图标大小也正常,只是让图标动起来,或者图标上叠加图标。保留图标的Highlight、Selected、Focus等显示状态,保留图标文字。
b. 图标比其它桌面图标更大,不再需要表现图标的Highlight、Selected、Focus等显示状态,也不需要图标文字(如“暴风看电影”的桌面图标)

对于形式a:
由于除了图标本身,保留其它所有属性,所以考虑采用把WM_PAINT默认内容绘制到内存DC上,然后再默认内容上绘制自己的图标的方式。
如果新图标icon的大小,比原来的图标大,图标的非Alpha区域可以完全挡住原来的图标时,不需要做过多额外的处理,直接绘制即可;
如果新图标icon的大小,比原来的图标小,如果直接绘制上去,原来的图标就会露出一部分来。所以需要想个办法,把原来的图标直接去掉,不让它显示。桌面List控件使用的是LVS_OWNERDATA风格,即vitual list。数据需要通过向父窗口发送LVN_GETDISPINFO通知消息请求数据。于是我们想到两种方法来实现修改桌面图标。
● 子类化桌面的父窗口SHELLDLL_DefView,处理LVN_GETDISPINFO(W)通知,然后把NMLVDISPINFOW::item::iImage设置成-1。这样图标就不会显示了。在实践过程中发现,如果是通过在线程中查找桌面窗口句柄和父窗口句柄子类化SHELLDLL_DefView后,父窗口一直收到不LVN_GETDISPINFOW通知,但SPY++可以收到,所以,有可能是多线程同步导致的。后来改为IShellIconOverlayIdentifiers方式,子类化父窗口后,就可以正确的接收到LVN_GETDISPINFO(W)通知了,代码如下:
LRESULT APIENTRY DesktopParent_WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
 // Get the old window proc
 WNDPROC oldWndProc = (WNDPROC)GetProp(hwnd, GETPROP_OLDWNDPROC);

 if (uMsg == WM_SETCURSOR || uMsg == WM_PRINTCLIENT || uMsg == WM_TIMER || uMsg == WM_ERASEBKGND)
  return CallWindowProc(oldWndProc, hwnd, uMsg, wParam, lParam);

 if (uMsg == WM_NOTIFY)
 {
  NMHDR *pnmhdr = (NMHDR *)lParam;

  // For virtual desktop list
  if (pnmhdr->code == LVN_GETDISPINFOW)
  {
   NMLVDISPINFO *plvDispInfo = (NMLVDISPINFO *)lParam;
   if (plvDispInfo->item.mask & LVIF_IMAGE)
   {
    LRESULT lr = CallWindowProc(oldWndProc, hwnd, uMsg, wParam, lParam);

    CStringW strItemW = STRING_ICON;
    if (strItemW == plvDispInfo->item.pszText)
    {
     // 通过把图标索引改为一个不存在的图标,达到禁止显示图标的目的。
     // 也可以把索引指向一个真实存在的图标,达到修改图标的目的。
     // 如果指向-1,那么图标item在拖动时,也是不显示图标的,
     // 所以,我们可以区分图标是否在拖动这种行为,
     // 如果图标正在拖动,我们可以指向一个我们希望显示的图标索引,否则指向-1。

     if (g_bShellGetDragImageMsg) // While dragging the item.
      plvDispInfo->item.iImage = 21; // 指向我们希望显示的图标索引,这个图标,是通过实现IShellIconOverlayIdentifier接口提供的。详见下面的杂项部分。
     else
      plvDispInfo->item.iImage = -1;
    }

    return lr;
   }
  }
 }

 if (oldWndProc != NULL)
  return CallWindowProc(oldWndProc, hwnd, uMsg, wParam, lParam);

 ASSERT(FALSE);
 return 0;
}

// 在线程中子类化父窗口,收不到LVN_GETDISPINFOW通知。测试线程同步后,能否收到。建议使用IShellIconOverlayIdentifiers方式。
DWORD WINAPI GetDesktopListHWND(LPVOID lpParameter)
{
 while (true)
 {
  g_hWndDesktop = GetDesktopListWindow();
  if (g_hWndDesktop != NULL)
  {
   // Subclass desktop's parent
   HWND hParent = GetParent(g_hWndDesktop);
   WNDPROC procOldDesktopParent = (WNDPROC)::SetWindowLongPtr(hParent, GWLP_WNDPROC, (LONG_PTR)DesktopParent_WndProc);
   SetProp(hParent, GETPROP_OLDWNDPROC, procOldDesktopParent);

   break;
  }

  Sleep(1);
 }

 return 0;
}

● Hook SendMessageW,截获桌面窗口向父窗口发送的LVN_GETDISPINFOW通知,然后把NMLVDISPINFOW::item::iImage设置成-1。此方法经过实践可行,代码如下:
////////////////////////////////////////////////////////////////////////
Hook SendMessageW
typedef LRESULT (WINAPI *PFNSMSGW)(__in HWND hWnd, __in UINT Msg, __in WPARAM wParam, __in LPARAM lParam);
extern CAPIHook g_SendMessageW;
LRESULT WINAPI Hook_SendMessageW(__in HWND hWnd, __in UINT Msg, __in WPARAM wParam, __in LPARAM lParam)
{
 if (Msg == WM_NOTIFY)
 {
  HWND hDesktopParent = ::GetParent(g_hWndDesktop);
  NMHDR *pnmhdr = (NMHDR *)lParam;
  if (pnmhdr->code == LVN_GETDISPINFOW && hWnd == hDesktopParent)
  {
   NMLVDISPINFOW *plvDispInfo = (NMLVDISPINFOW *)lParam;

   if (plvDispInfo->item.mask & LVIF_IMAGE)
   {
    LRESULT lr = ((PFNSMSGW)(PROC)g_SendMessageW)(hWnd, Msg, wParam, lParam);

    CStringW strItemW = STRING_ICON;
    if (strItemW == plvDispInfo->item.pszText)
    {
     plvDispInfo->item.iImage = -1;
    }

    return lr;
   }
  }
 }
 return ((PFNSMSGW)(PROC)g_SendMessageW)(hWnd, Msg, wParam, lParam);
}
CAPIHook g_SendMessageW("user32.dll", "SendMessageW", (PROC)Hook_SendMessageW);

采用Hook SendMessageW绘制出来的桌面Item,除了没有图标,一切显示和表现行为,都是常规图标完全一致,例如Highlight、Selected及Focus状态的显示、框选等。但拖动Item时,Drag image也是无图标的。不过这个容易解决,因为在拖动Item时,会向桌面List本身发送ShellGetDragImage消息(详见下文),并且也会向桌面父窗口发送LVN_GETDISPINFOW通知。所以只需要在收到ShellGetDragImage消息后的LVN_GETDISPINFOW通知中,把iImage恢复,或者不处理LVN_GETDISPINFOW通知。
我们只需要在采用Hook SendMessageW绘制出来的桌面Item上,绘制自已的图标,就可以了。

对于形式b:
如果自己绘制出来的Item,没有原来的Item大,则需要考虑禁止显示Item的Highlight、Selected等状态背景、禁止显示Item的文字。
可以Hook DrawThemeBackground和DrawTextW分别禁止显示状态背景和文字,但这种方法有些麻烦,并且也容易造成程序不稳定。另一种方法是任由桌面绘制出这些元素后,再调用DrawThemeParentBackground把这些元素盖住。盖住后,原来Item这块区域看上去就是个只显示桌面背景的空白区域了,我们可以在这块区域中,发挥想像力,随意绘制出自己的桌面Item。代码如下:
LRESULT APIENTRY DesktopList_WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
 // Call the old window proc
 WNDPROC oldWndProc = (WNDPROC)GetProp(hwnd, GETPROP_OLDWNDPROC);

 if (uMsg == WM_PAINT)
 {
  // If create the timer window at GetDesktopListHWND, it will be destroy after created.
  if (g_wndTimer.GetSafeHwnd() == NULL)
  {
   BOOL bRet = g_wndTimer.LoadFrame(0, WS_CLIPCHILDREN, NULL, g_hIns);
  }

  //
  // Get custom icon information
  //
  BOOL bItemCut = FALSE;
  BOOL bItemDropHilited = FALSE;
  BOOL bItemFocus = FALSE;
  BOOL bItemSelected = FALSE;
  BOOL bItemCursorIn = FALSE;

  int nCount = ListView_GetItemCount(hwnd);
  for (int i = 0; i < nCount; ++i)
  {
   CString strItem;
   ListView_GetItemText(hwnd, i, 0, strItem.GetBufferSetLength(MAX_PATH), MAX_PATH);

   if (strItem == STRING_ICON)
   {
    ListView_GetItemRect(hwnd, i, g_rcItem, LVIR_BOUNDS);
    ListView_GetItemRect(hwnd, i, g_rcIcon, LVIR_ICON);
    ListView_GetItemRect(hwnd, i, g_rcLabel, LVIR_LABEL);
    bItemCut = (ListView_GetItemState(hwnd, i, LVIS_CUT) & LVIS_CUT);
    bItemDropHilited = (ListView_GetItemState(hwnd, i, LVIS_DROPHILITED) & LVIS_DROPHILITED);
    bItemFocus = (ListView_GetItemState(hwnd, i, LVIS_FOCUSED) & LVIS_FOCUSED);
    bItemSelected = (ListView_GetItemState(hwnd, i, LVIS_SELECTED) & LVIS_SELECTED);

    CPoint pt;
    GetCursorPos(&pt);
    bItemCursorIn = g_rcItem.PtInRect(pt);

    break;
   }
  }

  //
  // If the custom icon not exist, use default draw
  //
  if (g_rcItem.IsRectEmpty())
   return CallWindowProc(oldWndProc, hwnd, uMsg, wParam, lParam);

  //
  CPaintDC dc(CWnd::FromHandle(hwnd));

  CRect rcClient;
  ::GetClientRect(hwnd, rcClient);

  //
  // Draw the default content to the dcDefaultContent
  //
  CDC dcDefaultContent;
  dcDefaultContent.CreateCompatibleDC(&dc);
  CBitmap bmpDefaultContent;
  bmpDefaultContent.CreateCompatibleBitmap(&dc, rcClient.Width(), rcClient.Height());
  CBitmap *pbmpTextOld = dcDefaultContent.SelectObject(&bmpDefaultContent);
  HFONT hFontOld = (HFONT)::GetCurrentObject(dcDefaultContent.GetSafeHdc(), OBJ_FONT); // store the Font object before call DefWindowProc

  ::CallWindowProc(oldWndProc, hwnd, uMsg, (WPARAM)dcDefaultContent.GetSafeHdc(), lParam);

  //
  // Draw parent part at custom icon to make custom item transparent.
  //
  DrawThemeParentBackground(hwnd, dcDefaultContent.GetSafeHdc(), g_rcItem);

  //
  // Draw custom item here...
  //

  //
  // Show
  //
  dc.BitBlt(rcClient.left, rcClient.top, rcClient.Width(), rcClient.Height(), &dcDefaultContent, rcClient.left, rcClient.top, SRCCOPY);

  //
  // Free
  //
  dcDefaultContent.SelectObject(pbmpTextOld);
  dcDefaultContent.SelectObject(hFontOld);

  return 0;
 }

 if (oldWndProc != NULL)
  return CallWindowProc(oldWndProc, hwnd, uMsg, wParam, lParam);

 return 0;
}

3.3 处理图标拖动
上述讨论,解决了图标绘制的问题,接下来解决图标拖动的问题。
在拖动图标时,仍然显示的是原图标,这显然不是我们想要的。通过SPY++,我们发现,在拖动图标时,桌面窗口会收到一个名字为“ShellGetDragImage”的注册消息,从名字上看,它是请求拖动时显示的图像的消息,实际也确实如此。
在MSDN中,并不能找到“ShellGetDragImage”这个消息,于是我们搜索VS2008安装目录,未找到“ShellGetDragImage”,接着我们搜索C:\Program Files\Microsoft SDKs\Windows\v6.0A目录,在ShObjIdl.h和ShObjIdl.idl中找到了“ShellGetDragImage”。在ShObjIdl.idl中“ShellGetDragImage”定义如下:
cpp_quote("#define DI_GETDRAGIMAGE     TEXT(\"ShellGetDragImage\")")
在DI_GETDRAGIMAGE关键字旁边,找到这么一句话:
//   If you wish to be able to source a drag image from a custom control,
//     implement a handler for the RegisterWindowMessage(DI_GETDRAGIMAGE).
//     The LPARAM is a pointer to an SHDRAGIMAGE structure.
翻译成中文意思为:如果你想指定自己的拖动图标,可以处理RegisterWindowMessage(DI_GETDRAGIMAGE)返回值代表的消息,消息的LPARAM参数为一个指向 SHDRAGIMAGE结构体的指针。在MSDN中,查找SHDRAGIMAGE,定义如下:
typedef struct tagSHDRAGIMAGE {
    SIZE sizeDragImage;  // 图标大小
    POINT ptOffset;  // 鼠标相对于图标左上角偏移
    HBITMAP hbmpDragImage; // 图标句柄
    COLORREF crColorKey; // 遮罩色
} SHDRAGIMAGE, *LPSHDRAGIMAGE;
其中,hbmpDragImage的创建,MSDN解释如下:
Use the following procedure to create the drag image.
Create a bitmap of the size specified by sizeDragImage with a handle to a device context (HDC) that is compatible with the screen.
Draw the bitmap.
Select the bitmap out of the HDC it was created with.
Destroy the HDC.
Assign the bitmap handle to hbmpDragImage.
意思为:
创建一个尺寸为sizeDragImage的与屏幕DC兼容的内存兼容位图;
把新建位图选入内存兼容DC;
在内存兼容DC中,绘制图标;
把内存兼容位图选出内存兼容DC;
销毁DC。
这时,这张内存兼容位图,就是我们需要的,把它赋值给hbmpDragImage即可。

我们需要先调用RegisterWindowMessage(DI_GETDRAGIMAGE)返回消息ID,这一步可以在子类化桌面之前:
DWORD WINAPI GetDesktopListHWND(LPVOID lpParameter)
{
 while (true)
 {
  HWND hWndDesktop = GetDesktopListWindow();
  if (hWndDesktop != NULL)
  {
   // Get the ID of ShellGetDragImage message. before drag the desktop icon, the "ShellGetDragImage" message will be send.
   g_uShellGetDragImageMsg = RegisterWindowMessage(DI_GETDRAGIMAGE); // g_uShellGetDragImageMsg是一个类型为UINT的全局变量。

   WNDPROC procOldDesktopList = (WNDPROC)::SetWindowLongPtr(hWndDesktop, GWLP_WNDPROC, (LONG_PTR)DesktopList_WndProc);
   SetProp(hWndDesktop, GETPROP_OLDWNDPROC, procOldDesktopList);

   break;
  }

  Sleep(1);
 }

 return 0;
}

响应“ShellGetDragImage”消息完整代码如下:
LRESULT APIENTRY DesktopList_WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
 // Call the old window proc
 WNDPROC oldWndProc = (WNDPROC)GetProp(hwnd, GETPROP_OLDWNDPROC);

 if (uMsg == WM_PAINT)
 {
  // If create the timer window at GetDesktopListHWND, it will be destroy after created.
  if (g_wndTimer.GetSafeHwnd() == NULL)
  {
   BOOL bRet = g_wndTimer.LoadFrame(0, WS_CLIPCHILDREN, NULL, g_hIns);
  }

  //
  // Get custom icon information
  //
  BOOL bItemCut = FALSE;
  BOOL bItemDropHilited = FALSE;
  BOOL bItemFocus = FALSE;
  BOOL bItemSelected = FALSE;
  BOOL bItemCursorIn = FALSE;

  int nCount = ListView_GetItemCount(hwnd);
  for (int i = 0; i < nCount; ++i)
  {
   CString strItem;
   ListView_GetItemText(hwnd, i, 0, strItem.GetBufferSetLength(MAX_PATH), MAX_PATH);

   if (strItem == STRING_ICON)
   {
    ListView_GetItemRect(hwnd, i, g_rcItem, LVIR_BOUNDS);
    ListView_GetItemRect(hwnd, i, g_rcIcon, LVIR_ICON);
    ListView_GetItemRect(hwnd, i, g_rcLabel, LVIR_LABEL);
    bItemCut = (ListView_GetItemState(hwnd, i, LVIS_CUT) & LVIS_CUT);
    bItemDropHilited = (ListView_GetItemState(hwnd, i, LVIS_DROPHILITED) & LVIS_DROPHILITED);
    bItemFocus = (ListView_GetItemState(hwnd, i, LVIS_FOCUSED) & LVIS_FOCUSED);
    bItemSelected = (ListView_GetItemState(hwnd, i, LVIS_SELECTED) & LVIS_SELECTED);

    CPoint pt;
    GetCursorPos(&pt);
    bItemCursorIn = g_rcItem.PtInRect(pt);

    break;
   }
  }

  //
  // If the custom icon not exist, use default draw
  //
  if (g_rcItem.IsRectEmpty())
   return CallWindowProc(oldWndProc, hwnd, uMsg, wParam, lParam);

  //
  CPaintDC dc(CWnd::FromHandle(hwnd));

  CRect rcClient;
  ::GetClientRect(hwnd, rcClient);

  //
  // Draw the default content to the dcDefaultContent
  //
  CDC dcDefaultContent;
  dcDefaultContent.CreateCompatibleDC(&dc);
  CBitmap bmpDefaultContent;
  bmpDefaultContent.CreateCompatibleBitmap(&dc, rcClient.Width(), rcClient.Height());
  CBitmap *pbmpTextOld = dcDefaultContent.SelectObject(&bmpDefaultContent);
  HFONT hFontOld = (HFONT)::GetCurrentObject(dcDefaultContent.GetSafeHdc(), OBJ_FONT); // store the Font object before call DefWindowProc

  ::CallWindowProc(oldWndProc, hwnd, uMsg, (WPARAM)dcDefaultContent.GetSafeHdc(), lParam);

  //
  // Draw parent part at custom icon to make custom item transparent.
  //
  DrawThemeParentBackground(hwnd, dcDefaultContent.GetSafeHdc(), g_rcItem);

  //
  // Draw custom item here...
  //

  //
  // Show
  //
  dc.BitBlt(rcClient.left, rcClient.top, rcClient.Width(), rcClient.Height(), &dcDefaultContent, rcClient.left, rcClient.top, SRCCOPY);

  //
  // Free
  //
  dcDefaultContent.SelectObject(pbmpTextOld);
  dcDefaultContent.SelectObject(hFontOld);

  return 0;
 }
 //
 // 绘制Drag image. 如果自绘的图标和原图标一样大,我们可以通过处理LVN_GETDISPINFOW消息,让它在拖动时,指向一个有效的图标索引来实现,这样就不用自己绘制drag image了。
 //
 else if (uMsg == g_uShellGetDragImageMsg)
 {
  // 如果自绘的图标和原图标一样大,只要打开下面4行代码即可。
  /*
  g_bShellGetDragImageMsg = TRUE;
  LRESULT lr = CallWindowProc(oldWndProc, hwnd, uMsg, wParam, lParam);
  g_bShellGetDragImageMsg = FALSE;
  return lr;
  */

  //
  // Skip other icons.
  //
  CPoint pt;
  GetCursorPos(&pt);

  if (g_rcItem.IsRectEmpty() || !g_rcItem.PtInRect(pt))
   return CallWindowProc(oldWndProc, hwnd, uMsg, wParam, lParam);

  //
  // Create drag image.
  //
  HDC hDC = GetDC(hwnd);
  HDC hdcMem = ::CreateCompatibleDC(hDC);

  if (g_hbmpIcon != NULL)
   ::DeleteObject(g_hbmpIcon);
  g_hbmpIcon = ::CreateCompatibleBitmap(hDC, g_rcItem.Width(), g_rcItem.Height());
  HBITMAP hOldBmp = (HBITMAP)::SelectObject(hdcMem, g_hbmpIcon);

  // Draw drag icon
  HICON hIcon = (HICON)LoadImage(g_hIns, _T("C:\\1.ico"), IMAGE_ICON, 0, 0, LR_LOADFROMFILE); // If use LoadIcon load the icon from resource, the icon distortion.

  CRect rcIcon = g_rcIcon;
  rcIcon.OffsetRect(-g_rcItem.left, -g_rcItem.top);

  CRect rcLabel = g_rcLabel;
  rcLabel.OffsetRect(-g_rcItem.left, -g_rcItem.top);
  int nColor = 128;
  DrawIconAndText(hwnd, hdcMem, hIcon, rcIcon, rcLabel, RGB(nColor, nColor, nColor));

  DestroyIcon(hIcon);

  //
  ::SelectObject(hdcMem, hOldBmp);
  ::ReleaseDC(hwnd, hDC);

  //
  // Set properties of drag image.
  //
  SHDRAGIMAGE *pShDragImage = (SHDRAGIMAGE *)lParam;

  pShDragImage->sizeDragImage.cx = g_rcItem.Width();
  pShDragImage->sizeDragImage.cy = g_rcItem.Height();

  pShDragImage->ptOffset.x = pt.x - g_rcItem.left;
  pShDragImage->ptOffset.y = pt.y - g_rcItem.top;

  pShDragImage->hbmpDragImage = g_hbmpIcon;
  pShDragImage->crColorKey = RGB(255, 0, 255);

  return TRUE;
 }

 if (oldWndProc != NULL)
  return CallWindowProc(oldWndProc, hwnd, uMsg, wParam, lParam);

 return 0;
}


在MSDN的SHDRAGIMAGE结构体介绍中,在See Also段,指向了IDragSourceHelper::InitializeFromBitmap, IDragSourceHelper::InitializeFromWindow,这个IDragSourceHelper接口从名字上来看,似乎也是提供图标拖动相关功能的,有兴趣可以研究一下。
当桌面上包含自己的Item的多个Items同时被选中后,再按下自己的Item一起拖动这些Items时,不应该只显示自己的Item drag image.

四、总结
上面长篇大论说了很多,你可能都晕了。不过总结起来,绘制自己的桌面Item,一共需要操作下面几个元素,及操作每种元素对应的方法
1. 为了全透明,需要把桌面父窗口的内容绘制到桌面上。这是通过DrawThemeParentBackground实现。
2. Item的Highlight、Selected。通过DrawThemeBackground实现
3. Item的Focus状态背景,是把Item的Rect各边向内缩进1个像素后,调用DrawFocusRect实现。
3. 禁止Item显示图标。通过Hook SendMessageW后,处理LVN_GETDISPINFOW的WM_NOTIFY通知消息实现。
4. 禁止显示或修改Item文本。可以通过Hook SendMessageW后,处理LVN_GETDISPINFOW的WM_NOTIFY通知消息实现。也可以通过Hook DrawTextW实现。
5. 显示Item文本。可以通过DrawShadowText实现。
6. 处理拖动中的Item。可以响应#define DI_GETDRAGIMAGE     TEXT("ShellGetDragImage")消息实现。
7. DrawThemeParentBackground后,框选时,会有破绽。这是因为WM_PAINT绘制绘制的内容,是包含选框的,在绘制完之后,再调用DrawThemeParentBackground,相当于把选框内Alpha后的内容用父窗口背景盖掉了。


五、杂项
桌面的窗口树结构如下:
"Program Manager" Progman或WorkerW
 "" SHELLDLL_DefView
  "FolderView" SysListView32
   ...
桌面是SHELLDLL_DefView的一个子窗口,把桌面隐藏掉后,壁纸仍然显示,说明壁纸不是桌面的背景图。把SHELLDLL_DefView窗口隐藏掉,壁纸也仍然显示,说明壁纸也不是SHELLDLL_DefView的背景图,同样,把"Program Manager" Progman或WorkerW这一级隐藏掉,壁纸还是会显示。这说明,壁纸是0x10010这个顶级窗口的背景,而桌面窗口树结构中这些窗口,全是透明显示的。

在桌面上右键,取消对【View/Show desktop icons】菜单项的选择,然后打开SPY++,发现"FolderView" SysListView32变成了隐藏状态。也就是说,隐藏桌面图标,是通过隐藏桌面窗口的形式实现的。

因为桌面List的image list是系统image list,我们无法直接向其中加入我们自己的图标,所以无法在处理LVN_GETDISPINFOW消息时,指定我们自己的图标。但IShellIconOverlayIdentifier接口,提供了把自己图标作为Overlay图标插入系统imagelist的途径,所以,只要在自己的DLL资源中,加入一个图标,然后在实现IShellIconOverlayIdentifier::GetOverlayInfo中,指定图标索引为0,就可以把自己写的DLL中,索引为0的图标,插入到系统的Image list中。所以,虽然我们实现IShellIconOverlayIdentifier接口并不是为了给图标加上Overlay图标,但利用IShellIconOverlayIdentifier的加载时机,查找了桌面窗口句柄,并利于了它提供的icon。
STDMETHODIMP CIUIOverlayIcon::GetOverlayInfo(LPWSTR pwszIconFile, int cchMax, int *pIndex, DWORD *pdwFlags)
{
 // Get our module's full path
 GetModuleFileNameW(_AtlBaseModule.GetModuleInstance(), pwszIconFile, cchMax);

 // Use first icon in the resource
 *pIndex = 0;

 *pdwFlags = ISIOI_ICONFILE | ISIOI_ICONINDEX;

 return S_OK;
}


附:
详细实现,可参考Depot\Project\IUIDesktopIcon工程。

BUG:
1. 当修改桌面其它文件的文件名,把文件名删除时,会弹出个对话框提示“你必须键入一个文件名”的模式对话框,这时自定义桌面图标会出现黑框。
2. 要兼容DPI变化


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章