TDXML: XML-Based Task Dialogs with a Visual Task Dialog Editor

TDXML: XML-Based Task Dialogs with a Visual Task Dialog Editor

 

Contents

Introduction

If you've read my first two articles on Vista task dialogs (Part 1Part 2), you know that task dialogs are a great new UI feature, but they can be cumbersome to use. At PDC 2005, session PRS319 ("Building Applications That Look Great in Windows Vista"), the speaker demonstrated a tool called TDPad that was developed by the shell team. TDPad is a simple task dialog editor that uses XML data files internally and does some of the grunt work, but it still takes a lot of manual labor to incorporate the output from TDPad into your app.

This article presents the TDXML (XML-based task dialogs) library, which is designed to be much easier to use compared to calling the TaskDialog() and TaskDialogIndirect() APIs directly. It also includes a visual task dialog editor, which you can use to generate the XML that is then used as input to TDXML. You can of course create the XML yourself, but I think you'll find that the instant feedback that the visual editor provides to be far more developer-friendly.

The TDXML library's design goals are:

  • Completely data-driven: You don't have to change any C++ code to change the appearance of a task dialog.
  • Uses a widely-available XML parser: TDXML uses the MSXML parser, which is present on all modern systems.
  • Localizable: The XML can be stored as a file along side your application, or put in a resource. Either way, it allows for easy localization. The data-driven nature of the library means you can localize the XML without touching or recompiling any C++ code.
  • Easy on the caller: To use a task dialog, you don't have to do anything more complex than initialize a C++ object and call DoModal(), similar to how you use CDialog in MFC and CDialogImpl in ATL.

This article is written for the RTM version of Vista, using Visual Studio 2005, WTL 7.5, and the Windows SDK. See the introduction in the first Vista Goodies article for more information on where you can download those components. The TDXML library uses the CString and _bstr_t string wrapper classes; I have only tested the code on VC 2005 in a WTL application, but since those string classes are available to MFC as well, the TDXML library should be easy to incorporate into an MFC app.

Using the Visual Editor

Overview

The task dialog editor, included in the demo download in the TDEditor project, is a tool you can use to set up a task dialog, see exactly how it will look in your program, and then generate an XML file that you can use with the TDXML library. When you run TDEditor, you'll see four pages where you can set up various UI elements of the task dialog. Here is how it looks initially (screen shots of the editor are shown as thumbnails in order to stay within CodeProject's limit on article width):

Image 1

TDEditor also shows a live preview of what the task dialog will look like, using the current text and settings that you have entered in the editor. This is a real task dialog, not an emulation, so you can be confident that what you see is what you will get. Here's the initial state of the task dialog upon running TDEditor:

Image 2

In most cases, TDEditor automatically updates the task dialog to reflect any changes you make in the editor, but there are a few settings where this isn't possible. When you change such a setting, an information icon is added to the Rebuild Dialog button to let you know that you need to rebuild the dialog to see your changes. If you make changes in the task dialog itself, such as clicking the expand/collapse button or selecting a radio button, you can discard those changes by clicking Rebuild Dialog. Click the thumbnail below to see a sample animation showing the live editing in action:

Image 3

Once you have the task dialog set up to your satisfaction, you can generate an XML file that contains all the information that you entered into the editor. First, click Save XML to bring up a file save dialog. Select the filename to use, and TDEditor will generate an XML file for you. You can then use that XML as input to TDXML, as described later in this article.

Adding Text and Icons

The Text and Icons page is where you set up the static text elements of the task dialog. The elements that have check boxes beside them are optional; if an element is unchecked, it will not be present in the task dialog. The exception is the Caption element -- if you uncheck Caption, Windows will provide a default caption. The default caption text in Vista is the name of the executable that is showing the dialog.

To demonstrate the editor in action, I'll show how to build a dialog similar to the one in my earlier article on using TaskDialogIndirect. After setting up the basic text elements, here is how the editor looks:

Image 4

Here is how the task dialog looks so far:

Image 5

Notice that the footer text contains an <a> tag, but it is not shown as a hyperlink. Don't worry about that for now, we'll fix that up later.

Adding Push Buttons

The Buttons page lets you specify which buttons you want to appear in the task dialog. You can add any of the six built-in buttons, or you can add any number of additional buttons with custom text. You can also choose whether the custom buttons will appear as regular push buttons or command links.

For our sample dialog, we'll add a Close button and two custom buttons. Use the buttons to the right of the Custom buttons list to add, remove, or edit the dialog's custom buttons. When you add or edit a custom button, TDEditor displays a dialog where you enter the button's details:

Image 6

If you are using command links, as we are in this sample, you can enter additional text in the Line 2 text edit box. This text will be shown on the second line of the button in a smaller font.

