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;
}
Related
So one of my latest side projects is developing a application detection and populating assistant. Programmatically I am absolutely fine populating the backend code for what I want accomplished. But I've run into a road block on the GUI. I need a GUI that is a Quarter circle that extends from the task bar to the bottom right of a standard windows operating system. When the user doubleclicks on the application, the circle rotates into view. I can do this with a typical windows form that has a transparent background and a fancy background image. But the square properties of the form will still apply when the user has the application open. And I do not want to block the user from higher priority apps when the circle is open.
I'm not really stuck on any one specific programming language. Although, I would prefer that it not contain much 3d rendering as it is supposed to be a computing assistant and should not maintain heavy RAM/CPU consumption whilst the user is browsing around.
Secondarily, I would like the notches of the outer rings to be mobile and extend beyond the gui a mere centimeter or so.
I would not be here if I hadn't had scoured the internet for direction on this capability. But what I've found is application GUI's of this nature tend to be most used in mobile environments.
So my questions are: How can I accomplish this? What programming language can I write this in? Is this a capability currently available? Will I have to sacrifice user control for design?
I wrote out some code doing something close to what you described.
I’m not sure to understand how you do want the circle to appear, so I just let a part of it always visible.
And I didn’t get the part about the mobile outer ring.
Creating and placing the window
The XAML is very simple, it just needs a grid to host the circle’s pieces, and some attributes to remove window decorations and taskbar icon:
<Window x:Class="circle.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Circle"
Width="250"
Height="250"
AllowsTransparency="True"
Background="Transparent"
MouseDown="WindowClicked"
ShowInTaskbar="False"
WindowStyle="None">
<Grid Name="Container"/>
</Window>
To place the window in the bottom right corner, you can use SystemParameters.WorkArea in the constructor:
public MainWindow()
{
InitializeComponent();
var desktopDim = SystemParameters.WorkArea;
Left = desktopDim.Right - Width;
Top = desktopDim.Bottom - Height;
}
Creating the shape
I build the circle as a bunch of circle pieces that I generate from code behind:
private Path CreateCirclePart()
{
var circle = new CombinedGeometry
{
GeometryCombineMode = GeometryCombineMode.Exclude,
Geometry1 = new EllipseGeometry { Center = _center, RadiusX = _r2, RadiusY = _r2 },
Geometry2 = new EllipseGeometry { Center = _center, RadiusX = _r1, RadiusY = _r1 }
};
var sideLength = _r2 / Math.Cos((Math.PI/180) * (ItemAngle / 2.0));
var x = _center.X - Math.Abs(sideLength * Math.Cos(ItemAngle * Math.PI / 180));
var y = _center.Y - Math.Abs(sideLength * Math.Sin(ItemAngle * Math.PI / 180));
var triangle = new PathGeometry(
new PathFigureCollection(new List<PathFigure>{
new PathFigure(
_center,
new List<PathSegment>
{
new LineSegment(new Point(_center.X - Math.Abs(sideLength),_center.Y), true),
new LineSegment(new Point(x,y), true)
},
true)
}));
var path = new Path
{
Fill = new SolidColorBrush(Colors.Cyan),
Stroke = new SolidColorBrush(Colors.Black),
StrokeThickness = 1,
RenderTransformOrigin = new Point(1, 1),
RenderTransform = new RotateTransform(0),
Data = new CombinedGeometry
{
GeometryCombineMode = GeometryCombineMode.Intersect,
Geometry1 = circle,
Geometry2 = triangle
}
};
return path;
}
First step is to build two concentric circles and to combine them in a CombinedGeometry with CombineMode set to exclude. Then I create a triangle just tall enough to contain the section of the ring that I want, and I keep the intersection of these shapes.
Seeing it with the second CombineMode set to xor may clarify:
Building the circle
The code above uses some instance fields that make it generic: you can change the number of pieces in the circle or their radius; it will always fill the corner.
I then populate a list with the required number of shape, and add them to the grid:
private const double MenuWidth = 80;
private const int ItemCount = 6;
private const double AnimationDelayInSeconds = 0.3;
private readonly Point _center;
private readonly double _r1, _r2;
private const double ItemSpacingAngle = 2;
private const double ItemAngle = (90.0 - (ItemCount - 1) * ItemSpacingAngle) / ItemCount;
private readonly List<Path> _parts = new List<Path>();
private bool _isOpen;
public MainWindow()
{
InitializeComponent();
// window in the lower right desktop corner
var desktopDim = SystemParameters.WorkArea;
Left = desktopDim.Right - Width;
Top = desktopDim.Bottom - Height;
_center = new Point(Width, Height);
_r2 = Width;
_r1 = _r2 - MenuWidth;
Loaded += (s, e) => CreateMenu();
}
private void CreateMenu()
{
for (var i = 0; i < ItemCount; ++i)
{
var part = CreateCirclePart();
_parts.Add(part);
Container.Children.Add(part);
}
}
ItemSpacingAngle define the blank between two consecutive pieces.
Animating the circle
The final step is to unfold the circle. Using a rotateAnimation over the path rendertransform make it easy.
Remember this part of the CreateCirclePart function:
RenderTransformOrigin = new Point(1, 1),
RenderTransform = new RotateTransform(0),
The RenderTransform tells that the animation we want to perform is a rotation, and RenderTransformOrigin set the rotation origin to the lower right corner of the shape (unit is percent).
We can now animate it on click event:
private void WindowClicked(object sender, MouseButtonEventArgs e)
{
for (var i = 0; i < ItemCount; ++i)
{
if (!_isOpen)
UnfoldPart(_parts[i], i);
else
FoldPart(_parts[i], i);
}
_isOpen = !_isOpen;
}
private void UnfoldPart(Path part, int pos)
{
var newAngle = pos * (ItemAngle + ItemSpacingAngle);
var rotateAnimation = new DoubleAnimation(newAngle, TimeSpan.FromSeconds(AnimationDelayInSeconds));
var tranform = (RotateTransform)part.RenderTransform;
tranform.BeginAnimation(RotateTransform.AngleProperty, rotateAnimation);
}
private void FoldPart(Path part, int pos)
{
var rotateAnimation = new DoubleAnimation(0, TimeSpan.FromSeconds(AnimationDelayInSeconds));
var tranform = (RotateTransform)part.RenderTransform;
tranform.BeginAnimation(RotateTransform.AngleProperty, rotateAnimation);
}
Not actually answering this, but I liked your question enough that I wanted to get a minimal proof of concept together for fun and I really enjoyed doing it so i thought I'd share my xaml with you:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ed="http://schemas.microsoft.com/expression/2010/drawing" x:Class="WpfApplication1.Window2"
Title="Window2" Height="150" Width="150" Topmost="True" MouseLeftButtonDown="Window2_OnMouseLeftButtonDown"
AllowsTransparency="True"
OpacityMask="White"
WindowStyle="None"
Background="Transparent" >
<Grid>
<ed:Arc ArcThickness="40"
ArcThicknessUnit="Pixel" EndAngle="0" Fill="Blue" HorizontalAlignment="Left"
Height="232" Margin="33,34,-115,-116" Stretch="None"
StartAngle="270" VerticalAlignment="Top" Width="232" RenderTransformOrigin="0.421,0.471"/>
<Button HorizontalAlignment="Left" VerticalAlignment="Top" Width="41" Margin="51.515,71.385,0,0" Click="Button_Click" RenderTransformOrigin="0.5,0.5">
<Button.Template>
<ControlTemplate>
<Path Data="M50.466307,88.795148 L61.75233,73.463763 89.647286,102.42368 81.981422,113.07109 z"
Fill="DarkBlue" HorizontalAlignment="Left" Height="39.606"
Stretch="Fill" VerticalAlignment="Top" Width="39.181"/>
</ControlTemplate>
</Button.Template>
</Button>
</Grid>
</Window>
And it looks like this:
I'm trying to create a border with a blue background and repeating circles. As an example:
For the vertical portion, I'm using a vertical StackPanel in a Grid. A circle (overlapping a blue Rectangle) is declared in a ControlTemplate. To produce the repetition, I've copy-pasted a bunch of ContentControls, each of which points to my ControlTemplate.
For example:
<StackPanel
Grid.Row="0"
Grid.RowSpan="3"
Grid.Column="0"
Orientation="Vertical"
>
<ContentControl
attachedProperties:LightEllipseAttachedProperties.LightState="{Binding ElementName=PhoneApplicationPage, Path=GameController.Instance.Lights}"
Template="{StaticResource LightbulbTemplate}"
/>
**Repeat N times**
<ContentControl
attachedProperties:LightEllipseAttachedProperties.LightState="{Binding ElementName=PhoneApplicationPage, Path=GameController.Instance.Lights}"
Template="{StaticResource LightbulbTemplate}"
/>
</StackPanel>
<ControlTemplate
x:Key="LightbulbTemplate"
>
<Grid>
<VisualStateManager.VisualStateGroups>
</VisualStateManager.VisualStateGroups>
<Rectangle
Fill="#3300CC"
Height="15"
Width="15"
/>
<Ellipse
x:Name="LightEllipse"
Height="8"
Width="8"
>
<Ellipse.Fill>
<SolidColorBrush />
</Ellipse.Fill>
</Ellipse>
</Grid>
</ControlTemplate>
My question: Is there a better way to create a border of repeating elements using Silverlight? Perhaps Border has a Tiling capability so it will repeat the ControlTemplate itself, rather than me adding individual ContentControls?
If you want a simple method for building a shape like that, you could try using a Rectangle with a custom StrokeDashArray:
It was generated by this XAML code:
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0" Height="200">
<Rectangle StrokeThickness="14" StrokeDashCap="Round" Stroke="#FF00B2E6" />
<Rectangle StrokeDashArray="0.1 1.3"
StrokeThickness="10" StrokeDashCap="Round" Margin=".9" >
<Rectangle.Stroke>
<SolidColorBrush Color="#BFFF0606"/>
</Rectangle.Stroke>
</Rectangle>
</Grid>
The XAML uses 2 rectangles. One as the "background" color and the other for the circles. By experimenting with the StrokeDashArray values, you can control the shape of the dash and the distance to the next dash. By using a Round dash cap, and a small dash size (.1), it generates a shape that looks nearly round. You can experiment with the location of the Rectangles, Margins etc., to control the final look.
The nice part about using this technique is that it's extremely an efficient operation on the Phone to draw the shape and it will automatically resize to content as needed.
I've a slightly different proposal (not only in XAML):
Make Background your Border. I've managed to do something like this:
It works quite good and automatically fits to UIElement size, thought may need some time (not much) to load (but can be prepared when you start your App and then reused). I've done it via WritableBitmap - just rendered that many elements (the adventage is that any elements may be used - stars, tringles, even other images) that I need:
private WriteableBitmap CreateBorderBrush(int width, int height)
{
Rectangle firstBrush = new Rectangle();
firstBrush.Width = 15;
firstBrush.Height = 15;
firstBrush.Fill = new SolidColorBrush(Colors.Blue);
Ellipse secondBrush = new Ellipse();
secondBrush.Width = 8;
secondBrush.Height = 8;
secondBrush.Fill = new SolidColorBrush(Colors.Orange);
int dimensionX = width - width % 15;
int dimensionY = height - height % 15;
WriteableBitmap bitmapToBrush = new WriteableBitmap(dimensionX, dimensionY);
for (int i = 0; i < width / 15; i++)
{
bitmapToBrush.Render(firstBrush, new TranslateTransform() { X = i * 15, Y = 0 });
bitmapToBrush.Render(secondBrush, new TranslateTransform() { X = i * 15 + 3, Y = 3 });
}
for (int i = 1; i < height / 15 - 1; i++)
{
bitmapToBrush.Render(firstBrush, new TranslateTransform() { X = 0, Y = i * 15 });
bitmapToBrush.Render(secondBrush, new TranslateTransform() { X = 3, Y = i * 15 + 3 });
bitmapToBrush.Render(firstBrush, new TranslateTransform() { X = dimensionX - 15, Y = i * 15 });
bitmapToBrush.Render(secondBrush, new TranslateTransform() { X = dimensionX - 15 + 3, Y = i * 15 + 3 });
}
for (int i = 0; i < width / 15; i++)
{
bitmapToBrush.Render(firstBrush, new TranslateTransform() { X = i * 15, Y = dimensionY - 15 });
bitmapToBrush.Render(secondBrush, new TranslateTransform() { X = i * 15 + 3, Y = dimensionY - 15 + 3 });
}
bitmapToBrush.Invalidate();
return bitmapToBrush;
}
In MainPage constructor I've used it like this:
this.Loaded += (s, e) =>
{
myGrid.Background = new ImageBrush() { ImageSource = CreateBorderBrush((int)myGrid.ActualWidth, (int)myGrid.ActualHeight) };
};
And the XAML code:
<Grid Name="myGrid" Grid.Row="0" Width="300" Height="200">
<Button x:Name="first" Content="Button" Width="150" Height="100"/>
</Grid>
I would suggest creating a separate UserControl. The number of circles can be a Dependencyproperty of the UserControl. You can then use a ItemsControl to repeat the circles.
Im working on flowchart kind of application in asp.net using silverlight.. Im a beginner in Silvelight, Creating the elements (Rectangle,Ellipse,Line.. ) dynamically using SHAPE and LINE Objects in codebehind (c#)
These shapes will be generated dynamically, meaning I'll be calling a Web service on the backend to determine how many objects/shapes need to be created. Once this is determined, I'll need to have the objects/shapes connected together.
how to connect dynamically created shapes with a line in Silverlight like a flowchart.
I read the below article, but its not working for me, actualHeight & actualWidth of shapes values are 0.
Connecting two shapes together, Silverlight 2
here is my MainPage.xaml
<UserControl x:Class="LightTest1.MainPage">
<Canvas x:Name="LayoutRoot" Background="White">
<Canvas x:Name="MyCanvas" Background="Red"></Canvas>
<Button x:Name="btnPush" Content="AddRectangle" Height="20" Width="80" Margin="12,268,348,12" Click="btnPush_Click"></Button>
</Canvas>
code behind MainPage.xaml.cs
StackPanel sp1 = new StackPanel();
public MainPage()
{
InitializeComponent();
sp1.Orientation = Orientation.Vertical;
MyCanvas.Children.Add(sp1);
}
Rectangle rect1;
Rectangle rect2;
Line line1;
private void btnPush_Click(object sender, RoutedEventArgs e)
{
rect1 = new Rectangle()
{
Height = 30,
Width = 30,
StrokeThickness = 3,
Stroke = new SolidColorBrush(Colors.Red),
};
sp1.Children.Add(rect1);
rect2 = new Rectangle()
{
Height = 30,
Width = 30,
StrokeThickness = 3,
Stroke = new SolidColorBrush(Colors.Red),
};
sp1.Children.Add(rect2);
connectShapes(rect1, rect2);
}
private void connectShapes(Shape s1, Shape s2)
{
var transform1 = s1.TransformToVisual(s1.Parent as UIElement);
var transform2 = s2.TransformToVisual(s2.Parent as UIElement);
var lineGeometry = new LineGeometry()
{
StartPoint = transform1.Transform(new Point(1, s1.ActualHeight / 2.0)),
EndPoint = transform2.Transform(new Point(s2.ActualWidth, s2.ActualHeight / 2.0))
};
var path = new Path()
{
Data = lineGeometry,
Stroke = new SolidColorBrush(Colors.Green),
};
sp1.Children.Add(path);
}
what I am doing in button click event is just adding two rectangle shapes and tring to connect them with a line (like flowchart).
Please suggest what is wrong in my code..
Try replacing the line
connectShapes(rect1, rect2);
with
Dispatcher.BeginInvoke(() => connectShapes(rect1, rect2));
I'm not sure of the exact reason why this works, but I believe the shapes are only rendered once control passes out of your code, and only once they are rendered do the ActualWidth and ActualHeight properties have a useful value. Calling Dispatcher.BeginInvoke calls your code a short time later; in fact, you may notice the lines being drawn slightly after the rectangles.
The TransformToVisual method behaves in much the same way as the ActualWidth and ActualHeight properties. It will return an identity transformation if the shape hasn't been rendered. Even if your lines were being drawn with a definite width and height, they would end up being drawn all on top of one another at the top-left.
I also found that I needed to add the lines to the Canvas, not the StackPanel, in order for them to be drawn over the rectangles. Otherwise the StackPanel quickly filled up with lines with a lot of space above them.
I am new to graphics in C#/WPF application.
I am having a WPF application and using Canvas for drawing various object at the runtime with the help of mouse. I am facing problem in drawing a line with arrow (like below):
(A) ------------>---- (B)
In this arrow sign should be at the 3rd part of the line (and should always point towards the moving mouse). For example if I click mouse at point "A" and move towards point "B" then arrow sign should point towards "B" as shown above.
Any help will be highly appreciated.
Best Regards,
Moon
Thanks for all your help and support. I have resolved my problem as below::
private static Shape DrawLinkArrow(Point p1, Point p2)
{
GeometryGroup lineGroup = new GeometryGroup();
double theta = Math.Atan2((p2.Y - p1.Y), (p2.X - p1.X)) * 180 / Math.PI;
PathGeometry pathGeometry = new PathGeometry();
PathFigure pathFigure = new PathFigure();
Point p = new Point(p1.X + ((p2.X - p1.X) / 1.35), p1.Y + ((p2.Y - p1.Y) / 1.35));
pathFigure.StartPoint = p;
Point lpoint = new Point(p.X + 6, p.Y + 15);
Point rpoint = new Point(p.X - 6, p.Y + 15);
LineSegment seg1 = new LineSegment();
seg1.Point = lpoint;
pathFigure.Segments.Add(seg1);
LineSegment seg2 = new LineSegment();
seg2.Point = rpoint;
pathFigure.Segments.Add(seg2);
LineSegment seg3 = new LineSegment();
seg3.Point = p;
pathFigure.Segments.Add(seg3);
pathGeometry.Figures.Add(pathFigure);
RotateTransform transform = new RotateTransform();
transform.Angle = theta + 90;
transform.CenterX = p.X;
transform.CenterY = p.Y;
pathGeometry.Transform = transform;
lineGroup.Children.Add(pathGeometry);
LineGeometry connectorGeometry = new LineGeometry();
connectorGeometry.StartPoint = p1;
connectorGeometry.EndPoint = p2;
lineGroup.Children.Add(connectorGeometry);
System.Windows.Shapes.Path path = new System.Windows.Shapes.Path();
path.Data = lineGroup;
path.StrokeThickness = 2;
path.Stroke = path.Fill = Brushes.Black;
return path;
}
Thanks,
Moon
I just drew two with Expression Blend 4's arrow shapes which created this:
<Window
...
xmlns:ed="http://schemas.microsoft.com/expression/2010/drawing" ... />
<ed:BlockArrow Height="8" Margin="119,181,169,0" Orientation="Left" Stroke="Black" VerticalAlignment="Top" />
<ed:LineArrow Fill="#FFF4F4F5" Height="1" Margin="119,117,169,0" Stroke="#FF027602" VerticalAlignment="Top" BendAmount="0" StartCorner="TopRight" StrokeThickness="2"/>
You can use the Canvas control to represent the arrow,
<Canvas Margin="5" Width="60" Height="20">
<Line X1="10" Y1="10" X2="50" Y2="10" Stroke="Black"
StrokeThickness="2" StrokeDashCap="Round" StrokeEndLineCap="Round" StrokeStartLineCap="Round"/>
<Line X1="35" Y1="10" X2="30" Y2="5" Stroke="Black"
StrokeThickness="2" StrokeDashCap="Round" StrokeEndLineCap="Round" StrokeStartLineCap="Round"/>
<Line X1="35" Y1="10" X2="30" Y2="15" Stroke="Black"
StrokeThickness="2" StrokeDashCap="Round" StrokeEndLineCap="Round" StrokeStartLineCap="Round"/>
</Canvas>
If you want to create this at runtime, you can create the Canvas in the code behind class.
Note that the above sample, does not actually follow arrow sign should be at the 3rd part of the line rule, you may have to modify the coordinates of the line accordingly.
I would suggest you use Path geometry. Have a look at this sample (maybe you already have) but it has similar requirements as yours, http://www.codeproject.com/KB/WPF/WPFDiagramDesigner_Part4.aspx
I would do this by using an image of the arrow you want. Make a quick photoshop .jpg or .png to use for the default arrow image and then just scale it based on the distance between points A and B. Rotation is a little more complicated, but if you've taken a basic trig course it should still be pretty easy.
The other way you could go about this is by drawing pixels. This is typically much more difficult, and the code for determining the direction of the arrow in this scenario is even more difficult. Again, I suggest the method above.
You can use the Line class to draw the line segments that compose your arrow. For example, you could draw one long line from A to B and two smaller, angled lines to draw the head pointing towards B.
The maths involved to find the appropriate line positions should not be too difficult. For the head, just settle on a size for the two segments and draw them near B angled by 30 degrees or so. Let me know if you have any issue.
The problem with the image technique is that the head will stretch with the length of the arrow. You would have to divide the bitmap between the head and the rest.
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.