如何保持WPF元素在背景图像上的相对位置

Mas*_*rfu 7 wpf layout

我是 WPF 的新手,所以以下问题的答案可能很明显,但对我来说不是。我需要显示一个图像,用户可以在其中设置标记(例如:您可能想用矩形在照片上标记一个人的脸),但是在缩放图像时标记需要保持它们的相对位置。

目前我通过使用 aCanvas并将a设置ImageBrush为背景来做到这一点。这将显示图像,我可以Label在图像顶部添加诸如 a (作为矩形的替代品)之类的元素。但是当我设置这样的标签时,它的位置是绝对的,所以当底层图片被缩放时(因为用户将窗口拖得更大)Label,它会保持在它的绝对位置(比如 100,100)而不是移动到新位置它与底层图像“同步”。

简而言之:当我在一个人的眼睛上设置一个标记时,缩放窗口后它不应该在这个人的耳朵上。

关于如何在 WPF 中做到这一点的任何建议?也许首先Canvas是错误的方法?我可以在代码中保留一组标记,并在每次调整窗口大小时重新计算它们的位置,但我希望有一种方法可以让 WPF 为我完成这项工作:-)

我很想听听您对此的意见。谢谢

Eli*_*ron 7

虽然这篇文章很旧并且已经回答了,但它仍然可以对其他人有所帮助,所以我会添加我的答案。

我想出了两种方法来保持元素的相对位置 Canvas

  1. 多值转换器
  2. 附加属性

这个想法是在 [0,1] 范围内提供两个值 (x,y),它们将定义元素相对于Canvas. 这些 (x,y) 值将用于计算和设置正确的Canvas.LeftCanvas.Top值。

为了放置中心的元素的相对位置,我们将需要ActualWidthActualHeightCanvas 元素。

多值转换器

多值转换器RelativePositionConverter

当与Canvas.Left和绑定时,此转换器可用于相对定位 X 和/或 Y 位置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();
    }
}
Run Code Online (Sandbox Code Playgroud)

示例用法RelativePositionConverter

Canvas宽度和高度绑定到一个Image。TheCanvas有一个子元素 - anEllipseCanvas(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>
Run Code Online (Sandbox Code Playgroud)

附加属性

附加属性RelativeXPropertyRelativeYProperty以及RelativePositionProperty

  • RelativeXProperty并且RelativeYProperty可用于通过两个单独的附加属性控制 X 和/或 Y 相对定位。
  • RelativePositionProperty 可用于通过单个附加属性控制 X 和 Y 相对定位。
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);
        };
    }
}
Run Code Online (Sandbox Code Playgroud)

的实施例的使用RelativeXPropertyRelativeYProperty

<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>
Run Code Online (Sandbox Code Playgroud)

示例用法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>
Run Code Online (Sandbox Code Playgroud)

和听到的是它的外观:Ellipse是一个孩子Canvas保持相对位置的Canvas(和Image)。 在此处输入图片说明


Mas*_*rfu 6

好吧,这似乎有效。这是我所做的:

  1. 写了一个自定义转换器
  2. 每次用户单击画布时,我都会创建一个新标签(稍后将与 UserComponent 交换),使用我的转换器类创建绑定并进行初始计算以从鼠标的绝对位置获取到画布的相对位置指针

以下是转换器的一些示例代码:

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

以下是在 XAML 中创建绑定的方法(请注意,我的画布被声明为<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>
Run Code Online (Sandbox Code Playgroud)

然而,更有用的是在代码中创建标签:

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);
}
Run Code Online (Sandbox Code Playgroud)

所以基本上,这几乎就像我的第一个想法:使用相对位置而不是绝对位置并在每次调整大小时重新计算所有位置,只有这样它才能由 WPF 完成。正是我想要的,谢谢马丁!

但是请注意,这些示例仅在内部的图像ImageBrush与周围的尺寸完全相同时才有效Canvas,因为这种相对定位不考虑边距等。我将不得不调整


Mar*_*ris 5

在我的脑海中,您可以编写一个转换器类,该类将接收一个百分比并返回一个绝对位置。例如,如果您的窗口是 200 X 200,并且当您将窗口缩放到 400 X 400 时将标签放置在 100 X 100,则标签将保持原样(根据您的原始问题)。但是,如果您使用转换器,因此您可以将标签位置设置为其父容器大小的 50%,那么当窗口缩放时,标签将随之移动。

您可能还需要为宽度和高度使用相同的转换器,以便它的大小也增加以匹配。

很抱歉缺乏细节,如果有机会,我将在一段时间内使用示例代码对其进行编辑。


编辑添加

这个问题给出了百分比转换器的一些代码。