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.
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.
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;
}
}
You can simply change the Origin with RenderTransformOrigin.
<Canvas Width="Auto" Height="Auto"
HorizontalAlignment="Center"
VerticalAlignment="Center"
RenderTransformOrigin="0.5,0.5">
<Canvas.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleY="-1" ScaleX="1" />
</TransformGroup>
</Canvas.RenderTransform>
</Canvas>
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.
Related
I have a draggable user control within another user control indicating the distance of a vessel from another vessel. The placement of the object is bound to X, Y values in the back end. If the user loads a "mission", the location of this draggable object should snap to the position based on the X and Y values. Additionally, if the user drags the boat but then cancels, the position of the boat should snap back to its original position before it was dragged.
I find that if I bind to the Margin field, the margin is set correctly on the control but the draggable object stays in the same place, as if it is at 0,0,0,0 with the updated values (I can force it to move by editing the xaml and proving that any margin changes act as if the new values are 0,0,0,0:
public Thickness DaughtershipMargin
{
get { return _daughtershipMargin; }
set
{
_daughtershipMargin = value;
OnPropertyChanged();
}
}
I have also experimented with using a Canvas instead with similar results.
When initially dragging, the position is based on the relative position of the control based on a parent control - so it needs I need to do something similar
private void MouseDragElementBehavior_Dragging(object sender, MouseEventArgs e)
{
double x = System.Math.Round((e.GetPosition(colabdragObjectGrid).X - 280), 3);
double y = System.Math.Round((-1 * (e.GetPosition(colabdragObjectGrid).Y - 280)), 3);
if (x >= _minFollowDistance && x <= _maxFollowDistance && y >= _minFollowDistance && y <= _maxFollowDistance)
{
_parent.ProcessDragEvent(x.ToString(), y.ToString());
}
}
<Grid x:Name="colabdragObjectGrid" Margin="0,-10,0,0" Height="575" Width="575">
<local:colabdrag x:Name="colabdragObject" Height="100" Width="100" RenderTransformOrigin="0.5,0.5" Margin="{Binding DaughtershipMargin}">
<i:Interaction.Behaviors>
<ei:MouseDragElementBehavior ConstrainToParentBounds="True" Dragging="MouseDragElementBehavior_Dragging"/>
</i:Interaction.Behaviors>
</local:colabdrag>
</Grid>
Edit:
I got this to work by creating a point relative to the draggable area and another point for the desired position based on that point, then using TranslateTranform to get the object where it needs to be
var pointA = (colabdragObject.TransformToAncestor(colabDragObjectGrid).Transform(new Point(X,Y))
var pointB = colabdragObjectGrid.TranslatePoint(pointA, colabDragObject);
colabdragObject.RenderTransform = new TranslateTransform(pointB.X, -1 * pointB.Y);
In my WPF app I dynamically load a XAML drawing from XML at runtime. This drawing is a complex series of nested canvas and geometry 'Path's (for example):
<?xml version="1.0" encoding="utf-8"?>
<Canvas Width="1593" Height="1515">
<Canvas.Resources />
<Canvas>
<Path Fill="…" Data="…"/>
<Path Fill="…" Data="…"/>
<Path Fill="…" Data="…"/>
<Canvas>
<Canvas>
<Path Stroke="…" StrokeThickness="…" StrokeMiterLimit="…" StrokeLineJoin="…" StrokeEndLineCap="…" Data="…"/>
<Path Stroke="…" StrokeThickness="…" StrokeMiterLimit="…" StrokeLineJoin="…" StrokeEndLineCap="…" Data="…"/>
</Canvas>
</Canvas>
<Path Fill="…" Data="…"/>
<Path Fill="…" Data="…"/>
<Path Fill="…" Data="…"/>
</Canvas>
</Canvas>
The outer canvas' Height/Width are incorrectly set, as many of the Path expressions exceeds these dimensions. I don't have any control over this source XML, so I'm required to fix it up at runtime after the drawing is loaded. To load the drawing I use code similar to the following:
public static Canvas LoadDrawing(string xaml)
{
Canvas drawing = null;
using (var stringreader = new StringReader(xaml))
{
using (var xmlReader = new XmlTextReader(stringreader))
{
drawing = (Canvas)XamlReader.Load(xmlReader);
}
}
return drawing;
}
Then, I attempt to reset the canvas size, using the following code:
var canvas = LoadDrawing(…);
someContentControOnTheExistingPage.Content = canvas;
var bounds = VisualTreeHelper.GetDescendantBounds(canvas); // << 'bounds' is empty here.
canvas.Width = bounds.Width;
canvas.Height = bounds.Height;
Except, at the point where I create the canvas element, the bounds is empty. However, if I just wire a simple button and invoke GetDescendantBounds() interactively on the same canvas, then I receive expected height/width.
My takeaway is that GetDescendantBounds() does not work unless the layout with the new control has completed. So my questions are:
Is there a way I can force a layout computation prior to running GetDescendantBounds()? Or…
Is there another way I can get the bounds/extents of a visual tree, prior adding it to its parent?
Thanks
-John
Is there a way I can force a layout computation prior to running GetDescendantBounds?
Yes, call the Arrange and Measure methods of the Canvas:
var canvas = LoadDrawing("...");
someContentControOnTheExistingPage.Content = canvas;
canvas.Arrange(new Rect(someContentControOnTheExistingPage.RenderSize));
canvas.Measure(someContentControOnTheExistingPage.RenderSize);
var bounds = VisualTreeHelper.GetDescendantBounds(canvas);
canvas.Width = bounds.Width;
canvas.Height = bounds.Height;
First you need to add this line in your xaml string.
xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
This is C# code example to find control and properties.
public void LoadXaml()
{
string canvasString = #"<Canvas Name='canvas1' xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' Width = '1593' Height = '1515'> </Canvas>";
var canvas = LoadDrawing(canvasString);
//Use this line you will find height and width.
Canvas canvasCtrl = (Canvas)LogicalTreeHelper.FindLogicalNode(canvas, "canvas1");
// var bounds = VisualTreeHelper.GetDescendantBounds(canvas); // << 'bounds' is empty here.
canvas.Width = canvasCtrl.Width; //bounds.Width;
canvas.Height = canvasCtrl.Height; //bounds.Height;
}
Ok, So I have an Image object that I want to rotate around its upper left corner. I setup a RotateTransform of 'X' degrees. My Image has the margin set to (400,400,0,0). This centers it on the canvas.
The problem is that when I apply different angles of rotation, the image shifts over to the right and down. Looking at posts, I now understand WHY this is happening. The rotate transform figures out a bounding rectangle so if you rotate the image, it will have a larger bounding rectangle. The Margin is applied to that bounding rectangle which results in the actual image shifting downward and to the right as the angle changes.
What I want is for the upper left corner of the image to always be at precisely (400,400) on my canvas and the image to rotate around that point.
I have done some calculations and can get it to behave by shifting the left and top margin, but can only get the calulations right in quadrant 1 and 4 (0-90 degrees) and (270-360 degrees) using the following equations:
For 0-90 degrees: (just have to alter the TOP part of the margin. Desired margin is (400,400,0,0)
Margin=(400-sin(angle)*Image.Height, 400, 0, 0);
For 270-360 degrees: (just have to alter the TOP part of the margin. Desired margin is (400,400,0,0)
Margin=(400, 400 + sin(angle)*Image.Width, 0, 0);
I have experimented using RotateTransform.Transform(point) and using Image.TranslatPoint(point) but cannot seem to get those to work.
Does anybody have any suggestions for me?
I guess if I could figure out what the binding rectangle of the image was at any given time, I could figure out the difference between it's width/height and my image width/height and use that to shift the margin appropriately.
I have put together a small app that will show my problem. When running, the app shows a red dot where I want the upper left of the rectangle to be. I simplified it to use a grid instead of an image. At the upper left of the screen, there is a textbox where you can specify the angle of rotation. To the right of the text box are two RepeatButtons that will increment and decrement the angle if you just click and hold them down. Notice how when you change the angle, the upper left corner of the grid shifts away from the red dot. I want it to act like you put a pin in the upper left corner, and rotated it about the pin with no shift from the dot that the corner starts on.
Thanks for your help.
Here's the code for the sample app:
xaml:
<Window x:Class="RenderTransformTester.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="900" Width="900">
<Canvas x:Name="_canvas">
<Grid x:Name="_gridImage" Width="400" Height="200" Canvas.Left="400" Canvas.Top="400" Background="Navy">
<Grid.LayoutTransform>
<RotateTransform Angle="45" CenterX="0" CenterY="0"/>
</Grid.LayoutTransform>
</Grid>
<Ellipse Fill="Red" Width="20" Height="20" Margin="390,390,0,0"/>
<TextBox x:Name="_textBoxAngle" Width="70" Height="30" TextChanged="_textBoxAngle_TextChanged" Text="0"/>
<RepeatButton x:Name="_buttonUp" Width="30" Height="15" Margin="70,0,0,0" Click="_buttonUp_Click"/>
<RepeatButton x:Name="_buttonDown" Width="30" Height="15" Margin="70,15,0,0" Click="_buttonDown_Click"/>
</Canvas>
</Window>
CodeBehind:
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace RenderTransformTester
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow
{
static double _left = -1;
static double _top = -1;
public MainWindow()
{
InitializeComponent();
}
private void _textBoxAngle_TextChanged(object sender, TextChangedEventArgs e)
{
int angle;
int.TryParse(_textBoxAngle.Text, out angle);
angle += 720;
angle %= 360;
_gridImage.LayoutTransform = new RotateTransform(angle, 0, 0);
}
private void _buttonUp_Click(object sender, RoutedEventArgs e)
{
int angle;
int.TryParse(_textBoxAngle.Text, out angle);
angle++;
angle = angle % 360;
_textBoxAngle.Text = angle.ToString();
}
private void _buttonDown_Click(object sender, RoutedEventArgs e)
{
int angle;
int.TryParse(_textBoxAngle.Text, out angle);
angle--;
angle = angle % 360;
_textBoxAngle.Text = angle.ToString();
}
private static double RadiansToDegrees(double radians)
{
return radians * 180 / Math.PI;
}
private static double DegreesToRadians(double degrees)
{
return degrees * Math.PI / 180;
}
}
}
You want a TranslateTransform within a TransformGroup
<Grid>
<Grid.RenderTransform>
<TransformGroup>
<RotateTransform Angle="45" CenterX="0" CenterY="0" />
<TranslateTransform X="400" Y="400" />
</TransformGroup>
</Grid.RenderTransform>
</Grid>
Don't use Margin for this kind of positioning, use the attached Canvas properties, i.e. Canvas.Left, etc.
You could also use a TransformGroup and apply a TranslateTransform and a RotateTransform, presumably the rotation should be done before the translation.
I have a ListBox with a ItemsPanelTemplate of Canvas. I know the ScrollViewer will not work with the Canvas unless its given a height and width. I DO NOT want to give the canvas a height and width because it will not always be constant. Is there any other work around or tricks anyone has gotten to work for this situation. I know I can't be the only one with this problem. Thanks in advance here is my code so far.
Another problem is I am unable to place the ScrollViewer inside the ItemsPanelTemplate because it can only have one element nested inside.
This also restricts me from placing the canvas inside a grid to get positioning.
XAML:
<!--Core Viewer-->
<ScrollViewer x:Name="scrollViewer"
VerticalScrollBarVisibility="Hidden"
HorizontalScrollBarVisibility="Hidden">
<ListBox x:Name="objCoreViewer"
ItemsSource="{Binding ItemsSource}"
Background="LightGray"
SelectionChanged="objCoreViewer_SelectionChanged"
ItemTemplateSelector="{DynamicResource CoreViewerDataTemplateSelector}"
ItemContainerStyleSelector="{DynamicResource ItemContainerStyleSelector}"
PreviewMouseWheel="objCoreViewer_PreviewMouseWheel">
<!-- Core Map Canvas -->
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<Canvas x:Name="objCoreViewerCanvas"
Background="Transparent">
<Canvas.LayoutTransform>
<ScaleTransform ScaleX="{Binding Path=Value, ElementName=ZoomSlider}"
ScaleY="{Binding Path=Value, ElementName=ZoomSlider}" />
</Canvas.LayoutTransform>
</Canvas>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
</ScrollViewer>
To get the canvas to grow based on the the child elements inside of it you will need to override the Canvas's MeasureOverride event in a custom class that inherits from Canavas. This code works great for me:
protected override System.Windows.Size MeasureOverride(System.Windows.Size constraint)
{
Size toReport = new Size();
foreach (UIElement element in this.InternalChildren)
{
//Get the left most and top most point. No using Bottom or Right in case the controls actual bottom and right most points are less then the desired height/width
var left = Canvas.GetLeft(element);
var top = Canvas.GetTop(element);
left = double.IsNaN(left) ? 0 : left;
top = double.IsNaN(top) ? 0 : top;
element.Measure(constraint);
Size desiredSize = element.DesiredSize;
if (!double.IsNaN(desiredSize.Width) && !double.IsNaN(desiredSize.Height))
{
//left += desiredSize.Width;
//top += desiredSize.Height;
toReport.Width = toReport.Width > left +desiredSize.Width ? toReport.Width : left + desiredSize.Width;
toReport.Height = toReport.Height > top+desiredSize.Height ? toReport.Height : top + desiredSize.Height;
}
}
//Make sure scroll includes the margins incase of a border or something
toReport.Width += this.Margin.Right;
toReport.Height += this.Margin.Bottom;
return toReport;
}
Why does this work so well in wpf
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<Canvas x:Name="MyDesigner">
</Canvas>
</ScrollViewer>
Now when I do the same thing in silverlight and load an control "that can be dragged" the scrollbars are not triggered, when I drag out of view, nothing happens... but in wpf it automatically shows them...
As a quick check against the normal 'gotcha' have you explicitly set the canvas height / width properties?
If I knock up some xaml for test purposes and run it:
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<Canvas x:Name="test" Background="Beige">
<TextBlock Canvas.Left="2000" Canvas.Top="200" Text="test"/>
</Canvas>
</ScrollViewer>
Will not show a scroll bar, even though I have explicitly created content in the canvas 2000 to the left, the canvas width not being set means the scroll viewer has no range to bind to so to speak. The canvas without a width is considered is just infinitely wide from what I can see. Whilst this is not the same as dragging the concept of putting a piece of content outside of the current view is there.
As soon as you add a width, it defines a finite area to scroll on and the scroll bar shows up.
Here is the solution I found.
The canvas can grow dynamically, but you will need to explicitly set the height to a new value.
So, if you have 20 textblocks with a height of 21, you need to set the canvas height as:
Canvas.Height = 22.0 * 100;
for the scrollviewer to pick up the new height.
MainPage.xaml
<Canvas Canvas.Left="5" Canvas.Top="25" >
<ScrollViewer Width="300" Height="700" x:Name="CanSummaryScroller">
<Canvas x:Name="canSummaryCells" >
</Canvas>
</ScrollViewer>
</Canvas>
MainPage.xaml.cs
boxDevSumList is a List of TextBoxes
for (int i = 0; i < 100; i++)
{
boxDevSumList.Add(new Cell());
(boxDevSumList.ElementAt(i)).Width = 271;
(boxDevSumList.ElementAt(i)).Height = 21;
(boxDevSumList.ElementAt(i)).SetValue(FontFamilyProperty, new FontFamily("Calibri"));
(boxDevSumList.ElementAt(i)).FontSize = 12;
(boxDevSumList.ElementAt(i)).Text = "this is a test";
if (i.Equals(1) || i.Equals(3) || i.Equals(5) || i.Equals(7) || i.Equals(9))
(boxDevSumList.ElementAt(i)).Background = lgBrush;
(boxDevSumList.ElementAt(i)).Opacity = .9;
canSummaryCells.Children.Add(boxDevSumList.ElementAt(i));
boxDevSumList.ElementAt(i).SetValue(Canvas.TopProperty, (double)(i * 21) + 45);
boxDevSumList.ElementAt(i).SetValue(Canvas.LeftProperty, 4.0);
canSummaryCells.Height = 22.0 * 100;
}