分类: C/C++
2008-08-07 17:40:49
class CMyApp : public CWinApp { public: CWinList m_winlist; // 打开的恶窗口列表 };
为了跟踪子窗口,你只需要在创建或销毁子窗口时对此列表进行添加或删除操作既可。显然做这件事情的最佳地点是在拟跟踪窗口的构造函数和析构函数中:
CMyView::CMyView() { theApp.m_winlist.Add(this); } CMyView::~CMyView() { theApp.m_winlist.Remove(this); }
除此之外,你还可以从 OnCreate 和 OnDestroy 处理函数中调用 Add 和 Remove,以保证你的列表之包含具备有效的
HWNDs 的窗口对象。因为 CWinList 是从 list
CWinList& wl = theApp.m_winlist; for (CWinList::iterator it=wl.begin(); it!=wl.end(); it ) { CWnd* pWnd = *it; // do something }
我写了一个小程序 WinCount,它用 CWinList 计数 MDI 子窗口。运行画面如 Figure 2 所示。
Figure 2 计算子窗口的 WinCount
WinCount 右下角有一个状态栏窗格显示打开的窗口数量,该程序的“关于”对话框列出了窗口的标题。这个“关于”对话框就是用 CWinList::iterator
来产生它的反馈消息;该状态栏窗格是一个标准的 MFC 指示器窗格,使用 ON_UPDATE_COMMAND_UI 来显示视图数量。
void CMainFrame::OnUpdateWinIndicator(CCmdUI* pCmdUI) { CString s; s.Format(_T("Open:%d"), theApp.m_winlist.size()); pCmdUI->SetText(s); }
List::size 是 STL list 方法,返回列表中的项数,注意在 WinCount
中,视图与子窗口是一对一的,所以计算出的视图数就是 MDI 子窗口的数量。如果你的 MDI 是更复杂的多视图子框架,那么你必须重写 CMDIChildWnd
以便在派生类中进行 Add/Remove 调用,或者你可以用其它窗口类,保证它在每个子框架中只出现一次。你可以随便使用多少个 CWinList
来跟踪不同的窗口类。 CFolderDialog 甚至具备一个将 PIDL 转换为 CString 类型路径名的函数。如果你给 出 BROWSEINFO::pszDisplayName
缓冲,则 SHBrowseForFolder 能将选中的文件夹作为 TCHAR
串返回,但返回的显示名称只有全路径的最后一部分——例如,如果全路径名是:C:\MyStuff\Pub\Photos,则显示的是“Photos”。如果你想得到全路径名,你必须用 SHGetPathFromIDList
或我自己的函数 GetPathName 转换 PIDL。如果显示名是你想要的,则调用 CFolderDialog::GetDisplayName。 这里有两件事情需要注意。第一,CFolderDialog 有包装器,SetStatusText 和 SetOKText,用于文件夹对话框消息 BFFM_SETSTATUSTEXT and BFFM_SETOKTEXT。如果你用
C 编程,你要调用 ::SendMessage;但 CFolderDialog 调用包装器既可。唯一的警告是你只能从通虚拟知消息处理函数(OnInitialized,OnSelChanged
等)中调用这些包装器,因为 m_hWnd 仅在文件夹对话框真正运行的时候才有效,调用 BrowseForFolder
之前或之后则不然。当其回调第一次收到通知消息时,CFolderDialog 在内部子类化文件夹对话框。第二,某些 BFFM_ 消息需要
Unicode 串,而不是 LPCTSTRs。这就是为何代码段中“Choose Me!”使用的是宽字符串的原因(带前缀 L)。
我知道我们处在一个令人兴奋得 MFC、.NET 和 GUI 框架时代,很多事情都可以让它们代劳——但不要忘了如何使用基本的数据结构!
[编辑更新 - 5/9/2005:在 Figure 1 所示的 CWinList 原创实现里,它是从 list
我正在用 Visual Studio .NET 和 MFC
做一个程序。在我的程序里,用户要选择一个文件夹,并在其中拷贝文件。我可以调用 OpenFileDialog
让用户选择某个文件,但如何让打开对话框只显示文件夹呢?我见过好多安装程序给出的对话框里只显示文件夹,但我好像找不到这样的标志。
你之所以找不到正确的标志,是因为你关注的函数不对!文件打开对话框(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);
如果你只是让用户得到一个文件夹,那么 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!");
}
如果你试图用 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