【问题标题】:How to keep relative position of WPF elements on background image如何保持 WPF 元素在背景图像上的相对位置
【发布时间】:2010-10-22 06:21:41
【问题描述】:

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

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

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

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

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

【问题讨论】:

    标签: wpf layout


    【解决方案1】:

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

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

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

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

    为了将元素的中心放在相对位置,我们需要CanvasActualWidthActualHeight 元素.

    多值转换器

    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>
    

    听听就是这样: 作为Canvas 的子代的Ellipse 保持相对于Canvas(和Image)的相对位置。

    【讨论】:

      【解决方案2】:

      好吧,这似乎可行。这是我所做的:

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

      下面是在 XAML 中创建绑定的方法(请注意,我的画布被声明为 &lt;Canvas x:Name="canvas" ... &gt;):

      <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 完成的。正是我想要的,谢谢 Martin!

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

      【讨论】:

        【解决方案3】:

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

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

        抱歉,没有详细说明,如果有机会,我稍后会用示例代码对其进行编辑。


        编辑添加

        This question 给出了一些百分比转换器的代码。

        【讨论】:

          猜你喜欢
          • 2013-06-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-07-21
          • 2023-03-12
          • 1970-01-01
          相关资源
          最近更新 更多