【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變化


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