Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1997635
  • 博文数量: 960
  • 博客积分: 52560
  • 博客等级: 大将
  • 技术积分: 13131
  • 用 户 组: 普通用户
  • 注册时间: 2008-07-31 14:15
文章分类

全部博文(960)

文章存档

2011年(1)

2008年(959)

我的朋友

分类: C/C++

2008-08-01 17:07:03

下载本文示例代码
下载源代码:CAtWork0506.exe (195KB)
原文出处:Counting MDI Children, Browsing for Folders

本文发布后内容有更新,详情参见文章的“编辑更新”


计算 MDI 子窗口数
仅显示文件夹的打开对话框


我正在写一个 MFC 的多文档(MDI)应用。在父窗口中,我如何检查所有的MDI子窗口是否都已经关闭?如果都关闭了,那么我想在我的主窗口激活一个窗格。
Ramesh
Windows 和 MFC 不提供任何专门的函数来获取 MDI 子窗口数,但实现你想要的这个功能很容易。实际上,我可以想到半打方法来解决这个问题。你可以捕获 WM_CREATE/WM_DESTROY 消息;可以用 SetWindowsHookEx 安装 Windows 钩子;可以用 EnumWindows 来枚举子窗口并计算它的数量。但最简单的解决方法常常是最容易被忽视的方法。
  这个问题说白了,无非就是——应用程序使用 MDI 界面或者其它你自己设计的多窗口用户界面——说到底就是一个窗口列表。Figure 1 展示的是一个基于标准模板库(STL) list 的类,很难说这样封装是否值得,但我只是觉得 Windows 程序员在代码中敲入 “push_back”太不可思议。CWinList 使你用“Add”取而代之。为了使用 CWinList,只需要在某个地方添加一个全局实例,既可以是一个全局变量,也可以是主应用程序类中的一个数据成员:
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 派生而来的,所以处理过程中可以充分借助 STL的威力。例如,你可以用 STL list 的迭代器(iterator)枚举列表中的窗口:

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 来跟踪不同的窗口类。
  我知道我们处在一个令人兴奋得 MFC、.NET 和 GUI 框架时代,很多事情都可以让它们代劳——但不要忘了如何使用基本的数据结构!
  [编辑更新 - 5/9/2005:在 Figure 1 所示的 CWinList 原创实现里,它是从 list 派生而来的。然而,一般情况下,从 STL 容器派生被认为是一种糟糕的做法,因为这样做搞不好会导致不可预知的结果。本文最终下载的代码是一个新版本的 CWinList,使用 typedef。需要用 push_back 和 remove 来代替 Add 和 Remove。]


我正在用 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)的作者。通过 可以获得更多了解。   本文出自 MSDN Magazine 的 June 2005 期刊,可通过当地报摊获得,或者最好是 订阅   下载本文示例代码
阅读(297) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~