Click here to Skip to main content
Click here to Skip to main content

A Pretty Good Splash Screen in C#

By , 23 Dec 2003
 
Prize winner in Competition "C# Oct 2003"

Introduction

Every time a customer loads your application, you have the opportunity to impress or disappoint with your splash screen. A good splash screen will:

  • Run on a separate thread
  • Fade in as it appears, and fade out as it disappears
  • Display a running status message that is updated using a static method
  • Display and update a predictive self-calibrating owner-drawn smooth-gradient progress bar
  • Display the number of seconds remaining before load is complete

In this tutorial, we'll explore how to create a splash screen and add these features one at a time. We start with the creation of a simple splash screen, followed by the code changes required for the addition of each feature. You can skip to the bottom of the article to see the complete source code. I've also included a small test project in the download that demonstrates the splash screen.

Background

It was a lot of fun writing the code for this article and, while it's not perfect for all needs, I hope it saves you some coding time. The most fun for me is seeing a completely accurate and smoothly progressing progress bar as my application loads up. Please feel free to post any enhancement suggestions, bugs or other comments you may have.

Create the Simple Splash Screen Project

Start out by creating a Windows Forms project. Name it SplashScreen. Add a Windows Form to the project and name it SplashScreen. Delete Form1.cs.

Now obtain a product bitmap with a light background suitable for putting text over. If you're lucky, a really talented person (like dzCepheus - see the thread below) will provide one for you. Set the Background Image property to it. Set the following properties on the form:
FormBorderStyle = None
StartPosition = CenterScreen

In the form constructor, add the line:

this.ClientSize = this.BackgroundImage.Size;

Make it Available from Static Methods

Because the splash screen will only need a single instance, you can simplify your code by using static methods to access it. By just referencing the SplashScreen project, a component can launch, update or close the splash screen without needing an object reference. Add the following code to SplashScreen.cs:
static SplashScreen ms_frmSplash = null;
// A static entry point to launch SplashScreen.
static public void ShowForm()
{
  ms_frmSplash = new SplashScreen();
  Application.Run(ms_frmSplash);
}
// A static method to close the SplashScreen
static public void CloseForm()
{
  ms_frmSplash.Close();
}

Put it on its Own Thread

A splash screen displays information about your application while it is loading and initializing its components. If you are going to display any dynamic information during that time, you should put it on a separate thread to prevent it from freezing when initialization is hogging the main thread. Generally speaking, you can safely update data in cross-thread method calls, but you cannot update the UI without using Invoke to call the updating methods. In this solution, we will use Invoke for launching the Splash Screen and limit all other calls to updating instance data. A timer (which we need anyway for other effects) will update the UI from the modified data.

Start by using the Threading namespace:

using System.Threading;

Declare a static variable to hold the thread:

static Thread ms_oThread = null;

Now add a method to create and launch the splash screen on its own thread:

static public void ShowSplashScreen()
{
  // Make sure it is only launched once.
  if( ms_frmSplash != null )
    return;
  ms_oThread = new Thread( new ThreadStart(SplashScreen.ShowForm));
  ms_oThread.IsBackground = true;
  ms_oThread.ApartmentState = ApartmentState.STA;
  ms_oThread.Start();
}
Now ShowForm() can be made private, since the form will now be shown using ShowSplashScreen().
// A static entry point to launch SplashScreen.
static private void ShowForm()

Add Code to Fade In and Fade Out

It can add real flair to your splash screen by having it fade in when it first appears, and fade out just as your application appears. The form's Opacity property makes this easy.

Declare variables defining increment and decrement rate. These define how quickly the form appears and disappears. They are directly related to the timer interval, since they represent how much the Opacity increases or decreases per timer tick, so if you modify the timer interval, you will want to change these proportionally.

Private double m_dblOpacityIncrement = .05;
private double m_dblOpacityDecrement = .1;
private const int TIMER_INTERVAL = 50;

Add a timer to the form and then modify the constructor to start the timer and initialize the opacity to zero.

this.Opacity = .0;
timer1.Interval = TIMER_INTERVAL;
timer1.Start();

Modify the CloseForm() method to initiate the fade away process instead of closing the form.

static public void CloseForm()
{
  if( ms_frmSplash != null )
  {
    // Make it start going away.
    ms_frmSplash.m_dblOpacityIncrement = -ms_frmSplash.m_dblOpacityDecrement;
  }
  ms_oThread = null;  // we do not need these any more.
  ms_frmSplash = null;
}

Add a Tick event handler to change the opacity as the form is fading in or fading out, and to close the splash screen form when the opacity reaches 0.