You can change the ID to any value, but be aware that small numbers may conflict with the IDs of built-in buttons. The default ID is 1000, but any number greater than 100 will work fine. TDEditor will automatically increment the ID for each additional button, and you can also click the Reset IDs button in the editor to reassign sequential IDs to all custom buttons.

Here is the Buttons page showing the buttons that we want in the task dialog:

Image 7

And here is the task dialog with the new buttons:

Image 8

Adding Radio Buttons

The Radio Buttons page works similarly to the Buttons page. Since there are no built-in radio buttons, this page only has a Custom buttons list and the associated controls. The Default button setting also works a bit differently -- it controls which radio button will be checked when the dialog is first shown. If you don't want any of the radio buttons to start out checked, uncheck the Default button setting.

This task dialog for our fictional AntiFluff program wouldn't normally have radio buttons, but to illustrate the feature, I'll add two buttons as shown here in the editor:

Image 9

And here's the task dialog. Notice how the second radio button is checked, in accordance with the Default button setting in the editor.

Image 10

Setting Dialog Flags

The Flags page is pretty simple, and lets you toggle some task dialog options and set a couple of additional properties. The marquee progress bar style also has a timer setting that defaults to 50 ms. This is the amount of time between progress bar updates; a smaller value creates a faster-moving marquee.

Here are the flags that we'll set on our sample task dialog:

Image 11

And here's the final version of the task dialog:

Image 12

Notice that the footer now contains a hyperlink, since we set the Allow hyperlinks flag.

Showing a Task Dialog with TDXML

Your interface to the TDXML library is the CTaskDialogXML class, which is in the TaskDialogXML.cpp and TaskDialogXML.h files. Using CTaskDialogXML involves just two steps:

  1. Initialize the dialog by telling it where to find the XML that describes the contents of the dialog.
  2. If initialization succeeds, call DoModal() to show the dialog.

There are three methods for initializing the dialog, each one reads the XML data from a different source:

bool InitFromFile ( LPCTSTR szFilename )
Call InitFromFile() to initialize the dialog using an XML file. szFilename is the full path to the file.
bool InitFromResource ( resourceID, resourceType, hModule )
Call InitFromResource() to initialize the dialog using data stored in a resource. resourceID can be either a numeric or a string ID. resourceType defaults to "TDXML"; you can put your XML data files in this custom resource type to keep them grouped together in the resources of your binaries. hModule is the HMODULE that contains the resource. If you don't specify hModule, the executable for the current process is used.
bool InitFromVariant ( const _variant_t variantData )
InitFromVariant() is the function that the other two initialization methods call, but you can call it directly if you have a VARIANT or _variant_t that holds a data source. Any data that can be passed to IXMLDOMDocument::load() is acceptable.

CTaskDialogXML uses a callback function to implement some features, like buttons with UAC shields, that are not built into the task dialog API. This means that if you need to use your own callback function, you must set two members of your CTaskDialogXML object that do the equivalent of the pfCallback and lpCallbackData members of TASKDIALOGCONFIG. The members are m_pfCallback and m_lpCallbackData; when you set them, CTaskDialogXML will call your callback just like TaskDialogIndirect() would.

After initializing the CTaskDialogXML object, call DoModal() to show the dialog:

HRESULT DoModal ( HWND hwndParent )
DoModal() returns E_FAIL if the dialog was not initialized or initialization failed, or the return value from TaskDialogIndirect() otherwise.

If DoModal() returns a successful HRESULT, you can read three public members of the CTaskDialogXML object to see what actions the user took in the dialog:

m_nButton
The ID of the push button that the user clicked to close the dialog.
m_nRadioButton
The ID of the radio button that was checked when the dialog was closed.
m_bChecked
BOOL indicating the state of the check box when the dialog was closed.

The latter two of those members will not contain meaningful data if the corresponding UI element was not included in the dialog. See the documentation on TaskDialogIndirect() for more details about how those values are set.

The TDXMLTester project in the downloadable code contains three task dialogs, and demonstrates various ways to use CTaskDialogXML. The first dialog uses an external file called testdialog.xml, which must be in the same directory as TDXMLTester.exe. A sample file is included in the source download, or you can create and use your own file if you like.

Here is the code from TDXMLTester that uses an external XML file:

 
void CMainDlg::OnShowDialog1 (
  UINT uCode, int nID, HWND hwndCtrl )
{
HRESULT hr;
tdxml::CTaskDialogXML dlg;
TCHAR szTestFilename[MAX_PATH];
 
  GetModuleFileName ( NULL, szTestFilename,
                      _countof(szTestFilename) );
  PathRemoveFileSpec ( szTestFilename );
  PathAppend ( szTestFilename, _T("testdialog.xml") );
 
  if ( dlg.InitFromFile ( szTestFilename ) )
    hr = dlg.DoModal ( m_hWnd );
}

