views:

947

answers:

4

The WPF Canvas has a coordinate system starting at (0,0) at the top-left of the control.

For example, setting the following will make my control appear on the top-left:

<Control Canvas.Left="0" Canvas.Top="0">

How can I change it to the standard cartesian coordinates?

Basically:

  • (0,0) at center
  • flip Y

I noticed this post is similar, but it does not talk about translating the coordinate system. I tried adding a TranslateTransform, but I can't make it work.

+3  A: 

The best thing is to write a custom Canvas, in which you can write ArrangeOverride in such a way that it takes 0,0 as the center.

Update : I had given another comment in the below answer (@decasteljau) I wont recommend deriving from Canvas, You can derive from Panel and add two Attached Dependancy properties Top and Left and put the same code you pasted above. Also doesnt need a constructor with LayoutTransform in it, And dont use any transform on the panel code use proper measure and arrange based on the DesiredSize of the panel So that you can get nice content resize behavior too. Canvas doesn't dynamically position items when the Canvas size changes.

Jobi Joy
ok, this was what I was thinking. Thanks, you confirmed it.
decasteljau
Unfortunately this answer is incorrect :-(. A custom canvas is far more work than is necessary: You can achieve exactly the same effect with no work at all by setting canvas height/width to zero, centering it in its container, and flipping it on render. See my answer below.
Ray Burns
I agree with your answer @Ray, but if you need a re sizable solution then I don't recommend your answer. which means if I set my child Canvas.Left = 0.5 and Canvas.Top=0.5, then when you resize the Parent window/canvas the child position will be totally messed up. But if you write a proper custom control (only 10-15 lines of code) it dynamically positions always to (0.5,0.5). So I recommend writing a custom panel - This resulting 'UnitPanel' is a very useful one than regular Canvas. :)
Jobi Joy
+2  A: 

It was very easy to do. I looked at the original Canvas's code using .NET Reflector, and noticed the implementation is actually very simple. The only thing required was to override the function ArrangeOverride(...)

public class CartesianCanvas : Canvas
{
    public CartesianCanvas()
    {
        LayoutTransform = new ScaleTransform() { ScaleX = 1, ScaleY = -1 };
    }
    protected override Size ArrangeOverride( Size arrangeSize )
    {
        Point middle = new Point( arrangeSize.Width / 2, arrangeSize.Height / 2 );

        foreach( UIElement element in base.InternalChildren )
        {
            if( element == null )
            {
                continue;
            }
            double x = 0.0;
            double y = 0.0;
            double left = GetLeft( element );
            if( !double.IsNaN( left ) )
            {
                x = left;
            }

            double top = GetTop( element );
            if( !double.IsNaN( top ) )
            {
                y = top;
            }

            element.Arrange( new Rect( new Point( middle.X + x, middle.Y + y ), element.DesiredSize ) );
        }
        return arrangeSize;
    }
}
decasteljau
good, But I wont recommend deriving from Canvas, You can derive from Panel and add two Attached Dependancy properties Top and Left and put the same code you pasted above. Also doesnt need a constructor with LayoutTransform in it.
Jobi Joy
A: 

I tried this stuff with a class derived from panel class but am unable to do it. Can you please post the full code. I'm pretty new to this. Also if you can add the functionality of allowing elements to be dragged around the panel. Please help. I'm very new to this and not getting any guides to help me out. If you can post the full code it'll be very helpful.

Soham Dasgupta
+5  A: 

There is no need to create a custom Panel. Canvas will do just fine. Simply wrap it inside another control (such as a border), center it, give it zero size, and flip it with a RenderTransform:

<Border>
  <Canvas HorizontalAlignment="Center" VerticalAlignment="Center"
          Width="0" Height="0"
          RenderTransform="1 0 0 -1 0 0">
    ...
  </Canvas>
</Border>

You can do this and everything in the canvas will still appear, except (0,0) will be at the center of the containing control (in this case, the center of the Border) and +Y will be up instead of down.

Again, there is no need to create a custom panel for this.

Ray Burns
@Ray, great, I was looking for something like that. However, I can't capture clicks on the canvas, is there a solution?
Bruno Reis
@Bruno: Like any panel, Canvas participates fully in event routing. You can handle mouse events at the canvas level or in its container. However it does not paint anything itself and thus does not participate in hit testing. If you want a click on a blank area of the canvas to be recognized as a click on the canvas, the easy way to do this is to add a Rectangle with Fill="Transparent" that covers the entire canvas. This rectangle will receive click events, which of course will be routed up to the canvas and above so they can be handled at any level.
Ray Burns