Click here to Skip to main content
12,243,180 members (21,392 online)
Click here to Skip to main content
Add your own
alternative version

Stats

185.7K views
582 downloads
144 bookmarked
Posted

Just another C# Collapsing Group Control

, 30 May 2004 CPOL
Rate this:
Please Sign up or sign in to vote.
An article on building an XP-style collapsing group box in C# with transparency.

Sample Image - XPGroupImage.gif

Introduction

I have been spending a lot of time writing business and data layer components for a number of different purposes lately - and to be honest, I was sick of looking at reams of code and no eye candy! So, I decided to turn my hand to a bit of control creation goodness.

I have put together a few UIs in VS.NET before, and I was a little disappointed by the lack of advanced XP-style controls in the toolbox. So I decided, I would try to create a version of the ubiquitous collapsing/expanding group-box you see in every XP explorer window. A brief search for code (Stealing from one source is plagiarism. Stealing from many sources is research - Laurendo Almeida) turned up a number of basic examples that had rectangular captions and "snapped" shut immediately, but none had all of the following:

  • A curved caption box
  • An animated collapse/expand (with that funky gradual collapse/expand that gives the feeling of mass to the control)
  • A gradual fade to/from transparent during the animation
  • A graduated fill left to right across the caption
  • A graduated fill from top left to bottom right within the group-box pane
  • A graduated fill from top to bottom for the group-box container
  • Full control of all color parameters
  • Design time support (properties and the ability to drop and arrange child controls)

The rest of the article represents the journey that has resulted in the controls shown above. I don't for a moment suggest that they are complete examples, but they are usable. Any tips on improvements are welcome! Smile | :)

Creating the code

As I sat down to design the group-box control (XPGroupBox) I soon realized that a collapsing XPGroupBox on its own is pretty useless - the neat thing is ther XPGroupBox moving in relation to the expansion/contraction and to do that, I needed a container - hence the XPGroupBoxContainer was born.

Both controls inherit from UserControl.

Using the code

The controls are contained within the DarenM.Controls assembly. Add the assembly to your project and compile, and the controls should appear in the toolbox's "My User Controls" area. Just drag the XPGroupContainer (dock it to the left if you like) and then drag XPGroupBox controls into it. You can then add child controls to the XPGroupBoxs as required.

XPGroupBox

This is actually the expanding/contracting group-box. There was a steep learning curve to move from my idea to having a control sitting on a form that expanded and contracted as I hoped.

XPGroupBox - Design-time support

In order to support design-time use of the control, I added properties:

[Description("Determines the radius of the curves 
   at the top-left and top-right of the control caption."), 
   DefaultValue(7),
   Category("Appearance")]
public int CaptionCurveRadius
{
   get { return captionCurveRadius; }
   set { captionCurveRadius = value; Invalidate(); }
}

I also added this code to specify that the UserControl used the ParentControlDesigner designer, allowing child controls to be dropped onto the XPGroupBox control.

/// <SUMMARY>
/// Summary description for XPGroupBox.
/// </SUMMARY>
[Designer("System.Windows.Forms.Design.ParentControlDesigner,System.Design", 
  typeof(System.ComponentModel.Design.IDesigner))]
public class XPGroupBox : System.Windows.Forms.UserControl
{
    ...

XPGroupBox - The caption box

As I mentioned above, I wanted my controls to have nice rounded captions (the radius for the curves are exposed by the property CaptionCurveRadius) and to look something like this:

Caption Image

In order to achieve this look, I needed to perform the following:

  • Enable transparent color support
  • Override the OnPaint event
  • Use a path to define the curved outline
  • Use a gradient brush to fill the path

In order to support all the transparency and to perform all the custom drawing I needed, I set the following styles in the constructor:

SetStyle(ControlStyles.ResizeRedraw, true);
SetStyle(ControlStyles.AllPaintingInWmPaint, true);
SetStyle(ControlStyles.UserPaint, true);
SetStyle(ControlStyles.DoubleBuffer, true);
SetStyle(ControlStyles.SupportsTransparentBackColor, true);
SetStyle(ControlStyles.ContainerControl, true);

this.BackColor = Color.Transparent;

To actually draw the caption box:

protected override void OnPaint(PaintEventArgs e)
{
   Rectangle rc = new Rectangle(0, 0, this.Width, captionHeight);
   LinearGradientBrush b = new LinearGradientBrush(rc, 
      captionLeftColor, captionRightColor, 
      LinearGradientMode.Horizontal);

   // Now draw the caption areas with the rounded corners at the top
   GraphicsPath path = new GraphicsPath();
   path.AddLine(captionCurveRadius, 0, this.Width - 
       (captionCurveRadius*2), 0);
   path.AddArc(this.Width - (captionCurveRadius*2)-1, 0, 
       (captionCurveRadius*2), (captionCurveRadius*2), 270, 90);
   path.AddLine(this.Width, captionCurveRadius, 
       this.Width-1 , captionHeight);
   path.AddLine(this.Width , captionHeight, 0, captionHeight);
   path.AddLine(0 , captionHeight, 0, captionCurveRadius);
   path.AddArc(0, 0, (captionCurveRadius*2), 
       (captionCurveRadius*2), 180, 90);
   
   // Remove jaggies
   e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
   
   // Smooooth fill
   e.Graphics.FillPath(b, path);

    ...

You may note the inclusion of e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; which removes jaggies from the curves.

Note: I have enhanced the caption to include a pseudo-button with chevrons:

Caption with pseudo button

I added this code to the OnPaint event handler:

// Draw the pseudo button indicating caption state
int psuedoButtonDiameter = 14;
Point psuedoButtonorigin = new 
     Point(this.Width - psuedoButtonDiameter - 5, 5);
Size psuedoButtonSize = new 
     Size(psuedoButtonDiameter, psuedoButtonDiameter);
Rectangle psuedoButtonRect = new 
     Rectangle(psuedoButtonorigin, psuedoButtonSize);
e.Graphics.DrawEllipse(new Pen(CaptionFontColor), psuedoButtonRect);
DrawChevrons(e.Graphics, psuedoButtonRect.X, 
     psuedoButtonRect.Y, psuedoButtonRect.Width/4);

I then added the following support methods:

private void DrawChevrons(Graphics g, int x, int y, int offset)
{
   if (expanded)
   {
      DrawChevron(g, x + offset + 1, y + 1*offset, -offset);
      DrawChevron(g, x + offset + 1, y + 2*offset, -offset);
   }
   else
   {
      DrawChevron(g, x + offset + 1, y + 2*offset, offset);
      DrawChevron(g, x + offset + 1, y + 3*offset, offset);
   }
}   

private void DrawChevron(Graphics g, int x, int y, int offset)
{
   Pen p;
   if (captionHighlighted)
   {
      p = new Pen(captionFontHighLightColor);
   }
   else
   {
      p = new Pen(CaptionFontColor);
   }
   Point[] points = { new Point(x, y),
                      new Point(x+Math.Abs(offset), y-offset),
                      new Point(x+2*Math.Abs(offset), y)
                    };
   g.DrawLines(p, points);
}

XPGroupBox - The pane

To draw the outline of the pane (body) for the control, I use the following code, again in the OnPaint method:

// Draw the outline around the work area if expanded
if ( this.Height > captionHeight)
{
   e.Graphics.DrawLine(new Pen(paneOutlineColor), 
      0, this.captionHeight, 0, this.Height);
   e.Graphics.DrawLine(new Pen(paneOutlineColor), 
      this.Width -1, this.captionHeight, this.Width-1, this.Height);
   e.Graphics.DrawLine(new Pen(paneOutlineColor), 
      0, this.Height - 1, this.Width-1 , this.Height - 1);
}

To fill in the background, I used OnBackgroundPaint:

protected override void OnPaintBackground(PaintEventArgs pevent)
{
   if (this.Height > captionHeight)
   {
      Rectangle rect = new Rectangle(0, captionHeight, 
                this.Width, this.Height - captionHeight); 

      LinearGradientBrush b = new LinearGradientBrush(rect, 
         paneTopLeftColor, paneBottomRightColor, 
         LinearGradientMode.ForwardDiagonal);

      pevent.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
      pevent.Graphics.FillRectangle(b, rect);
   }
}

XPGroupBox - collapsing/ expanding

In order to implement the "weighted" collapsing look that I wanted, I used a timer control and "slowly" (hey we are talking milliseconds here...) decreased the period. For each tick, I reduce the size of the control, increase the transparency, etc. This is the code that handles the tick:

private void timer1_Elapsed(object sender, 
                          System.Timers.ElapsedEventArgs e)
{
   // Initializes the transition delta
   if (transitionSizeDelta == 0)
   {
      transitionSizeDelta = 1;
   }

   // Reduces the interval between timer events - 
   // this gives the visual effect of the 
   // control slowly starting to collapse/expand then accelertaing
   if (timer1.Interval > 20)
   {
      timer1.Interval -= 20;
   }
   else
   {
      transitionSizeDelta+=2;
   }

   // Initialises the control transparency
   if (transitionAlphaChannel == 0)
   {
      transitionAlphaChannel = 10;
   }
   else
   {
      if ( transitionAlphaChannel + 10 < 255)
      {
         // Increase control transparency as it collapses
         transitionAlphaChannel += 10;
      }
   }

   switch (groupState)
   {
      case GroupState.Expanding:
         if ((this.Height + transitionSizeDelta)< controlHeight )
         {
            SetControlsOpacity(transitionAlphaChannel);
            paneBottomRightColor = 
                 Color.FromArgb(transitionAlphaChannel, 
                 paneBottomRightColor);
            paneTopLeftColor = 
                 Color.FromArgb(transitionAlphaChannel, 
                 paneTopLeftColor);
            paneOutlineColor = 
                 Color.FromArgb(transitionAlphaChannel, 
                 paneOutlineColor);
            this.Height += transitionSizeDelta;
            SetControlsVisible();
         }
         else
         {
            SetControlsOpacity(255);
            paneBottomRightColor = Color.FromArgb(255 , 
                                   paneBottomRightColor);
            paneTopLeftColor = Color.FromArgb(255 , 
                                   paneTopLeftColor);
            paneOutlineColor = Color.FromArgb(255 , 
                                   paneOutlineColor);
            transitionAlphaChannel = 0;
            this.Height = controlHeight;
            expanded = true;
            groupState = GroupState.Static;
            SetControlsVisible();
         }
         break;
      
      case GroupState.Collapsing:
         if ((this.Height - transitionSizeDelta) > captionHeight )
         {
            SetControlsOpacity(transitionAlphaChannel);
            this.Height -= transitionSizeDelta;
            paneBottomRightColor = Color.FromArgb(255 - 
                transitionAlphaChannel, paneBottomRightColor);
            paneTopLeftColor = Color.FromArgb(255 - 
                transitionAlphaChannel, paneTopLeftColor);
            paneOutlineColor = Color.FromArgb(255 - 
                transitionAlphaChannel, paneOutlineColor);
            SetControlsVisible();
         }
         else
         {
            transitionAlphaChannel = 0;
            SetControlsOpacity(0);
            paneBottomRightColor = Color.FromArgb(0, 
                                paneBottomRightColor);
            paneTopLeftColor = Color.FromArgb(0, 
                                paneTopLeftColor);
            paneOutlineColor = Color.FromArgb(0, 
                                paneOutlineColor);
            this.Height = captionHeight;
            expanded = false;
            groupState = GroupState.Static;
            SetControlsVisible();
         }
         break;
      
      case GroupState.Static:
         timer1.Enabled = false;
         transitionSizeDelta = 0;
         break;
   
      default:
         throw new 
            InvalidExpressionException(
            "groupState variable set to incorrect value");
   }

   Invalidate();  
   OnSizeChanging(new EventArgs());
}

You will note that the code fires an event on each tick - OnSizeChanging. The event is defined as:

// Defined outside the class
public delegate void SizeChangingHandler(Object sender, EventArgs e);

   ...
   
// Defined within the class
public event SizeChangingHandler SizeChanging;

protected virtual void OnSizeChanging(EventArgs e)
{
   // Only fires event if something is handling the event
   if (SizeChanging != null)
   {
      SizeChanging(this, e);
   }
}

   ...

The event is handled by the XPGroupBoxContainer to move other controls as it collapses/ expands.

XPGroupBoxContainer

The XPGroupBoxContainer is fairly simple (a lot less code than I imagined - .NET is a wonderful thing!), but there were a number of nasty issues I stumbled into and had to solve to get it working acceptably.

XPGroupBoxContainer - Design-time support

In order to support design-time use of the control, I added properties:

[Description("Determines the starting 
  (light) color of the pane gradient fill."), 
  Category("Appearance")]
public Color PaneTopLeftColor
{
   get { return paneTopLeftColor; }
   set { paneTopLeftColor = value; Invalidate(); }
}

I also added this code to specify that the UserControl used the ParentControlDesigner designer, allowing child controls to be dropped onto the XPGroupBoxContainer control.

/// <SUMMARY>
/// Summary description for XPGroupBoxContainer.
/// </SUMMARY>
[Designer("System.Windows.Forms.Design.ParentControlDesigner,System.Design", 
    typeof(System.ComponentModel.Design.IDesigner))]
public class XPGroupBoxContainer : System.Windows.Forms.UserControl

In order to auto-position the XPGroupBox controls when they are added, I implemented the following override:

protected override void OnControlAdded(ControlEventArgs e)
{
   base.OnControlAdded (e);
   if (e.Control is XPGroupBox)
   {
      RepositionControls();
      ((XPGroupBox)e.Control).SizeChanging += 
          new SizeChangingHandler(XPGroupBoxContainer_SizeChanging);
   }
   else
   {
      throw new InvalidOperationException("Can only add XPGroupBoxControls");
   }
}

Note that the XPGroupBoxContainer registers itself to handle each XPGroupBox OnSizeChanging event.

The following code is called to actually position the controls vertically within the control. Note that this method is also invoked in response to the OnSizeChanging event from any of the controls. You will also note the use of AutoScrollPosition.Y to provide the control positioning offset.

public void RepositionControls()
{
   int lastVerticalPosition = AutoScrollPosition.Y;
   foreach (Control c in this.Controls)
   {
      XPGroupBox xpg = c as XPGroupBox;
      if (xpg != null)
      {
         xpg.Left = groupBoxSpacing;
         xpg.Top = lastVerticalPosition + groupBoxSpacing;
         lastVerticalPosition += xpg.Height + groupBoxSpacing ;
         xpg.Width = this.Width - 2 * groupBoxSpacing - 16; 
      }
   }

}

XPGroupBoxContainer - painting and scrolling

The background painting of the gradient is virtually a replica of that used by the XpGroupBox, except that the gradient direction is vertical.

I ran into a lot of issues relating to positioning of controls on a scrolling control, performing gradient fills, etc. I have noted them in the section - Points of interest. The OnBackgroundPaint method below shows how I overcame the issues. Note the use of AutoScrollPosition and DisplayRectangle:

protected override void OnPaintBackground(PaintEventArgs pevent)
{
   //base.OnPaintBackground (pevent);
   Rectangle rect = new Rectangle(0, AutoScrollPosition.Y, this.Width, 
      this.Height); 
   //Rectangle rect = new Rectangle(0, 0, this.Width, this.Height);

   LinearGradientBrush b = new 
      LinearGradientBrush(this.DisplayRectangle, 
      paneTopLeftColor, paneBottomRightColor, 
      LinearGradientMode.Vertical);

   pevent.Graphics.FillRectangle(b, this.DisplayRectangle);
}

Points of interest

A list of problems I encountered and the solutions in the order I remember them, not the order they occurred:

  • Not using a transparent background (or enabling its support) during the early stages left false-background color displayed by my nice curves - very disappointing! Took me a while to figure out what was glaringly obvious - groan!
  • In order for the child controls to move up when the control collapses - remember to anchor them to the bottom!
  • When I first implemented collapsing (and remembered to anchor the controls to the bottom...), the child controls moved over the caption - UGLY! I spent a long time trying to see if I could offset the client area so that child controls would not draw over the caption. But I could not work out how to do this. (Any tips on this will be hugely appreciated!) I implemented a "kludge" that sets control.Visible = false; whenever control.TOP < captionHeight;. Not great, but it works!
  • I ran into a large number of problems associated with scrolling. The solutions to all my woes occurred once I realized a few fundamental truths about GDI+:
    • The drawing origin of the viewable area of any scrolled control is always (0,0) - i.e. the top-left of the ClientRectangle. If you mean to position a control at the top-left of the entire surface (the DisplayRectangle) you must add AutoScrollPosition.X and AutoScrollPosition.Y to the control's X,Y coordinates. I had a nasty case of scrollbar confusion until I sorted this out.
    • Perform the paint of the graduated fill background over the DisplayRectangle, as otherwise the fill looks naff and you get the incorrect background appearing on the "transparent" corners of the caption where the curves are.
    • Is it just me or do ClientRectangle and DisplayRectangle seem to be inappropriately named?
  • Occasionally, the visual designer loses child controls - why?!

History

  • Version 2.0 - 05/25/04
    • Substantial update!
    • Uploaded latest source.
    • Updated screenshot
  • Version 1.4 - 08/18/03
    • Uploaded latest source.
  • Version 1.3 - 08/08/03
    • Updated the document to include code for a pseudo-button I have added to the caption, which enhances the look (I think :P). Modified source projects will follow shortly.
  • Version 1.2 - 07/29/03
    • Uploaded new project containing fixes for:
      • Top-most control not being set to visible, after control expanded.
      • Multiple clicks on caption during expansion and collapse caused incorrect control height to be set.
  • Version 1.1 - 07/24/03
    • Fixed some typos.
  • Version 1.0 - 07/23/03
    • Initial version.

License

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

Share

About the Author

Daren May
Web Developer
United States United States
I have been working with software development for around 20 years, working with assembler, C, C++, Java and now C#.

You may also be interested in...

Comments and Discussions

 
GeneralMy vote of 1 Pin
Member 1030304910-Feb-14 13:12
memberMember 1030304910-Feb-14 13:12 
QuestionCan not expand XPGroupBox while adding new XPGroupBoxItem at run time and intially XPGroupBox was in Collaspe mode [modified] Pin
yuvraj.raj10-Feb-10 2:20
memberyuvraj.raj10-Feb-10 2:20 
GeneralGood Job Pin
Dariush Tasdighi21-Nov-08 7:40
memberDariush Tasdighi21-Nov-08 7:40 
Generalmouse over Pin
VogueDev23-Oct-08 0:32
memberVogueDev23-Oct-08 0:32 
GeneralFantastic Pin
VCKicks1-Oct-08 20:12
memberVCKicks1-Oct-08 20:12 
Generalbegin to debug run,a error throw ! Pin
zzjml16815-Jan-08 15:15
memberzzjml16815-Jan-08 15:15 
QuestionManual Add Item [modified] Pin
seasea24-Sep-06 22:29
memberseasea24-Sep-06 22:29 
QuestionHow about code in VB.NET 2.0 Pin
seasea28-Aug-06 18:50
memberseasea28-Aug-06 18:50 
AnswerRe: How about code in VB.NET 2.0 Pin
Ray Cassick28-Aug-06 19:31
memberRay Cassick28-Aug-06 19:31 
GeneralRe: How about code in VB.NET 2.0 Pin
seasea28-Aug-06 21:07
memberseasea28-Aug-06 21:07 
GeneralAdd Controls Pin
sbprogrammer2-Jun-06 6:21
membersbprogrammer2-Jun-06 6:21 
GeneralKeep Open Pin
sbprogrammer2-Jun-06 4:03
membersbprogrammer2-Jun-06 4:03 
AnswerRe: Keep Open Pin
Daren May2-Jun-06 5:25
memberDaren May2-Jun-06 5:25 
QuestionToolBox problem Pin
Ahmed.mb16-May-06 7:18
memberAhmed.mb16-May-06 7:18 
AnswerRe: ToolBox problem Pin
Daren May16-May-06 7:33
memberDaren May16-May-06 7:33 
GeneralRe: ToolBox problem Pin
Ahmed.mb17-May-06 4:54
memberAhmed.mb17-May-06 4:54 
GeneralAbout license Pin
Anonymous3-Oct-05 10:13
sussAnonymous3-Oct-05 10:13 
GeneralRe: About license Pin
Daren May14-Feb-06 8:05
memberDaren May14-Feb-06 8:05 
GeneralRe: About license Pin
balexis15-Sep-08 5:07
memberbalexis15-Sep-08 5:07 
GeneralTitle Pin
Arcadenut19-Oct-04 12:31
memberArcadenut19-Oct-04 12:31 
GeneralDisappearing child controls Pin
Rakker78929-Jun-04 11:53
sussRakker78929-Jun-04 11:53 
GeneralRe: Disappearing child controls Pin
Daren May29-Jun-04 16:02
memberDaren May29-Jun-04 16:02 
GeneralRe: Disappearing child controls Pin
Nicola Costantini8-Nov-06 22:42
memberNicola Costantini8-Nov-06 22:42 
GeneralControl has changed too much Pin
Wuzza1-Jun-04 2:16
memberWuzza1-Jun-04 2:16 
GeneralExcellent work! Pin
Palladino31-May-04 17:27
memberPalladino31-May-04 17:27 
GeneralInteresting Problem... Pin
Craig Eddy31-May-04 4:58
memberCraig Eddy31-May-04 4:58 
GeneralRe: Interesting Problem... Pin
Daren May31-May-04 11:37
memberDaren May31-May-04 11:37 
GeneralRe: Interesting Problem... Pin
Daren May31-May-04 13:23
memberDaren May31-May-04 13:23 
GeneralRe: Interesting Problem... Pin
Craig Eddy1-Jun-04 11:10
memberCraig Eddy1-Jun-04 11:10 
GeneralRe: Interesting Problem... Pin
Craig Eddy1-Jun-04 11:20
memberCraig Eddy1-Jun-04 11:20 
GeneralRe: Interesting Problem... Pin
Daren May1-Jun-04 12:25
memberDaren May1-Jun-04 12:25 
GeneralRe: Interesting Problem... Pin
Daren May1-Jun-04 12:50
memberDaren May1-Jun-04 12:50 
GeneralNew code submitted Pin
Daren May25-May-04 12:00
memberDaren May25-May-04 12:00 
GeneralOrder of group boxes Pin
tjrcon23-May-04 22:59
membertjrcon23-May-04 22:59 
GeneralRe: Order of group boxes Pin
Daren May24-May-04 6:28
memberDaren May24-May-04 6:28 
GeneralRe: Order of group boxes Pin
tjrcon24-May-04 9:54
membertjrcon24-May-04 9:54 
GeneralRe: Order of group boxes Pin
Daren May27-May-04 11:39
memberDaren May27-May-04 11:39 
GeneralRe: Large Update Coming! Pin
jordics12-May-04 6:38
memberjordics12-May-04 6:38 
GeneralRe: Large Update Coming! Pin
Daren May12-May-04 6:42
memberDaren May12-May-04 6:42 
GeneralRe: Large Update Coming! Pin
jordics20-May-04 8:23
memberjordics20-May-04 8:23 
GeneralRe: Large Update Coming! Pin
Daren May20-May-04 8:26
memberDaren May20-May-04 8:26 
GeneralRe: Large Update Coming! Pin
jogy3-Jun-04 1:46
memberjogy3-Jun-04 1:46 
GeneralRe: Large Update Coming! Pin
Daren May10-Jun-04 10:30
memberDaren May10-Jun-04 10:30 
GeneralRedraw problem with colapsed xpGroupBoxes Pin
mikaelsorensen7-May-04 5:42
membermikaelsorensen7-May-04 5:42 
GeneralRe: Redraw problem with colapsed xpGroupBoxes Pin
Daren May7-May-04 7:08
memberDaren May7-May-04 7:08 
GeneralRe: Redraw problem with colapsed xpGroupBoxes Pin
mikaelsorensen7-May-04 7:21
membermikaelsorensen7-May-04 7:21 
GeneralLarge Update Coming! Pin
Daren May15-Mar-04 11:55
memberDaren May15-Mar-04 11:55 
GeneralRe: Large Update Coming! Pin
meuri16-Mar-04 3:00
membermeuri16-Mar-04 3:00 
GeneralCollapse some Pin
meuri8-Jan-04 0:27
membermeuri8-Jan-04 0:27 
GeneralRe: Collapse some Pin
Daren May8-Jan-04 6:56
memberDaren May8-Jan-04 6:56 

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
Web01 | 2.8.160426.1 | Last Updated 31 May 2004
Article Copyright 2003 by Daren May
Everything else Copyright © CodeProject, 1999-2016
Layout: fixed | fluid