WPF GridSplitter - saving and restoring location AND splitting proportionately

dinane picture dinane · Feb 16, 2011 · Viewed 9.6k times · Source

I am creating a 3-column UI with grid splitters between the columns. I have the requirement to save the sate of the columns so that if the user closes and reopens the app it looks just like they left it. But I am also trying to get the columns to split proportionately - by which I mean if you stretch the window, all three panels grow and if you move the left splitter it changes the division between the left and center columns.

What I have currently achieves only the first requirement - it saves the state of the column widths. I have also made the columns enforce minimum widths for all three columns. But as I understand it, the way to tell a splitter to split proportionately is to use star-sized column widths. Since I am using the Width property already to save and restore the state, I'm not sure I can accomplish what I want to.

Has anyone managed to both save the state of column widths AND have the split be proportional?

Here is some code for what I've got currently:

   <Grid x:Name="mainGrid" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
        <Grid.ColumnDefinitions>
            <ColumnDefinition x:Name="leftColumn" Width="{Binding MainWindowLeftColumnWidth, Mode=TwoWay, Source={x:Static prop:Settings.Default}}" MinWidth="200" MaxWidth="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MainWindow}}, Path=LeftColumnMaxWidth, Mode=OneWay}"/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition x:Name="centerColumn" Width="{Binding MainWindowCenterColumnWidth, Mode=TwoWay, Source={x:Static prop:Settings.Default}}" MinWidth="300" MaxWidth="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MainWindow}}, Path=CenterColumnMaxWidth, Mode=OneWay}"/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition x:Name="rightColumn" Width="*" MinWidth="500"/>
        </Grid.ColumnDefinitions>
        <StackPanel x:Name="leftPanel" Grid.Column="0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="0"/>
        <GridSplitter x:Name="leftSplitter" Grid.Column="1" HorizontalAlignment="Center" VerticalAlignment="Stretch" ResizeBehavior="PreviousAndNext" Width="5" ResizeDirection="Columns"/>
        <StackPanel x:Name="centerPanel" Grid.Column="2" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="0"/>
        <GridSplitter x:Name="rightSplitter" Grid.Column="3" HorizontalAlignment="Center" VerticalAlignment="Stretch" ResizeBehavior="PreviousAndNext" Width="5" ResizeDirection="Columns"/>
        <StackPanel x:Name="rightPanel" Grid.Column="4" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="0" SizeChanged="rightPanel_SizeChanged"/>
    </Grid>

I have dependency properties of type double for both LeftColumnMaxWidth and CenterColumnMaxWidth. And the rightPanel_SizeChanged handler as well as the window Loaded handler both call this method:

    private void CalculateMaxWidths()
    {
          FrameworkElement content = Content as FrameworkElement;
          if (content != null)
          {
              LeftColumnMaxWidth = content.ActualWidth
                                 - leftSplitter.ActualWidth
                                 - centerColumn.ActualWidth
                                 - rightSplitter.ActualWidth
                                 - rightColumn.MinWidth;
              CenterColumnMaxWidth = content.ActualWidth
                                   - leftColumn.ActualWidth
                                   - leftSplitter.ActualWidth
                                   - rightSplitter.ActualWidth
                                   - rightColumn.MinWidth;
          }
    }

I still have some work to do to make sure that resizing the window doesn't clip the right column. I think that solution may be related to trying to make the splitters split proportionately. The particularly peculiar behavior of my current set up is that the left splitter resizes the left and right columns, and leaves the center column size fixed.

I am not afraid of handling SizeChanged or DragDelta to achieve my goals. But what I believe I cannot do is actually set the Width property of the first two columns, as that would destroy my binding to the user setting that saves the state.

Thank you in advance for any help.

Answer

dinane picture dinane · Feb 17, 2011

So I believe I have figured this out. It is possible that some old values in my settings.settings were causing me issues, and it's possible that the default values I put in caused me issues. But here's what I did:

  1. Changed my user settings to save all THREE (not just the left two) column widths. And save them as strings.
  2. Set the default in the user settings (as well as the width property on the columns) to something like 200*.
  3. Set only the MinWidth - not the max - on all three columns.
  4. Manually load and save the user settings for the columns using a GridLengthConverter.

