How to have Grid row height take only the space it

2019-08-21 05:44发布

I want to have a header, then below that a ScrollViewer with an ItemsControl, then below that a footer. Something like:

<Window x:Class="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"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid ShowGridLines="True">
        <Grid.Resources>
            <Style TargetType="TextBlock">
                <Setter Property="FontSize" Value="36"/>
            </Style>
        </Grid.Resources>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <TextBlock Grid.Row="0">Header</TextBlock>
        <ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
            <ItemsControl>
                <ItemsControl.Items>
                    <TextBlock>Item 1</TextBlock>
                    <TextBlock>Item 2</TextBlock>
                    <TextBlock>Item 3</TextBlock>
                    <TextBlock>Item 4</TextBlock>
                    <TextBlock>Item 5</TextBlock>
                </ItemsControl.Items>
            </ItemsControl>
        </ScrollViewer>
        <TextBlock Grid.Row="2">Footer</TextBlock>
    </Grid>
</Window>

The above is almost what I want, except the middle row is greedy; even if the window is very tall, it takes up as much space as it can, pushing the footer to the bottom of the window.

If I change the middle row's definition to Height="Auto", it takes up exactly the amount of space that it needs, even if that space isn't available, so the ScrollViewer never shows the scrollbar, and the footer will get lost off the bottom of the window if the window isn't tall enough.

How do I make it so that if the window is tall enough for everything to fit, the footer is immediately below the ItemsControl, but if the window isn't tall enough, the ScrollViewer shows a scrollbar and the footer is at the bottom of the window?

I don't necessarily need to do this with a Grid, but I didn't find any other Panel that would do what I want either. E.g., a DockPanel with the header set to DockPanel.Dock="Top", the footer set to DockPanel.Dock="Bottom", and the ItemsControl filling the rest behaves exactly the same way.

Some other stuff I've tried:

  • Setting VerticalAlignment="Stretch" on the footer TextBlock: no change.
  • Making the footer row Height="*": still not what I want; the footer and the ItemsControl get the same height, so the footer takes up too much space most of the time, or if you make the window extremely short, it goes off the bottom of the window.

标签: wpf layout
2条回答
虎瘦雄心在
2楼-- · 2019-08-21 06:13

Probably there is a much more elegant way, but at least the below works for me in a similar situation. It is about calculating the real heigth of all items in the ItemsControl and adjusting the Grid's row height if needed.

<Grid ShowGridLines="True" Loaded="Grid_Loaded">
    <Grid.Resources>
        <Style TargetType="TextBlock">
            <Setter Property="FontSize" Value="36"/>
        </Style>
    </Grid.Resources>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*" Name="middlerow"/>
        <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>
    <TextBlock Grid.Row="0">Header</TextBlock>
    <ScrollViewer Name="scv" Grid.Row="1" VerticalScrollBarVisibility="Auto">
        <ItemsControl x:Name="items" Height="Auto" VerticalAlignment="Top">
            <ItemsControl.Items>
                <TextBlock>Item 1</TextBlock>
                <TextBlock>Item 2</TextBlock>
                <TextBlock>Item 3</TextBlock>
                <TextBlock>Item 4</TextBlock>
            </ItemsControl.Items>
        </ItemsControl>
    </ScrollViewer>
    <TextBlock Grid.Row="2">Footer</TextBlock>
</Grid>

And in code behind:

private void Grid_Loaded(object sender, RoutedEventArgs e)
    {
        double height = 0;
        foreach (var item in items.Items)
        {
            height += (item as TextBlock).ActualHeight;
        }
        if (height < scv.ActualHeight)
        {
            middlerow.MaxHeight = height;
        }
    }
查看更多
forever°为你锁心
3楼-- · 2019-08-21 06:15

Thanks to mami's answer for the concept of how to do this, and Markus's answer for the idea to bind the Row's MaxHeight.

XAML:

<Window x:Class="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"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid ShowGridLines="True">
        <Grid.Resources>
            <Style TargetType="TextBlock">
                <Setter Property="FontSize" Value="36"/>
            </Style>
        </Grid.Resources>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*" MaxHeight="{Binding ItemsMaxHeight,RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=Window}}"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <TextBlock Grid.Row="0">Header</TextBlock>
        <ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
            <ItemsControl Name="ic" SizeChanged="Ic_SizeChanged">
                <ItemsControl.Items>
                    <TextBlock>Item 1</TextBlock>
                    <TextBlock>Item 2</TextBlock>
                    <TextBlock>Item 3</TextBlock>
                    <TextBlock>Item 4</TextBlock>
                    <TextBlock>Item 5</TextBlock>
                </ItemsControl.Items>
            </ItemsControl>
        </ScrollViewer>
        <TextBlock Grid.Row="2">Footer</TextBlock>
    </Grid>
</Window>

Code behind:

Imports System.ComponentModel

Class MainWindow
    Implements INotifyPropertyChanged

    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

    Public ReadOnly Property ItemsMaxHeight As Double
        Get
            Dim Height = 0.0
            Dim icg = ic.ItemContainerGenerator

            For i = 0 To ic.Items.Count - 1
                Height += DirectCast(icg.ContainerFromIndex(i), FrameworkElement).ActualHeight
            Next

            Return Height + 6.0 ' 6.0 to account for the size of borders? Not sure :(
        End Get
    End Property

    Private Sub Ic_SizeChanged(sender As Object, e As SizeChangedEventArgs)
        If e.HeightChanged Then
            RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs("ItemsMaxHeight"))
        End If
    End Sub
End Class

In this sample XAML, I don't need to increase the MaxHeight by 6, but in my actual app, if I don't add in a little extra, the ScrollViewer always shows a scrollbar that can be scrolled a tiny bit. Not sure what causes the discrepancy.

查看更多
登录 后发表回答