开发者

WPF WrapPanel with some items having a height of *

开发者 https://www.devze.com 2023-02-02 04:53 出处:网络
How do I make a WrapPanel with some items having a Height of *? A deceptively simple question that I have been trying to solve. I want a control (or some XAML layout magickry) that behaves similar to

How do I make a WrapPanel with some items having a Height of *?

A deceptively simple question that I have been trying to solve. I want a control (or some XAML layout magickry) that behaves similar to a Grid that has some rows with a Height of *, but supports wrapping of columns. Hell; call it a WrapGrid. :)

Here's a mockup to visualize this. Imagine a grid defined as such:

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Window1" Height="400">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Button Grid.Row="0" MinHeight="30">I'm auto-sized.</Button>
        <Button Grid.Row="1" MinHeight="90">I'm star-sized.</Button>
        <Button Grid.Row="2" MinHeight="30">I'm auto-sized.</Button>
        <Button Grid.Row="3" MinHeight="90">I'm star-sized, too!</Button>
        <Button Grid.Row="4" MinHeight="30">I'm auto-sized.</Button>
        <Button Grid.Row="5" MinHeight="30">I'm auto-sized.</Button>
    </Grid>
</Window>

WPF WrapPanel with some items having a height of *

What I want this panel to do is wrap an item into an additional column when the item can not get any smaller than its minHeight. Here is a horrible MSPaint of some mockups I made detailing this process.

WPF WrapPanel with some items having a height of *

Recall from the XAML that the auto-sized buttons have minHeights of 30, and the star-sized buttons have minHeights of 90.

This mockup is just two grids side by side and I manually moved buttons around in the designer. Conceivably, this could be done programmatically and serve as a sort of convoluted solution to this.

How can this be done? I will accept any solution whether it's through xaml or has some code-behind (though I would prefer pure XAML if possible since xaml code behind is tougher to implement in IronPython).

Updated with a bounty


Meleak's Solution

I managed to work out how to use Meleak's solution in my IPy app:

1) I compiled WrapGridPanel.cs into a DLL with csc:

C:\Projects\WrapGridTest\WrapGridTest>csc /target:library "WrapGridPan开发者_StackOverflow中文版el.cs" /optimize /reference:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\PresentationFramework.dll" /reference:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\PresentationCore.dll" /reference:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\WindowsBase.dll" /reference:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\System.Xaml.dll" 

Update: Added the /optimize switch, this nets a small performance increase

2) I added it to my application's xaml with the following line.

xmlns:local="clr-namespace:WrapGridTest;assembly=WrapGridPanel.dll"

This runs fine, but it breaks the designer. I can't really find a workaround for this yet, it looks to be a bug in VS2010. So as a workaround, in order to be able to use the designer, I just add the WrapGridPanel programmatically at runtime:

clr.AddReference("WrapGridPanel.dll")
from WrapGridTest import WrapGridPanel
wgp = WrapGridPanel()

Slow performance when resizing:

In my IronPython application, resizing the window containing this WrapGridPanel is slow and hitchy. Could the RecalcMatrix() algorithm be optimized? Could it perhaps be called less frequently? Maybe overriding MeasureOverride and ArrangeOverride, as Nicholas suggested, would perform better?

Update: According to the VS2010 Instrumentation Profiler, 97% of the time spent in RecalcMatrix() is spent on Clear() and Add(). Modifying each element in-place would be a huge performance improvement. I'm taking a whack at it myself but it's always tough modifying someone else's code... http://i.stack.imgur.com/tMTWU.png

Update: Performance issues have been mostly ironed out. Thanks Meleak!

Here is a mockup of part of my actual application's UI, in XAML, if you wish to try it out. http://pastebin.com/2EWY8NS0


Update
Optimized RecalcMatrix so the UI is only rebuilt when needed. It doesn't touch the UI unless necessary so it should be much faster.

Update Again
Fixed problem when using Margin

