Fixing WPF GridSplitter

Introduction

While working on a WPF tool for work – I was creating ColumnDefinitions in a Grid dynamically (with code) and also dynamically adding GridSplitters to allow users (myself) to resize the columns. Well, it all worked OK, except for the many times that it did not. I thought I made some mistake when creating these GridSplitters, but it seemed like some other people had similar problems with GridSplitters, namely sometimes – they would not work when used with a mouse. When you drag a splitter – it would spring back to original position and stop being dragged or it would jump to some other random (?) position – maybe resizing one of the columns to 0. I tried to fix it by configuring the GridSplitters in different ways – in code behind, between Begin/EndInit blocks, in XAML – nothing helped. I simplified the repro and it seemed to be happening more often when the Grid had many GridSplitters. I checked the same layout in Silverlight and it worked without any problems (well, except for needing to reference another library since GridSplitters are not available in Silverlight using the basic Visual Studio application template). This seemed to indicate the WPF implementation is just somehow broken and it won’t work. Well then I guess I should implement the control myself – how hard would it be? – handle some mouse and keyboard events and resize an associated ColumnDefinition or RowDefinition…

The Fix – SimpleGridSplitter

Well, I guess it is not super hard, but also not trivial – as we can see from the problems with the original WPF implementation, so it did take some trial and error and I left out some pieces that were not interesting to me.

My implementation is called SimpleGridSplitter and it is a UserControl. I briefly considered making it a ContentControl, but a UserControl has two benefits – it is easier to implement and easier to copy and paste, which I prefer over creating a separate library and referencing a project or dll in your application. With a custom control – you would need to copy the default style as well as the control code, while with UserControl – you just need to create a UserControl called SimpleGridSplitter in your application and replace some code behind, while being free to modify the XAML representation any way you like. I might create a full-blown re-template-able custom control if I find someone might need it and put it out on CodePlex. Meanwhile – if you find problems with my implementation – let me know and I can fix it for you.

Here are the properties that the WPF GridSplitter provides and my SimpleGridSplitter implementation coverage:

  • DragIncrement – left out – specifies “minimum distance that a user must drag a mouse to resize rows or columns with a GridSplitter control.” – I just implemented the basic immediate response to dragging with a mouse.
  • KeyboardIncrement – done – specifies “the distance that each press of an arrow key moves the splitter.”
  • PreviewStyle – left out – defines “the style that customizes the appearance, effects, or other style characteristics for the GridSplitter control preview indicator that is displayed when the ShowsPreview property is set to true.” – I just respond to mouse and keyboard dragging of the splitter immediately instead of providing a preview, which is a bit easier and should be enough in most scenarios.
  • ResizeBehavior – done – specifies “which columns or rows are resized relative to the column or row for which the GridSplitter control is defined.”
  • ResizeDirection – done – “indicates whether the GridSplitter control resizes rows or columns.”
  • ShowsPreview – left out – “indicates whether the GridSplitter control updates the column or row size as the user drags the control.” – left it out for now for the same reason I did not implement PreviewStyle.

The main problems I had was with defining how to set the widths of column definitions (and respectively heights of row definitions) when dragging the splitter to resize star-sized columns and rows. Auto and Pixel-sized columns and rows are easy – I just get the delta from the mouse move, adjust for minimum and maximum widths or heights and set them to updated pixel-sized values, but star-sized columns and rows are a bit more tricky since you need to take into account other rows or columns. I first came up with the solution that I just proportionally modify the star-sized column width based on proportional actual width before and after the update, which works great unless one of the widths is zero… The more robust solution was to take into account the total width available to all star-sized columns and the sums of star-sized values, but this is a bit complicated when you get a mixture of star-sized and pixel/auto-sized columns. I found the formula that seems to work and even accounted for some special conditions, but I have a feeling it might still not always work. So far though – it works better than the base-WPF  implementation, which makes me happy to share it with you and use it at work tomorrow.

There is something I guess I learned about making a UserControl focusable here and I think my solution might not be very clean, but it seems like the control works with keyboard to – you can focus it using the Tab key or by clicking on the splitter with your mouse.

The Code:

