The progress dialog box object

Fabio Lucarelli

Introduction

During long file operations, the Windows shell displays a dialog box (see Figure 1) that shows the user how much time remains until the copy/delete/move/etc. operation is complete.

Figure 1: the progress dialog box

By default, the dialog box consists of a TAnimate control, three TLabel controls, a TProgressBar control and a “Cancel” button. It automatically computes and displays an estimation of time remaining.

Using the IProgressBar interface exposed by the progress dialog COM object, it is just a matter of less then 10 lines of code to prepare, display, control and close the dialog box.

Unlike what the platform SDK mentions, the object can be used in all Windows version. However, there is a known issue with all versions prior to Windows XP but I will show how to work around it.

Object initialization

Before displaying the dialog box, it is necessary to instantiate the progress dialog box object and to initialize a few parameters (see Listing 1). Note that the interface and the constants are not included in the shlobj unit of Delphi (or anywhere else), I have, therefore, included the declarations with the demo application.

procedure TForm1.Button1Click(Sender: TObject);
var
  pd: IProgressDialog;
begin
  pd := CreateComObject(CLSID_ProgressDialog) as IProgressDialog;
  pd.SetTitle('Progress dialog');
  pd.SetAnimation(HInstance, 1001);
  pd.SetCancelMsg('Closing...', nil);
...
end;
Listing 1: object initialization

After creating the object and requesting a pointer to its IProgressDialog interface, you call these three optional members that return S_OK if successful:

// Possible values: 150, 151, 152, 160, 161, 162, 163, 164
// Windows 2000 & XP also these ones 165, 166, 167, 168, 169, 170
pd.SetAnimation(GetModuleHandle('shell32.dll'), 160);
Listing 2: Loading an AVI from the shell library

Showing the progress

Now that the initialization part is done, let’s display the dialog and show the progress of the operation (see Listing 3).

procedure TForm1.Button1Click(Sender: TObject);
var
  i: Integer;
  pd: IProgressDialog;
begin
...
  pd.StartProgressDialog(Handle, nil, 
             PROGDLG_MODAL or PROGDLG_NOMINIMIZE,
             nil);
  for i := 1 to 100 do
  begin
    if pd.HasUserCancelled then
      break;
    pd.SetProgress(i, 100);
    // Changing line 1 every 20 steps
    pd.SetLine(1, PWideChar(WideString('Phase ' 
           + IntToStr(i div 20+1))), False, nil);
    // Changing line 2
    pd.SetLine(2, PWideChar(WideString('Step '
           + IntToStr(i))), False, nil);
    // Let's pretend this loop takes time
    Sleep(100);
  end;
Listing 3: Showing the progress

Listing 3 is using four different members of the interface:

One last thing, you can reset the progress to zero by using the Timer member with the PDTIMER_RESET flag; the second parameter should be “nil” (see Listing 4).

...
pd.Timer(PDTIMER_RESET, nil);
...
Listing 4: Reset the progress to zero

Closing the dialog box

Use StopProgressDialog procedure to hide the dialog box.

Known issues and workarounds

The progress dialog box object has two known(?) issues on older systems (windows 95/98/NT4/2000): the minimize button is displayed even if you use the appropriate flag to hide it, and the CompactPath parameter of SetLine() is ignored no matter its value.

Another issue (that has nothing to do with the object): if you provide the handle of your application’s main form with StartProgressDialog(), your application looses the focus when the progress dialog box (in modal “mode”) is closed. This can lead to unexpected results like your application “disappearing” under other applications. Of course, if you provide the handle of a child form, there is no problem.

The minimize button issue

The workaround is hopefully pretty easy because the progress dialog box object exposes the IOleWindow interface that has a GetWindow member returning the handle of the dialog box. IOleWindow is declared in the ActiveX unit.

Using the handle, you change the style of the dialog box in order to hide that unwanted minimize button (see Listing 5).

uses ActiveX;
...
const
  IID_IOleWindow: TGUID = (
    D1:$00000114;D2:$0000;D3:$0000;D4:($C0,$00,$00,$00,$00,$00,$00,$46));
