Using .NET Framework Multithreading and GDI+ to Enrich the user experience


Introduction

Today, almost all mayor commercial applications are designed using multiple threads, either to enrich the user experience or increase performance.  The .Net Framework enables multiple threads implementation to all .Net languages.  This article shows you a multithread windows application, using GDI+ in C#.  

Article

Samples of multithread applications are all around you, consider Word, Excel, Powerpoint, FrontPage, even the VS.Net (notice the syntax checker).  Although a lottery windows application is pretty useless (unless you are in the gambling/gaming business or want a method to pick randomly your next lotto numbers) it fits the purpose to show how to leverage the power of multiple thread application. 

The lottery app, contain two classes (Canvas.cs and Ball.cs).

Canvas contains the main execution thread and spawned five service threads which contributes to enrich the user experience and control of the execution flow as follows:

1)Valve thread controls the opening of six valves which enables the draw of a winner ball, maintain the open valve status bar on the windows form, and the window title opening valve graphical rendering. 

2)Winner thread monitors the number of winner balls, maintain the winner status bar on the windows form, and upon the final winner balls, set a property to all non-winner balls to drop, and spawn the Final thread.

3)Final thread controls the final number rendering, control the flow to re-start, and spawn the Exit thread.

4)Exit thread controls the winner balls exit motion and the cool ball motion.

5)Music thread controls the background music for the different events.

ValveThread = new Thread(new ThreadStart(ValveOpen));
ValveThread.Name = "Valve";
ValveThread.Start();
WinnerThread =
new Thread(new ThreadStart(WinnerNumbers));
WinnerThread.Name = "Winner";
WinnerThread.Start();
if (cbMusic.Checked)
{
MusicThread =
new Thread(new ThreadStart(BackgroundMusic));
MusicThread.Name = "Music";
MusicThread.Start();
}
FinalThread =
new Thread(new ThreadStart(FinalNumbers));
FinalThread.Name = "Final";
ExitThread =
new Thread(new ThreadStart(Showoff));
ExitThread.Name = "Exit" 

In addition, the Canvas (the main thread) spawns up to 56 autonomous Ball threads and sets it initial properties (like id, icon, position, direction, canvas).  All Ball instances shares the Canvas instance (form) upon which it renders it graphical representation (icon).   The main thread controls the form click event (to open a valve), and all other graphical objects in the form (like start button, vscroll, checkbox,  active balls, and music).

// Create Ball Instance and thread...
for (c=0;c<=ActiveBalls;c++)
{
Random r =
new Random();
Balls[c] =
new Ball();
Balls[c].Canvas =
this;
Balls[c].Icon =
new Icon(System.AppDomain.CurrentDomain.BaseDirectory + @"\" +
.ToString() + ".ico");
Balls[c].Id = c;
Balls[c].X = r.Next(
this.Width - icon.Width - 20);Balls[c].Y = r.Next(this.Height -
icon.Height - 70);
while (Balls[c].dX == 0)Balls[c].dX = r.Next(-1,1);
while
(Balls[c].dY == 0)
Balls[c].dY = r.Next(-1,1);
tb[c] =
new Thread(new ThreadStart(Balls[c].Move));
tb[c].Name = "Ball " + c.ToString();
tb[c].Start();
while (tb[c].ThreadState != ThreadState.Running)
{
continue;
}
System.WinForms.Application.DoEvents();
}

The Ball class controls it movement, drawing, bouncing, collision, vacuum effects, state, and winner movement (showoff).

Design Considerations:

Designing a multi-thread application requires careful attention to thread management (state, time slice), and thread concurrency (synchronization, starvation, performance, priority) (see http://www.msdn.microsoft.com/library/default.asp  .Net Framework Developer Specifications/Threading Model Concepts).  For this particular application, performance is a critical factor (due to its dynamic nature) which influence many of the design decisions.  For example: you could centralized the ball movement on the main thread and use a delegate/event mechanism to report the ball's position changes, unfortunately this solution is a poor performer since when you call a multicast delegate, the delegates in the invocation list are called synchronously (see: http://msdn.microsoft.com/voices/csharp04192001.asp ).

The background music is also a synchronous call, therefore couldn't be used in the ball class nor in the main thread.  The final and the exit thread need to produce the winner's balls and cool ball movement simultaneously (to provide the animation illusion), thus, I allocate it in a separate threads. 

Thread Design:

When you think on the thread design of an application like this one, several issues needs to be addressed, a) Events timing for animation , b) Thread starvation, c) Component multitasking.  For the timing issue the utilization of delays (Thread.Sleep(n)) was the prefer method (instead of loops), To help with thread starvation yielding (Thread.Sleep(0)) was used to release the remainder of the thread cycles and the prevention of locking or allocating resources for long periods,  To be a good multitasking citizen it is required that you allow the OS to pay attention to other events, to attain this (DoEvents()) was utilized.

The winner objects thread instance is suspended using Thread.CurrentThread.Suspend() method to save cycles, while the non-winner objects are aborted (after it drop down to the bottom) using Thread.CurrentThread.Abort() method:

