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

[expanders]

The example Use Windows Forms controls to make an expander in C# shows how to make simple expanders. 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 come later in the StackPanel move up to fill in the available space.

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

The FlowLayoutPanel control 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 Panel controls in the FlowLayoutPanel that all have the same width and that are so wide that the FlowLayoutPanel cannot 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. As a result, the FlowLayoutPanel places the Panel controls 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 Panel controls each contain an expand/collapse Button and a Label. The other Panel controls contain TextBox, Label, ComboBox, and a PictureBox controls.

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 Timer components: one to expand its Panel and one to collapse its Panel. This example has three expanding Panel controls so the previous method would require six Timer components 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 Panel controls 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 control’s initial state to expanded and saves the Panel and Button controls. It then sets each Button control’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 control’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 control’s expanded 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 control’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 its state to Collapsing to make it start collapsing.

This event handler finishes by enabling the tmrExpand Timer. The following code shows that component’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 Panel controls. For each Panel, it checks the corresponding ExpandStates entry to see whether the 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 Panel controls, the code checks the not_done variable and sets the Timer component’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 Panel controls to the FlowLayoutPanel and set their widths equal to the width of the other Panel controls. 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.


Download Example   Follow me on Twitter   RSS feed   Donate




This entry was posted in animation, controls, multimedia and tagged , , , , , , , , , , , , , . Bookmark the permalink.

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

  1. Steve Moore says:

    I just implemented something very similar to this. I have the user select an XML file and use deserialization to load the data into an object. I then use a TableLayoutPanel to hold all the objects. I create a button in the first column and a FlowLayoutPanel in the second column for each main section in the XML file. A custom user object is create in the FlowLayoutPanels for each item within the main sections of the XML file. The buttons are set to no border and the click events for all buttons are tied to the same event. Within the event, the text of the button is toggled and the visibility of the corresponding FlowLayoutPanel is toggled. To make it easy to keep track of the Buttons and FlowLayoutPanels I used List and List lists. The FlowLayoutPanels are set to autosize and grow topdown. This makes it so that I don’t have to worry about the position of my custom controls that I am adding, the FlowLayoutPanel does all the work for me.

    This gave me lots of trouble until I found your post and change my approach. Thanks for this write up, it really helped a lot.

  2. Nguyen Duy Tu says:

    i need DLL file, please,
    tks you

    • RodStephens says:

      Sorry I don’t have a compiled DLL. In fact, I generally don’t post DLLs because there’s no way for you to be sure they’re safe.

      You can use the code to build your own DLL. Or you could build a custom control to make it even easier to use.

  3. Nguyen Duy Tu says:

    ok, tks you very much

Leave a Reply

Your email address will not be published. Required fields are marked *