Is think what you're looking at is basically a WrapPanel with Horizontal Orientation where every element in it is a 1 Column Grid. Each element in a Column then has a corresponding RowDefinition where the Height Property matches an attached property ("WrapHeight") set on its Child. This Panel would have to be in a Grid itself, with Height="*" and Width="Auto" because the Children should be positioned by the available Height and not care about the available Width.

I made an implementation of this which I called a WrapGridPanel. You can use it like this

<local:WrapGridPanel>
    <Button MinHeight="30" local:WrapGridPanel.WrapHeight="Auto">I'm auto-sized.</Button>
    <Button MinHeight="90" local:WrapGridPanel.WrapHeight="*">I'm star-sized.</Button>
    <Button MinHeight="30" local:WrapGridPanel.WrapHeight="Auto">I'm auto-sized.</Button>
    <Button MinHeight="90" local:WrapGridPanel.WrapHeight="*">I'm star-sized, too!</Button>
    <Button MinHeight="30" local:WrapGridPanel.WrapHeight="Auto">I'm auto-sized.</Button>
    <Button MinHeight="30" local:WrapGridPanel.WrapHeight="Auto">I'm auto-sized.</Button>
</local:WrapGridPanel>

WPF WrapPanel with some items having a height of *

WrapGridPanel.cs

