Click here to Skip to main content
12,304,823 members (70,899 online)
Click here to Skip to main content
Add your own
alternative version

Stats

78.6K views
3.7K downloads
123 bookmarked
Posted

Expression Plotter Control

, 28 May 2008 CPOL
Rate this:
Please Sign up or sign in to vote.
A control that plots any number and any combination of mathematical expressions in rectangular or polar mode
A graph in rectangular mode A graph in polar mode

Table of Contents

Introduction

This article describes a graphing control that can be dropped into any Windows application to create scientific and technical graphs in a huge range of styles. The control plots any number of mathematical expressions in rectangular as well as polar mode. By "expression," I mean any combination of algebraic, trigonometric, exponential, logarithmic, hyperbolic or custom user-defined functions. There are various graphing controls at CodeProject (and outside), but almost all of them require us to provide a list of values (points). This control is unique in the way that it expects the user to provide an expression, e.g. 6*sin(5*x)*cos(8*x).

Features

The most important issue for every custom control is how extensible and customizable it is, and how easy it is to use in other Windows applications. Some salient features of this control are:

  • Multiple expressions with different colors
  • Rectangular as well as polar mode
  • High extensibility due to the IEvaluatable interface
  • Capability of zooming and scrolling the graph
  • Ability to save the current graph as an image
  • Capability to reverse axes

Public Interface

The public interface of the control is very handy. Various methods and properties are provided to provide a high degree of customization. Let us have a quick look at some selected methods and properties.

Methods

void AddExpression(IEvaluatable expression, Color color, bool visible) This function adds an expression to the graph. We will be looking at IEvaluatable later. color is the color of expression to be plotted while visible controls the visibility of the expression.
void SetRangeX(double StartX, double EndX) This sets the range for the X-axis. For example, use SetRangeX(-5,15) to construct a graph having x-axis starting from -5 and ending at 15
void SetRangeY(double StartY, double EndY) This sets the range for the Y-axis. For example, use SetRangeY(15,25) to construct a graph having y-axis starting from 15 and ending at 25
void ZoomIn() Zooms-in the graph. Similarly, we have functions ZoomInX() (for zooming X-axis only), ZoomInY (for zooming Y-axis only) and the corresponding functions for zoom-out (ZoomOut, ZoomOutX and ZoomOutY). Note that these functions automatically adjust their increasing/decreasing ratio, i.e., if we try to zoom-in/out while viewing a large scale graph, the zooming ratio is also large; and if we zoom a graph displayed for a short range, the zooming ratio is small.
void MoveLeft(int division) Scrolls the graph to left the number of divisions specified. In a similar fashion, we have functions like MoveRight(), MoveUp() and MoveDown().
Bitmap GetGraphBitmap() Returns a bitmap object for the current graph.
void CopyToClipboard() Copies the current graph to clipboard.
double[] GetValues(double point) Evaluates all the expressions at a given point and returns the result as an array.

Properties

ScaleX double Base scale for the X-axis, e.g. if ScaleX is 10 then the graph will be created from -10 to 10. If we supply a -ve value, the axis is reversed, i.e. supplying ScaleX=-10 draws graph from 10 to -10.
ForwardX double Controls navigation within the X-axis. Note that the graph will be drawn from -ScaleX+ForwardX to ScaleX+ForwardX, so if we have specified ScaleX=20 and ForwardX=0 then the graph will draw X-axis from -20 to +20. Similarly, if we have set ScaleX=20 with ForwardX=-10 then the graph will have X-axis from -30 to +10.
DivisionX int No of grid divisions for the X-axis.
PrintStepX int Step (increment) for printing the X-axis labels. Its range is from 0 to DivisionX. If set to 0, the graph does not display any labels; if set to DivisionX, the graph displays labels with each grid division.
GraphMode Enum Switches between rectangular and polar modes.
PolarSensitivity double Adjusts the sensitivity for polar graphs. The higher the value, the more accurate the polar graph.
DisplayText bool Sets whether to display expression text inside the graph.
Grids bool Turns on/off the grids.
PenWidth int Adjusts the pen width for drawing graphs.

Almost all these methods and properties are demonstrated in the application and the user interface of the demo project simply calls the respective methods of the control.

Using the Code

