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.