Itemscontrol that arrange Items with Gridsplitter-

2019-07-13 11:20发布

问题:

I need a layout that stacks Items horizontal or vertical. Each Item should be separated by a Splitter, so the User can change the Size (Like the Toolbox Container in VisualStudio) The Items come from a Model, so I need a ItemsControl where I can set the ItemsSource.

My first idea was to build a CustomControl derived from Grid. Their I used the "OnVisualChildrenChanged" Method to react when a new Item is added. Then I wantet to add new Column-/Rowdefinitions (Depending wether it schould be horizontal or vertical). But here was the first Proplem. When the Grid is set to IsItemsHost="true" I get a Error that the DefinitionsCollection is not accessable...

Heres a CodeSnipped from the custom control, where I tried to add a row.

 private void SetRows()
    {
       this.RowDefinitions.Add(new RowDefinition() { Height = GridLength.Auto };             
    }

    protected override void OnVisualChildrenChanged(DependencyObject visualAdded, DependencyObject visualRemoved)
    {
        base.OnVisualChildrenChanged(visualAdded, visualRemoved);
        SetRows();            
    }

EDIT:

I think I found a solution right now.

First I created a CustomControl that derives from ItemsControl. In his template I add a Grid without inserting a Itempresenter. Now I override OnItemsChange. When a Item is added to the Collection I create a FrameworkElement and set DataContext to the added Item, than I add two Column definition to the Grid and add my Item plus an Gridsplitter to the Grid. This works well, now I only need to get the ItemTemplate from my Itemscontrol to use it in the grid.

When I solved this last issue I will add a small example.

回答1:

Heres my Solution so far. If someone has a better idea, you're welcome.

Heres the CustomControl Code behind:

public class ResizableItemControl : ItemsControl
{
    public ObservableCollection<FrameworkElement> _gridItems = new ObservableCollection<FrameworkElement>();
    private Grid _grid;

    static ResizableItemControl()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(ResizableItemControl), new FrameworkPropertyMetadata(typeof(ResizableItemControl)));
    }

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();

        if (this.Template != null)
        {
            _grid = this.Template.FindName("PART_Grid", this) as Grid;
        }

        // Add all existing items to grid
        foreach (var item in Items)
        {
            AddItemToGrid(item);
        }
    }

    /// <summary>
    /// Called when Items in ItemsCollection changing
    /// </summary>
    /// <param name="e"></param>
    protected override void OnItemsChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        base.OnItemsChanged(e);
        switch (e.Action)
        {
            case NotifyCollectionChangedAction.Add:
                if (Items.Count > 0)
                {
                    //Add Items in Grid when new Items where add
                    var myItem = this.Items[Items.Count - 1];
                    AddItemToGrid(myItem);
                }
                break;
        }


    }

    /// <summary>
    ///  Adds Items to grid plus GridSplitter
    /// </summary>
    /// <param name="myItem"></param>
    private void AddItemToGrid(object myItem)
    {
        var visualItem = this.ItemTemplate.LoadContent() as FrameworkElement;
        if (visualItem != null)
        {
            visualItem.DataContext = myItem;

            if (_grid != null)
            {
                if (_grid.ColumnDefinitions.Count != 0)
                {
                    _grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Auto) });
                    _grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Auto) });
                    var gridSplitter = CreateSplitter();
                    Grid.SetColumn(gridSplitter, _grid.ColumnDefinitions.Count - 2);
                    Grid.SetColumn(visualItem, _grid.ColumnDefinitions.Count - 1);
                    _grid.Children.Add(gridSplitter);
                    _grid.Children.Add(visualItem);

                    //_grid.Children.Add(myTest);
                }
                else
                {
                    _grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Auto) });
                    Grid.SetColumn(visualItem, _grid.ColumnDefinitions.Count - 1);
                    _grid.Children.Add(visualItem);
                }
            }
        }

    }

    private static GridSplitter CreateSplitter()
    {
        var gridSplitter = new GridSplitter() {ResizeDirection = GridResizeDirection.Columns};
        gridSplitter.Width = 5;
        gridSplitter.HorizontalAlignment = HorizontalAlignment.Stretch;
        gridSplitter.Background = new SolidColorBrush(Colors.Black);
        return gridSplitter;
    }
}