<UserControl
    x:Class="SimpleGridSplitterTest.SimpleGridSplitter"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    mc:Ignorable="d"
    d:DesignHeight="300"
    d:DesignWidth="300"
    Background="Black"
    Opacity="0.5"
    MouseEnter="UserControl_MouseEnter"
    MouseLeftButtonDown="UserControl_MouseLeftButtonDown"
    MouseMove="UserControl_MouseMove"
    MouseLeftButtonUp="UserControl_MouseLeftButtonUp" />
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace SimpleGridSplitterTest
{
    /// <summary>
    /// Interaction logic for SimpleGridSplitter.xaml
    /// </summary>
    public partial class SimpleGridSplitter : UserControl
    {
        private const double DefaultKeyboardIncrement = 1d;
        private Point lastPosition;
        private bool dragging;

        #region ResizeBehavior
        /// <summary>
        /// ResizeBehavior Dependency Property
        /// </summary>
        public static readonly DependencyProperty ResizeBehaviorProperty =
            DependencyProperty.Register(
                "ResizeBehavior",
                typeof(GridResizeBehavior),
                typeof(SimpleGridSplitter),
                new PropertyMetadata(GridResizeBehavior.BasedOnAlignment));

        /// <summary>
        /// Gets or sets the ResizeBehavior property. This dependency property 
        /// indicates which columns or rows are resized relative
        /// to the column or row for which the GridSplitter control is defined.
        /// </summary>
        public GridResizeBehavior ResizeBehavior
        {
            get { return (GridResizeBehavior)GetValue(ResizeBehaviorProperty); }
            set { SetValue(ResizeBehaviorProperty, value); }
        }
        #endregion

        #region ResizeDirection
        /// <summary>
        /// ResizeDirection Dependency Property
        /// </summary>
        public static readonly DependencyProperty ResizeDirectionProperty =
            DependencyProperty.Register(
                "ResizeDirection",
                typeof(GridResizeDirection),
                typeof(SimpleGridSplitter),
                new PropertyMetadata(GridResizeDirection.Auto, OnResizeDirectionChanged));

        /// <summary>
        /// Gets or sets the ResizeDirection property. This dependency property 
        /// indicates whether the SimpleGridSplitter control resizes rows or columns.
        /// </summary>
        public GridResizeDirection ResizeDirection
        {
            get { return (GridResizeDirection)GetValue(ResizeDirectionProperty); }
            set { SetValue(ResizeDirectionProperty, value); }
        }

        /// <summary>
        /// Handles changes to the ResizeDirection property.
        /// </summary>
        /// <param name="d">
        /// The <see cref="DependencyObject"/> on which
        /// the property has changed value.
        /// </param>
        /// <param name="e">
        /// Event data that is issued by any event that
        /// tracks changes to the effective value of this property.
        /// </param>
        private static void OnResizeDirectionChanged(
            DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var target = (SimpleGridSplitter)d;
            GridResizeDirection oldResizeDirection = (GridResizeDirection)e.OldValue;
            GridResizeDirection newResizeDirection = target.ResizeDirection;
            target.OnResizeDirectionChanged(oldResizeDirection, newResizeDirection);
        }

        /// <summary>
        /// Provides derived classes an opportunity to handle changes
        /// to the ResizeDirection property.
        /// </summary>
        /// <param name="oldResizeDirection">The old ResizeDirection value</param>
        /// <param name="newResizeDirection">The new ResizeDirection value</param>
        protected virtual void OnResizeDirectionChanged(
            GridResizeDirection oldResizeDirection, GridResizeDirection newResizeDirection)
        {
            this.DetermineResizeCursor();
        }
        #endregion

        #region KeyboardIncrement
        /// <summary>
        /// KeyboardIncrement Dependency Property
        /// </summary>
        public static readonly DependencyProperty KeyboardIncrementProperty =
            DependencyProperty.Register(
                "KeyboardIncrement",
                typeof(double),
                typeof(SimpleGridSplitter),
                new FrameworkPropertyMetadata(DefaultKeyboardIncrement));

        /// <summary>
        /// Gets or sets the KeyboardIncrement property. This dependency property 
        /// indicates the distance that each press of an arrow key moves
        /// a SimpleGridSplitter control.
        /// </summary>
        public double KeyboardIncrement
        {
            get { return (double)GetValue(KeyboardIncrementProperty); }
            set { SetValue(KeyboardIncrementProperty, value); }
        }
        #endregion

        #region DetermineEffectiveResizeDirection()
        private GridResizeDirection DetermineEffectiveResizeDirection()
        {
            if (ResizeDirection == GridResizeDirection.Columns)
            {
                return GridResizeDirection.Columns;
            }

            if (ResizeDirection == GridResizeDirection.Rows)
            {
                return GridResizeDirection.Rows;
            }

            // Based on GridResizeDirection Enumeration documentation from
            // http://msdn.microsoft.com/en-us/library/system.windows.controls.gridresizedirection(v=VS.110).aspx

            // Space is redistributed based on the values of the HorizontalAlignment, VerticalAlignment, ActualWidth, and ActualHeight properties of the SimpleGridSplitter.

            // * If the HorizontalAlignment is not set to Stretch, space is redistributed between columns.
            if (HorizontalAlignment != HorizontalAlignment.Stretch)
            {
                return GridResizeDirection.Columns;
            }

            // * If the HorizontalAlignment is set to Stretch and the VerticalAlignment is not set to Stretch, space is redistributed between rows.
            if (this.HorizontalAlignment == HorizontalAlignment.Stretch &&
                this.VerticalAlignment != VerticalAlignment.Stretch)
            {
                return GridResizeDirection.Rows;
            }

            // * If the following conditions are true, space is redistributed between columns:
            //   * The HorizontalAlignment is set to Stretch.
            //   * The VerticalAlignment is set to Stretch.
            //   * The ActualWidth is less than or equal to the ActualHeight.
            if (this.HorizontalAlignment == HorizontalAlignment.Stretch &&
                this.VerticalAlignment == VerticalAlignment.Stretch &&
                this.ActualWidth <= this.ActualHeight)
            {
                return GridResizeDirection.Columns;
            }

            // * If the following conditions are true, space is redistributed between rows:
            //   * HorizontalAlignment is set to Stretch.
            //   * VerticalAlignment is set to Stretch.
            //   * ActualWidth is greater than the ActualHeight.
            //if (this.HorizontalAlignment == HorizontalAlignment.Stretch &&
            //    this.VerticalAlignment == VerticalAlignment.Stretch &&
            //    this.ActualWidth > this.ActualHeight)
            {
                return GridResizeDirection.Rows;
            }
        } 
        #endregion

        #region DetermineEffectiveResizeBehavior()
        private GridResizeBehavior DetermineEffectiveResizeBehavior()
        {
            if (ResizeBehavior == GridResizeBehavior.CurrentAndNext)
            {
                return GridResizeBehavior.CurrentAndNext;
            }

            if (ResizeBehavior == GridResizeBehavior.PreviousAndCurrent)
            {
                return GridResizeBehavior.PreviousAndCurrent;
            }

            if (ResizeBehavior == GridResizeBehavior.PreviousAndNext)
            {
                return GridResizeBehavior.PreviousAndNext;
            }

            // Based on GridResizeBehavior Enumeration documentation from
            // http://msdn.microsoft.com/en-us/library/system.windows.controls.gridresizebehavior(v=VS.110).aspx

            // Space is redistributed based on the value of the
            // HorizontalAlignment and VerticalAlignment properties.

            var effectiveResizeDirection =
                DetermineEffectiveResizeDirection();

            // If the value of the ResizeDirection property specifies
            // that space is redistributed between rows,
            // the redistribution follows these guidelines:

            if (effectiveResizeDirection == GridResizeDirection.Rows)
            {
                // * When the VerticalAlignment property is set to Top,
                //   space is redistributed between the row that is specified
                //   for the GridSplitter and the row that is above that row.
                if (this.VerticalAlignment == VerticalAlignment.Top)
                {
                    return GridResizeBehavior.PreviousAndCurrent;
                }

                // * When the VerticalAlignment property is set to Bottom,
                //   space is redistributed between the row that is specified
                //   for the GridSplitter and the row that is below that row.
                if (this.VerticalAlignment == VerticalAlignment.Bottom)
                {
                    return GridResizeBehavior.CurrentAndNext;
                }

                // * When the VerticalAlignment property is set to Center,
                //   space is redistributed between the row that is above and
                //   the row that is below the row that is specified
                //   for the GridSplitter.
                // * When the VerticalAlignment property is set to Stretch,
                //   space is redistributed between the row that is above
                //   and the row that is below the row that is specified
                //   for the GridSplitter.
                return GridResizeBehavior.PreviousAndNext;
            }

            // If the value of the ResizeDirection property specifies
            // that space is redistributed between columns,
            // the redistribution follows these guidelines:

            // * When the HorizontalAlignment property is set to Left,
            //   space is redistributed between the column that is specified
            //   for the GridSplitter and the column that is to the left.
            if (this.HorizontalAlignment == HorizontalAlignment.Left)
            {
                return GridResizeBehavior.PreviousAndCurrent;
            }

            // * When the HorizontalAlignment property is set to Right,
            //   space is redistributed between the column that is specified
            //   for the GridSplitter and the column that is to the right.
            if (this.HorizontalAlignment == HorizontalAlignment.Right)
            {
                return GridResizeBehavior.CurrentAndNext;
            }

            // * When the HorizontalAlignment property is set to Center,
            //   space is redistributed between the columns that are to the left
            //   and right of the column that is specified for the GridSplitter.
            // * When the HorizontalAlignment property is set to Stretch,
            //   space is redistributed between the columns that are to the left
            //   and right of the column that is specified for the GridSplitter.
            return GridResizeBehavior.PreviousAndNext;
        } 
        #endregion

        #region DetermineResizeCursor()
        private void DetermineResizeCursor()
        {
            var effectiveResizeDirection =
                this.DetermineEffectiveResizeDirection();

            if (effectiveResizeDirection == GridResizeDirection.Columns)
            {
                this.Cursor = Cursors.SizeWE;
            }
            else
            {
                this.Cursor = Cursors.SizeNS;
            }
        } 
        #endregion

        #region CTOR - SimpleGridSplitter()
        // The below code throws for some reason, so the focus properties 
        // for keyboard support had to be moved to the constructor.
        // 
        //static SimpleGridSplitter()
        //{
        //    FocusableProperty.OverrideMetadata(
        //        typeof(SimpleGridSplitter),
        //        new UIPropertyMetadata(true));
        //    IsTabStopProperty.OverrideMetadata(
        //        typeof(SimpleGridSplitter),
        //        new UIPropertyMetadata(true));
        //}

        public SimpleGridSplitter()
        {
            this.Focusable = true;
            this.IsTabStop = true;
            //FocusManager.SetIsFocusScope(this, true);
            this.InitializeComponent();
            this.DetermineResizeCursor();
        } 
        #endregion

        #region Mouse event handlers
        private void UserControl_MouseEnter(object sender, MouseEventArgs e)
        {
            this.DetermineResizeCursor();
        }

        private void UserControl_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            this.CaptureMouse();
            var grid = GetGrid();
            this.lastPosition = e.GetPosition(grid);
            this.dragging = true;
            this.Focus();
        }

        private void UserControl_MouseMove(object sender, MouseEventArgs e)
        {
            if (!dragging)
            {
                return;
            }

            GridResizeDirection effectiveResizeDirection =
                this.DetermineEffectiveResizeDirection();

            var grid = GetGrid();
            var position = e.GetPosition(grid);

            if (effectiveResizeDirection == GridResizeDirection.Columns)
            {
                var deltaX = position.X - this.lastPosition.X;
                this.ResizeColumns(grid, deltaX);
            }
            else
            {
                var deltaY = position.Y - this.lastPosition.Y;
                this.ResizeRows(grid, deltaY);
            }

            this.lastPosition = position;
        }

        private void UserControl_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            this.ReleaseMouseCapture();
            this.dragging = false;
        }

        protected override void OnKeyDown(KeyEventArgs e)
        {
            base.OnKeyDown(e);

            GridResizeDirection effectiveResizeDirection =
                this.DetermineEffectiveResizeDirection();

            if (effectiveResizeDirection == GridResizeDirection.Columns)
            {
                if (e.Key == Key.Left)
                {
                    this.ResizeColumns(this.GetGrid(), -KeyboardIncrement);
                    e.Handled = true;
                }
                else if (e.Key == Key.Right)
                {
                    this.ResizeColumns(this.GetGrid(), KeyboardIncrement);
                    e.Handled = true;
                }
            }
            else
            {
                if (e.Key == Key.Up)
                {
                    this.ResizeRows(this.GetGrid(), -KeyboardIncrement);
                    e.Handled = true;
                }
                else if (e.Key == Key.Down)
                {
                    this.ResizeRows(this.GetGrid(), KeyboardIncrement);
                    e.Handled = true;
                }
            }
        }
        #endregion

        #region ResizeColumns()
        private void ResizeColumns(Grid grid, double deltaX)
        {
            GridResizeBehavior effectiveGridResizeBehavior =
                this.DetermineEffectiveResizeBehavior();

            int column = Grid.GetColumn(this);
            int leftColumn;
            int rightColumn;

            switch (effectiveGridResizeBehavior)
            {
                case GridResizeBehavior.PreviousAndCurrent:
                    leftColumn = column - 1;
                    rightColumn = column;
                    break;
                case GridResizeBehavior.PreviousAndNext:
                    leftColumn = column - 1;
                    rightColumn = column + 1;
                    break;
                default:
                    leftColumn = column;
                    rightColumn = column + 1;
                    break;
            }

            if (rightColumn >= grid.ColumnDefinitions.Count)
            {
                return;
            }

            var leftColumnDefinition = grid.ColumnDefinitions[leftColumn];
            var rightColumnDefinition = grid.ColumnDefinitions[rightColumn];
            var leftColumnGridUnitType = leftColumnDefinition.Width.GridUnitType;
            var rightColumnGridUnitType = rightColumnDefinition.Width.GridUnitType;
            var leftColumnActualWidth = leftColumnDefinition.ActualWidth;
            var rightColumnActualWidth = rightColumnDefinition.ActualWidth;
            var leftColumnMaxWidth = leftColumnDefinition.MaxWidth;
            var rightColumnMaxWidth = rightColumnDefinition.MaxWidth;
            var leftColumnMinWidth = leftColumnDefinition.MinWidth;
            var rightColumnMinWidth = rightColumnDefinition.MinWidth;

            //deltaX = 200;
            if (leftColumnActualWidth + deltaX > leftColumnMaxWidth)
            {
                deltaX = Math.Max(
                    0,
                    leftColumnDefinition.MaxWidth - leftColumnActualWidth);
            }

            if (leftColumnActualWidth + deltaX < leftColumnMinWidth)
            {
                deltaX = Math.Min(
                    0,
                    leftColumnDefinition.MinWidth - leftColumnActualWidth);
            }

            if (rightColumnActualWidth - deltaX > rightColumnMaxWidth)
            {
                deltaX = -Math.Max(
                    0,
                    rightColumnDefinition.MaxWidth - rightColumnActualWidth);
            }

            if (rightColumnActualWidth - deltaX < rightColumnMinWidth)
            {
                deltaX = -Math.Min(
                    0,
                    rightColumnDefinition.MinWidth - rightColumnActualWidth);
            }

            var newLeftColumnActualWidth = leftColumnActualWidth + deltaX;
            var newRightColumnActualWidth = rightColumnActualWidth - deltaX;

            grid.BeginInit();

            double totalStarColumnsWidth = 0;
            double starColumnsAvailableWidth = grid.ActualWidth;

            if (leftColumnGridUnitType ==
                    GridUnitType.Star ||
                rightColumnGridUnitType ==
                    GridUnitType.Star)
            {
                foreach (var columnDefinition in grid.ColumnDefinitions)
                {
                    if (columnDefinition.Width.GridUnitType ==
                        GridUnitType.Star)
                    {
                        totalStarColumnsWidth +=
                            columnDefinition.Width.Value;
                    }
                    else
                    {
                        starColumnsAvailableWidth -=
                            columnDefinition.ActualWidth;
                    }
                }
            }

            if (leftColumnGridUnitType == GridUnitType.Star)
            {
                if (rightColumnGridUnitType == GridUnitType.Star)
                {
                    // If both columns are star columns
                    // - totalStarColumnsWidth won't change and
                    // as much as one of the columns grows
                    // - the other column will shrink by the same value.

                    // If there is no width available to star columns
                    // - we can't resize two of them.
                    if (starColumnsAvailableWidth < 1)
                    {
                        return;
                    }

                    var oldStarWidth = leftColumnDefinition.Width.Value;
                    var newStarWidth = Math.Max(
                        0,
                        totalStarColumnsWidth * newLeftColumnActualWidth /
                            starColumnsAvailableWidth);
                    leftColumnDefinition.Width =
                        new GridLength(newStarWidth, GridUnitType.Star);

                    rightColumnDefinition.Width =
                        new GridLength(
                            Math.Max(
                                0,
                                rightColumnDefinition.Width.Value -
                                    newStarWidth + oldStarWidth),
                            GridUnitType.Star);
                }
                else
                {
                    var newStarColumnsAvailableWidth =
                        starColumnsAvailableWidth +
                        rightColumnActualWidth -
                        newRightColumnActualWidth;

                    if (newStarColumnsAvailableWidth - newLeftColumnActualWidth >= 1)
                    {
                        var newStarWidth = Math.Max(
                            0,
                            (totalStarColumnsWidth -
                             leftColumnDefinition.Width.Value) *
                            newLeftColumnActualWidth /
                            (newStarColumnsAvailableWidth - newLeftColumnActualWidth));

                        leftColumnDefinition.Width =
                            new GridLength(newStarWidth, GridUnitType.Star);
                    }
                }
            }
            else
            {
                leftColumnDefinition.Width =
                    new GridLength(
                        newLeftColumnActualWidth, GridUnitType.Pixel);
            }

            if (rightColumnGridUnitType ==
                GridUnitType.Star)
            {
                if (leftColumnGridUnitType !=
                    GridUnitType.Star)
                {
                    var newStarColumnsAvailableWidth =
                        starColumnsAvailableWidth +
                        leftColumnActualWidth -
                        newLeftColumnActualWidth;

                    if (newStarColumnsAvailableWidth - newRightColumnActualWidth >= 1)
                    {
                        var newStarWidth = Math.Max(
                            0,
                            (totalStarColumnsWidth -
                             rightColumnDefinition.Width.Value) *
                            newRightColumnActualWidth /
                            (newStarColumnsAvailableWidth - newRightColumnActualWidth));
                        rightColumnDefinition.Width =
                            new GridLength(newStarWidth, GridUnitType.Star);
                    }
                }
                // else handled in the left column width calculation block
            }
            else
            {
                rightColumnDefinition.Width =
                    new GridLength(
                        newRightColumnActualWidth, GridUnitType.Pixel);
            }

            grid.EndInit();
        } 
        #endregion

        #region ResizeRows()
        private void ResizeRows(Grid grid, double deltaX)
        {
            GridResizeBehavior effectiveGridResizeBehavior =
                this.DetermineEffectiveResizeBehavior();

            int row = Grid.GetRow(this);
            int upperRow;
            int lowerRow;

            switch (effectiveGridResizeBehavior)
            {
                case GridResizeBehavior.PreviousAndCurrent:
                    upperRow = row - 1;
                    lowerRow = row;
                    break;
                case GridResizeBehavior.PreviousAndNext:
                    upperRow = row - 1;
                    lowerRow = row + 1;
                    break;
                default:
                    upperRow = row;
                    lowerRow = row + 1;
                    break;
            }

            if (lowerRow >= grid.RowDefinitions.Count)
            {
                return;
            }

            var upperRowDefinition = grid.RowDefinitions[upperRow];
            var lowerRowDefinition = grid.RowDefinitions[lowerRow];
            var upperRowGridUnitType = upperRowDefinition.Height.GridUnitType;
            var lowerRowGridUnitType = lowerRowDefinition.Height.GridUnitType;
            var upperRowActualHeight = upperRowDefinition.ActualHeight;
            var lowerRowActualHeight = lowerRowDefinition.ActualHeight;
            var upperRowMaxHeight = upperRowDefinition.MaxHeight;
            var lowerRowMaxHeight = lowerRowDefinition.MaxHeight;
            var upperRowMinHeight = upperRowDefinition.MinHeight;
            var lowerRowMinHeight = lowerRowDefinition.MinHeight;

            //deltaX = 200;
            if (upperRowActualHeight + deltaX > upperRowMaxHeight)
            {
                deltaX = Math.Max(
                    0,
                    upperRowDefinition.MaxHeight - upperRowActualHeight);
            }

            if (upperRowActualHeight + deltaX < upperRowMinHeight)
            {
                deltaX = Math.Min(
                    0,
                    upperRowDefinition.MinHeight - upperRowActualHeight);
            }

            if (lowerRowActualHeight - deltaX > lowerRowMaxHeight)
            {
                deltaX = -Math.Max(
                    0,
                    lowerRowDefinition.MaxHeight - lowerRowActualHeight);
            }

            if (lowerRowActualHeight - deltaX < lowerRowMinHeight)
            {
                deltaX = -Math.Min(
                    0,
                    lowerRowDefinition.MinHeight - lowerRowActualHeight);
            }

            var newUpperRowActualHeight = upperRowActualHeight + deltaX;
            var newLowerRowActualHeight = lowerRowActualHeight - deltaX;

            grid.BeginInit();

            double totalStarRowsHeight = 0;
            double starRowsAvailableHeight = grid.ActualHeight;

            if (upperRowGridUnitType ==
                    GridUnitType.Star ||
                lowerRowGridUnitType ==
                    GridUnitType.Star)
            {
                foreach (var rowDefinition in grid.RowDefinitions)
                {
                    if (rowDefinition.Height.GridUnitType ==
                        GridUnitType.Star)
                    {
                        totalStarRowsHeight +=
                            rowDefinition.Height.Value;
                    }
                    else
                    {
                        starRowsAvailableHeight -=
                            rowDefinition.ActualHeight;
                    }
                }
            }

            if (upperRowGridUnitType == GridUnitType.Star)
            {
                if (lowerRowGridUnitType == GridUnitType.Star)
                {
                    // If both rows are star rows
                    // - totalStarRowsHeight won't change and
                    // as much as one of the rows grows
                    // - the other row will shrink by the same value.

                    // If there is no width available to star rows
                    // - we can't resize two of them.
                    if (starRowsAvailableHeight < 1)
                    {
                        return;
                    }

                    var oldStarHeight = upperRowDefinition.Height.Value;
                    var newStarHeight = Math.Max(
                        0,
                        totalStarRowsHeight * newUpperRowActualHeight /
                            starRowsAvailableHeight);
                    upperRowDefinition.Height =
                        new GridLength(newStarHeight, GridUnitType.Star);

                    lowerRowDefinition.Height =
                        new GridLength(
                            Math.Max(
                                0,
                                lowerRowDefinition.Height.Value -
                                    newStarHeight + oldStarHeight),
                            GridUnitType.Star);
                }
                else
                {
                    var newStarRowsAvailableHeight =
                        starRowsAvailableHeight +
                        lowerRowActualHeight -
                        newLowerRowActualHeight;

                    if (newStarRowsAvailableHeight - newUpperRowActualHeight >= 1)
                    {
                        var newStarHeight = Math.Max(
                            0,
                            (totalStarRowsHeight -
                             upperRowDefinition.Height.Value) *
                            newUpperRowActualHeight /
                            (newStarRowsAvailableHeight - newUpperRowActualHeight));

                        upperRowDefinition.Height =
                            new GridLength(newStarHeight, GridUnitType.Star);
                    }
                }
            }
            else
            {
                upperRowDefinition.Height =
                    new GridLength(
                        newUpperRowActualHeight, GridUnitType.Pixel);
            }

            if (lowerRowGridUnitType ==
                GridUnitType.Star)
            {
                if (upperRowGridUnitType !=
                    GridUnitType.Star)
                {
                    var newStarRowsAvailableHeight =
                        starRowsAvailableHeight +
                        upperRowActualHeight -
                        newUpperRowActualHeight;

                    if (newStarRowsAvailableHeight - newLowerRowActualHeight >= 1)
                    {
                        var newStarHeight = Math.Max(
                            0,
                            (totalStarRowsHeight -
                             lowerRowDefinition.Height.Value) *
                            newLowerRowActualHeight /
                            (newStarRowsAvailableHeight - newLowerRowActualHeight));
                        lowerRowDefinition.Height =
                            new GridLength(newStarHeight, GridUnitType.Star);
                    }
                }
                // else handled in the upper row width calculation block
            }
            else
            {
                lowerRowDefinition.Height =
                    new GridLength(
                        newLowerRowActualHeight, GridUnitType.Pixel);
            }

            grid.EndInit();
        }
        #endregion

        #region GetGrid()
        private Grid GetGrid()
        {
            var grid = this.Parent as Grid;

            if (grid == null)
            {
                throw new InvalidOperationException(
                    "SimpleGridSplitter only works when hosted in a Grid.");
            }
            return grid;
        } 
        #endregion
    }
}