Before getting into implementation details of the control, we will have a look at its usage. The range for a graph can be controlled using properties (ScaleX, ForwardX, etc.) as well as functions (SetRange(), ZoomIn(), Move(), etc). Constructing a graph using methods is easy while that using properties is recommended for advanced users that want to have greater control.

Let's construct a simple graph using methods. After we have created all the necessary references and placed the control (named "expPlotter") on a Windows form, here's some code to experiment:

expPlotter.SetRangeX(-6, 14); //set the x-axis range from -6 to 14
expPlotter.SetRangeY(-5, 5); //set the y-axis scale from -5 to +5

expPlotter.DivisionsX = 5; //set no. of grid divisions
expPlotter.DivisionsY = 5; 
expPlotter.PenWidth = 2; //set pen width for graph

//now add some expressions
expPlotter.AddExpression((IEvaluatable)new Expression("-exp(x/2-3)"), 
    Color.Green, true);
expPlotter.AddExpression((IEvaluatable)new Expression("2*sin(x/2)*cos(3*x)"),
    Color.Blue, true);
expPlotter.AddExpression((IEvaluatable)new Expression("abs(x/2)"), 
    Color.Brown, true);

//we need to manually refresh our graph for the changes to take effect
expPlotter.Refresh();

The above code will produce the following output:

Example1

Let's reconstruct the same graph using properties, the more flexible approach.

expPlotter.ScaleX = 10; //set the base scale to -10 to +10

expPlotter.ForwardX = 4; 
//since the base scale was -10 to +10 and our ForwardX value is 4
// so now our graph will have x-axis range: -6 to 14

expPlotter.ScaleY = 5; //set the y-axis scale from -5 to 5
expPlotter.ForwardY = 0; //Y-axis origin at the center

expPlotter.DivisionsX = 5; //set no. of grid divisions
expPlotter.DivisionsY = 5;
expPlotter.PenWidth = 2; //set pen width for graph

//add expressions
expPlotter.AddExpression((IEvaluatable)new Expression("-exp(x/2-3)"), 
    Color.Green, true);
expPlotter.AddExpression((IEvaluatable)new Expression("2*sin(x/2)*cos(3*x)"),
    Color.Blue, true);
expPlotter.AddExpression((IEvaluatable)new Expression("abs(x/2)"), 
    Color.Brown, true);

expPlotter.Refresh(); //refresh the graph

The above code will produce the same output as the previous one. Similarly, other methods and properties can be used for further customization. Now we will have a look at the implementation of the control.

Implementation Details

ExpressionPlotterControl

Plotting in rectangular mode: The control internally stores all expressions in a List of IEvaluatable interface. We iterate through all these expressions (List<Evaluatable>) and start a loop from -ScaleX+ForwardX to ScaleX+ForwardX. For each value of the loop variable, we find the value of expression using IEvaluatable.Evaluate(loop variable) and plot it. For continuity in our graph, we join the previously evaluated value to the newly evaluated value.

Plotting in polar mode: Nothing special is done for handling polar modes since we can transform polar coordinates to rectangular coordinates by using the famous formulae: x=r*cos(theta) and y=r*sin(theta). We evaluate the expressions from -PI to +PI and plot the equivalent rectangular coordinates.

Here's the code for PlotGraph() method:

