|
|||||||||||||||||||||||
|
|||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
This article has been provided courtesy of MSDN. SummaryThis 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
IntroductionThis 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 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:
All of the splash screen interfaces are implemented in the form Setting Up the Forms in the DesignerThe 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. MainFormThe 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:
SplashFormThe splash form remains "as is." Adding Resources to the ProjectTwo 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." SplashForm VariablesThe 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:
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;
SplashForm MethodsThe methods of the form The following is a complete list of the form's methods and descriptions of what each does:
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 protected override void Dispose( bool disposing )
{
base.Dispose( disposing );
}
The 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 protected override void OnPaint(PaintEventArgs e)
{
Draw(true, false);
}
protected override void OnPaintBackground(PaintEventArgs e){}
The public void KillMe(object o, EventArgs e)
{
splashTimer.Dispose();
this.Close();
}
The first overload of the 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, 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 if (g == null)
return;
Because this function is called from a timer thread and from 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);
MainForm InteractionThe 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.
The static public void StartSplash()
{
// Instance a splash form given the image names
splash = new SplashForm(kSplashUpdateInterval_ms);
// Run the form
Application.Run(splash);
}
The if (splash != null)
return;
The function responsible for shutting down the splash screen is the method private void CloseSplash()
{
if (splash == null)
return;
// Shut down the splash screen
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 private void MainForm_Load(object sender, System.EventArgs e)
{
// Create a new thread from which to start the splash screen form
Thread splashThread = new Thread(new ThreadStart(StartSplash));
splashThread.Start();
// Pretend that our application is doing a bunch of loading and
// initialization
Thread.Sleep(kMinAmountOfSplashTime_ms / 2);
// Sit and spin while we wait for the minimum timer interval if
// the interval has not already passed
while (splash.GetUpMilliseconds() < kMinAmountOfSplashTime_ms)
{
Thread.Sleep(kSplashUpdateInterval_ms / 4);
}
// Close the splash screen
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 protected override void OnClosing
(
System.ComponentModel.CancelEventArgs e
)
{
// Make sure the splash screen is closed
CloseSplash();
base.OnClosing (e);
}
ConclusionThe 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
|
||||||||||||||||||||||