Sample:

<Grid
    Background="Yellow">
    <Grid.RowDefinitions>
        <RowDefinition
            Height="40*" />
        <RowDefinition
            Height="40" />
        <RowDefinition
            Height="40*" />
        <RowDefinition
            Height="40" />
        <RowDefinition
            Height="40*" />

        <RowDefinition
            Height="40" />
        <RowDefinition
            Height="40*" />
        <RowDefinition
            Height="40" />
        <RowDefinition
            Height="40*" />
        <RowDefinition
            Height="40" />

        <RowDefinition
            Height="40*" />
        <RowDefinition
            Height="40*" />
        <RowDefinition
            Height="40" />
        <RowDefinition
            Height="40" />
        <RowDefinition
            Height="40*" />

        <RowDefinition
            Height="300*" />
    </Grid.RowDefinitions>

    <local:SimpleGridSplitter
        Grid.Row="0"
        Height="6"
        VerticalAlignment="Bottom"
        HorizontalAlignment="Stretch" />
    <local:SimpleGridSplitter
        Grid.Row="1"
        Height="6"
        VerticalAlignment="Bottom"
        HorizontalAlignment="Stretch" />
    <local:SimpleGridSplitter
        Grid.Row="2"
        Height="6"
        VerticalAlignment="Bottom"
        HorizontalAlignment="Stretch" />
    <local:SimpleGridSplitter
        Grid.Row="3"
        Height="6"
        VerticalAlignment="Bottom"
        HorizontalAlignment="Stretch" />
    <local:SimpleGridSplitter
        Grid.Row="4"
        Height="6"
        VerticalAlignment="Bottom"
        HorizontalAlignment="Stretch" />
    <local:SimpleGridSplitter
        Grid.Row="5"
        Height="6"
        VerticalAlignment="Bottom"
        HorizontalAlignment="Stretch" />
    <local:SimpleGridSplitter
        Grid.Row="6"
        Height="6"
        VerticalAlignment="Bottom"
        HorizontalAlignment="Stretch" />
    <local:SimpleGridSplitter
        Grid.Row="7"
        Height="6"
        VerticalAlignment="Bottom"
        HorizontalAlignment="Stretch" />
    <local:SimpleGridSplitter
        Grid.Row="8"
        Height="6"
        VerticalAlignment="Bottom"
        HorizontalAlignment="Stretch" />
    <local:SimpleGridSplitter
        Grid.Row="9"
        Height="6"
        VerticalAlignment="Bottom"
        HorizontalAlignment="Stretch" />
    <local:SimpleGridSplitter
        Grid.Row="10"
        Height="6"
        VerticalAlignment="Bottom"
        HorizontalAlignment="Stretch" />
    <local:SimpleGridSplitter
        Grid.Row="11"
        Height="6"
        VerticalAlignment="Bottom"
        HorizontalAlignment="Stretch" />
    <local:SimpleGridSplitter
        Grid.Row="12"
        Height="6"
        VerticalAlignment="Bottom"
        HorizontalAlignment="Stretch" />
    <local:SimpleGridSplitter
        Grid.Row="13"
        Height="6"
        VerticalAlignment="Bottom"
        HorizontalAlignment="Stretch" />
    <local:SimpleGridSplitter
        Grid.Row="14"
        Height="6"
        VerticalAlignment="Bottom"
        HorizontalAlignment="Stretch" />