[ContentProperty("WrapChildren")] 
public class WrapGridPanel : Grid
{
    private WrapPanel m_wrapPanel = new WrapPanel();
    public WrapGridPanel()
    {
        ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1.0, GridUnitType.Auto) } );
        RowDefinitions.Add(new RowDefinition { Height = new GridLength(1.0, GridUnitType.Star) } );
        Children.Add(m_wrapPanel);
        WrapChildren = new ObservableCollection<FrameworkElement>();
        WrapChildren.CollectionChanged += WrapChildren_CollectionChanged;
        DependencyPropertyDescriptor actualHeightDescriptor
            = DependencyPropertyDescriptor.FromProperty(WrapGridPanel.ActualHeightProperty,
                                                        typeof(WrapGridPanel));
        if (actualHeightDescriptor != null)
        {
            actualHeightDescriptor.AddValueChanged(this, ActualHeightChanged);
        }
    }

    public static void SetWrapHeight(DependencyObject element, GridLength value)
    {
        element.SetValue(WrapHeightProperty, value);
    }
    public static GridLength GetWrapHeight(DependencyObject element)
    {
        return (GridLength)element.GetValue(WrapHeightProperty);
    }
    public ObservableCollection<FrameworkElement> WrapChildren
    {
        get { return (ObservableCollection<FrameworkElement>)base.GetValue(WrapChildrenProperty); }
        set { base.SetValue(WrapChildrenProperty, value); }
    }

    void ActualHeightChanged(object sender, EventArgs e)
    {
        RecalcMatrix();
    }
    void WrapChildren_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        RecalcMatrix();
    }

    List<List<FrameworkElement>> m_elementList = null;
    private bool SetupMatrix()
    {
        m_elementList = new List<List<FrameworkElement>>();
        double minHeightSum = 0;
        m_elementList.Add(new List<FrameworkElement>());
        int column = 0;
        if (WrapChildren.Count > 0)
        {
            foreach (FrameworkElement child in WrapChildren)
            {
                double tempMinHeight = 0.0;
                if (WrapGridPanel.GetWrapHeight(child).GridUnitType != GridUnitType.Star)
                {
                    tempMinHeight = Math.Max(child.ActualHeight, child.MinHeight) + child.Margin.Top + child.Margin.Bottom;
                }
                else
                {
                    tempMinHeight = child.MinHeight + child.Margin.Top + child.Margin.Bottom;
                }
                minHeightSum += tempMinHeight;
                if (minHeightSum > ActualHeight)
                {
                    minHeightSum = tempMinHeight;
                    m_elementList.Add(new List<FrameworkElement>());
                    column++;
                }
                m_elementList[column].Add(child);
            }
        }
        if (m_elementList.Count != m_wrapPanel.Children.Count)
        {
            return true;
        }
        for (int i = 0; i < m_elementList.Count; i++)
        {
            List<FrameworkElement> columnList = m_elementList[i];
            Grid wrapGrid = m_wrapPanel.Children[i] as Grid;
            if (columnList.Count != wrapGrid.Children.Count)
            {
                return true;
            }
        }
        return false;
    }
    private void RecalcMatrix()
    {
        if (ActualHeight == 0 || SetupMatrix() == false)
        {
            return;
        }

        Binding heightBinding = new Binding("ActualHeight");
        heightBinding.Source = this;
        while (m_elementList.Count > m_wrapPanel.Children.Count)
        {
            Grid wrapGrid = new Grid();
            wrapGrid.SetBinding(Grid.HeightProperty, heightBinding);
            m_wrapPanel.Children.Add(wrapGrid);
        }
        while (m_elementList.Count < m_wrapPanel.Children.Count)
        {
            Grid wrapGrid = m_wrapPanel.Children[m_wrapPanel.Children.Count - 1] as Grid;
            wrapGrid.Children.Clear();
            m_wrapPanel.Children.Remove(wrapGrid);
        }

        for (int i = 0; i < m_elementList.Count; i++)
        {
            List<FrameworkElement> columnList = m_elementList[i];
            Grid wrapGrid = m_wrapPanel.Children[i] as Grid;
            wrapGrid.RowDefinitions.Clear();
            for (int j = 0; j < columnList.Count; j++)
            {
                FrameworkElement child = columnList[j];
                GridLength wrapHeight = WrapGridPanel.GetWrapHeight(child);
                Grid.SetRow(child, j);
                Grid parentGrid = child.Parent as Grid;
                if (parentGrid != wrapGrid)
                {
                    if (parentGrid != null)
                    {
                        parentGrid.Children.Remove(child);
                    }
                    wrapGrid.Children.Add(child);
                }

                RowDefinition rowDefinition = new RowDefinition();
                rowDefinition.Height = new GridLength(Math.Max(1, child.MinHeight), wrapHeight.GridUnitType);
                wrapGrid.RowDefinitions.Add(rowDefinition); 
            }
        }
    }

    public static readonly DependencyProperty WrapHeightProperty =
            DependencyProperty.RegisterAttached("WrapHeight",
                                                typeof(GridLength),
                                                typeof(WrapGridPanel),
                                                new FrameworkPropertyMetadata(new GridLength(1.0, GridUnitType.Auto)));

    public static readonly DependencyProperty WrapChildrenProperty =
            DependencyProperty.Register("WrapChildren",
                                        typeof(ObservableCollection<FrameworkElement>),
                                        typeof(WrapGridPanel),
                                        new UIPropertyMetadata(null));
}

Update
Fixed more than one star-sized column problem.
New sample app here: http://www.mediafire.com/?28z4rbd4pp790t2

Update
A picture that tries to explain what WrapGridPanel does

WPF WrapPanel with some items having a height of *


I don't think there is a standard panel implementation that behave like you want. So, your best option is probably to roll your own.

It may sound intimidating at first, but it's not that difficult.

You can probably dig the WrapPanel source code somewhere (mono?), and adapt it to your need.

You don't need columns and rows. All you need is an attached Size property:

<StuffPanel Orientation="Vertical">
    <Button StuffPanel.Size="Auto" MinHeight="30">I'm auto-sized.</Button>
    <Button StuffPanel.Size="*" MinHeight="90">I'm star-sized.</Button>
    <Button StuffPanel.Size="Auto"  MinHeight="30">I'm auto-sized.</Button>
    <Button StuffPanel.Size="*" MinHeight="90">I'm star-sized, too!</Button>
    <Button StuffPanel.Size="Auto"  MinHeight="30">I'm auto-sized.</Button>
    <Button StuffPanel.Size="Auto" MinHeight="30">I'm auto-sized.</Button>
</StuffPanel>
0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号