viksoe.dk

Task Dialog for Windows 98

Task Dialog for Windows 98


This article was submitted .


Finally it found its replacement. No more MessageBox.
At last in Windows Vista we now have the Task Dialog to replace the MessageBox API so we can have all the nice little gimmicks that we have always yearned for. Like a checkbox that says "Only display this message once", the ability to have timed messages, or just to have sensible button captions so we no longer have to see popup messages like this:

Image from www.thedailywtf.com


Chances are that if you are maintaining a large project in any programming language on Windows, you have already rolled out your own Message Box dialog replacement. But now a full featured replacement has finally become part of the Microsoft Style Guide for Windows Vista.

One of the great new features in the WTL 8 library is the ability to dynamically construct dialogs. You may not immediately recognize what you would use such a feature for but try to imagine that you just downloaded the Visual Studio Express version and panicked when you discovered that it did not come with a Resource Editor. Of course you can download a free Resource Editor off the Internet, but when you panic you often learn something new and the new CIndirectDialogImpl WTL class will help you there. Or maybe you are a former C# developer and really liked the way WinForms were constructed line by line. So now you want to construct your C++ dialogs the same way. Now seriously, one use for dynamically constructed dialogs would be a dialog that changes its appearance slightly each time it is launched. Slightly in such a way that you would conceive it as one dialog, rather than trying to tie multiple dialog resources to almost identical behaviour. One such dialog is the Task Dialog in Windows Vista.

The problem with the Windows Vista Task Dialog is just that. It's a Windows Vista-only component. The control presented here is a remake of the Task Dialog. It captures nearly all the functionality and works on platforms from Windows 98 and above. It doesn't try to preserve the unique Vista theme visuals though, but instead render its controls with the native look of the platform you're running on. Nor does it use scores of custom painted controls to emulate all the features of the Vista version. For some parts (such as the ability to embed HTML links in the footer text) it just leaves them out when there is no support on the platform it runs on.
Ultimately you may wish to combine this control with the AtlTaskDialog function in WTL. You can call this to detect if the Task Dialog is available on the running platform. If it's not, then fall back to the Task98Dialog call.

Dynamic Dialogs

The dialog implementation is available in a separate C++ class called CTask98DialogImpl. This is the class that does dynamic construction of the dialog elements based on the passed Task Dialog configuration. Don't be scared by the plentiful code, it does a number of checks to determine the size of the window and how to best fit the controls. The dialog passes through 2 phases to construct the dialog: First it attempts to decide on the optimal dimensions of the dialog and then it constructs the dialog with its controls. Various types of controls are used to display the buttons, Command Links etc. on the different platforms. Command Links are only supported on Vista so they must be emulated in other versions of Windows.

The dialog is constructed in-memory using the WTL CIndirectDialogImpl class. Using the accompanied macros in WTL to generate a dialog with fixed controls or constructing a dialog where positions and sizes are known in advance, that is very easy. However, if your dialog can have a more dynamic layout, like the Task Dialog, where buttons and labels are shuffled around depending on some kind of configuration, you will need to convert back and forth between the units system used by dialogs: Dialogs use Dialog Base Units measurements rather than pixels. Dialog Units are a strange beast as they are based on the average character pixel width and height of the font attached to the dialog. The idea is quite resourceful because it means that the dialog can scale properly when you change system font. If you changed your display to use a high-DPI resolution your dialogs should display perfectly too since the system font dimensions are likely to change as well.

The native Vista Task Dialog shies away from using a traditional dialog. Instead it uses the now mythical DirectUser library, which is an internally developed - partially windowless - library used by Microsoft. I suspect it has some form of automatic layout algorithm like I experimented with in my windowless test project.

Using the control

To use this control, there are some new functions which mimic the API for the original Task Dialog component.
Task98Dialog
Task98DialogIndirect
You call these with the same arguments as the original Task Dialog, except that the Task98Dialog function takes a string type as defined by your project settings (UNICODE or MBCS). This makes it slightly easier to use in legacy applications.
The Task98DialogIndirect function takes the same configuration structure as the original Task Dialog function. It also implements the callback functionality so you can override the behaviour of the Task Dialog.

The Task98Dialog method is called like this:

int iRes = 0;
Task98Dialog(m_hWnd, _Module.GetResourceInstance(), 
  "Window Title", 
  "Main Instructions", 
  "This is the message text.", 
  TDCBF_OK_BUTTON | TDCBF_CANCEL_BUTTON, 
  MAKEINTRESOURCE(IDR_MAINFRAME), &iRes);
