Search code examples
wpfxamlwrappaneluniformgrid

How to draw a WrapPanel in a specific way in XAML / WPF


I have a listbox of elements, and I set the ItemPanel as a WrapPanel as I want my Panel to wrap every 4 elements.

I used the following:

 <ListBox.ItemsPanel>
     <ItemsPanelTemplate>
         <WrapPanel Orientation="Horizontal" IsItemsHost="True" />
     </ItemsPanelTemplate>
 </ListBox.ItemsPanel> 

Now, I want thay my items will be displayed as follows:

When only 4 Items or less: enter image description here

When more than 4 Items: enter image description here

I need that the cornerRadius will be applied on the four edges whether it's only one row or not.


Solution

  • This turned out to be a bit painful. If you want the corner radius and the border thickness to be properly parameterized, that'll take more work: You'd need value converters to create or modify CornerRadius and Thickness values as needed.

    Another approach would have been to omit the triggers and write two big multiconverters, for Thickness and CornerRadius, that take the same parameters as the one I wrote, plus the "default" border thickness and corner radius values, and then return Thickness and CornerRadius respectively.

    <Style TargetType="ListBox" x:Key="GridLineListBox">
        <Style.Resources>
            <local:CellTypeConverter x:Key="CellTypeConverter" />
        </Style.Resources>
        <Setter Property="AlternationCount" Value="{x:Static sys:Int32.MaxValue}" />
        <Setter Property="BorderThickness" Value="2" />
        <Setter Property="SnapsToDevicePixels" Value="True" />
        <Setter Property="UseLayoutRounding" Value="True" />
        <Setter Property="BorderBrush" Value="SteelBlue" />
        <Setter Property="local:GridLineListBox.ColumnCount" Value="6" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="ListBox">
                    <Border
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}"
                        CornerRadius="16"
                        ClipToBounds="True"
                        >
                        <ItemsPresenter Margin="-1" />
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
        <Setter Property="ItemsPanel">
            <Setter.Value>
                <ItemsPanelTemplate>
                    <UniformGrid 
                        Columns="{Binding (local:GridLineListBox.ColumnCount), RelativeSource={RelativeSource AncestorType=ListBox}}" 
                        IsItemsHost="True" 
                        />
                </ItemsPanelTemplate>
            </Setter.Value>
        </Setter>
        <Setter Property="ItemContainerStyle">
            <Setter.Value>
                <Style TargetType="ListBoxItem" BasedOn="{StaticResource {x:Type ListBoxItem}}">
                    <Setter Property="HorizontalContentAlignment" Value="Center" />
                    <Setter Property="VerticalContentAlignment" Value="Center" />
                    <!-- Put this in an attached property so we don't have to copy/paste the whole binding for each trigger -->
                    <Setter Property="local:GridLineListBox.CellType">
                        <Setter.Value>
                            <MultiBinding Converter="{StaticResource CellTypeConverter}">
                                <Binding Path="Items.Count" RelativeSource="{RelativeSource AncestorType=ListBox}" />
                                <Binding Path="(ItemsControl.AlternationIndex)" RelativeSource="{RelativeSource Self}" />
                                <Binding Path="(local:GridLineListBox.ColumnCount)" RelativeSource="{RelativeSource AncestorType=ListBox}" />
                            </MultiBinding>
                        </Setter.Value>
                    </Setter>
                    <Setter Property="Margin" Value="0" />
                    <Setter Property="BorderBrush" Value="{Binding BorderBrush, RelativeSource={RelativeSource AncestorType=ListBox}}" />
                    <Setter Property="Template">
                        <Setter.Value>
                            <ControlTemplate TargetType="ListBoxItem">
                                <!-- 
                                Negative right/bottom margin because I'm getting a gap with 
                                SnapToDevicePixels and I'm too lazy to figure out the real reason.
                                -->
                                <Border 
                                    x:Name="Bd"
                                    BorderBrush="{TemplateBinding BorderBrush}"
                                    BorderThickness="0,0,2,2"
                                    Background="{TemplateBinding Background}"
                                    ClipToBounds="True"
                                    Margin="-1"
                                    >
                                    <ContentPresenter 
                                        HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" 
                                        SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" 
                                        VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                                        />
                                </Border>
                                <ControlTemplate.Triggers>
                                    <Trigger Property="local:GridLineListBox.CellType" Value="TopLeft">
                                        <Setter TargetName="Bd" Property="CornerRadius" Value="16,0,0,0" />
                                    </Trigger>
                                    <Trigger Property="local:GridLineListBox.CellType" Value="TopRight">
                                        <Setter TargetName="Bd" Property="BorderThickness" Value="0,0,0,2" />
                                        <Setter TargetName="Bd" Property="CornerRadius" Value="0,16,0,0" />
                                    </Trigger>
                                    <Trigger Property="local:GridLineListBox.CellType" Value="Right">
                                        <Setter TargetName="Bd" Property="BorderThickness" Value="0,0,0,2" />
                                    </Trigger>
                                    <Trigger Property="local:GridLineListBox.CellType" Value="BottomRight">
                                        <Setter TargetName="Bd" Property="BorderThickness" Value="0,0,0,0" />
                                        <Setter TargetName="Bd" Property="CornerRadius" Value="0,0,16,0" />
                                    </Trigger>
                                    <Trigger Property="local:GridLineListBox.CellType" Value="Bottom">
                                        <Setter TargetName="Bd" Property="BorderThickness" Value="0,0,2,0" />
                                    </Trigger>
                                    <Trigger Property="local:GridLineListBox.CellType" Value="BottomLeft">
                                        <Setter TargetName="Bd" Property="BorderThickness" Value="0,0,2,0" />
                                        <Setter TargetName="Bd" Property="CornerRadius" Value="0,0,0,16" />
                                    </Trigger>
                                    <Trigger Property="local:GridLineListBox.CellType" Value="SingleRowLeft">
                                        <Setter TargetName="Bd" Property="BorderThickness" Value="0,0,2,0" />
                                        <Setter TargetName="Bd" Property="CornerRadius" Value="16,0,0,16" />
                                    </Trigger>
                                    <Trigger Property="local:GridLineListBox.CellType" Value="SingleRowRight">
                                        <Setter TargetName="Bd" Property="BorderThickness" Value="0,0,0,0" />
                                        <Setter TargetName="Bd" Property="CornerRadius" Value="0,16,16,0" />
                                    </Trigger>
                                    <MultiTrigger>
                                        <MultiTrigger.Conditions>
                                            <Condition Property="IsMouseOver" Value="True"/>
                                        </MultiTrigger.Conditions>
                                        <Setter Property="Background" TargetName="Bd" Value="{StaticResource Item.MouseOver.Background}"/>
                                    </MultiTrigger>
                                    <MultiTrigger>
                                        <MultiTrigger.Conditions>
                                            <Condition Property="Selector.IsSelectionActive" Value="False"/>
                                            <Condition Property="IsSelected" Value="True"/>
                                        </MultiTrigger.Conditions>
                                        <Setter Property="Background" TargetName="Bd" Value="{StaticResource Item.SelectedInactive.Background}"/>
                                    </MultiTrigger>
                                    <MultiTrigger>
                                        <MultiTrigger.Conditions>
                                            <Condition Property="Selector.IsSelectionActive" Value="True"/>
                                            <Condition Property="IsSelected" Value="True"/>
                                        </MultiTrigger.Conditions>
                                        <Setter Property="Background" TargetName="Bd" Value="{StaticResource Item.SelectedActive.Background}"/>
                                    </MultiTrigger>
                                    <Trigger Property="IsEnabled" Value="False">
                                        <Setter Property="TextElement.Foreground" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
                                    </Trigger>
                                </ControlTemplate.Triggers>
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </Style>
            </Setter.Value>
        </Setter>
    </Style>
    

    C#

    
    public enum CellType {
        TopLeft, Top, TopRight, Right, BottomRight, Bottom, BottomLeft, Left,
        SingleRowLeft, SingleRowRight, Inner
    }
    
    public class CellTypeConverter : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            var itemsCount = System.Convert.ToInt32(values[0]);
            var itemIndex = System.Convert.ToInt32(values[1]);
            var columnCount = System.Convert.ToInt32(values[2]);
    
            int rowCount = itemsCount / columnCount;
    
            if (itemsCount % columnCount > 0)
                ++rowCount;
    
            int lowerRightIndex = (rowCount * columnCount) - 1;
            int lowerLeftIndex = (rowCount - 1) * columnCount;
    
            if (itemIndex == 0)
            {
                return (rowCount == 1) ? CellType.SingleRowLeft : CellType.TopLeft;
            }
            else if (itemIndex == columnCount - 1)
            {
                return (rowCount == 1) ? CellType.SingleRowRight : CellType.TopRight;
            }
    
            else if (itemIndex < columnCount)
                return CellType.Top;
            else if (itemIndex == lowerRightIndex)
                return CellType.BottomRight;
            else if ((itemIndex + 1) % columnCount == 0)
                return CellType.Right;
            else if (itemIndex == lowerLeftIndex)
                return CellType.BottomLeft;
            else if (itemIndex > lowerLeftIndex)
                return CellType.Bottom;
            else if (itemIndex % columnCount == 0)
                return CellType.Left;
    
            return CellType.Inner;
        }
    
        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
    
    public static class GridLineListBox
    {
        public static CellType GetCellType(ListBoxItem obj)
        {
            return (CellType)obj.GetValue(CellTypeProperty);
        }
    
        public static void SetCellType(ListBoxItem obj, CellType value)
        {
            obj.SetValue(CellTypeProperty, value);
        }
    
        public static readonly DependencyProperty CellTypeProperty =
            DependencyProperty.RegisterAttached("CellType", typeof(CellType), typeof(GridLineListBox),
                new PropertyMetadata((CellType)(-1)));
    
        public static int GetColumnCount(ListBox obj)
        {
            return (int)obj.GetValue(ColumnCountProperty);
        }
    
        public static void SetColumnCount(ListBox obj, int value)
        {
            obj.SetValue(ColumnCountProperty, value);
        }
    
        public static readonly DependencyProperty ColumnCountProperty =
            DependencyProperty.RegisterAttached("ColumnCount", typeof(int), typeof(GridLineListBox),
                new PropertyMetadata(0, ColumnCount_PropertyChanged));
    
        private static void ColumnCount_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var target = d as ListBox;
        }
    }
    

    Example:

    <ListBox 
        ItemsSource="{Binding CollectionOfStrings}" 
        Style="{StaticResource GridLineListBox}"
        >
        <ListBox.ItemTemplate>
            <DataTemplate>
                <StackPanel Orientation="Vertical">
                    <Label
                        Content="{Binding}"
                        HorizontalAlignment="Center"
                        />
                    <Label 
                        Content="{Binding (local:GridLineListBox.CellType), RelativeSource={RelativeSource AncestorType=ListBoxItem}}" 
                        HorizontalAlignment="Center"
                        />
                </StackPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
    

    enter image description here