void PlotGraph(Graphics g)
{
    DisplayScale(g);
    if (this.bDisplayText)
        DisplayExpressionsText(g);

    double X, Y;
    double dPointX, dPointY;
    double dLeastStepX, dLeastStepY;
    double dMin, dMax, dStep;
    int i;

    //All the time, (X1,Y1) will be the previous plotted point, 
    //while (X2,Y2) will be the current point to plot. 
    //We will join both to have our graph continuous.
    float X1 = 0, Y1 = 0, X2 = 0, Y2 = 0;
    
    //This variable controls whether our graph should be continuous or not
    bool bContinuity = false;

    //divide scale with its length(pixels) to get increment per pixel
    dLeastStepX = dScaleX / iLengthScale;
    dLeastStepY = dScaleY / iLengthScale;

    //prepare variables for loop
    if (graphMode == GraphMode.Polar)
    {
        dMin = -Math.PI;
        dMax = Math.PI;
        dStep = dScaleX / iPolarSensitivity;
    }
    else //if (Rectangular Mode)
    {
        dStep = dLeastStepX;
        dMin = -dScaleX + dForwardX;
        dMax = dScaleX + dForwardX;
    }

    for (i = 0; i < this.expressions.Count; i++)
    {
        //check if expression needs to be drawn and is valid
        if (expVisible[i] == true && expressions[i].IsValid == true)
        {
            bContinuity = false;
            for (X = dMin; X != dMax; X += dStep)
            {
                if (dScaleX < 0 && X < dMax)
                    break;
                if (dScaleX > 0 && X > dMax)
                    break;
                try
                {
                    //evaluate expression[i] at point: X
                    Y = expressions[i].Evaluate(X);

                    if (double.IsNaN(Y))
                    {
                        //break continuity in graph if expression returned a NaN
                        bContinuity = false;
                        continue;
                    }

                    //get points to plot
                    if (graphMode == GraphMode.Polar)
                    {
                        dPointX = Y * Math.Cos(X) / dLeastStepX;
                        dPointY = Y * Math.Sin(X) / dLeastStepY;
                    }
                    else // if (Rectangular mode;
                    {
                        dPointX = X / dLeastStepX;
                        dPointY = Y / dLeastStepY;
                    }

                    //check if the point to be plotted lies 
                    //inside our visible area(i.e. inside our 
                    //current axes ranges)
                    if ((iOriginY - dPointY + dForwardY / 
                        dLeastStepY) < iOriginY - iLengthScale
                        || (iOriginY - dPointY + dForwardY / 
                        dLeastStepY) > iOriginY + iLengthScale
                        || (iOriginX + dPointX - dForwardX / 
                        dLeastStepX) < iOriginX - iLengthScale
                        || (iOriginX + dPointX - dForwardX / 
                        dLeastStepX) > iOriginX + iLengthScale)
                    {
                        //the point lies outside our current scale so 
                        //break continuity
                        bContinuity = false;
                        continue;
                    }

                    //get coordinates for currently evaluated point
                    X2 = (float)(iOriginX + dPointX - dForwardX / 
                        dLeastStepX);
                    Y2 = (float)(iOriginY - dPointY + dForwardY / 
                        dLeastStepY);

                    //if graph should not be continuous
                    if (bContinuity == false)
                    {
                        X1 = X2;
                        Y1 = Y2;
                        
                        // the graph should be continuous afterwards 
                        // since the current evaluated value is valid 
                        // and can be plotted within our axes range
                        bContinuity = true;
                    }

                    //join points (X1,Y1) and (X2,Y2)
                    g.DrawLine(new Pen(expColors[i], iPenWidth), 
                        new PointF(X1, Y1), new PointF(X2, Y2));

                    //get current values into X1,Y1
                    X1 = X2;
                    Y1 = Y2;
                }
                catch
                {
                    bContinuity = false;
                    continue;
                }
            }
        }
    }
}

IEvaluatable

IEvaluatable

The expression plotter control expects the expressions to implement IEvaluatable. This way, we can increase the extensibility of our control since we can write any class with custom evaluation behavior. We just need to provide a definition for the following:

  • string ExpressionText

    Get/Set the text of expression

  • bool IsValid

    Should return true if the expression can be evaluated without any exception

  • double Evaluate(double dvalueX)

    This function should evaluate the expression at dvalueX and return the result as double. If the result cannot be calculated (e.g. log for a -ve number) then it should return double.NaN.

Expression: IEvaluatable

The control contains a sample implementation of IEvaluatable, the Expression class. Let me briefly describe this implementation. The following pseudo-code can be used to evaluate a simple expression:

int runningTotal = 0;
Operator lastOperator = "+";
While ( Expression is not scanned )
{
  if Expression.Encountered( operand )
    runningTotal = runningTotal <lastOperator> operand;
  else if Expression.Encountered( operator )
    lastOperator = operator;
}

Let's see how the above code works on a sample expression 2*5+6-9:

Expression evaluation

