Friday, December 18, 2009

WPF expanders with stretching height

I needed 2 collapsible WPF expanders to use up all the available space in the window, no matter what size the window is, and allow the users to resize the expanders. This can be done by putting the expanders in a grid, and setting the row's height to "*" when the expander is expanded, and to "Auto" when the expander is collapsed, and adding a GridSplitter to the grid. Below is the code that does this, along with some screenshots.

 
using System.Windows;
using System.Windows.Controls;

namespace WpfApplication17
{
   partial class Window1
   {
       GridLength[] starHeight;

       public Window1()
       {
           InitializeComponent();

           starHeight = new GridLength[expanderGrid.RowDefinitions.Count];
           starHeight[0] = expanderGrid.RowDefinitions[0].Height;
           starHeight[2] = expanderGrid.RowDefinitions[2].Height;
           ExpandedOrCollapsed(topExpander);
           ExpandedOrCollapsed(bottomExpander);

           // InitializeComponent calls topExpander.Expanded
           // while bottomExpander is null, if we hook this up in the xaml
           topExpander.Expanded += ExpandedOrCollapsed;
           topExpander.Collapsed += ExpandedOrCollapsed;
           bottomExpander.Expanded += ExpandedOrCollapsed;
           bottomExpander.Collapsed += ExpandedOrCollapsed;
       }

       void ExpandedOrCollapsed(object sender, RoutedEventArgs e)
       {
           ExpandedOrCollapsed(sender as Expander);
       }

       void ExpandedOrCollapsed(Expander expander)
       {
           var rowIndex = Grid.GetRow(expander);
           var row = expanderGrid.RowDefinitions[rowIndex];
           if (expander.IsExpanded)
           {
               row.Height = starHeight[rowIndex];
               row.MinHeight = 50;
           }
           else
           {
               starHeight[rowIndex] = row.Height;
               row.Height = GridLength.Auto;
               row.MinHeight = 0;
           }

           var bothExpanded = topExpander.IsExpanded && bottomExpander.IsExpanded;
           splitter.Visibility = bothExpanded ?
               Visibility.Visible : Visibility.Collapsed;
       }
   }
}
<Window x:Class="WpfApplication17.Window1"
       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
       Title="Window1" Height="300" Width="300">
   <!-- main grid -->
   <Grid>
       <Grid.ColumnDefinitions>
           <ColumnDefinition Width="Auto"/>
           <ColumnDefinition/>
       </Grid.ColumnDefinitions>
       <Grid Name="expanderGrid">
           <Grid.RowDefinitions>
               <RowDefinition/>
               <RowDefinition Height="Auto"/>
               <RowDefinition/>
           </Grid.RowDefinitions>
           <Expander Name="topExpander"
                     Header="Top expander"
                     IsExpanded="True">
               <Label Content="Top expander content"
                      Background="AntiqueWhite"/>
           </Expander>
           <GridSplitter Name="splitter"
                         ResizeDirection="Rows"
                         HorizontalAlignment="Stretch"
                         Grid.Row="1"
                         Height="5"
                         Background="LightBlue"/>
           <Expander Name="bottomExpander"
                     Header="Bottom expander"
                     Grid.Row="2"
                     IsExpanded="True">
               <Label Content="Bottom expander content"
                      Background="LightGreen"/>
           </Expander>
       </Grid>
       <Label Content="Workarea"
              Grid.Column="1"
              Background="LightYellow"/>
   </Grid>
</Window>

5 comments:

  1. Very helpful post!

    To let the minimum expander height match the header height make the following modifications (assuming that topExpander is expanded and bottomExpander is not expanded by default):
    1) add a field to the Window1 class: "double minHeight = 0;"
    2) change "row.MinHeight = 50;" to "row.MinHeight = minHeight;" in the second ExpandedOrCollapsed function
    3) add the following function:
    void CalculateMinHeight(object sender, RoutedEventArgs e) {
    minHeight = bottomExpander.ActualHeight;
    expanderGrid.RowDefinitions[0].MinHeight = minHeight;
    expanderGrid.RowDefinitions[2].MinHeight = minHeight;
    bottomExpander.Expanded -= CalculateMinHeight;
    }
    4) add "bottomExpander.Expanded += CalculateMinHeight;" to the Window1 constructor

    ReplyDelete
  2. So thanks for your helpfully post! I added some lines your ExpandedOrCollapsed event for my work:

    void ExpandedOrCollapsed(object sender, RoutedEventArgs e)
    {
    Expander expander = sender as Expander;
    foreach (var item in expanderGrid.Children)
    {
    if (item.GetType() == typeof(Expander))
    {
    Expander expItem = item as Expander;
    expItem.Expanded -= ExpandedOrCollapsed;
    expItem.Collapsed -= ExpandedOrCollapsed;
    bool isExpanded = (expItem == expander && expItem.IsExpanded);
    expItem.IsExpanded = isExpanded;
    expItem.Expanded += ExpandedOrCollapsed;
    expItem.Collapsed += ExpandedOrCollapsed;
    }
    }
    ExpandedOrCollapsed(sender as Expander);
    }

    ReplyDelete
  3. Very helpful piece. With help of your codes I created a collapsible Grid column which was a daunting task otherwise.

    Thanks !

    ReplyDelete
  4. Nice work! Very elegant & simple.
    Thanks!

    ReplyDelete
  5. Works like a charm, thanks a ton!

    ReplyDelete