The Grouper: Custom Groupbox Control

The Grouper is a special groupbox control that is rounded and fully customizable. The control can paint borders, drop shadows, gradient and solid backgrounds, custom text, and custom icons. The purpose of this control was to design a better looking groupbox winform control. I used the Keep it Simple Stupid philosophy to design a tightly knitted re-useable control that I hope you all enjoy. This control is still in beta version, but it seems to be stable.

Terms and Conditions For Use, Copy, Distribution, and Modification

THIS CODE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS CODE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Using the Code

Before you add the control, you must add a reference (CodeVendor.Controls) to your project.

using CodeVendor.Controls;

The grouper control can be added to your project with the following code. Add this code to your class:

private CodeVendor.Controls.Grouper Grouper1;
this.Grouper1 = new CodeVendor.Controls.Grouper();

Custom Control Properties

Below is a list of grouper control properties that change the physical appearance of the control.

Control Properties

Property Name Type Description
BackColor Color Paints the background color of the control.
BackgroundColor Color Changes the group control color. This color also can be used in combination with BackgroundGradientColor for a gradient paint.
BackgroundGradientColor Color Use in combination with BackgroundColor to create a gradient background.
BackgroundGradientMode GroupBoxGradientMode Turns on background gradient painting.
BorderColor Color This feature will allow you to change the color of the control's border.
BorderThickness Float Sets the control's border size.
CustomGroupBoxColor Color Paints the group title background to the specified color if PaintGroupBox is set to true.
GroupImage Image Adds a 16 x 16 image to the group title bar.
GroupTitle string Adds a group title to the control.
PaintGroupBox bool Paints the group title background to the CustomGroupBoxColor.
RoundCorners int Rounds the corners of the control.
ShadowColor Color Changes the control's shadow color.
ShadowControl bool Turns on control shadowing.
ShadowThickness int Changes the size of the shadow border.

Grouper Source Code

I started writing this control because I was tired of the plain old Windows groupbox. My winform application groupbox needed an updated XP look with round edges and gradient backgrounds. Like most developers writing controls, I thought to myself, "Should I custom draw the original groupbox control or re-invent the wheel and start from the ground up?" I chose to re-invent the wheel.

Re-inventing the wheel gave me the power to draw the control without flicker and customize it to my preferences. Taking the time to make a control from the ground up seems long and futile, but having the satisfaction of knowing that I wrote the code and if anything breaks it's because of something I wrote and not someone else's is fully worth it.

To start designing the control, I had to figure out how the original groupbox acts as a container for other controls during design mode. Following is the code to turn any control into a design-time container control.

[Designer("System.Windows.Forms.Design.ParentControlDesigner,
           System.Design", typeof(IDesigner))]

This code must be added on top of your class as an attribute to your control, like so:

[Designer("System.Windows.Forms.Design.ParentControlDesigner,
           System.Design", typeof(IDesigner))]
public class Grouper : System.Windows.Forms.UserControl { }

Once the attribute is added your control, the control will now automatically turn into a Design-Time Control Container. WOW! Very easy and self contained; Microsoft sure does think of everything. The URL I found for this resource is:

http://support.microsoft.com/default.aspx?scid=kb;en-us;813450

Since that took hardly any time at all to add such power to my control, I decided to invest more time into where the original groupbox left off, which was the design.

The Grouper: Custom Groupbox Control

Painting a Control Without Flicker

If you are painting your own custom control, you should think about "What is the best way to paint without Flicker?" To paint without flicker, you need a way to double buffer your drawing or graphics. Microsoft has given us this way by using Control Styles. The following code shows how to add double buffering to your control.

//Set the control styles----------------------------------
this.SetStyle(ControlStyles.DoubleBuffer,true;
this.SetStyle(ControlStyles.AllPaintingInWmPaint, true);
this.SetStyle(ControlStyles.UserPaint, true);
this.SetStyle(ControlStyles.SupportsTransparentBackColor, true);
//--------------------------------------------------------

This code should be added to the constructor method of your class. All of the styles are needed to create a flicker-free control. If you want to make a custom double buffer, you can create the same effect by drawing all graphics to an off-screen bitmap, and then drawing the bitmap to the screen all at once. This will create virtually the same effect as the code above. Also, notice this style:

this.SetStyle(ControlStyles.SupportsTransparentBackColor, true);

This style allows any control to support transparent colors. By default, usercontrols cannot support transparent colors. So, you must enable this to add transparency to your backcolor property. Continue on with the painting of your custom painting needed to override one of two methods, depending on the desired effect. The two methods are OnPaint() and OnPaintBackground(). Below is the code to override these methods.

protected override void OnPaint(PaintEventArgs e)
{
   base.OnPaint (e);
}

protected override void OnPaintBackground(PaintEventArgs pevent)
{
   base.OnPaintBackground (pevent);
}

For the grouper, you will use the OnPaint() method. The code is below:

/// <SUMMARY>Overrides the OnPaint method to paint
///          control.</SUMMARY>
protected override void OnPaint(PaintEventArgs e)
{
   PaintBack(e.Graphics);
   PaintGroupText(e.Graphics);
}

I receive the control's graphic object from the Paint Event Arguments and pass it to my two custom methods, PaintBack() and PaintGroupText().

Creating a Rounded Control

So, you want to create a rounded custom usercontrol? It is pretty simple to create a rounded control; a bit of help from GDI+ will show you how it works.

/// <SUMMARY>This method will paint the control.</SUMMARY>
private void PaintBack(System.Drawing.Graphics g)
{
   //Set Graphics smoothing mode to Anit-Alias--
   g.SmoothingMode = SmoothingMode.AntiAlias;
   //-------------------------------------------
   //Declare Variables------------------
   int ArcWidth  = this.RoundCorners * 2;
   int ArcHeight = this.RoundCorners * 2;
   int ArcX1 = 0;
   int ArcX2 = (this.ShadowControl) 
             ? (this.Width - (ArcWidth + 1))
             - this.ShadowThickness this.Width
             - (ArcWidth + 1); int ArcY1 = 10;
   int ArcY2 = (this.ShadowControl)
             ? (this.Height - (ArcHeight + 1))
             - this.ShadowThickness : this.Height
             - (ArcHeight + 1);
   System.Drawing.Drawing2D.GraphicsPath path =
      new System.Drawing.Drawing2D.GraphicsPath();

   System.Drawing.Brush BorderBrush =
      new SolidBrush(this.BorderColor);
   System.Drawing.Pen BorderPen =
      new Pen(BorderBrush, this.BorderThickness);
   System.Drawing.Drawing2D.LinearGradientBrush
      BackgroundGradientBrush = null;
   System.Drawing.Brush BackgroundBrush =
      new SolidBrush(this.BackgroundColor);
   System.Drawing.SolidBrush ShadowBrush = null;
   System.Drawing.Drawing2D.GraphicsPath ShadowPath = null;
   //-----------------------------------

   // Check if shadow is needed----------
   if(this.ShadowControl)
   {
      ShadowBrush = new SolidBrush(this.ShadowColor);
      ShadowPath  = new System.Drawing.Drawing2D.GraphicsPath();
      // Top Left
      ShadowPath.AddArc(ArcX1+this.ShadowThickness,
                        ArcY1+this.ShadowThickness,
                        ArcWidth, ArcHeight, 180,
                        GroupBoxConstants.SweepAngle);
      //Top Right
      ShadowPath.AddArc(ArcX2+this.ShadowThickness,
                        ArcY1+this.ShadowThickness,
                        ArcWidth, ArcHeight, 270,
                        GroupBoxConstants.SweepAngle);
      //Bottom Right
      ShadowPath.AddArc(ArcX2+this.ShadowThickness,
                        ArcY2+this.ShadowThickness,
                        ArcWidth, ArcHeight, 360,
                        GroupBoxConstants.SweepAngle);
      //Bottom Left
      ShadowPath.AddArc(ArcX1+this.ShadowThickness,
                        ArcY2+this.ShadowThickness,
                        ArcWidth, ArcHeight, 90,
                        GroupBoxConstants.SweepAngle);
      ShadowPath.CloseAllFigures();

      //Paint Rounded Rectangle------------
      g.FillPath(ShadowBrush, ShadowPath);
      //-----------------------------------
   }
   //-----------------------------------

   //Create Rounded Rectangle Path------
   // Top Left
   path.AddArc(ArcX1, ArcY1, ArcWidth, ArcHeight, 180,
               GroupBoxConstants.SweepAngle);
   //Top Right
   path.AddArc(ArcX2, ArcY1, ArcWidth, ArcHeight, 270,
               GroupBoxConstants.SweepAngle);
   //Bottom Right
   path.AddArc(ArcX2, ArcY2, ArcWidth, ArcHeight, 360,
               GroupBoxConstants.SweepAngle);
   //Bottom Left
   path.AddArc(ArcX1, ArcY2, ArcWidth, ArcHeight, 90,
               GroupBoxConstants.SweepAngle);
   path.CloseAllFigures();
   //-----------------------------------

   //Check if Gradient Mode is enabled--
   if(this.BackgroundGradientMode==GroupBoxGradientMode.None)

   {
      //Paint Rounded Rectangle------------
      g.FillPath(BackgroundBrush, path);
      //-----------------------------------
   }
   else
   {
      BackgroundGradientBrush =
         new LinearGradientBrush(new Rectangle(0,0,this.Width,
            this.Height), this.BackgroundColor,
            this.BackgroundGradientColor,
            (LinearGradientMode)this.BackgroundGradientMode);

      //Paint Rounded Rectangle----------
      g.FillPath(BackgroundGradientBrush, path);
      //-----------------------------------
   }
   //-----------------------------------

   //Paint Border-----------------------
   g.DrawPath(BorderPen, path);
   //-----------------------------------

   //Destroy Graphic Objects------------
   if(path!=null){path.Dispose();}
      if(BorderBrush!=null){BorderBrush.Dispose();}
   if(BorderPen!=null) { BorderPen.Dispose(); }
   if(BackgroundGradientBrush!=null)
      {BackgroundGradientBrush.Dispose();}
   if(BackgroundBrush!=null){BackgroundBrush.Dispose();}
   if(ShadowBrush!=null){ShadowBrush.Dispose();}
   if(ShadowPath!=null){ShadowPath.Dispose();}
   //-----------------------------------
}

The PaintBack() method above uses the GraphicsPath() method from the System.Drawing.Drawing2D class to create four arcs starting in the order of Top Left, Top Right, Bottom Right, and Bottom Left. The arcs need to be set to TopLeft: 180, TopRight: 270, BottomRight: 360, and BottomLeft: 90, with a sweep angle of 90. The CloseAllFigures() method of the GraphicsPath closes all of these arcs together for you, creating a rounded rectangle path. All that is left to do is to fill your path, using the FillPath() method. You can also choose from a solid brush or linear gradient brush for the background of the groupbox. To draw a border around the control, I use the DrawPath() method of the graphics library. By passing the same path to this method with a graphics pen, you can create a line border that follows the same path.

Disposing of Graphic Objects!

So many coders—including myself—forget to dispose() of their graphics brushes, pens, paths, colors, and so forth. Leaving unmanaged resources open can cause memory leaks and will eventually slow your machine to a dead stop. Make sure you follow these common rules whenever you use the graphics library:

  1. Dispose of all graphic objects by calling their dispose() methods.
  2. Make sure to check that your graphic objects are not null before disposing of them.
  3. If you have common graphic objects among multiple methods, think about declaring them as class variables or constants.
  4. Don't rely on the garbage collector to auto collect your used graphic resources.

Control Improvements Needed

  1. The group title bar does not size properly to an increased font size.
  2. The control needs to be able to shrink to a minimum height and width, without drawing over itself.
  3. The group title bar needs to be able to accept icons bigger than 16 x 16, and expand to the appropriate size.
  4. Some fonts have an issue reporting exact sizes when measured with the measurestring() method.
  5. Add an option to minimize groupbox, and a user button to minimize.

Please let me know about any improvements or features you would like to see with the 1.0 version of this control.

History

Version 1.0a Beta: December 17, 2005

Awards



About the Author

Adam Smith

Visit my website http://www.codevendor.com for all my latest new code releases and tutorials.

Downloads

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • As everyone scrambles to protect customers and consumers from the Heartbleed virus, there will be a variety of mitigating solutions offered up to address this pesky bug. There are a variety of points within the data path where solutions could be put into place to mitigate this (and similar) vulnerabilities and customers must choose the most strategic point in the network at which to deploy their selected mitigation. Read this white paper to learn the ins and outs of mitigating the risk of Heartbleed and the …

  • 10 Rules that Make or Break Enterprise App Development Projects In today's app-driven world, application development is a top priority. Even so, 68% of enterprise application delivery projects fail. Designing and building applications that pay for themselves and adapt to future needs is incredibly difficult. Executing one successful project is lucky, but making it a repeatable process and strategic advantage? That's where the money is. With help from our most experienced project leads and software engineers, …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds