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. 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);
}
要注意一下这两者的区别。
- 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控件
- 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方法来设置显示的标题
- 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)