...
procedure TForm1.Button1Click(Sender: TObject);
var
  pd: IProgressDialog;
  ow: IOleWindow;
  pdHandle: HWND;
begin
...
  pd := CreateComObject(CLSID_ProgressDialog) as IProgressDialog;
...
  pd.StartProgressDialog(Handle, nil, PROGDLG_MODAL or PROGDLG_NOMINIMIZE, nil);
...
  if pd.QueryInterface(IID_IOleWindow, ow) = S_OK then
  begin
    ow.GetWindow(pdHandle);
    SetWindowLong(pdHandle, GWL_STYLE, GetWindowLong(pdHandle, GWL_STYLE) and not WS_MINIMIZE);
  end;
...
end;
Listing 5: Workaround for the minimize button issue

Note that you can query the IOleWindow interface only after StartProgressDialog().

The compact path issue

Since Windows might ignore your parameter, the best thing to do is to call directly the PathCompactPath() function on older systems and use the result directly with SetLine() (see Listing 6). Note however that version 4.71 or higher of the shlwapi library is required (ie at least Internet Explorer 4).

function PathCompactPath(hDC: hDC; lpszPath: PChar; dx: UInt): Bool; stdcall; external 'shlwapi.dll' name 'PathCompactPathA';
procedure TForm1.Button1Click(Sender: TObject);
const
  cPath = 'C:\Program Files\Microsoft Visual Studio\Common\IDE\IDE98\Resources\1036\HTMEDUI.DLL';
var
  pdHandle: HWND;
  lHandle: HWND;
  dc: HDC;
  R: TRect;
  Path: array[0..MAX_PATH] of Char;
begin
...
  Path := cPath;
  lHandle := FindWindowEx(pdHandle, 0, 'static', nil);
  Windows.GetClientRect(lHandle, R);
  dc := GetDC(lHandle);
  PathCompactPath(dc, Path, R.Right);
  ReleaseDC(lHandle, dc);
  pd.SetLine(2, PWideChar(WideString(Path)), False, nil);
...
end;
Listing 6: PathCompactPath api call

The focus issue

I couldn’t reproduce the behavior under Windows 98, but under XP, your application looses the focus and may disappear under other windows when the dialog box (in modal “mode”) is closed.

The solution is to provide the handle of the application instead of the handle of the main form (see Listing 7). Doing so, however, confuses the dialog box who tries to position itself in the upper left corner of the window provided with StartProgressDialog(); hence the call to SetWindowPos() to restore the position in Listing 7.

uses ActiveX;
...
const
  IID_IOleWindow: TGUID = (
    D1:$00000114;D2:$0000;D3:$0000;D4:($C0,$00,$00,$00,$00,$00,$00,$46));
...
var
  pd: IProgressDialog;
  ow: IOleWindow;
  pdHandle: HWND;
begin
  pd := CreateComObject(CLSID_ProgressDialog) as IProgressDialog;
  ...
  pd.StartProgressDialog(Application.Handle, nil, PROGDLG_MODAL, nil);
  ...
  if pd.QueryInterface(IID_IOleWindow, ow) = S_OK then
  begin
    // Handle of the dialog
    ow.GetWindow(pdHandle);
    // Position in the upper left corner of the main form
    SetWindowPos(pdHandle, 0, Left + 20, Top + 20, 0, 0, SWP_NOSIZE or SWP_NOZORDER);
  end;
Listing 7: Workaround for the focus issue

Need customization?

Using the IOleWindow interface, you can retrieve the handle of the progress dialog box. Using API calls, it’s therefore pretty easy to retrieve the handle of all its children and modify their style.

Modifying existing controls

Let’s imagine you want to modify the style (to smooth) and color (to purple) of the progressbar control and the caption (to “Stop”) of the button control. Using the handle of the dialog box and FindWindowEx(), you retrieve the handle of the controls to modify and then use specific messages (see Listing 8 and Figure 2).

uses ActiveX, CommCtrl;
...
const
  IID_IOleWindow: TGUID = (
    D1:$00000114;D2:$0000;D3:$0000;D4:($C0,$00,$00,$00,$00,$00,$00,$46));
...
var
  pd: IProgressDialog;
  ow: IOleWindow;
  pdHandle: HWND;
  cHandle: HWND;
begin
  pd := CreateComObject(CLSID_ProgressDialog) as IProgressDialog;
  ...
  pd.StartProgressDialog(0, nil, 0, nil);
  ...
  if pd.QueryInterface(IID_IOleWindow, ow) = S_OK then
  begin
    // Handle of the dialog
    ow.GetWindow(pdHandle);
    // Handle of the Button
    cHandle := FindWindowEx(pdHandle, 0, 'button', nil);
    // Changing caption
    SendMessage(cHandle, WM_SETTEXT, 0, Longint(PChar('&Stop')));
    // Handle of the ProgressBar
    cHandle := FindWindowEx(pdHandle, 0, PROGRESS_CLASS, nil);
    // Changing color
    SendMessage(cHandle, PBM_SETBARCOLOR, 0, ColorToRGB(clPurple));
    // Changing style to smooth
    SetWindowLong(cHandle, GWL_STYLE, GetWindowLong(cHandle, GWL_STYLE) or PBS_SMOOTH);
  end;
Listing 8: Customizing the dialog box

Figure 2: a customized progress dialog box

Adding new controls

I’ve showed it several times in my previous articles, thanks to the “ParentWindow” property, it’s pretty easy to create and “drop” VCL controls on an “API” window. The trick is to sacrifice a “host” panel and use it as a parent for all VCL controls you need to create.

Let’s imagine you want to add a checkbox at the bottom of the dialog: you increase the height of the dialog box, create a panel, change a few properties to position it and make “invisible” on the dialog box (by removing the bevel) and create a checkbox with the panel as a parent (see Listing 9 and Figure 3).

As I’ve showed it, you can also force the dialog not to display the TAnimate control and reuse that area to drop your controls.

var
  pd: IProgressDialog;
  ow: IOleWindow;
  pdHandle: HWND;
  panel: TPanel;
  cb: TCheckBox;
  R: TRect;
begin
  ...
  if pd.QueryInterface(IID_IOleWindow, ow) = S_OK then
  begin
    // Dialog handle
    ow.GetWindow(pdHandle);
    // Size of dialog
    GetWindowRect(pdHandle, R);
    // Changing position and size of dialog
    SetWindowPos(pdHandle, 0, Left + 20, Top + 20, R.Right-R.Left, R.Bottom-R.Top+20, SWP_NOZORDER);
    // Creating “host” panel that will cover completely the dialog
    panel := TPanel.Create(Self);
    panel.ParentWindow := pdHandle;
    panel.bevelOuter := bvNone;
    panel.Height := R.Bottom-R.Top+20;
    panel.Width := R.Right-R.Left;
    // Creating checkbox
    cb := TCheckBox.Create(Self);
    cb.Parent := panel;
    cb.Left := 5;
    cb.Top := R.Bottom-R.Top-25;
    // Changing font to dialog’s font
    SendMessage(cb.handle, WM_SETFONT, SendMessage(pdhandle, WM_GETFONT, 0, 0), 0);
    cb.Caption := 'Do not show this dialog';
  end;
...
  cb.Free;
  panel.Free;
  pd.StopProgressDialog;
end;
Listing 9: More customization...

Figure 3: More customisation…

Conclusion

Yes, it wouldn’t be that hard to build the same dialog box from scratch using a form and a few VCL controls. But why reinvent the wheel ? Except for the issues that are anyway easily fixed (and only a problem for older systems), the progress dialog box is another nifty tool offered by the Windows Shell.

About ten lines of code are necessary and I appreciate the automatic computation of the time remaining. As I’ve showed you, using a few API calls, it’s easy to customize even more the dialog box.

Wrapping this object in an invisible component wouldn’t be that hard, just reuse the code provided with this article.