Click here to Skip to main content
Click here to Skip to main content

Just another C# Collapsing Group Control

By , 30 May 2004
Rate this:
Please Sign up or sign in to vote.

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)

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#.

Comments and Discussions

 
GeneralMy vote of 1 [modified] PinmemberMember 1030304910-Feb-14 12:12 
QuestionCan not expand XPGroupBox while adding new XPGroupBoxItem at run time and intially XPGroupBox was in Collaspe mode [modified] Pinmemberyuvraj.raj10-Feb-10 1:20 
GeneralGood Job PinmemberDariush Tasdighi21-Nov-08 6:40 
Generalmouse over PinmemberVogueDev22-Oct-08 23:32 
GeneralFantastic PinmemberVCKicks1-Oct-08 19:12 
Generalbegin to debug run,a error throw ! Pinmemberzzjml16815-Jan-08 14:15 
QuestionManual Add Item [modified] Pinmemberseasea24-Sep-06 21:29 
QuestionHow about code in VB.NET 2.0 Pinmemberseasea28-Aug-06 17:50 
AnswerRe: How about code in VB.NET 2.0 PinmemberRay Cassick28-Aug-06 18:31 
GeneralRe: How about code in VB.NET 2.0 Pinmemberseasea28-Aug-06 20:07 
GeneralAdd Controls Pinmembersbprogrammer2-Jun-06 5:21 
GeneralKeep Open Pinmembersbprogrammer2-Jun-06 3:03 
AnswerRe: Keep Open PinmemberDaren May2-Jun-06 4:25 
QuestionToolBox problem PinmemberAhmed.mb16-May-06 6:18 
AnswerRe: ToolBox problem PinmemberDaren May16-May-06 6:33 
GeneralRe: ToolBox problem PinmemberAhmed.mb17-May-06 3:54 
GeneralAbout license PinsussAnonymous3-Oct-05 9:13 
GeneralRe: About license PinmemberDaren May14-Feb-06 7:05 
GeneralRe: About license Pinmemberbalexis15-Sep-08 4:07 
GeneralTitle PinmemberArcadenut19-Oct-04 11:31 
GeneralDisappearing child controls PinsussRakker78929-Jun-04 10:53 
GeneralRe: Disappearing child controls PinmemberDaren May29-Jun-04 15:02 
GeneralRe: Disappearing child controls PinmemberNicola Costantini8-Nov-06 21:42 
GeneralControl has changed too much PinmemberWuzza1-Jun-04 1:16 
GeneralExcellent work! PinmemberPalladino31-May-04 16:27 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    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 | Mobile
Web01 | 2.8.140415.2 | Last Updated 31 May 2004
Article Copyright 2003 by Daren May
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid