Search code examples
wpfxamldatagridcontroltemplate

Putting a little space between column headers


In WPF I'm using custom ColumnHeaderStyle ...

             <DataGrid.ColumnHeaderStyle>
                <Style TargetType="{x:Type DataGridColumnHeader}">
                    <Setter Property="Template">
                        <Setter.Value>
                            <ControlTemplate TargetType="{x:Type DataGridColumnHeader}">
                                <Border Background="#3ec9ed"
                                        BorderBrush="Black"
                                        BorderThickness="0"
                                        Padding="10"
                                        CornerRadius="25">
                                    <ContentPresenter HorizontalAlignment="Center"
                                                      VerticalAlignment="Center"
                                                      Margin="5,0,5,0" />
                                </Border>
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                    <Setter Property="Foreground"
                            Value="White" />
                    <Setter Property="HorizontalContentAlignment"
                            Value="Center" />
                    <Setter Property="Padding"
                            Value="15" />
                    <Setter Property="SeparatorBrush"
                            Value="Red" />
                    <Setter Property="SeparatorVisibility"
                            Value="Visible" />
                </Style>
            </DataGrid.ColumnHeaderStyle>

Now it looks like this.

enter image description here

I want to add a little space between every column header. Like this

enter image description here

What can I do to achieve this?


Solution

  • The DataGridwraps all column headers into a common top level DataGridColumnHeader container (it's basically a big single column that contains the individual column headers).
    When you set the DataGridColumnHeader.Background via the global Style the value also applies to this outer container which will in effect make the gap between the column headers disappear (because they share the same Background.

    The solution is to set the Background for this top level column to e.g. Brushes.Transparent. The top level column usually has a DataGridColumnHeader.DisplayIndex of -1.
    This allows us to address this special column using a template Trigger.

    You must also wire your template elements to the templated parent properly in order to allow the DataGridColumnHeader properties like Background or Margin to behave as expected (i.e. to have any effect on the layout). You usually use the TemplateBinding markup extension for this.

    To enable styling of the first and last item/column border individually (to apply the round corners), you need a MultiBinding with a custom IMultiValueConverter. The purpose of this converter is to detect the last item/column.
    The ItemIndexComparerConverter from the below example allows to specify the index of the item of interest using the Index notation (for example ^1 to reference the last item or 1 to reference the second item). Simply pass the index value of the desired column to the MultiBinding.ConverterParameter property.

    The fixed and improved Style that applies a gap between the columns of 10 DIP (Margin left and right of 5 DIP) could look as follows:

    <DataGrid.ColumnHeaderStyle>
      <Style TargetType="{x:Type DataGridColumnHeader}">
        <Setter Property="Margin"
                Value="5,0" />
        <Setter Property="Background"
                Value="#3ec9ed" />
        <Setter Property="BorderBrush"
                Value="Black" />
        <Setter Property="BorderThickness"
                Value="0" />
        <Setter Property="Padding"
                Value="10" />
        <Setter Property="Foreground"
                Value="White" />
        <Setter Property="HorizontalContentAlignment"
                Value="Center" />
        <Setter Property="Template">
          <Setter.Value>
            <ControlTemplate TargetType="{x:Type DataGridColumnHeader}">
              <Border>
                <Border x:Name="Border"
                        Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}"
                        Padding="{TemplateBinding Padding}"
                        CornerRadius="0">
                  <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                    VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
                </Border>
              </Border>
    
              <ControlTemplate.Triggers>
    
                <!-- Set the top level column header that contains the individual table column headers 
                     to have a Transparent background in order to make the gaps visible -->
                <Trigger Property="DisplayIndex"
                         Value="-1">
                  <Setter Property="Background"
                          Value="Transparent" />
                </Trigger>
    
                <!-- Round corners on the left for first column's border -->
                <Trigger Property="DisplayIndex"
                         Value="0">
                  <Setter TargetName="Border"
                          Property="CornerRadius"
                          Value="25,0,0,25" />
                </Trigger>
    
                <!-- Round corners on the right for last column's border -->
                <DataTrigger Value="True">
                  <DataTrigger.Binding>
    
                    <!-- Pass '^1' as  ConverterParameter to indicate that the Converter has to find the last column -->
                    <MultiBinding ConverterParameter="^1">
                      <MultiBinding.Converter>
                        <local:ItemIndexComparerConverter />
                      </MultiBinding.Converter>
                      <Binding RelativeSource="{RelativeSource Self}"
                               Path="DisplayIndex" />
                      <Binding RelativeSource="{RelativeSource AncestorType=ItemsControl}" />
                    </MultiBinding>
                  </DataTrigger.Binding>
    
                  <Setter TargetName="Border"
                          Property="CornerRadius"
                          Value="0,25,25,0" />
                </DataTrigger>
              </ControlTemplate.Triggers>
            </ControlTemplate>
          </Setter.Value>
        </Setter>
      </Style>
    </DataGrid.ColumnHeaderStyle>
    

    ItemIndexComparerConverter.cs
    Converter that returns true when the Index reference value of the MultiBinding.ConverterParameter equals the item's index, otherwise false.
    Required input (via MultiBinding) is the current item's index and the ItemsControl of the related items source collection.

    public class ItemIndexComparerConverter : IMultiValueConverter
    {
      public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
      {
        if (parameter is not string referenceIndexText
          || values.FirstOrDefault(item => item is int) is not int indexOfCurrentItem
          || values.FirstOrDefault(item => item is ItemsControl) is not ItemsControl itemsControl)
        {
          return false;
        }
    
        Index referenceIndex = referenceIndexText.StartsWith('^')
          ? Index.FromEnd(int.Parse(referenceIndexText[1..]))
          : Index.FromStart(int.Parse(referenceIndexText));
        int trueReferenceIndex = referenceIndex.GetOffset(itemsControl.Items.Count);
    
        return indexOfCurrentItem == trueReferenceIndex;
      }
    
      public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        => throw new NotSupportedException();
    }