I'm not 100% convinced this is the best way, but it does seem to work, which makes me quite happy. In case anyone else has trouble and comes across this post, here is the working XAML:

    <Grid x:Name="mainGrid" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
        <Grid.ColumnDefinitions>
            <ColumnDefinition x:Name="leftColumn" MinWidth="200" Width="200*"/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition x:Name="centerColumn" MinWidth="300" Width="300*"/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition x:Name="rightColumn" MinWidth="498" Width="498*"/>
        </Grid.ColumnDefinitions>
        <StackPanel x:Name="leftPanel" Grid.Column="0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="0"/>
        <GridSplitter x:Name="leftSplitter" Grid.Column="1" HorizontalAlignment="Center" VerticalAlignment="Stretch" ResizeBehavior="PreviousAndNext" Width="5" ResizeDirection="Columns"/>
        <StackPanel x:Name="centerPanel" Grid.Column="2" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="0"/>
        <GridSplitter x:Name="rightSplitter" Grid.Column="3" HorizontalAlignment="Center" VerticalAlignment="Stretch" ResizeBehavior="PreviousAndNext" Width="5" ResizeDirection="Columns"/>
        <StackPanel x:Name="rightPanel" Grid.Column="4" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="0" SizeChanged="rightPanel_SizeChanged"/>
    </Grid>

That size changed event is still there only for debug tracing. I output the values of Width to see what is happening. Curiously, after expanding everything and going back to the window's minimum size, the right column width stays larger. But since they all have minwidths and the widths are all star-sized, it works itself out.

I did try to put this back into a binding, but since I'm now storing a string, and the GridLengthConverter is a TypeConverter, not an IValueConverter, it didn't work. I think it may be possible to store the values as GridLengths, though I've reached a point where I'm happy with what I've done. So my load and save are like this:

    protected override void OnSourceInitialized(EventArgs e)
    {
        base.OnSourceInitialized(e);

        //...

        try
        {
            GridLengthConverter converter = new GridLengthConverter();
            leftColumn.Width = (GridLength)converter.ConvertFromString(Settings.Default.MainWindowLeftColumnWidth);
            centerColumn.Width = (GridLength)converter.ConvertFromString(Settings.Default.MainWindowCenterColumnWidth);
            rightColumn.Width = (GridLength)converter.ConvertFromString(Settings.Default.MainWindowRightColumnWidth);

            Trace.WriteLine(string.Format("LOADED Left: {0}, Center: {1}, Right {2}", leftColumn.Width, centerColumn.Width, rightColumn.Width));
        }
        catch (Exception)
        {
            // Fail silently, the worse case is we go with the defaults, it's going to be okay
        }
    }

    protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
    {
        base.OnClosing(e);

        //...

        try
        {
            GridLengthConverter converter = new GridLengthConverter();
            Settings.Default.MainWindowLeftColumnWidth = converter.ConvertToString(leftColumn.Width);
            Settings.Default.MainWindowCenterColumnWidth = converter.ConvertToString(centerColumn.Width);
            Settings.Default.MainWindowRightColumnWidth = converter.ConvertToString(rightColumn.Width);

            Trace.WriteLine(string.Format("SAVED Left: {0}, Center: {1}, Right {2}", Settings.Default.MainWindowLeftColumnWidth, Settings.Default.MainWindowCenterColumnWidth, Settings.Default.MainWindowRightColumnWidth));
        }
        catch (Exception)
        {
            // Fail silently, the worst case is we don't save a little something, it's going to be okay
        }
    }

And that all worked for me. So I'm going to go with it!

EDIT: I later did some refinement to prevent the "curiosity" of the right column staying larger. I now have all panel size changes go to one event handler:

    private void PanelSizeChanged(object sender, SizeChangedEventArgs e)
    {
        // In order to keep the resizing from losing proportionality, we'll force it to be proportional to the current size.
        // Otherwise the right panel edges up and up while the other two remain at their starting 200*/300* values.
        // And when that happens eventually resizing the window only resizes the right panel, not proportionately as it does at the start.
        leftColumn.Width = new GridLength(leftColumn.ActualWidth, GridUnitType.Star);
        centerColumn.Width = new GridLength(centerColumn.ActualWidth, GridUnitType.Star);
        rightColumn.Width = new GridLength(rightColumn.ActualWidth, GridUnitType.Star);
    }