private void timer1_Tick(object sender, System.EventArgs e)
{
  if( m_dblOpacityIncrement > 0 )
  {
    if( this.Opacity < 1 )
      this.Opacity += m_dblOpacityIncrement;
  }
  else
  {
    if( this.Opacity > 0 )
      this.Opacity += m_dblOpacityIncrement;
    else
      this.Close();
  }
}

At this point, you have a splash screen that fades into view when you call the ShowSplashScreen() method and starts fading away when you call the CloseForm() method.

Add Code to Display a Status String

Now that the basic splash screen is complete, we can add status information to the form, so the user can tell that something's going on. To do this, we add the member variable m_sStatus to the form to store the status and a label lblStatus to display it. We then add an accessor method to set the variable and modify the timer tick method to update the label. The accessor is thread-safe because it only modifies the data; it doesn't directly modify the label.

private string m_sStatus;
...
// A static method to set the status.
static public string SetStatus(string newStatus)
{
  if( ms_frmSplash == null )
    return;
  ms_frmSplash.m_sStatus = newStatus;
}

Now we modify the timer1_Tick method to update the label.

lblStatus.Text = m_sStatus;

Now Add a Progress Bar

There's no reason to use the standard WinForms progress bar here unless you really want that look. We'll make a gradient progress bar by painting our own Panel control. To do this, add a panel named pnlStatus to the form and set its background color to Transparent. In practice, you might want to derive your own control from the Panel if you expect to use it in more than one place. Here, we'll just respond to the paint event.

Declare a variable to hold the percent completion value. It is a double with a value that will vary between 0 and 1 as the progress bar progresses. Also declare a rectangle to hold the current progress rectangle.

private double m_dblCompletionFraction = 0;
private Rectangle m_rProgress;

For now, add a public property for setting the current percent complete. Later, when we add the self-calibration feature, we'll eliminate the need for it.

// Static method for updating the progress percentage.
static public double Progress
{
  get 
  {
    if( ms_frmSplash != null )
      return ms_frmSplash.m_dblCompletionFraction; 
    return 100.0;
  } 
  set
  {
    if( ms_frmSplash != null )
      ms_frmSplash.m_dblCompletionFraction = value;
  }
}

Now we modify the timer's Tick event handler to invalidate the portion of the Panel we want to paint.

  ...
  int width = (int)Math.Floor(pnlStatus.ClientRectangle.Width 
     * m_dblCompletionFraction);
  int height = pnlStatus.ClientRectangle.Height;
  int x = pnlStatus.ClientRectangle.X;
  int y = pnlStatus.ClientRectangle.Y;
  if( width > 0 && height > 0 )
  {
    m_rProgress = new Rectangle( x, y, width, height);
    pnlStatus.Invalidate(m_rProgress);
  }
  ...

Finally, add a Panel control named pnlStatus to the form and a paint handler to paint the gradient progress bar. You will probably want to fiddle with the RGB values to get a color scheme that works with your graphic.

// Paint the portion of the panel invalidated during the tick event.
private void pnlStatus_Paint(object sender, 
    System.Windows.Forms.PaintEventArgs e)
{
  if( e.ClipRectangle.Width > 0 && m_iActualTicks > 1 )
  {
    LinearGradientBrush brBackground = 
      new LinearGradientBrush(m_rProgress, 
                              Color.FromArgb(50, 50, 200),
                              Color.FromArgb(150, 150, 255), 
                              LinearGradientMode.Horizontal);
    e.Graphics.FillRectangle(brBackground, m_rProgress);
  }
}

Smooth the Progress by Extrapolating Between Progress Updates

I don't know about you, but I've always been annoyed by the way progress bars progress. They're jumpy, stop during long operations, and always cause me vague anxiety that maybe they've stopped responding.

Well, this next bit of code tries to alleviate that anxiety by making the progress bar move even during lengthy operations. We do this by changing the meaning of the Progress updates. Instead of indicating current percent complete, they now indicate the percentage of time we expect the current activity to take before the next Progress update. For example, the first update might indicate that 25% of the total will pass before the second update. This allows us to use the timer to paint more and more of the status bar, up to and including 25% (but not beyond) while we are waiting for the next update. For now, we'll guess at how much to progress per timer tick. Later, we'll calculate this based on experience.

Add member variables to represent the previous progress and the amount to increment the progress bar per timer tick.

private double m_dblLastCompletionFraction = 0.0;
private double m_dblPBIncrementPerTimerInterval = .0015;

Modify the Progress property to save the previous value before setting the new Progress value.

ms_frmSplash.m_dblLastCompletionFraction = 
    ms_frmSplash.m_dblCompletionFraction;

