Standing in the Way of Progress

by Ross Mack - GUI Computing

This is the window that goes 'PING'; it shows the user that your app is still alive!

Sometimes it seems I'm writing the same code in my current project as I have written a dozen times in the past year. Then I look at it, and there are subtle differences between this incarnation of the function and the last, so I just keep on coding. This is what I call 'coding for the moment', and is typically what user requests and deadlines force us to do. Also, VB is not the ideal environment for reusable code, so it takes a little more effort to get it to happen. At 2am I am generally not very interested in structuring what I am writing in hope that I will be able to use it in my next project.

Sometimes, however, I relieve myself of this torture and take that extra care to build something I can use again. This way I also feel justified in working on it long enough to get the exact effect I want (I am quite picky after all). In this case I have spent some time writing and refining a progress indicator that can be used anywhere. Sure it's not difficult to drop a flood panel on a form and then update it in a loop - but I usually find that this is the sort of thing I do last, and that means I need to move controls around to make room for it and write code to initialise it, and clear it, and so on. Then I find it stops in a silly place when an error trap fires, or the user hits 'Cancel'. It shouldn't be that much hassle every time.

This progress indicator merely requires that you add its form (PGRESS.FRM) and BAS module (PGRESS.BAS) to your project. The project is zipped up and available for downloading (11kb). Then it's ready to rock n' roll. When called from modeless windows it works very simply - you call the SetProgress function, passing the caption you want the progress indicator to have, and the percentage you are up to.

  SetProgress Nothing, "Doing something…", 10
Every time you want to change the percentage, make the same call:
  SetProgress Nothing, "Still doing something…", 28
It self initialises and updates neatly, handling its own redraws. If you pass an empty caption (the second parameter) it keeps the caption it currently has. Because the window shows itself modelessly, your code keeps running and you can keep updating it. When you are finished simply call KillProgress. It does all the cleaning up and shuts down the progress indicator for next time.
  KillProgress                               ' Simple call isn't it ?
You will notice that the SetProgress call (above) has a parameter to which I passed the keyword Nothing. This is how the call is made from a modeless window. Unfortunately a modeless window, which the progress meter is, cannot be shown from a modal window. Because we want the ability to call this from a modal window you can pass the current window as the first parameter. The progress indicator detects this, and instead of attempting to show itself as a modeless window (not allowed as mentioned above), it makes all the relevant controls children of the window that was passed in the call. Therefore allowing the calling procedure to keep updating the progress indicator as it goes, instead of being halted by showing a modal progress indicator window.

In this case, the KillProgress call puts everything back in its proper place before shutting the progress indicator down. Because we are playing around with hWnds and swapping parents, don't forget to call KillProgress before you close the form that called the progress indicator. It just gets upset.

This progress indicator also has two features that you otherwise might not bother to put in.

  1. The progress indicator is always centred on screen and is set as a topmost window. When a form is passed in the first parameter it is displayed centred on that form.
  2. The flood panel works like those funky Microsoft flood panels, which reverse the colour of the text they are flooding across. This is done basically by using an XOR brush to draw the coloured section of the meter, and setting its colour to the colour given as the background - when XORd with the colour of the percentage text. Have a look at the controls on the form and it should start to make sense.

Progress Meter Methodology.

It's all very well to have a progress meter you can slot in wherever you like, but bear in mind users don't respect you unless your progress indicator looks accurate. If it takes a minute to move from 0 to 10 percent, and then a second and a half to get to 99 percent, you may lose all credibility. If you have no credibility to start off with this is no concern. For everyone else, here is a simple method to make your progress meters look reasonably credible.

The problem with progress meters is that it often looks like the programmer made up the numbers that the thing stops at. Often the programmer really did make up the numbers the thing stops at. So what we need is a way to establish what these numbers should be.

This is reasonably simple, I use only one API call in this attempt at credibility (less than usual as it typically takes at least four and a large ER diagram).

  Declare Function GetTickCount Lib "User" () As Long
First, break the code you want to progress meter into small bite-size chunks. In theory your code should be neatly separated into functional units already, but as there are sometimes deadlines - with changes made by the users, well... it might not be as neatly arranged as you may have intended. At the start of each of these sections assign the return value from GetTickCount to a long.
  lStartTime = GetTickCount()                ' Get process start time
Then at the end of each chunk place the code:
  Msgbox CStr(GetTickCount() - lStartTime) & " milliseconds"
With this code you will get a msgbox at the end of each code segment that tells you how long it took. Note these figures down next to a description of what was happening. It might look something like the following table.

You will notice in the third column I have calculated what percentage of the total time is represented by each section. We now have the basis of our SetProgress calls.

  SetProgress Me, "Opening Database", 0 
  SetProgress Me, "Checking for table", 2         ' we have opened the database which takes 2% 
  SetProgress Me, "Adding to table", 13           ' 2 + 11 
  SetProgress Me, "Updating table", 31            ' 13 + 18  
  SetProgress Me, "Deleting stupid records", 80   ' 31 + 49  
  SetProgress Me, "Closing database", 99          ' 80 + 19  
  SetProgress Me, "Done",100 

Now replace the GetTickCount and Msgbox calls with these SetProgress calls. Even when the code is run on a different machine the relative speed of each section should maintain the same proportions, whether that computer is a Pentium or a 386.

You will notice that the first call passes 0 as the percentage, this is because we are starting on the process that will take 2% of the time. Once it is done we indicate that we are now performing the second step, and show the time elapsed by performing the first step. When this is finished we do one last call to show we're done, and then push the percentage up to 100%. This is merely because one of my pet hates is progress indicators that never reach 100% - they leave me wondering if the thing actually finished. In the case of some code I had to maintain, I found it actually hadn't, it was trapping an error towards the end of a hefty save routine and just stopping - interesting technique.

Obviously what I've presented here can be expanded and built upon, but I find it is pretty generic and more than useful enough to keep in my toolkit. In particular, it is the sort of extra that you can add to a prototype as easily as you can to a shipping product, which can really add a look of polish to an otherwise hollow and hasty prototype (trust me on this one).

Code long and prosper.

Written by: Ross Mack
Feb 1996