
Introduction
To enhance the design of our applications, most of us use images or icons. With .NET, it's very easy to do, because most of the controls provided by Microsoft already have an Image or Icon property. Unfortunately, it is not the case of the GroupBox control. In fact, with the standard GroupBox control, it is not possible to display an icon in the header section.
Background
I was very surprised to see first that the GroupBox has no Icon nor Image property, and next that no code was available on this topic on the Internet. Of course, great controls like The Grouper did what I was looking for (and much, much more). But I wanted something more simple. So I started to write my own control, thinking that it would be easy ... Here it is.
How does it work?
Of course, the GroupBox must be overridden, and the only property added to the base GroupBox is the Icon property, defined like this:
private Icon m_Icon = null;
[Description("Icon before the text"),
AmbientValue((string)null),
Category("Appearance"),Localizable(true)]
public Icon Icon {
get { return m_Icon; }
set { if(m_Icon != value) { m_Icon = value;
this.Invalidate(false); } }
}
For working with visual styles, it's better to use the VisualStyleRenderer class:
private VisualStyleRenderer m_Renderer = null;
Next, the OnPaint method is overridden too. It allows custom painting, exactly what we need to introduce the drawing of the icon. The basic idea of this code is simple, as you can see:
protected override void OnPaint(PaintEventArgs e) {
if(m_Icon != null && (Application.RenderWithVisualStyles
&& (base.Width >= 10)) && (base.Height >= 10)) {
this.DrawGroupBox(e.Graphics);
} else base.OnPaint(e);
}
The main code lies in the DrawGroupBox method. This method draws the entire control, with the help of three methods: DrawIcon, DrawText, and DrawBackground.
private void DrawGroupBox(Graphics grfx) {
GroupBoxState state = base.Enabled ?
GroupBoxState.Normal : GroupBoxState.Disabled;
Rectangle bounds = new Rectangle(0,0,base.Width,base.Height);
Size txtsize = TextRenderer.MeasureText(grfx,text,
font,new Size(bounds.Width-14,bounds.Height));
int headerheight = Math.Max(m_Icon.Height,txtsize.Height);
Rectangle iconrect = new Rectangle(9,
(headerheight - m_Icon.Height) / 2,
m_Icon.Width,m_Icon.Height);
Rectangle textrect = new Rectangle(new
Point(iconrect.Right,(headerheight -
txtsize.Height) / 2),txtsize);
Rectangle displayrect = bounds; displayrect.Y +=
headerheight / 2; displayrect.Height
-= headerheight / 2;
DrawIcon(grfx,m_Icon,iconrect,state);
DrawText(grfx,this.Text,this.Font,textrect,
m_Renderer.GetColor(ColorProperty.TextColor),
this.BackColor,txtflags);
DrawBackground(grfx,displayrect,textrect,m_Icon.Width);
grfx.Dispose();
Then, the three distinct objects are painted in the following functions:
- the icon: it is done with the
Graphics.DrawIcon method for the Enabled state, and the Control.ControlPaint.DrawImageDisabled method for the Disabled state:
private void DrawIcon(Graphics grfx,Icon icon,
Rectangle rc,GroupBoxState state) {
if(state == GroupBoxState.Disabled) {
using(Image image = m_Icon.ToBitmap()) {
ControlPaint.DrawImageDisabled(grfx,image,
rc.Left,rc.Top,Color.Empty);
}
} else {
grfx.DrawIcon(icon,rc);
}
}
- the text: this can be done with the
Graphics.DrawString method, but for respect of the base properties (like RightToLeft), it is better to use the TextRenderer.DrawText method:
private void DrawText(Graphics grfx,string text,
Font font,Rectangle bounds,
Color txtcolor,Color backcolor) {
TextRenderer.DrawText(grfx,text,font,
bounds,txtcolor,backcolor);
}
- the rounded rectangle: it's easy to do with the
VisualStyleRenderer class. This class encapsulates the visual styles handling, and prevents writing big amounts of lines of code. Because the upper border of the rounded rectangle must not overlay the header of the control, only three parts are drawn, gathered in such a manner that they seem to form a rectangle: private void DrawBackground(Graphics grfx,
Rectangle bounds,Rectangle headerrect, int iconwidth) {
Rectangle leftrect = bounds; leftrect.Width = 7;
Rectangle middlerect = bounds; middlerect.Width =
Math.Max(0,headerrect.Width + iconwidth);
Rectangle rightrect = bounds;
if((txtflags & TextFormatFlags.Right) == TextFormatFlags.Right) {
leftrect.X = bounds.Right - 7;
middlerect.X = leftrect.Left - middlerect.Width;
rightrect.Width = middlerect.X - bounds.X;
} else {
middlerect.X = leftrect.Right;
rightrect.X = middlerect.Right;
rightrect.Width = bounds.Right - rightrect.X;
}
middlerect.Y = headerrect.Bottom;
middlerect.Height -= headerrect.Bottom - bounds.Top;
m_Renderer.DrawBackground(grfx,bounds,leftrect);
m_Renderer.DrawBackground(grfx,bounds,middlerect);
m_Renderer.DrawBackground(grfx,bounds,rightrect);
}
I recommend you to see the entire code (downloadable at the top of this page) for all the details, particularly for the TextFormatFlags, omitted here for simplicity.
This approach is not the only possible, of course. After I wrote the ImageGroupBox control, I saw the XP Style Collapsible GroupBox that uses the GroupBoxRenderer.DrawGroupBox method.
Points of Interest
As you could see, overriding the OnPaint method involves redrawing the entire control. And it's not a so little work when the visual styles must be respected! But in fact, the tools based on the VisualStyleRenderer and TextRenderer classes, help. That's what I thought interesting in this approach. It's a way of using underground classes to product nice results, without writing too many lines of code.