Use Windows Forms controls to make multiple stacked expanders in C#

The example Make an expander in C# shows how to make a simple expander. By collapsing a Panel, the program allows the user to hide unwanted information. That frees up space on the form that the Panel used to occupy. Unfortunately in the previous example that new space is unused because no control can easily take it over.

In WPF, Silverlight, or Metro you can place Expander controls inside a StackPanel. Then if one Expander collapses, the others that are later in the StackPanel move up to fill the newly available space.

The Windows Forms controls do not include StackPanel but they do include a few others that can fill in. Initially I tried using a TableLayoutPanel and adjusting row heights to expand and collapse rows but that proved difficult. This example uses a FlowLayoutPanel control instead, which turns out to be a lot easier.

The FlowLayoutPanel arranges its child controls in a row or column (depending on its FlowDirection property) as long as the children fit. If the row/column becomes full, the control starts a new row/column. This behavior is almost the same as a StackPanel except the StackPanel doesn't start a new row/column when there's no more room in the current row/column.

To make the FlowLayoutPanel work for this example, I set it to flow controls in rows. I then placed a series of Panels in the control that all have the same width and that are too wide for the FlowLayoutPanel to place more than one control on a single row. In this example, the FlowLayoutPanel is 268 pixels wide and the Panels are 260 pixels wide. The result is that the FlowLayoutPanel places the Panels on top of each other.

If you look at the picture, you can probably figure out that there are 5 Panels. The 1st, 3rd, and 5th Panels each contain an expand/collapse Button and a Label. The other Panels contain TextBoxes, Labels, ComboBoxes, and a PictureBox.

Now the program can adjust the sizes of the 2nd, 4th, and 6th Panels to expand and collapse them.

So far so good but there's an important difference between this example and the previous one that used only a single expander. The previous example used two Timers: one to expand its Panel and one to collapse its Panel. This example has three expanding Panels so the previous method would require six Timers and lots of duplicated code, which is never a good idea.

To work around this problem, this example uses a single Timer that expands or collapses any Panel that is currently expanding or collapsing. To keep track of what the Panels are doing, the program defines the following enumerated type.

// The state of an expanding or collapsing panel.
private enum ExpandState
{
    Expanded,
    Expanding,
    Collapsing,
    Collapsed,
}

The program also uses the following arrays.

// The expanding panels' current states.
private ExpandState[] ExpandStates;

// The Panels to expand and collapse.
private Panel[] ExpandPanels;

// The expand/collapse buttons.
private Button[] ExpandButtons;

These arrays hold the expand state of each expanding Panel, references to the expanding Panels themselves, and the expand buttons.

When the program starts, the following code initializes these arrays.

// Initialize.
private void Form1_Load(object sender, EventArgs e)
{
    // Select a state.
    cboState1.SelectedIndex = 0;

    // Initialize the arrays.
    ExpandStates = new ExpandState[]
    {
        ExpandState.Expanded,
        ExpandState.Expanded,
        ExpandState.Expanded,
    };
    ExpandPanels = new Panel[]
    {
        panAddress1,
        panAddress2,
        panImage,
    };
    ExpandButtons = new Button[]
    {
        btnExpand1,
        btnExpand2,
        btnExpand3,
    };

    // Set expander button Tag properties to give indexes
    // into these arrays and display expanded images.
    for (int i = 0; i < ExpandButtons.Length; i++)
    {
        ExpandButtons[i].Tag = i;
        ExpandButtons[i].Image = Properties.Resources.expander_up;
    }
}

This code sets each Panel's initial state to expanded and saves the Panels and Buttons. It then sets each Button's Tag property to its index in the array and makes each Button display the collapse image.

All of the expand/collapse buttons use the following Click event handler.

