【问题标题】:WPF WrapPanel with some items having a height of *WPF WrapPanel 的某些项目的高度为 *
【发布时间】:2011-06-03 15:48:51
【问题描述】:

如何制作包含高度为 * 的某些项目的 WrapPanel?

我一直在尝试解决一个看似简单的问题。我想要一个控件(或一些 XAML 布局魔术),其行为类似于具有某些高度为 * 的行的网格,但支持列的换行。地狱;称它为 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>

我希望这个面板做的是当项目不能小于其 minHeight 时,将项目包装到一个附加列中。 这是我制作的一些模型的可怕 MSPaint,详细说明了这个过程.

从 XAML 中回想一下,自动大小按钮的 minHeights 为 30,而星号按钮的 minHeights 为 90。

这个模型只是两个并排的网格,我在设计器中手动移动了按钮。可以想象,这可以通过编程方式完成,并作为一种复杂的解决方案。

如何做到这一点?我会接受任何解决方案,无论是通过 xaml 还是有一些代码隐藏(但如果可能,我更喜欢纯 XAML,因为 xaml 代码在 IronPython 中更难实现)。

更新了赏金


Meleak 的解决方案

我设法弄清楚如何在我的 IPy 应用程序中使用 Meleak 的解决方案:

1) 我将WrapGridPanel.cs 编译成带有csc 的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 中的一个错误。因此,作为一种解决方法,为了能够使用设计器,我只需在运行时以编程方式添加 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

更新:性能问题已基本解决。谢谢梅莱克!

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

【问题讨论】:

  • 您应该能够接受代码隐藏解决方案,只需在 IronPython 解决方案中引用该 .Net 类型。
  • @siz:是的,但我不确定如何让代码隐藏“编译”,可以这么说,以便能够从 IPy 引用它。
  • @Aphex:我会调查你的问题。它肯定可以优化。
  • @Aphex:用优化版本更新了我的代码。应该快得多。我接下来看看另一个问题
  • @Aphex:问题是保证金,我没有考虑到这一点。我会解决的

标签: .net wpf visual-studio wpf-controls


【解决方案1】:

更新
优化了RecalcMatrix,因此仅在需要时才重新构建 UI。除非必要,否则它不会接触 UI,因此它应该更快。

再次更新
修复使用Margin时的问题

我认为您正在查看的基本上是带有水平方向的WrapPanel,其中每个元素都是1 列Grid。列中的每个元素都有一个对应的RowDefinition,其中Height 属性匹配其子级上设置的附加属性("WrapHeight")。该面板本身必须位于Grid 中,并带有Height="*"Width="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>

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做什么的图片

【讨论】:

  • 非常酷!我下载了你的测试代码并试了一下。我只发现了一个错误:似乎忽略了 MinHeight 高于最小 MinHeight 的星型元素。也就是说,如果有几个MinHeight为90的星型对象,那么MinHeight为180的星型对象将被视为MinHeight为90,并且会重叠。这是显示此内容的屏幕截图。我已经用最小高度标记了每个星大小的元素:i.imgur.com/H9GqU.png
  • @Aphex:没注意到那个!我会调查的
  • @Aphex:问题是每个星形大小的行都有 * 作为高度,它应该是 90*、180* 等。我更新了我的示例。我也上传了一个新的示例应用
  • 完美!感谢您的辛勤工作!现在要弄清楚如何在我的 IronPython 应用程序中使用它...
  • @Aphex:没问题!祝 IronPython 好运 :)
【解决方案2】:

我不认为有一个标准的面板实现可以像你想要的那样。所以,你最好的选择可能是自己动手。

一开始可能听起来很吓人,但并不难。

您可能可以在某处挖掘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>

【讨论】:

  • 这让我开始思考。有没有办法派生一个支持接受 * 的高度依赖属性的 WrapPanel?
  • 没有理由从中派生,因为 WrapPanel 除了覆盖 MeasureOverrideArrangeOverride (这两个方法包含“布局行为”)之外几乎没有什么作用。
  • 好的,那我该如何实现这个StuffPanel
  • 我会将赏金奖励给一个可以让我了解如何实现支持此行为的 WrapPanel 的答案。 Size 属性是一个不错的语法想法,但它是如何实现的呢?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-11-09
  • 1970-01-01
  • 2018-09-06
  • 2013-02-27
  • 2010-11-07
  • 1970-01-01
相关资源
最近更新 更多