Archives

Archives / 2006 / November
  • 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.