As you can see, there isn't much to it. The code builds the full path to the XML file, then calls InitFromFile() and passes that path. If InitFromFile() succeeds, the code then calls DoModal().

The other two dialogs use XML contained in resources in TDXMLTester.exe. If you look at the TDXMLTester resources, you'll see two nodes under the "TDXML" type:

Image 13

Test dialog 2 uses the IDR_CHNXML resource, like this:

 
void CMainDlg::OnShowDialog2 (
  UINT uCode, int nID, HWND hwndCtrl )
{
HRESULT hr;
tdxml::CTaskDialogXML dlg;
 
  if ( dlg.InitFromResource ( IDR_CHNXML ) )
    hr = dlg.DoModal ( m_hWnd );
}

The last dialog is very similar, it just uses a string resource ID instead of a numeric ID. Dialog 3 also shows how to use a custom callback function to handle the notification sent when the user clicks a hyperlink.

 
void CMainDlg::OnShowDialog3 (
  UINT uCode, int nID, HWND hwndCtrl )
{
HRESULT hr;
tdxml::CTaskDialogXML dlg;
 
  if ( dlg.InitFromResource ( _T("BIGTESTDIALOG") ))
    {
    // This dialog has a hyperlink, so we need to set
    // up a callback function that launches the URL
    // associated with the link.
    dlg.m_pfCallback = TDCallback;
    dlg.m_lpCallbackData = (LONG_PTR) this;
 
    hr = dlg.DoModal ( m_hWnd );
    }
}

Other CTaskDialogXML Details

CTaskDialogXML contains a public member m_td, which is the TASKDIALOGCONFIG struct that is passed to TaskDialogIndirect(). You can modify this struct before calling DoModal() if you want to do any additional initialization or setup steps.

CTaskDialogXML::DoModal() has two ways of calling TaskDialogIndirect(). The default behavior is to call the API directly, which requires that your application be statically linked to the Vista version of comctl32.dll. If you don't want to have that dependency (for example, you want your program to run on XP as well), you can define the TDXML_DONT_LINK_TO_API symbol in your stdafx.h. This makes DoModal() use GetProcAddress() to get the address of TaskDialogIndirect(). Note that this method still requires your app to have comctl32.dll loaded into its process, but if the OS does not support task dialogs, DoModal() will simply return E_FAIL.

Bugs and Feature To-Do List

Here are the bugs (that I know about!) and some features that I have in mind for the future. If you have any other feature requests or bug reports, don't hesitate to post on this article's forum.

In the task dialog editor:

  • You can't set the base ID for buttons when using the Reset IDs command.
  • The editor doesn't support loading a previously-generated XML file.
  • The standard dialog navigation keys don't work (Tab, Alt+letter, etc.).

In the TDXML library:

  • The XML used to build a task dialog doesn't support loading string or icon resources. You can work around this limitation by modifying the m_td member as necessary before calling DoModal().
  • Fix any problems related to using the class with MFC.
  • The default module for loading resources should be different based on whether the app is using MFC or ATL. Currently, it always uses NULL, which is the API-level default -- see the FindResource() docs for more info.

Further Reading

Vista Goodies in C++: Showing Friendly Messages with Task Dialogs

Vista Goodies in C++: Using TaskDialogIndirect to Build Dialogs that Get User Input

Windows Vista for Developers - Part 2 - Task Dialogs in Depth. Kenny Kerr has a really long post talking about more features of task dialogs, along with lots of sample code and an ATL wrapper class.

MSDN has some lengthy pages on the new Vista UI guidelines. For task dialogs, the page to be familiar with is Text and Tone.

Copyright and License

This article is copyrighted material, ©2007 by Michael Dunn, all rights reserved. I realize this isn't going to stop people from copying it all around the 'net, but I have to say it anyway. If you are interested in doing a translation of this article, please email me to let me know. I don't foresee denying anyone permission to do a translation, I would just like to be aware of the translation so I can post a link to it here.

The demo code that accompanies this article is released to the public domain. I release it this way so that the code can benefit everyone. (I don't make the article itself public domain because having the article available only on CodeProject helps both my own visibility and the CodeProject site.) If you use the demo code in your own application, an email letting me know would be appreciated (just to satisfy my curiosity about whether folks are benefitting from my code) but is not required. Attribution in your own source code is also appreciated but not required.

Revision History

March 19, 2007: Article first published.

posted @ 2022-12-13 14:49  小风风的博客  阅读(27)  评论(0编辑  收藏  举报