A VBA Progress Bar for Excel and Other Microsoft Apps.

AID: 1756
  • Status: Published

17100 points

  • Byalainbryden
  • TypeResource
  • Posted on2009-10-13 at 10:45:35
Awards
  • Community Pick
  • Experts Exchange Approved
  • Editor's Choice
Keeping the user informed is important
If you have written some VBA code for Excel (or Word, or Access), and it takes a long time to execute, it is important to keep the user informed that the code is progressing. Psychologically, it can also have a tremendously positive effect on the perceived responsiveness and speed of your program if the user can see a visual representation of the current progress.

Common methods of keeping the user informed
Currently, a lot of people keep the user informed by allowing the actions of the macro to be seen. (i.e., the flicker of sheets being activated, cells being changed, etc...). This is generally a terrible idea unless the whole routine is finished very quickly. Excel experts should have learned by now that by setting Application.ScreenUpdating = False as the first step to a VBA routine can speed up code execution by orders of magnitude. The only problem with this, is that now the program appears to be frozen until the code sets screen updating back to true. If this takes a long time, the user might think excel is frozen.

Now that Excel's interface is frozen to speed up procedures, it's especially important to inform the user. Many people do this by setting the value of Application.StatusBar. This can hold any text, and updates even when screen updating is disabled. By changing this regularly, the programmer can give a play by play of the current position in the code, or display a percentage completion in a large loop.
Prog-Status-Bar.PNG
  • 3 KB
  • setting the value of Application.StatusBar
setting the value of Application.StatusBar


Something truly impressive
If you want a great tool for keeping the user informed you should create a progress bar form, show the form before beginning code execution, and hide it afterwards. This is something you could do yourself, but to get you started, I've been employing and improving on my own custom progress bar for quite some time now. I think you should find it useful and very customizable:
Progress-Bar.PNG
  • 10 KB
  • Progress Bar
Progress Bar


Features
  • Programmer can configure the progress bar with one function call, then all that is needed to update is to call SetValue.
  • The progress bar's position and percentage label are automatically computed based on the minimum, maximum, and last set value.
  • The status (caption above the progress bar) can be set dynamically to indicate additional information, such as the current stage of execution.
  • An optional cancel button allows the user a mechanism to halt your lengthy process. The cancel button text can also be set, for instance to "Stop" or "Cancel Generation". The default caption is "Cancel". If set to vbNullString, the cancel button is hidden entirely.
  • An optional label can show the time elapsed since the dialogue box was configured. (It is assumed that you will configure the dialogue directly before showing it). If this option is disabled, the progress bar update routine will not waste time in computing the elapsed time.
  • An optional label can estimate the time remaining before completion. This is a very accurate estimate if you have appropriately set the min and max values. (You can test this in the example below.) This can be disabled, in which case no time is wasted in computing the remaining time.
  • Your calling routine can get the current value of the progress bar, as well as the formatted run time. I use this, for instance, for logging, or to display on the status bar the final run time of the routine after it is complete.
Time-Taken.PNG
  • 2 KB
  • Display the final runtime in the status bar
Display the final runtime in the status bar


Using the progress bar practically
You are responsible for picking an appropriate minimum and maximum value. For instance, if you are looping over R rows (from 1 to R) and C columns (from 1 to C), you should set the minimum to 0 and the maximum to ( R*C ). Furthermore, if you are currently on row i and column j, (and you are looping over all rows within each column), you should be setting the current value to [ i + (j-1)*R ]

If the cancel button is being displayed, you are responsible for periodically checking whether or not the cancel button has been pressed. Do this by checking whether ProgressDialogue.cancelIsPressed is true. If it is true, you should exit the loop and clean up after your routine. Ideally, we could avoid polling using events, but there is no sophisticated event hooking in VBA. An alternative would be to modify the progress dialogue CancelButton_Click method to work very specifically with what you are doing, but regularly checking cancelIsPressed() shouldn't take too much out of your routine's runtime.