This call doesn't actually give you much new functionality that you didn't already have with the old MessageBox call. To make use of all the Task Dialog's functionality you'll need to call the Task98DialogIndirect method. Here's an example that adds a verification checkbox and a couple of custom buttons.
TASKDIALOGCONFIG cfg = { 0 };
cfg.cbSize = sizeof(cfg);
cfg.hInstance = _Module.GetResourceInstance();
cfg.pszWindowTitle = L"Window Title";
cfg.pszMainIcon = MAKEINTRESOURCEW(IDR_MAINFRAME);
cfg.pszMainInstruction = L"Main Instructions";
cfg.pszContent = L"This is the message text.";
cfg.pszVerificationText = L"Verifcation text.";
cfg.dwCommonButtons = TDCBF_OK_BUTTON;
TASKDIALOG_BUTTON buttons[] = {
  { 100, L"Button #1" },
  { 101, L"Button #2\nText Below button." }
};
cfg.pButtons = buttons;
cfg.cButtons = 2;
cfg.nDefaultButton = 101;
int iRes, iRadio, iVerify;
Task98DialogIndirect(&cfg, &iRes, &iRadio, &iVerify);

If you really want to take control of the dialog, you can attach a callback function and process notifications when something happens on the dialog. For instance, when the dialog is constructed it will send the TDN_CREATED notification, and if you enable a timer you'll see the TDN_TIMER notification.

...
cfg.pfCallback = TaskDialogCallback;
cfg.dwFlags = TDF_SHOW_PROGRESS_BAR | TDF_CALLBACK_TIMER;
...

static HRESULT CALLBACK TaskDialogCallback(HWND hWnd, 
  UINT msg, WPARAM wParam, LPARAM lParam, LONG_PTR lData)
{
  switch( msg ) {
  case TDN_TIMER:
    ::SendMessage(hWnd, TDM_SET_PROGRESS_BAR_POS, wParam / 30, 0L);
    if( wParam / 30 >= 100 ) {
       ::SendMessage(hWnd, TDM_CLICK_BUTTON, IDOK, 0L);
    }
    break;
  }
  return 0;
}
This produces a dialog with a Progress Bar control. The callback will step the Progress Bar whenever it gets called with the timer notification (ignited by the TDF_CALLBACK_TIMER flag). When the Progress Bar reaches 100% it will automatically press the OK button.

Finally, if you are more comfortable with using the WTL TaskDialog wrapper style of code, this is also possible. Here is a similar example, rewritten to use inheritance programming style:

class CMyTask98Dialog : 
  public CTask98DialogImpl<CMyTask98Dialog>
{
public:
  int DoModal(HWND hWnd = NULL)
  {
    m_cfg.hwndParent = hWnd;
    m_cfg.pszWindowTitle = L"Window Title";
    m_cfg.pszMainIcon = MAKEINTRESOURCEW(IDR_MAINFRAME);
    m_cfg.pszMainInstruction = L"This is Progress Bar test";
    m_cfg.pszContent = L"This is the content text.";
    m_cfg.dwCommonButtons = TDCBF_OK_BUTTON | TDCBF_CANCEL_BUTTON;
    TASKDIALOG_BUTTON radios[] = {
      { 200, L"Radio #1" },
      { 201, L"Radio #2" },
      { 202, L"Radio #3" }
    };
    m_cfg.pRadioButtons = radios;
    m_cfg.cRadioButtons = 3;
    m_cfg.dwFlags = TDF_POSITION_RELATIVE_TO_WINDOW 
      | TDF_SHOW_PROGRESS_BAR | TDF_CALLBACK_TIMER;
    return CTask98DialogImpl<CMyTask98Dialog>::DoModal(hWnd);
  }

  void OnCreated()
  {
    EnableButton(IDCANCEL, FALSE);
  }
  BOOL OnTimer(UINT wTime)
  {
    SetProgressBarPos(wTime / 30);
    return FALSE;
  }
};

...

LRESULT OnButtonPress(...)
{
  CMyTask98Dialog dlg;
  dlg.DoModal();
  return 0;
}

Ironically, you will need to define a few string resources and add a couple of icons to your project before you can use this dynamically created dialog. Have a look in the sample project attached below for more examples.
When specifying text for the popups, all the strings can be replaced with string resource ID's, as in...

MAKEINTRESOURCEW(IDS_MSGBOX_TITLE)
This should help the pain of converting everything to UNICODE if your project is not already compiled as that.

Notes

This code requires a version of WTL greater than 8.0 since there is a bug in the atldlgs.h code which occasionally prevents large dialogs from being constructed. Please get the latest version from the SourceForge download area or the latest files from the Subversion repository.

Source Code Dependencies

Microsoft WTL 8.5 Library

Download Files

DownloadSource Code (60 Kb)

To the top