Wednesday, 15 April 2009

Super splash screen in C#

I was working on an windows forms application that has a fairly hefty start-up time of around 10-30 seconds so I decided that an awesome super cool splash screen would be in order. I started with CodeProject: A Pretty Good Splash Screen in C# by the author Tom Clement. Whilst this is a pretty good example for a splash screen it just didn’t quite fit my needs.

I wanted all the “splash screen” code to be separate from the form that was displayed to the user so it could be reused for several applications that needed a splash screen. To that end I created a simple interface that would define all the parameters the splash screen controller would manage.

   1: public interface ISplashForm : ISynchronizeInvoke 
   2: { 
   3:     void UpdateStatus(string status); 
   4:     void UpdateProgress(int progress); 
   5:     void UpdateInfo(string info); 
   6: }


Now you can use any windows form for your splash screen as long as you implement the interface above.

On the other end the Application wants to initialise the splash screen controller with a windows form implementing ISplashForm then interact with the controller to set properties progress etc. I wanted that to look something like:

   1: SplashScreen.Show(new WinFormImplementingISplashForm()); 
   2: SplashScreen.UpdateProgress(somePercentage); 
   3: SplashScreen.UpdateInfo(someInfo); 
   4: SplashScreen.UpdateStatus(someStatus); 
   5: SplashScreen.Close();

Using all static methods the controller will handle everything for the application.

On to the controller then, first we need to be able to display the form the user wishes to use as the splash screen. This will need to run a different thread to the main application so that progress can continue loading the application in the first place. Creating threads in code really scares the pants off me so I will be using the BackgroundWorker object to handle the worker thread. When the show method is called the controller needs to ensure that the background worker is correctly setup and that it is not already running.

   1: public static void Show(ISplashForm DisplayForm) 
   2: {
   3:     //Singleton love baby 
   4:     if(!EnsureWorker() && worker.IsBusy) 
   5:         return;  
   6:  
   7:     worker.RunWorkerAsync(DisplayForm); 
   8: }

The worker method simply spins up the provided form so it is visible to the end user.

Every splash screen needs to fade in and out to look super slick and professional. To do that we need to use a timer I would recommend using the System.Timers.Timer not the windows forms one because the latter is only accurate to 55ms which means that the form would fade in more slowly than you might like. Additionally the System.Timers.Timer can be assigned to the main thread of the form to be displayed making the code simpler. To fade the form in and out the timer will adjust the opacity of the form.


   1: static void fader_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
   2: {
   3:     //Runs on the forms main UI thread
   4:     fader.Stop();
   5:     Form splashForm = displayForm as Form;
   6:     if (opacityIncrement > 0 && splashForm.Opacity < 1)
   7:     {
   8:         splashForm.Opacity += opacityIncrement;
   9:     }
  10:     else
  11:     {
  12:         if (splashForm.Opacity > -opacityIncrement)
  13:             splashForm.Opacity += opacityIncrement;
  14:         else //Opacity is 0 so close the form
  15:         {
  16:             splashForm.Opacity = 0;
  17:             splashForm.Close();
  18:         }
  19:     }
  20:  
  21:     // if the form is fading in or out keep the timer going
  22:     if (splashForm.Opacity > 0 && splashForm.Opacity < 1)
  23:         fader.Start();
  24: }

To actually allow the client to update the splash screen through the interface the controller needs to marshal the data over to the splash screens interface thread this is pretty easy to do using the Invoke method and a couple of simple delegates.

   1: private delegate void UpdateText(string text);
   2: private delegate void UpdateInt(int number);
   3:  
   4: public static void UpdateStatus(string status) 
   5: { 
   6:    if(displayForm!=null) 
   7:        displayForm.Invoke(new UpdateText(displayForm.UpdateStatus), new object[] { status }); 
   8: } 
   9:  
  10: public static void UpdateProgress(int progress)
  11: {
  12:    if (displayForm != null && ((Form)displayForm).IsHandleCreated)
  13:        displayForm.Invoke(new UpdateInt(displayForm.UpdateProgress), new object[] { progress });
  14: }
  15:  
  16: public static void UpdateInfo(string info)
  17: {
  18:    if (displayForm != null && ((Form)displayForm).IsHandleCreated)
  19:        displayForm.Invoke(new UpdateText(displayForm.UpdateInfo), new object[] { info });
  20: }

