Windows Vista for Developers – Part 6 – The New File Dialogs
Just as
Aero wizards
enable a better user experience compared to traditional
wizards and
task dialogs
enable a better user experience compared to the age-old
message box, so the new file dialogs provide a fresh new
look and a welcome replacement for the aged
GetOpenFileName and
GetSaveFileName functions. Not only do the
new file dialogs provide a very consistent appearance
relative to the Windows Vista shell but they are also
exposed through a brand new COM interface, simplifying their
use and opening the door for future improvements.
In
this part 6 of the
Windows Vista for Developers
series, we are looking at the new file dialogs API that is
exposed through IFileDialog and friends.
We’ll first examine the various interfaces and then take a
look at a C++ class template that simplifies their use
considerably. Before we dive into the code let’s take a look
at appearances to see what benefit users gain from this
change.
User Experience
The following
window clipping
presents the traditional open dialog created using the
GetOpenFileName function:
Most users are familiar with this dialog,
however users who are learning to use Windows for the first
time on Windows Vista or even users who have just become
used to Windows Vista may find it a bit frustrating since it
doesn’t have any consistency with the Windows Vista shell.
The way folders are navigated is different to Windows
Explorer and many of the tools that users might depend on
such as the built-in search capabilities are not present in
the traditional file dialogs. The following image presents
the same folder in Windows Explorer:
Now look at the new open dialog introduced
with Windows Vista:
Notice how it looks almost exactly the same
with the exception of the lower panel. This is the kind of
consistency that makes working with Windows a pleasure. On
that note, let’s look at some code to see what’s involved
with adopting the new file dialogs in your own
applications.
The Old Way
As a point of reference, let’s quickly recap
what most Windows developers (using C++) have traditionally
used to construct a file dialog. Most developers avoid using
the GetOpenFileName and
GetSaveFileName functions and instead used
MFC or WTL’s CFileDialog class. Here is an
example using WTL:
class OldSampleDialog : public CFileDialog
{
public:
OldSampleDialog() :
CFileDialog(true) // open
dialog
{
// TODO: Customize dialog
with m_ofn
}
};
OldSampleDialog
dialog;
INT_PTR result = dialog.DoModal();
Neither MFC nor WTL support the new file
dialogs at the time of writing but one of the goals of this
article is to reach a comparable level of conciseness with a
little help from C++. But first let’s examine the new
API.
The New API
As I
hinted at before, the new file dialogs are exposed through a
set of COM interfaces. Here are the main interfaces that you
should familiarize yourself with.
IFileOpenDialog – Implemented by the FileOpenDialog COM
class and provides methods specific to open dialogs.
IFileSaveDialog – Implemented by the FileSaveDialog COM
class and provides methods specific to save dialogs.
IFileDialog – Inherited by both IFileOpenDialog and
IFileSaveDialog and provides most of the
methods for customizing and interacting with a dialog.
IModalWindow – Inherited by IFileDialog and provides
the Show method.
IFileDialogCustomize – Implemented by the FileOpenDialog and
FileSaveDialog COM classes and provides
methods to add controls to a dialog.
IFileDialogEvents – Implemented by an application to allow notification of
events within a file dialog.
IFileDialogControlEvents – Implemented by an application to allow notification of
events related to controls added by an application to a file
dialog.
There are some not entirely new
interfaces, such as IShellItem, that you
may also need to use. I’ll talk about those a little later
in this article.
You can use the
CComPtr class template to create an open
dialog as follows:
CComPtr<IFileOpenDialog> dialog;
HRESULT
result =
dialog.CoCreateInstance(__uuidof(FileOpenDialog));
Typically you will simply customize the dialog
using the methods inherited from
IFileDialog. We will explore this in the
following sections. For further customization you can query
for the IFileDialogCustomize interface as
follows:
CComPtr<IFileDialogCustomize> customize;
HRESULT
result = dialog.QueryInterface(&customize);
Once you’re ready to display the dialog you can
do so using the Show method:
HRESULT result = dialog->Show(parentWindow);
The Show method will block until the dialog is
dismissed.
File Types
You should be relieved to know that
IFileDialog has not adopted the use of
REG_MULTI_SZ strings from
OPENFILENAME and instead uses the more
approachable COMDLG_FILTERSPEC structure to
declare the file types that a dialog can open or save:
COMDLG_FILTERSPEC fileTypes[] =
{
{ L"Text
Documents", L"*.txt" },
{ L"All Files", L"*.*"
}
};
HRESULT result =
dialog->SetFileTypes(_countof(fileTypes),
fileTypes);
This model simplifies constructing the list of
file types dynamically and also reduces the headaches
involved with using a string table. The following example
demonstrates how you might use a string table to simplify
localization:
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 }
};
The SetFileTypeIndex method
allows you to indicate which file type appears selected in
the dialog. Keep in mind that this is a one-based index
rather than the usual zero-based index familiar to C and C++
developers.
HRESULT result = dialog->SetFileTypeIndex(2);
SetFileTypeIndex can be called
before the dialog is displayed as well as while the dialog
is open to change the selection programmatically. The
GetFileTypeIndex method on the other hand
can only be called while the dialog is open or after it has
closed.
UINT index = 0;
HRESULT result =
dialog->GetFileTypeIndex(&index);
Options
The file
dialog offers a long list of options that you can control to
affect the appearance and behavior of the dialog. The
options are declared as flags packed into a
DWORD. The options currently defined can be
retrieved using the GetOptions method and
changes can be made using the
SetOptions method. Unless you know exactly
which options you need and which you don’t, it’s generally a
good idea to call GetOptions first and then
modify the resulting DWORD before calling
SetOptions again. Doing so will preserve
any default options that may already be set depending on the
type of dialog.
The following example shows how
you can force the preview pane to be displayed:
DWORD options = 0;
HRESULT result =
dialog->GetOptions(&options);
if
(SUCCEEDED(result))
{
options |=
FOS_FORCEPREVIEWPANEON;
result =
dialog->SetOptions(options);
}
The user can of course manually show or hide
the preview pane using the Organize drop-down menu from the
toolbar.
The documentation for the
SetOptions
method provides a complete list of options that you can
control.
Labels and Edit Controls
A few of the static text elements on the dialog
can be changed from their defaults.
The
SetTitle method sets
the dialog’s title. The
SetOkButtonLabel method sets the text for the dialog’s default button. The
SetFileNameLabel method sets the text for the label appearing next to the
file name edit control.
You can also get and set
the text that appears in the file name edit control using
the SetFileName and
GetFileName methods.
Shell Items
Quite a few of the interface methods used for
controlling the file dialogs use shell items to refer to
folders instead of file system paths. The reason for this is
so that the dialog can communicate information about not
only file system folders but also other virtual folders that
you find in the shell such as the control panel or the
“Computer” folder. To get a good feel for all the well-known
folders on your computer, both virtual and file
system-based, check out my
Known Folders Browser.
Shell items are very flexible and provide a
great way to interact with many of the other shell APIs that
use IShellItem to
represent shell items. Of course if all you need is to
specify a file system path then you’ll need a way to convert
a path to a shell item. Fortunately Windows Vista introduces
the extremely handy
SHCreateItemFromParsingName function to do just this:
CComPtr<IShellItem> shellItem;
HRESULT result =
::SHCreateItemFromParsingName(L"D:\\SampleFolder",
0,
IID_IShellItem,
reinterpret_cast<void**>(&shellItem));
Similarly if you are handed a shell item and
need to determine the file system path it represents you can
use the
GetDisplayName method
as follows:
AutoTaskMemory<WCHAR> buffer;
HRESULT result =
shellItem->GetDisplayName(SIGDN_FILESYSPATH,
&buffer.p);
AutoTaskMemory is just a simple class template I use. It calls the
CoTaskMemFree function in its destructor to free the memory allocated by
the
GetDisplayName function. Keep in mind that a given shell item might not
actually refer to a file system item. For example if you
pass “::{ED228FDF-9EA8-4870-83b1-96b02CFE0D52}” to
SHCreateItemFromParsingName you will get the shell item referring to “Games” virtual
folder. Since this is not a file system folder, requesting
its file system path with the
GetDisplayName method
will fail.
With that extremely light introduction
to shell items out of the way let’s take a look at how shell
items are used with file dialogs. The
IFileDialog interface
uses shell items to identify folders and selections. For
example, you can set the folder that is initially displayed
as follows:
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);
}
The
SetFolder method can
also be called while the dialog is open, causing the dialog
to navigate to the specified folder. The
GetFolder method on
the other hand can be used to determine the folder that will
be initially selected when the dialog is opened or, if the
dialog is already open, the folder that is currently
displayed. A related method is
GetCurrentSelection which specifically indicates the user’s current selection
within the displayed folder. So if you need to determine the
exact file that the user has selected while the dialog is
open then
GetCurrentSelection will give you the shell item describing it.
Another
place (no pun intended) where shell items come in handy is
to add links to the “Favorite Links” section. Consider the
following example:
HRESULT result = m_dialog->AddPlace(shellItem,
FDAP_TOP);
Notice the “Window Clippings” link at the top
of the “Favorite Links” list. This link was added because
the shell item I provided to the
AddPlace method referred to a folder on my
computer where I store my
window clippings.
Adding Controls
The developers that created the new dialogs
realized that no matter how cool they make the new file
dialog, software vendors are inevitably going to want to
customize it. Microsoft has embraced this notion and added a
very rich interface for extending the standard dialog with
application-specific controls.
I’ve already
described how you can get a reference to the
IFileDialogCustomize interface by calling
the QueryInterface method. The
documentation for IFileDialogCustomize is
straightforward so you shouldn’t have a problem making use
of it. There are methods for adding buttons, combo boxes,
check boxes, edit boxes and more. The interface is also used
to update and query the controls while the dialog is open.
The following example illustrates how you can add a text box
to allow the user to specify the author of the document to
be saved:
COM_VERIFY(customize->StartVisualGroup(100,
L"Author:"));
COM_VERIFY(customize->AddEditBox(101,
L""));
COM_VERIFY(customize->EndVisualGroup());
WCHAR
buffer[256] = { 0 };
ULONG size =
_countof(buffer);
if (::GetUserName(buffer,
&size))
{
COM_VERIFY(customize->SetEditBoxText(101,
buffer));
}
As you can see, although the
IFileDialogCustomize interface allows you
to add controls and even visually group them, it does not
allow you full control over placement. This allows the
dialog to provide the most appropriate layout even as it
evolves in future releases.
Events
Two
interfaces are defined for applications to implement in
order to receive notifications from a dialog. The
IFileDialogEvents interface can be
implemented if you wish to be notified about navigation
events and as well as folder and selection changes. The
IFileDialogControlEvents interface should
be implemented if you’ve added controls of your own to the
dialog and wish to respond in some way when the user
interacts with those controls.
To receive events
you need to do a few things. First you need to implement the
IFileDialogEvents interface and optionally
the IFileDialogControlEvents interface. You
then need to call the
IFileDialog::Advise method with a pointer
to your IFileDialogEvents implementation.
The dialog will query the
IFileDialogEvents interface pointer for
IFileDialogControlEvents as needed.
CComPtr<IFileDialogEvents> events = // your
implementation
DWORD cookie = 0;
HRESULT
result = dialog->Advise(events,
&cookie);
The file dialog will store a reference to your
implementation until the dialog is destroyed or sooner if
you call the dialog’s
Unadvise method.
HRESULT result = dialog->Unadvise(cookie);
Open Dialogs
So far
we have discussed the file dialog in general. In this
section we are taking a look at the open dialog specifically
and in the next section we’ll take a look at what the save
dialog offers.
Both the open and save dialogs
inherit the GetResult method from
IFileDialog. This method is useful in
determining the choice that the user made. It can be called
once the dialog has closed with a success code. It can also
be called in your implementation of the
IFileDialogEvents::OnFileOk method before
the dialog is closed. You should however only call the
GetResult method if the dialog is
configured to accept a single selection. This is controlled
with the
FOS_ALLOWMULTISELECT
option.
CComPtr<IShellItem> shellItem;
HRESULT result
= dialog->GetResult(&shellItem);
If multiple selections are allowed you need to
use the GetResults method provided by the
IFileOpenDialog interface. It returns a
pointer to the IShellItemArray interface
that is new to Windows Vista. As its name implies, it
provides access to an array of shell items. The following
example illustrates how you can enumerate the results:
CComPtr<IShellItemArray> shellItemArray;
HRESULT
result = dialog->GetResults(&shellItemArray);
if
(SUCCEEDED(result))
{
DWORD count = 0;
result = shellItemArray->GetCount(&count);
if (SUCCEEDED(result))
{
for (DWORD
index = 0; index < count; ++index)
{
CComPtr<IShellItem> shellItem;
if (SUCCEEDED(shellItemArray->GetItemAt(index,
&shellItem)))
{
AutoTaskMemory<WCHAR> name;
if
(SUCCEEDED(shellItem->GetDisplayName(SIGDN_NORMALDISPLAY,
&name.p)))
{
TRACE(L"%s\n", name.p);
}
}
}
}
}
If you need to determine the current selection
while the dialog is open then you can use the
GetSelectedItems method, which also returns
a shell item array.
Save Dialogs
The save dialog contains a few unique features
of its own. The SetSaveAsItem method can be
used to select an initial entry:
CComPtr<IShellItem> shellItem = // load shell
item
HRESULT result =
dialog->SetSaveAsItem(shellItem);
Methods are also provided to interface with a
property store, however that is beyond the scope of this
article. This may be covered in a future article in the
series so stay tuned.
The FileDialog C++ Class Template
As nice as it is to have the file dialogs
exposed through COM interfaces, providing a much richer
programming model and more flexible extensibility, in many
cases a simple solution is desired that does not involve
writing a lot of code and implementing COM interfaces. To
that end, I wrote the FileDialog class
template. It implements both COM event interfaces used by
the file dialogs and provides a very simple mechanism for
adding the new file dialogs to your applications:
template <typename Interface, REFCLSID ClassId>
class
FileDialog :
protected CWindow,
private
IFileDialogEvents,
private
IFileDialogControlEvents
{
public:
__checkReturn HRESULT Load();
HRESULT
DoModal(HWND parent = ::GetActiveWindow());
_RestrictedFileDialogPointer<Interface>*
operator->() const;
protected:
//
...
The following type definitions are also
provided:
typedef FileDialog<IFileSaveDialog,
__uuidof(FileSaveDialog)> FileSaveDialog;
typedef
FileDialog<IFileOpenDialog,
__uuidof(FileOpenDialog)> FileOpenDialog;
With only three public members it is very
approachable but opens up tremendous power as needed. The
Load method creates the underlying dialog
object and notifies the dialog that it would like to receive
event notifications. The overloaded operator allows direct
access to the underlying dialog object through the
appropriate interface. The traditional
DoModal method makes the blocking call to
the dialog’s Show method to display it.
Here is an example:
Kerr::FileOpenDialog dialog;
HRESULT result =
dialog.Load();
if (SUCCEEDED(result))
{
COMDLG_FILTERSPEC fileTypes[] =
{
{
L"Text Documents", L"*.txt" },
{ L"All
Files", L"*.*" }
};
result =
dialog->SetFileTypes(_countof(fileTypes),
fileTypes);
if (SUCCEEDED(result))
{
result = dialog.DoModal();
if (SUCCEEDED(result))
{
CComPtr<IShellItem> selection;
result = dialog->GetResult(&selection);
// use selection somehow
}
}
}
The FileDialog class template
also provides the following protected virtual methods that
derived classes can override to handle specific dialog
events:
virtual bool OnFileOk(); // Return true to accept
the result and close the dialog
virtual bool
OnFolderChanging(IShellItem* shellItem); // Return true to
allow the folder change
virtual void
OnFolderChange(IShellItem* shellItem);
virtual
void OnSelectionChange();
virtual
FDE_SHAREVIOLATION_RESPONSE OnShareViolation(IShellItem*
shellItem);
virtual void OnTypeChange(UINT
index);
virtual FDE_OVERWRITE_RESPONSE
OnOverwrite(IShellItem* shellItem);
FileDialog internally
implements the IFileDialogEvents interface
methods and forwards events to the methods listed above,
simplifying the use of events considerably. Consider the
following example:
class SampleDialog : public Kerr::FileOpenDialog
{
public:
SampleDialog()
{
COM_VERIFY(Load());
}
virtual void
OnSelectionChange() override
{
CComPtr<IShellItem> selection;
HRESULT
result =
m_dialog->GetCurrentSelection(&selection);
if (SUCCEEDED(result))
{
AutoTaskMemory<WCHAR> name;
if
(SUCCEEDED(selection->GetDisplayName(SIGDN_NORMALDISPLAY,
&name.p)))
{
TRACE(L"%s\n", name.p);
}
}
}
};
SampleDialog dialog;
dialog.DoModal();
The previous sample dialog will trace the name of the
shell item every time the user changes the selection.
FileDialog also provides the following protected virtual methods for
handling control-specific events:
virtual void OnItemSelected(DWORD controlId,
DWORD itemId);
virtual void
OnButtonClicked(DWORD controlId);
virtual void
OnCheckButtonToggled(DWORD controlId,
bool checked);
virtual void
OnControlActivating(DWORD controlId);
FileDialog internally
implements the
IFileDialogControlEvents interface methods
and forwards events to the methods listed above, again
simplifying the use of these events considerably. Consider
the following example:
class SampleDialog : public Kerr::FileOpenDialog
{
public:
SampleDialog()
{
COM_VERIFY(Load());
COM_VERIFY(m_customize->AddPushButton(100, L"I'm
Feeling Lucky"));
}
virtual void
OnButtonClicked(DWORD controlId) override
{
ASSERT(100 == controlId);
// Handle
event...
}
};
FileDialog also automatically
retrieves the dialog’s window handle through the
IOleWindow implementation and makes that
available through the inherited
CWindow base class from ATL. Derived
classes can use this to create a popup window or interact
with the dialog window in some way that isn’t directly
provided by the dialog’s COM interfaces.
Conclusion
There is even more that you can do with the new
file dialogs in Windows Vista but hopefully this
introduction will give you enough to get you going.
This article doesn’t include a sample
application but you can
download
the FileDialog class discussed in the
previous section for use in your own applications. You can
then explore the FileDialog class as well
as Windows Vista’s new file dialogs in more detail in your
own time.
Stay tuned for part 7.
© 2006 Kenny Kerr