GridSplitter MinWidth with fixed size

2019-05-02 19:55发布

问题:

I have a Grid with 2 columns separated by a GridSplitter using the following XAML code :

<Grid>
    <Grid.ColumnDefinitions>
    <ColumnDefinition Width="100" MinWidth="20" />
    <ColumnDefinition Width="10" />
    <ColumnDefinition Width="*" MinWidth="100" />
    </Grid.ColumnDefinitions>

    <Rectangle Fill="Blue" />
    <GridSplitter Grid.Column="1" Background="LightGray" HorizontalAlignment="Stretch" />
    <Rectangle Fill="Yellow" Grid.Column="2" />
</Grid>

Problem : The MinWidth of the Column on the right is ignored

  • I definitely need the first column Width to be "100px" when page loads, so It cannot be * sized.
  • I do not want to set a MaxWidth on the first column

*I know that has been adressed before but it always suggest to set column size to * or set a maxWidth on the first column... I don't want that.


Found a solution, but its UGLY! :p, anybody has a cleaner way to achieve what I want... CODELESS (if possible)?

private void Grid_SizeChanged(object sender, SizeChangedEventArgs e)
{
   var g = (Grid)sender;

   Double maxW = e.NewSize.Width - g.ColumnDefinitions[2].MinWidth - g.ColumnDefinitions[1].ActualWidth;
   g.ColumnDefinitions[0].MaxWidth = maxW;
}

回答1:

The basic problem is that a grid splitter works by adjusting the width of the left column only and assumes the right column will star-size to fit the remaining space.

That means the problem you are trying to solve is actually "how do I limit the max width of the left column so that the right column does not get too small?". This is basically what your code sample is doing.

If you want a more portable solution, that you can implement in XAML, create a Silverlight behavior that can be applied to a grid (as shown below). It will attach to the parent grid's SizeChanged event and do pretty much exactly what your code snippet does, but being a behavior you can drag and drop them in Blend or attach them in XAML.

Here is a sample behavior I threw together for you as an example (based on your own code):

MinWidthSplitterBehavior.cs

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

namespace GridSplitterMinWidth
{
    public class MinWidthSplitterBehavior : Behavior<Grid>
    {
        public Grid ParentGrid { get; set; }

        protected override void OnAttached()
        {
            base.OnAttached();
            ParentGrid = this.AssociatedObject as Grid;
            ParentGrid.SizeChanged += parent_SizeChanged;
        }

        void parent_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            if (ParentGrid.ColumnDefinitions.Count == 3)
            {
                Double maxW = e.NewSize.Width - ParentGrid.ColumnDefinitions[2].MinWidth -
                              ParentGrid.ColumnDefinitions[1].ActualWidth;
                ParentGrid.ColumnDefinitions[0].MaxWidth = maxW;
            }
        }

        protected override void OnDetaching()
        {
            base.OnDetaching();
            if (ParentGrid != null)
            {
                ParentGrid.SizeChanged -= parent_SizeChanged;
            }
        }
    }
}

and use it like this:

<UserControl x:Class="GridSplitterMinWidthApp.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:GridSplitterMinWidth="clr-namespace:GridSplitterMinWidth"
    xmlns:interactivity="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
    xmlns:Controls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">

    <Grid x:Name="LayoutRoot" Background="White">
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="100" MinWidth="20" />
                <ColumnDefinition Width="10" />
                <ColumnDefinition Width="*" MinWidth="100" />
            </Grid.ColumnDefinitions>
            <interactivity:Interaction.Behaviors>
                <GridSplitterMinWidth:MinWidthSplitterBehavior/>
            </interactivity:Interaction.Behaviors>
            <Rectangle Fill="Blue" />
            <Controls:GridSplitter Grid.Column="1" Background="LightGray" HorizontalAlignment="Stretch">
            </Controls:GridSplitter>
            <Rectangle Fill="Yellow" Grid.Column="2" />
        </Grid>
    </Grid>
</UserControl>


回答2:

This seems to me to be a bug in the WPF GridSplitter. The root of the problem is using different types of values for the Width column.

This will not work:

<Grid.ColumnDefinitions>
    <ColumnDefinition Width="100" MinWidth="20" />
    <ColumnDefinition Width="10" />
    <ColumnDefinition Width="*" MinWidth="100" />
</Grid.ColumnDefinitions>

Because column [0] has a width of "100", an explicit number type, and column [1] has a width of "*". But this will work:

<Grid.ColumnDefinitions>
    <ColumnDefinition Width="*" MinWidth="20" />
    <ColumnDefinition Width="10" />
    <ColumnDefinition Width="*" MinWidth="100" />
</Grid.ColumnDefinitions>

When you do this, both the left and right "MinWidth" values are respected by the GridSplitter. To me, this means bug in WPF.

In my case, I need to set the left column to "Auto" with a minimum, and the right to "*" with a minimum, but there doesn't seem to be any way to do that without some form of codebehind. My guess is that the simplest solution will be to just * size everything and use a custom behavior to replace the "auto" functionality of the column (since that is a one-column, one-time fix), but I have yet to do that. Hopefully, the XAML will look something like this:

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*" MinWidth="20" />
        <ColumnDefinition Width="10" />
        <ColumnDefinition Width="*" MinWidth="100" />
    </Grid.ColumnDefinitions>
    <interactivity:Interaction.Behaviors>
        <MyBehaviors:GridColumnStarSizedToAutoBehavior Column="0"/>
    </interactivity:Interaction.Behaviors>
    ...
</Grid>        

Update*: I implemented a behavior as noted in this post above. it was not as simple as I was hoping due to Grid limitations. I can not post the solution due to company limitations, but I can give some tips. The biggest problem I ran into is that you can't really get away from star sizing, even with behavior codebehind, because it makes the MinWidth stop working for the star-sized columns.

To compensate for this involved processing all the columns for the grid. You have to take the columns you want auto-sized, determine their children's desired size by getting UIElements by column and using Measure on them, and then changing their star-size-value to a percentage of total available star-size spacing. You also have to adjust non-auto-star-sized columns star-size value accordingly.

As an example, consider a grid with 3 columns: 1st star-sized with MinWidth we want to be auto, 2nd auto-sized without MinWidth, and third star-sized with MinWidth. If the control is 100 units wide, and the content of the first column wants 25 pixels, and the content of the third column wants 5 pixels, the code has to star-size the first and last column as "25*" and "70*" respectively (or any other numbers that get the right ratio).

This was non-trivial, but did work in the end. I did not account for column spanning.

Sorry I can't post the code.