// (c) Copyright Microsoft Corporation.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
namespace Microsoft.Windows.Controls
{
/// <summary>
/// Positions child elements in sequential position from left to right,
/// breaking content to the next line at the edge of the containing box.
/// Subsequent ordering happens sequentially from top to bottom or from
/// right to left, depending on the value of the Orientation property.
/// </summary>
/// <QualityBand>Stable</QualityBand>
public partial class WrapPanel : Panel
{
/// <summary>
/// A value indicating whether a dependency property change handler
/// should ignore the next change notification. This is used to reset
/// the value of properties without performing any of the actions in
/// their change handlers.
/// </summary>
private bool _ignorePropertyChange;
#region public double ItemHeight
/// <summary>
/// Gets or sets the Double that represents the uniform height of all
/// items that are contained within the WrapPanel.
/// </summary>
/// <remarks>
/// The default value is NaN.
/// </remarks>
[TypeConverter(typeof(LengthConverter))]
public double ItemHeight
{
get { return (double) GetValue(ItemHeightProperty); }
set { SetValue(ItemHeightProperty, value); }
}
/// <summary>
/// Identifies the ItemHeight dependency property.
/// </summary>
public static readonly DependencyProperty ItemHeightProperty =
DependencyProperty.Register(
"ItemHeight",
typeof(double),
typeof(WrapPanel),
new PropertyMetadata(double.NaN, OnItemHeightOrWidthPropertyChanged));
#endregion public double ItemHeight
#region public double ItemWidth
/// <summary>
/// Gets or sets a Double that represents the uniform width of all items
/// that are contained within the WrapPanel.
/// </summary>
/// <remarks>
/// The default value is NaN.
/// </remarks>
[TypeConverter(typeof(LengthConverter))]
public double ItemWidth
{
get { return (double) GetValue(ItemWidthProperty); }
set { SetValue(ItemWidthProperty, value); }
}
/// <summary>
/// Identifies the ItemWidth dependency property.
/// </summary>
public static readonly DependencyProperty ItemWidthProperty =
DependencyProperty.Register(
"ItemWidth",
typeof(double),
typeof(WrapPanel),
new PropertyMetadata(double.NaN, OnItemHeightOrWidthPropertyChanged));
#endregion public double ItemWidth
#region public Orientation Orientation
/// <summary>
/// Gets or sets a value that specifies the dimension in which
/// child content is arranged.
/// </summary>
/// <remarks>
/// The default value is Horizontal.
/// </remarks>
public Orientation Orientation
{
get { return (Orientation) GetValue(OrientationProperty); }
set { SetValue(OrientationProperty, value); }
}
/// <summary>
/// Identifies the Orientation dependency property.
/// </summary>
// TODO: In WPF, WrapPanel uses AddOwner to register the Orientation
// property via StackPanel. It then gets the default value of
// StackPanel's Orientation property. It looks like this should be no
// different than using the same default value on a new Orientation
// property.
public static readonly DependencyProperty OrientationProperty =
DependencyProperty.Register(
"Orientation",
typeof(Orientation),
typeof(WrapPanel),
new PropertyMetadata(Orientation.Horizontal, OnOrientationPropertyChanged));
/// <summary>
/// OrientationProperty property changed handler.
/// </summary>
/// <param name="d">WrapPanel that changed its Orientation.</param>
/// <param name="e">Event arguments.</param>
[SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Justification = "Almost always set from the CLR property.")]
private static void OnOrientationPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
WrapPanel source = (WrapPanel)d;
Orientation value = (Orientation)e.NewValue;
// Ignore the change if requested
if (source._ignorePropertyChange)
{
source._ignorePropertyChange = false;
return;
}
// Validate the Orientation
if ((value != Orientation.Horizontal) &&
(value != Orientation.Vertical))
{
// Reset the property to its original state before throwing
source._ignorePropertyChange = true;
source.SetValue(OrientationProperty, (Orientation) e.OldValue);
string message = string.Format(
CultureInfo.InvariantCulture,
Properties.Resources.WrapPanel_OnOrientationPropertyChanged_InvalidValue,
value);
throw new ArgumentException(message, "value");
}
// Orientation affects measuring.
source.InvalidateMeasure();
}
#endregion public Orientation Orientation
/// <summary>
/// Initializes a new instance of the WrapPanel class.
/// </summary>
public WrapPanel()
{
}
/// <summary>
/// Property changed handler for ItemHeight and ItemWidth.
/// </summary>
/// <param name="d">
/// WrapPanel that changed its ItemHeight or ItemWidth.
/// </param>
/// <param name="e">Event arguments.</param>
[SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Justification = "Almost always set from the CLR property.")]
private static void OnItemHeightOrWidthPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
WrapPanel source = (WrapPanel) d;
double value = (double) e.NewValue;
// Ignore the change if requested
if (source._ignorePropertyChange)
{
source._ignorePropertyChange = false;
return;
}
// Validate the length (which must either be NaN or a positive,
// finite number)
if (!value.IsNaN() && ((value <= 0.0) || double.IsPositiveInfinity(value)))
{
// Reset the property to its original state before throwing
source._ignorePropertyChange = true;
source.SetValue(e.Property, (double) e.OldValue);
string message = string.Format(
CultureInfo.InvariantCulture,
Properties.Resources.WrapPanel_OnItemHeightOrWidthPropertyChanged_InvalidValue,
value);
throw new ArgumentException(message, "value");
}
// The length properties affect measuring.
source.InvalidateMeasure();
}
/// <summary>
/// Measures the child elements of a WrapPanel in anticipation of
/// arranging them during the ArrangeOverride pass.
/// </summary>
/// <param name="constraint">
/// An upper limit Size that should not be exceeded.
/// </param>
/// <returns>
/// Desired size of the WrapPanel and its child elements.
/// </returns>
[SuppressMessage("Microsoft.Naming", "CA1725:ParameterNamesShouldMatchBaseDeclaration", MessageId = "0#", Justification = "Compat with WPF.")]
protected override Size MeasureOverride(Size constraint)
{
// Variables tracking the size of the current line, the total size
// measured so far, and the maximum size available to fill. Note
// that the line might represent a row or a column depending on the
// orientation.
Orientation o = Orientation;
OrientedSize lineSize = new OrientedSize(o);
OrientedSize totalSize = new OrientedSize(o);
OrientedSize maximumSize = new OrientedSize(o, constraint.Width, constraint.Height);
// Determine the constraints for individual items
double itemWidth = ItemWidth;
double itemHeight = ItemHeight;
bool hasFixedWidth = !itemWidth.IsNaN();
bool hasFixedHeight = !itemHeight.IsNaN();
Size itemSize = new Size(
hasFixedWidth ? itemWidth : constraint.Width,
hasFixedHeight ? itemHeight : constraint.Height);
// Measure each of the Children
foreach (UIElement element in Children)
{
// Determine the size of the element
element.Measure(itemSize);
OrientedSize elementSize = new OrientedSize(
o,
hasFixedWidth ? itemWidth : element.DesiredSize.Width,
hasFixedHeight ? itemHeight : element.DesiredSize.Height);
// If this element falls of the edge of the line
if (NumericExtensions.IsGreaterThan(lineSize.Direct + elementSize.Direct, maximumSize.Direct))
{
// Update the total size with the direct and indirect growth
// for the current line
totalSize.Direct = Math.Max(lineSize.Direct, totalSize.Direct);
totalSize.Indirect += lineSize.Indirect;
// Move the element to a new line
lineSize = elementSize;
// If the current element is larger than the maximum size,
// place it on a line by itself
if (NumericExtensions.IsGreaterThan(elementSize.Direct, maximumSize.Direct))
{
// Update the total size for the line occupied by this
// single element
totalSize.Direct = Math.Max(elementSize.Direct, totalSize.Direct);
totalSize.Indirect += elementSize.Indirect;
// Move to a new line
lineSize = new OrientedSize(o);
}
}
else
{
// Otherwise just add the element to the end of the line
lineSize.Direct += elementSize.Direct;
lineSize.Indirect = Math.Max(lineSize.Indirect, elementSize.Indirect);
}
}
// Update the total size with the elements on the last line
totalSize.Direct = Math.Max(lineSize.Direct, totalSize.Direct);
totalSize.Indirect += lineSize.Indirect;
// Return the total size required as an un-oriented quantity
return new Size(totalSize.Width, totalSize.Height);
}
/// <summary>
/// Arranges the content of a WrapPanel element.
/// </summary>
/// <param name="finalSize">
/// The Size that this element should use to arrange its child elements.
/// </param>
/// <returns>
/// The arranged size of this WrapPanel element and its children.
/// </returns>
protected override Size ArrangeOverride(Size finalSize)
{
// Variables tracking the size of the current line, and the maximum
// size available to fill. Note that the line might represent a row
// or a column depending on the orientation.
Orientation o = Orientation;
OrientedSize lineSize = new OrientedSize(o);
OrientedSize maximumSize = new OrientedSize(o, finalSize.Width, finalSize.Height);
// Determine the constraints for individual items
double itemWidth = ItemWidth;
double itemHeight = ItemHeight;
bool hasFixedWidth = !itemWidth.IsNaN();
bool hasFixedHeight = !itemHeight.IsNaN();
double indirectOffset = 0;
double? directDelta = (o == Orientation.Horizontal) ?
(hasFixedWidth ? (double?) itemWidth : null) :
(hasFixedHeight ? (double?) itemHeight : null);
// Measure each of the Children. We will process the elements one
// line at a time, just like during measure, but we will wait until
// we've completed an entire line of elements before arranging them.
// The lineStart and lineEnd variables track the size of the
// currently arranged line.
UIElementCollection children = Children;
int count = children.Count;
int lineStart = 0;
for (int lineEnd = 0; lineEnd < count; lineEnd++)
{
UIElement element = children[lineEnd];
// Get the size of the element
OrientedSize elementSize = new OrientedSize(
o,
hasFixedWidth ? itemWidth : element.DesiredSize.Width,
hasFixedHeight ? itemHeight : element.DesiredSize.Height);
// If this element falls of the edge of the line
if (NumericExtensions.IsGreaterThan(lineSize.Direct + elementSize.Direct, maximumSize.Direct))
{
// Then we just completed a line and we should arrange it
ArrangeLine(lineStart, lineEnd, directDelta, indirectOffset, lineSize.Indirect);
// Move the current element to a new line
indirectOffset += lineSize.Indirect;
lineSize = elementSize;
// If the current element is larger than the maximum size
if (NumericExtensions.IsGreaterThan(elementSize.Direct, maximumSize.Direct))
{
// Arrange the element as a single line
ArrangeLine(lineEnd, ++lineEnd, directDelta, indirectOffset, elementSize.Indirect);
// Move to a new line
indirectOffset += lineSize.Indirect;
lineSize = new OrientedSize(o);
}
// Advance the start index to a new line after arranging
lineStart = lineEnd;
}
else
{
// Otherwise just add the element to the end of the line
lineSize.Direct += elementSize.Direct;
lineSize.Indirect = Math.Max(lineSize.Indirect, elementSize.Indirect);
}
}
// Arrange any elements on the last line
if (lineStart < count)
{
ArrangeLine(lineStart, count, directDelta, indirectOffset, lineSize.Indirect);
}
return finalSize;
}
/// <summary>
/// Arrange a sequence of elements in a single line.
/// </summary>
/// <param name="lineStart">
/// Index of the first element in the sequence to arrange.
/// </param>
/// <param name="lineEnd">
/// Index of the last element in the sequence to arrange.
/// </param>
/// <param name="directDelta">
/// Optional fixed growth in the primary direction.
/// </param>
/// <param name="indirectOffset">
/// Offset of the line in the indirect direction.
/// </param>
/// <param name="indirectGrowth">
/// Shared indirect growth of the elements on this line.
/// </param>
private void ArrangeLine(int lineStart, int lineEnd, double? directDelta, double indirectOffset, double indirectGrowth)
{
double directOffset = 0.0;
Orientation o = Orientation;
bool isHorizontal = o == Orientation.Horizontal;
UIElementCollection children = Children;
for (int index = lineStart; index < lineEnd; index++)
{
// Get the size of the element
UIElement element = children[index];
OrientedSize elementSize = new OrientedSize(o, element.DesiredSize.Width, element.DesiredSize.Height);
// Determine if we should use the element's desired size or the
// fixed item width or height
double directGrowth = directDelta != null ?
directDelta.Value :
elementSize.Direct;
// Arrange the element
Rect bounds = isHorizontal ?
new Rect(directOffset, indirectOffset, directGrowth, indirectGrowth) :
new Rect(indirectOffset, directOffset, indirectGrowth, directGrowth);
element.Arrange(bounds);
directOffset += directGrowth;
}
}
}
}