How to keep relative position of WPF elements on background image
Okay that seems to work. Here's what I did:
- Wrote a custom converter
- Every time a user clicks on the canvas, I create a new Label (will exchange that with a UserComponent later), create bindings using my converter class and do the initial calculations to get the relative position to the canvas from the absolute position of the mouse pointer
Here's some sample code for the converter:
public class PercentageConverter : IValueConverter
{
/// <summary>
/// Calculates absolute position values of an element given the dimensions of the container and the relative
/// position of the element, expressed as percentage
/// </summary>
/// <param name="value">Dimension value of the container (width or height)</param>
/// <param name="parameter">The percentage used to calculate new absolute value</param>
/// <returns>parameter * value as Double</returns>
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
//input is percentage
//output is double
double containerValue = System.Convert.ToDouble(value, culture.NumberFormat);
double perc;
if (parameter is String)
{
perc = double.Parse(parameter as String, culture.NumberFormat);
}
else
{
perc = (double)parameter;
}
double coord = containerValue * perc;
return coord;
}
/// <summary>
/// Calculates relative position (expressed as percentage) of an element to its container given its current absolute position
/// as well as the dimensions of the container
/// </summary>
/// <param name="value">Absolute value of the container (width or height)</param>
/// <param name="parameter">X- or Y-position of the element</param>
/// <returns>parameter / value as double</returns>
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
//output is percentage
//input is double
double containerValue = System.Convert.ToDouble(value, culture.NumberFormat);
double coord = double.Parse(parameter as String, culture.NumberFormat);
double perc = coord / containerValue;
return perc;
}
}
And here's how you can create bindings in XAML (note that my canvas is declared as <Canvas x:Name="canvas" ... >
):
<Label Background="Red" ClipToBounds="True" Height="22" Name="label1" Width="60"
Canvas.Left="{Binding Converter={StaticResource PercentageConverter}, ElementName=canvas, Path=ActualWidth, ConverterParameter=0.25}"
Canvas.Top="{Binding Converter={StaticResource PercentageConverter}, ElementName=canvas, Path=ActualHeight, ConverterParameter=0.65}">Marker 1</Label>
More useful, however, is to create Labels in code:
private void canvas_MouseDown(object sender, MouseButtonEventArgs e)
{
var mousePos = Mouse.GetPosition(canvas);
var converter = new PercentageConverter();
//Convert mouse position to relative position
double xPerc = (double)converter.ConvertBack(canvas.ActualWidth, typeof(Double), mousePos.X.ToString(), Thread.CurrentThread.CurrentCulture);
double yPerc = (double)converter.ConvertBack(canvas.ActualHeight, typeof(Double), mousePos.Y.ToString(), Thread.CurrentThread.CurrentCulture);
Label label = new Label { Content = "Label", Background = (Brush)new BrushConverter().ConvertFromString("Red")};
//Do binding for x-coordinates
Binding posBindX = new Binding();
posBindX.Converter = new PercentageConverter();
posBindX.ConverterParameter = xPerc;
posBindX.Source = canvas;
posBindX.Path = new PropertyPath("ActualWidth");
label.SetBinding(Canvas.LeftProperty, posBindX);
//Do binding for y-coordinates
Binding posBindY = new Binding();
posBindY.Converter = new PercentageConverter();
posBindY.ConverterParameter = yPerc;
posBindY.Source = canvas;
posBindY.Path = new PropertyPath("ActualHeight");
label.SetBinding(Canvas.TopProperty, posBindY);
canvas.Children.Add(label);
}
So basically, it's almost like my first idea: Use relative position instead of absolute and recalculate all positions on every resize, only this way it's being done by WPF. Just what I wanted, thanks Martin!
Note however, that these examples only work if the Image inside the ImageBrush
has exactly the same dimensions as the surrounding Canvas
, because this relative positioning does not take margins etc into account. I will have to tune that
Although this post is old and already answered, it can still be helpful to others so I will add my answer.
I came up with two ways for maintaining a relative position for elements in a Canvas
- MultiValueConverter
- Attached Properties
The idea is to provide two values (x,y) in range [0,1] that will define the relative position of the element with respect to the top-left corner of the Canvas
. These (x,y) values will be used to calculate and set the correct Canvas.Left
and Canvas.Top
values.
In order to place the center of the element at a relative position, we will need the ActualWidth
and ActualHeight
of the Canvas
and the element.
MultiValueConverter
The MultiValueConverter RelativePositionConverter
:
This converter can be used to relatively position the X and/or Y position when binding with Canvas.Left
and Canvas.Top
.
public class RelativePositionConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values?.Length < 2
|| !(values[0] is double relativePosition)
|| !(values[1] is double size)
|| !(parameter is string)
|| !double.TryParse((string)parameter, out double relativeToValue))
{
return DependencyProperty.UnsetValue;
}
return relativePosition * relativeToValue - size / 2;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Example usage of RelativePositionConverter
:
A Canvas
width and height are binded to an Image
. The Canvas
has a child element - an Ellipse
that maintains a relative position with the Canvas
(and Image
).
<Grid Margin="10">
<Image x:Name="image" Source="Images/example-graph.png" />
<Canvas Background="#337EEBE8" Width="{Binding ElementName=image, Path=ActualWidth}" Height="{Binding ElementName=image, Path=ActualHeight}">
<Ellipse Width="35" Height="35" StrokeThickness="5" Fill="#D8FFFFFF" Stroke="#FFFBF73C">
<Canvas.Left>
<MultiBinding Converter="{StaticResource RelativePositionConverter}" ConverterParameter="0.461">
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType=Canvas}" Path="ActualWidth" />
<Binding RelativeSource="{RelativeSource Self}" Path="ActualWidth" />
</MultiBinding>
</Canvas.Left>
<Canvas.Top>
<MultiBinding Converter="{StaticResource RelativePositionConverter}" ConverterParameter="0.392">
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType=Canvas}" Path="ActualHeight" />
<Binding RelativeSource="{RelativeSource Self}" Path="ActualHeight" />
</MultiBinding>
</Canvas.Top>
</Ellipse>
</Canvas>
</Grid>
Attached Properties
The Attached Properties RelativeXProperty
, RelativeYProperty
and RelativePositionProperty
:
RelativeXProperty
andRelativeYProperty
can be used to control the X and/or Y relative positioning with two separate attached properties.RelativePositionProperty
can be used to control the X and Y relative positioning with a single attached property.
public static class CanvasExtensions
{
public static readonly DependencyProperty RelativeXProperty =
DependencyProperty.RegisterAttached("RelativeX", typeof(double), typeof(CanvasExtensions), new PropertyMetadata(0.0, new PropertyChangedCallback(OnRelativeXChanged)));
public static readonly DependencyProperty RelativeYProperty =
DependencyProperty.RegisterAttached("RelativeY", typeof(double), typeof(CanvasExtensions), new PropertyMetadata(0.0, new PropertyChangedCallback(OnRelativeYChanged)));
public static readonly DependencyProperty RelativePositionProperty =
DependencyProperty.RegisterAttached("RelativePosition", typeof(Point), typeof(CanvasExtensions), new PropertyMetadata(new Point(0, 0), new PropertyChangedCallback(OnRelativePositionChanged)));
public static double GetRelativeX(DependencyObject obj)
{
return (double)obj.GetValue(RelativeXProperty);
}
public static void SetRelativeX(DependencyObject obj, double value)
{
obj.SetValue(RelativeXProperty, value);
}
public static double GetRelativeY(DependencyObject obj)
{
return (double)obj.GetValue(RelativeYProperty);
}
public static void SetRelativeY(DependencyObject obj, double value)
{
obj.SetValue(RelativeYProperty, value);
}
public static Point GetRelativePosition(DependencyObject obj)
{
return (Point)obj.GetValue(RelativePositionProperty);
}
public static void SetRelativePosition(DependencyObject obj, Point value)
{
obj.SetValue(RelativePositionProperty, value);
}
private static void OnRelativeXChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is FrameworkElement element)) return;
if (!(VisualTreeHelper.GetParent(element) is Canvas canvas)) return;
canvas.SizeChanged += (s, arg) =>
{
double relativeXPosition = GetRelativeX(element);
double xPosition = relativeXPosition * canvas.ActualWidth - element.ActualWidth / 2;
Canvas.SetLeft(element, xPosition);
};
}
private static void OnRelativeYChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is FrameworkElement element)) return;
if (!(VisualTreeHelper.GetParent(element) is Canvas canvas)) return;
canvas.SizeChanged += (s, arg) =>
{
double relativeYPosition = GetRelativeY(element);
double yPosition = relativeYPosition * canvas.ActualHeight - element.ActualHeight / 2;
Canvas.SetTop(element, yPosition);
};
}
private static void OnRelativePositionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is FrameworkElement element)) return;
if (!(VisualTreeHelper.GetParent(element) is Canvas canvas)) return;
canvas.SizeChanged += (s, arg) =>
{
Point relativePosition = GetRelativePosition(element);
double xPosition = relativePosition.X * canvas.ActualWidth - element.ActualWidth / 2;
double yPosition = relativePosition.Y * canvas.ActualHeight - element.ActualHeight / 2;
Canvas.SetLeft(element, xPosition);
Canvas.SetTop(element, yPosition);
};
}
}
Example usage of RelativeXProperty
and RelativeYProperty
:
<Grid Margin="10">
<Image x:Name="image" Source="Images/example-graph.png" />
<Canvas Background="#337EEBE8" Width="{Binding ElementName=image, Path=ActualWidth}" Height="{Binding ElementName=image, Path=ActualHeight}">
<Ellipse Width="35" Height="35" StrokeThickness="5" Fill="#D8FFFFFF" Stroke="#FFFBF73C"
local:CanvasExtensions.RelativeX="0.461"
local:CanvasExtensions.RelativeY="0.392">
</Ellipse>
</Canvas>
</Grid>
Example usage of RelativePositionProperty
:
<Grid Margin="10">
<Image x:Name="image" Source="Images/example-graph.png" />
<Canvas Background="#337EEBE8" Width="{Binding ElementName=image, Path=ActualWidth}" Height="{Binding ElementName=image, Path=ActualHeight}">
<Ellipse Width="35" Height="35" StrokeThickness="5" Fill="#D8FFFFFF" Stroke="#FFFBF73C"
local:CanvasExtensions.RelativePosition="0.461,0.392">
</Ellipse>
</Canvas>
</Grid>
And hear is how it looks:
The Ellipse
that is a child of a Canvas
maintains a relative position with respect to the Canvas
(and an Image
).
Of the top of my head you could write a converter class that would take in a percentage and return an absolute position. As an example if your window was 200 X 200 and you placed the label at 100 X 100 when you scale the window to 400 X 400 the label would stay where it is (as per your original question). However if you used a converter so that instead you could set the labels position to 50% of its parent container's size then as the window scaled the label would move with it.
You may also need to use the same converter for width and height so that it increased in size to match as well.
Sorry for the lack of detail, if I get a chance I'll edit this with example code in a little while.
Edited to add
This question gives some code for a percentage converter.