However, the problem with this code is that it "always" evaluates left to right, "ignoring" operator precedences. Thus, 4+3*5 is evaluated as 35 instead of 19 because first 4+3=7 is evaluated and then multiplied by 5 to get 7*5=35. I solved this problem by inserting parenthesis at appropriate positions in the expression and evaluating parenthesis first, i.e., I converted 4+3*5 to 4+(3*5). InsertPrecedenceBrackets() is the function that does this stuff while the main function: EvaluateInternal(), calls itself recursively whenever it encounters a parenthesis. The DoAngleOperation() contains the definition of functions that the class supports.

Here's the code for EvaluateInternal():

public double EvaluateInternal(double dvalueX, 
    int startIndex, out int endIndex)
{
    //dAnswer is the running total
    double dAnswer = 0, dOperand = 0;
    char chCurrentChar, chOperator = '+';
    string strAngleOperator;

    for (int i = startIndex + 1; i < textInternal.Length; i++)
    {
        startIndex = i;
        chCurrentChar = textInternal[startIndex];

        // if found a number, update dOperand
        if (char.IsDigit(chCurrentChar))
        {
            while (char.IsDigit(textInternal[i]) || textInternal[i] == '.')
                i++;
            dOperand = 
                Convert.ToDouble(textInternal.Substring(startIndex, 
                i - startIndex));
            i--;
        }
        
        //if found an operator
        else if (IsOperator(chCurrentChar))
        {
            dAnswer = DoOperation(dAnswer, dOperand, chOperator);
            chOperator = chCurrentChar;
        }
        
        //if found independent variable
        else if (char.ToLower(chCurrentChar) == charX)
        {
            dOperand = dvalueX;
        }
        
        //if found a bracket, solve it first
        else if (chCurrentChar == '(')
        {
            dOperand = EvaluateInternal(dvalueX, i, out endIndex);
            i = endIndex;
        }
        
        //if found closing bracket, return result
        else if (chCurrentChar == ')')
        {
            dAnswer = DoOperation(dAnswer, dOperand, chOperator);
            endIndex = i;
            return dAnswer;
        }
        
        else //could be any function e.g. "sin" or any constant e.g "pi"
        {
            while (char.IsLetter(textInternal[i]))
                i++;
                
            //if we got letters followed by "(", we've got a 
            //function else a constant
            if (textInternal[i] == '(')
            {
                strAngleOperator = textInternal.Substring(startIndex, 
                    i - startIndex).ToLower();
                dOperand = EvaluateInternal(dvalueX, i, out endIndex);
                i = endIndex;
                dOperand = DoAngleOperation(dOperand, strAngleOperator);
            }
            else //constant
            {
                dOperand = this.constants[textInternal.Substring(startIndex, 
                    i - startIndex).ToLower()];
                i--;
            }
        }
        
        //return if we got a NaN
        if (double.IsNaN(dAnswer) || double.IsNaN(dOperand))
        {
            endIndex = i;
            return double.NaN;
        }
        
    }
    endIndex = textInternal.Length;
    return 0;
}

Also, here are few lines from DoAngleOperation():

//this function contains definitions for supported functions, 
//Ofcourse, we can add more here.
static double DoAngleOperation(double dOperand, string strOperator)
{
    strOperator = strOperator.ToLower();
    switch (strOperator)
    {
        case "abs":
            return Math.Abs(dOperand);
        case "sin":
            return Math.Sin(dOperand);
        case "arctan":
            return Math.Atan(dOperand);
        case "arcsinh":
            return Math.Log(dOperand + Math.Sqrt(dOperand * dOperand + 1));
        case "arccosh":
            return Math.Log(dOperand + Math.Sqrt(dOperand * dOperand - 1));
        case "MyCustomFunction":
            return MyFunctionsClass.MyCustomFunction(dOperand));
        :
        :
    }
}

Supported Functions

The current implementation of the Expression class contains definitions for abs (absolute), sin (trigonometric sine), cos (trigonometric cosine), tan (trigonometric tangent), sec (trigonometric secant), cosec (trigonometric cosecant), cot (trigonometric cotangent), arcsin (trigonometric sine inverse), arccos (trigonometric cosine inverse), arctan (trigonometric tangent inverse), exp (exponent), ln (natural logarithm), log (logarithm in base 10), antilog (antilog in base 10), sqrt (square root), sinh (hyperbolic sine), cosh (hyperbolic cosine), tanh (hyperbolic tangent), arcsinh (hyperbolic sine inverse), arccosh (hyperbolic cosine inverse) and arctanh (hyperbolic tangent inverse). As mentioned previously, this list can be extended by adding custom user-defined functions.

