This article has been provided courtesy of MSDN.
Summary
This sample demonstrates how to create a splash screen form on a separate thread from the main form. The splash screen will periodically update a "busy" animation and shut down when either the form has finished loading or a specified amount of time has elapsed, whichever comes last.
Contents
This example demonstrates how to create a splash screen that is run on a thread separate from the main thread. This is done so as to allow the main thread to process loading and initialization while not having to explicitly update a progress animation. It is also advantageous to have the splash form on a separate thread so that it can process its own messages and thus receive paint messages when other applications interfere with its drawing. The main form will not be receiving these messages as it is theoretically busy in a load function and not processing messages.
The splash screen consists of a static background and an 8 cell animation that moves from left to right across the bottom of the image at a frame rate dependant, hard-coded rate. The animation was designed in this fashion to keep the code simple but still allow it to demonstrate how to optimize a draw routine by only updating areas of the background that were invalidated in the previous animation draw cycle. This optimization also helps identify the need for receiving paint messages while running on a separate thread because other applications may invalidate the form requiring it to redraw the entire background. On a single thread, these paint messages would not be received by the splash screen form until the main form had completed its processing – unless the code was peppered with Application.DoEvents()
calls which is not an ideal or consistent solution.
The busy animation is updated at regular periodic intervals by a threaded timer (yes another thread). This allows it to give the processor back to the main form while it waits for its next draw cycle. Timing of the splash screen's total duration is also tracked using this timer.
The sample source code demonstrates the following concepts:
- Loading and displaying a static background image
- Loading and displaying an animated image
- Starting two forms on separate threads
- Invoking an
EventHandler
to shutdown a form on a separate thread
- Creating a threaded timer
- Loading embedded resources
- Optimizing drawing by tracking invalid (dirty) rectangles
All of the splash screen interfaces are implemented in the form SplashForm
so as to keep the interaction with the main form, appropriately title, MainForm
as clean as possible. The code for this sample is available in Visual Basic and C# but in the interest of concision, this document refers only to the C# sample code.
The main and splash forms behave very differently so a few parameters need to be modified in the form designer. The format of the splash form is vitally important to ensure it is full screen and maximized so all form initialization for that form is done programmatically in the load function.
MainForm
The main form simulates the actual application, therefore, I chose only to make minimal modifications to it. The MainForm properties that were modified are listed below:
- Appearance->Text – Splash Sample
- Design->Name – MainForm
- Layout->Size – 240,320
- Window Style->MaximizeBox – false
- Window Style->MinimizeBox – false
SplashForm
The splash form remains "as is."
Two embedded resources exist in the project. These resources are the splash screen background image and the "busy" animation image. There are several ways to add a resource to a project. The simplest method is to place the files in the project directory, select "Show All Files" in the Solution Explorer, highlight the files and select "Include In Project" from the files' right-click menu. To make these items into embedded resources highlight them, select "Properties" from the right-click menu, and set the "Advanced->Build Action" property to "Embedded Resource."
The SplashForm
class contains various member variables used to maintain and update the draw conditions of the splash screen. Many of these variables do not need visibility outside of the draw function but are kept global for caching.
The first definition is a constant, specifying the number of cells in the animation. This is a bit of a hack to keep the code simple. Realistically this should be read in an animation file which also contains animation rate, cell ordering, image data, etc.
const int kNumAnimationCells = 8;
The remaining variables are described below with the actual declarations following:
bmpSplash
- the background image for the splash screen
bmpAnim
- the animation image for the splash screen
animPos
- defines the screen location of the animation
g
- global Graphics
object representing the form's client area
splashRegion
- represents the area on the screen to which the background will be drawn. This will also act as the clipping region for drawing the moving animation
splashSrc
- represents the source rectangle for the background draw
attr
- an ImageAttributes
object used for transparency when drawing the animation over the background
splashTimer
- represents the timer that will be used to signal the form that it is time to update the screen with a new draw
redrawSrc
- the source rectangle for redrawing the background in the position that the animation previously occupied i.e., this is the area of the background that the animation occupied in the previous frame
curAnimCell
- the current animation cell being displayed. The animation bitmap is made up of 8 cells so only 1/8th of the bitmap is ever displayed at a time
numUpdates
- the number of times the timer triggered the Draw
function. This is used in conjunction with timerInterval_ms
to determine the duration of the splash screen
timerInterval_ms
- the interval of the timer in milliseconds. This will determine how often the screen is updated
Bitmap bmpSplash = null;
Bitmap bmpAnim = null;
Rectangle animPos = new Rectangle(0,0,0,0);
Graphics g = null;
Rectangle splashRegion = new Rectangle(0,0,0,0);
Rectangle splashSrc = new Rectangle(0,0,0,0);
ImageAttributes attr = new ImageAttributes();
System.Threading.Timer splashTimer = null;
Rectangle redrawSrc = new Rectangle(0,0,0,0);
int curAnimCell = 0;
int numUpdates = 0;
int timerInterval_ms = 0;
The methods of the form SplashForm
are responsible for complete maintenance of the splash screen. This includes loading images, setting an update timer, drawing the background image, updating and drawing the animation, and shutting down the form.
The following is a complete list of the form's methods and descriptions of what each does:
public SplashForm(int timerInterval)
The splash screen form's contstructor is responsible for loading the embedded Bitmap
object resources, as well as initializing the timerInterval_ms
member.
protected override void Dispose( bool disposing )
All resources are needed until the form is closed so this function is unmodified.
public int GetUpMilliseconds()
This function returns the number of milliseconds that the splash screen has been displayed. Note that it is only as accurate as the resolution of timerInterval_ms
.
private void SplashForm_Load(object sender, System.EventArgs e)
The load function is responsible for the majority of the initialization code. Nearly every variable is set to its initial condition in this function. This function also formats and displays the form and commits to a single draw call which will draw the entire background image and the first frame of the animation.
protected override void OnPaint(PaintEventArgs e)
The OnPaint
method of the splash screen form forces the form to redraw the entire background and animation – without updating the animation location.
protected override void OnPaintBackground(PaintEventArgs e)
The OnPaintBackground
function is stubbed out so the form will not attempt to draw anything other than what is explicitly implemented in the Draw function.
public void KillMe(object o, EventArgs e)
This function is responsible for stopping the timer thread and shutting down the form.
protected void Draw(Object state)
This function is only called by the timer and therefore will increment the number of updates that have occurred, update the location of the animation, and draw the animation. The latter two being done through a call to the overloaded Draw
function.
protected void Draw(bool bFullImage, bool bUpdateAnim)
This overload of the Draw
function is where the actual drawing and animation updating gets done. This function will only redraw the entire background if bFullImage
is set to true, otherwise it will optimize drawing by only updating the background that was "dirtied" by the previous location of the animation. The second parameter, bUpdateAnim
, determines whether or not to move the animation.
The constructor for the class sets the interval of the timer based on the value passed to it by the parent form and loads the necessary images as embedded resource streams. The background is a jpg image, however the animation is a bitmap to ensure that transparency is not affected by compression. This is a straightforward implementation so the function is shown in its entirety below.
public SplashForm(int timerInterval)
{
timerInterval_ms = timerInterval;
Assembly asm = Assembly.GetExecutingAssembly();
bmpSplash = new Bitmap
(
asm.GetManifestResourceStream(asm.GetName().Name +
".splash.jpg")
);
bmpAnim = new Bitmap
(
asm.GetManifestResourceStream(asm.GetName().Name +
".animation.bmp")
);
InitializeComponent();
}
The form's Dispose
method remains unchanged from the default.
protected override void Dispose( bool disposing )
{
base.Dispose( disposing );
}
The GetUpMilliseconds
method returns the amount of time, in milliseconds, that the splash screen has been active. This is by no means exact and the resolution is only as good as the timer but it is close enough for its purposes. The function determines the time from the number of times the timer was triggered and the interval of the timer.
public int GetUpMilliseconds()
{
return numUpdates * timerInterval_ms;
}
Quite a large amount of initialization occurs during the form's load function. In fact, nearly every member of the form is initialized at this point. The code in this function is detailed below.
The function is declared as follows:
private void Form2_Load(object sender, System.EventArgs e)
The form must be full-screen and active in order for drawing to take place properly so the first steps the function takes are to set the correct properties for the form.
this.Text = "";
this.MaximizeBox = false;
this.MinimizeBox = false;
this.ControlBox = false;
this.FormBorderStyle = FormBorderStyle.None;
this.WindowState = FormWindowState.Maximized;
this.Menu = null;
Next, the form will initialize the background image source and destination regions. These are set up such that the image will be centered on the screen and the entire image will be drawn.
splashRegion.X =
(Screen.PrimaryScreen.Bounds.Width - bmpSplash.Width) / 2;
splashRegion.Y =
(Screen.PrimaryScreen.Bounds.Height - bmpSplash.Height) / 2;
splashRegion.Width = bmpSplash.Width;
splashRegion.Height = bmpSplash.Height;
splashSrc.X = 0;
splashSrc.Y = 0;
splashSrc.Width = bmpSplash.Width;
splashSrc.Height = bmpSplash.Height;
The animation position is set next, such that it is at the lower left corner of the background but not yet visible. This will give it the effect of coming in from outside of the form. The clipping of the image will be handled automatically by the background region so it is okay to have values that are off of the screen or even negative.
animPos.X = splashRegion.X - bmpAnim.Width / kNumAnimationCells;
animPos.Y = splashRegion.Y + splashRegion.Height - bmpAnim.Height;
animPos.Width = bmpAnim.Width / kNumAnimationCells;
animPos.Height = bmpAnim.Height;
The source of the background redraw has to be calculated during draw updates but the width and height can be cached now.
redrawSrc.Width = bmpAnim.Width / kNumAnimationCells;
redrawSrc.Height = bmpAnim.Height;
The pixel color value for source key transparency is stored in the upper left pixel (0,0) of the animation bitmap.
attr.SetColorKey(bmpAnim.GetPixel(0,0), bmpAnim.GetPixel(0,0));
The Graphics object is created now and cached so it does not have to be recreated at every draw call. Once the Graphics object is created, the clipping region can be set as well.
g = CreateGraphics();
g.Clip = new Region(splashRegion);
Now that the global Graphics object is valid, the form can force a single draw with the full background and the animation not yet moving.
Draw(true, false);
Finally, the function creates a timer based on the timer interval specified in the constructor. This timer will run on a separate thread and call the overloaded draw function directly.
System.Threading.TimerCallback splashDelegate =
new System.Threading.TimerCallback(this.Draw);
this.splashTimer = new System.Threading.Timer(splashDelegate,
null, timerInterval_ms, timerInterval_ms);
The OnPaint
function must simply redraw the background image and the animation image without updating the animation position. Positional updating should only be done by the timer triggered updates. OnPaintBackground
is stubbed out so as to be innocuous to this form.
protected override void OnPaint(PaintEventArgs e)
{
Draw(true, false);
}
protected override void OnPaintBackground(PaintEventArgs e){}
The KillMe
function is responsible for shutting down the timer and closing the form. This is accomplished by simply calling the timer's Dispose
method and the form's Close
method. The only caveat to watch for in this function is that the timer has to be shut down before the form is closed or it may get triggered while the form is still closing.
public void KillMe(object o, EventArgs e)
{
splashTimer.Dispose();
this.Close();
}
The first overload of the Draw
function is called by the timer exclusively. This function increments the number of updates that have occurred and then calls the other Draw
method, specifying not to redraw the entire background (use an optimized drawing routine), and to update the position of the animation.
protected void Draw(Object state)
{
numUpdates++;
Draw(false, true);
}
The following Draw function is the overload that is actually responsible for drawing the background and animation images on the form. This function takes two parameters, bFullImage
and bUpdateAnim
, which specify whether to redraw the entire background and whether to move the position of the animation respectively.
The function is declared as follows.
protected void Draw(bool bFullImage, bool bUpdateAnim)
It may be possible to get a paint message, which would in turn call Draw
before the Graphics object is initialized so g
needs to be verified before progressing to the draw code.
if (g == null)
return;
Because this function is called from a timer thread and from OnPaint
, it is possible that a data collision may occur. Therefore, we will lock the form before processing the draw routine.
lock (this)
If the function was asked to redraw the entire background then simply draw the entire background image using the regions initialized in the load function.
if (bFullImage)
{
g.DrawImage(bmpSplash, splashRegion, splashSrc,
GraphicsUnit.Pixel);
}
If a redraw of the entire background is not required then the code is optimized to only refill the region that was occupied by the animation in the last draw. This could be optimized further to only redraw the overlapping edge(s) from the previous draw.
else if (bUpdateAnim)
{
redrawSrc.X = animPos.X - splashRegion.X;
redrawSrc.Y = animPos.Y - splashRegion.Y;
g.DrawImage(bmpSplash, animPos, redrawSrc, GraphicsUnit.Pixel);
}
Now that the background is updated, it is time to move and draw the animation. If the caller of this method has requested that the animation position be updated then move the animation to the right and increment the current cell. Both of these updates must be checked for "overflow." If the current animation cell is greater than the last cell in the bitmap then start it over. Likewise, if the position causes the left edge of the animation to be off screen then start it back at the beginning.
if (bUpdateAnim)
{
curAnimCell++;
if (curAnimCell >= kNumAnimationCells)
curAnimCell = 0;
animPos.X += 5;
if (animPos.X > splashRegion.X + splashRegion.Width)
{
animPos.X =
splashRegion.X - bmpAnim.Width / kNumAnimationCells;
}
}
Finally, the animation is drawn in its current position.
g.DrawImage(bmpAnim, animPos,
curAnimCell * bmpAnim.Width / kNumAnimationCells, 0,
bmpAnim.Width / kNumAnimationCells, bmpAnim.Height,
GraphicsUnit.Pixel, attr);
The code for this sample has been structured to minimize the burden of supporting the splash screen on the main form. Thus, the required interaction between the main form and splash form is minimal.
MainForm
defines the following members:
const int kSplashUpdateInterval_ms
The millisecond interval between splash screen updates.
const int kMinAmountOfSplashTime_ms
The minimum amount of time the splash screen should be displayed. In the case where the main form finishes loading early, the splash screen will keep displaying until this timer interval has elapsed.
static SplashForm splash
The actual splash screen form.
The StartSplash
function instantiates the splash screen form and starts its message pump.
static public void StartSplash()
{
splash = new SplashForm(kSplashUpdateInterval_ms);
Application.Run(splash);
}
The OnPaint
and OnPaintBackground
functions require only that they not draw if the splash screen is active. This can be modified depending on the requirements of your application. To achieve this, the following code appears in each method before the base class method is called.
if (splash != null)
return;
The function responsible for shutting down the splash screen is the method CloseSplash
. This method checks if the splash screen still exists and then invokes the SplashForm.KillMe
function if it does, followed by clean up of the splash form's resources.
private void CloseSplash()
{
if (splash == null)
return;
splash.Invoke(new EventHandler(splash.KillMe));
splash.Dispose();
splash = null;
}
The main form's load function is responsible for starting the splash screen thread and will simulate the loading and initialization of the main form by sleeping for half of the splash screen's life. Once the simulated initializing is done, the function checks the time and waits until the splash screen has been visible for the minimum required period of time before closing it.
The splash screen worker thread is strategically placed here to avoid racing conditions when multiple forms are started simultaneously. Starting this thread in Main
before the main form is initialized may seem like an ideal solution but intermittent bugs may occur involving the improper initialization of the splash form. This can manifest itself as strange draw clipping behavior or incorrectly initialized client regions.
private void MainForm_Load(object sender, System.EventArgs e)
{
Thread splashThread = new Thread(new ThreadStart(StartSplash));
splashThread.Start();
Thread.Sleep(kMinAmountOfSplashTime_ms / 2);
while (splash.GetUpMilliseconds() < kMinAmountOfSplashTime_ms)
{
Thread.Sleep(kSplashUpdateInterval_ms / 4);
}
CloseSplash();
}
If the main form is closed then there is a danger that the splash form will get stuck processing forever. To eliminate this, the OnClosing
method captures the closing event and shuts down the splash form if it is still active.
protected override void OnClosing
(
System.ComponentModel.CancelEventArgs e
)
{
CloseSplash();
base.OnClosing (e);
}
The code for creating and rendering the splash screen itself is straightforward and simply requires some basic knowledge of the Graphics class. The most important aspect in understanding this sample is the interaction between the main and splash forms. In this sample, the interaction was minimized by utilizing multi-threading to create a completely independent process.
Links