The generic Xaml:

<Style TargetType="{x:Type controls:ResizableItemControl}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type controls:ResizableItemControl}">
                <Border Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}">
                    <ScrollViewer HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Hidden">
                        <Grid x:Name="PART_Grid"></Grid>
                    </ScrollViewer>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

And how to use it:

            <controls:ResizableItemControl
                ItemsSource="{Binding ElementName=this,Path=Items}">
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Label
                            Content="{Binding Name}"
                            ToolTip="{Binding Description}"
                            Background="Black"/>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </controls:ResizableItemControl>


回答2:

You could create a Stackpanel, and for each element, a Grid inside it, with the following structure:

<StackPanel Orientation="Vertical">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
        </Grid.RowDefinitions>
        <Button>Hallo</Button>
        <GridSplitter Height="5" Background="Transparent" VerticalAlignment="Bottom" HorizontalAlignment="Stretch" Grid.Row="0" ResizeDirection="Rows" ResizeBehavior="CurrentAndNext"/>
    </Grid>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
        </Grid.RowDefinitions>
        <Button>Hallo</Button>
        <GridSplitter Height="5" Background="Transparent" VerticalAlignment="Bottom" HorizontalAlignment="Stretch" Grid.Row="0" ResizeDirection="Rows" ResizeBehavior="CurrentAndNext"/>
    </Grid>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
        </Grid.RowDefinitions>
        <Button>Hallo</Button>
        <GridSplitter Height="5" Background="Transparent" VerticalAlignment="Bottom" HorizontalAlignment="Stretch" Grid.Row="0" ResizeDirection="Rows" ResizeBehavior="CurrentAndNext"/>
    </Grid>       
</StackPanel>

Note that you can only resize one column at a time, but i'm sure you can refine this a bit.



回答3:

I cannot add comments to waits83's answer. Instead of create grid splitter on OnApplyTemplate, use OnItemsSourceChanged may be better. because with OnApplyTemplate, need to bind data context before initializeComponents() in view constructor.



回答4:

Here is a panel I ended up with after a few failed attempts to implement similar scenario:

class SplitPanel : Grid
{
    public static readonly DependencyProperty ItemTemplateProperty = DependencyProperty.Register(
        "ItemTemplate", typeof (DataTemplate), typeof (SplitPanel), new PropertyMetadata(default(DataTemplate), OnItemTemplateChanged));
    public DataTemplate ItemTemplate
    {
        get { return (DataTemplate) GetValue(ItemTemplateProperty); }
        set { SetValue(ItemTemplateProperty, value); }
    }
    private static void OnItemTemplateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        foreach (var child in ((SplitPanel)d).Children.OfType<ContentPresenter>())
        {
            child.ContentTemplate = (DataTemplate)e.NewValue;
        }
    }

    public static readonly DependencyProperty ItemContainerStyleProperty = DependencyProperty.Register(
           "ItemContainerStyle", typeof(Style), typeof(SplitPanel), new PropertyMetadata(default(Style), OnContainerStyleChanged));
    public Style ItemContainerStyle
    {
        get { return (Style)GetValue(ItemContainerStyleProperty); }
        set { SetValue(ItemContainerStyleProperty, value); }
    }
    private static void OnContainerStyleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        foreach (var child in ((SplitPanel)d).Children.OfType<ContentPresenter>())
        {
            child.Style = (Style) e.NewValue;
        }
    }

    public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register(
        "ItemsSource", typeof (IEnumerable), typeof (SplitPanel), new PropertyMetadata(null ,OnItemsSourceChanged));
    public IEnumerable ItemsSource
    {
        get { return (IEnumerable) GetValue(ItemsSourceProperty); }
        set { SetValue(ItemsSourceProperty, value); }
    }
    private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var grid = (SplitPanel)d;

        grid.Children.Clear();
        grid.ColumnDefinitions.Clear();
        var items = e.NewValue as IEnumerable;
        if (items == null) return;
        var children = items.Cast<object>().Select(grid.GenerateContainer).ToArray();
        for (int i = 0; ; i++)
        {
            var child = children[i]; 
            var column = new ColumnDefinition
            {
                MinWidth = GetMinColumnWidth(child),
                Width = GetColumnWidth(child),
                MaxWidth = GetMaxColumnWidth(child)
            };
            grid.ColumnDefinitions.Add(column);
            grid.Children.Add(child);
            SetColumn(child, i);

            if (i == children.Length - 1) break;

            var splitter = new GridSplitter
            {
                Width = 5,
                ResizeBehavior = GridResizeBehavior.CurrentAndNext,
                VerticalAlignment = VerticalAlignment.Stretch,
                HorizontalAlignment = HorizontalAlignment.Right,
                Background = Brushes.Transparent
            };
            SetColumn(splitter, i);
            grid.Children.Add(splitter);
        }
    }

    public static readonly DependencyProperty ColumnWidthProperty = DependencyProperty.RegisterAttached(
        "ColumnWidth", typeof (GridLength), typeof (SplitPanel), new PropertyMetadata(new GridLength(1, GridUnitType.Star), OnColumnWidthChanged));
    public static void SetColumnWidth(DependencyObject element, GridLength value)
    {
        element.SetValue(ColumnWidthProperty, value);
    }
    public static GridLength GetColumnWidth(DependencyObject element)
    {
        return (GridLength) element.GetValue(ColumnWidthProperty);
    }
    private static void OnColumnWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        UpdateColumnDefinition(d, column => column.Width = (GridLength)e.NewValue);
    }

    public static readonly DependencyProperty MinColumnWidthProperty = DependencyProperty.RegisterAttached(
        "MinColumnWidth", typeof (double), typeof (SplitPanel), new PropertyMetadata(100d, OnMinColumnWidthChanged));
    public static void SetMinColumnWidth(DependencyObject element, double value)
    {
        element.SetValue(MinColumnWidthProperty, value);
    }
    public static double GetMinColumnWidth(DependencyObject element)
    {
        return (double) element.GetValue(MinColumnWidthProperty);
    }
    private static void OnMinColumnWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        UpdateColumnDefinition(d, column => column.MinWidth = (double)e.NewValue);
    }

    public static readonly DependencyProperty MaxColumnWidthProperty = DependencyProperty.RegisterAttached(
        "MaxColumnWidth", typeof (double), typeof (SplitPanel), new PropertyMetadata(double.MaxValue, OnMaxColumnWidthChanged));
    public static void SetMaxColumnWidth(DependencyObject element, double value)
    {
        element.SetValue(MaxColumnWidthProperty, value);
    }
    public static double GetMaxColumnWidth(DependencyObject element)
    {
        return (double) element.GetValue(MaxColumnWidthProperty);
    }
    private static void OnMaxColumnWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        UpdateColumnDefinition(d, column => column.MaxWidth = (double)e.NewValue);
    }

    private static void UpdateColumnDefinition(DependencyObject child, Action<ColumnDefinition> updateAction)
    {
        var grid = VisualTreeHelper.GetParent(child) as SplitPanel;
        if (grid == null) return;
        var column = GetColumn((UIElement)child);
        if (column >= grid.ColumnDefinitions.Count) return;
        grid.Dispatcher.BeginInvoke(new Action(() => updateAction(grid.ColumnDefinitions[column])));
    }

    private ContentPresenter GenerateContainer(object item)
    {
        return new ContentPresenter {Content = item, ContentTemplate = ItemTemplate};
    }
}

Usage:

<wpfApplication1:SplitPanel ItemsSource="{Binding Items}">
    <wpfApplication1:SplitPanel.ItemContainerStyle>
        <Style TargetType="ContentPresenter">
            <Setter Property="wpfApplication1:SplitPanel.ColumnWidth" Value="{Binding Width}"/>
            <Setter Property="wpfApplication1:SplitPanel.MinColumnWidth" Value="10"/>
            <Setter Property="wpfApplication1:SplitPanel.MaxColumnWidth" Value="100"/>
        </Style>
    </wpfApplication1:SplitPanel.ItemContainerStyle>
    <wpfApplication1:SplitPanel.ItemTemplate>
        <DataTemplate>
            <TextBlock Text="123"/>
        </DataTemplate>
    </wpfApplication1:SplitPanel.ItemTemplate>
</wpfApplication1:SplitPanel>

It only auto-generates columns and it does not support INotifyCollectionChanged (I do not need those in my use case). It has built-in grid splitters though, and you can specify column sizes using attached properties. I hope it helps someone. :)