Point of Interest

Please note that the control does not redraw itself unless asked to do so. So, we manually need to call expPlotter.Refresh() to reflect our changes on the graph. This is done because redrawing the control is a time-consuming task; it involves re-evaluation of all the expressions and re-plotting of all the points. I first thought of an AutoRefresh property for controlling this, but later dropped this idea because most of the time we will be performing more than one operation before refreshing the graph. Please tell me your thoughts on this.

About the Demo Application: Graph Plotter

The application fairly demonstrates the usage of the control. Since the control provides a very handy interface, we just need to call the respective functions of the control to make a cool application. A quick look at the event handlers of the program can give us a nice idea of how to use the control. Resize the graph window to observe how the control adjusts itself for every size. Enter expressions and notice how the application provides assistance (e.g. text coloring, inserting * and parenthesis at appropriate places, etc) while entering expressions. I hope you will like this control. Happy graphing..

History

  • Version 1.0: Initial version
  • Version 1.1: The control can now have non-rectangular size (snapshot below)
A non-rectangular sized graph

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Syed Mehroz Alam
Software Developer
Pakistan Pakistan

Syed Mehroz Alam, living in Karachi, Pakistan, is a developer focusing Microsoft technologies. He has completed his bachelors as a Computer Systems Engineer in 2006 and is currently pursuing a Masters degree in Computer Science. He loves to learn, discover and master all aspects of .NET and SQL Server. Mehroz has developed rich internet enterprise applications using Silverlight in addition to the traditional ASP.NET and Windows Forms applications. He has worked with all three components of SQL Business Intelligence Studio: SSIS, SSRS and SSAS for developing BI Solutions and Data warehouse. He loves to write complex TSQL queries and evaluate his skills by participating in various TSQL Challenges. His blog can be viewed at http://smehrozalam.wordpress.com.


You may also be interested in...

