11,802,076 members (52,801 online)

# How long is this process going to take?

, 18 Jul 2006 CPOL 37K 756 47
 Rate this:
A class for creating a progress control with text and estimated completion time.

## Introduction

Several years ago, a co-worker was working on a module that involved a progress bar. After he got the prototype up and running and asked for input from the rest of the team, I suggested to him that, for example, even though I know the operation is 25% done, it'd be nice to know about how much longer it was going to take, or when it would finish. Since a lot of operations were very lengthy (e.g., several hours), an estimated completion time, give or take a few minutes, would be a real benefit.

While he agreed with my suggestion, he declined, stating that it would involve too much code. I offered to him what I thought was involved, but it never came to fruition. Oh well, maybe another day...

## First Approach

I guess that day has arrived. Back then, it first occurred to me that if I knew how long each % took, I could extrapolate the remaining % by using an average. It sounded good in theory. For example, if the first % took two seconds, then it stood to reason that the remaining 99% would take roughly 198 seconds. If the second % took three seconds, for an average of 2.5 seconds, the remaining 98% would take roughly 245 seconds. After a while, the numbers should start to level out. The problem I began to notice was that it took a long time, if at all, for this "leveling out" to occur. Why?

Averages, as you know, are highly affected by outliers. If one of those %s took 10 seconds to complete, the average suddenly becomes artificially high. Likewise, if one of those %s took less than a second to complete, the average suddenly becomes artificially low. There are a couple of ways to deal with this.

