The Universal Progress Dialog

Introduction

In any interactive application Progress-Dialogs (modal dialog boxes that display the progress of lengthy operations) play important role in keeping the interface alive by providing the user real-time feedback about the application status. There is almost no context where these dialogs could not be used - ranging from simple file loading operations to complex OS loading operations - we find them useful. However, for each application that we develop, creating a new progress dialog from scratch is a boring task. Under these circumstances we find it useful to have a simple dialog template that could be used in any application any time for any task. Our Universal Progress Dialog that we are about to unleash here is one such modal dialog box that satisfies all these requirements, that is - it could be used any where any time for any operation.

The Universal Dialog

The design goals that have driven the development of this Universal Progress Dialog are:

Simplicity (Usage should be as simple as possible) In fact, as you would learn soon, you need to add just 2 extra lines to your existing code to start using this dialog in your app!!
Flexibility (Should be able to use it for any task) This dialog is perfectly suitable to display the progress any kind of operation that you know how to measure the progress. This is facilitated by accepting a Pointer to any user defined Function.
Interactivity (End-user should not suffer from loss of interactivity) This dialog allows the end-user to cancel the operation anytime before the operation completes on its own. To facilitate this, the dialog internally manipulates a worker thread and manages the communication between the interface thread and the worker thread in a transparent way freeing the developer from unnecessary thread-management issues.

