Windows下使用标准Shell接口遍历文件和文件夹

Windows下使用标准Shell接口遍历文件和文件夹

在Windows中,经常需要遍历一个文件夹或遍历一个磁盘。本文介绍如何使用标准Shell接口进行遍历。介绍过程中会逐步实现一个类似FileZilla的TreeView+ListView界面。

基础知识

Windows中的目录可理解为是一个树型结构,树的根是“桌面”,“桌面”中一般拥有“我的电脑”、“网上邻居”、“回收站”等文件夹。这我们称为命名空间。物理上,“桌面”一般位于“C:\Documents and Settings\用户名称\桌面”。

IShellFolder接口用于管理文件夹,所有Shell命名空间中的文件夹对象都暴露这个接口。我们可以通过SHGetDesktopFolder方法获得“桌面”的IShellFolder接口。当然,最后不使用的时候要调用IShellFolder的Release方法释放接口。

通过调用“桌面”的IShellFolder接口的EnumObjects方法获得IENUMIDLIST接口的指针。IENUMIDLIST接口用于遍历IShellFolder接口表示的文件夹下的所有对象(这里的对象是指文件或者文件夹)。通过IENUMIDLIST接口可以遍历子对象的ITEMIDLIST数组。ITEMIDLIST数组表示一个对象的绝对或者相对路径。当前可以将ITEMIDLIST数组理解为给Shell使用的,代替我们常用的“WINDOWS\system32”这样的路径表示形式。而这里通过IENUMIDLIST接口遍历获得的IENUMIDLIST数组是一个相对路径,相对于当前IShellFolder的路径。调用IShellFolder的BindToObject方法,并传递IENUMIDLIST数组的相对路径,可以获得IENUMIDLIST数组表示的子文件夹的IShellFolder接口。这样我们可以通过递归或者循环遍历桌面开始的整个逻辑目录树。

Shell命名空间(翻译自MSDN)

简介

下面详细介绍一下上面提到的Shell使用的路径。Shell管理的文件和文件夹有存储在磁盘上的,也有不存储在磁盘上的,如网络打印机网络邻居控制面板回收站。这些不存储在磁盘上的文件或文件夹成为虚拟对象。像“网络打印机”这样的虚拟对象,根本不需要存储在磁盘上,它只存储几个网络打印机的链接。而像“回收站”这样的虚拟对象,它位于磁盘上,但需要进行与普通文件或文件夹不同的操作。例如,虚拟对象可能在Explorer中显示的是两个对象,但它们可能被存储在同一个磁盘文件中。
      在文件系统的命名空间中,包含两种对象:文件夹对象文件对象文件夹对象是树的节点,它包含文件和子文件夹。文件对象是树的叶子,它可能是一个磁盘文件也可能是一个虚拟对象。如果一个文件夹不是文件系统的一部分,它通常被称为虚拟文件夹

标识命名空间中的对象

在使用命名空间中的对象前,我们必须先标识它。由于在文件系统中文件名是可以重复使用的,所以我们使用完整限定名(完整路径),如:“C:\MyDocs\MyFile.htm”。但是这无法表示虚拟对象。所以Shell使用一种替代的标识,这个标识能够表示命名空间中所有的对象。

Item IDs

在一个文件夹中,每一个对象都有一个item ID,它等价于文件或文件夹名称的功能。item ID实际上是一个SHITEMID结构
      typedef struct _SHITEMID
      {
            USHORT cb;
            BYTE abID[1];
      } SHITEMID, * LPSHITEMID;
      其中abID成员是对象的标识符。abID的长度没有定义。它的值由包含它的文件夹来探测。abID的大小是可变的,所以cb成员存储SHITEMID结构的字节数。
      因为item ID不是用于显示,所以包含它的文件夹通常为它分配一个“显示名称”。这个“显示名称”由Windows Explorer用来显示一个文件夹的内容。

Item ID 列表

Item ID很少单独使用,它通常是item ID列表中的一部分。item ID 列表与系统路径意义相同。但item ID 列表不是一个字符串,而是一个ITEMIDLIST结构,这个结构是一序列的item ID(一个或者多个),并由2个字节的NULL表示结束。item ID 列表中的每一个item ID 都对应命名空间中的一个对象。它们的次序表示命名空间中的路径,这很像文件系统路径。下面的图表显示了对应于“C:\MyDocs\MyFile.htm”的ITEMIDLIST的结构:

 

PIDLs