One way to deal with outliers is to not use them. That, however, requires the list of times to be sorted so that we could then lop off a determinate number of low and high times. This much sorting was not desirable. :(

The other way of dealing with outliers is to use a FIFO approach, or let the oldest times fall off the end, and let the newest times enter on the other end. Think of it as keeping a sliding window of most recent times. This approach had merit, but the question comes up of how big this sliding window should be (i.e., how many samples to retain).

For this exercise, I copied a 43.9 MB file over the network, 80 KB at a time. The sample sizes I used were 50, 75, 100, 150, and ∞. The following chart shows the result:

The smaller sample sizes (e.g., dark blue) are just all over the place. This means that the completion time will keep adjusting itself right up to the very end, which is not very helpful. With the larger numbers (e.g., cyan), the lines slowly started leveling out, but they were still far from useful. It wasn't until I averaged all of the times together (e.g., magenta) that a "smooth" line was revealed. That produced a fairly accurate completion time, but there was one major drawback: it was very inefficient. Each time an 80 KB chunk of the file was copied, another time value was added to the list. This list was then averaged 563 times! The loop to do this averaging was having to process 158,766 time values. Imagine how bad this would be for a file 2-3 times larger.

Each time the progress bar was updated, the following code was executed:

```double dAverage = 0.0;
int    nLower,
nUpper;

COleDateTime timeCurrent = COleDateTime::GetCurrentTime();

// if our list is full, remove the oldest item
if ((UINT) m_listTimes.GetCount() == m_uSampleSize)

// if we have at least two, average them
if (m_listTimes.GetCount() > 1)
{
double dSum = 0.0;

for (int nIndex = 0; nIndex < m_listTimes.GetCount() - 1; nIndex++)
{
COleDateTime time1 = m_listTimes.GetNext(pos);
COleDateTime time2 = m_listTimes.GetAt(pos);

dSum += (time2 - time1);
}

dAverage = dSum / (double) m_listTimes.GetCount();
}

int nPos = GetPos();
GetRange(nLower, nUpper);

// assert that the current position is between the lower and upper limits
ASSERT(nLower <= nPos);
ASSERT(nPos <= nUpper);

// what % (0-100) are we through?
UINT uPercent = (UINT) ((double) nPos / (double) nUpper * 100.0);

// now spread that average out over the remaining percent, and
// add the result to the current time
m_timeEstimated = timeCurrent.m_dt + (dAverage * (100U - uPercent));```

Surely, a better way exists.

## Second Approach

After looking a bit closer at the math, I realized that a better solution was to simply divide the elapsed time by the % done. The result would be the estimated completion time. By adding this value to the operation's start time, we knew approximately when the operation would finish.

Copying the same file as before, the results look vastly different.

While the starting numbers are still a bit rough up front, they quickly level out and remain so for the duration of the operation. This equates to the estimated completion time being fairly accurate very early on.

The changed code looks like:

```int     nLower,
nUpper;

COleDateTime timeCurrent = COleDateTime::GetCurrentTime();

// save starting time if we don't have one
if (m_timeStart.GetStatus() == COleDateTime::invalid)
m_timeStart = timeCurrent;

// how much time has elapsed?
COleDateTimeSpan timeElapsed = timeCurrent - m_timeStart;

int nPos = GetPos();
GetRange(nLower, nUpper);

// assert that the current position
// is between the lower and upper limits
ASSERT(nLower <= nPos);
ASSERT(nPos <= nUpper);

// what % are we done
double dPercent = (double) nPos / (double) nUpper;

// based on how long it has taken so far,
// how long will it take altogether
double dEstTotalTime = timeElapsed.m_span / dPercent;

// add the estimated completion time to the operation's start time
m_timeEstimated = m_timeStart.m_dt + dEstTotalTime;

// subtract the elapsed time from the estimated completion time
m_timeRemaining = dEstTotalTime - timeElapsed.m_span;```

The result of these calculations is the `CEstProgressCtrl` class. While it could be used as a direct replacement for any `CProgressCtrl` object, it currently has no support for the "block" style.

## The CEstProgressCtrl Class

 `CEstProgressCtrl` Construct a `CEstProgressCtrl` object. `GetCompletionTime` Get the control's estimated completion time. `GetRemainingTime` Get the control's estimated remaining time. `SetProgressText` Set the control's text to display in the progress area.

### bool GetCompletionTime( COleDateTime &timeEstimated ) const

#### Return Value

• `true` if the estimated completion time is valid;
• `false` otherwise.

#### Parameters

• `timeCompletion`
• Reference to a `COleDateTime` object that will receive the estimated completion time.

### bool GetRemainingTime( COleDateTime &timeRemaining ) const

#### Return Value

• `true` if the estimated remaining time is valid;
• `false` otherwise.

#### Parameters

• `timeRemaining`
• Reference to a `COleDateTime` object that will receive the estimated remaining time.

### virtual CString SetProgressText( void ) const

#### Return Value

A `CString` object representing the text to be displayed in the control. Along with showing the percent complete, the estimated completion time is also displayed. This time's format (e.g., hh:mm) comes from whatever the system's current time format is.

#### Remarks

This virtual method has access to the three member variables that represent the estimated completion time, the estimated remaining time, and the percent complete value. Override this method to customize the text displayed in the control.

## Rendering the Text

Since we are deriving from `CProgressCtrl` and drawing on the control in the `OnPaint()` method, we are in charge of the control's background and foreground colors, as well as its font.

In the `OnPaint()` method, the first thing we need to do is get the size of the control's client area. This is a simple call to `GetClientRect()`:

```// get this control's size
CRect rc;
GetClientRect(rc);```

Our `OnPaint()` method is going to be called by the framework each time Windows needs to repaint a portion of the control. This is in contrast to us having to call some 'calculate' function each time we deem it necessary (e.g., when `SetPos()` is called). Therefore, to keep one repaint from obscuring another, we need to start with a clean slate each time:

```// erase what's in the control
CPaintDC dc(this);
COLORREF crErase(GetSysColor(COLOR_3DFACE));
dc.FillSolidRect(rc, crErase);```

I chose to use `FillSolidRect()` instead of `FillRect()` since it is a bit faster when dealing with a solid palette. Now we can render the correct amount of the progress bar depending on the percent complete:

```// render the right amount of completeness
CRect rcPct(rc);
rcPct.right = (LONG) ((double) rcPct.right * m_dPctComplete);
dc.FillSolidRect(rcPct, crBackground);```

At this point, the control behaves like normal (i.e., solid bar, no text). The next few code snippets deal with rendering text on the control in a visually-appealing and theme-aware manner. The first thing we'll need is an in-memory DC:

```// create an in-memory DC
CDC dcText;
dcText.CreateCompatibleDC(&dc);```

Before we can use this DC, we must create and select a bitmap of the correct width, height, and color into it:

```CBitmap bmpText;
bmpText.CreateCompatibleBitmap(&dc, rc.Width(), rc.Height());
CBitmap *pOldBitmap = dcText.SelectObject(&bmpText);```

Rather than set the DC's font to some arbitrary value, that may be different from how the system has been configured, we will use the control's current font:

```// select the control's font into the DC
CFont *pFont = GetFont();
CFont *pOldFont = dcText.SelectObject(pFont);```

We'll need to set the background to be transparent, otherwise the background color behind the text will be undesirable. The color we selected for the text looks quite odd. An explanation is in order. The effect we want is for the text's color to be one thing when the progress bar and the text have mixed, and another color when they have not. The benefit to this is that the text is not obscured by whatever color is being used by the progress bar (e.g., blue on blue). See the picture at the top of this article for an example. The text in the completed area is white (on blue). The text in the non-completed area is blue (on gray). The colors in parenthesis represent the transparent effect. If your computer happens to have a different theme than the default blue one, the correct colors will still be used.

To get 'blue' text, whose RGB value is 0x00316AC5, we must flip the bits using the one's complement operator. This yields 0xFFCE953A. And, since the `SetTextColor()` method is not interested in the most significant 8-bits, we'll just mask those out. This gives us a final RGB value of 0x003A95CE. Contrary to what MSDN states, as best as I can tell, `GetSysColor()` returns a `COLORREF` value instead of an RGB value. This doesn't necessarily affect anything, other than the occasional need to switch back and forth between the two formats.

```// text starts out 'blue' on 'gray'
// as progress bar advances, text changes to 'white' on 'blue'
dcText.SetBkMode(TRANSPARENT);
dcText.SetTextColor((~GetSysColor(COLOR_HIGHLIGHT)) & 0x00ffffff);```

At this point, we can render the text, centered both vertically and horizontally, using `DrawText()`:

`dcText.DrawText(str, rc, DT_VCENTER | DT_SINGLELINE | DT_CENTER);`

That's it. We now have a progress bar, very similar to the default one, that also shows the estimated completion time and how much time is remaining. Enjoy!

## Similar Implementations

Indeed there is more than one way to skin a cat:

## Share

 Software Developer (Senior) Pinnacle Business Systems United States

The page you are looking for might have been removed, had its name changed, or is temporarily unavailable.

Internet Information Services

## You may also be interested in...

 First Prev Next
 interesting Chris Losinger2-Sep-07 9:39 Chris Losinger 2-Sep-07 9:39
 A small problem mc_cappy20-Nov-06 5:27 mc_cappy 20-Nov-06 5:27
 When I use this class in an dinamically created dialog, and I call the function "DestroyWindow()" for my dialog then I try to delete the dialog in OnPostNcDestroy() I get an error something like "Debug error Program myprog.exe DAMAGE: after Client block(#1253) at 0x00d77b00"... Can anybody help me? mc_cappy
 Re: A small problem DavidCrow20-Nov-06 6:05 DavidCrow 20-Nov-06 6:05
 Re: A small problem mc_cappy24-Nov-06 6:56 mc_cappy 24-Nov-06 6:56
 Averaging... MJessick19-Jul-06 10:21 MJessick 19-Jul-06 10:21
 Re: Averaging... DavidCrow19-Jul-06 11:07 DavidCrow 19-Jul-06 11:07
 Re: Averaging... Paul Hooper19-Jul-06 15:29 Paul Hooper 19-Jul-06 15:29
 Re: Averaging... DavidCrow20-Jul-06 3:12 DavidCrow 20-Jul-06 3:12
 Cool, it's like mine :) Kochise18-Jul-06 21:15 Kochise 18-Jul-06 21:15
 Last Visit: 31-Dec-99 18:00     Last Update: 8-Oct-15 3:34 Refresh 1