<

Updating a Splash Screen with Loading Progress in a VB.Net WinForms Application

Published on
49,546 Points
38,746 Views
8 Endorsements
Last Modified:
Awarded
Community Pick
Since .Net 2.0, Visual Basic has made it easy to create a splash screen and set it via the "Splash Screen" drop down in the Project Properties.  A splash screen set in this manner is automatically created, displayed and closed by the framework itself.  The default minimum display time is two seconds, and the splash will stay open longer if necessary until the main form has completely loaded.  All of this is well documented on MSDN, commonly known, and discussed pretty extensively in tutorials easily found on the internet.

The "Splash Screen" Setting in Project Properties:
Splash Screen set via Project --> Properties
What isn't commonly found, though, are good examples of how to update the splash screen with progress information from the main form as it loads.  Many examples and tutorials simply use a static splash screen and leave it at that.  MSDN provides an example of how to "update the splash screen with status information" in the MSDN documentation of: My.Application.SplashScreen() property.

In that example, the code is being run from the Application.Startup() event, and changes to the splash screen are done in a direct manner:
Private Sub MyApplication_Startup(ByVal sender As Object, ByVal e As Microsoft.VisualBasic.ApplicationServices.StartupEventArgs) Handles Me.Startup
    ' Get the splash screen.
    Dim splash As SplashScreen1 = CType(My.Application.SplashScreen, SplashScreen1)
    ' Display current status information.
    splash.Status = "Current user: " & My.User.Name
End Sub

Open in new window


Careful!  Multi-threading ahead...

The code is misleading because it implies that we can do this from anywhere.  What isn't well documented or being made clear is that the splash screen and the main form of an application actually run in two different threads.  Attempts to use similar code from the Load() event of your main form will result in varying degrees of success depending upon your version of Visual Studio.  The code does demonstrate that "My.Application.SplashScreen" can be used to obtain a reference to the instance of the splash screen displayed automatically by the framework.  Well use that little nugget later...

Since the splash screen and the main form are in two different threads, the correct approach to communicating between them is to use the Invoke() method with a delegate.  This is covered in the MSDN article, How to: Make Thread-Safe Calls to Windows Forms Controls.

Many don't even think to use this approach, though, since cross-thread communication is not normally an issue when dealing with two forms.  The splash screen, then, is an exception to the rule!

The pattern outlined in the article above is as follows:
Delegate Sub SetTextCallback([text] As String)

Private Sub SetText(ByVal [text] As String)
    If Me.textBox1.InvokeRequired Then
        Dim d As New SetTextCallback(AddressOf SetText)
        Me.Invoke(d, New Object() {[text]})
    Else
        Me.textBox1.Text = [text]
    End If
End Sub

Open in new window


We use InvokeRequired() to determine if the calling thread is different from the thread that created the control.  If yes, then we create an instance of the delegate that points to the same exact method.  Next we use Invoke() to run the delegate on the thread that created the control and pass the parameters using an array of Object.  This actually results in a recursive call since the delegate points to the same method.  On the second run InvokeRequired() will return false and the Else block will execute where the control can be safely updated.

So let's apply the same pattern to a splash screen.  Below is a simple setup consisting of a Borderless Form with a BackgroundImage, Label and a ProgressBar:
Splash Screen Form Setup
Here is the code for frmSplashScreen:
Public Class frmSplashScreen

    Private Delegate Sub UpdateProgressDelegate(ByVal msg As String, ByVal percentage As Integer)

    Public Sub UpdateProgress(ByVal msg As String, ByVal percentage As Integer)
        If Me.InvokeRequired Then
            Me.Invoke(New UpdateProgressDelegate(AddressOf UpdateProgress), New Object() {msg, percentage})
        Else
            Me.Label1.Text = msg
            If percentage >= Me.ProgressBar1.Minimum AndAlso percentage <= Me.ProgressBar1.Maximum Then
                Me.ProgressBar1.Value = percentage
            End If
        End If
    End Sub

    Private Sub frmSplashScreen_Paint(ByVal sender As Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles Me.Paint
        ' Draw a Black Border around the Borderless Form:
        Dim rc As New Rectangle(0, 0, Me.ClientRectangle.Width - 1, Me.ClientRectangle.Height - 1)
        e.Graphics.DrawRectangle(Pens.Black, rc)
    End Sub

End Class

Open in new window


Note that I'm using "Me" instead of a control name to check for InvokeRequired().  The "Me" in this case represents the Form itself and is valid since Forms also Inherit from Control.  All controls run in the same thread as the form that contains them so this is a clean and safe method of checking.  Also note that in the Else block I am updating both the Label and the ProgressBar at the same time.  You don't need a separate method with an accompanying delegate for every control; just check against the form and update all controls at once.  The delegate being used receives both parameters and the Object array contains both parameters passed to the method.

With that code in place, all the main form has to do is call the UpdateProgress() method and the splash screen will take care of the rest.  Remember, a reference to the splash screen instance can be obtained with "My.Application.SplashScreen".  So here is an example Load() event for a main form that updates the splash screen with made up statuses:
Public Class Form1

    Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        Dim splash As frmSplashScreen = CType(My.Application.SplashScreen, frmSplashScreen)
        Dim MadeUpSteps() As String = {"Initializing...", "Authenticating...", "Retrieving Widgets...", "Loading Components...", "Updating Doomahickies..."}

        For i As Integer = 0 To MadeUpSteps.Length - 1
            splash.UpdateProgress(MadeUpSteps(i), CInt((i + 1) / MadeUpSteps.Length * 100))
            System.Threading.Thread.Sleep(1500)
        Next
    End Sub

End Class

Open in new window


The Splash Screen in Action!
Idle-Mind-346085.flv

This article hasn't really introduced anything new or mind blowing...it just puts two and two together to accomplish something that really should be much simpler!
8
Ask questions about what you read
If you have a question about something within an article, you can receive help directly from the article author. Experts Exchange article authors are available to answer questions and further the discussion.
Get 7 days free