wtlExplorer研究

wtlExplorer研究

一直都想写个类似于window浏览器那样的程序。以前的想法就是手动的将几个磁盘加到根节点,然后通过FindFile来进行目录的列表和文件的列表。感觉还是没有找到真正正确的方法,昨天看wtlExplorer这个wtl库自带的例子时,发现他使用SHGetDesktopFolder等Shell函数来进行的,这个应该正儿八经的标准的做法了。因此,决定对这个例子进行仔细研究,希望通过这个例子能够将Shell的一些用法熟悉,特别是目录和文件处理相关的,第二点就是熟悉treeview和listview控件。最后将wtl进一步熟悉。

第一天,IShellFolder接口

先从IShellFolder这个接口开始吧,这个接口用来对windows的目录进行管理。在msdn中关于这个接口的详细说明。这个接口一般都是通过SHGetDesktopFolder这个shell函数来获得,这个函数用来获取桌面所对应的目录的IShellFolder接口。通过IShellFolder的 EnumObjects可以返回当前这个目录下所有的对象的ITEMIDLIST,有了这些idlist,就可以使用GetDisplayNameof来取得各个对象的名称,这样就可以显示在树形控件里。我先做了一个console的程序来测试一下各个函数的用法,具体程序如下:

int _tmain(int argc, _TCHAR* argv[])

{

     setlocale(LC_ALL, "chs"); //用来设置在控制台输出中文

     CComPtr<IShellFolder> spFolder;

     CComPtr<IEnumIDList> spEnumIDs;

     HRESULT hr = SHGetDesktopFolder(&spFolder);  //拿到桌面的IShellFolder接口

     ATLASSERT(SUCCEEDED(hr));

     hr = spFolder->EnumObjects(NULL, SHCONTF_FOLDERS | SHCONTF_NONFOLDERS, &spEnumIDs);

     ATLASSERT(SUCCEEDED(hr));

 

     LPITEMIDLIST pIDs;   //直接定义一个指针,试验中刚开始用的是ITEMIDLIST,然后求地址,发现不对

     ULONG ulFetched;

     STRRET str = { STRRET_WSTR };

 

     hr = spEnumIDs->Next(1, &pIDs, &ulFetched);

     ATLASSERT(SUCCEEDED(hr));

     while(ulFetched == 1)

     {

         hr = spFolder->GetDisplayNameOf(pIDs, SHGDN_NORMAL, &str);

         ATLASSERT(SUCCEEDED(hr));

         printf("%ls\n", str.pOleStr);

         hr = spEnumIDs->Next(1, &pIDs, &ulFetched);

     }

     return 0;

}

 

第二集 TREEView控件和IShellFolder

首先,在第一集中利用一个Console程序对IShellFolder接口进行了简单的熟悉,有了基础的了解,我尝试着在一个wtl程序中利用递归将目录树家在到一个treeview中,代码如下

LRESULT OnFileNew(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)

     {

         CComPtr<IShellFolder> spFolder;

         HRESULT hr = SHGetDesktopFolder(&spFolder);

         ATLASSERT(SUCCEEDED(hr));

         if(SUCCEEDED(hr))

         {

              CComPtr<IEnumIDList> spIdlst;

              hr = spFolder->EnumObjects(m_hWnd, SHCONTF_FOLDERS , &spIdlst);

              if(SUCCEEDED(hr))

              {

                   LPITEMIDLIST idlst;

                   ULONG ulFetched;

                   while(spIdlst->Next(1, &idlst, &ulFetched) == S_OK && ulFetched ==1)

                   {

                       InsertTreeNode(spFolder, NULL, idlst);

                   }

              }

         }

         return 0;

     }

 

     void InsertTreeNode(IShellFolder * pFolder, HTREEITEM hParentNode, LPITEMIDLIST lpChildObj)

     {

         iCnt ++;

         if(iCnt > 1000)    return ;  //如果不加这个限制,会有COM错误,我估计是树的节点太多了

         HRESULT hr;

         ULONG ulAtr;

         STRRET strDispName;

         hr = pFolder->GetDisplayNameOf(lpChildObj, SHGDN_NORMAL, &strDispName);

         hr = pFolder->GetAttributesOf(1, (LPCITEMIDLIST*)&lpChildObj, &ulAtr);

 

         if(ulAtr & (SFGAO_FOLDER | SFGAO_HASSUBFOLDER) )  // is folder

         {

         HTREEITEM hCurTreeNode = m_view.InsertItem(strDispName.pOleStr, hParentNode, NULL);

              CComPtr<IShellFolder> spSubFolder;

              hr = pFolder->BindToObject(lpChildObj, NULL, IID_IShellFolder, (void **)&spSubFolder);

              ATLASSERT(SUCCEEDED(hr));

 

              CComPtr< IEnumIDList> spEnumIds;

 

              hr = spSubFolder->EnumObjects(m_hWnd, SHCONTF_FOLDERS , &spEnumIds);

 

              ATLASSERT(SUCCEEDED(hr));

              LPITEMIDLIST pids;

              ULONG ulFetched;

 

              while(spEnumIds->Next(1, &pids, &ulFetched) == NOERROR)

              {

                   InsertTreeNode(spSubFolder, hCurTreeNode, pids);                

              }   

         }

     }

 

这是一段不成熟的代码。问题一,就是不断的递归,如果目录很多的话,就会报一个COM错误。这个问题我看了wtlexploer的代码,他每次只加了一层,由于在读取某个PIDL的属性的时候,可以获取到SFGAO_HASSUBFOLDER和SFGAO_FOLDER这个属性,这样就可以判断这个目录是不是一个目录。 问题二,GetAttributesOf的使用,这个函数在使用时,第三个参数要先初始化成我们需要查询的参数,然后再执行完后进行一下判断,我在刚开始使用时,没有初始化ULONG ulAtr,应该如下初始化:

         SFGAOF sf = SFGAO_FOLDER | SFGAO_HASSUBFOLDER;

         spFolder->GetAttributesOf(1, (LPCITEMIDLIST*)&pIDs, & sf);

         if(sf & (SFGAO_FOLDER | SFGAO_HASSUBFOLDER ))

                   ……….

对于每次只打开当前节点的一层的这种做法,必须要相应节点的打开操作,这个操作对应的消息是TVN_ITEMEXPANDING,是一个Notify的消息,在wtl中使用如下方式来进行绑定

NOTIFY_CODE_HANDLER(TVN_ITEMEXPANDING, OnTVItemExpanding)

 

第三集 SplitterWindow

为了实现类似于windows资源管理器的功能,必选先得学会怎么样进行双栏显示,左边那一栏显示树形结构,右面那一栏显示当前目录下的子项。在wtl中,这种风格使用CSplitterWindow来进行实现。

  1. 1.       PreTranslateMessage

首先在mainFrm中定义一个CSplitterWindow的m_view变量,而不是使用向导所生成的View。这时编译会报错,提示CSplitterWindow么有实现PreTranslateMessage函数。向导生成的代码如下:   

virtual BOOL PreTranslateMessage(MSG* pMsg)    {

         if(CFrameWindowImpl<CMainFrame>::PreTranslateMessage(pMsg))

              return TRUE;

         return m_view.PreTranslateMessage(pMsg);

     }

在wtlExplore中,这个函数的定义如下;

     virtual BOOL PreTranslateMessage(MSG* pMsg)

     {

         return CFrameWindowImpl<CMainFrame>::PreTranslateMessage(pMsg);

     }

要注意一下这两者的区别。

  1. 2.   创建树形控件和列表控件

有了这个splitter窗体之后,就可以创建包含在这个窗体之间的树形控件和列表控件,以树形控件为例

