1
votes

I have TreeViewItems where the HierarchicalDataTemplate consists of a Grid with 3 columns with the following definitions:

<Grid>    
  <Grid.ColumnDefinitions>
    <ColumnDefinition Width="Auto" />
    <ColumnDefinition Width="1*" />
    <ColumnDefinition Width="Auto" />
...

I want to set the width of the grid so that it would take exactly all the available space of the TreeViewItem inside TreeView. The third column of the grid should therefore be right-aligned inside the TreeView.

How do I get the correct value for the Grid's width?

I know that for the ListBox ItemTemplate,I can set the width by binding it to the ScrollContentPresenter:

Width="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=ScrollContentPresenter, AncestorLevel=1}, Path=ActualWidth}"

This trick doesn't work in TreeView, since children have less space available than the root treeview items.

Any ideas?

3

3 Answers

7
votes

After working on it for a few hours I found a working solution using Multibinding and two Converters.

First, the definition of HierarchicalDataTemplate in XAML:

<HierarchicalDataTemplate>
<Grid>
   <Grid.Width>
      <MultiBinding Converter="{StaticResource SumConverterInstance}">
          <Binding RelativeSource="{RelativeSource FindAncestor, AncestorType=ScrollContentPresenter, AncestorLevel=1}" Path="ActualWidth" />
          <Binding RelativeSource="{RelativeSource FindAncestor, AncestorType=TreeViewItem, AncestorLevel=1}" Converter="{StaticResource ParentCountConverterInstance}" />
      </MultiBinding>
    </Grid.Width>   
   .... (content of the template) ....
</Grid>
</HierarchicalDataTemplate>

The first binding in multibinding gets the width of the ScrollContentPresenter in TreeView, which is the total visible width of the TreeView. The second binding calls a converter with the TreeViewItem as an argument and computes how many parents does the TreeViewItem have before reaching the root item. Using these two inputs we use the SumConverterInstance in Multibinding to compute the available width for the given TreeViewItem.

Here are the converter instances defined in XAML:

  <my:SumConverter x:Key="SumConverterInstance" />
  <my:ParentCountConverter x:Key="ParentCountConverterInstance" />

and the code for the two converters:

// combine the width of the TreeView control and the number of parent items to compute available width
public class SumConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        double totalWidth = (double)values[0];
        double parentCount = (double)values[1];
        return totalWidth - parentCount * 20.0;
    }

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

// count the number of TreeViewItems before reaching ScrollContentPresenter
public class ParentCountConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        int parentCount = 1;
        DependencyObject o = VisualTreeHelper.GetParent(value as DependencyObject);
        while (o != null && o.GetType().FullName != "System.Windows.Controls.ScrollContentPresenter")
        {
            if (o.GetType().FullName == "System.Windows.Controls.TreeViewItem")
                parentCount += 1;
            o = VisualTreeHelper.GetParent(o);
        }
        return parentCount;
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

This is now the correct look:

alt text

1
votes

You should let the layout system handle this for you instead of forcing a fixed width in your ItemTemplate. For a ListBox, all you need to do is set HorizontalContentAlignment="Stretch". That's the first step for a TreeView too, but unfortunately there's some other layout in the default template that requires an additional change. It uses a 3 column Grid which puts the content into the second column and only the children extend into the third (*) column. By adding a Grid.ColumnSpan="2" onto the ContentPresenter's containing Border, the content will stretch across the entire item's area.

<PathGeometry x:Key="TreeArrow" Figures="M0,0 L0,6 L6,0 z"/>
<Style x:Key="ExpandCollapseToggleStyle" TargetType="{x:Type ToggleButton}">
    <Setter Property="Focusable" Value="False"/>
    <Setter Property="Width" Value="16"/>
    <Setter Property="Height" Value="16"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ToggleButton}">
                <Border Background="Transparent" Height="16" Padding="5,5,5,5" Width="16">
                    <Path x:Name="ExpandPath" Data="{StaticResource TreeArrow}" Fill="Transparent" Stroke="#FF989898">
                        <Path.RenderTransform>
                            <RotateTransform Angle="135" CenterY="3" CenterX="3"/>
                        </Path.RenderTransform>
                    </Path>
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsMouseOver" Value="True">
                        <Setter Property="Stroke" TargetName="ExpandPath" Value="#FF1BBBFA"/>
                        <Setter Property="Fill" TargetName="ExpandPath" Value="Transparent"/>
                    </Trigger>
                    <Trigger Property="IsChecked" Value="True">
                        <Setter Property="RenderTransform" TargetName="ExpandPath">
                            <Setter.Value>
                                <RotateTransform Angle="180" CenterY="3" CenterX="3"/>
                            </Setter.Value>
                        </Setter>
                        <Setter Property="Fill" TargetName="ExpandPath" Value="#FF595959"/>
                        <Setter Property="Stroke" TargetName="ExpandPath" Value="#FF262626"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<ControlTemplate TargetType="{x:Type TreeViewItem}">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition MinWidth="19" Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <ToggleButton x:Name="Expander" ClickMode="Press" IsChecked="{Binding IsExpanded, RelativeSource={RelativeSource TemplatedParent}}" Style="{StaticResource ExpandCollapseToggleStyle}"/>
        <Border x:Name="Bd" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}"
                Grid.Column="1" Grid.ColumnSpan="2" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="true">
            <ContentPresenter x:Name="PART_Header" ContentSource="Header" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
        </Border>
        <ItemsPresenter x:Name="ItemsHost" Grid.ColumnSpan="2" Grid.Column="1" Grid.Row="1"/>
    </Grid>
    <ControlTemplate.Triggers>
        <Trigger Property="IsExpanded" Value="false">
            <Setter Property="Visibility" TargetName="ItemsHost" Value="Collapsed"/>
        </Trigger>
        <Trigger Property="HasItems" Value="false">
            <Setter Property="Visibility" TargetName="Expander" Value="Hidden"/>
        </Trigger>
        <Trigger Property="IsSelected" Value="true">
            <Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
            <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}"/>
        </Trigger>
        <MultiTrigger>
            <MultiTrigger.Conditions>
                <Condition Property="IsSelected" Value="true"/>
                <Condition Property="IsSelectionActive" Value="false"/>
            </MultiTrigger.Conditions>
            <Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>
            <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
        </MultiTrigger>
        <Trigger Property="IsEnabled" Value="false">
            <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>
0
votes

What you are describing is the TreeView's ItemContainerStyle and the grid I'm describing above is a HierarchicalDataTemplate.

In my TreeView I'm showing email attachments. TreeViewItems contain an icon (grid's first column), filename and size (second column) and time (third column). I would like to see items stretched or squeezed (by wrapping the middle column of the grid) based on the width of the TreeView.

Below is a non-working example. As you can see the filename is not wrapped so you can't see the date of some items.

alt text