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

时间:2009-04-28 08:54:23

标签: wpf layout

我是WPF的新手,所以对以下问题的回答可能很明显,但对我来说并非如此。 我需要显示一个图像,用户可以在其上设置标记(例如:您可能希望在带有矩形的照片上标记人物的脸部),但标记需要在缩放图像时保持其相对位置。

目前,我正在使用Canvas并将ImageBrush设置为背景。这将显示图像,我可以在图像顶部添加Label(作为矩形的替换)等元素。但是当我设置这样的标签时,它的位置是绝对的,因此当缩放基础图片时(因为用户拖动窗口较大),Label保持在它的绝对位置(比如100,100),而不是移动到保持与基础图像“同步”的新位置。

简化问题:当我在人眼上设置标记时,在缩放窗口后不应该在人的耳朵上。

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

我有兴趣听取您的意见。 感谢

4 个答案:

答案 0 :(得分:6)

尽管这篇文章很旧并且已经回答了,但对其他人还是有帮助的,所以我将添加我的答案。

我想出了两种方法来维护Canvas

中元素的相对位置
  1. MultiValueConverter
  2. 附加属性

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

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

MultiValueConverter

MultiValueConverter RelativePositionConverter

在与Canvas.LeftCanvas.Top绑定时,可以使用此转换器相对定位X和/或Y位置。

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();
    }
}

RelativePositionConverter的用法示例:

Canvas的宽度和高度绑定到ImageCanvas有一个子元素-Ellipse,它与Canvas(和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>

附加属性

附加属性RelativeXPropertyRelativeYPropertyRelativePositionProperty

  • RelativeXPropertyRelativeYProperty可用于通过两个单独的附加属性来控制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);
        };
    }
}

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>

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>

然后听听它的外观: 作为Ellipse的子元素的Canvas相对于Canvas(和Image)保持相对位置。 enter image description here

答案 1 :(得分:4)

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

  1. 写了一个自定义转换器
  2. 每次用户点击画布时,我都会创建一个新的Label(稍后会与UserComponent交换),使用我的转换器类创建绑定并进行初始计算以从绝对位置获取画布的相对位置鼠标指针
  3. 以下是转换器的一些示例代码:

    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;
        }
    }
    

    以下是如何在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>
    

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

    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);
        }
    

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

    请注意,这些示例仅在ImageBrush内的图片与周围Canvas具有完全相同的尺寸时才有效,因为此相对定位不会占用边距等等。我将不得不调整

答案 2 :(得分:2)

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

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

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


已编辑添加

This question为百分比转换器提供了一些代码。

答案 3 :(得分:0)

非常有用的答案。只需修改PercentageConverter类应该继承IValueConverter的一行。 感谢。