先在mainfrm中顶一个CTreeViewCtrlEx的成员变量。    CTreeViewCtrlEx m_tvFolders;

 

         m_tvFolders.Create(m_view, CWindow::rcDefault, NULL,

              WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN |

              TVS_HASLINES | TVS_LINESATROOT | TVS_HASBUTTONS | TVS_SHOWSELALWAYS,

              WS_EX_CLIENTEDGE);    

         m_view.SetSplitterPanes(m_tvFolders, NULL);

第一行代码是创建树形控件,以splitterControl为父窗体;同时定义一些风格。第二句是splitter两个栏中的控件设置,我们只设置了左栏,右栏设置为空。

按照同样的步骤我们可以建立一个CListViewCtrl控件

  1. 3.   CPaneContainer

这个控件产生一个带有标题和关闭按钮的容器,在split中使用的方法和其他控件一样。代码如下

         m_leftPane.Create(m_view);

         m_tvFolders.Create(m_leftPane, CWindow::rcDefault, NULL,

              WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN |

              TVS_HASLINES | TVS_LINESATROOT | TVS_HASBUTTONS | TVS_SHOWSELALWAYS,

              WS_EX_CLIENTEDGE);

        

         m_leftPane.SetClient(m_tvFolders);

SetClient函数可以设置这个容器里所包含的子控件。

使用SetTitle方法来设置显示的标题

 

  1. 4.   CSplitterWindow

bool SetSinglePaneMode(int nPane = SPLIT_PANE_NONE)可以通过这个方法来设置分栏显示的栏位数量。

第四集 TreeView中节点数据的添加, PIDL

在Treeview中,节点状态TVIS_EXPANDEDONCE 表示这个节点至少被打开过一次。

在WTLExplorer中,由于树形列表不是一次性加载,所以在每个节点中都保存了这个节点的相关信息,每个节点的数据定义为:

typedef struct _TVItemData

{

       _TVItemData()

       { }

      

       CComPtr<IShellFolder> spParentFolder;

      

       CShellItemIDList lpi;

       CShellItemIDList lpifq;

 

} TVITEMDATA, *LPTVITEMDATA;

 

系统中与PIDL有关的机构如下:

typedef struct _ITEMIDLIST

    {

    SHITEMID mkid;

    } ITEMIDLIST;

 

typedef struct _SHITEMID

    {

    USHORT cb;

    BYTE abID[ 1 ];

    } SHITEMID;

一个PIDL就是一个pointer to an item identifier list,就是由SHITEMID所组成的一个列表。这个列表的末尾用一个cb为0的SHITEMID来表示。假设lpIDL是指向当前SHITEMID的一个指针,那lpIDL+(lpIDL->cb)就是指向下一个SHITEMID的地址了。

 

 

 

这个结构的特点

 

 

 

 

 

 

 

 

WM_NOTIFY消息

Msdn里这个消息的功能如下:

Sent by a common control to its parent window when an event has occurred or the control requires some information.

这个消息是控件用来主动通知父窗体,他有个事件发生了。

要使用这个消息,应该通过SendMessage来将消息发送给父窗体,具体用法可以参见msdn。

lResult = SendMessage( (HWND)hWndControl , (UINT) WM_NOTIFY, (WPARAM) wParam, (LPARAM) lParam)

其中第一个参数据msdn来说,应该是子空间的父控件的HWND。

第四个参数lParam是一个指向NMHDR 结构的一个指针,这个结构中包含了notification code 和一些附加的信息。同时,具体该消息相关的控件ID也在这个结构NMHDR的hwndFrom和idFrom数据成员里。

如果要在wtl中对该TreeView的SelChanged事件进行相应,可以用下面的宏

       NOTIFY_HANDLER(IDC_TREE, TVN_SELCHANGED, OnChange)

还可以用

    NOTIFY_CODE_HANDLER(TVN_SELCHANGED, OnChange)

总结。。

原文地址:https://www.cnblogs.com/kwliu/p/2080000.html