Tuesday, June 2, 2009

Creating a Splash Screen in .NET with a progress bar.

EDIT: I've added a link to the source code: http://www.box.net/shared/xbo9xvlguu

Many times an application needs to do many time consuming operations at start-up. Sometimes you need to read data from a database, sometimes you may need to retrieve some data from a web service. When this happens, it's often useful to display a "Splash Screen" to the user, with a company logo or something, along with a progress bar to indicate how much longer it will take for the app to load. While it may sound simple at first, it can be a bit tricky; if you simply show a screen, and do your time consuming operations, your UI will hang and your progress bar will never update. Therefore, there's some threading involved (not too much, don't get scared!), so I'll demonstrate a simple example here.

Start off by creating a simple Windows Forms project. Once it's loaded, add another windows form (besides for the Form1 that's already there) and call it "SplashScreen". To get the look and feel right, let's set some properties:

  • FormBorderStyle: None
  • TopMost : True
  • StartPosition: CenterScreen
Now, in the properties window, find the BackgroundImage property and click the little elipsis {...} and select a picture from your hard drive. I also then changed the BackgroundImageLayout property to None but you can do whatever you want. Then, add a progress bar to the bottom of the form, and set the Dock property to Bottom. Here's what my splash screen looks like in the designer: (not sure why I chose a Halo picture....)

Now, we need to give access to someone outside of this class to update the progress (the progressBar is a private member and can't be accessed.) Here's the problem; if we simply wrap the progress bar's Value property in our own getter/setter like this:



public int Progress
{
get
{
return this.progressBar1.Value;
}
set
{
this.progressBar1.Value = value;
}
}

while you can do that, remember, this splash screen will be shown in a seperate thread. If you then try to access this property from your main thread, you'll get an InvalidOperationException that "Cross-thread operation not valid: Control 'progressBar1' accessed from a thread other than the thread it was created on." So, in order to be able to set any of the UI elements from another thread, we need to call the form's Invoke method which takes a delegate. Here's the complete code for the SplashScreen class:


using System.Windows.Forms;

namespace SplashScreenTesting
{
public partial class SplashScreen : Form
{
private delegate void ProgressDelegate(int progress);

private ProgressDelegate del;
public SplashScreen()
{
InitializeComponent();
this.progressBar1.Maximum = 100;
del = this.UpdateProgressInternal;
}

private void UpdateProgressInternal(int progress)
{
if (this.Handle == null)
{
return;
}

this.progressBar1.Value = progress;
}

public void UpdateProgress(int progress)
{
this.Invoke(del, progress);
}
}
}

As you can see, we created a delegate that we'll use to invoke the update to the progress bar. (The reason why I have a null check for this.Handle is because I was getting an exception right at the start up that the Handle wasn't created yet.)

Ok, now let's create a class that simulates a time consuming operation. Basically, it just calculates Math.Pow for numbers 1 - 100 raised to the 1 - 500,000th power. Every time we move on to another outer number (the 1 - 100) we raise an event that reports progress. Once it's done, we raise an event that we're done. Here's the complete class:



using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace SplashScreenTesting
{
public class Hardworker
{
public event EventHandler<HardWorkerEventArgs> ProgressChanged;
public event EventHandler HardWorkDone;

public void DoHardWork()
{
for (int i = 1; i <= 100; i++)
{
for (int j = 1; j <= 500000; j++)
{
Math.Pow(i, j);
}
this.OnProgressChanged(i);
}

this.OnHardWorkDone();
}

private void OnProgressChanged(int progress)
{
var handler = this.ProgressChanged;
if (handler != null)
{
handler(this, new HardWorkerEventArgs(progress));
}
}

private void OnHardWorkDone()
{
var handler = this.HardWorkDone;
if (handler != null)
{
handler(this, EventArgs.Empty);
}
}
}

public class HardWorkerEventArgs : EventArgs
{
public HardWorkerEventArgs(int progress)
{
this.Progress = progress;
}

public int Progress
{
get;
private set;
}
}
}


I also created a custom event args so that we can pass the progress on to the subscriber of our ProgressChanged event. (As a side note, if you're wondering about the strange assignement taking place when raising the event, see Eric Lippert's blog on the subject.)

Now, on to the main part of the app; the displaying of the splash screen. In the Form1's load event, we'll spawn off another thread to actually display the splash screen and then on the main thread we'll do our "Hard Work". Once the HardWorker has reported that the progress is complete, we'll dispose of the splashscreen and display the main Form. Here's the code, I'll try my best to explain:

using System;
using System.Windows.Forms;
using System.Threading;

namespace SplashScreenTesting
{
public partial class Form1 : Form
{
private SplashScreen splashScreen;
private bool done = false;
public Form1()
{
InitializeComponent();
this.Load += new EventHandler(HandleFormLoad);
this.splashScreen = new SplashScreen();
}

private void HandleFormLoad(object sender, EventArgs e)
{
this.Hide();

Thread thread = new Thread(new ThreadStart(this.ShowSplashScreen));
thread.Start();

Hardworker worker = new Hardworker();
worker.ProgressChanged += (o, ex) =>
{
this.splashScreen.UpdateProgress(ex.Progress);
};

worker.HardWorkDone += (o, ex) =>
{
done = true;
this.Show();
};

worker.DoHardWork();
}



private void ShowSplashScreen()
{
splashScreen.Show();
while (!done)
{
Application.DoEvents();
}
splashScreen.Close();
this.splashScreen.Dispose();
}
}
}