// Start expanding.
private void btnExpander_Click(object sender, EventArgs e)
{
    // Get the button.
    Button btn = sender as Button;
    int index = (int)btn.Tag;

    // Get this panel's current expand
    //  state and set its new state.
    ExpandState old_state = ExpandStates[index];
    if ((old_state == ExpandState.Collapsed) ||
        (old_state == ExpandState.Collapsing))
    {
        // Was collapsed/collapsing. Start expanding.
        ExpandStates[index] = ExpandState.Expanding;
        ExpandButtons[index].Image = Properties.Resources.expander_up;
    }
    else
    {
        // Was expanded/expanding. Start collapsing.
        ExpandStates[index] = ExpandState.Collapsing;
        ExpandButtons[index].Image = Properties.Resources.expander_down;
    }

    // Make sure the timer is enabled.
    tmrExpand.Enabled = true;
}

This code converts its sender parameter into the Button that the user clicked. It uses the Button's Tag property to get the index of the Button and its corresponding Panel.

The code then uses the ExpandStates array to get the corresponding Panel's expand or collapsed state. If the Panel is currently collapsed or in the process of collapsing, the code makes its button display the collapse image and sets the Panel's state to Expanding to make it start expanding.

Conversely if the Panel is currently expanded or in the process of expanding, the code makes its button display the expand image and sets the Panel's state to Collapsing to make it start collapsing.

This event handler finishes by enabling the tmrExpand Timer. The following code shows that Timer's Tick event handler.

// The number of pixels expanded per timer Tick.
private const int ExpansionPerTick = 7;

// Expand or collapse any panels that need it.
private void tmrExpand_Tick(object sender, EventArgs e)
{
    // Determines whether we need more adjustments.
    bool not_done = false;

    for (int i = 0; i < ExpandPanels.Length; i++)
    {
        // See if this panel needs adjustment.
        if (ExpandStates[i] == ExpandState.Expanding)
        {
            // Expand.
            Panel pan = ExpandPanels[i];
            int new_height = pan.Height + ExpansionPerTick;
            if (new_height <= pan.MaximumSize.Height)
            {
                // This one is done.
                new_height = pan.MaximumSize.Height;
            }
            else
            {
                // This one is not done.
                not_done = true;
            }

            // Set the new height.
            pan.Height = new_height;
        }
        else if (ExpandStates[i] == ExpandState.Collapsing)
        {
            // Collapse.
            Panel pan = ExpandPanels[i];
            int new_height = pan.Height - ExpansionPerTick;
            if (new_height <= pan.MinimumSize.Height)
            {
                // This one is done.
                new_height = pan.MinimumSize.Height;
            }
            else
            {
                // This one is not done.
                not_done = true;
            }

            // Set the new height.
            pan.Height = new_height;
        }
    }

    // If we are done, disable the timer.
    tmrExpand.Enabled = not_done;
}

This event handler updates each of the expandable Panels. For each Panel, it checks the corresponding ExpandStates entry to see whether if Panel is expanding. If the Panel is expanding, the code increases its height. If the Panel has not reached its maximum height, the code sets not_done to true so it knows that the Timer should run again to continue expanding the Panel.

If the current Panel is collapsing, the code takes similar steps to make the Panel smaller.

After it has processed all of the Panels, the code checks the not_done variable and sets the Timer's Enabled property appropriately.

In some ways this example's code is simpler than the code used by the previous example to handle only one expandable Panel. This version only uses one Timer and can handle any number of expandable Panels. To use another Panel, add two more Panels to the FlowLayoutPanel and set their widths equal to the width of the other Panels. Add the expand/collapse button to the first new Panel and give it the same Click event handler as the existing buttons. Update the code in the form's Load event handler to prepare the new controls and you should be set.

   

 

What did you think of this article?




Trackbacks
  • 9/28/2012 9:40 AM The Microsoft MVP Award Program Blog wrote:
    1. Use Windows Forms Controls to Make Multiple Stacked Expanders in C# By Visual Basic MVP Rod Stephens
Comments
  • No comments exist for this post.
Leave a comment

Submitted comments are subject to moderation before being displayed.

 Name

 Email (will not be published)

 Website

Your comment is 0 characters limited to 3000 characters.