WPF WrapPanel中有一些项的高度为*

4

如何创建一个WrapPanel,其中某些项的高度为*?

这是一个看似简单但我一直在尝试解决的问题。我想要一个类似于Grid的控件(或者一些XAML布局魔法),它可以像Grid一样设置一些行的高度为*,但同时支持列的换行。甚至可以称之为WrapGrid。

以下是一个模拟图以帮助理解。请想象一个定义如下的网格:

<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>

我希望这个面板能够在项的最小高度无法再缩小时,将该项包装到另一列中。以下是我制作的一些模型的可怕MSPaint详细说明此过程。

从XAML中可以看出,自动调整大小的按钮具有最小高度为30,而星号调整大小的按钮具有最小高度为90。
这个模拟只是将两个网格并排放置,并手动在设计师中移动按钮。理论上,这可以通过编程来完成,并作为一种解决方案。
如何做到这一点?我将接受任何解决方案,无论是通过xaml还是具有一些代码后台(尽管我更喜欢纯粹的XAML,如果可能的话,因为在IronPython中实现xaml代码后台更加困难)。
使用悬赏更新。

Meleak的解决方案

我成功地找出了如何在我的IPy应用程序中使用Meleak的解决方案:

1)我使用cscWrapGridPanel.cs编译成DLL:

C:\Projects\WrapGridTest\WrapGridTest>csc /target:library "WrapGridPanel.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" 

更新: 添加了/optimize开关,这将带来小幅的性能提升。

2) 我在应用程序的XAML中添加了以下代码。

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

这段代码可以正常运行,但会导致设计器出现问题。目前我还没有找到解决办法,看起来是VS2010的一个bug。因此,为了能够使用设计器,我只能在运行时通过编程方式添加WrapGridPanel作为临时解决方法:
clr.AddReference("WrapGridPanel.dll")
from WrapGridTest import WrapGridPanel
wgp = WrapGridPanel()

调整大小时性能缓慢:

在我的IronPython应用程序中,调整包含此WrapGridPanel的窗口的大小很慢且不流畅。 RecalcMatrix()算法是否可以进行优化?它可能被调用得更少吗?也许像Nicholas建议的那样覆盖MeasureOverrideArrangeOverride会更好?

更新: 根据VS2010 Instrumentation Profiler,RecalcMatrix()中花费的时间有97%用于Clear()和Add()。 在原地修改每个元素将大大提高性能。 我正在尝试自己解决,但修改别人的代码总是很困难... http://i.stack.imgur.com/tMTWU.png

更新:性能问题已经基本解决。谢谢Meleak!

这是我实际应用程序UI的部分XAML模型,如果您想尝试一下。 http://pastebin.com/2EWY8NS0


1
你应该能够接受一个代码后台解决方案,并且只需从你的IronPython解决方案中引用那个.Net类型。 - Szymon Rozga
@Aphex:我会研究你的问题。它肯定可以进行优化。 - Fredrik Hedblad
@Aphex:用优化后的版本更新了我的代码,应该会快很多。我接下来会看看另一个问题。 - Fredrik Hedblad
@Aphex:修复了边距问题,代码已经在帖子中发布。试一下看看是否效果更好。 - Fredrik Hedblad
不错!这个版本更加流畅,卡顿现象也减少了。根据分析器的数据,RecalcMatrix() 现在大约有 39% 的时间花费在 Remove() 上,35% 花费在 Add() 上,13% 花费在 Clear() 上。我敢打赌,就地修改会进一步提高性能,但现在这已经足够好了!顺便说一下,如果你想亲自测试一下,我是通过启动程序、抓住调整大小的角落并拖动几秒钟来测试的。 - Aphex
显示剩余16条评论
2个回答

6
更新
优化了RecalcMatrix,只有在需要重建UI时才会涉及到UI。除非必要,否则不会触及UI,因此速度应该更快。 再次更新
修复了使用Margin时的问题。
我认为你看到的基本上是一个水平方向的WrapPanel,其中每个元素都是一个1列的Grid。然后,每个列中的元素都有相应的RowDefinition,其中Height属性与其子元素设置的附加属性("WrapHeight")相匹配。这个面板必须本身就在一个Grid里面,高度设置为"*",宽度设置为"Auto",因为子元素应该根据可用的Height定位,而不关心可用的Width
我做了一个实现,称之为WrapGridPanel。你可以像这样使用它:
<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>

alt text

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));
}
更新
修复了多个星号大小列的问题。
这里有一个新的示例应用程序:http://www.mediafire.com/?28z4rbd4pp790t2

更新
一张试图解释WrapGridPanel作用的图片:

alt text


@Aphex:问题在于每个星形行的高度都是,它应该是90、180*等。我更新了我的示例。我也上传了一个新的样例应用程序。 - Fredrik Hedblad
太好了!感谢你的辛勤工作!现在要想办法在我的IronPython应用程序中使用它... - Aphex
@Aphex:当然没问题!祝你在IronPython方面好运 :) - Fredrik Hedblad
@Aphex:是的,仅仅查看别人的代码并不能让你清楚地了解正在发生的事情,而且,确实非常接近 :) - Fredrik Hedblad
我刚开始使用这个自定义的WrapPanel,但为什么它默认是垂直方向排列的?我该如何将其改为水平面板? - FireFalcon
显示剩余4条评论

1

我认为没有标准的面板实现可以像你想要的那样行为。因此,你最好的选择可能是自己编写。

一开始可能会感到有些吓人,但其实并不难。

你可以在某个地方(mono?)找到WrapPanel源代码,并将其适应到你的需求。

你不需要列和行。你所需要的只是一个附加的Size属性:

<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>

这让我想到了一件事。有没有一种方法可以派生出支持接受 * 的 Height 依赖属性的 WrapPanel? - Aphex
没有理由从它派生,因为WrapPanel除了重写MeasureOverrideArrangeOverride(这两个方法包含“布局行为”)之外几乎不做任何事情。 - Nicolas Repiquet
好的,那么我该如何实现这个“StuffPanel”? - Aphex
我将授予赏金给一个能够给我一些关于如何实现支持此行为的WrapPanel的想法的答案。那个Size属性是一个不错的语法想法,但它是如何实现的? - Aphex

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接