Comments and Discussions

 
Questionhelp Pin
Tien-Sang12-Jan-14 3:26
professionalTien-Sang12-Jan-14 3:26 
GeneralDiscrete Points Pin
Archimedes2413-Mar-11 23:25
memberArchimedes2413-Mar-11 23:25 
GeneralRe: Discrete Points Pin
cdafg29-Aug-12 15:38
membercdafg29-Aug-12 15:38 
GeneralRe: Discrete Points Pin
Archimedes2429-Aug-12 21:54
memberArchimedes2429-Aug-12 21:54 
GeneralModification to expression evaluation Pin
Manfred R. Bihy4-Jan-11 9:41
memberManfred R. Bihy4-Jan-11 9:41 
GeneralRe: Modification to expression evaluation Pin
Syed Mehroz Alam31-Jan-11 22:52
memberSyed Mehroz Alam31-Jan-11 22:52 
GeneralMy vote of 5 Pin
Manfred R. Bihy4-Jan-11 9:35
memberManfred R. Bihy4-Jan-11 9:35 
GeneralRe: My vote of 5 Pin
Syed Mehroz Alam31-Jan-11 22:49
memberSyed Mehroz Alam31-Jan-11 22:49 
GeneralBackground Expression Plotter Control Pin
anh2toan27-Jun-10 22:38
memberanh2toan27-Jun-10 22:38 
GeneralRe: Background Expression Plotter Control Pin
Syed Mehroz Alam6-Jul-10 2:11
memberSyed Mehroz Alam6-Jul-10 2:11 
GeneralShade Pin
tovanvu24-Jun-10 19:15
membertovanvu24-Jun-10 19:15 
GeneralRe: Shade Pin
Syed Mehroz Alam6-Jul-10 2:12
memberSyed Mehroz Alam6-Jul-10 2:12 
GeneralSorry! x = f(y). not y = x * x ... Pin
tovanvu22-Jun-10 18:47
membertovanvu22-Jun-10 18:47 
GeneralRe: Sorry! x = f(y). not y = x * x ... Pin
Syed Mehroz Alam23-Jun-10 4:53
memberSyed Mehroz Alam23-Jun-10 4:53 
GeneralMy vote of 1 Pin
tovanvu121-Jun-10 1:44
membertovanvu121-Jun-10 1:44 
GeneralExpression Plotter Control with Expression y = x * x Pin
tovanvu20-Jun-10 18:30
membertovanvu20-Jun-10 18:30 
GeneralRe: Expression Plotter Control with Expression y = x * x Pin
Syed Mehroz Alam21-Jun-10 22:42
memberSyed Mehroz Alam21-Jun-10 22:42 
GeneralThanks Pin
sabitzhabit22-Dec-09 3:46
membersabitzhabit22-Dec-09 3:46 
GeneralRe: Thanks Pin
Syed Mehroz Alam21-Jun-10 22:42
memberSyed Mehroz Alam21-Jun-10 22:42 
GeneralAs regards efficiency of (re-)draws Pin
xian1233-Dec-09 1:04
memberxian1233-Dec-09 1:04 
GeneralRe: As regards efficiency of (re-)draws Pin
Syed Mehroz Alam15-Dec-09 2:39
memberSyed Mehroz Alam15-Dec-09 2:39 
GeneralRe: As regards efficiency of (re-)draws Pin
xian12318-Dec-09 20:21
memberxian12318-Dec-09 20:21 
GeneralInsertPrecedenceBrackets Pin
ncnlam22-Nov-09 17:19
memberncnlam22-Nov-09 17:19 
GeneralRe: InsertPrecedenceBrackets Pin
Syed Mehroz Alam22-Nov-09 22:37
memberSyed Mehroz Alam22-Nov-09 22:37 
GeneralRe: InsertPrecedenceBrackets Pin
ncnlam22-Nov-09 23:45
memberncnlam22-Nov-09 23:45 
GeneralRe: InsertPrecedenceBrackets Pin
Syed Mehroz Alam22-Nov-09 23:52
memberSyed Mehroz Alam22-Nov-09 23:52 
QuestionHow to use it from Visual C++ 2005? Pin
pepe_ltd21-Jul-08 13:00
memberpepe_ltd21-Jul-08 13:00 
GeneralBug when using floating numbers in expression Pin
Sir Pustekuchen12-Jun-08 3:23
memberSir Pustekuchen12-Jun-08 3:23 
GeneralRe: Bug when using floating numbers in expression Pin
Syed Mehroz Alam13-Jun-08 1:40
memberSyed Mehroz Alam13-Jun-08 1:40 
QuestionHow about to plug delegate [double Func(double)] to graph? Pin
alxxl3-Jun-08 1:57
memberalxxl3-Jun-08 1:57 
AnswerRe: How about to plug delegate [double Func(double)] to graph? Pin
Syed Mehroz Alam3-Jun-08 17:31
memberSyed Mehroz Alam3-Jun-08 17:31 
GeneralGood one Pin
Uro28-May-08 9:50
memberUro28-May-08 9:50 
GeneralRe: Good one Pin
Syed Mehroz Alam28-May-08 19:31
memberSyed Mehroz Alam28-May-08 19:31 
QuestionControl have the same width and height? Pin
Member 392746617-Mar-08 6:50
memberMember 392746617-Mar-08 6:50 
GeneralRe: Control have the same width and height? Pin
Syed Mehroz Alam17-Mar-08 23:29
memberSyed Mehroz Alam17-Mar-08 23:29 
AnswerRe: Control have the same width and height? Pin
Syed Mehroz Alam28-May-08 8:43
memberSyed Mehroz Alam28-May-08 8:43 
GeneralUnable to include the control in .NET 1.1 Pin
RajeevBhatt29-Sep-07 0:19
memberRajeevBhatt29-Sep-07 0:19 
GeneralRe: Unable to include the control in .NET 1.1 Pin
Syed Mehroz Alam29-Sep-07 4:07
memberSyed Mehroz Alam29-Sep-07 4:07 
GeneralGood work.... Pin
RajeevBhatt27-Sep-07 21:18
memberRajeevBhatt27-Sep-07 21:18 
Generalvery nice Pin
Sacha Barber24-Sep-07 23:47
memberSacha Barber24-Sep-07 23:47 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.160530.1 | Last Updated 28 May 2008
Article Copyright 2007 by Syed Mehroz Alam
Everything else Copyright © CodeProject, 1999-2016
Layout: fixed | fluid