</Grid>
</TabItem>
<TabItem
Header="SimpleGridSplitter/Columns">
<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition
            Width="40*" />
        <ColumnDefinition
            Width="Auto" />
        <ColumnDefinition
            Width="40*" />
        <ColumnDefinition
            Width="Auto" />
        <ColumnDefinition
            Width="40*" />
        <ColumnDefinition
            Width="Auto" />
        <ColumnDefinition
            Width="40*" />
        <ColumnDefinition
            Width="Auto" />
        <ColumnDefinition
            Width="40*" />
        <ColumnDefinition
            Width="Auto" />

        <ColumnDefinition
            Width="40*" />
        <ColumnDefinition
            Width="Auto" />
        <ColumnDefinition
            Width="40*" />
        <ColumnDefinition
            Width="Auto" />
        <ColumnDefinition
            Width="40*" />
        <ColumnDefinition
            Width="Auto" />
        <ColumnDefinition
            Width="40*" />
        <ColumnDefinition
            Width="Auto" />
        <ColumnDefinition
            Width="40*" />
        <ColumnDefinition
            Width="Auto" />

        <ColumnDefinition
            Width="40*" />
        <ColumnDefinition
            Width="Auto" />
        <ColumnDefinition
            Width="40*" />
        <ColumnDefinition
            Width="Auto" />
        <ColumnDefinition
            Width="40*" />
        <ColumnDefinition
            Width="Auto" />
        <ColumnDefinition
            Width="40*" />
        <ColumnDefinition
            Width="Auto" />
        <ColumnDefinition
            Width="40*" />
        <ColumnDefinition
            Width="Auto" />

        <ColumnDefinition
            Width="300*" />
    </Grid.ColumnDefinitions>

    <local:SimpleGridSplitter
        Grid.Column="1"
        Width="6"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch" />
    <local:SimpleGridSplitter
        Grid.Column="3"
        Width="6"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch" />
    <local:SimpleGridSplitter
        Grid.Column="5"
        Width="6"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch" />
    <local:SimpleGridSplitter
        Grid.Column="7"
        Width="6"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch" />
    <local:SimpleGridSplitter
        Grid.Column="9"
        Width="6"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch" />
    <local:SimpleGridSplitter
        Grid.Column="11"
        Width="6"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch" />
    <local:SimpleGridSplitter
        Grid.Column="13"
        Width="6"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch" />
    <local:SimpleGridSplitter
        Grid.Column="15"
        Width="6"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch" />
    <local:SimpleGridSplitter
        Grid.Column="17"
        Width="6"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch" />
    <local:SimpleGridSplitter
        Grid.Column="19"
        Width="6"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch" />
    <local:SimpleGridSplitter
        Grid.Column="21"
        Width="6"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch" />
    <local:SimpleGridSplitter
        Grid.Column="23"
        Width="6"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch" />
    <local:SimpleGridSplitter
        Grid.Column="25"
        Width="6"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch" />
    <local:SimpleGridSplitter
        Grid.Column="27"
        Width="6"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch" />
    <local:SimpleGridSplitter
        Grid.Column="29"
        Width="6"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch" />