The following code block shows the function calls you should remember to include in your loops when using the progress bar dialogue:
Sub wasteTime()
    Dim i As Long
    Dim diag As New ProgressDialogue
    diag.Configure "Wasting Time", "Now wasting your time...", -10000, 10000
    diag.Show
    For i = -10000 To 10000
        diag.SetValue i
        diag.SetStatus "Now wasting your time... " & i
        If diag.cancelIsPressed Then Exit For
    Next i
    diag.Hide
End Sub
                                    
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:

Select allOpen in new window



This produces the following progress bar:
Sample-Progress-Bar.PNG
  • 9 KB
  • Using All Features in a Simple Case
Using All Features in a Simple Case


If you are looping over code where you are making a large number of iterations over code that is very quick to execute, then you might find that updating the dialogue becomes your bottleneck. To prevent this, you can only update the progress bar periodically, using a quick check. For instance, you can say:
If index MOD 1000 = 0 Then ProgressDialogue.SetValue(index)
This way, the dialogue will only get updated every 1000 loops, and it will have much less impact on your code. You will want to play around with how often you update it, to balance the refresh rate with the amount of time spent updating the progress bar. Ideally, in other applications, a progress bar is kept up to date in a thread, but VBA has no support for threading.

Implemented Example and Code
Here, I've attached a workbook containing the 'wasting time' example above. You can use this to experiment with the ProgressDialogue form, and take a look at its implementation.
Progress-Bar-Example.xls
  • 56 KB
  • 'Wasting Time' Example
Progress-Bar-Example.xls


For convenience, I've also included in this article the code within the ProgressDialogue class:
Option Explicit 
Dim Cancelled As Boolean, showTime As Boolean, showTimeLeft As Boolean
Dim startTime As Long
Dim BarMin As Long, BarMax As Long, BarVal As Long  

Private Declare Function GetTickCount Lib "Kernel32" () As Long  

'Title will be the title of the dialogue.
'Status will be the label above the progress bar, and can be changed with SetStatus.
'Min is the progress bar minimum value, only set by calling configure.
'Max is the progress bar maximum value, only set by calling configure.
'CancelButtonText is the caption of the cancel button. If set to vbNullString, it is hidden.
'optShowTimeElapsed controls whether the progress bar computes and displays the time elapsed.
'optShowTimeRemaining controls whether the progress bar estimates and displays the time remaining.
'calling Configure sets the current value equal to Min.
'calling Configure resets the current run time.
Public Sub Configure(ByVal title As String, ByVal status As String, _
                     ByVal Min As Long, ByVal Max As Long, _
                     Optional ByVal CancelButtonText As String = "Cancel", _
                     Optional ByVal optShowTimeElapsed As Boolean = True, _
                     Optional ByVal optShowTimeRemaining As Boolean = True)
    Me.Caption = title
    lblStatus.Caption = status
    BarMin = Min
    BarMax = Max
    BarVal = Min
    CancelButton.Visible = Not CancelButtonText = vbNullString
    CancelButton.Caption = CancelButtonText
    startTime = GetTickCount
    showTime = optShowTimeElapsed
    showTimeLeft = optShowTimeRemaining
    lblRunTime.Caption = ""
    lblRemainingTime.Caption = ""
    Cancelled = False
End Sub  

'Set the label text above the status bar
Public Sub SetStatus(ByVal status As String)
    lblStatus.Caption = status
    DoEvents
End Sub  

'Set the value of the status bar, a long which is snapped to a value between Min and Max
Public Sub SetValue(ByVal value As Long)
    If value < BarMin Then value = BarMin
    If value > BarMax Then value = BarMax
    Dim progress As Double, runTime As Long
    BarVal = value
    progress = (BarVal - BarMin) / (BarMax - BarMin)
    ProgressBar.Width = 292 * progress
    lblPercent = Int(progress * 10000) / 100 & "%"
    runTime = GetRunTime()
    If showTime Then lblRunTime.Caption = "Time Elapsed: " & GetRunTimeString(runTime, True)
    If showTimeLeft And progress > 0 Then _
        lblRemainingTime.Caption = "Est. Time Left: " & GetRunTimeString(runTime * (1 - progress) / progress, False)
    DoEvents
