在vc6中,如何创建“浏览文件夹”对话框(转)

来源:互联网 发布:tensorflow源代码阅读 编辑:程序博客网 时间:2024/05/01 23:48

我正在用 Visual Studio .NET 和 MFC 做一个程序。在我的程序里,用户要选择一个文件夹,并在其中拷贝文件。我可以调用 OpenFileDialog 让用户选择某个文件,但如何让打开对话框只显示文件夹呢?我见过好多安装程序给出的对话框里只显示文件夹,但我好像找不到这样的标志。

Laine Chandler

你之所以找不到正确的标志,是因为你关注的函数不对!文件打开对话框(Win32 是 GetOpenFileName,而 MFC 是 CFileDialog)是不处理文件夹的。为了只显示文件夹,你必须调用专门的外壳函数 SHBrowseForFolder。为了使用这个函数,你得填写一大堆 BROWSEINFO 结构信息,然后调用 SHBrowseForFolder。Windows 显示一个类似 Figure 3 这样的对话框。用户可以导航文件夹层次,扩展和收缩它们,并选择想要的文件夹。


Figure 3 调用 SHBrowseForFolder

  不幸的是,使用 SHBrowseForFolder 并不象 CFileDialog 那么容易。它需要理解外壳的基本工作模式,外壳严重依赖 COM,IShellFolder 和 PIDLs。所谓 PIDL(读作“piddle”)是 pointer-to-item-ID-list (ID 列表项指针)的简称。实际对应的 C 类型是 LPITEMIDLIST,或常量变体 LPCITEMIDLIST。PIDL 是一字节串,外壳用它来确定外壳对象,如文件、文件夹,以及伪对象如:我的电脑或网络邻居。对于普通的文件和文件夹,该字节串包含 Unicode 路径名,而对于其它对象则不然。重要的是当你的用户最终选择某个文件夹时,SHBrowseForFolder 返回的是一个 PIDL。为了获得路径名,你得进行转换。
  因为 SHBrowseForFolder 非常有用,而 MFC 又没有提供什么类来封装它,我决定自己为大家写一个。(我知道,我是个好人)。CFolderDialog 隐藏了复杂的代码,使得你浏览文件夹易如反掌。你只需要实例化并调用BrowseForFolder 既可:
CFolderDialog dlg(this);LPCITEMIDLIST pidl = dlg.BrowseForFolder(_T("Pick a folder, dude!"), BIF_RETURNONLYFSDIRS | BIF_STATUSTEXT);CString path = dlg.GetPathName(pidl);

  CFolderDialog 甚至具备一个将 PIDL 转换为 CString 类型路径名的函数。如果你给 出 BROWSEINFO::pszDisplayName 缓冲,则 SHBrowseForFolder 能将选中的文件夹作为 TCHAR 串返回,但返回的显示名称只有全路径的最后一部分——例如,如果全路径名是:C:/MyStuff/Pub/Photos,则显示的是“Photos”。如果你想得到全路径名,你必须用 SHGetPathFromIDList 或我自己的函数 GetPathName 转换 PIDL。如果显示名是你想要的,则调用 CFolderDialog::GetDisplayName。
如果你只是让用户得到一个文件夹,那么 BrowseForFolder 和 GetPathName 就足够了。但 SHBrowseForFolder能做更多的事情。由于有了 GetOpenFileName,你可以提供一个回调函数来定制其行为。如果提供 BROWSEINFO::lpfn 中的 BrowseCallbackProc,Windows 只要发现你填充了这个域便会调用它。例如,Windows 在文件夹对话框自身被初始化时发送 BFFM_INITIALIZED,当用户选择某个新文件夹时发送 BFFM_SELCHANGED。你的回调过程能处理这些通知消息并做任何想做的事情。例如,你可以通过发送 BFFM_ENABLEOK 来启动或禁用 OK 按钮,或者用 BFFM_SETOKTEXT 改变按钮文本。CFolderDialog 用典型的 MFC 方式的 C++ 虚拟处理函数来替代 C 语言风格的回调,所以要写一个从 CFolderDialog 派生的回调过程并该写 OnInitialized 和 OnSelChanged 这样的虚函数。CFolderDialog 在内部使用其自己调用这些方法的回调。
  为了操练更多 CFolderDialog 的高级特性,我写了一个测试程序 FolderPick。它有两个运行文件夹对话框的命令。一个显示“旧式”对话框;另一个显示新式对话框。新式样(BIF_NEWDIALOGSTYLE)创建一个更大的,可变大小的对话框,对应的按钮是“Make New Folder”——如果你指定 BIF_EDITBOX——则有一个编辑框,用户可以敲入文件夹的名字。其它标志还有 BIF_BROWSEFORCOMPUTER,用于显示计算机,BIF_BROWSEFORPRINTER 用于打印机。BIF_RETURNONLYFSDIRS 告诉 Windows 仅返回文件系统目录,而非伪文件夹,如网络邻居,BIF_STATUSTEXT 创建一个状态窗口,你可以设置其文本。(新式对话框不支持 BIF_STATUSTEXT)。完整的标志清单请参考 BROWSEINFO 文档。
  FolderPick 派生一个新类,CMyFolderDialog,重写了 nInitialized 和 OnValidateFailed。当该对话框被初始化时,FolderPick 设置状态文本并将 OK 按钮的名字改为“Choose Me!”:

