Windows Vista for Developers——第六部分:新的文件对话框
作者:Kenny Kerr
翻译:Dflying Chen
请同时参考《Windows Vista for Developers》系列。
正如Aero向导比传统的向导更加友好,任务对话框比老式的消息框更加友好一样,Windows Vista中最新的文件对话框也给用户带来了全新的体验,代替了那有些年头的GetOpenFileName 和GetSaveFileName 函数。最新的文件对话框不但与Windows Vista的外观保持一致,而且也提供了全新的COM接口,简化了使用的方式并为今后的扩展留下了充分的空间。
在《Windows Vista for Developers》系列系列的第六篇中,我们就来看看这些最新的、通过IFileDialog 相关接口提供的文件对话框API。本文将首先查看一下相关的各种接口,然后使用一个C++类模板简化其使用方式。在进入代码之前,我们还是先来看看这个新的文件对话框能给用户带来什么改变吧。
用户体验
下面这个截图就是使用GetOpenFileName 函数得到的传统打开文件对话框:
大多数用户都对该对话框很熟悉,但对于那些习惯了使用Windows Vista的用户来说,这个对话框却似乎有些让人迷惑,显得与Windows Vista的格格不入。文件夹的导航方式与Vista的资源管理器不一样,很多Vista中的新东西,例如那个内建的搜索框也不见了。在Vista中,资源管理器的样式如下所示:
下面一张截图就是Windows Vista中的新版本的打开文件对话框:
除了底部的那个面板之外,其他部分都与资源管理器窗口非常类似。这种一致性保证了用户在使用Windows Vista时感到得心应手。接下来就让我们看看如何才能在应用程序中使用这个最新的打开文件对话框。
传统的方式
作为参考,我们首先看看大多数Windows开发者(使用C++)是如何创建一个传统的文件对话框的。大多数开发者都不会直接使用GetOpenFileName 或GetSaveFileName 函数,而是会使用MFC或WTL的CFileDialog 类。下面就是在WTL中的一个示例:
class OldSampleDialog : public CFileDialog
{
public:
OldSampleDialog() :
CFileDialog(true) // open dialog
{
// TODO: Customize dialog with m_ofn
}
};
OldSampleDialog dialog;
INT_PTR result = dialog.DoModal();
MFC和WTL都不支持新版本的文件对话框,至少在我书写本文的时候还不能,但本文的目的之一就是用C++封装出一个类似的、使用起来同样方便的辅助类。首先我们先来看一下这些新的API。
新的API
正如我在前面提到过的那样,最新的文件对话框API是通过一系列的COM接口提供给开发者的。下面就是其中一些最常见的:
- IFileOpenDialog:由FileOpenDialog COM类实现,提供打开文件对话框的相关方法。
- IFileSaveDialog:由FileSaveDialog COM类实现,提供保存文件对话框的相关方法。
- IFileDialog:IFileOpenDialog 和IFileSaveDialog 的基类,提供大多数与对话框交互并进行自定义的功能的实现。
- IModalWindow: IFileDialog 的基类。提供了Show方法。
- IFileDialogCustomize:由FileOpenDialog 和FileSaveDialog COM 类实现,提供了对在对话框中添加新控件的支持。
- IFileDialogEvents:由应用程序实现,实现侦听对话框中发生的事件的功能。
- IFileDialogControlEvents:由应用程序实现,实现侦听对话框中添加的控件所发生的事件的功能。
我们还可能会使用到一些不是全新的接口,例如IShellItem。本文将在稍后作以讨论。
我们可以使用CComPtr 类模板创建一个打开文件对话框,代码如下:
CComPtr<IFileOpenDialog> dialog;
HRESULT result = dialog.CoCreateInstance(__uuidof(FileOpenDialog));
一般来讲,我们会使用从IFileDialog继承下来的方法来自定义该对话框,这部分内容将在稍后介绍。若想更进一步进行自定义,那么可以使用IFileDialogCustomize 接口所提供的功能:
CComPtr<IFileDialogCustomize> customize;
HRESULT result = dialog.QueryInterface(&customize);
准备完成之后,即可将该对话框显示出来了:
HRESULT result = dialog->Show(parentWindow);
Show 方法将阻止在这里,直到对话框关闭为止。
文件类型
我们应该知道,IFileDialog 不使用来自于OPENFILENAME 的REG_MULTI_SZ相关字符串,而使用了更加简单的COMDLG_FILTERSPEC 结构来指定该文件对话框能够接收(打开/保存)的文件类型:
COMDLG_FILTERSPEC fileTypes[] =
{
{ L"Text Documents", L"*.txt" },
{ L"All Files", L"*.*" }
};
HRESULT result = dialog->SetFileTypes(_countof(fileTypes),
fileTypes);
这种模型简化了动态创建文件类型列表时的工作,也不必再引入那个令人头痛的字符串表了。下面这个示例演示了如何使用字符串表实现本地化支持:
CString textName;
VERIFY(textName.LoadString(IDS_FILTER_TEXT_NAME));
CString textPattern;
VERIFY(textPattern.LoadString(IDS_FILTER_TEXT_PATTERN));
CString allName;
VERIFY(allName.LoadString(IDS_FILTER_ALL_NAME));
CString allPattern;
VERIFY(allPattern.LoadString(IDS_FILTER_ALL_PATTERN));
COMDLG_FILTERSPEC fileTypes[] =
{
{ textName, textPattern },
{ allName, allPattern }
};
SetFileTypeIndex 方法允许我们设置对话框中被选中的文件类型。需要注意的是这个索引是从1开始的,而不是C或C++程序员所熟悉的从0开始。
HRESULT result = dialog->SetFileTypeIndex(2);
无论对话框是否已经打开,我们都可以调用SetFileTypeIndex 方法来改变当前被选中的文件类型。而GetFileTypeIndex 则可以在对话框打开时或关闭后调用。
UINT index = 0;
HRESULT result = dialog->GetFileTypeIndex(&index);
文件对话框选项
文件对话框提供了非常多的选项,我们可以借助于这些选项控制对话框的样式以及行为。这些选项均被声明为标记,并封装在一个DWORD中。当前应用的选项可通过GetOptions 方法获取,修改后也可以使用SetOptions 方法重新设定。除非你非常清楚每一个选项的意义,否则最好首先调用GetOptions 方法,然后根据需要修改之后再使用SetOptions 方法重新设定回去。这样即可避免丢失某些预先设定好的选项。
下列代码演示了如何强制打开预览面板:
DWORD options = 0;
HRESULT result = dialog->GetOptions(&options);
if (SUCCEEDED(result))
{
options |= FOS_FORCEPREVIEWPANEON;
result = dialog->SetOptions(options);
}
当然,用户也可以手工显示/隐藏该预览面板(通过工具栏上的Organize 下拉菜单):
SetOptions方法的文档详细介绍了我们能够使用的所有选项。
标签和文本框控件
我们也可以改变对话框上的一些文本元素的默认值。
SetTitle 方法可以设定该对话框的标题。SetOkButtonLabel 方法用来设置对话框默认按钮上的文字。SetFileNameLabel 方法用来设定文件名文本框控件旁边的那个标签上的文字。
我们也可以使用SetFileName 和GetFileName 方法设定/获取文件名文本框控件中的文本。
Shell项目
很多接口中的用来控制文件对话框的方法使用shell项目,而不是文件系统路径来引用文件夹。之所以要这样做,是因为这样之后,对话框不仅仅能够与文件系统中真实存在的文件夹打交道,还可以引用一些“虚拟”的文件夹。例如,“Computer”文件夹中就有控制面板的条目。若想了解更多常见的shell项目(无论是否为虚拟项目),你可以参考我的《Known Folders Browser》这篇文章。
shell项目非常灵活,借助于他们的帮助,我们可以容易地与很多接受IShellItem 的shell API打交道。但如果你只知道文件系统路径的话,那么就必须将其转换为相应的shell项目。幸运的是Windows Vista引入了一个新的名为SHCreateItemFromParsingName 的函数帮助你完成这件事:
CComPtr<IShellItem> shellItem;
HRESULT result = ::SHCreateItemFromParsingName(L"D:\\SampleFolder",
0,
IID_IShellItem,
reinterpret_cast<void**>(&shellItem));
类似地,若想得到某个shell项目的文件系统路径,那么可以使用GetDisplayName 方法:
AutoTaskMemory<WCHAR> buffer;
HRESULT result = shellItem->GetDisplayName(SIGDN_FILESYSPATH,
&buffer.p);
这里的AutoTaskMemory 是一个简单的类模板。在其析构函数中调用了CoTaskMemFree 方法,以便释放由GetDisplayName 函数分配的内存。需要注意的是,shell项目并不一定总是指向某个文献系统路径的。例如若你将“::{ED228FDF-9EA8-4870-83b1-96b02CFE0D52}”传递给SHCreateItemFromParsingName 函数,将会得到指向“Games”虚拟文件夹的shell项目。因为该虚拟文件夹在文件系统上并不存在,所以调用其GetDisplayName 方法将会失败。
简单介绍shell项目之后,我们也应该回到正题上来了,接下来我们来看看文件对话框是如何使用shell项目的。IFileDialog 接口使用shell项目来甄别文件夹和选择。例如,我们可以按照如下代码设置文件对话框的初始文件夹:
CString path = // load path...
CComPtr<IShellItem> shellItem;
HRESULT result = ::SHCreateItemFromParsingName(path,
0,
IID_IShellItem,
reinterpret_cast<void**>(&shellItem));
if (SUCCEEDED(result))
{
result = m_dialog->SetFolder(shellItem);
}
在对话框打开之后,我们仍可以调用SetFolder 方法,让对话框导航至指定的文件夹。而GetFolder 则可以用来得到该文件对话框在初始时将要显示的文件夹,若对话框已经打来了的话,那么得到当前正选中的文件%