Modify the Timer.Tick event handler to do the progressive update:

if( m_dblLastCompletionFraction < m_dblCompletionFraction )
{
  m_dblLastCompletionFraction += m_dblPBIncrementPerTimerInterval;
  int width = (int)Math.Floor(pnlStatus.ClientRectangle.Width 
                   * m_dblLastCompletionFraction);
  int height = pnlStatus.ClientRectangle.Height;
  int x = pnlStatus.ClientRectangle.X;
  int y = pnlStatus.ClientRectangle.Y;
  if( width > 0 && height > 0 )
  {
    pnlStatus.Invalidate(new Rectangle( x, y, width, height));
  }
}

Now Make the Progress Bar Calibrate Itself

We can now eliminate the need to specify the progress percentages by calculating the values and remembering them between splash screen invocations. Notice that this will work only if you make a fixed sequence of calls to SetStatus() and SetReferencePoint() during startup.

Registry Access

For completeness, we'll define a simple utility class for accessing the registry. You can replace this with whatever persistent string storage mechanisms you use in your application. In the source code provided, this class appears below the SplashScreen class.

Don't forget to update the registry key strings to reflect your application name and company name!

using Microsoft.Win32;
...
/// A class for managing registry access.
public class RegistryAccess
{
  private const string SOFTWARE_KEY = "Software";
  private const string COMPANY_NAME = "MyCompany";
  private const string APPLICATION_NAME = "MyApplication";
  // Method for retrieving a Registry Value.
  static public string GetStringRegistryValue(string key, 
    string defaultValue)
  {
    RegistryKey rkCompany;
    RegistryKey rkApplication;
    rkCompany = Registry.CurrentUser
                        .OpenSubKey(SOFTWARE_KEY, false)
                        .OpenSubKey(COMPANY_NAME, false);
    if( rkCompany != null )
    {
      rkApplication = rkCompany.OpenSubKey(APPLICATION_NAME, true);
      if( rkApplication != null )
      {
        foreach(string sKey in rkApplication.GetValueNames())
        {
          if( sKey == key )
          {
            return (string)rkApplication.GetValue(sKey);
          }
        }
      }
    }
    return defaultValue;
  }
  // Method for storing a Registry Value.
  static public void SetStringRegistryValue(string key, string stringValue)
  {
    RegistryKey rkSoftware;
    RegistryKey rkCompany;
    RegistryKey rkApplication;
    rkSoftware = Registry.CurrentUser.OpenSubKey(SOFTWARE_KEY, true);
    rkCompany = rkSoftware.CreateSubKey(COMPANY_NAME);
    if( rkCompany != null )
    {
      rkApplication = rkCompany.CreateSubKey(APPLICATION_NAME);
      if( rkApplication != null )
      {
        rkApplication.SetValue(key, stringValue);
      }
    }
  }
}

Member Variables

Now declare variables for keeping track of how long each interval between updates is taking (this time) and what it took per interval last time (from the registry). Declare registry key constants and a Boolean flag to indicate whether this is the first launch.

private bool m_bFirstLaunch = false;
private DateTime m_dtStart;
private bool m_bDTSet = false;
private int m_iIndex = 1;
private int m_iActualTicks = 0;
private ArrayList m_alPreviousCompletionFraction;
private ArrayList m_alActualTimes = new ArrayList();
private const string REG_KEY_INITIALIZATION = "Initialization";
private const string REGVALUE_PB_MILISECOND_INCREMENT = "Increment";
private const string REGVALUE_PB_PERCENTS = "Percents";

Reference Points

We need to declare methods for recording various reference points during application startup. Reference points are critical to making a self-calibrating progress bar since they replace progress bar percent-complete updates. To make the best use of this capability, you should sprinkle reference points inside of the initialization code that runs during application startup. The more you place, the smoother and more accurate your progress bar will be. This is when static access really pays off, because you don't have to pass a reference to SplashScreen to the initialization code.

First, we'll need a simple utility function to return elapsed Milliseconds since the Splash Screen first appeared. This is used for calculating the percentage of overall time allocated to each interval between ReferencePoint calls.

// Utility function to return elapsed Milliseconds since the 
// SplashScreen was launched.
private double ElapsedMilliSeconds()
{
  TimeSpan ts = DateTime.Now - m_dtStart;
  return ts.TotalMilliseconds;
}

SetStatus() and SetReferencePoint() both call SetReferenceInternal() which records the time of the first call and adds the elapsed time of each subsequent call to an array for later processing. It sets the progress bar values by referencing previous recorded values for the progress bar. For example, if we're processing the 3rd SetReferencePoint() call, we use the actual percentage of the overall load time that occurred between the 3rd and 4th calls during the previous invocation.