</Grid>
</TabItem>
<TabItem
Header="GridSplitter">
<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition
            Width="40*" />
        <ColumnDefinition
            Width="Auto" />
        <ColumnDefinition
            Width="40*" />
        <ColumnDefinition
            Width="Auto" />
        <ColumnDefinition
            Width="40*" />
        <ColumnDefinition
            Width="Auto" />
        <ColumnDefinition
            Width="40*" />
        <ColumnDefinition
            Width="Auto" />
        <ColumnDefinition
            Width="40*" />
        <ColumnDefinition
            Width="Auto" />

        <ColumnDefinition
            Width="40*" />
        <ColumnDefinition
            Width="Auto" />
        <ColumnDefinition
            Width="40*" />
        <ColumnDefinition
            Width="Auto" />
        <ColumnDefinition
            Width="40*" />
        <ColumnDefinition
            Width="Auto" />
        <ColumnDefinition
            Width="40*" />
        <ColumnDefinition
            Width="Auto" />
        <ColumnDefinition
            Width="40*" />
        <ColumnDefinition
            Width="Auto" />

        <ColumnDefinition
            Width="40*" />
        <ColumnDefinition
            Width="Auto" />
        <ColumnDefinition
            Width="40*" />
        <ColumnDefinition
            Width="Auto" />
        <ColumnDefinition
            Width="40*" />
        <ColumnDefinition
            Width="Auto" />
        <ColumnDefinition
            Width="40*" />
        <ColumnDefinition
            Width="Auto" />
        <ColumnDefinition
            Width="40*" />
        <ColumnDefinition
            Width="Auto" />

        <ColumnDefinition
            Width="300*" />
    </Grid.ColumnDefinitions>

    <GridSplitter
        Grid.Column="1"
        Width="6"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch" />
    <GridSplitter
        Grid.Column="3"
        Width="6"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch" />
    <GridSplitter
        Grid.Column="5"
        Width="6"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch" />
    <GridSplitter
        Grid.Column="7"
        Width="6"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch" />
    <GridSplitter
        Grid.Column="9"
        Width="6"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch" />
    <GridSplitter
        Grid.Column="11"
        Width="6"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch" />
    <GridSplitter
        Grid.Column="13"
        Width="6"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch" />
    <GridSplitter
        Grid.Column="15"
        Width="6"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch" />
    <GridSplitter
        Grid.Column="17"
        Width="6"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch" />
    <GridSplitter
        Grid.Column="19"
        Width="6"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch" />
    <GridSplitter
        Grid.Column="21"
        Width="6"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch" />
    <GridSplitter
        Grid.Column="23"
        Width="6"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch" />
    <GridSplitter
        Grid.Column="25"
        Width="6"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch" />
    <GridSplitter
        Grid.Column="27"
        Width="6"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch" />
    <GridSplitter
        Grid.Column="29"
        Width="6"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch" />
