Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to keep relative position of WPF elements on background image

Tags:

layout

wpf

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

like image 330
Masterfu Avatar asked Apr 28 '09 08:04

Masterfu


3 Answers

Okay that seems to work. Here's what I did:

  1. Wrote a custom converter
  2. 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

like image 51
Masterfu Avatar answered Nov 16 '22 04:11

Masterfu


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.

like image 32
Martin Harris Avatar answered Nov 16 '22 02:11

Martin Harris


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

  1. MultiValueConverter
  2. 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 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). enter image description here

like image 3
Eliahu Aaron Avatar answered Nov 16 '22 02:11

Eliahu Aaron