Keep Canvas elements positioned relative to background image

我怕爱的太早我们不能终老 提交于 2020-02-24 00:46:14

问题


I'm trying to position elements in my Canvas relative to my background.

Window is re-sized keeping the aspect ratio. Background is stretched with window size.

The problem is once window is re-sized the element positions are incorrect. If window is re-sized just a little, elements will adjust their size a bit and would be still in the correct position, but if window is re-sized to double it's size then positioning is completely off.

So far I used Grid, but it was to no avail as well. Here is the XAML

<Window x:Class="CanvasTEMP.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow"  ResizeMode="CanResizeWithGrip" SizeToContent="WidthAndHeight" MinHeight="386" MinWidth="397.5" Name="MainWindow1"
    xmlns:c="clr-namespace:CanvasTEMP" Loaded="onLoad" WindowStartupLocation="CenterScreen" Height="386" Width="397.5" WindowStyle="None" AllowsTransparency="True" Topmost="True" Opacity="0.65">

<ItemsControl ItemsSource="{Binding}">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Canvas Height="77" Width="218">
                <Label Content="{Binding OwnerData.OwnerName}" Height="36" Canvas.Left="8" Canvas.Top="55" Width="198" Padding="0" HorizontalAlignment="Left" HorizontalContentAlignment="Center" VerticalAlignment="Center"/>
            </Canvas>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Canvas>
                <Canvas.Background>
                <ImageBrush ImageSource="Resources\default_mapping.png" Stretch="Uniform"/>
                </Canvas.Background>
            </Canvas>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemContainerStyle>
        <Style TargetType="ContentPresenter">
            <Setter Property="Canvas.Left" Value="{Binding OwnerData.left}" />
            <Setter Property="Canvas.Top" Value="{Binding OwnerData.top}" />
        </Style>
    </ItemsControl.ItemContainerStyle>
</ItemsControl>

Class that is used for data binding

public class Owner : INotifyPropertyChanged
{
    public double _left;
    public double _top;

    public string OwnerName { get; set; }
    public double top { get { return _top; }
        set
        {
            if (value != _top)
            {
                _top = value;
                OnPropertyChanged();
            }
        }
    }

    public double left
    {
        get { return _left; }
        set
        {
            if (value != _left)
            {
                _left = value;
                OnPropertyChanged();
            }
        }
    }

    public string icon { get; set; }

    public event PropertyChangedEventHandler PropertyChanged;

    private void OnPropertyChanged([CallerMemberName] string propertyName = "none passed")
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
            handler(this, new PropertyChangedEventArgs(propertyName));
    }
}

public class ForDisplay
{
    public Owner OwnerData { get; set; }
    public int Credit { get; set; }
}

And here is the code that is run every second to keep elements' position relative to window size

items[0].OwnerData.left = this.Width * (10 / Defaul_WindowSize_Width); 
items[0].OwnerData.top = this.Height * (55 / Defaul_WindowSize_Height);

10 and 50 are default Canvas.Left and Canvas.Top that are used when window is first initialized.

Would appreciate if anyone can point out what I'm doing wrong.


回答1:


Although this post is old, it can still be helpful so here is 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.

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).




回答2:


You need an attach property:

public static readonly DependencyProperty RelativeProperty = 
    DependencyProperty.RegisterAttached("Relative", typeof(double), typeof(MyControl));

public static double GetRelative(DependencyObject o)
{
    return (double)o.GetValue(RelativeProperty);
}

public static void SetRelative(DependencyObject o, double value)
{
    o.SetValue(RelativeProperty, value);
}

and a converter:

public class RelativePositionConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        var rel = (double)values[0];
        var width = (double)values[1];
        return rel * width;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

and when you add child to Canvas you need like this:

var child = new Child();
SetRelative(child, currentPosition / ActualWidth);
var multiBinding = new MultiBinding { Converter = new RelativePositionConverter() };
multiBinding.Bindings.Add(new Binding { Source = child, Path = new PropertyPath(RelativeProperty) });
multiBinding.Bindings.Add(new Binding { Source = canvas, Path = new PropertyPath(ActualWidthProperty) });
BindingOperations.SetBinding(child, LeftProperty, multiBinding);
Children.Add(child);

If you need, you can change Relative value of child separate of Canvas



来源:https://stackoverflow.com/questions/16949423/keep-canvas-elements-positioned-relative-to-background-image

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!