In the constructor we just hook into the Load event and new up the SplashScreen. Then, in the Load event handler, we first hide the current form, because we don't want that to be seen just yet. We then create a Thread and pass in a delegate to our ShowSplashScreen method. The show splash screen method is what's actually going to be run on a seperate thread. First, it displays the SplashScreen. Then, it just sits there in a constant loop waiting for the "done" bool to be set to true. The key ingredient here is the call to Application.Doevents(). I think Microsoft does a good job explaining what this does so I'll let them do that talking:

When you run a Windows Form, it creates the new form, which then waits for events to handle. Each time the form handles an event, it processes all the code associated with that event. All other events wait in the queue. While your code handles the event, your application does not respond. For example, the window does not repaint if another window is dragged on top.

If you call DoEvents in your code, your application can handle the other events. For example, if you have a form that adds data to a ListBox and add DoEvents to your code, your form repaints when another window is dragged over it. If you remove DoEvents from your code, your form will not repaint until the click event handler of the button is finished executing. For more information on messaging, see User Input in Windows Forms.


Basically, it allows other events to be taken care of, even though the current procedure is blocking execution. This allows our progress bar to be updated.

Back in the form load, we then new up our HardWorker and hook into it's events. (I'm using the more terse Lambda syntax, see my previous blog post on the subject here for more information.) Basically, every time the HardWorker reports progress, we update our progress bar. Then when it's done, we set our done flag to true, and show the main form. Finally, we actually kick it off with a call to DoHardWork();

Try it out and run it. It's actually kinda neat to see it in action. This is obviously a very rough example, but it should give you a basic idea on how to get this done.

9 comments:

Unknown said...

I like very much this example but I have a problem I have a application that InitComponent is heavy and I have initializing another process then when I try with this example the splash delay in show ..
please do you have any recomedation?

A.Friedman said...

It's a bit hard to understand exactly what you're asking. If you want, you can email me a small code sample that demonstrates your problem and I'll have a look.

--Alex

Unknown said...

Hi Alex
Last week, I sent a email but I don't know if you recieved it
This is my code ..
public frmEnviroDeskMain()
{

IOL.EnviroDesk.EnviroDeskUI.frmEnviroDeskSplash splash = new frmEnviroDeskSplash();
splash.Show();
splash.label2.Text = "Loading Configuration ...";
InitializeComponent();



SetApplicationDefaults(schema_flag);
SetUIDefaults();
ContextMenu_Init();
TOC_UnselectAll();
Initialize_StdReports(null);


//splash.Close();

}

I need to show the splash before Initializecomponent() because this take a lot of time to load ..
I want to show a label that it change its text depend the process is loading in this time, but the label never change ..Do you have any idea?

Warren Meyer said...

I really like this example. I used this in an application I am currently working on and it has worked for several days and then suddenly it has stopped working. I now receive the following:
"Invoke or BeginInvoke cannot be called on a control until the window handle has been created."
What is really baffling me is that after I discovered this error, I copied the whole project folder to another machine and it works perfectly on the other machine. I am really confused, any insight you could give would be greatly appreciated.

A.Friedman said...

@Warren: If you look closely at my example, you'll see that in my UpdateProgressInternal method I have this code:

if (this.Handle == null)
{
return;
}

This is to avoid the exact exception you're getting. The reason why this is happening (and this is just a guess) is because this is running on another thread, there's no guarantee that the windows was created yet, and therefore the Handle is null. Remember, Windows Forms is just a wrapper around the Win32 API, so if Windows hasn't created the Handle yet, you can't use it.

Just add that if check in your code, and you should be OK.

louico said...

Hi, I also tried to use your code for a project I'm doing and I'm also getting the "Invoke or BeginInvoke cannot be called on a control until the window handle has been created."
I get this error at
this.Invoke(del, progress)
even if there is
if(this.Handle==null) return;

I placed a Thread.Sleep(1000) right after thread.Start(); and it works ok. But I don't want to have a Thread.Sleep(1000)randomly placed in my code. Can you help me? Thanks.

ms.donut smidgets said...

Good day Sir Alex. :) The Progress Bar doesn't progress itself. I exactly run the code you posted... I did not change anything except of course of my classes. I'm using VS 2008.

A.Friedman said...

@louise: Try changing the line of code form if(this.Handle == null) to if(this.Handle == IntPtr.Zero).

I just realized, IntPtr is a struct, which can't be null. Frankly I'm surprised it even compiles...

@ms.donut smidgets: Without seeing more of your code, I really have no way of helping you. I posted a full working sample (download it and run it) so you must have changed something or left something out.

Unknown said...

Helped me so much in my program thanks again!!