1

I'm trying to create canvas which resizes its children when canvas itself is resized. So I create my own class which inherits from canvas and overrides method ArrangeOverride where I set positions and sizes for all children defined in canvas. Everything looks fine but when I resize window of the application, items weren't resized to correct size or position.

This is simplified example, which tries to snap its elements to right border of canvas:

Xaml Code:

<Border BorderBrush="Black" BorderThickness="1" Margin="10" SnapsToDevicePixels="True">
    <l:CustomPanel>
        <Rectangle Fill="Red" />
        <Button>a</Button>
    </l:CustomPanel>
</Border>

CustomPanel:

public class CustomPanel : Canvas
{
    protected override System.Windows.Size ArrangeOverride(System.Windows.Size arrangeSize)
    {
        var ret = base.ArrangeOverride(arrangeSize);
        var top = 0;

        foreach(UIElement child in Children)
        {                               
            Canvas.SetLeft(child, arrangeSize.Width - 20.0);                
            child.SetValue(WidthProperty, arrangeSize.Width - Canvas.GetLeft(child));                
            Canvas.SetTop(child, top);
            child.SetValue(HeightProperty, 20.0);
            top += 30;
        }
        return ret;
    }
}

When I change width of the window, sometimes canvas looks like on this image:

error

And then, if I change height of the window, items move to correct position

What am I doing wrong? I tried to set SnapsToDevicePixels to True and it doesn't work for me. :-(

Community
  • 1
  • 1
Adam
  • 45
  • 1
  • 6
  • Could you please explain what exactly you are trying to achieve? A Canvas is meant for absolute positioning of its children. It should not resize them. Hence you should not derive your custom panel from Canvas, but instead from Panel. I'm pretty sure that there is a much cleaner solution than this. – Clemens Jan 16 '14 at 09:10
  • Moreover, a Panel should *never* set the Width or Height (or any other) property of its child elements. Setting Width or Height might even create another layout pass on the parent panel. Instead, child elements are *arranged* by calling UIElement.Arrange. – Clemens Jan 16 '14 at 09:26
  • The reason of doing this is that I have to create something like graph. So there are many items, which must be on specific (percentage) position and the "graph" must support resizing. I thought that I can calculate specific position of items in canvas. And I recalculate these items after the window is being resized. Do you have any better idea, how to achieve this behavior? – Adam Jan 16 '14 at 09:42

3 Answers3

3

Your custom panel should derive from Panel instead of Canvas and override the MeasureOverride and ArrangeOverride methods. Moreover it should define its own attached properties for child element layout, like the four properties RelativeX, RelativeY, RelativeWidth and RelativeHeight shown below.

It would be used in XAML like this:

<local:RelativeLayoutPanel>
    <Rectangle Fill="Red"
               local:RelativeLayoutPanel.RelativeX="0.2"
               local:RelativeLayoutPanel.RelativeY="0.1"
               local:RelativeLayoutPanel.RelativeWidth="0.6"
               local:RelativeLayoutPanel.RelativeHeight="0.8"/>
</local:RelativeLayoutPanel>

Here's the implementation:

public class RelativeLayoutPanel: Panel
{
    public static readonly DependencyProperty RelativeXProperty = DependencyProperty.RegisterAttached(
        "RelativeX", typeof(double), typeof(RelativeLayoutPanel),
        new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsArrange));

    public static readonly DependencyProperty RelativeYProperty = DependencyProperty.RegisterAttached(
        "RelativeY", typeof(double), typeof(RelativeLayoutPanel),
        new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsArrange));

    public static readonly DependencyProperty RelativeWidthProperty = DependencyProperty.RegisterAttached(
        "RelativeWidth", typeof(double), typeof(RelativeLayoutPanel),
        new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsArrange));

    public static readonly DependencyProperty RelativeHeightProperty = DependencyProperty.RegisterAttached(
        "RelativeHeight", typeof(double), typeof(RelativeLayoutPanel),
        new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsArrange));

    public static double GetRelativeX(UIElement element)
    {
        return (double)element.GetValue(RelativeXProperty);
    }

    public static void SetRelativeX(UIElement element, double value)
    {
        element.SetValue(RelativeXProperty, value);
    }

    public static double GetRelativeY(UIElement element)
    {
        return (double)element.GetValue(RelativeYProperty);
    }

    public static void SetRelativeY(UIElement element, double value)
    {
        element.SetValue(RelativeYProperty, value);
    }

    public static double GetRelativeWidth(UIElement element)
    {
        return (double)element.GetValue(RelativeWidthProperty);
    }

    public static void SetRelativeWidth(UIElement element, double value)
    {
        element.SetValue(RelativeWidthProperty, value);
    }

    public static double GetRelativeHeight(UIElement element)
    {
        return (double)element.GetValue(RelativeHeightProperty);
    }

    public static void SetRelativeHeight(UIElement element, double value)
    {
        element.SetValue(RelativeHeightProperty, value);
    }

    protected override Size MeasureOverride(Size availableSize)
    {
        availableSize = new Size(double.PositiveInfinity, double.PositiveInfinity);

        foreach (UIElement element in InternalChildren)
        {
            element.Measure(availableSize);
        }

        return new Size();
    }

    protected override Size ArrangeOverride(Size finalSize)
    {
        foreach (UIElement element in InternalChildren)
        {
            element.Arrange(new Rect(
                GetRelativeX(element) * finalSize.Width,
                GetRelativeY(element) * finalSize.Height,
                GetRelativeWidth(element) * finalSize.Width,
                GetRelativeHeight(element) * finalSize.Height));
        }

        return finalSize;
    }
}