</Grid>

Full Source

Zipped and shared at DropBox.

Note: I created a WinRT (Windows 8 Metro Style App) version of this control that is now available as part of the WinRT XAML Toolkit that is implemented as a custom control, so it should be easier to retemplate. It is also likely to see some more features, so if you need it for WinRT – grab it from CodePlex or if you need it for WPF/Silverlight – feel free to port it over.

Advertisements
Tagged , , , , ,

12 thoughts on “Fixing WPF GridSplitter

  1. […] has one either. It probably just isn’t the most usable paradigm for touch UIs, though my SimpleGridSplitter should be a straightforward port if you really want to use […]

  2. […] how much the GridSplitter will be able to resize them. I actually take these into account in my SimpleGridSplitter implementation, so if you have lots of rows or columns in your WPF app – you can use that […]

  3. Manoj Bhatt says:

    Hi,
    I was reading your article and I would like to appreciate you for making it very simple and understandable. This article gives me a basic idea of GridSplitter control in WPF. Some good articles also I was found during searching time which also explained very well about WPF GridSplitter Control, that post url are…
    http://www.c-sharpcorner.com/uploadfile/nipuntomar/gridsplitter-with-tab-controls-in-wpf-4-using-expression-blend-4-0/
    and
    http://www.mindstick.com/Articles/6c64bf8a-6704-4bd7-90ab-524689c390e5/?GridSpliter%20control%20in%20WPF

  4. Niel says:

    Thank you thank you thank you! This has been a lifesaver!

    I’ve been looking into how to modify your code to cause the double-click event to make the splitter move all the way up in order to maximize the row size. Basically so the user can quickly expand the work area.

    Probably pretty simple. Any tips on how I would do this?

    Thanks again for your great work!

    • xyzzer says:

      Glad you find it helpful. Not sure what you mean by maximize – do you want all other rows to be of size 0 so one of the two next to the GridSplitter uses all of the Grid?

      • Niel says:

        Almost. I would like to make the double-click event cause the same exact behavior as if I dragged the splitter all the way to the top with the mouse. For completeness, double-clicking again would cause the splitter to go all the way to the bottom. Kinda like in Excel when double clicking on the row/column splitters. They maximize as far as they need to to fit the width or height of the data.

  5. Niel says:

    So not exactly like Excel, but similar.

  6. xyzzer says:

    Sounds challenging – I don’t know the answer, but I can think of a few ways to try to approach it depending on what you want to happen with a neighboring row. You can maybe try calling Measure/Arrange on the content to see what its preferred size is and then just set the height of the grid row definition for the row you want to grow to that GridLength value and decrease the next one by the difference. Note that there are lots of special cases depending on your row definitions setup. If some rows are star-sized while others are point-sized – you have to figure out what you want the effect to be. If the next row is not as high as the current one – you need to decide what you want to achieve. If it is OK for your entire grid height to grow – you can just set the height of the RowDefinition corresponding to the selected row.

  7. […] Выходом из ситуации явился найденный на просторах интернета реализованный кем-то GridSplitter. […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: