Friday, July 23, 2021

Even WrapPanel

I have a screen that has 16 buttons at the bottom and at our minimum supported resolution of 1024x768 there is not enough room for them even when arranged in two rows. I want a panel that will grow in height to accommodate all the buttons but the standard WrapPanel looks nasty because it crams as much into the first row before wrapping around. This doesn't look nice and causes the buttons to jump around a lot as the page is resized.

I thought it would be good to have a version of a WrapPanel that tries to keep the number of controls (buttons) the same in each row. ie. If the panel is wide enough for 10 buttons but it contains 16 it puts 8 on each row instead of 10 and 6.

I had to learn about Measure and Arrange, and this is what I came up with.

The MeasureOverride method determines the number of rows I need, then determines how many controls should be on each row to be as even as possible. It asks for enough space to display the controls in this layout. Note it assumes all the controls are the same size, because math.

The ArrangeOverride method does the same thing, but this time it also arranges each control in the layout.

    Imports System.Windows
    Imports System.Windows.Controls
 
    Public Class EvenWrapPanel
        Inherits WrapPanel
 
        Public Sub New()
            Me.Orientation = Orientation.Horizontal
            Me.HorizontalAlignment = HorizontalAlignment.Left
        End Sub
 
        Protected Overrides Function MeasureOverride(constraint As Size) As Size
            Dim MaxHCount As Double
            Dim VCount As Double
            Dim HCount As Double
 
            If Children.Count = 0 Or constraint.Width = 0 Then
                Return MyBase.MeasureOverride(constraint)
            End If
 
            For Each child As UIElement In Me.Children
                child.Measure(constraint)
            Next
 
            MaxHCount = Math.Floor(constraint.Width / Children(0).DesiredSize.Width)
            VCount = Math.Ceiling(Children.Count / MaxHCount)
            HCount = Math.Ceiling(Children.Count / VCount)
            Return New Size(Children(0).DesiredSize.Width * HCount, Children(0).DesiredSize.Height * VCount)
        End Function
 
        Protected Overrides Function ArrangeOverride(arrangeSize As Size) As Size
            Dim MaxHCount As Double
            Dim VCount As Double
            Dim HCount As Double
 
            If Children.Count = 0 Or arrangeSize.Width = 0 Then
                Return MyBase.ArrangeOverride(arrangeSize)
            End If
 
            MaxHCount = Math.Floor(arrangeSize.Width / Children(0).DesiredSize.Width)
            VCount = Math.Ceiling(Children.Count / MaxHCount)
            HCount = Math.Ceiling(Children.Count / VCount)
 
            For Each child As UIElement In Me.Children
                Dim x As Integer = Convert.ToInt32(Children.IndexOf(child) Mod HCount)
                Dim y As Integer = Convert.ToInt32(Math.Floor(Children.IndexOf(child) / HCount))
                child.Arrange(New Rect(New Point(x * child.DesiredSize.Width, y * child.DesiredSize.Height), child.DesiredSize))
            Next
 
            Return New Size(Children(0).DesiredSize.Width * HCount, Children(0).DesiredSize.Height * VCount)
        End Function
    End Class

Here is a comparison of the two panels. The background colors are to show the dimensions of the controls.

Layout at approximately 1280 pixels width

Layout at approximately 800 pixels width

Here's the XAML that consumes the EvenWrapPanel.

<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"
        xmlns:local="clr-namespace:EvenWrapPanel"
        mc:Ignorable="d"
        Title="Even Wrap Panel" Height="450" Width="800">
    <Window.Resources>
        <Style TargetType="Button">
            <Setter Property="Width" Value="100"/>
            <Setter Property="Height" Value="20"/>
            <Setter Property="Margin" Value="2"/>
        </Style>
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="auto"/>
        </Grid.RowDefinitions>
        <Grid Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="auto"/>
                <ColumnDefinition Width="auto"/>
            </Grid.ColumnDefinitions>
            <local:EvenWrapPanel Grid.Column="0" Background="AliceBlue" >
                <Button Content="Button1"/>
                <Button Content="Button1"/>
                <Button Content="Button1"/>
                <Button Content="Button1"/>
                <Button Content="Button1"/>
                <Button Content="Button1"/>
                <Button Content="Button1"/>
                <Button Content="Button1"/>
                <Button Content="Button1"/>
                <Button Content="Button1"/>
                <Button Content="Button1"/>
                <Button Content="Button1"/>
                <Button Content="Button1"/>
                <Button Content="Button1"/>
            </local:EvenWrapPanel>
            <TextBlock Grid.Column="1" Text="Status Message" Margin="10" Background="AntiqueWhite"/>
            <StackPanel Grid.Column="2" Orientation="Vertical">
                <Button Content="ButtonA"/>
                <Button Content="ButtonA"/>
            </StackPanel>
        </Grid>
    </Grid>
</Window>
 

No comments:

Post a Comment