Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

BarTender - Group your contents

4.82/5 (25 votes)
25 Apr 2006CPOL8 min read 1   3.1K  
An Outlook bar like control with generic contents and animation effects.

Sample Image - BarTender.gif

Contents

Introduction

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.

Background

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.

Using the code

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:

C#
public MyForm() {
   InitializeComponent();
   _groupPaneBar.Add(new DataGrid(), "Bar 1", 
         null, false); //runtime created control
   _groupPaneBar.Add(_panelWithLinks, "Bar 2", 
         null, true); //design time created control
}

Compile, start, and you can see two groups in the bar.

Samples

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 ScrollBars can show up simultaneously.

Architecture

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.

GroupPane

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.

C#
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;

    //fill gradient backcolor of header
    using (LinearGradientBrush brush = 
                new LinearGradientBrush(headerRectangle, 
                GetColor(_parent.HeaderColor1), 
                GetColor(_parent.HeaderColor2), 
                _parent.HeaderGradientMode))
    {
        e.Graphics.FillRectangle(brush, headerRectangle);
    }

    //draw surrounding borders
    using (Pen pen = new Pen(GetColor(_parent.BorderColor), 
                     _parent.BorderWidth))
    {
        //top
        e.Graphics.DrawLine(pen, _parent.BorderWidth * 2, 
           (_parent.BorderWidth) / 2, 
            Width - _parent.BorderWidth * 2.5f, 
            (_parent.BorderWidth) / 2);

        //topleft
        e.Graphics.DrawArc(pen, _parent.BorderWidth / 2f, 
            _parent.BorderWidth / 2f, 
            _parent.BorderWidth * 4, 
            _parent.BorderWidth * 4, 180, 90);
    
        //topright
        e.Graphics.DrawArc(pen, Width - 1 - _parent.BorderWidth * 4.5f, 
            _parent.BorderWidth / 2f, _parent.BorderWidth * 4,
            _parent.BorderWidth * 4, 270, 90);

        //bottom
        e.Graphics.DrawLine(pen, 1, Height -  2 * 
            _parent.BorderWidth, Width - 2, 
            Height - 2 * _parent.BorderWidth);

        //left
        e.Graphics.DrawLine(pen, (_parent.BorderWidth) / 2, 
             _parent.BorderWidth * 2f, 
            (_parent.BorderWidth) / 2, Height - 1.5f * 
            _parent.BorderWidth - 1);
        
        //right
        e.Graphics.DrawLine(pen, Width - 1 - _parent.BorderWidth / 2, 
            _parent.BorderWidth * 2, 
            Width - 1 - _parent.BorderWidth / 2, 
            Height - 1.5f * _parent.BorderWidth - 1);

        //under header
        e.Graphics.DrawLine(pen, _parent.BorderWidth / 2f,
            _parent.BorderWidth + _parent.HeaderHeight, 
             Width - _parent.BorderWidth,
            _parent.BorderWidth  + _parent.HeaderHeight);
    }

    //draw expand/collapse button
    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)
    {
        //draw button highlighting
        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));
        }
    }
    
    //draw expand/collapse arrow
    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)
    //draw image
    {
        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)
    {
        //draw text
        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.

GroupPaneBar

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 GroupPanes. 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 GroupPanes 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:

C#
/// <summary>
/// Adds a new <see cref="GroupPane"/> to the end of the list.
/// </summary>
/// <param name="control">Element which should
/// initially beend placed in the new group.</param>
/// <param name="text">Initial text of the new group.</param>
/// <param name="image">Initial image of the new group.</param>
/// <param name="adjustGroupPaneHeightToControlheight">
///    Sets whether the expanded height of 
///    the resulting group pane should match the height
///    of the given control.</param>
/// <returns>The newly created <see cref="GroupPane"/>.</returns>
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.

ToDo's

  • 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.

History

  • April 16th, 2006 - Version 1.0:
    • Initial release.
  • April 17th, 2006 - Version 1.0.1:
    • Fixed the flickering when nesting several GroupPaneBars 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 Forms 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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)