June 30, 2009

Stretch Panel

I believe that Silverlight missing panel that will stretch children proportionally to their size. For example:


As you can see button with longer text appears longer, all buttons filling available client area. So I decide to implement StretchPanel, by inheriting from Panel and overriding MeasureOverride and ArrageOverride method.
First, we need Orientation. So I added Orientation dependency property to my controls (the same that StackPanel has). During the calculation we will need to operate either on Width or on Height of property of Size construct. And additionally in arrange method will need to move either on x or on y axis. Instead of checking Orientation value when I need to decide on which side to operate I created following delegates that are assigned in OnOrientationChanged method and then used elsewhere.
private delegate Point GetOffsetPointDelegate(double offset);
private delegate double GetSideDelegate(Size size);
private delegate void SetSideDelegate(ref Size size, double value);
We need 3 delegate types: to get side, to set side (notice ref since Size is value type), and to get offset point. We also need 2 delegates for each delegate type: to get primary side (width for horizontal) and secondary side (height for horizontal). Here is implementation of delegates along with Orientation dependency property:
private GetSideDelegate GetOtherSide;
private GetSideDelegate GetSide;
private SetSideDelegate SetOtherSide;
private SetSideDelegate SetSide;
private GetOffsetPointDelegate GetOffsetPoint;

public StretchPanel()
{
    SetDelegates(Orientation);
}

private void SetDelegates(Orientation value)
{
    GetSide = (value == Orientation.Horizontal) ? GetWidth : new GetSideDelegate(GetHeight);
    GetOtherSide = (value == Orientation.Horizontal) ? GetHeight : new GetSideDelegate(GetWidth);
    SetSide = (value == Orientation.Horizontal) ? SetWidth : new SetSideDelegate(SetHeight);
    SetOtherSide = (value == Orientation.Horizontal) ? SetHeight : new SetSideDelegate(SetWidth);
    GetOffsetPoint = (value == Orientation.Horizontal) ? GetXOffset : new GetOffsetPointDelegate(GetYOffset);
}

#region Orientation Property 

public static readonly DependencyProperty OrientationProperty =
    DependencyProperty.Register(
        "Orientation", typeof (Orientation), typeof (StretchPanel),
        new PropertyMetadata(Orientation.Horizontal, OnOrientationChanged));

public Orientation Orientation
{
    get { return (Orientation) GetValue(OrientationProperty); }
    set { SetValue(OrientationProperty, value); }
}

private static void OnOrientationChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
    ((StretchPanel) o).OnOrientationChanged((Orientation) e.NewValue, (Orientation) e.OldValue);
}

private void OnOrientationChanged(Orientation newValue, Orientation oldValue)
{
    SetDelegates(newValue);
    UpdateLayout();
}

#endregion

private static double GetWidth(Size size)
{
    return size.Width;
}

private static double GetHeight(Size size)
{
    return size.Height;
}

private static void SetWidth(ref Size size, double value)
{
    size.Width = value;
}

private static void SetHeight(ref Size size, double value)
{
    size.Height = value;
}

private static Point GetXOffset(double offset)
{
    return new Point(offset, 0);
}

private static Point GetYOffset(double offset)
{
    return new Point(0, offset);
}

All we need is to implement MeasureOverride and ArrageOverride methods.
protected override Size MeasureOverride(Size availableSize)
{
    var size = new Size();
    foreach (UIElement child in Children)
    {
        child.Measure(availableSize);
        SetSide(ref size, GetSide(size) + GetSide(child.DesiredSize));
        SetOtherSide(ref size, Math.Max(GetOtherSide(size), GetOtherSide(child.DesiredSize)));
    }
    _measureSize = size;
    return size;
}

protected override Size ArrangeOverride(Size finalSize)
{
    double offset = 0;
    foreach (UIElement child in Children)
    {
        double side = Math.Floor(GetSide(child.DesiredSize)/GetSide(_measureSize)*GetSide(finalSize));
        var final = new Size();
        SetSide(ref final, side);
        SetOtherSide(ref final, GetOtherSide(finalSize));
        child.Arrange(new Rect(GetOffsetPoint(offset), final));
        offset += side;
    }
    return finalSize;
}
In the measure method we measure all the children. And then sum up all desired size along primary axis and get the maximum value on the secondary axis. Then we save that value (_meassureSize) for later use and return it to caller. In arrange method we calculate side length as ration of desired side length on measure side length multiplied by final size side length. This will make child appear proportional to its content among others.
Markup for example above is following:
<Controls:StretchPanel xmlns:Controls="clr-namespace:Samples.Controls" 
                       Orientation="Horizontal" Height="30">
    <Button Content="Short Text" />
    <Button Content="Shorter" />
    <Button Content="Long Long Text" />
    <Button Content="Very Very Long Long Text" />
</Controls:StretchPanel>
Download source code

No comments:

Post a Comment