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>
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.
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>
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
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>
精彩评论