End Sub  

'Get the time (in milliseconds) since the progress bar "Configure" routine was last called
Public Function GetRunTime() As Long
    GetRunTime = GetTickCount - startTime
End Function  

'Get the time (in hours, minutes, seconds) since "Configure" was last called
Public Function GetFormattedRunTime() As String
    GetFormattedRunTime = GetRunTimeString(GetTickCount - startTime)
End Function  

'Formats a time in milliseconds as hours, minutes, seconds.milliseconds
'Milliseconds are excluded if showMsecs is set to false
Private Function GetRunTimeString(ByVal runTime As Long, Optional ByVal showMsecs As Boolean = True) As String
    Dim msecs&, hrs&, mins&, secs#
    msecs = runTime
    hrs = Int(msecs / 3600000)
    mins = Int(msecs / 60000) - 60 * hrs
    secs = msecs / 1000 - 60 * (mins + 60 * hrs)
    GetRunTimeString = IIf(hrs > 0, hrs & " hours ", "") _
                     & IIf(mins > 0, mins & " minutes ", "") _
                     & IIf(secs > 0, IIf(showMsecs, secs, Int(secs + 0.5)) & " seconds", "")
End Function  

'Returns the current value of the progress bar
Public Function GetValue() As Long
    GetValue = BarVal
End Function  

'Returns whether or not the cancel button has been pressed.
'The ProgressDialogue must be polled regularily to detect whether cancel was pressed.
Public Function cancelIsPressed() As Boolean
    cancelIsPressed = Cancelled
End Function  

'Recalls that cancel was pressed so that they calling routine can be notified next time it asks.
Private Sub CancelButton_Click()
    Cancelled = True
    lblStatus.Caption = "Cancelled By User. Please Wait."
End Sub
                                    
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
77:
78:
79:
80:
81:
82:
83:
84:
85:
86:
87:
88:
89:
90:
91:
92:
93:
94:
95:
96:
97:

Select allOpen in new window



Because .frm and .frx files cannot be attached on experts-exchange, I cannot provide you with those directly, but you can easily add this progress bar to any project by following these steps:
1. Download the example excel file above
2. Open the file, and open the VBA project (Alt+F11)
3. Export the ProgressDialogue form. (Save it somewhere you will remember in 5 seconds).
4. Import the exported file in any other excel project. (You can now delete the exported .frm and .frx files from your system.)
Export-and-Re-import.PNG
  • 39 KB
  • Step 3 and 4 of adding a progress bar
Step 3 and 4 of adding a progress bar


Remember that this same concept can be adapted to other Microsoft Applications, Excel is simply where I've built it here.

Building pleasant user interfaces is crucial to the success and reception of your VBA applications and I think this progress bar dialogue can go a long way to improving that. Feel free to use these ideas and code, and build on them. I wish you success!

--
Alain Bryden


References
Progress bar with a user form in Excel - http://support.microsoft.com/kb/211736
GetTickCount system function in VBA - http://msdn.microsoft.com/en-us/library/aa537191%28office.11%29.aspx
Asked On
2009-10-13 at 10:45:35ID1756
Tags

Excel

,

VBA

,

Automation

,

Progress Bar

,

Cancel Button

,

Time Elapsed

Topic

Microsoft Excel Spreadsheet Software

Views
12093

Comments

Expert Comment

by: DrewBowen on 2010-02-08 at 13:42:26ID: 9347

This is a really nice upgrade from the basic progress bar that I have been using. Thanks!

Expert Comment

by: tigermatt on 2010-12-07 at 11:12:36ID: 21936


What a fantastic article! Thank you Alain for your efforts!

Expert Comment

by: TigerMan on 2011-12-21 at 20:27:44ID: 33847

alainbryden,

quite elegant ... this however "If index MOD 1000 = 0 Then ProgressDialogue.SetValue(index)" is not adequately explained ...

here is a very related question that you might be interested in ...
http://www.experts-exchange.com/Software/Office_Productivity/Office_Suites/MS_Office/Excel/Q_27505597.html

Author Comment

by: alainbryden on 2012-01-13 at 08:07:38ID: 34526