If you don't need the four layout properties to be independently bindable or settable by style setters etc. you could perhaps replace them by a single attached property of type Rect:

<local:RelativeLayoutPanel>
    <Rectangle Fill="Red" local:RelativeLayoutPanel.RelativeRect="0.2,0.1,0.6,0.8"/>
</local:RelativeLayoutPanel>

with this much shorter implementation:

public class RelativeLayoutPanel: Panel
{
    public static readonly DependencyProperty RelativeRectProperty = DependencyProperty.RegisterAttached(
        "RelativeRect", typeof(Rect), typeof(RelativeLayoutPanel),
        new FrameworkPropertyMetadata(new Rect(), FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsArrange));

    public static Rect GetRelativeRect(UIElement element)
    {
        return (Rect)element.GetValue(RelativeRectProperty);
    }

    public static void SetRelativeRect(UIElement element, Rect value)
    {
        element.SetValue(RelativeRectProperty, value);
    }

    protected override Size MeasureOverride(Size availableSize)
    {
        availableSize = new Size(double.PositiveInfinity, double.PositiveInfinity);

        foreach (UIElement element in InternalChildren)
        {
            element.Measure(availableSize);
        }

        return new Size();
    }

    protected override Size ArrangeOverride(Size finalSize)
    {
        foreach (UIElement element in InternalChildren)
        {
            var rect = GetRelativeRect(element);

            element.Arrange(new Rect(
                rect.X * finalSize.Width,
                rect.Y * finalSize.Height,
                rect.Width * finalSize.Width,
                rect.Height * finalSize.Height));
        }

        return finalSize;
    }
}
Clemens
  • 123,504
  • 12
  • 155
  • 268
  • That looks much better than canvas solution and works properly. Thank you very much. – Adam Jan 16 '14 at 10:16
  • Works perfect even with an ItemsControl with DataTemplate. Then one has to use a Style Setter for the attached property, because InternalChildren return wrapping ContentPresenters – Tarnschaf Apr 08 '16 at 22:56
  • 1
    @Tarnschaf I guess what you want to say is that if you use a Panel as the ItemsPanel of an ItemsControl, and that Panel uses attached properties on its child elements for their layout, you'll have to set these attached properties in an ItemsContainerStyle instead of an ItemTemplate to apply them to the item container (i.e. the ContentPresenter here). This is true for all Panels that use attached properties for layout, but has nothing to with the InternalChildren property. See [this answer](http://stackoverflow.com/a/22325266/1136211) for an example with Canvas. – Clemens Apr 09 '16 at 07:21
  • Exactly what I wanted to say but you definitely stated that better. – Tarnschaf Apr 09 '16 at 10:56
1

Ok, so I tried messing around with this and I found some hacky way to make it work. I don't really understand why it works this way (yet), but this works for me:

public class CustomPanel : Canvas
{
    private bool isFirstArrange = true;

    protected override Size ArrangeOverride(Size arrangeSize)
    {
        var ret = new Size();
        bool isFirstArrangeLocal = isFirstArrange;
        if (isFirstArrangeLocal)
        {
            ret = base.ArrangeOverride(arrangeSize);
            isFirstArrange = false;
        }

        var top = 0;
        foreach (UIElement child in Children)
        {
            Canvas.SetLeft(child, arrangeSize.Width - 20.0);
            child.SetValue(WidthProperty, arrangeSize.Width - Canvas.GetLeft(child));
            Canvas.SetTop(child, top);
            child.SetValue(HeightProperty, 20.0);

            top += 30;
        }

        if (!isFirstArrangeLocal)
        {
            ret = base.ArrangeOverride(arrangeSize);
        }

        return ret;
    }
}

So the idea is to put ArrangeOverride() after the foreach loop in all situations except on first call.

The first call must be before foreach or for some reason I get this:

Invalid

mrzli
  • 16,799
  • 4
  • 38
  • 45
  • It works with this simple example, thank you. I'm trying doing the same in my real project and unfortunately it doesn't work, yet. :-( – Adam Jan 16 '14 at 09:47
0

This may or may not exactly fit your requirements, but whenever I hear/read 'resize', I think of the UI control that was built just for that purpose; the Viewbox. Using this class, you have several options over how the content should be resized, but I think that you would want the default Uniform method.

Just put whatever you need to be resized into a ViewBox and see how you like it... it should do the job nicely and simply for you:

<ViewBox Stretch="Uniform">
    <Canvas ... />
</ViewBox>

You don't actually need to add the Stretch="Uniform" here because that is the default... it's just here for demonstration purposes. To find out more, please see the Viewbox Class page on MSDN.

Sheridan
  • 68,826
  • 24
  • 143
  • 183