There is a potential problem here that is someone shows the splash screen then immediately attempts to update the status then the application will error because the splash screen does not have a windows handle yet. To solve this problem we need to make the Show method block the running thread until the splash screen has started up. To block the thread you can use a ManualResetEvent and attach to the HandleCreated event of the splash screen and set the reset event.

   1: const int WAIT_TIME = 2000;
   2:  
   3: // Need to block the main thread until the worker 
   4: // has created the window handle
   5: windowCreated = new ManualResetEvent(false);
   6: ((Form)displayForm).HandleCreated += SplashScreenController_HandleCreated;
   7: worker.RunWorkerAsync(displayForm);
   8: if (windowCreated.WaitOne(WAIT_TIME))
   9: {
  10:     ((Form)displayForm).HandleCreated -= SplashScreenController_HandleCreated;
  11:  
  12:     fader.Start();
  13: }
  14: else
  15: {
  16:     throw new ApplicationException("Did not create form handle within a reasonable time.");
  17: }
  18:  
  19: static void SplashScreenController_HandleCreated(object sender, EventArgs e)
  20: {
  21:     windowCreated.Set();
  22: }
  23:  

Phew that was quite a monster post, here is the whole code for the splash screen controller. Comments, suggestions, critisicms, flames all welcome.

SplashScreen.cs

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Text;
   4: using System.Windows.Forms;
   5: using System.Threading;
   6: using System.ComponentModel;
   7: using System.Reflection;
   8:  
   9: namespace Esprit.Common.AppFramework
  10: {
  11:     public class SplashScreen
  12:     {
  13:         private static BackgroundWorker worker;
  14:         private static ISplashForm displayForm;
  15:         private delegate void UpdateText(string text);
  16:         private delegate void UpdateInt(int number);
  17:         private static System.Timers.Timer fader;
  18:         private static double opacityIncrement = .05;
  19:         private static double opacityDecrement = .125;
  20:         private const int FADE_SPEED = 50;
  21:         private const int WAIT_TIME = 2000;
  22:         private static ManualResetEvent windowCreated;
  23:  
  24:         public static double CheckOpacity()
  25:         {
  26:             return ((Form)displayForm).Opacity;
  27:         }
  28:  
  29:         public static void Show(ISplashForm DisplayForm)
  30:         {
  31:             //Singleton love baby
  32:             if (!EnsureWorker() && worker.IsBusy)
  33:                 return;
  34:  
  35:             if (!(DisplayForm is Form))
  36:                 throw new ArgumentException("DisplayForm must be a windows form", "DisplayForm");
  37:  
  38:             displayForm = DisplayForm;
  39:             ((Form)displayForm).Opacity = 0;
  40:             fader.SynchronizingObject = displayForm;  //Force the timer to run on the forms ui thread
  41:  
  42:             // Need to block the main thread until the worker 
  43:             // has created the window handle
  44:             windowCreated = new ManualResetEvent(false);
  45:             ((Form)displayForm).HandleCreated += SplashScreenController_HandleCreated;
  46:             worker.RunWorkerAsync(displayForm);
  47:             if (windowCreated.WaitOne(WAIT_TIME))
  48:             {
  49:                 ((Form)displayForm).HandleCreated -= SplashScreenController_HandleCreated;
  50:  
  51:                 fader.Start();
  52:             }
  53:             else
  54:             {
  55:                 throw new ApplicationException("Did not create form handle within a reasonable time.");
  56:             }
  57:         }
  58:  
  59:         static void SplashScreenController_HandleCreated(object sender, EventArgs e)
  60:         {
  61:             windowCreated.Set();
  62:         }
  63:  
  64:         public static void Close()
  65:         {
  66:             if (worker != null && worker.IsBusy && fader != null)
  67:             {
  68:                 opacityIncrement = -opacityDecrement;
  69:                 fader.Start();
  70:             }
  71:         }
  72:  
  73:         private static bool EnsureWorker()
  74:         {
  75:             // TODO: Add error handling
  76:             if (worker == null)
  77:             {
  78:                 worker = new BackgroundWorker();
  79:                 worker.WorkerReportsProgress = false;
  80:                 worker.WorkerSupportsCancellation = true;
  81:                 worker.DoWork += worker_DoWork;
  82:                 worker.RunWorkerCompleted += worker_RunWorkerCompleted;
  83:  
  84:                 fader = new System.Timers.Timer(FADE_SPEED);
  85:                 fader.Elapsed += new System.Timers.ElapsedEventHandler(fader_Elapsed);
  86:             }
  87:             return true;
  88:         }
  89:  
  90:         static void fader_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
  91:         {
  92:             //Form UI thread
  93:             fader.Stop();
  94:             Form splashForm = displayForm as Form;
  95:             if (opacityIncrement > 0 && splashForm.Opacity < 1)
  96:             {
  97:                 splashForm.Opacity += opacityIncrement;
  98:             }
  99:             else
 100:             {
 101:                 if (splashForm.Opacity > -opacityIncrement)
 102:                     splashForm.Opacity += opacityIncrement;
 103:                 else //Opacity is 0 so close the form
 104:                 {
 105:                     splashForm.Opacity = 0;
 106:                     splashForm.Close();
 107:                 }
 108:             }
 109:  
 110:             // if the form is fading in or out keep the timer going
 111:             if (splashForm.Opacity > 0 && splashForm.Opacity < 1)
 112:                 fader.Start();
 113:         }
 114:  
 115:         private static void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
 116:         {
 117:             // Runs on main thread
 118:             //MessageBox.Show("done");
 119:         }
 120:  
 121:         private static void worker_DoWork(object sender, DoWorkEventArgs e)
 122:         {
 123:             // Runs on background thread
 124:             Thread.CurrentThread.Name = "SplashScreen";
 125:             Application.Run((Form)displayForm);
 126:         }
 127:  
 128:         public static void UpdateStatus(string status)
 129:         {
 130:             if(displayForm!=null && ((Form)displayForm).IsHandleCreated)
 131:                 displayForm.Invoke(new UpdateText(displayForm.UpdateStatus), new object[] { status });
 132:         }
 133:  
 134:         public static void UpdateProgress(int progress)
 135:         {
 136:             if (displayForm != null && ((Form)displayForm).IsHandleCreated)
 137:                 displayForm.Invoke(new UpdateInt(displayForm.UpdateProgress), new object[] { progress });
 138:         }
 139:  
 140:         public static void UpdateInfo(string info)
 141:         {
 142:             if (displayForm != null && ((Form)displayForm).IsHandleCreated)
 143:                 displayForm.Invoke(new UpdateText(displayForm.UpdateInfo), new object[] { info });
 144:         }
 145:     }
 146: }

ISplashForm.cs
   1: using System; 
   2: using System.Collections.Generic; 
   3: using System.Linq; 
   4: using System.Text; 
   5: using System.ComponentModel; 
   6:  
   7: namespace SplashScreen 
   8: { 
   9:     public interface ISplashForm : ISynchronizeInvoke 
  10:     { 
  11:         void UpdateStatus(string status); 
  12:         void UpdateProgress(int progress); 
  13:         void UpdateInfo(string info); 
  14:     } 
  15: }



5 comments:

Gustavo said...

Hi, I am trying your implementation of the Splash Screen and I get to the point of SplashScreen.Show() and I do not know exactly how to proceed. Should I put Form1 as an argumento or what?

Thanks,

Gus De la MOra

John Hunter said...

You need to create a form that you want to display as your splash screen and implement the ISplashForm interface on the form.

Tom Clement said...

Great job John. Thanks for your improvements and sharing them.

Anonymous said...

thanks, was very very helpfull

Edgar F Delgado said...

Hello!

im a begginer, so i dont know how implement your interfaces.

would you have an demo project where youve implemented the super splash screen interface, so we can download it and test and analize?

Thanks in advance!!

Sorry for my poor writing, im from mexico, so i hope you can understand what i mean.