Sure thing:

If ( value mod X = 0 )
    ...
   value ++;
                                        
1:
2:
3:

Select allOpen in new window



This is a common way to execute an operation only every X iterations of a loop. Updating the status bar isn't a hugely expensive operation - but if you're looping over relatively fast code thousands and thousands of times, it can quickly become the bottleneck - updating the status can make your process take much longer!

The way to cut down on this overhead is to only update the status every thousand or so loops - just pick a number that gives you a good looking refresh rate, but one so needlessly fast that you're wasting computing power on invisible updates.

MOD is a function which returns the remainder of an integer division. If you divide a number by 1000, the only time the result will be 0 is if the number is a multiple of 1000, so that's why the code will only get executed every 1000 loops.

Hope that helps.

Expert Comment

by: Rayne on 2012-05-14 at 12:14:49ID: 53759

Thank you

Add your Comment

Please Sign up or Log in to comment on this article.

Join Experts Exchange Today

Gain Access to all our Tech Resources

Get personalized answers

Ask unlimited questions

Access Proven Solutions

Search 3.2 million solutions

Read In-Depth How-To Guides

1000+ articles, demos, & tips

Watch Step by Step Tutorials

Learn direct from top tech pros

And Much More!

Your complete tech resource

See Plans and Pricing

30-day free trial. Register in 60 seconds.

Loading Advertisement...

Top MS Excel Experts

  1. dlmille

    1,351,499

    Genius

    10,680 points yesterday

    Profile
    Rank: Genius
  2. ssaqibh

    542,555

    Sage

    0 points yesterday

    Profile
    Rank: Genius
  3. rorya

    381,757

    Wizard

    4,225 points yesterday

    Profile
    Rank: Savant
  4. imnorie

    334,112

    Wizard

    0 points yesterday

    Profile
    Rank: Genius
  5. teylyn

    282,850

    Guru

    20 points yesterday

    Profile
    Rank: Genius
  6. barryhoudini

    280,460

    Guru

    0 points yesterday

    Profile
    Rank: Genius
  7. redmondb

    235,511

    Guru

    2,000 points yesterday

    Profile
    Rank: Sage
  8. matthewspatrick

    230,947

    Guru

    2,010 points yesterday

    Profile
    Rank: Savant
  9. byundt

    197,840

    Guru

    820 points yesterday

    Profile
    Rank: Savant
  10. zorvek

    144,626

    Master

    0 points yesterday

    Profile
    Rank: Savant
  11. StephenJR

    136,537

    Master

    0 points yesterday

    Profile
    Rank: Genius
  12. nutsch

    117,005

    Master

    0 points yesterday

    Profile
    Rank: Genius
  13. gowflow

    110,036

    Master

    0 points yesterday

    Profile
    Rank: Sage
  14. MartinLiss

    107,333

    Master

    0 points yesterday

    Profile
    Rank: Wizard
  15. GlennLRay

    95,652

    Master

    0 points yesterday

    Profile
    Rank: Guru
  16. robhenson

    90,250

    Master

    0 points yesterday

    Profile
    Rank: Sage
  17. ScriptAddict

    88,470

    Master

    0 points yesterday

    Profile
    Rank: Guru
  18. kgerb

    85,022

    Master

    0 points yesterday

    Profile
    Rank: Wizard
  19. aikimark

    84,456

    Master

    3,310 points yesterday

    Profile
    Rank: Genius
  20. andrewssd3

    80,242

    Master

    0 points yesterday

    Profile
    Rank: Wizard
  21. Wiesje

    69,918

    Master

    0 points yesterday

    Profile
    Rank: Master
  22. Shanan212

    66,418

    Master

    0 points yesterday

    Profile
    Rank: Master
  23. krishnakrkc

    59,548

    Master

    0 points yesterday

    Profile
    Rank: Wizard
  24. Michael74

    54,744

    Master

    0 points yesterday

    Profile
    Rank: Wizard
  25. regmigrant

    51,070

    Master

    0 points yesterday

    Profile
    Rank: Guru

Hall Of Fame