// Final bounce when end is set...
if ((end) && (!winner))
{
FinalBounce();
Thread.CurrentThread.Abort();
}
// Winner ball goes to top of form...
if (winner)
{
// Draw winner ball in the socket...
g.DrawIcon(clear,x,y);
x = canvas.Holes[winnerhole,0] + canvas.DesktopLocation.X + 30;
y = canvas.DesktopLocation.Y;
dt.DrawIcon(icon,x,y);
// Suspend winner ball thread...
Thread.CurrentThread.Suspend();
}
Once the exit thread
is started, the winner balls thread that were suspended, need to be
estart
using Resume() method.
// Winner Show...
for (int i=0;i<=5;i++)
{
if (i<3)
{
Balls[HoleBall[i]].ShowoffDir = -1;
// Left
}
else
{
Balls[HoleBall[i]].ShowoffDir = 1;
// Right
}
Balls[HoleBall[i]].Showoff =
true;
tb[HoleBall[i]].Resume();
}

GDI+ 101:

Graphics Device Interface Windows API has been around for a while.  The .NET Framework has simplified, and extended the use of the API by providing fundamental classes that enhanced the access to graphics via GDI+.  The System.Drawing namespace provides the functions needed.  In order to utilize the graphics methods, you need to create an instance of a Graphics class:

Graphics g = this.CreateGraphics(); // From a Windows Form
Graphics dt = Graphics.FromHDC(hDC); // From a Handle Device Content

In this application, I use several graphics methods to create a more appealing UI, for example: When you change the value of Balls vertical scroll bar, the current value is displayed with a C# bitmap TextureBrush() and labels are displayed using DrawString() methods.

protected void vsbActiveBalls_ValueChanged (object sender, System.EventArgs e)

{
Point[] cf =
new Point[4];
cf[0].X = vsbActiveBalls.Location.X - 70;
cf[0].Y = vsbActiveBalls.Location.Y + 20;
cf[1].X = vsbActiveBalls.Location.X;
cf[1].Y = vsbActiveBalls.Location.Y + 20;
cf[2].X = vsbActiveBalls.Location.X;
cf[2].Y = vsbActiveBalls.Location.Y + 90;
cf[3].X = vsbActiveBalls.Location.X - 70;
cf[3].Y = vsbActiveBalls.Location.Y + 90;
Font ab =
new Font("Arial",12,FontStyle.Bold);
g.DrawString("Balls",ab,Brushes.Red,
new Point(vsbActiveBalls.Location.X -
0,vsbActiveBalls.Location.Y));
Font f =
new Font("Arial",30,FontStyle.Bold);
Brush cb = new TextureBrush(csharp);
g.FillPolygon(Brushes.White,cf);
g.DrawString(vsbActiveBalls.Value.ToString(),f,cb,
new Point(vsbActiveBalls.Location.X -
0,vsbActiveBalls.Location.Y + 20));
}
Icons and Bitmaps are displayed
using DrawIcon() or DrawImage():
for (int a=0;a<=15;a++)
{
dt.DrawIcon(coolclear,x,y);
x += ((7*((Math.Sign(7-a)+1)/2))-Math.Abs((7-a)));
y -= 1;
dt.DrawIcon(cool1,x,y);
g.DrawImage(sax,x,y+70);
Thread.Sleep(30);
}

Final winner numbers are displayed
using DrawString() method.

Font f = new Font("Arial",30,FontStyle.Bold);
g.DrawString(HoleBall[0].ToString(),f,Brushes.Red,
new Point(Holes[0,0],50));
g.DrawString(HoleBall[1].ToString(),f,Brushes.Red,
new Point(Holes[1,0],50));
g.DrawString(HoleBall[2].ToString(),f,Brushes.Red,
new Point(Holes[2,0],50));
g.DrawString(HoleBall[3].ToString(),f,Brushes.Red,
new Point(Holes[3,0],50));
g.DrawString(HoleBall[4].ToString(),f,Brushes.Red,
new Point(Holes[4,0],50));
g.DrawString(HoleBall[5].ToString(),f,Brushes.Red,
new Point(Holes[5,0],50));
The valve opening graphical rendering
is achieve using DrawLine() method.
for (int i=0;i<=27;i++)
{
dt.DrawLine(Pens.White, dtx + Holes[hb - 1, 0], dty - i, dtx + Holes[hb -1, 1], dty - i);
Thread.Sleep(1);
}

How to use

  • Load the application into the IDE
  • Press F5 to start
  • Make the Thread output window tab visible on top
  • Select number of balls and sound
  • Click Start button
  • Once the status bar says Ready, drag the window to smooth the balls painting
  • Click on the windows to open the valves
  • Enjoy the show!...

Tips:

  • DrawIcon() and DrawImage() are much faster than draing ellipse, polygon and fillpolygon methods.
  • Use Onpaint handler to make you GDI+ graphics permanent
  • Use Thread.Sleep(0) to release the remainder thread cycle
  • Use System.Winforms.Application.DoEvents() to yield
  • Use properties instead of events to communicate among multiple concurrent thread if performance is a requirement.
  • To dispose a thread, don't forget to set the thread instance to null for the garbage collector.  

Conclusion:

After reading this article two things should be clear to you, a multiple thread application is capable to provide a much richer user experience as well as better performer, and the .Net Framework empower you to deliver such an application.  Hopefully thru this example you will see the benefits of using multi-threaded applications and GDI+, However, I didn't cover threading nor GDI+ in it entirety, I'll encourage you to read more about this topic in the .Net Framework Developers Specifications.  Additionally, I have used API to access the desktop, this could be a good tip for all of you interested in screen savers.  As a side note, the Ball class has commented the collision algorithm, I'll leave it as an exercise for you to make it work smoothly without major performance degradation.  Play with it and have fun!.  

Notes: Special thanks to Migui to  put up with this, to Marlenne for designing the cool balls, to Eli for the salsa music, Rick(Vini) for the encouragement to create this example and Mahesh for posting it.


Similar Articles