// Static method called from the initializing application to 
// give the splash screen reference points.  Not needed if
// you are using a lot of status strings.
static public void SetReferencePoint()
{
  if( ms_frmSplash == null )
    return;
  ms_frmSplash.SetReferenceInternal();
}
// Internal method for setting reference points.
private void SetReferenceInternal()
{
  if( m_bDTSet == false )
  {
    m_bDTSet = true;
    m_dtStart = DateTime.Now;
    ReadIncrements();
  }
  double dblMilliseconds = ElapsedMilliSeconds();
  m_alActualTimes.Add(dblMilliseconds);
  m_dblLastCompletionFraction = m_dblCompletionFraction;
  if( m_alPreviousCompletionFraction != null 
      && m_iIndex < m_alPreviousCompletionFraction.Count )
    m_dblCompletionFraction = (double)m_alPreviousCompletionFraction[
      m_iIndex++];
  else
    m_dblCompletionFraction = ( m_iIndex > 0 )? 1: 0;
}

The next two functions, ReadIncrements() and StoreIncrements(), read and write the calculated intervals associated with each of the ReferencePoint values.

// Function to read the checkpoint intervals from the 
// previous invocation of the
// splashscreen from the registry.
private void ReadIncrements()
{
  string sPBIncrementPerTimerInterval = GetStringRegistryValue(
                            REGVALUE_PB_MILISECOND_INCREMENT, "0.0015");
  double dblResult;
  if( Double.TryParse( sPBIncrementPerTimerInterval,
                       System.Globalization.NumberStyles.Float,
                       System.Globalization.NumberFormatInfo.InvariantInfo,
                       out dblResult) )
    m_dblPBIncrementPerTimerInterval = dblResult;
  else
    m_dblPBIncrementPerTimerInterval = .0015;
  string sPBPreviousPctComplete = GetStringRegistryValue( 
     REGVALUE_PB_PERCENTS, "" );
  if( sPBPreviousPctComplete != "" )
  {
    string [] aTimes = sPBPreviousPctComplete.Split(null);
    m_alPreviousCompletionFraction = new ArrayList();
    for(int i = 0; i < aTimes.Length; i++ )
    {
      double dblVal;
      if( Double.TryParse(aTimes[i], 
                     System.Globalization.NumberStyles.Float,
                     System.Globalization.NumberFormatInfo.InvariantInfo,
                     out dblVal) )
        m_alPreviousCompletionFraction.Add(dblVal);
      else
        m_alPreviousCompletionFraction.Add(1.0);
    }
  }
  else
  {
    // If this is the first launch, flag it so we don't try to 
    // show the scroll bar.
    m_bFirstLaunch = true;
  }
}
// Method to store the intervals (in percent complete) 
// from the current invocation of
// the splash screen to the registry.
private void StoreIncrements()
{
  string sPercent = "";
  double dblElapsedMS = ElapsedMilliSeconds();
  for( int i = 0; i < m_alActualTimes.Count; i++ )
    sPercent += ((double)m_alActualTimes[i]/dblElapsedMS).ToString(
        "0.####", System.Globalization.NumberFormatInfo.InvariantInfo) + " ";

  SetStringRegistryValue( REGVALUE_PB_PERCENTS, sPercent );
  m_dblPBIncrementPerTimerInterval = 1.0/(double)m_iActualTicks;
  SetStringRegistryValue( REGVALUE_PB_MILISECOND_INCREMENT,
       m_dblPBIncrementPerTimerInterval.ToString("#.000000",
       System.Globalization.NumberFormatInfo.InvariantInfo));
}

We now can modify the SetStatus() method to add a Reference when the Status is updated. We also add an overloaded method to permit a Status update without the SetReferenceInternal() call. This is useful if you are in a section of code that has a variable set of status string updates. Note that depending on how often SetStatus() is called, you may not need many SetReference() calls in your startup code.

static public void SetStatus(string newStatus)
{
  SetStatus(newStatus, true);
}
static public void SetStatus(string newStatus, bool setReference)
{
  if( ms_frmSplash == null )
    return;
  ms_frmSplash.m_sStatus = newStatus;
  if( setReference )
    ms_frmSplash.SetReferenceInternal();
}

We also need to modify the timer tick and progress bar paint event handlers to paint only when m_bFirstLaunch is false. This prevents the first launch from showing an uncalibrated progress bar.

...
// Timer1_Tick()
if( m_bFirstLaunch == false && m_dblLastCompletionFraction 
    < m_dblCompletionFraction )
