Everyone knows the navigation in Outlook with its neat bar, and there are many different implementations for it already existing. Some of them are very neat, but are rather inflexible on what kind of contents can be inserted into each group. What I needed was a possibility to show and hide several large grids quickly in all possible combinations. I could have used some sort of docking and/or MDI style to achieve this, but for several reasons, this was not good in my situation. Moreover, I think my solution is more convenient and fast than having to tackle with docking via drag and drop.
This component has a look similar to that of a normal Outlook Bar (I admit I didn't stick to a certain style but invented my own based on the several implementations I have already seen). The big difference is that any control can be placed within each group, and that each group can not only be expanded or collapsed but also resized by the user. To make it look fancy, I also added animation effects for the expand/collapse process.
Warning: Do not use this component if you just want to insert links/icons into the groups (like in Outlook). There are other components out there which are better in handling this.
To use this component, you should just have a bit of experience working with Windows Forms. As there isn't much built-in designer support, it is necessary to build the contents of the bars at runtime. To add animations, I have used my own Animations
component. That one is only included as a compiled assembly in the download. If you want to explore it further, then have a look at my article: Animating Windows Forms.
Although there is no extensive designer support, the usage is straightforward. First, drag a GroupPaneBar
onto your Form
and compile it. You will notice that some groups are already showing up in it. These are only there for a visual feedback of the settings made to it, and are only there while in design mode. Now, have a look at its properties, and change them until you are happy with the look and feel. I will discuss some of them later, in more detail.
All what is left to do is adding groups to the bar. This cannot be done with the designer. As a preparation, you could nevertheless already drag and adjust the controls you want to add onto your Form
. Now, in the constructor of the Form
(and after InitializeComponent
), you can add those controls to the bar:
public MyForm() {
InitializeComponent();
_groupPaneBar.Add(new DataGrid(), "Bar 1",
null, false);
_groupPaneBar.Add(_panelWithLinks, "Bar 2",
null, true);
}
Compile, start, and you can see two groups in the bar.
As I always do, I provided a sample application which should show most of the capabilities of this component. It's rather heavyweight, because I stuffed everything I could think of into it, and also used it for my final testing. Note that, because of the nesting of the bars and the huge content, several ScrollBar
s can show up simultaneously.
The structure is rather simple. The whole component has only four classes of which only two are doing the relevant work. The other two are just specialized event classes.
This class represents a group within a bar. It handles the painting, event management, state holding, and animation. For those of you interested in programming self drawn controls, the OnPaint
method is the most interesting part.
private void DrawArrow(Graphics graphics, Rectangle rect,
Color color, bool isUp)
{
int arrowHeight = rect.Height - 8;
if (arrowHeight > 5)
arrowHeight = 5;
int halfLeftHeight = (rect.Height - arrowHeight) / 2;
int halfWidth = (rect.Width / 2) - 1;
using (SolidBrush brush = new SolidBrush(color))
{
int upwardsOffset = isUp ? 1 : -1;
int curLine = 0;
for (int i = (upwardsOffset < 0) ?
(arrowHeight - 1) : 0; (upwardsOffset < 0) ?
(i >= 0) : (i < arrowHeight); i += upwardsOffset)
{
graphics.FillRectangle(brush, (rect.X + halfWidth) - i,
(rect.Y + halfLeftHeight) +
curLine, (i * 2) + 1, 1);
curLine++;
}
}
}
private GraphicsPath CreateRoundedRectPath(Rectangle r, int radius)
{
GraphicsPath path = new GraphicsPath();
path.AddLine(r.Left + radius, r.Top,
(r.Left + r.Width) - (radius * 2), r.Top);
path.AddArc((r.Left + r.Width) - (radius * 2), r.Top,
radius * 2, radius * 2, 270f, 90f);
path.AddLine((int) (r.Left + r.Width),
(int) (r.Top + radius), (int) (r.Left + r.Width),
(int) ((r.Top + r.Height) - (radius * 2)));
path.AddArc((int) ((r.Left + r.Width) - (radius * 2)),
(int) ((r.Top + r.Height) - (radius * 2)),
(int) (radius * 2), (int) (radius * 2),
(float) 0f, (float) 90f);
path.AddLine((int) ((r.Left + r.Width) - (radius * 2)),
(int) (r.Top + r.Height),
(int) (r.Left + radius), (int) (r.Top + r.Height));
path.AddArc(r.Left, (r.Top + r.Height) - (radius * 2),
radius * 2, radius * 2, 90f, 90f);
path.AddLine(r.Left, (r.Top + r.Height) -
(radius * 2), r.Left, r.Top + radius);
path.AddArc(r.Left, r.Top, radius * 2, radius * 2, 180f, 90f);
path.CloseFigure();
return path;
}
private Color GetColor(Color color)
{
return GetColor(color, base.Enabled);
}
private Color GetColor(Color color, bool enabled)
{
if (enabled)
return color;
return ControlPaint.LightLight(color);
}
protected override void OnPaint(PaintEventArgs e)
{
e.Graphics.SmoothingMode = SmoothingMode.HighQuality;
e.Graphics.Clear(this.BackColor);
Rectangle headerRectangle = GetHeaderRectangle();
if (headerRectangle.Width == 0 || headerRectangle.Height == 0)
return;
using (LinearGradientBrush brush =
new LinearGradientBrush(headerRectangle,
GetColor(_parent.HeaderColor1),
GetColor(_parent.HeaderColor2),
_parent.HeaderGradientMode))
{
e.Graphics.FillRectangle(brush, headerRectangle);
}
using (Pen pen = new Pen(GetColor(_parent.BorderColor),
_parent.BorderWidth))
{
e.Graphics.DrawLine(pen, _parent.BorderWidth * 2,
(_parent.BorderWidth) / 2,
Width - _parent.BorderWidth * 2.5f,
(_parent.BorderWidth) / 2);
e.Graphics.DrawArc(pen, _parent.BorderWidth / 2f,
_parent.BorderWidth / 2f,
_parent.BorderWidth * 4,
_parent.BorderWidth * 4, 180, 90);
e.Graphics.DrawArc(pen, Width - 1 - _parent.BorderWidth * 4.5f,
_parent.BorderWidth / 2f, _parent.BorderWidth * 4,
_parent.BorderWidth * 4, 270, 90);
e.Graphics.DrawLine(pen, 1, Height - 2 *
_parent.BorderWidth, Width - 2,
Height - 2 * _parent.BorderWidth);
e.Graphics.DrawLine(pen, (_parent.BorderWidth) / 2,
_parent.BorderWidth * 2f,
(_parent.BorderWidth) / 2, Height - 1.5f *
_parent.BorderWidth - 1);
e.Graphics.DrawLine(pen, Width - 1 - _parent.BorderWidth / 2,
_parent.BorderWidth * 2,
Width - 1 - _parent.BorderWidth / 2,
Height - 1.5f * _parent.BorderWidth - 1);
e.Graphics.DrawLine(pen, _parent.BorderWidth / 2f,
_parent.BorderWidth + _parent.HeaderHeight,
Width - _parent.BorderWidth,
_parent.BorderWidth + _parent.HeaderHeight);
}
int buttonSize = (int)(headerRectangle.Height -
_parent.BorderWidth - 5);
_buttonRect = new Rectangle(this.Width -
_parent.BorderWidth - 5 - buttonSize,
_parent.BorderWidth + 2, buttonSize, buttonSize);
using (Pen pen = new Pen(GetColor(_parent.BorderColor,
Enabled && _parent.CanExpandCollapse), 1))
{
e.Graphics.DrawPath(pen,
CreateRoundedRectPath(_buttonRect, 1));
}
if (_buttonHighlighted && Enabled)
{
using (Pen pen =
new Pen(Color.FromArgb(_parent.ButtonHighlightAlpha,
_parent.ButtonHighlightColor), 4))
{
Rectangle highlightRect = _buttonRect;
highlightRect.Inflate(-2, -2);
e.Graphics.DrawPath(pen,
CreateRoundedRectPath(highlightRect, 3));
}
}
Rectangle shapeRect = new Rectangle(_buttonRect.X + 1,
_buttonRect.Y + 1, _buttonRect.Width - 1,
_buttonRect.Height - 1);
if (_heightAnimator.IsRunning || _expanded)
DrawArrow(e.Graphics, shapeRect,
GetColor(_parent.ButtonArrowColor,
Enabled && _parent.CanExpandCollapse), true);
if (_heightAnimator.IsRunning || !_expanded)
DrawArrow(e.Graphics, shapeRect,
GetColor(_parent.ButtonArrowColor,
Enabled && _parent.CanExpandCollapse), false);
if (_image != null && _parent.ImagesEnabled)
{
int x = _parent.BorderWidth * 3;
int y = (int)(_parent.BorderWidth +
_parent.HeaderHeight / 2f - _image.Height / 2f);
if (Enabled)
e.Graphics.DrawImageUnscaled(_image, x, y);
else
ControlPaint.DrawImageDisabled(e.Graphics, _image, x, y,
GetColor(_parent.HeaderColor1));
}
if (_text != null)
{
int textX = _parent.BorderWidth * 3 +
(_image == null ? 0 : _image.Width);
Rectangle textRect = new Rectangle(textX, _parent.BorderWidth,
_buttonRect.Left - textX - _parent.BorderWidth,
_parent.HeaderHeight - _parent.BorderWidth);
if (Enabled)
{
using (SolidBrush brush = new SolidBrush(base.ForeColor))
{
e.Graphics.DrawString(_text, base.Font, brush,
textRect, _parent.GetStringFormat());
}
}
else
{
ControlPaint.DrawStringDisabled(e.Graphics, _text, base.Font,
GetColor(base.ForeColor), textRect,
_parent.GetStringFormat());
}
}
}
It uses many different kinds of painting operations, and makes sure that the GroupPane
is cleanly painted in disabled mode. I think, that is one of the things many control programmers just miss while developing a new control. All visual settings are fetched from the corresponding GroupPaneBar
, which means that a GroupPane
cannot exist without one. The main reason for this was that, I wanted that all settings are done in a centralized place and not separately within each group. Normally, when using this component, you won't even have to think about this class, but if you want to programmatically expand/collapse or change properties of certain groups, then you can set them here.
The following are the most important members of the public interface:
ParentBar
Simply gets the GroupPaneBar
which holds the group.
Text
Gets or sets the text to be displayed at the top of the group.
Image
Gets or sets the Image
to be displayed at the top of the group.
Expanded
Gets or sets whether the group should be expanded or collapsed. When setting this property, it will use the GroupPaneBar.AnimationEnabled
property to determine whether it is animated. To directly influence this behavior, the methods Expand
and Collapse
have some overloads where this can be overridden. This can be useful when expanding/collapsing certain groups at startup.
ExpandedHeight
Gets or sets the height of the group when it is expanded. If the group is expanded while setting this property, it will get resized to the new height immediately. Note that this value also includes the borders and the header of the group.
IsAnimationRunning
Gets whether the group is currently in the process of being collapsed or expanded.
Control
Gets or sets the control to be shown within the group. This enables you to exchange the complete contents of the groups.
Events
The GroupPane
class has a total of eight events. Four of them are fired when certain properties change - the other four are about notifying about collapsing and expanding of the group. As those properties are also raised by the GroupPaneBar
, I will explain them there in detail.
A GroupPaneBar
holds several instances of a GroupPane
. As mentioned, a GroupPane
cannot exist without a GroupPaneBar
, and thus it also functions as a factory for GroupPane
s. Because of the size of it, I will not list the complete public interface here - especially regarding visual settings. Please have a look into the code for this. All properties are well documented.
Group organization
To create GroupPane
s for a GroupPaneBar
, the overloaded function CreateGroupPane
will create instances. Those instances are not added immediately to the bar. Thus, a call to Add
is needed to add the group to the bar afterwards. Moreover, the GroupPaneBar
has several other collection-like functions like Clear
, Remove
, or RemoveAt
to change the contained groups. For a more convenient way to add groups, there are several Add
overrides which not only create groups but also add them directly to the bar:
public GroupPane Add(Control control, string text, Image image,
bool adjustGroupPaneHeightToControlheight);
The events GroupPaneAdded
and GroupPaneRemoved
can be used to get notified whenever a group is added or removed from the bar.
Visual properties
Except the Image
and Text
properties of the GroupPane
, all visual settings are done via the GroupPaneBar
. As I said, I wanted it to be made centralized. The drawback is that it is not possible to set different styles for each group. But this wasn't on my wish list, and probably is also not on yours, and this way, it is far easier to change the whole bar. If enough members here shout long and loud enough at me, I will probably rethink this point. Note that there is an event for every property which will get fired whenever it gets changed (and only if it is changed - if setting a property with the exact same value, no event will get fired).
Expanding and collapsing
Besides the ExpandAll
and CollapseAll
methods which are similar to the Expand
/Collapse
methods found in the GroupPane
(except that they naturally expand/collapse all contained groups), there are four events regarding expanding and collapsing. All of them provide event arguments which hold the affected GroupPane
. Two of them fire after a group has been expanded or collapsed, and more interestingly, the other two fire before this happens. They provide a Cancel
property in the event arguments which can be used to disallow the user from expanding/collapsing certain groups while still allowing other groups to be modified.
- Designer support like in the
TabControl
should be very cool.
- The contents of groups are currently very generic. Building a real Outlook-like bar with this component isn't as easy as with other components but it could be. Some more concrete implementations (using or inheriting from the existing classes) could improve the ease of usage.
- Anything you like :). Please feel free to post requests.
- April 16th, 2006 - Version 1.0:
- April 17th, 2006 - Version 1.0.1:
- Fixed the flickering when nesting several
GroupPaneBar
s into each other, by overriding AdjustFormScrollbars
and inheriting from ScrollableControl
instead of a Panel
. Thanks to Josh Smith for giving me some ideas on this.
- Because of the changed base class, I had to implement the
BorderStyle
property myself, and I added a Changed
event for it and made it configurable in the properties example.
- April 22nd, 2006 - Version 1.1:
- Automatically set the
TopLevel
property of a Form
inserted into a GroupPane
to false
. This allows easier usage of Form
s with this component. Note that this operation results in a ArgumentException
when the Form
is a MDI container. This is based on a request from duffman071.
- Added a property
ShowExpandCollapseButton
to GroupPaneBar
. Settings this property to false
will remove the expand/collapse button from the groups. Then, they can be expanded or collapsed by clicking anywhere into the header of a group. I also added a new CheckBox
into the properties sample, to test this new feature. This is based on a request from duffman071.