With these design goals in mind, the class CUPDialog (No, it's not CUP dialog - it is to mean CUniversalProgressDialog) that we are about to study provides simple to use, flexible and interactive interface in an elegant way very much similar to the well known CDialog class from MFC. All that we need to do is create the dialog class object and call the method DoModal() on it as shown below.

CUPDialog Dlg(. . .); //construct the dialog class object
 
INT_PTR nResult = Dlg.DoModal();
 
if(nResult != IDOK) return;

The method CUPDialog::DoModal() displays a modal dialog box that returns IDOK upon successful completion. The dialog template that gets displayed is but a simple dialog box containing one progress bar, one static control and one cancel button as shown in Figure 1. As could be expected easily, the progress bar displays the progress of the underlying lengthy operation while the static control displays any suitable text appropriate for the operation. The cancel button allows the user to cancel the operation at any time before the operation gets completed on its own.

Dialog Template For the Universal Progress Dialog

Figure 1 Dialog Template for the Universal Progress Dialog

When the CUPDialog::DoModal() is invoked, the dialog box automatically creates a background worker thread and schedules a user supplied function for execution in that thread's context. We can specify which function should be executed by supplying a pointer to the function as one of the parameters of the CUPDialog class constructor. The complete prototype of the constructor of CUPDialog is:

CUPDialog(HWND hParentWnd,LP_CUPDIALOG_USERPROC lpUserProc,LPVOID lpUserProcParam,LPCTSTR lpszDlgTitle=_T("Please Wait.."),bool bAllowCancel=true);

The parameters for the constructor are:

HWND hParentWnd
The application window that is creating the dialog box. This value would be used as the parent window handle for the dialog box.
LP_CUPDIALOG_USERPROC lpUserProc
Pointer to a user defined function. The dialog box internally creates a thread and executes this function in that thread's context. The function should be of the form bool UserProc(const CUPDUPDATA* pParam);
LPVOID lpUserProcParam
Argument for the user defined function. This value could be accessed from the UserProc by accessing the method CUPDUPDATA::GetAppData()
LPCTSTR lpszDlgTitle
Parameter specifying the caption for the progress dialog. Default value = _T("Please Wait..");
bool bAllowCancel
Parameter indicating if the user can cancel the operation. When set to false the cancel button would be disabled so that user cannot cancel the operation. Default value is true.

For example, the following code fragment executes a function LengthyOperationProc() while displaying a cancelable modal dialog box with default title "Please Wait..".

bool LengthyOperationProc(const CUPDUPDATA* pCUPDUPData);
 
void CApplicationDlg::OnBnClickedOk()
{
    int nData =0;
    
    CUPDialog Dlg(GetSafeHwnd(),LengthyOperationProc,&nData);
    
    INT_PTR nResult = Dlg.DoModal();    
}

In the above, we are trying to display the progress dialog for some lengthy operation to be executed in the function LengtyOperationProc(). As per the prototype requirements the function LengtyOperationProc() accepts a CUPDUPDATA* as argument and returns a boolean value as result. CUPDUPDATA is meant for CUniversalProgressDialogUserProcedureDATA. This key structure supports the following important methods.

LPVOID GetAppData()
Provides access to the user supplied parameter. This value is same as the third parameter supplied as part of the CUPDialog constructor.
bool ShouldTerminate()
Indicates if the function should terminate. We should execute our lengthy operation only if this return value is false. This would return true when the user has cancelled the dialog - which means we should stop and return. We should check this frequently so as not to stall the app.
void SetProgress(LPCTSTR lpszText)
This facilitates us to set the text for the static control appropriately as per the progress.
void SetProgress(UINT_PTR dwPbarPos)
This facilitates us to set the position for the progress bar control appropriately as per the progress.
void SetProgress(LPCTSTR, UINT_PTR)
This facilitates us to set both the text of static control and position of the progress control at the same time as per the progress appropriately.
void AllowCancel(bool bAllow)
Useful for enabling or disabling the Cancel button on the progress dialog.
void SetDialogCaption(LPCTSTR )
Facilitates modifying the progress dialog caption.

To demonstrate the use of CUPDUPDATA let's consider some pseudo lengthy operation: counting numbers from 1 to 100. As part of this operation we wish to display the progress for each number that we counted. The code fragment that achieves such thing would look like the following:

bool LengthyOperationProc (const CUPDUPDATA* pCUPDUPData)
{
    int* pnCount= (int*)pCUPDUPData->GetAppData(); //Retrieve the App Supplied Data
    
    pCUPDUPData->SetProgress(_T("Counting.."),0);
    
    while(pCUPDUPData->ShouldTerminate()== false && *pnCount != 100)
    {
        pCUPDUPData->SetProgress(*pnCount); //Update Progress Bar
                         
        *pnCount = *pnCount + 1;
    
        Sleep(100);
    }    
    pCUPDUPData->SetProgress(_T("Done !!"),100);
    
    return true;
}

In the above, we are first retrieving a pointer to the user supplied number by using the method GetAppData(). Then we start by setting the progress bar to 0 and for each number that is counted we are repositioning the progress bar to reflect the latest status. Please observe the usage of CUPDUPData::ShouldTerminate() in the main while loop. The method ShouldTerminate() returns true only when the user presses the cancel button or the close system button. By placing the ShouldTerminate() check in the loop, we are making sure that we would stop immediately as and when the user wants. It is good practice with this design to keep such checking as often as possible so that we could respond immediately the moment user cancels the dialog. Its importance need not be stressed twice given the fact that we are executing the function in a background thread context and not in the main application thread context.

Finally when done, we set the progress to 100 and exit from the function by returning true. This indicates that we have successfully completed the lengthy operation, which results in a return value of IDOK for the method CUPDialog::DoModal(). However, we may sometimes require indicating failure in the lengthy operation. For example, in operations that involve file manipulations we may encounter file loading/reading/writing errors. In such cases we exit from the function by returning false. This would result in a return value of INT_PTR made of LOWORD(IDCANCEL) and HIWORD(0) for the method CUPDialog::DoModal(). Incase of the user canceling the dialog before the operation gets completed on its own, the return value for the method CUPDialog::DoModal() would be an INT_PTR made of LOWORD(IDCANCEL) and HIWORD(1). The following code fragment illustrates such error checking mechanism.

bool LengthyOperationProc(const CUPDUPDATA* pCUPDUPData);
 
void CApplicationDlg::OnBnClickedOk()
{
    CUPDialog Dlg(GetSafeHwnd(), LengthyOperationProc, this);
    
    INT_PTR nResult = Dlg.DoModal();
    
    if(nResult != IDOK)
    {
        if(!HIWORD(nResult))
            MessageBox(_T("Error Occurred !!"));
        else
            MessageBox(_T("User Cancelled the Dialog !!"));
    }
}

In any typical application, when the user presses the cancel button before the operation gets completed on its own, the value related to CUPDUPData::ShouldTerminate() would be set to true by the dialog. However, if we are unable to check it immediately due to any reason, such as being stuck in some time consuming primitive operations or forgot to call CUPDUPData::ShouldTerminate(), the dialog would wait for some time before actually returning the control to the parent window. In such case, the thread might still continue to execute in the background even though the dialog box has terminated. It would be killed when the dialog class object variable gets out of scope. That is, the thread would get killed in the destructor of the CUPDialog object, if still found alive by that time.

What you should do?

To use this Universal Progress Dialog in your code, all that you need to do is add UPDialog.h, UPDialog.cpp and InitCommonControls.h files to your project and start using the CUPDialog class. UPDialog.h file contains the CUPDialog class declaration and other related structures, and can be accessed from your code with:

#include "UPDialog.h"

Wherever you require to display the progress operation, declare a variable for the class CUPDialog and call the method CUPDialog::DoModal() on it. (Do not worry about creating a dialog resource. CUPDialog has a built-in template that it can load from memory.) To declare the variable for the class CUPDialog, you need to pass the function that you want to execute in the background as a constructor parameter. Remember that the function should be of the form:

bool UserProc(const CUPDUPDATA* pParam);

Inside that UserProc you may use the overloaded methods CUPDUPDATA::SetProgress() and CUPDUPDATA::ShouldTerminate() to set the progress, and to determine if the user has cancelled the dialog - respectively. To access the data that you have supplied, use the CUPDUPDATA::GetAppData() method.

Please note that this UserProc function is being executed in a different thread context than the main application thread context. So, your application remains being interactive while performing your lengthy operation. Feel free to use it any where for any operation any number of times!!

What you don't need to do?

As you could easily recollect, using the dialog controls require you to add the "commctrl.h" to the list of header files and "comctl32.lib" to the list of library files. However, the "InitCommonControls.h" header file automatically does both of this. It adds the header file by using the #include and the library file by using the #pragma pre-compiler directives as shown below.

#include <commctrl.h>

#pragma comment(lib, "comctl32.lib")

Another thing that should be taken enough note is the call to the InitCommonControlsEx() function. It is well known that, in order to use common controls on any dialog box,  we should call  InitCommonControlsEx() once per application before creating the dialog box. To fulfill this requirement we could make such function call a part of the CUPDialog class constructor. However, that would result in calling the InitCommonControlsEx() function every time a CUPDialog object is created, which is not advisable. To overcome this, we use a nice technique of creating a templated class, whose sole purpose is just calling InitCommonControlsEx() in its constructor, and declaring a static variable for that class in our application. Since static variables are constructed at the start of the application, we are guaranteed to get our static object created at that time and place the necessary call as part of its construction. The following illustrates this technique.

class _tagInitCommonControls
{ 
  _tagInitCommonControls()
  {
    INITCOMMONCONTROLSEX icce;
    
    icce.dwSize = sizeof(INITCOMMONCONTROLSEX);
    icce.dwICC = CUPDIALOG_CONTROL_CLASSES;
    InitCommonControlsEx(&icce);
  }
};
static _tagInitCommonControls m_InitCommonControls;

Note that, by using this static variable technique, we are calling the required function only once and that too before any of the user code gets executed. Furthermore, to prevent the user from accessing this class, we make it as a private base class of our CUPDialog, thereby restricting its usage only to ourselves. Please refer to InitCommonControlsEx Helper Class for more details on this.

What you could do?

The Universal Progress Dialog not only saves you from redundant work but also offers you many customizations. For example, if you have already designed some dialog template of your own containing more controls (or) have a similar dialog with different control Id values, CUPDialog allows you to use your custom dialog template in place of the default template.

To avail this facility and many such facilities of customization, all that you need to do is use the method CUPDialog::SetDialogTemplate() passing your own custom dialog template resource name and the static, progress bar and cancel button control ids.

	inline void SetDialogTemplate(HINSTANCE hInst, LPCTSTR lpTemplateName, int StaticControlId, int ProgressBarControlId, int CancelButtonId)

CUPDialog::SetDialogTemplate() accepts the control Ids at runtime instead of choosing them at compile time. This way CUPDialog can provide the runtime logic for more than one template simultaneously. The first parameter to this method is a HINSTANCE of the dialog resource, which makes it possible for you to supply a template that can be loaded from other modules.

To process any additional control messages for your custom dialog box, you can use the overloaded CUPDialog::OnMessage() method. This CUPDialog::OnMessage() gets called whenever the dialog receives a message (from WM_INTIDIALOG onwards). You can override this method in a CUPDialog derived calss of your own and process the additional messages. Please refer to the sample demo application to see this in action.

Furthermore, to refine the time the dialog should wait after signaling the termination, you can use the method CUPDialog::SetTerminationDelay().

By default, the dialog waits for 500ms. Changing it to larger values (such as 1000) would make the dialog wait for more time (1000ms), and to smaller values would make it wait for less time. Both are not suggestible actions as they would either cause the dialog to stall for long times or the thread to get killed prematurely. The default value of 500ms is suitable for most applications. Note that, this value is used only for forced terminations, such as the user canceling the dialog, and that too if the thread is found to be alive at the time of cancellation. If the thread completes on its own before any interruption, then this value would not come into scene. This way both efficiency and interactivity are guaranteed to be at their best in all the situations.

Conclusions

The Universal Progress Dialog is a simple modal dialog box aimed to be handy in most of the cases where we want to add a simple progress dialog without wasting much time redesigning a template and logic from scratch. You can use it any where any time for any task. Next time when you feel that some operation is taking long time - try and see if you could plug in this CUPDialog. All it needs is just adding two more lines of code to your existing project.

By   

P.GopalaKrishna