Reputation: 6128
I have a resizable container control in my layout. It should contain any number of my user controls (which are also resizeable and always 3*2 for now) in following way:
I hope you have got the idea. I was trying to use StackPanel but failed. It didn't centred the elements for me.
So, how do I get the desired layout using XAML? Hopefully avoid coding as much as possible...
THE CODE I ENDED UP WITH:
I believe I must share the result. I took the code from this answer WPF - How can I center all items in a WrapPanel? and added child controls resizing.
private Size elementFinalSize = new Size(0, 0);
private Size MeasureDesiredSize(Size containerSize)
{
if (base.InternalChildren.Count == 0)
{
return new Size(0, 0);
}
// NB!: We assume all the items in the panel are of the same size.
var child = base.InternalChildren[0];
double elementAspectRatio = child.DesiredSize.Height == 0 ? 2.0 / 3.0 : child.DesiredSize.Width / child.DesiredSize.Height;
Size finalElementSize = new Size(0, 0);
Size newElementSize = finalElementSize;
for (int possibleRowsNumber = 1; ; possibleRowsNumber++)
{
int numberOfElementInRow = this.GetElementsNumberInRow(base.InternalChildren.Count, possibleRowsNumber);
double maxElementHeight = containerSize.Height / possibleRowsNumber;
double maxElementWidth = containerSize.Width / numberOfElementInRow;
if (maxElementWidth / elementAspectRatio > maxElementHeight)
{
// So many elements is more in width than container size, thus use Height.
newElementSize = new Size(maxElementHeight * elementAspectRatio, maxElementHeight);
}
else
{
// The element Height is greater than container row size, thus use Width.
newElementSize = new Size(maxElementWidth, maxElementWidth / elementAspectRatio);
}
if (newElementSize.Height > finalElementSize.Height)
{
// With such number of row a single element would be bigger than with previous number of rows.
finalElementSize = newElementSize;
}
else
{
// With such number of rows a single element is less than the previous biggest one, thus stop searching.
break;
}
}
return finalElementSize;
}
private int GetElementsNumberInRow(int elementsCount, int rowsNumber)
{
int x = elementsCount % rowsNumber;
if (x == 0)
{
return elementsCount / rowsNumber;
}
int maxPossibleElementsCount = elementsCount + rowsNumber - x;
return maxPossibleElementsCount / rowsNumber;
}
protected override Size MeasureOverride(Size constraint)
{
// This MeasureOverride functions is called the first of the three overridden.
// We need to understand the best (maximum) size for future element and arrange items accordingly.
// Drop the desired size to zero in order to recalculate it as soon as necessary.
this.elementFinalSize = new Size(0, 0);
Size curLineSize = new Size();
Size panelSize = new Size();
UIElementCollection children = base.InternalChildren;
for (int i = 0; i < children.Count; i++)
{
UIElement child = children[i] as UIElement;
// Flow passes its own constraint to children
if (this.elementFinalSize.Height == 0)
{
child.Measure(constraint);
this.elementFinalSize = this.MeasureDesiredSize(constraint);
child.InvalidateMeasure();
}
if (curLineSize.Width + this.elementFinalSize.Width > constraint.Width) //need to switch to another line
{
panelSize.Width = Math.Max(curLineSize.Width, panelSize.Width);
panelSize.Height += curLineSize.Height;
curLineSize = this.elementFinalSize;
if (this.elementFinalSize.Width > constraint.Width) // if the element is wider then the constraint - give it a separate line
{
panelSize.Width = Math.Max(this.elementFinalSize.Width, panelSize.Width);
panelSize.Height += this.elementFinalSize.Height;
curLineSize = new Size();
}
}
else //continue to accumulate a line
{
curLineSize.Width += this.elementFinalSize.Width;
curLineSize.Height = Math.Max(this.elementFinalSize.Height, curLineSize.Height);
}
}
foreach (UIElement child in children)
{
child.Measure(this.elementFinalSize);
}
// the last line size, if any need to be added
panelSize.Width = Math.Max(curLineSize.Width, panelSize.Width);
panelSize.Height += curLineSize.Height;
return panelSize;
}
protected override Size ArrangeOverride(Size arrangeBounds)
{
int firstInLine = 0;
Size curLineSize = new Size();
double accumulatedHeight = 0;
UIElementCollection children = this.InternalChildren;
for (int i = 0; i < children.Count; i++)
{
if (curLineSize.Width + this.elementFinalSize.Width > arrangeBounds.Width) //need to switch to another line
{
ArrangeLine(accumulatedHeight, curLineSize, arrangeBounds.Width, firstInLine, i);
accumulatedHeight += curLineSize.Height;
curLineSize = this.elementFinalSize;
if (this.elementFinalSize.Width > arrangeBounds.Width) //the element is wider then the constraint - give it a separate line
{
ArrangeLine(accumulatedHeight, this.elementFinalSize, arrangeBounds.Width, i, ++i);
accumulatedHeight += this.elementFinalSize.Height;
curLineSize = new Size();
}
firstInLine = i;
}
else //continue to accumulate a line
{
curLineSize.Width += this.elementFinalSize.Width;
curLineSize.Height = Math.Max(this.elementFinalSize.Height, curLineSize.Height);
}
}
if (firstInLine < children.Count)
ArrangeLine(accumulatedHeight, curLineSize, arrangeBounds.Width, firstInLine, children.Count);
return arrangeBounds;
}
private void ArrangeLine(double y, Size lineSize, double boundsWidth, int start, int end)
{
double x = 0;
if (this.HorizontalContentAlignment == HorizontalAlignment.Center)
{
x = (boundsWidth - lineSize.Width) / 2;
}
else if (this.HorizontalContentAlignment == HorizontalAlignment.Right)
{
x = (boundsWidth - lineSize.Width);
}
UIElementCollection children = InternalChildren;
for (int i = start; i < end; i++)
{
UIElement child = children[i];
child.Arrange(new Rect(x, y, this.elementFinalSize.Width, this.elementFinalSize.Height));
x += this.elementFinalSize.Width;
}
}
Upvotes: 2
Views: 4015
Reputation: 2299
The following will produce a nearly the same effect:
<Grid>
<WrapPanel HorizontalAlignment="Center" VerticalAlignment="Center" >
<Rectangle Stroke="Red" StrokeThickness="2" Width="200" Height="200"/>
<Rectangle Stroke="Red" StrokeThickness="2" Width="200" Height="200"/>
<Rectangle Stroke="Red" StrokeThickness="2" Width="200" Height="200"/>
</WrapPanel>
</Grid>
but the items are left aligned so in your case your last screenshot would show the bottom rectangle aligned to the left. To fix that, you can create a custom implementation of WrapPanel just for that purpose
Edit: Of course this is just a dummy example with the rectangles and all, usually one would create a ListView/ListBox/ItemContainer (depends on the scenario) and make the ItemsPanel be a WrapPanel:
<ListView>
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel .../>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
</ListView>
Upvotes: 5