...
//pnlStatus_Paint()
if( m_bFirstLaunch == false && e.ClipRectangle.Width > 0 
    && m_iActualTicks > 1 )

Add a Time Remaining Counter

Finally, we can fairly accurately estimate the remaining time for initialization by examining what percentage is yet to be done. Add a label called lblTimeRemaining to the splash screen form to display it. Add the following code to the timer1_Tick() event handler to update the lblTimeRemaining label on the SplashScreen form.
int iSecondsLeft = 1 + (int)(TIMER_INTERVAL * 
  ((1.0 - m_dblLastCompletionFraction)/m_dblPBIncrementPerTimerInterval)) 
  / 1000;
if( iSecondsLeft == 1 )
  lblTimeRemaining.Text = string.Format( "1 second remaining");
else
  lblTimeRemaining.Text = string.Format( "{0} seconds remaining",
     iSecondsLeft);

Using the SplashScreen

To use the splash screen, just call SplashScreen.ShowSplashScreen() on the first line of your Main() entry point. Periodically call either SetStatus() (if you have a new status to report) or SplashScreen.SetReferencePoint() (if you don't) to calibrate the progress bar. When your initialization is complete, call SplashScreen.CloseForm() to start the fade out process. Take a look at the test module provided in the download if you have any questions.

You may want to play around with the various constants to adjust the time of fade in and fade out. If you set the interval to a very short time (like 10 ms), you'll get a beautiful smoothly progressing progress bar but your performance may suffer.

When the application first loads, you will notice that the progress bar and time remaining counter do not display. This is because the splash screen needs one load to calibrate the progress bar. It will appear on subsequent application launches.

SplashScreen.cs Source Code

using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Threading;
using System.Diagnostics;
using Microsoft.Win32;

namespace SplashScreen
{
  /// Summary description for SplashScreen.
  public class SplashScreen : System.Windows.Forms.Form
  {
    // Threading
    static SplashScreen ms_frmSplash = null;
    static Thread ms_oThread = null;

    // Fade in and out.
    private double m_dblOpacityIncrement = .05;
    private double m_dblOpacityDecrement = .08;
    private const int TIMER_INTERVAL = 50;

    // Status and progress bar
    private string m_sStatus;
    private double m_dblCompletionFraction = 0;
    private Rectangle m_rProgress;

    // Progress smoothing
    private double m_dblLastCompletionFraction = 0.0;
    private double m_dblPBIncrementPerTimerInterval = .015;

    // Self-calibration support
    private bool m_bFirstLaunch = false;
    private DateTime m_dtStart;
    private bool m_bDTSet = false;
    private int m_iIndex = 1;
    private int m_iActualTicks = 0;
    private ArrayList m_alPreviousCompletionFraction;
    private ArrayList m_alActualTimes = new ArrayList();
    private const string REG_KEY_INITIALIZATION = "Initialization";
    private const string REGVALUE_PB_MILISECOND_INCREMENT = "Increment";
    private const string REGVALUE_PB_PERCENTS = "Percents";

    private System.Windows.Forms.Label lblStatus;
    private System.Windows.Forms.Label lblTimeRemaining;
    private System.Windows.Forms.Timer timer1;
    private System.Windows.Forms.Panel pnlStatus;
    private System.ComponentModel.IContainer components;

    /// Constructor
    public SplashScreen()
    {
      InitializeComponent();
      this.Opacity = .00;
      timer1.Interval = TIMER_INTERVAL;
      timer1.Start();
      this.ClientSize = this.BackgroundImage.Size;
    }

    /// Clean up any resources being used.
    protected override void Dispose( bool disposing )
    {
      if( disposing )
      {
        if(components != null)
        {
          components.Dispose();
        }
      }
      base.Dispose( disposing );
    }

    #region Windows Form Designer generated code
    /// Required method for Designer support - do not modify
    /// the contents of this method with the code editor.
    private void InitializeComponent()
    {
      this.components = new System.ComponentModel.Container();
      System.Resources.ResourceManager resources = 
          new System.Resources.ResourceManager(typeof(SplashScreen));
      this.lblStatus = new System.Windows.Forms.Label();
      this.pnlStatus = new System.Windows.Forms.Panel();
      this.lblTimeRemaining = new System.Windows.Forms.Label();
      this.timer1 = new System.Windows.Forms.Timer(this.components);
      this.SuspendLayout();
      // 
      // lblStatus
      // 
      this.lblStatus.BackColor = System.Drawing.Color.Transparent;
      this.lblStatus.Location = new System.Drawing.Point(152, 116);
      this.lblStatus.Name = "lblStatus";
      this.lblStatus.Size = new System.Drawing.Size(237, 14);
      this.lblStatus.TabIndex = 0;
      // 
      // pnlStatus
      // 
      this.pnlStatus.BackColor = System.Drawing.Color.Transparent;
      this.pnlStatus.Location = new System.Drawing.Point(152, 138);
      this.pnlStatus.Name = "pnlStatus";
      this.pnlStatus.Size = new System.Drawing.Size(237, 24);
      this.pnlStatus.TabIndex = 1;
      this.pnlStatus.Paint += 
        new System.Windows.Forms.PaintEventHandler(this.pnlStatus_Paint);
      // 
      // lblTimeRemaining
      // 
      this.lblTimeRemaining.BackColor = System.Drawing.Color.Transparent;
      this.lblTimeRemaining.Location = new System.Drawing.Point(152, 169);
      this.lblTimeRemaining.Name = "lblTimeRemaining";
      this.lblTimeRemaining.Size = new System.Drawing.Size(237, 16);
      this.lblTimeRemaining.TabIndex = 2;
      this.lblTimeRemaining.Text = "Time remaining";
      // 
      // timer1
      // 
      this.timer1.Tick += new System.EventHandler(this.timer1_Tick);
      // 
      // SplashScreen
      // 
      this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
      this.BackColor = System.Drawing.Color.LightGray;
      this.BackgroundImage = ((System.Drawing.Image)(
        resources.GetObject("$this.BackgroundImage")));
      this.ClientSize = new System.Drawing.Size(419, 231);
      this.Controls.Add(this.lblTimeRemaining);
      this.Controls.Add(this.pnlStatus);
      this.Controls.Add(this.lblStatus);
      this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
      this.Name = "SplashScreen";
      this.StartPosition = 
         System.Windows.Forms.FormStartPosition.CenterScreen;
      this.Text = "SplashScreen";
      this.DoubleClick += new System.EventHandler(
         this.SplashScreen_DoubleClick);
      this.ResumeLayout(false);

    }
    #endregion

    // ************* Static Methods *************** //

    // A static method to create the thread and 
    // launch the SplashScreen.
    static public void ShowSplashScreen()
    {
      // Make sure it is only launched once.
      if( ms_frmSplash != null )
        return;
      ms_oThread = new Thread( new ThreadStart(SplashScreen.ShowForm));
      ms_oThread.IsBackground = true;
      ms_oThread.ApartmentState = ApartmentState.STA;
      ms_oThread.Start();
    }

    // A property returning the splash screen instance
    static public SplashScreen SplashForm 
    {
      get
      {
        return ms_frmSplash;
      } 
    }

    // A private entry point for the thread.
    static private void ShowForm()
    {
      ms_frmSplash = new SplashScreen();
      Application.Run(ms_frmSplash);
    }

    // A static method to close the SplashScreen
    static public void CloseForm()
    {
      if( ms_frmSplash != null && ms_frmSplash.IsDisposed == false )
      {
        // Make it start going away.
        ms_frmSplash.m_dblOpacityIncrement = - 
           ms_frmSplash.m_dblOpacityDecrement;
      }
      ms_oThread = null;  // we do not need these any more.
      ms_frmSplash = null;
    }

    // A static method to set the status and update the reference.
    static public void SetStatus(string newStatus)
    {
      SetStatus(newStatus, true);
    }
    
    // A static method to set the status and optionally update the reference.
    // This is useful if you are in a section of code that has a variable
    // set of status string updates.  In that case, don't set the reference.
    static public void SetStatus(string newStatus, bool setReference)
    {
      if( ms_frmSplash == null )
        return;
      ms_frmSplash.m_sStatus = newStatus;
      if( setReference )
        ms_frmSplash.SetReferenceInternal();
    }

    // Static method called from the initializing application to 
    // give the splash screen reference points.  Not needed if
    // you are using a lot of status strings.
    static public void SetReferencePoint()
    {
      if( ms_frmSplash == null )
        return;
      ms_frmSplash.SetReferenceInternal();

    }

    // ************ Private methods ************

    // Internal method for setting reference points.
    private void SetReferenceInternal()
    {
      if( m_bDTSet == false )
      {
        m_bDTSet = true;
        m_dtStart = DateTime.Now;
        ReadIncrements();
      }
      double dblMilliseconds = ElapsedMilliSeconds();
      m_alActualTimes.Add(dblMilliseconds);
      m_dblLastCompletionFraction = m_dblCompletionFraction;
      if( m_alPreviousCompletionFraction != null 
          && m_iIndex < m_alPreviousCompletionFraction.Count )
        m_dblCompletionFraction = 
            (double)m_alPreviousCompletionFraction[m_iIndex++];
      else
        m_dblCompletionFraction = ( m_iIndex > 0 )? 1: 0;
    }

    // Utility function to return elapsed Milliseconds since the 
    // SplashScreen was launched.
    private double ElapsedMilliSeconds()
    {
      TimeSpan ts = DateTime.Now - m_dtStart;
      return ts.TotalMilliseconds;
    }

    // Function to read the checkpoint intervals 
    // from the previous invocation of the
    // splashscreen from the registry.
    private void ReadIncrements()
    {
      string sPBIncrementPerTimerInterval = 
             RegistryAccess.GetStringRegistryValue( 
                 REGVALUE_PB_MILISECOND_INCREMENT, "0.0015");
      double dblResult;

      if( Double.TryParse(sPBIncrementPerTimerInterval, 
              System.Globalization.NumberStyles.Float,
              System.Globalization.NumberFormatInfo.InvariantInfo, 
              out dblResult) )
        m_dblPBIncrementPerTimerInterval = dblResult;
      else
        m_dblPBIncrementPerTimerInterval = .0015;

      string sPBPreviousPctComplete = RegistryAccess.GetStringRegistryValue(
            REGVALUE_PB_PERCENTS, "" );

      if( sPBPreviousPctComplete != "" )
      {
        string [] aTimes = sPBPreviousPctComplete.Split(null);
        m_alPreviousCompletionFraction = new ArrayList();

        for(int i = 0; i < aTimes.Length; i++ )
        {
          double dblVal;
          if( Double.TryParse(aTimes[i],
                  System.Globalization.NumberStyles.Float, 
                  System.Globalization.NumberFormatInfo.InvariantInfo, 
                  out dblVal) )
            m_alPreviousCompletionFraction.Add(dblVal);
          else
            m_alPreviousCompletionFraction.Add(1.0);
        }
      }
      else
      {
        m_bFirstLaunch = true;
        lblTimeRemaining.Text = "";
      }      
    }

    // Method to store the intervals (in percent complete)
    // from the current invocation of
    // the splash screen to the registry.
    private void StoreIncrements()
    {
      string sPercent = "";
      double dblElapsedMilliseconds = ElapsedMilliSeconds();
      for( int i = 0; i < m_alActualTimes.Count; i++ )
        sPercent += ((double)m_alActualTimes[i]/
              dblElapsedMilliseconds).ToString("0.####",
              System.Globalization.NumberFormatInfo.InvariantInfo) + " ";

      RegistryAccess.SetStringRegistryValue( 
          REGVALUE_PB_PERCENTS, sPercent );

      m_dblPBIncrementPerTimerInterval = 1.0/(double)m_iActualTicks;
      RegistryAccess.SetStringRegistryValue( 
           REGVALUE_PB_MILISECOND_INCREMENT, 
           m_dblPBIncrementPerTimerInterval.ToString("#.000000",
           System.Globalization.NumberFormatInfo.InvariantInfo));
    }

    //********* Event Handlers ************

    // Tick Event handler for the Timer control.  
    // Handle fade in and fade out.  Also
    // handle the smoothed progress bar.
    private void timer1_Tick(object sender, System.EventArgs e)
    {
      lblStatus.Text = m_sStatus;

      if( m_dblOpacityIncrement > 0 )
      {
        m_iActualTicks++;
        if( this.Opacity < 1 )
          this.Opacity += m_dblOpacityIncrement;
      }
      else
      {
        if( this.Opacity > 0 )
          this.Opacity += m_dblOpacityIncrement;
        else
        {
          StoreIncrements();
          this.Close();
        }
      }
      if( m_bFirstLaunch == false && m_dblLastCompletionFraction 
          < m_dblCompletionFraction )
      {
        m_dblLastCompletionFraction += m_dblPBIncrementPerTimerInterval;
        int width = (int)Math.Floor(
           pnlStatus.ClientRectangle.Width * m_dblLastCompletionFraction);
        int height = pnlStatus.ClientRectangle.Height;
        int x = pnlStatus.ClientRectangle.X;
        int y = pnlStatus.ClientRectangle.Y;
        if( width > 0 && height > 0 )
        {
          m_rProgress = new Rectangle( x, y, width, height);
          pnlStatus.Invalidate(m_rProgress);
          int iSecondsLeft = 1 + (int)(TIMER_INTERVAL * 
            ((1.0 - m_dblLastCompletionFraction)/
              m_dblPBIncrementPerTimerInterval)) / 1000;
          if( iSecondsLeft == 1 )
            lblTimeRemaining.Text = string.Format( "1 second remaining");
          else
            lblTimeRemaining.Text = string.Format( "{0} seconds remaining", 
              iSecondsLeft);

        }
      }
    }

    // Paint the portion of the panel invalidated during the tick event.
    private void pnlStatus_Paint(object sender, 
         System.Windows.Forms.PaintEventArgs e)
    {
      if( m_bFirstLaunch == false && e.ClipRectangle.Width > 0 
            && m_iActualTicks > 1 )
      {
        LinearGradientBrush brBackground = 
          new LinearGradientBrush(m_rProgress, 
                                  Color.FromArgb(100, 100, 100),
                                  Color.FromArgb(150, 150, 255), 
                                  LinearGradientMode.Horizontal);
        e.Graphics.FillRectangle(brBackground, m_rProgress);
      }
    }

    // Close the form if they double click on it.
    private void SplashScreen_DoubleClick(object sender, System.EventArgs e)
    {
      CloseForm();
    }
  }

  /// A class for managing registry access.
  public class RegistryAccess
  {
    private const string SOFTWARE_KEY = "Software";
    private const string COMPANY_NAME = "MyCompany";
    private const string APPLICATION_NAME = "MyApplication";

    // Method for retrieving a Registry Value.
    static public string GetStringRegistryValue(string key, 
        string defaultValue)
    {
      RegistryKey rkCompany;
      RegistryKey rkApplication;

      rkCompany = Registry.CurrentUser.OpenSubKey(SOFTWARE_KEY, 
           false).OpenSubKey(COMPANY_NAME, false);
      if( rkCompany != null )
      {
        rkApplication = rkCompany.OpenSubKey(APPLICATION_NAME, true);
        if( rkApplication != null )
        {
          foreach(string sKey in rkApplication.GetValueNames())
          {
            if( sKey == key )
            {
              return (string)rkApplication.GetValue(sKey);
            }
          }
        }
      }
      return defaultValue;
    }

    // Method for storing a Registry Value.
    static public void SetStringRegistryValue(string key, 
         string stringValue)
    {
      RegistryKey rkSoftware;
      RegistryKey rkCompany;
      RegistryKey rkApplication;

      rkSoftware = Registry.CurrentUser.OpenSubKey(SOFTWARE_KEY, true);
      rkCompany = rkSoftware.CreateSubKey(COMPANY_NAME);
      if( rkCompany != null )
      {
        rkApplication = rkCompany.CreateSubKey(APPLICATION_NAME);
        if( rkApplication != null )
        {
          rkApplication.SetValue(key, stringValue);
        }
      }
    }
  }
}

History

  • 11-16-2003 First Version.
  • 11-18-2003 Corrected some typos and clarified behavior when the application is first called. Changed code to not display the progress bar on the first load.
  • 11-20-2003 Added improvements and bug fixes based on Quentin Pouplard's comments (below).
  • 12-23-2003 Added the graphic provided for us by dzCepheus (below).

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Tom Clement
Serena Software, Inc.
United States United States
Member
I've been programming in C, C++, Visual Basic and C# for over 25 years. I've worked at Sierra Systems, ViewStar, Mosaix, Lucent, Avaya, Avinon, Apptero and now Serena in various roles over my career.
 
There was a time, before all that, when I was a foosball player, then a litigation attorney. My wife gave me a Tornado foosball table for my birthday, so I'm starting to feel the power again!

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralThanks - an an update... [modified]memberpt140125 Jul '12 - 2:14 
There aren't many articles on CodeProject of this age that are still so relevant & useful - that's partly because there's still nothing in the framework that makes it easy to do splash screens well, but also because this code did the job so well in the first place.
 
Having said that, there is an update floating around, written by Mahin Gupta, which you can find here[^]
It addresses a few issues with the original code (notably the cross-thread calls and registry access). It's written for .Net 3.5
 
Update: Just noticed that Tom has also posted the same link below... Smile | :)

modified 25 Jul '12 - 8:25.

GeneralRe: Thanks - an an update... PinmentorTom Clement25 Jul '12 - 5:06 
Hi Pt,
 
Thanks for posting this. I agree, and I also appreciate the work done by Mahin. No idea why your message was downvoted, as it's both a positive note about my very old code (thank you) and very helpful to those looking for a splash screen. It remind me how neglectful I've been about updating this article Smile | :) .
 
Tom
Tom Clement
Serena Software, Inc.
www.serena.com
 
articles[^]

GeneralRe: Thanks - an an update... Pinmemberpt140125 Jul '12 - 6:03 
> No idea why your message was downvoted
Nor me - good job I don't care Smile | :)

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web03 | 2.6.130516.1 | Last Updated 24 Dec 2003
Article Copyright 2003 by Tom Clement
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid