2
votes

I'm trying to achieve something that sounds pretty simple, in WPF, but just can't get around doing it. I have a ScrollViewer which contains two GroupBoxes. First one has it's height set to a fixed value, second one would take what's left of the window but have a MinHeight. Each GroupBox contains a DataGrid.

What i'm trying to do is : The second groupbox should be sized to what's left of the Window and the DataGrid inside of it should be sized to fill the group box and have it's own scrollbar if rows can't all be shown. A scrollbar should appear in the window if i resize the window to be less than GroupBox1.Height+GroupBox2.MinHeight.

The behavior i get now is, the DataGrid in the second groupbox's height grows with the number of lines, thus expanding the Groupbox and having the Scrollviewer's scrollbar show up.

I came up with a little demo-app to show this behavior

WPF:

<Window x:Class="WpfApp1.MainWindow"
    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:local="clr-namespace:WpfApp1"
    mc:Ignorable="d"
    Title="MainWindow"
    Height="400"
    Width="500">
<Grid>
    <ScrollViewer VerticalScrollBarVisibility="Auto">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="150" />
                <RowDefinition Height="*" />
            </Grid.RowDefinitions>
            <GroupBox Header="test1"
                      Grid.Row="0">
                <DataGrid ItemsSource="{Binding Colors}">
                </DataGrid>
            </GroupBox>
            <GroupBox Header="test2"
                      Grid.Row="1"
                      MinHeight="50">
                <DataGrid ItemsSource="{Binding Colors}">
                </DataGrid>
            </GroupBox>
        </Grid>

    </ScrollViewer>
</Grid>

C#

namespace WpfApp1
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = this;
            Colors = new List<Color>();
            for (int i = 1; i < 51; i++)
            {
                byte b = (byte)(i * 5);
                Colors.Add(Color.FromRgb(b,b,b));
            }
        }

        private List<Color> _colors;
        public List<Color> Colors
        {
            get
            {
                return _colors;
            }
            set
            {
                _colors = value;
            }
        }
    }
}

What i'm getting :

What i'm getting

What i would want (sorry for the bad photo-manipulation skills) :

What i would want

Unless, as specified earlier, i resize the window to be smaller than the sum of the group1's fixed size and group2's min size, in which case i want the window's scrollbar.

In which case, i would want it to look like this : (again a mock-up, not an actual screenshot)

Mockup2

Mind you, the example is pretty simple but the window i'm trying to do it in is much more complex and it makes more sense to have a vertical scrollbar than it does in this example.

Thanks!

2
Specify a fixed Height for the ScrollViewer element? - mm8
@mm8 Doing so would make it useless to a resizeable window. Having a small value as ScrollViewer height would make it so that it is always visible and is limiting the space taken by the groups. Having a big value ends up showing the exact same issue. EDIT: I tried it, just to make sure and the issue is there, even for a small ScrollViewer. The 2nd group expands to the grid's content and i just have less space to scroll and the window's resizing becomes useless. - Carl Quirion
I don't think I understand your issue. There is no ScrollViewer on your second picture. So when exactly do you expect this ScrollViewer to show up? - mm8
As mentionned, the ScrollViewer should showup only if i resize my window so that it is too small to show Group1's Height (fixed size) and Group2's MinHeight. - Carl Quirion
Then you should set the Height or the MinHeight of the ScrollViewer to Group1.Height + Group2.Height. - mm8

2 Answers

3
votes

You can simply bind the MaxHeight property of the second GroupBox to the ActualHeight of the container of the ScrollViewer minus the first GroupBox.

Complete example (excluding the code behind which is same as yours.):

<Window.Resources>
    <wpfApp1:SubtractConverter x:Key="SubtractConverter"/>
</Window.Resources>

<Grid Name="Root">
    <ScrollViewer VerticalScrollBarVisibility="Auto">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="150" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <GroupBox
                Name="Test1"
                Header="test1"
                Grid.Row="0">

                <DataGrid ItemsSource="{Binding Colors}"/>
            </GroupBox>
            <GroupBox
                Header="test2"
                Grid.Row="1"
                MinHeight="250">
                <DataGrid ItemsSource="{Binding Colors}"/>

                <GroupBox.MaxHeight>
                    <MultiBinding Converter="{StaticResource SubtractConverter}">
                        <Binding Path="ActualHeight" ElementName="Root"/>
                        <Binding Path="ActualHeight" ElementName="Test1"/>
                    </MultiBinding>
                </GroupBox.MaxHeight>
            </GroupBox>
        </Grid>
    </ScrollViewer>
</Grid>

public class SubtractConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        double[] doubles = values.Cast<double>().ToArray();

        double result = doubles[0];

        for (int i = 1; i < doubles.Length; i++)
        {
            result -= doubles[i];
        }

        return result;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}
1
votes

I don't know if this would be the easiest solution for your problem but you could do something along this line:

<Window x:Class="WpfApp1.MainWindow"
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:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="MainWindow"
Height="400"
Width="500">
<Window.Resources>
    <local:HeightConverter x:Key="HeightConverter"/>
</Window.Resources>
<Grid>
    <ScrollViewer VerticalScrollBarVisibility="Auto" x:Name="MainView">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="150" />
                <RowDefinition Height="*" />
            </Grid.RowDefinitions>
            <GroupBox Header="test1"
                  Grid.Row="0">
                <DataGrid ItemsSource="{Binding Colors}">
                </DataGrid>
            </GroupBox>
            <GroupBox Header="test2"
                  Grid.Row="1"
                  x:Name="grpBox2"
                  MinHeight="50">
                <GroupBox.Height>
                    <MultiBinding Converter="{StaticResource HeightConverter}" ConverterParameter="150">
                        <Binding Path="ActualHeight" ElementName="MainView" />
                        <Binding Path="MinHeight" ElementName="grpBox2" />
                    </MultiBinding>
                </GroupBox.Height>
                <DataGrid ItemsSource="{Binding Colors}">
                </DataGrid>
            </GroupBox>
        </Grid>

    </ScrollViewer>
</Grid>

And for the converter something like this:

public class HeightConverter : IMultiValueConverter
{
   public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
   {
        if (values == null || parameter == null || values[0] == null || values[1] == null)
        {
            return null;
        }

        var currentWindowHeight = double.Parse(values[0].ToString());
        var currentMinHeight = double.Parse(values[1].ToString());

        var currentTopWindowHeight = double.Parse(parameter.ToString());

        var newHeight = currentWindowHeight - currentTopWindowHeight;

        if (newHeight < currentMinHeight)
            newHeight = currentMinHeight;

        return newHeight;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}