The Amazing GroupBox, Part 1

The GroupBox, as you know it, has a specific function, and that is to group several objects together, so that you only have to manipulate one object. For example, to make the items inside the Groupbox invisible, you just need to set the GroupBox's Visible property to False, instead of making each child object invisible one by one. Because I'm me, I asked myself: What if I spice up the Groupbox a bit? That simple question led me towards creating my own GroupBox control, with which you can properly manipulate the title of the GroupBox (in other words, Alignment options which aren't present in the ordinary Groupbox), as well as adding a Scrollbar to the Groupbox, so that you can add more items into it, without sizing it too big. In this, the first part of this series, you will concentrate on creating the shape of the GroupBox, and all the new funky GroupBox title features.

Your Project

  1. Open Visual Studio 2005.
  2. Start a New Visual Basic.NET Windows Forms Project.
  3. You can name the project anything you like. I've named mine HTG_Group.
  4. Once the Form designer is shown, select the Project menu, and add a UserControl.
  5. You can name the form and the UserControl anything you like. Mine are named frmTest and HTG_Group respectively.
  6. Open the UserControl's designer if necessary, Resize it to 230, 264.
  7. Add the following controls with their associated properties to the UserControl:
  8. Control Property Value
    Label Name lblGBHead
      BackColor Control
      Size 230, 66
      Location 0, 0
    Panel Name pnlGBBody
      BackColor LightGray
      Dock Bottom
      Location 0, 50
      Size 230, 214

Allowing the GroupBox to be Transparent

As you may or may not know, you cannot set a User control's BackColor to Transparent because it is not immediately supported. You have to enable this capability, and the best place to do this is in the UserControl's Constructor. Follow the next few steps carefully:

  1. In the Solution Explorer, click the Show All Files Button. This will display the UserControl's Designer file, as well as its resource file.
  2. Click the Designer file, and select the View Code button in the Solution Explorer.
  3. Once inside the Designer file's code, you may not see the Constructor immediately. You should select New from the Method Name drop-down list.
  4. Under the call to InitializeComponent, add the following:
  5. 'Enable Transparent BackColour property
       SetStyle(ControlStyles.SupportsTransparentBackColor, True)
       Me.BackColor = Color.Transparent    'Set BackColour
    

Shaping the UserControl

If you look at an ordinary GroupBox, you would note that its top two corners are rounded whereas the bottom corners are not. With your GroupBox, you will have a complete Rounded rectangle shape, which looks a bit prettier. Before you create the shape, you need to have two variables (one for the width, and one for the height), so you should declare and initialize them now.

Private gbHeight As Integer    'Height
Private gbWidth As Integer     'Width

Initialize these variables inside the Constructor:

gbHeight = Me.Height
gbWidth  = Me.Width

Remember to Import the System.Drawing.Drawing2D Namespace:

Imports System.Drawing.Drawing2D    'Drawing Functions

Now, create the Round Rectangle function:

''' <summary>
''' Creates The Round rectangular Shape Of GroupBox
''' </summary>
Private Function gbRoundRect(ByVal gbX As Integer, )
   ByVal gbY As Integer, ByVal gbWidth As Single, _
   ByVal gbHeight As Single, ByVal gbRadius As Integer, _
   ByVal gbLineCol As Color, ByVal gbFillCol As Color, _
   ByVal gbGraphics As System.Drawing.Graphics) As _
      System.Drawing.Graphics


   Dim gbPen As New System.Drawing.Pen(gbLineCol, 5)
   Dim gbBrush As New System.Drawing.SolidBrush(gbFillCol)

   Dim gbPath As System.Drawing.Drawing2D.GraphicsPath = _
      New System.Drawing.Drawing2D.GraphicsPath
   If gbRadius > gbWidth / 2 OrElse gbRadius > gbHeight / 2 _
      Then gbRadius = gbHeight / 2

   gbPath.StartFigure()

   gbPath.AddArc(gbX, gbY, gbRadius * 2, gbRadius * 2, 180, 90)
   gbPath.AddArc(gbX + gbWidth - gbRadius * 2, gbY, gbRadius * 2, _
                 gbRadius * 2, 270, 90)
   gbPath.AddArc(gbX + gbWidth - gbRadius * 2, gbY + gbHeight - _
                 gbRadius * 2, _ gbRadius * 2, gbRadius * 2, 0, 90)
   gbPath.AddArc(gbX, gbY + gbHeight - gbRadius * 2, _
                 gbRadius * 2, gbRadius * 2, 90, 90)
   gbPath.CloseFigure()

   gbGraphics.FillPath(gbBrush, gbPath)
   gbGraphics.DrawPath(gbPen, gbPath)
   Me.Region = New Region(gbPath)
   Return gbGraphics

End Function

This function returns your rounded rectangular shape. All that is left is to call this function within your UserControl's resize event:

Private Sub HTG_Group_Resize(ByVal sender As Object, _
   ByVal e As System.EventArgs) Handles Me.Resize
   gbWidth = Me.Width
   gbHeight = Me.Height

   Dim gbGraphics As Graphics
   gbGraphics = Graphics.FromHwnd(Me.Handle)

   gbRoundRect(0, 0, gbWidth, gbHeight, 15, Color.Black, _
      Color.Transparent, gbGraphics)
   Me.Invalidate()
End Sub

In the Resize event, you just set the value of the gbWidth and gbHeight variables to the UserControl Width and Height, so that, as you resize, their values update as well. Then, you created a Graphics object to assist you in drawing the shape of your UserControl, and call the gbRoundRect function.

Build your project now, and if there aren't any problems, you now can safely continue to add a Border to the UserControl.

The Amazing GroupBox, Part 1

Adding a UserControl Border

This is another capability that is not present with UserControls. Don't get me wrong; I'm not saying it is a bad thing. You have to remember the whole function of a UserControl, and that, as you should perhaps know by now, is to make your own customized controls for a specific need. If you are curious enough, like me, you can indeed add a Border to a UserControl. Now, don't think I'm adding a Form—like BorderStyle (with Close, Maximize, Minimize buttons) here. What I'm aiming to do here is to add a border capability similar to what a Label control has.

What you need to do, to add the border to our UserControl, is to override the CreateParams property of the UserControl, to allow you to add a Fixed3D or FixedSingle border. Before you get there though, you need to add the Border API Constants.

Add the following API constants

Private Const WS_EX_CLIENTEDGE = &H200       'Edge Of UserControl
Private Const WS_BORDER        = &H800000    'Border API Constant

You need to Import the System.ComponentModel Namespace, so that if something goes wrong, it can be picked up:

Imports System.ComponentModel    'Component Properties

This allows you to add an edge around the UserControl, as well as to set a Border style for our UserControl. Now, add the overridden CreateParams property:

'Create The Border Parameters
Protected Overrides ReadOnly Property CreateParams() As _
   System.Windows.Forms.CreateParams
   Get
   'Create
      Dim gbParam As System.Windows.Forms.CreateParams = 
      MyBase.CreateParams

      'Set Edge
      gbParam.ExStyle = gbParam.ExStyle And (Not WS_EX_CLIENTEDGE)
      'Set Border
      gbParam.Style = gbParam.Style And (Not WS_BORDER)

      Select Case BorderStyle            'Determine BorderStyle
         Case BorderStyle.Fixed3D        'Fixed 3D
            gbParam.ExStyle = gbParam.ExStyle Or WS_EX_CLIENTEDGE
         Case BorderStyle.FixedSingle    'Fixed Single
            gbParam.Style = gbParam.Style Or WS_BORDER
      End Select

      Return gbParam    'Return Border
   End Get
End Property;

Because of this, you now can safely add a BorderStyle property:

'BorderStyle Property
Public Property BorderStyle() As System.Windows.Forms.BorderStyle
   Get
      Return gbBorderStyle    'Return BorderStyle
   End Get

   Set(ByVal Value As System.Windows.Forms.BorderStyle)
      If (Not (gbBorderStyle = Value)) Then
         'Convert Current Selected Value To BorderStyle
         If (Not ([Enum].IsDefined(GetType(BorderStyle), Value))) Then
            Throw New InvalidEnumArgumentException("value", _
               CType(Value, Integer), GetType(BorderStyle))
         End If
         gbBorderStyle = Value
         UpdateStyles()    'Update UserControl
      End If
   End Set
End Property

And, add a variable holding the value of this property:

'Border To Be Applied
Private gbBorderStyle As System.Windows.Forms.BorderStyle

Because the System.Windows.Forms.BorderStyle is an enum, you need to cast the value you supply to your BorderStyle property appropriately, so that the value you supply will indeed be a border that can be applied. The call to UpdateStyles is used to update the Border setting change on the user control.

If you were to build your project, and add your Groupbox to a form, you will see that you are able to set a borderstyle property.

The basic framework for your UserControl is now complete.

The Amazing GroupBox, Part 1

Adding a Heading to Your GroupBox

Okay, I have a bad habit of always going overboard with stuff, so, apart from adding alignment options, I decided it would be funky to have a text Shadow capability as well. All this, and more you are going to cover now. First, add all the necessary variables to hold each Property's value.

The variables

Private gbHeadFont As Font = New Font("Arial", 12, FontStyle.Bold, _
   GraphicsUnit.Pixel)                              'Head Font
Private gbHeadLoc As Point = New Point(0, 0)        'Head Location
Private gbHeadAlign As gbEAlign = gbEAlign.Left     'Head Alignment
Private gbHeadForeCol As Color = Color.Red          'Head ForeColour
Private gbHeadBackCol As Color = Color.Beige        'Head BackColour
'Head Border
Private gbHeadBorderStyle As System.Windows.Forms.BorderStyle
Private gbHeadVis As gbEVis = gbEVis.True           'Head Visibility
Private gbHeadText As String = "Testing"            'Head Text
Private gbHeadShadow As gbEShadow = gbEShadow.True  'Shadow
Private gbHeadShadowCol As Color = Color.Beige      'Shadow Colour
Private gbHeadShadowSpace As Short = 3              'Shadow Spacing
Private gbHeadStrSize As New SizeF(gbWidth + 5, 30) 'Size Of Head Text
Private gbSize As New SizeF(Me.width, Me.height)    'GroupBox Size

Public Enum gbEShadow            'Shadow Settings
   [True]  = 0
   [False] = 1
End Enum

Public Enum gbEVis               'Visibility Settings
   [True]  = 0
   [False] = 1
End Enum

Public Enum gbEAlign As Short    'Alignment Options
   Left   = 0
   Center = 1
   Right  = 2

End Enum

Here, you created variables and default values for the Heading's Font, Location, Alignment, Fore - and BackColours, Borderstyle, Visibility, Text, StringSize, as well as all the necessary default values for the heading's Shadow.

The properties

Now, create these variables' associated properties:

Public Property HeadFont() As Font    'Header Font
   Get
      Return gbHeadFont
   End Get
   Set(ByVal value As Font)
      gbHeadFont = value
      lblGBHead.Font = gbHeadFont
      lblGBHead.Invalidate()
   End Set
End Property

Public Property HeadLocation() As Point    'Header Location
   Get
      Return gbHeadLoc
   End Get
   Set(ByVal value As Point)
      gbHeadLoc = value
      lblGBHead.Location = New Point(value)
      lblGBHead.Invalidate()
   End Set
End Property

Public Property HeadAlign() As gbEAlign    'Header Alignment
   Get
      Return gbHeadAlign
   End Get
   Set(ByVal value As gbEAlign)
      gbHeadAlign = value
      lblGBHead.Location = New Point(0, 0)
      lblGBHead.Invalidate()

   End Set
End Property

Public Property HeadForeColour() As Color    'Header ForeColour
   Get
      Return gbHeadForeCol
   End Get
   Set(ByVal value As Color)
      gbHeadForeCol = value
      lblGBHead.ForeColor = gbHeadForeCol
      lblGBHead.Invalidate()
   End Set
End Property

Public Property HeadBackColour() As Color    'Header BackColor
   Get
      Return gbHeadBackCol
   End Get
   Set(ByVal value As Color)
      gbHeadBackCol = value
      lblGBHead.BackColor = gbHeadBackCol
      lblGBHead.Invalidate()
   End Set
End Property

'Header BorderStyle
Public Property HeadBorderStyle() As System.Windows.Forms.BorderStyle
   Get
      Return gbHeadBorderStyle
   End Get
   Set(ByVal value As System.Windows.Forms.BorderStyle)
      gbHeadBorderStyle = value
      lblGBHead.BorderStyle = gbHeadBorderStyle
      lblGBHead.Invalidate()
   End Set
End Property

Public Property HeadVisible() As gbEVis    'Header Visibility
   Get
      Return gbHeadVis
   End Get
   Set(ByVal value As gbEVis)
      gbHeadVis = value
      Select Case gbHeadVis
         Case 0
            lblGBHead.Visible = True
         Case 1
            lblGBHead.Visible = False
      End Select
   End Set
End Property

Public Property HeadText() As String    'Header Text
   Get
      Return gbHeadText
   End Get
   Set(ByVal value As String)
      gbHeadText = value
      lblGBHead.Invalidate()

   End Set
End Property

Public Property HeadShadow() As gbEShadow    'Header Shadow
   Get
      Return gbHeadShadow
   End Get
   Set(ByVal value As gbEShadow)
      gbHeadShadow = value
      lblGBHead.Invalidate()
   End Set
End Property

Public Property HeadShadowColour() As Color    'Shadow Colour
   Get
      Return gbHeadShadowCol
   End Get
   Set(ByVal value As Color)
      gbHeadShadowCol = value
      lblGBHead.Invalidate()
   End Set
End Property

Public Property HeadShadowSpacing() As Short    'Shadow Spacing
   Get
      Return gbHeadShadowSpace
   End Get
   Set(ByVal value As Short)
      gbHeadShadowSpace = value
      lblGBHead.Invalidate()
   End Set
End Property

The Amazing GroupBox, Part 1

Most of these properties are quite self-explanatory. What you need to realise is that, when dealing with properties that update/change the appearance of a UserControl, you need to use Invalidate to force a repaint of that control. You will notice that, with each of these properties, I have added Invalidate because of this. The Shadow properties are quite interesting. My plan with the Shadow was to allow you to change its colour, but most importantly, its location; this is dealt with in the HeadShadowSpacing property. If you give this a positive value, it will shift left and a bit down. If we supply a negative value, it will shift right and a bit up. To get the shadow of the existing Head Text, you need to add the following function:

''' <summary>
''' Retrieves Existing Text, And Add A Shadow
''' </summary>
Private Function gbShadowPath(ByVal gbStr As String, _
   ByVal gbDPI As Single, _
   ByVal gbShadowRect As RectangleF, ByVal gbSFont As Font, _
   ByVal gbSFormat As StringFormat) As GraphicsPath

Dim ShadowPath As GraphicsPath = New GraphicsPath()
   Dim gbSSize As Single = gbDPI * gbSFont.SizeInPoints / 72
   ShadowPath.AddString(gbStr, gbSFont.FontFamily,_
       CInt(gbSFont.Style), gbSSize, gbShadowRect, gbSFormat)
   Return ShadowPath
End Function

Because of all these changes, and mainly because of you needing the size of the Heading Text, you need to edit your existing resize event as follows:

Private Sub HTG_Group_Resize(ByVal sender As Object, _
   ByVal e As System.EventArgs) Handles Me.Resize

   gbWidth         = Me.Width
   gbHeight        = Me.Height
   lblGBHead.Width = gbWidth
   pnlGBBody.Width = gbWidth

   pnlGBBody.Height = gbHeight - gbHeadStrSize.Height
   pnlGBBody.Top = gbHeadStrSize.Height - 2

   If Me.Width < gbHeadStrSize.Width Then
      Me.Width = gbHeadStrSize.Width + 2
   End If

   If Me.Height < gbHeadStrSize.Height Then
      Me.Height = gbSize.Height
   End If
   Dim gbGraphics As Graphics
   gbGraphics = Graphics.FromHwnd(Me.Handle)

   gbRoundRect(0, 0, gbWidth, gbHeight, 15, Color.Black, _
      Color.Transparent, gbGraphics)
   Me.Invalidate()
End Sub

This prevents you from resizing your control, in height and width, so that the full heading text is always visible. If you size the UserControl to small, it will quickly jump back so that you can see the full heading.

All that is left now is to add a Paint event for the Heading label:

Private Sub lblGBHead_Paint(ByVal sender As Object, _
   ByVal e As System.Windows.Forms.PaintEventArgs) _
   Handles lblGBHead.Paint
   If lblGBHead.BackColor <> gbHeadBackCol Then
      Me.HeadBackColour = gbHeadBackCol
   End If
   If gbHeadText <> String.Empty Then gbHeadStrSize = _
      e.Graphics.MeasureString(gbHeadText, gbHeadFont)

   If gbHeadShadow = gbEShadow.True Then
      Dim gbShadowBrush As New SolidBrush(gbHeadShadowCol)
      Dim gbHeadBrush As New SolidBrush(gbHeadForeCol)
      Dim gbAlign As New System.Drawing.StringFormat

      Select Case gbHeadAlign
         Case gbEAlign.Left
            gbAlign.Alignment = StringAlignment.Near
         Case gbEAlign.Center
            gbAlign.Alignment = StringAlignment.Center
         Case gbEAlign.Right
            gbAlign.Alignment = StringAlignment.Far
      End Select
      Dim gGB As Graphics = e.Graphics
      Dim gbSize As SizeF = New SizeF(Me.ClientRectangle.Width - _
         gbHeadShadowSpace, _
         Me.ClientRectangle.Height - gbHeadShadowSpace)

      Dim gbRect As RectangleF = New RectangleF(0, 0, gbSize.Width, _
         gbSize.Height)

      Dim gbpShadow As GraphicsPath = gbShadowPath(gbHeadText, _
         gGB.DpiY, _
         New RectangleF(gbHeadShadowSpace, gbHeadShadowSpace, _
            gbSize.Width, gbSize.Height), gbHeadFont, gbAlign)
      Dim gbpLabel As GraphicsPath = gbShadowPath(gbHeadText, _
      gGB.DpiY, gbRect, gbHeadFont, gbAlign)

      gGB.FillPath(gbShadowBrush, gbpShadow)
      gGB.FillPath(gbHeadBrush, gbpLabel)
   Else
      Dim gbHeadBrush As New SolidBrush(gbHeadForeCol)
      Dim gbAlign As New System.Drawing.StringFormat

      Select Case gbHeadAlign
         Case gbEAlign.Left
            gbAlign.Alignment = StringAlignment.Near
         Case gbEAlign.Center
            gbAlign.Alignment = StringAlignment.Center
         Case gbEAlign.Right
            gbAlign.Alignment = StringAlignment.Far
      End Select
      Dim gGB As Graphics = e.Graphics
      Dim gbSize As SizeF = New SizeF(Me.ClientRectangle.Width - _
         gbHeadShadowSpace, _
         Me.ClientRectangle.Height - gbHeadShadowSpace)

      Dim gbRect As RectangleF = New RectangleF(0, 0, gbSize.Width, _
         gbSize.Height)

      Dim gbpLabel As GraphicsPath = gbShadowPath(gbHeadText, _
         gGB.DpiY, gbRect, gbHeadFont, gbAlign)

      gGB.FillPath(gbHeadBrush, gbpLabel)
   End If
End Sub

This is where the physical drawing of your heading caption and shadow takes place, with appropriate formatting and alignment depending on the selected properties.

The Amazing GroupBox, Part 1

Feel free to build your project now; there should be no errors. If there are any, make sure you have followed all the steps outlined above carefully. I am including the source code with this article as well.

Once built, you can now add the UserControl to your form. you simply can add it from the Toolbox. Experiment with all the various property settings, and see how versatile, and beautiful, this control can be. The following are some examples demonstrating how this control behaves.

[Preview.jpg]

Conclusion

Well, that's it for now. I sincerely hope you have enjoyed this article and learnt something useful with it. Be sure to look out for Part 2 of this article; it will cover adding a scrollbar and of course, the GroupBox items. Until Part 2, happy coding!



About the Author

Hannes du Preez

Hannes du Preez is a Microsoft MVP for Visual Basic. He is a trainer at a South African-based company. He is the co-founder of hmsmp.co.za, a community for South African developers.

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

  • Live Event Date: November 20, 2014 @ 2:00 p.m. ET / 11:00 a.m. PT Are you wanting to target two or more platforms such as iOS, Android, and/or Windows? You are not alone. 90% of enterprises today are targeting two or more platforms. Attend this eSeminar to discover how mobile app developers can rely on one IDE to create applications across platforms and approaches (web, native, and/or hybrid), saving time, money, and effort and introducing apps to market faster. You'll learn the trade-offs for gaining long …

  • Live Event Date: October 29, 2014 @ 11:00 a.m. ET / 8:00 a.m. PT Are you interested in building a cognitive application using the power of IBM Watson? Need a platform that provides speed and ease for rapidly deploying this application? Join Chris Madison, Watson Solution Architect, as he walks through the process of building a Watson powered application on IBM Bluemix. Chris will talk about the new Watson Services just released on IBM bluemix, but more importantly he will do a step by step cognitive …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds