I am new to WPF, so the answer to the following question might be obvious, however it isn't to me. I need to display an image where users can set markers on (As an example: You might want to mark a person's face on a photograph with a rectangle), however the markers need to keep their relative position when scaling the image.
Currently I am doing this by using a Canvas
and setting an ImageBrush
as Background. This displays the image and I can add elements like a Label
(as replacement for a rectangle) on top of the image. But when I set a label like this, it's position is absolute and so when the underlying picture is scaled (because the user drags the window larger) the Label
stays at it's absolute position (say, 100,100) instead of moving to the new position that keeps it "in sync" with the underlying image.
To cut the matter short: When I set a marker on a person's eye, it shouldn't be on the person's ear after scaling the window.
Any suggestions on how to do that in WPF? Maybe Canvas
is the wrong approach in the first place? I could keep a collection of markers in code and recalculate their position every time the window gets resized, but I hope there is a way to let WPF do that work for me :-)
I am interested in hearing your opinions on this. Thanks
Okay that seems to work. Here's what I did:
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
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.
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
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.
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>
The Attached Properties RelativeXProperty
, RelativeYProperty
and RelativePositionProperty
:
RelativeXProperty
and RelativeYProperty
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
).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With