void CMyFolderDialog::OnInitialized(){    SetStatusText(_T("Nice day, isn''t it?"));    SetOKText(L"Choose Me!");}

  这里有两件事情需要注意。第一,CFolderDialog 有包装器,SetStatusText 和 SetOKText,用于文件夹对话框消息 BFFM_SETSTATUSTEXT and BFFM_SETOKTEXT。如果你用 C 编程,你要调用 ::SendMessage;但 CFolderDialog 调用包装器既可。唯一的警告是你只能从通虚拟知消息处理函数(OnInitialized,OnSelChanged 等)中调用这些包装器,因为 m_hWnd 仅在文件夹对话框真正运行的时候才有效,调用 BrowseForFolder 之前或之后则不然。当其回调第一次收到通知消息时,CFolderDialog 在内部子类化文件夹对话框。第二,某些 BFFM_ 消息需要 Unicode 串,而不是 LPCTSTRs。这就是为何代码段中“Choose Me!”使用的是宽字符串的原因(带前缀 L)。
  如果你试图用 C 编写 SHBrowseForFolder 程序,Microsoft 的文档有两个小错误,我必须在此指出来。文档中说 BFFM_SETOKTEXT 使用的字符串是在 WPARAM 中传递的,但实际上却是在 LPARAM 中。文档还说 BFFM_SETSELECTION 需要 Unicode 字符串,但 BFFM_SETSELECTION 的处理函数 A 和 W两个版本都有。所以你可以在其中使用 LPCTSTR。
  如果你在新式样对话框中使用 BIF_EDITBOX,Windows 将显示一个编辑框,用户可以在其中敲入文件夹名。如果用户敲入非法数据,Windows 将用 BFFM_VALIDATEFAILED 调用浏览过程。CFolderDialog 会调用 OnValidateFailed 来处理它。FolderPick 该写了 OnValidateFailed 以显示如 Figure 4 所示的出错信息框。

BOOL CMyFolderDialog::OnValidateFailed(LPCTSTR lpsz){    MessageBox(...);    return TRUE; // don''t dismiss dialog}


Figure 4 FolderPick 的出错框

  SHBrowseForFolder 支持的另一个很酷的特性是过滤器定制。它能让你以项目为单位控制在文件夹对话框中显示那些项。使用回调时,这种机制冗长而繁琐。你必须得实现 COM 接口,IFolderFilter,它有两个方法:GetEnumFlags 和 ShouldShow。当文件夹对话框向你的回调发送 BFFM_IUNKNOWN 时,你必须要用 QueryInterface 查询为 IFolderFilterSite 而传递的 IUnknown,然后用你的 IFolderFilter 调用 IFolderFilterSite::SetFilter。现在文件夹对话框调用你的 IFolderFilter::ShouldShow 来过滤每一项,当不再需要 IFolderFilterSite 时必须对之进行 Release 操作。
  自然,我将所有这些细节都作了封装。为了使用定制的过滤器,你只需以 bFilter=TRUE 调用 BrowseForFolder 既可,并改写两个虚函数:OnGetEnumFlags 和 OnShouldShow。不需要再处理 COM 的细节——QueryInterface 或 IFolderFilter。Figure 5 具体实现代码和 Figure 6。CFolderDialog 在内部实现了自己的 IFolderFilter,也就是调用对应的虚拟函数。CFolderDialog::OnIUnknown 使用了活动模板库(ATL)CComQIPtr 智能指针类,它使得 COM 编码轻而易举。
  如果你决定用定制的过滤器,要小心,因为它改写了 BIF_RETURNONLYFSDIRS 这样的标志(只返回文件系统目录)。出于好玩,我决定当所选的项目不是文件系统目录时,通过手动禁用 OK 按钮,自己为 FolderPick 实现 BIF_RETURNONLYFSDIRS。为了检查文件系统对象,你要考虑适当的方式调用 IShellFolder::GetAttributesOf 并寻找 SFGAO_FILESYSTEM。但是当我尝试这样做的时候,“我的电脑”得到的是SFGAO_FILESYSTEM 属性,尽管它并非真正的文件系统目录!后来,我发现要想知道某个外壳对象是否为真正的文件或文件夹,唯一可靠的方法是调用 GetPathName 并检查串是否为空。在 CMyFolderDialog 中我就是这么做的,一旦碰到非文件夹则禁用 OK 按钮。具体细节请参考下载的源代码。
  最后,为了帮助你理解来龙去脉,我对运行时的 CFolderDialog 进行了 TRACE 跟踪诊断。如图 Figure 6 所示。


Figure 6 TraceWin

  你可以通过设置 CFolderDialog::bTRACE 全局变量对诊断进行开/关控制。当然,跟踪只能在调试模式进行。下载的代码中附带有 TraceWin 的免费拷贝,以便不用运行调试器就能察看诊断输出。
  SHBrowseForFolder 还有许多标志和特性我没有发掘,但是不论你使用哪种特性,CFolderDialog 都能减轻你的工作负担并让你能用 MFC 的方式来编写使用 SHBrowseForFolder 的程序。

祝编程愉快!

您的提问和评论可发送到 Paul 的信箱:cppqa@microsoft.com
 

 

作者简介
  Paul DiLascia 是一名自由作家,顾问和 Web/UI 设计者。他是《Writing Reusable Windows Code in C++》书(Addison-Wesley, 1992)的作者。通过 http://www.dilascia.com 可以获得更多了解。   本文出自 MSDN Magazine 的 June 2005 期刊,可通过当地报摊获得,或者最好是 订阅