命名空间对象通常由ITEMIDLIST结构的指针来标识,或者指向一个item ID 列表的指针(PIDL。为了方便,以后使用PIDL表示ITEMIDLIST结构,而不是item ID 列表的指针。上面图表显示的PIDL被称为“全的”或“完整的”PIDL。一个全的PIDL是由“桌面”开始,包含所有中间的路径的item ID。
      PIDL不常被使用。很多函数和方法使用相对PIDL。相对PIDL的根是一个文件夹,不是“桌面”。虽然它不是一个对象的唯一标识符,但是它要比全PIDL短,并且对于某些应用来说它能够充分说明该对象。
      最常见的相对PIDL是单层PIDL,它相对于这个对象的父文件夹。它仅包含这个对象的item ID以及NULL结束符。多层的PIDL通常包含两个或更多的item ID,并且表示出了从父文件夹到这个对象的路径,这个路径中会包含中间的一些子文件夹。注意,单层PIDL也可能是一个全PIDL(如“我的电脑”相对“桌面”的相对PIDL)。特殊的,“桌面”对象是“桌面”的子文件夹。

分配PIDL

虽然PIDL与系统路径很相似,但是它们还是有一些不同。主要的不同是如何分配和销毁它们的内存。在应用中,通常是系统分配PIDL使用的保持item ID的内存,而用户释放它。
      所以,我们必须使用IMalloc接口来分配和释放PIDL。可以调用SHGetMalloc来获取IMalloc接口指针,调用IMalloc::Alloc方法来分配内存,IMalloc::Free方法来释放内存。最后调用IMalloc::Release释放指针。

实现PIDL相关方法

我们提供几个方法来实现关于PIDL的处理:

   1: /**
   2:   * 创建一个新的PIDL。
   3:   * @param pIMalloc IMalloc接口指针。
   4:   * @param size 新的PIDL的字节数。
   5:   * @return 返回新创建的PIDL。
   6:   * 不再使用的时候需要调用IMalloc的Free方法释放。
   7:   */
   8:  LPITEMIDLIST pidl_create (IMalloc* pIMalloc, size_t size);
   9:  
  10:  /**
  11:   * 返回pidl所分配的总字节数。
  12:   * @param pidl 要计算的PIDL对象。
  13:   * @return 返回pidl所占用的字节数。
  14:   */
  15:  size_t pidl_size (LPCITEMIDLIST pidl);
  16:  
  17:  /**
  18:   * 获得PIDL中下一个ITEMIDLIST结构指针。
  19:   * @param pidl 要计算的PIDL。
 20:    * @return 返回pidl中下一个ITEMIDLIST结构指针。
  21:   */
 22:  LPCITEMIDLIST pidl_next (LPCITEMIDLIST pidl);
  23:  
  24:  /**
  25:   * 将两个PIDL连接起来。
  26:   * @param pIMalloc IMalloc接口指针。
  27:   * @param pidl_parent 要连接的PIDL。
  28:   * @param pidl 要连接的PIDL。
  29:   * @return 将两个PIDL连接后生成的PIDL。
  30:   * 不再使用的时候需要调用IMalloc的Free方法释放。
  31:   */
  32:  LPITEMIDLIST pidl_concat (IMalloc* pIMalloc, LPCITEMIDLIST pidl_parent, LPCITEMIDLIST pidl);
  33:  
  34:  /**
  35:   * 深度拷贝一个PIDL,并返回新创建的PIDL指针。
  36:   * @param pIMalloc IMalloc接口指针。
  37:   * @param pidl 要被复制的PIDL。
  38:   * @return 返回复制的新的PIDL。
  39:   * 不再使用的时候需要调用IMalloc的Free方法释放。
  40:   */
  41:  LPITEMIDLIST pidl_copy (IMalloc* pIMalloc, LPCITEMIDLIST pidl);
  42:  
  43:  /**
  44:   * 对比两个PIDL对象是否完全相同。
  45:   * @return 返回结果与memcmp一致。
  46:   */
  47:  int pidl_compare_all (LPCITEMIDLIST pidl1, LPCITEMIDLIST pidl2);
  48:  
  49:  /**
  50:   * 获取PIDL中最后一个ITEMIDLIST对象的指针。
  51:   * @param pidl PIDL对象的指针。
  52:   * @return 返回最后一个ITEMIDLIST结构的指针。
  53:   */
  54:  LPCITEMIDLIST pidl_last (LPCITEMIDLIST pidl);

LPITEMIDLIST pidl_create (IMalloc* pIMalloc, size_t size);

创建ITEMIDLIST数组。

   1: LPITEMIDLIST 
   2: pidl_create (IMalloc* pIMalloc, size_t size)
   3: {
   4:     LPITEMIDLIST pidl = NULL;
   5:     if (pIMalloc != NULL)
   6:     {
   7:         pidl = (LPITEMIDLIST)pIMalloc->lpVtbl->Alloc(pIMalloc, size);
   8:         if (pidl != NULL)
   9:         {
  10:             ZeroMemory (pidl, size);
  11:         }
  12:     }
  13:     return pidl;
  14: }

size_t pidl_size (LPCITEMIDLIST pidl);

获得某个PIDL使用的字节数。

   1: size_t 
   2: pidl_size (LPCITEMIDLIST pidl)
   3: {
   4:     size_t size = 0;
   5:     if (pidl != NULL)
   6:     {
   7:         size += sizeof (pidl->mkid.cb);
   8:         while (pidl->mkid.cb > 0)
   9:         {
  10:             size += pidl->mkid.cb;
  11:             pidl = pidl_next (pidl);
  12:         }
  13:     }
  14:     return size;
  15: }

LPCITEMIDLIST pidl_next (LPCITEMIDLIST pidl);

得到一个PIDL中指向下一个item ID的指针。

   1: LPCITEMIDLIST 
   2: pidl_next (LPCITEMIDLIST pidl)
   3: {
   4:     LPBYTE pb = (LPBYTE) pidl;
   5:     if (pidl != NULL)
   6:     {
   7:         pb += pidl->mkid.cb;
   8:     }
   9:     return (LPCITEMIDLIST)pb;
  10: }

LPITEMIDLIST pidl_concat (IMalloc* pIMalloc, LPCITEMIDLIST pidl_parent, LPCITEMIDLIST pidl);

合并两个PIDL,通常用于将父文件夹的全PIDL与它的子文件或文件夹对象的相对PIDL合并,从而得到文件或者子文件夹的全PIDL。

   1: LPITEMIDLIST 
   2: pidl_concat (IMalloc* pIMalloc, LPCITEMIDLIST pidl_parent, LPCITEMIDLIST pidl)
   3: {
   4:     size_t size1 = 0, size2 = 0;
   5:     LPITEMIDLIST pidl_new = NULL;
   6:  
   7:     if (NULL == pidl || NULL == pIMalloc)
   8:     {
   9:         return NULL;
  10:     }
  11:  
  12:     if (pidl_parent != NULL)
  13:     {
  14:         size1 = pidl_size (pidl_parent) - sizeof (pidl_parent->mkid.cb);
  15:     }
  16:     size2 = pidl_size (pidl);
  17:  
  18:     pidl_new = pidl_create (pIMalloc, size1 + size2);
  19:     if (pidl_new != NULL)
  20:     {
  21:         if (pidl_parent != NULL)
  22:             memcpy(pidl_new, pidl_parent, size1);
  23:         memcpy(((LPBYTE)pidl_new) + size1, pidl, size2);
  24:     }
  25:     return pidl_new;
  26: }

LPITEMIDLIST pidl_copy (IMalloc* pIMalloc, LPCITEMIDLIST pidl);

深度拷贝一个PIDL。

   1: LPITEMIDLIST 
   2: pidl_copy (IMalloc* pIMalloc, LPCITEMIDLIST pidl)
   3: {
   4:     LPITEMIDLIST lpi_tmp = NULL;
   5:     if (NULL == pIMalloc || NULL == pidl)
   6:     {
   7:         return NULL;
   8:     }
   9:  
  10:     lpi_tmp = (LPITEMIDLIST)pIMalloc->lpVtbl->Alloc(pIMalloc, pidl->mkid.cb+sizeof(pidl->mkid.cb));
  11:     CopyMemory((PVOID)lpi_tmp, (CONST VOID *)pidl, pidl->mkid.cb + sizeof(pidl->mkid.cb));
  12:  
  13:     return lpi_tmp;
  14: }

int pidl_compare_all (LPCITEMIDLIST pidl1, LPCITEMIDLIST pidl2);

对比两个PIDL是否完全相同,如果是两个全PIDL完全相同,则表示它们代表命名空间中同一个对象。

   1: int 
   2: pidl_compare_all (LPCITEMIDLIST pidl1, LPCITEMIDLIST pidl2)
   3: {
   4:     int us = 0;
   5:     if (NULL == pidl1 || NULL == pidl2)
   6:     {
   7:         return -1;
   8:     }
   9:  
  10:     us = pidl1->mkid.cb - pidl2->mkid.cb;
  11:     if (0 == us)
  12:     {
  13:         us = memcmp (&(pidl1->mkid.abID), &(pidl2->mkid.abID), pidl1->mkid.cb);
  14:     }
  15:     return us;
  16: }

LPCITEMIDLIST pidl_last (LPCITEMIDLIST pidl);

获得某个PIDL中最后一个item ID的指针。

   1: LPCITEMIDLIST 
   2: pidl_last (LPCITEMIDLIST pidl)
   3: {
   4:     LPCITEMIDLIST next = NULL;
   5:     
   6:     next = pidl_next (pidl);
   7:     while (next->mkid.cb > 0)
   8:     {
   9:         next = pidl_next (pidl);
  10:     }
  11:     return pidl;
  12: }

回顾IShellFolder接口

下面我们详细介绍一下IShellFolder接口的几个主要的方法:

BindToObject

HRESULT BindToObject( LPCITEMIDLIST pidl, LPBC pbc, REFIID riid, VOID** ppvOut );

我们通常使用该方法来获取当前IShellFolder接口的某个子文件夹的IShellFolder接口。
      pidl是子文件夹的相对PIDL。
      pbc通常是0.
      riid要检索的子文件夹对象的接口ID,通常我们检索IShellFolder接口,所以该值通常是IID_IShellFolder。
      ppvOut接收检索到的接口指针。

EnumObjects

HRESULT EnumObjects( HWND hwndOwner, SHCONTF grfFlags, IENUMIDLIST** ppenumIDList );

我们通常使用该方法获取IENUMIDLIST接口,IENUMIDLIST接口用于遍历当前IShellFolder表示的文件夹包含的文件或者子文件夹对象。
      hwndOwner设置一个窗体句柄,如果要弹出提示信息(如“请插入光盘”)时,弹出的窗体是这个窗体的子窗体。
      grfFlags参数表示要枚举哪些对象。
      ppenumIDList用于接收IENUMIDLIST接口指针。

HRESULT GetAttributesOf( UINT cidl, LPCITEMIDLIST* apidl, SFGAOF* rgfInOut );

获得文件对象或者子文件夹的属性。例如,可以通过获得某些属性得知该IShellFolder是否是一个文件夹、是否有子文件夹。(如果有子文件夹,在TreeView中,这个节点应该可以被展开)
      cidl通常是1.
      apidl是一个相对与这个IShellFolder的相对PIDL。
      rgfInOut是一个ULONG的值,作为输入和输出参数使用。作为输入参数来说,用它来设置调用者想要获得哪些属性。调用结束后,相应的属性位被置位并返回。例如,我们要查看这个IShellFolder接口代表的对象是否是一个文件夹,是否有子文件夹,我们应该这样做:
      ULONG ulAttrs = SFGAO_HASSUBFOLDER | SFGAO_FOLDER;
      pIShellFolder->GetAttributesOf (1, &pidl, &ulAttrs);
      if (ulAttrs & SFGAO_FOLDER)
      {
            // 是文件夹
      }
      if (ulAttrs & SFGAO_HASSUBFOLDER )
      {
            // 有子文件夹
      }

HRESULT GetDisplayNameOf( LPCITEMIDLIST pidl, DWORD uFlags, LPSTRRET lpName );

用这个方法获得某个文件对象或者子文件夹的显示名称。
      pidl要获得的文件对象或子文件夹的相对pidl。
      uFlags要获得显示名称的类型。
      lpName接收显示名称。可以使用StrRetToBuf函数将lpName转换成字符串。

C语言要注意的问题

由于接口对象都是用类的形式存在的,而C语言中没有类的概念,所以要在C语言中使用接口提供的方法需要做一点点额外的操作。在C语言中所有的接口指针实际上都是一个结构指针。所有的接口结构中都有一个lpVtbl成员这个成员是一个指向另外一个结构(struct *_lpVtbl)的指针。这个*_lpVtbl结构中的所有成员都是接口所提供的方法的函数指针。所以我们要想在C语言中调用某个接口的方法,应该这样调用:
      pIShellFolder->lpVtbl->GetAttributesOf (pIShellFolder, 1, &pidl, &ulAttrs);
      如果你自己要实现某些接口供Windows系统调用,记住,一定也要这样来实现。具体原理,这里就不讲了,看看C++方面的书吧。后面实例中都是使用C语言来实现的。

对Shell命名空间相关方法的封装

因为我们使用C语言实现,所以先定义需要用的的数据结构。我通常要使用有“桌面”的IShellFolder接口指针,还有IMalloc接口指针来释放或者分配内存。

   1: /**
   2:  * 操作Shell扩展接口的结构定义
   3:  * @struct shell_folder
   4:  */
   5: struct shell_folder
   6: {
   7:   IShellFolder* pDesktop; /**< 桌面IShellFolder接口指针 */
   8:   IMalloc* pIMalloc;      /**< IMalloc接口指针 */
   9: };

对于枚举某个目录的文件对象和子文件夹来说,我们需要传递一些信息给回调函数,初步计划,在回调函数中负责向TreeView或者ListView中添加节点。我们给出遍历一个IShellFolder接口的所有文件对象和子文件夹的参数结构:

   1: /**
   2:  * 枚举一个目录中所有文件时使用的数据对象。
   3:  * @struct sf_enum_data
   4:  */
   5: struct sf_enum_data
   6: {
   7:   LPSHELLFOLDER lpsfParent;    /**< 当前对象的父目录IShellFolder指针 */
   8:   LPITEMIDLIST lpi;            /**< 当前对象相对于父目录的相对PIDL */
   9:   LPITEMIDLIST lpifq;          /**< 当前对象相对于桌面的绝对PIDL */
  10: };

遍历用的回调函数类型声明:

typedef BOOL(*sf_cb_enum_func)(struct shell_folder* sf, struct sf_enum_data* data, void* args);

下面给出需要的函数:

   1: /**
   2:  * 创建shell_folder结构对象指针。
   3:  * @return 返回创建的shell_folder结构指针。
   4:  */
   5: struct shell_folder* sf_create ();
   6:  
   7: /**
   8:  * 释放shell_folder对象指针。
   9:  * @param sf 要释放的shell_folder对象指针。
  10:  */
  11: void sf_free (struct shell_folder* sf);
  12:  
  13: /**
  14:  * 获得桌面的绝对路径。
  15:  * @param sf shell_folder对象指针。
  16:  * @param hWnd 进行查询的窗体句柄。
  17:  * @apram ppifq 接收桌面的PIDL。
  18:  * @return 成功返回TRUE。
  19:  */
  20: BOOL sf_get_desktop_pidl (struct shell_folder* sf, HWND hWnd, LPITEMIDLIST* ppifq);
  21:  
  22: /**
  23:  * 释放PIDL对象。
  24:  * @param sf shell_folder对象指针。
  25:  * @param lpifq 要释放的PIDL。
  26:  */
  27: void sf_free_pidl (struct shell_folder* sf, LPITEMIDLIST lpifq);
  28:  
  29: /**
  30:  * 释放枚举数据对象。
  31:  * @param sf shell_folder对象指针。
  32:  * @param ed 要释放的sf_enum_data对象指针。
  33:  */
  34: void sf_free_enum_data (struct shell_folder* sf, struct sf_enum_data* ed);
  35:  
  36: /**
  37:  * 枚举一个文件夹中的所有对象。
  38:  * @param sf shell_folder结构对象指针。
  39:  * @param hWnd 要显示提示信息的窗体句柄。
  40:  * @param pSF 要遍历的目录对象的IShellFolder接口指针。
  41:  * @param lpifq 父目录的绝对路径。
  42:  * @param grfFlags 遍历使用的标志参考IShellFolder::EnumObjects。
  43:  * @param func 遍历到一个对象调用的回调函数指针。
  44:  * 如果回调函数返回TRUE则删除sf_enum_data对象指针。
  45:  * @param args 传递给回调函数的参数。
  46:  * @return 遍历成功返回TRUE,失败返回FALSE。
  47:  */
  48: BOOL sf_enum_folder (struct shell_folder* sf, 
  49:     HWND hWnd, 
  50:     IShellFolder* pSF, 
  51:     LPITEMIDLIST lpifq, 
  52:     SHCONTF grfFlags, 
  53:     sf_cb_enum_func func, 
  54:     void* args);
  55:  
  56: /**
  57:  * 获得某个绝对PIDL的名称。
  58:  * @param sf shell_folder对象指针。
  59:  * @param pSF 父级文件夹的IShellFolder接口指针。
  60:  * @param uFlags 参考IShellFolder::GetDisplayNameOf。
  61:  * @param lpi 相对于pSF的PIDL。
  62:  * @param szName 接收文件名。
  63:  * @return 成功返回FALSE,失败返回FALSE。
  64:  */
  65: BOOL sf_get_displayname (struct shell_folder* sf, IShellFolder* pSF, DWORD uFlags, LPITEMIDLIST lpi, TCHAR szName[MAX_PATH]);
  66:  
  67: /**
  68:  * 获得某个绝对PIDL的系统小图标和选择图标。
  69:  * @param sf shell_folder对象指针。
  70:  * @param lpifq 完整路径的PIDL。
  71:  * @param iSmallIcon 接收小图标索引。
  72:  * @param iSelIcon 接收选择图标索引。
  73:  * @return 成功返回TRUE,失败返回FALSE。
  74:  */
  75: BOOL sf_get_icon (struct shell_folder* sf, LPITEMIDLIST lpifq, int* iSmallIcon, int* iSelIcon);
  76:  
  77: /**
  78:  * 获得某个绝对PIDL是否是文件夹,是否有子文件夹。
  79:  * @param sf shell_folder对象指针。
  80:  * @param pSF 父级文件夹的IShellFolder接口指针。
  81:  * @param lpi 相对于pSFP的IDL。
  82:  * @param pbIsFolder 是否是文件夹。
  83:  * @param pbHasChild 是否有子文件夹。
  84:  * @return 成功返回TRUE。
  85:  */
  86: BOOL sf_has_child (struct shell_folder* sf, IShellFolder* pSF, LPITEMIDLIST lpi, LPBOOL pbIsFolder, LPBOOL pbHasChild);

我们逐一的讲解一下实现吧:

struct shell_folder* sf_create ();

这个方法比较简单,就是创建一个shell_folder结构对象。用到了上面提到的SHGetDesktopFolder函数。

   1: struct shell_folder* 
   2: sf_create ()
   3: {
   4:     HRESULT hr;
   5:     struct shell_folder* sf = NULL;
   6:     sf = (struct shell_folder*) malloc (sizeof (struct shell_folder));
   7:     if (NULL == sf)
   8:     {
   9:         return sf;
  10:     }
  11:     ZeroMemory (sf, sizeof (struct shell_folder));
  12:  
  13:     hr = SHGetDesktopFolder (&(sf->pDesktop));
  14:     if (FAILED (hr))
  15:     {
  16:         free (sf);
  17:         return NULL;
  18:     }
  19:  
  20:     hr = SHGetMalloc (&(sf->pIMalloc));
  21:     if (FAILED (hr))
  22:     {
  23:         sf->pDesktop->lpVtbl->Release (sf->pDesktop);
  24:         free (sf);
  25:         return NULL;
  26:     }
  27:  
  28:     return sf;
  29: }

void sf_free (struct shell_folder* sf);

释放一个shell_folder结构对象指针使用的所有内存。同时要释放“桌面”的IShellFolder接口指针和IMalloc接口指针。

   1: void 
   2: sf_free (struct shell_folder* sf)
   3: {
   4:     if (sf)
   5:     {
   6:         if (sf->pDesktop)
   7:         {
   8:             sf->pDesktop->lpVtbl->Release (sf->pDesktop);
   9:         }
  10:         if (sf->pIMalloc)
  11:         {
  12:             sf->pIMalloc->lpVtbl->Release (sf->pIMalloc);
  13:         }
  14:         free (sf);
  15:     }
  16: }

BOOL sf_get_desktop_pidl (struct shell_folder* sf, HWND hWnd, LPITEMIDLIST* ppifq);

获得桌面的PIDL。调用了SHGetSpecialFolderLocation函数,这个函数可以获得一些特殊文件夹的PIDL。

   1: BOOL 
   2: sf_get_desktop_pidl (struct shell_folder* sf, HWND hWnd, LPITEMIDLIST* ppifq)
   3: {
   4:     HRESULT hr;
   5:     if (NULL == sf || NULL == ppifq)
   6:     {
   7:         return FALSE;
   8:     }
   9:  
  10:     hr = SHGetSpecialFolderLocation (hWnd, CSIDL_DESKTOP, ppifq);
  11:     if (FAILED (hr))
  12:     {
  13:         return FALSE;
  14:     }
  15:     return TRUE;
  16: }

void sf_free_pidl (struct shell_folder* sf, LPITEMIDLIST lpifq);

上面提到了,系统给出的PIDL都是ITEMIDLIST数组,系统负责分配内存,使用后需要调用IMalloc::Free来释放这些内存。

   1: void 
   2: sf_free_pidl (struct shell_folder* sf, LPITEMIDLIST lpifq)
   3: {
   4:     if (NULL == sf || NULL == lpifq)
   5:     {
   6:         return ;
   7:     }
   8:  
   9:     sf->pIMalloc->lpVtbl->Free (sf->pIMalloc, lpifq);
  10: }

void sf_free_enum_data (struct shell_folder* sf, struct sf_enum_data* ed);

枚举某个文件夹的的文件对象和子文件夹对象时,传递给回调函数的sf_enum_data结构需要使用IMalloc::Free方法来释放其全部资源。

   1: void 
   2: sf_free_enum_data (struct shell_folder* sf, struct sf_enum_data* ed)
   3: {
   4:     if (NULL == sf || NULL == ed)
   5:     {
   6:         return;
   7:     }
   8:  
   9:     if (ed->lpsfParent)
  10:     {
  11:         ed->lpsfParent->lpVtbl->Release (ed->lpsfParent);
  12:     }
  13:     if (ed->lpi)
  14:     {
  15:         sf->pIMalloc->lpVtbl->Free (sf->pIMalloc, ed->lpi);
  16:     }
  17:     if (ed->lpifq)
  18:     {
  19:         sf->pIMalloc->lpVtbl->Free (sf->pIMalloc, ed->lpifq);
  20:     }
  21:     sf->pIMalloc->lpVtbl->Free (sf->pIMalloc, ed);
  22: }

BOOL sf_enum_folder (struct shell_folder* sf, HWND hWnd, IShellFolder* pSF, LPITEMIDLIST lpifq, SHCONTF grfFlags,  sf_cb_enum_func func,  void* args);

枚举一个文件夹的所有文件对象和子文件夹对象。
      func是回调函数,当枚举到一个对象,就调用一次这个函数。
      args用户参数,调用func时,这个参数会传递给func。

这个方法首先调用IShellFolder接口对象pSF的EnumObjects方法获得IEnumIDList接口对象指针。
      调用IEnumIDList接口对象的Next方法遍历,并获得每个子对象的相对PIDL。
      将pSF的PIDL与子对象的相对PIDL相连接获得子对象的全PIDL。

使用相对父目录pSF的PIDL、全PIDL和父目录IShellFolder接口指针pSF构造sf_enum_data结构对象,并传递给回调函数。
      根据回调函数的返回值判断是否释放构造的sf_enum_data结构对象指针。
      最后别忘记释放IEnumIDList接口的指针。

   1: BOOL 
   2: sf_enum_folder (struct shell_folder* sf, HWND hWnd, IShellFolder* pSF, LPITEMIDLIST lpifq, 
   3:         SHCONTF grfFlags, sf_cb_enum_func func, void* args)
   4: {
   5:     HRESULT hr;
   6:     LPENUMIDLIST lpEnum = NULL;
   7:     LPITEMIDLIST lpi = NULL, lpifqThisItem = NULL, lpiTemp = NULL;
   8:     ULONG ulwork = 0;
   9:     struct sf_enum_data* sf_ed = NULL;
  10:  
  11:     if (NULL == sf || NULL == func)
  12:     {
  13:         return FALSE;
  14:     }
  15:     if (NULL == pSF || NULL == lpifq)
  16:     {
  17:         // 当作桌面处理
  18:         pSF = sf->pDesktop;
  19:     }
  20:     pSF->lpVtbl->AddRef (pSF);
  21:  
  22:     hr = pSF->lpVtbl->EnumObjects (pSF, hWnd, grfFlags, 
  23:         & lpEnum);
  24:     if (FAILED (hr))
  25:     {
  26:         goto GOERROR;
  27:     }
  28:  
  29:     while (S_OK == lpEnum->lpVtbl->Next(lpEnum, 1, &lpi, &ulwork) && ulwork > 0)
  30:     {
  31:         sf_ed = (struct sf_enum_data*) sf->pIMalloc->lpVtbl->Alloc(sf->pIMalloc, sizeof(struct sf_enum_data));
  32:         if (NULL == sf_ed)
  33:         {
  34:             goto GOERROR;
  35:         }
  36:         ZeroMemory (sf_ed, sizeof (struct sf_enum_data));
  37:  
  38:         lpifqThisItem = pidl_concat(sf->pIMalloc, lpifq, lpi);
  39:         if (NULL == lpifqThisItem)
  40:         {
  41:             goto GOERROR;
  42:         }
  43:  
  44:         sf_ed->lpifq = lpifqThisItem;
  45:         sf_ed->lpi = lpi;
  46:         sf_ed->lpsfParent = pSF;
  47:  
  48:         if (func (sf, sf_ed, args))
  49:         {
  50:             if (sf_ed)
  51:             {
  52:                 if (sf_ed->lpi)
  53:                 {
  54:                     sf->pIMalloc->lpVtbl->Free (sf->pIMalloc, sf_ed->lpi);
  55:                 }
  56:                 if (sf_ed->lpifq)
  57:                 {
  58:                     sf->pIMalloc->lpVtbl->Free (sf->pIMalloc, sf_ed->lpifq);
  59:                 }
  60:                 sf->pIMalloc->lpVtbl->Free (sf->pIMalloc, sf_ed);
  61:             }
  62:         }
  63:     }
  64:     if (pSF)
  65:     {
  66:         pSF->lpVtbl->Release (pSF);
  67:     }
  68:     if (lpEnum)
  69:     {
  70:         lpEnum->lpVtbl->Release (lpEnum);
  71:     }
  72:     return TRUE;
  73: GOERROR:
  74:     if (pSF)
  75:     {
  76:         pSF->lpVtbl->Release (pSF);
  77:     }
  78:     if (lpEnum)
  79:     {
  80:         lpEnum->lpVtbl->Release (lpEnum);
  81:     }
  82:     if (sf_ed)
  83:     {
  84:         if (sf_ed->lpi)
  85:         {
  86:             sf->pIMalloc->lpVtbl->Free (sf->pIMalloc, sf_ed->lpi);
  87:         }
  88:         if (sf_ed->lpifq)
  89:         {
  90:             sf->pIMalloc->lpVtbl->Free (sf->pIMalloc, sf_ed->lpifq);
  91:         }
  92:         sf->pIMalloc->lpVtbl->Free (sf->pIMalloc, sf_ed);
  93:     }
  94:     return FALSE;
  95: }

BOOL sf_get_displayname (struct shell_folder* sf, IShellFolder* pSF, DWORD uFlags, LPITEMIDLIST lpi, TCHAR szName[MAX_PATH]);

使用某个文件夹的IShellFolder,根据文件或子文件夹的相对PIDL,获得文件或子文件夹的显示名称。使用了上面提到的IShellFolder的GetDisplayNameOf方法获得显示名称,再调用StrRetToBuf将STRRET转换为字符串。

   1: BOOL 
   2: sf_get_displayname (struct shell_folder* sf, IShellFolder* pSF, DWORD uFlags, LPITEMIDLIST lpi, TCHAR szName[MAX_PATH])
   3: {
   4:     STRRET str;
   5:     HRESULT hr;
   6:     if (NULL == sf)
   7:     {
   8:         return FALSE;
   9:     }
  10:  
  11:     if (NULL == pSF)
  12:     {
  13:         pSF = sf->pDesktop;
  14:     }
  15:  
  16:     hr = pSF->lpVtbl->GetDisplayNameOf (pSF, lpi, uFlags, &str);
  17:     if (FAILED (hr))
  18:     {
  19:         return FALSE;
  20:     }
  21:  
  22:     hr = StrRetToBuf (&str, lpi, szName, MAX_PATH);
  23:     if (FAILED (hr))
  24:     {
  25:         return FALSE;
  26:     }
  27:     return TRUE;
  28: }

BOOL sf_get_icon (struct shell_folder* sf, LPITEMIDLIST lpifq, int* iSmallIcon, int* iSelIcon);

通过这个函数可以获得某个对象的图标,以及选中时候的图标。例如在TreeView中,当选中某个文件夹时,某个文件夹的图标就会变成打开的样子。这里获得的是图标的索引。图标存储在系统的ImageList里面,后面的TreeView例子会详细介绍。我们通过SHGetFileInfo函数获取图标信息。

   1: BOOL 
   2: sf_get_icon (struct shell_folder* sf, LPITEMIDLIST lpifq, int* iSmallIcon, int* iSelIcon)
   3: {
   4:     SHFILEINFO sfi;
   5:  
   6:     if (NULL == sf || NULL == lpifq || NULL == iSmallIcon || NULL == iSelIcon)
   7:     {
   8:         return FALSE;
   9:     }
  10:  
  11:     // 获取小图标
  12:     ZeroMemory (&sfi, sizeof (SHFILEINFO));
  13:     SHGetFileInfo ((LPCSTR)lpifq, 0, &sfi, sizeof (SHFILEINFO), SHGFI_PIDL | SHGFI_SYSICONINDEX | SHGFI_SMALLICON);
  14:     *iSmallIcon = sfi.iIcon;
  15:  
  16:     // 获取选择图标
  17:     ZeroMemory (&sfi, sizeof (SHFILEINFO));
  18:     SHGetFileInfo ((LPCSTR)lpifq, 0, &sfi, sizeof (SHFILEINFO), SHGFI_PIDL | SHGFI_SYSICONINDEX | SHGFI_SMALLICON | SHGFI_OPENICON);
  19:     *iSelIcon = sfi.iIcon;
  20:  
  21:     return TRUE;
  22: }

BOOL sf_has_child (struct shell_folder* sf, IShellFolder* pSF, LPITEMIDLIST lpi, LPBOOL pbIsFolder, LPBOOL pbHasChild);

这个函数用于判断某个对象是否是文件夹,这个文件夹是否有子文件夹。在TreeView中只显示文件夹,并且如果某个文件夹有子文件夹那么这个文件夹节点应该可以被继续展开。我们通过上面介绍的IShellFolder的GetAttributesOf方法实现该功能。

   1: BOOL 
   2: sf_has_child (struct shell_folder* sf, IShellFolder* pSF, LPITEMIDLIST lpi, LPBOOL pbIsFolder, LPBOOL pbHasChild)
   3: {
   4:     ULONG ulAttrs = SFGAO_HASSUBFOLDER | SFGAO_FOLDER;
   5:     if (NULL == sf || NULL == pbIsFolder || NULL == pbHasChild)
   6:     {
   7:         return FALSE;
   8:     }
   9:     
  10:     *pbIsFolder = FALSE;
  11:     *pbHasChild = FALSE;
  12:  
  13:     if (NULL == pSF)
  14:     {
  15:         pSF = sf->pDesktop;
  16:     }
  17:  
  18:     pSF->lpVtbl->GetAttributesOf (pSF, 1, &lpi, &ulAttrs);
  19:     if (ulAttrs & SFGAO_FOLDER)
  20:     {
  21:         *pbIsFolder = TRUE;
  22:     }
  23:  
  24:     if (ulAttrs & SFGAO_HASSUBFOLDER)
  25:     {
  26:         *pbHasChild = TRUE;
  27:     }
  28:  
  29:     return TRUE;
  30: }

TreeView实现目录树-tree_view的相关方法

使用TreeView实现目录树,其中每个节点都是一个文件夹对象(也可能是虚拟文件夹)。而ListView显示所有的文件对象和子文件夹对象。如下图:

 

如果某个文件夹对象拥有子文件夹,这个文件夹所对应的节点前面会有一个表示可以展开的符号“+”。点击“+”可以展开当前节点,显示这个文件夹对象中的所有子文件夹对象。

实现的时候,我们的TreeView包含一个普通的子窗体和一个TreeView窗体,将这两个窗体作为TreeView使用。TreeView窗体是普通子窗体的子窗体。在使用的时候,如果在主窗体使用我们提供的方法创建TreeView,实际上是在主窗体上创建了一个子窗体,在子窗体中创建一个TreeView。下面要实现的是一个普通的TreeView所必备的一些功能的封装。我称为tree_view。要实现真正的显示本地的命名空间树,或者显示远程主机的命名空间树的时候,可以扩展使用tree_view来实现。

tree_view的实现

下面先定义了一些要使用的回调函数:

   1: typedef HRESULT(*pfn_tv_after_create)(struct tree_view* tv);
   2: typedef BOOL(*pfn_tv_first_expanding)(struct tree_view* tv, LPTVITEM ptvi);
   3: typedef BOOL(*pfn_tv_expanding)(struct tree_view* tv, LPTVITEM ptvi);
   4: typedef BOOL(*pfn_tv_collapse)(struct tree_view* tv, LPTVITEM ptvi);
   5: typedef HRESULT(*pfn_tv_delitem)(struct tree_view* tv, LPTVITEM ptvi);
   6: typedef HRESULT(*pfn_tv_selchanged)(struct tree_view* tv, LPTVITEM ptvi_old, LPTVITEM ptvi_new);
   7: typedef HRESULT(*pfn_tv_check_drag_type)(struct tree_view* tv, CLIPFORMAT cfFormat);
   8: typedef HRESULT(*pfn_tv_drop)(struct tree_view* tv, CLIPFORMAT cfFormat, STGMEDIUM medium);
   9: typedef HRESULT(*pfn_tv_begin_drag) (struct tree_view* tv, LPNMTREEVIEW lpNMTV);

这些回调函数实际上是为了扩展准备的,供给tree_view的使用者实现一些特殊功能所使用的。

下面的结构保存所有的回调函数指针:

   1: /**
   2:  * TreeView使用到的事件处理函数指针集合。
   3:   * @struct tv_callback
   4:   */
   5:  struct tv_callback
   6:  {
   7:      pfn_tv_after_create fn_after_create;                /**< 当创建TreeView完成后调用的函数 */
   8:      pfn_tv_first_expanding fn_first_expanding;          /**< 当某个节点第一次被展开时调用的函数 */
   9:      pfn_tv_expanding fn_expanding;                      /**< 当某个节点不是第一次,但是被展开时调用的函数 */
  10:      pfn_tv_collapse fn_collapse;                        /**< 当某个节点被合并时调用的函数 */
  11:      pfn_tv_delitem fn_delitem;                          /**< 当某个节点被删除时调用的函数 */
  12:      pfn_tv_selchanged fn_selchanged;                    /**< 当选择了一个新的节点时调用的函数 */
  13:      pfn_tv_check_drag_type fn_check_drag_type;          /**< 判断给定的拖拽对象类型是否可以接受 */
  14:      pfn_tv_drop fn_drop;                                /**< 一个对象被拖拽到窗口这个函数进行实际操作 */
  15:      pfn_tv_begin_drag fn_begin_drag;                    /**< 开始拖拽某个对象时调用的函数 */
  16:  };

下面的结构保持着tree_view所需要的所有信息:

   1: /**
   2:  * TREEVIEW结构
   3:  * @struct tree_view
   4:  */
   5: struct tree_view
   6: {
   7:     struct tv_callback cb;    /**< TreeView使用到的回调函数 */
   8:     HWND hParent;              /**< 父窗体句柄 */
   9:     HWND hWnd;                 /**< TreeView父窗体窗体句柄 */
  10:     HWND hTree;                /**< TreeView的句柄 */
  11:     ATOM atom;                 /**< 注册窗体类的ATOM对象 */
  12:     BOOL fDragging;            /**< 是否正在拖拽的标志 */
  13:     LPVOID lpArgs;             /**< 用户参数 */
  14: };

下面这个结构是tree_view中每个节点需要保存的信息:

   1: /**
   2:  * TreeView中结点的信息。
   3:  * @struct tv_node_info
   4:  */
   5: struct tv_node_info
   6: {
   7:     LPTSTR lpszText;          /**< 节点显示的文字 */
   8:     int iImage;                /**< 小图标索引 */
   9:     int iSelImage;             /**< 选择图标索引 */
  10:     int iHasChild;             /**< 是否有子结点 */
  11:     HTREEITEM hParent;        /**< 父节点句柄 */
  12:     HTREEITEM hAfter;         /**< 在某个兄弟节点的后面 */
  13:     LPVOID lpParam;           /**< 参数 */
  14: };

下面是提供的对外方法:

   1: /**
   2:  * 创建TREE VIEW。
   3:  * @param hParent 父窗体句柄。
   4:  * @param lpszClassName 创建的窗体类名。
   5:  * @param nID TreeView的控件ID。
   6:  * @param lpszTitle 创建的窗体标题名称。
   7:  * @param rtPos 创建的窗体大小,由于TreeView充满整个新创建的窗体,所以这也是TreeView的大小。
   8:  * @param cb 设置TreeView所有要调用的回调函数。
   9:  * @param lpArgs 用户自定义参数。
  10:  * @return 创建成功返回tree_view结构对象指针。
  11:  */
  12: struct tree_view* tv_create (HWND hParent, LPCTSTR lpszClassName, UINT nID, LPCTSTR lpszTitle, RECT rtPos, 
  13:     struct tv_callback cb, LPVOID lpArgs);
  14:  
  15: /**
  16:  * 删除tree_view结构对象指针,并删除说有窗体。
  17:  * @param tv 要删除的tree_view结构对象指针。
  18:  */
  19: void tv_free (struct tree_view* tv);
  20:  
  21: /**
  22:  * 创建的新窗体的消息处理函数。
  23:  * @param hWnd 窗体句柄。
  24:  * @param message 接收到的消息。
  25:  * @param wParam 参数。
  26:  * @param lParam 参数。
  27:  */
  28: LRESULT CALLBACK tv_WndProc (HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
  29:  
  30: /**
  31:  * 获得TreeView句柄。
  32:  * @param tv tree_view结构对象指针。
  33:  * @return 返回TreeView句柄。
  34:  */
  35: HWND tv_get_tree_view (struct tree_view* tv);
  36:  
  37: /**
  38:  * 插入一个节点。
  39:  * @param tv tv tree_view结构对象指针。
  40:  * @param tvni tv_node_info 结构对象指针。
  41:  * @return 插入成功返回新节点句柄。
  42:  */
  43: HTREEITEM tv_insert_node (struct tree_view* tv, struct tv_node_info* tvni);
  44:  
  45: /**
  46:  * 负责构造IDataObject和IDataSource接口,并调用DoDragDrop。
  47:  * @param pFormatEtc 类型数组。
  48:  * @param pStgMedium 每种类型对应的数据数组。
  49:  * @param nCount 类型数组和数据数组中元素的个数。
  50:  * @param pArgs 用户参数。
  51:  * @return 成功返回S_OK。
  52:  */
  53: HRESULT tv_DoDragDrop (FORMATETC* pFormatEtc, STGMEDIUM* pStgMedium, UINT nCount, void* pArgs);

下面逐步介绍各个方法

struct tree_view* tv_create (HWND hParent, LPCTSTR lpszClassName, UINT nID, LPCTSTR lpszTitle, RECT rtPos, struct tv_callback cb, LPVOID lpArgs);

这个方法创建一个tree_view对象指针,这个对象指针不再使用的时候需要调用tv_free来释放。

   1: struct tree_view* 
   2: tv_create (HWND hParent, LPCTSTR lpszClassName, UINT nID, LPCTSTR lpszTitle, RECT rtPos, 
   3:                      struct tv_callback cb, LPVOID lpArgs)
   4: {
   5:     struct tree_view* tv = NULL;
   6:     HINSTANCE hInst = NULL;
   7:     WNDCLASSEX wcex;
   8:     HWND hWnd;
   9:  
  10:     tv = (struct tree_view*) malloc (sizeof (struct tree_view));
  11:     if (NULL == tv)
  12:     {
  13:         return NULL;
  14:     }
  15:     ZeroMemory (tv, sizeof (struct tree_view));
  16:  
  17:     hInst = (HINSTANCE) GetWindowLong (hParent, GWL_HINSTANCE);
  18:     ZeroMemory (&wcex, sizeof (WNDCLASSEX));
  19:     wcex.cbSize = sizeof(WNDCLASSEX);
  20:  
  21:     wcex.style            = CS_HREDRAW | CS_VREDRAW;
  22:     wcex.lpfnWndProc    = tv_WndProc;
  23:     wcex.cbClsExtra        = 0;
  24:     wcex.cbWndExtra        = 0;
  25:     wcex.hInstance        = hInst;
  26:     wcex.hIcon            = NULL;
  27:     wcex.hCursor        = NULL;
  28:     wcex.hbrBackground    = (HBRUSH)(COLOR_WINDOW+1);
  29:     wcex.lpszMenuName    = NULL;
  30:     wcex.lpszClassName    = lpszClassName;
  31:     wcex.hIconSm        = NULL;
  32:  
  33:     tv->atom = RegisterClassEx(&wcex);
  34:     if (!tv->atom)
  35:     {
  36:         free (tv);
  37:         return NULL;
  38:     }
  39:  
  40:     tv->hParent = hParent;
  41:     memcpy (&(tv->cb), &cb, sizeof (struct tv_callback));
  42:     tv->lpArgs = lpArgs;
  43:  
  44:     hWnd = CreateWindowEx (WS_EX_CLIENTEDGE, tv->atom, lpszTitle, WS_CHILD | WS_VISIBLE, 
  45:         rtPos.left, rtPos.top, rtPos.right-rtPos.left, rtPos.bottom-rtPos.top, hParent, NULL, hInst, tv);
  46:  
  47:     if (!hWnd)
  48:     {
  49:         DWORD dwErr = GetLastError ();
  50:         free (tv);
  51:         return NULL;
  52:     }
  53:  
  54:     ShowWindow (hWnd, SW_SHOW);
  55:     UpdateWindow (hWnd);
  56:  
  57:     return tv;
  58: }

这个函数创建了tree_view结构对象并注册一个窗体类,最后调用CreateWindowEx创建一个窗体,这个窗体就是TreeView的父窗体,我们会在这个窗体的WM_CREATE消息中创建TreeView。用户传递的参数pArgs保持在tree_view的pArgs成员中,这是用户数据。特别注意的是在调用CreateWindowEx时,最后一个参数传递了tree_view的指针,这个指针在tree_view的父窗体(以后称为当前窗体)接收到WM_CREATE消息时放在CREATESTRUCT结构的lParam成员中。

void tv_free (struct tree_view* tv);

这个方法销毁创建的当前窗体和tree_view对象。

   1: void 
   2: tv_free (struct tree_view* tv)
   3: {
   4:     HINSTANCE hInst = NULL;
   5:     if (tv)
   6:     {
   7:         if (tv->hWnd)
   8:         {
   9:             DestroyWindow (tv->hWnd);
  10:         }
  11:         if (tv->atom)
  12:         {
  13:             hInst = (HINSTANCE) GetWindowLong (tv->hParent, GWL_HINSTANCE);
  14:             UnregisterClass ((LPCTSTR)tv->atom, hInst);
  15:         }
  16:         free (tv);
  17:     }
  18: }

下面是当前窗体的消息响应函数,我们先给出代码,然后进行详细介绍:

   1: LRESULT CALLBACK 
   2: tv_WndProc (HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
   3: {
   4:     switch (message)
   5:     {
   6:     case WM_CREATE:
   7:         {
   8:             CREATESTRUCT* cs;
   9:             cs = (CREATESTRUCT*) lParam;
  10:             return _tv_on_create (hWnd, cs);
  11:         }
  12:         break;
  13:     case WM_NOTIFY:
  14:         {
  15:             struct tree_view* tv = (struct tree_view*) GetWindowLong (hWnd, GWL_USERDATA);
  16:             if (NULL == tv)
  17:             {
  18:                 return TRUE; // 有错误,不能展开
  19:             }
  20:             switch (((LPNMHDR) lParam)->code)
  21:             {
  22:             case TVN_ITEMEXPANDING:
  23:                 {
  24:                     LPNMTREEVIEW lpNMTV = (LPNMTREEVIEW) lParam;
  25:                     switch (lpNMTV->action)
  26:                     {
  27:                     case TVE_EXPAND:
  28:                         {
  29:                             if (!(lpNMTV->itemNew.state & TVIS_EXPANDEDONCE))
  30:                             {
  31:                                 return tv->cb.fn_first_expanding (tv, &(lpNMTV->itemNew));
  32:                             }
  33:                             else
  34:                             {
  35:                                 return tv->cb.fn_expanding (tv, &(lpNMTV->itemNew));
  36:                             }
  37:                         }
  38:                         break;
  39:                     case TVE_COLLAPSE:
  40:                         {
  41:                             return tv->cb.fn_collapse (tv, &(lpNMTV->itemNew));
  42:                         }
  43:                         break;
  44:                     }
  45:                 }
  46:                 break;
  47:             case TVN_DELETEITEM:
  48:                 {
  49:                     LPNMTREEVIEW lpNMTV = (LPNMTREEVIEW) lParam;
  50:                     return tv->cb.fn_delitem (tv, &(lpNMTV->itemOld));
  51:                 }
  52:                 break;
  53:             case TVN_SELCHANGED:
  54:                 {
  55:                     LPNMTREEVIEW lpNMTV = (LPNMTREEVIEW) lParam;
  56:                     return tv->cb.fn_selchanged (tv, &(lpNMTV->itemOld), &(lpNMTV->itemNew));
  57:                 }
  58:                 break;
  59:             case TVN_BEGINDRAG:
  60:                 {
  61:                     LPNMTREEVIEW* lpNMTV = (LPNMTREEVIEW) lParam;
  62:                     return _tv_on_begin_drag (tv, lpNMTV);
  63:                 }
  64:                 break;
  65:             }
  66:         }
  67:         break;
  68:     case WM_MOUSEMOVE:
  69:         {
  70:             struct tree_view* tv = (struct tree_view*) GetWindowLong (hWnd, GWL_USERDATA);
  71:             return _tv_on_mouse_move_drag (tv, wParam, lParam);
  72:         }
  73:         break;
  74:     case WM_LBUTTONUP:
  75:         {
  76:             struct tree_view* tv = (struct tree_view*) GetWindowLong (hWnd, GWL_USERDATA);
  77:             return _tv_on_lbtn_up_drag (tv, wParam, lParam);
  78:         }
  79:         break;
  80:     case TV_M_CHANGEFOCUS:
  81:         {
  82:             struct tree_view* tv = (struct tree_view*) GetWindowLong (hWnd, GWL_USERDATA);
  83:             TreeView_SelectDropTarget(tv->hTree, NULL);
  84:             TreeView_SelectItem(tv->hTree, (HTREEITEM)lParam) ;
  85:         }
  86:         break;
  87:     default:
  88:         return DefWindowProc(hWnd, message, wParam, lParam);
  89:     }
  90:     return S_OK;
  91: }

WM_CREATE 消息

当调用CreateWindowEx创建当前窗体时,我们在最后的参数中传入了tree_view对象的指针。这里通过CREATESTRUCT结构的lParam成员获得了这个指针,并调用了我们自己的内部函数_tv_on_create:

   1: static LRESULT 
   2: _tv_on_create (HWND hWnd, CREATESTRUCT* cs)
   3: {
   4:     HRESULT hr;
   5:     RECT rt;
   6:     HWND hTree = NULL;
   7:     struct tree_view* tv = NULL;
   8:     IDropTarget* pDT = NULL;
   9:     struct idt_lpVtbl idtVtbl;
  10:  
  11:     GetClientRect (hWnd, &rt);
  12:  
  13:     // 创建TreeView
  14:     hTree = CreateWindow (WC_TREEVIEW,
  15:         "",
  16:         WS_CHILD | LVS_REPORT | WS_VISIBLE|TVS_HASLINES|TVS_HASBUTTONS|TVS_LINESATROOT|TVS_SHOWSELALWAYS ,
  17:         0,
  18:         0,
  19:         rt.right-rt.left, 
  20:         rt.bottom-rt.top,
  21:         hWnd,
  22:         (HMENU) ID_LOCAL_DIR_TREE,
  23:         (HINSTANCE) GetWindowLong (hWnd, GWL_HINSTANCE),
  24:         NULL);
  25:     if (NULL == hTree)
  26:     {
  27:         return -1;
  28:     }
  29:  
  30:     tv = (struct tree_view*) cs->lpCreateParams;
  31:     tv->hTree = hTree;
  32:     tv->hWnd = hWnd;
  33:  
  34:     // 设置窗体参数
  35:     SetWindowLong (hWnd, GWL_USERDATA, (LONG) tv);
  36:  
  37:     /*
  38:     // 注册为支持拖拽窗体
  39:     ZeroMemory (&idtVtbl, sizeof (struct idt_lpVtbl));
  40:     idtVtbl.DragEnter = _tv_DragEnter;
  41:     idtVtbl.DragLeave = _tv_DragLeave;
  42:     idtVtbl.DragOver = _tv_DragOver;
  43:     idtVtbl.Drop = _tv_Drop;
  44:     pDT = idt_create (&idtVtbl, tv);
  45:     hr = RegisterDragDrop (hWnd, pDT);
  46:     if (FAILED (hr))
  47:     {
  48:         return -1;
  49:     }
  50:     */
  51:     if (tv->cb.fn_after_create)
  52:     {
  53:         return tv->cb.fn_after_create (tv);
  54:     }
  55:     return S_OK;
  56: }

与拖拽有关的方法我都注释掉了,因为在测试拖拽的时候遇到了一些问题。这个方法调用CreateWindow创建了一个TreeView子窗体,并将tree_view对象指针保存在窗体的用户数据中,以便以后处理其它消息的时候使用。最后我们调用了用户的回调函数。

下面我们看一下WM_NOTIFY消息,这个消息用于当前窗体处理TreeView的事件。我们处理了TVN_ITEMEXPANDING、TVN_DELETEITEM、TVN_SELCHANGED和TVN_BEGINDRAG事件。

TVN_ITEMEXPANDING 事件

当TreeView中的某个节点被展开或者收缩的时候,这个事件被激发。这时,WM_NOTIFY消息的lParam参数是NMTREEVIEW结构的指针。NMTREEVIEW结构中的action成员表示当前节点是被展(TVE_EXPAND)开还是被收缩(TVE_COLLAPSE)。NMTREEVIEW结构中的itemNew表示发出事件的当前节点。如果是被展开,当前节点的state成员标识着当前节点的状态,当某个节点第一次被展开时,state的TVIS_EXPANDEDONCE标志没有被置位,第一次展开完成后,state的TVIS_EXPANDEDONCE标志被置位。所以我们通过探测TVIS_EXPANDEDONCE标志是否被置位,来判断当前节点是否是第一次被展开。通常在某个节点第一次被展开时,准备相关的数据(如创建并填充所有的子节点);而不是第一次被展开时,我们可以什么都不做。无论是被展开还是被收缩,具体的操作我们都是调用回调函数,让用户实现。

TVN_DELETEITEM 事件

当TreeView中某个节点被删除时,这个事件被激发。这时,WM_NOTIFY消息的lParam参数是NMTREEVIEW结构的指针。NMTREEVIEW结构中的itemOld成员表示要被删除的节点。在这里我们调用用户参数,因为节点是用户创建的,用户可能在节点中存储了用户自定义信息,在这里用户可以释放用户自定义信息所使用的资源。

TVN_SELCHANGED 事件

当TreeView中某个节点被选中时,这个事件被激发。这时,WM_NOTIFY消息的lParam参数是NMTREEVIEW结构的指针。其中itemOld表示以前被选中的节点,itemNew表示后来被选中的新节点。这里调用用户的回调函数,让用户处理,如控制ListView显示当前选中的新节点的文件对象和子文件夹对象等。

TVN_BEGINDRAG 事件

当TreeView中某个节点开始被拖拽时,这个事件被激发。这时,WM_NOTIFY消息的lParam参数是NMTREEVIEW结构的指针。NMTREEVIEW结构中的itemNew表示被拖拽的当前节点。这里调用_tv_on_begin_drag函数:

   1: static LRESULT 
   2: _tv_on_begin_drag (struct tree_view* tv, LPNMTREEVIEW lpNMTV)
   3: {
   4:     /*
   5:     HIMAGELIST hDrag = NULL;
   6:     if (tv->cb.fn_begin_drag)
   7:     {
   8:         if (S_OK != tv->cb.fn_begin_drag (tv, lpNMTV))
   9:         {
  10:             return S_OK;
  11:         }
  12:     }
  13:     //Create an image list that holds our drag image
  14:     hDrag = TreeView_CreateDragImage(tv->hTree, lpNMTV->itemNew.hItem) ;
  15:     //begin the drag operation
  16:     ImageList_BeginDrag(hDrag, 0, 0, 0) ;
  17:     //hide the cursor
  18:     ShowCursor(FALSE) ;
  19:     //capture the mouse
  20:     SetCapture(GetParent(tv->hTree)) ;
  21:     //set global flag to indicate we are in the middle of a drag operation
  22:     tv->fDragging = TRUE ;
  23:     //convert coordinates to screen coordinates
  24:     ClientToScreen(tv->hTree, &(lpNMTV->ptDrag)) ;
  25:     //paint our drag image and lock the screen.
  26:     ImageList_DragEnter(NULL, lpNMTV->ptDrag.x, lpNMTV->ptDrag.y) ;
  27:     */
  28:     return S_OK ;
  29: }

这个函数被注释的部分先调用了用户的回调函数,然后设置拖拽的图标信息。

WM_MOUSEMOVE 消息

这个消息中调用了_tv_on_mouse_move_drag来处理拖拽过程中的图标显示:

   1: static BOOL 
   2: _tv_on_mouse_move_drag (struct tree_view* tv, WPARAM wParam, LPARAM lParam)
   3: {
   4:     POINT pnt;
   5:     HTREEITEM hItem = NULL;
   6:     TVHITTESTINFO tv_ht;
   7:  
   8:     pnt.x = GET_X_LPARAM(lParam);
   9:     pnt.y = GET_Y_LPARAM(lParam);
  10:  
  11:     if (tv->fDragging)
  12:     {
  13:         //unlock window and allow updates to occur
  14:         ImageList_DragLeave(NULL) ;
  15:         ClientToScreen(tv->hWnd, &pnt) ;
  16:         //check with the tree control to see if we are on an item
  17:         ZeroMemory(&tv_ht, sizeof(TVHITTESTINFO));
  18:         tv_ht.flags = TVHT_ON allowScriptAccess="never" allowNetworking="internal" wmode="transparent"ITEM;
  19:         tv_ht.pt.x = pnt.x;
  20:         tv_ht.pt.y = pnt.y;
  21:         ScreenToClient(tv->hTree, &(tv_ht.pt));
  22:         hItem = (HTREEITEM)SendMessage(tv->hTree, TVM_HITTEST, 0, (LPARAM)&tv_ht);
  23:  
  24:         if (hItem)
  25:         {
  26:             //if we had a hit, then drop highlite the item
  27:             TreeView_SelectItem (tv->hTree, hItem);
  28:         }
  29:  
  30:         //paint the image in the new location
  31:         ImageList_DragMove(pnt.x,pnt.y);
  32:         //lock the screen again
  33:         ImageList_DragEnter(NULL, pnt.x, pnt.y);
  34:     }
  35:     return TRUE;
  36: }

在这个处理过程中,不断的探测鼠标拖拽的对象覆盖了哪个节点,使被覆盖的节点处于被选中状态。

WM_LBUTTONUP 消息

这个消息表示拖拽结束,调用_tv_on_lbtn_up_drag停止拖拽图标的显示:

   1: static BOOL 
   2: _tv_on_lbtn_up_drag (struct tree_view* tv, WPARAM wParam, LPARAM lParam)
   3: {
   4:     HTREEITEM hItem = NULL;
   5:     TVHITTESTINFO tv_ht;
   6:     TVITEM tvi;
   7:     ZeroMemory(&tvi, sizeof(TVITEM));
   8:     ZeroMemory(&tv_ht, sizeof(TVHITTESTINFO));
   9:  
  10:     if (tv->fDragging)
  11:     {
  12:         ImageList_DragLeave(NULL);
  13:         ImageList_EndDrag();
  14:         ReleaseCapture();
  15:         //determin if we let up on an item
  16:         GetCursorPos(&(tv_ht.pt));
  17:         ScreenToClient(tv->hTree, &(tv_ht.pt));
  18:         tv_ht.flags = TVHT_ON allowScriptAccess="never" allowNetworking="internal" wmode="transparent"ITEM;
  19:         hItem = (HTREEITEM)SendMessage(tv->hTree, TVM_HITTEST, 0, (LPARAM)&tv_ht);
  20:         ShowCursor(TRUE);
  21:         tv->fDragging = FALSE;
  22:         if (hItem)
  23:         {
  24:             /*we need to defer changing the selection until done processing this message post message allows us to do this. */ 
  25:         PostMessage(tv->hWnd, TV_M_CHANGEFOCUS, (WPARAM)0, (LPARAM)hItem);
  26:         }
  27:     }
  28:     return TRUE;
  29: }

在这个消息中调用PostMessage发送了一个自定义的TV_M_CHANGEFOCUS异步消息,在这个消息中,我们调用TreeView_SelectDropTarget来选中最终拖拽的目标节点。

HWND tv_get_tree_view (struct tree_view* tv);

用于获得tree_view中的TreeView窗体句柄。

   1: HWND 
   2: tv_get_tree_view (struct tree_view* tv)
   3: {
   4:     if (tv)
   5:     {
   6:         return tv->hTree;
   7:     }
   8:     return NULL;
   9: }

HTREEITEM tv_insert_node (struct tree_view* tv, struct tv_node_info* tvni);

用于插入一个新节点:

   1: HTREEITEM 
   2: tv_insert_node (struct tree_view* tv, struct tv_node_info* tvni)
   3: {
   4:     TVINSERTSTRUCT tvins;
   5:     TVITEM tvi;
   6:     if (NULL == tv || NULL == tvni)
   7:     {
   8:         return NULL;
   9:     }
  10:  
  11:     ZeroMemory (&tvins, sizeof (TVINSERTSTRUCT));
  12:     ZeroMemory (&tvi, sizeof (TVITEM));
  13:  
  14:     tvi.mask = TVIF_TEXT | TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_PARAM;
  15:     if (tvni->iHasChild)
  16:     {
  17:         tvi.mask |= TVIF_CHILDREN;
  18:         tvi.cChildren = 1;
  19:     }
  20:     tvi.cchTextMax = MAX_PATH;
  21:     tvi.pszText = tvni->lpszText;
  22:     tvi.iImage = tvni->iImage;
  23:     tvi.iSelectedImage = tvni->iSelImage;
  24:     tvi.lParam = (LPARAM) tvni->lpParam;
  25:     tvins.item = tvi;
  26:     tvins.hInsertAfter = tvni->hAfter;
  27:     tvins.hParent = tvni->hParent;
  28:     return TreeView_InsertItem (tv->hTree, &tvins);
  29: }

在这个函数中,调用TreeView_InsertIterm来插入节点,并且设置了节点的文字、图片、被选中时的图片以及用户参数。

HRESULT tv_DoDragDrop (FORMATETC* pFormatEtc, STGMEDIUM* pStgMedium, UINT nCount, void* pArgs);

这个函数用于拖拽某个节点后,构造拖拽信息,并调用DoDragDrop函数进行拖拽后的处理。其中需要实现IDataObject和IDropSource这两个系统接口。如果想实现将某个节点表示的文件夹拖拽到支持拖拽的目标后,将这个文件夹拷贝到目标对象,那么必须调用这个函数。它可以支持将节点拖拽到Explorer。以后再详细介绍吧:

   1: HRESULT tv_DoDragDrop (FORMATETC* pFormatEtc, STGMEDIUM* pStgMedium, UINT nCount, void* pArgs)
   2: {
   3:     
   4:     IDataObject* pDO = NULL;
   5:     IDropSource* pDS = NULL;
   6:     struct ido_lpVtbl ido_vtbl;
   7:     struct ids_lpVtbl ids_vtbl;
   8:     DWORD dwOKEffect;
   9:  
  10:     ZeroMemory (&ido_vtbl, sizeof (struct ido_lpVtbl));
  11:     ZeroMemory (&ids_vtbl, sizeof (struct ids_lpVtbl));
  12:  
  13:     ido_vtbl.DAdvise = _tv_DAdvise;
  14:     ido_vtbl.DUnadvise = _tv_DUnadvise;
  15:     ido_vtbl.EnumDAdvise = _tv_EnumDAdvise;
  16:     ido_vtbl.EnumFormatEtc = _tv_EnumFormatEtc;
  17:     ido_vtbl.GetCanonicalFormatEtc = _tv_GetCanonicalFormatEtc;
  18:     ido_vtbl.GetData = _tv_GetData;
  19:     ido_vtbl.GetDataHere = _tv_GetDataHere;
  20:     ido_vtbl.QueryGetData = _tv_QueryGetData;
  21:     ido_vtbl.SetData = _tv_SetData;
  22:  
  23:     pDO = ido_create (&ido_vtbl, pFormatEtc, pStgMedium, nCount, pArgs);
  24:     if (NULL == pDO)
  25:     {
  26:         return S_FALSE;
  27:     }
  28:  
  29:     ids_vtbl.GiveFeedback = _tv_GiveFeedback;
  30:     ids_vtbl.QueryContinueDrag = _tv_QueryContinueDrag;
  31:  
  32:     pDS = ids_create (&ids_vtbl, pArgs);
  33:     if (NULL == pDS)
  34:     {
  35:         pDO->lpVtbl->Release (pDO);
  36:         return S_FALSE;
  37:     }
  38:  
  39:     ZeroMemory (&dwOKEffect, sizeof (DWORD));
  40:  
  41:     return DoDragDrop (pDO, pDS, DROPEFFECT_COPY, &dwOKEffect);
  42: }
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值