如何在 TreeView 中包装 WrapPanel children?

How to wrap WrapPanel children in a TreeView?

我有一个包含 parent 和几个 children 的 TreeView。 children 又由一个 WrapPanel 组成,它有自己的 children("One"、"Two"、"Three" 等)。当 parent window 不足以容纳这些最后的元素时,我如何才能将它们换行?

这是我的代码:

<TreeView ScrollViewer.HorizontalScrollBarVisibility="Disabled">

    <TreeViewItem Header="Parent" IsExpanded="True" >

        <ItemsControl>
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <WrapPanel Orientation="Horizontal"/>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
            <ItemsControl.Items>
                <TextBlock Text="One" />
                <TextBlock Text="Two" />
                <TextBlock Text="Three" />
                <TextBlock Text="Four" />
                <TextBlock Text="Five" />
                <TextBlock Text="Six" />
                <TextBlock Text="Seven" />
                <TextBlock Text="Eight" />
                <TextBlock Text="Nine" />
                <TextBlock Text="Ten" />
            </ItemsControl.Items>
        </ItemsControl>

        <ItemsControl>
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <WrapPanel Orientation="Horizontal"/>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
            <ItemsControl.Items>
                <TextBlock Text="One" />
                <TextBlock Text="Two" />
                <TextBlock Text="Three" />
                <TextBlock Text="Four" />
                <TextBlock Text="Five" />
                <TextBlock Text="Six" />
                <TextBlock Text="Seven" />
                <TextBlock Text="Eight" />
                <TextBlock Text="Nine" />
                <TextBlock Text="Ten" />
            </ItemsControl.Items>
        </ItemsControl>

    </TreeViewItem>

</TreeView>

这是它目前产生的结果:

请测试此解决方法。您必须将 SizeChanged="TreeView_SizeChanged" 添加到 TreeView 项。

private void TreeView_SizeChanged(object sender, SizeChangedEventArgs e)
{
    var treeView = (TreeView) sender;
    foreach (TreeViewItem treeViewItem in treeView.Items)
    {
        foreach (ItemsControl ic in treeViewItem.Items)
        {
            Point relativeLocation = ic.TranslatePoint(new Point(0, 0), treeView);
            var wpMaxWidth = treeView.ActualWidth - relativeLocation.X;

            WrapPanel itemsWp = GetVisualChild<WrapPanel>(ic);
            itemsWp.MaxWidth = wpMaxWidth;
        }
    }
}

private static T GetVisualChild<T>(DependencyObject parent) where T : Visual
{
    int numVisuals = VisualTreeHelper.GetChildrenCount(parent);
    for (int i = 0; i < numVisuals; i++)
    {
        Visual v = (Visual) VisualTreeHelper.GetChild(parent, i);
        var child = v as T;
        return child ?? GetVisualChild<T>(v);
    }
    return null;
}

解决方案 2:使用 MultiBindingIMultiValueConverter

...
<WrapPanel.MaxWidth>
    <MultiBinding Converter="{StaticResource Converter1}">
        <MultiBinding.Bindings>
            <Binding RelativeSource="{RelativeSource Self}"/>
            <Binding RelativeSource="{RelativeSource Mode=FindAncestor,AncestorType=TreeView}"/>
        </MultiBinding.Bindings>
    </MultiBinding>
</WrapPanel.MaxWidth>
...

IMultiValueConverterConvert方法实现:

...
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
    var wp = values[0] as WrapPanel;
    var tv = values[1] as TreeView;
    if (wp == null || tv == null)
        return 0d;

    Point relativeLocation = wp.TranslatePoint(new Point(0, 0), tv);
    var wpMaxWidth = tv.ActualWidth - relativeLocation.X;
    return wpMaxWidth;
}
...

注意:容器大小更改后必须刷新Binding

我修改了 dbvega's 解决方案 2 以考虑垂直滚动条宽度和垂直滚动条可见性。还添加了对树视图宽度更改的重新计算。 XAML:

...
<WrapPanel.Width>
  <MultiBinding Converter="{StaticResource Converter1}">
    <MultiBinding.Bindings>
        <Binding RelativeSource="{RelativeSource Self}"/>
        <Binding RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=ScrollViewer}"/>
        <Binding RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=ScrollViewer}" Path="ViewportWidth"/>
    </MultiBinding.Bindings>
  </MultiBinding>
</WrapPanel.Width>
...

转换器:

...
object IMultiValueConverter.Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
    foreach (var v in values)
        if (v is null)
           return DependencyProperty.UnsetValue;
    var panel = (Panel)values[0];
    var scrollviewer = (ScrollViewer)values[1];

    Point relativeLocation = panel.TranslatePoint(new Point(0, 0), scrollviewer);
        var width = scrollviewer.ViewportWidth - relativeLocation.X;

    return width;
}
...

我问这个问题已经有几年了,在此期间我学到了很多关于 WPF 布局的知识。事实上,这可以单独在 XAML 中完成,我现在将 post 用于将来遇到此问题的任何人。关键是模板化 TreeViewItem 并检查它用于填充树的每个级别的 XAML。

简而言之,TreeView 的每一层都由一个包含两行三列的网格组成。第一行是 TreeViewItem header,第二行是 children.

也有三列。第 0 列用于扩展器图标,第 1 列用于 header 内容,第 2 列用于所有剩余的 space。 children 都位于 ContentPresenter 中,该 ContentPresenter 占据第二行的第 1 列和第 2 列。此网格的列定义揭示了一些有趣的东西:

<ColumnDefinition MinWidth="19" Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>

扩展器图标默认为 16x16,但第 0 列的 MinWidth 设置为 19 像素。因此,除非您更改扩展器图标,否则 19 像素将是 ContentPresenter 开头的缩进。因此,您需要做的就是将每个 child ItemsControl 绑定到其 TreeViewItem 的宽度,并在 right-手边:

<ItemsControl Width="{Binding RelativeSource={RelativeSource AncestorType= {x:Type TreeViewItem}, AncestorLevel=1}, Path=ActualWidth}" Padding="0 0 19 0">

结果是pixel-perfect换行:

(请注意,此解决方案还将适应出现和消失的滚动条,并且会在出现和消失时相应地 re-arrange WrapPanel 项目)。