WPF在画布上的数据绑定元素周围放置任意内容



我们有一个(动态的)东西列表(来自同一个基或接口的几种类型),我们想要放在画布上。物体指定它们的位置。这些东西包含一些必须显示在画布上的精确位置的图形。例如,地图(作为画布背景)和具有特定符号的城市列表。这些东西的大小可以在运行时改变。但我们也想在这些东西周围添加一些其他内容,不是固定大小的内容。它必须相对于事物定位,但不影响它们的位置。例如,添加边框以指示选择。或者这个城市的名字,或者它的人口数量。

我已经用附加属性(一个行为)解决了这个问题,见下面我自己的答案。

有人知道更好的方法吗?

我解决这个问题的方法是将属性(数据绑定)附加到关键图形元素。这些属性背后的代码调整ItemContainerCanvas.LeftCanvas.Top属性。从另一端执行(绑定容器属性的样式)不起作用,或者至少我想不出一种优雅的方法来引用容器样式中的数据模板成员。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
namespace WpfApp2
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new List<Thing>
{
new Marker() { Position = new Point(10, 10) },
new Marker() { Position = new Point(250, 50) },
new Marker() { Position = new Point(50, 250) },
new Marker() { Position = new Point(250, 250) },
new Marker() { Position = new Point(150, 150) },
new RoundThing() { Name = "R1", Radius = 10, Position = new Point(10, 10) },
new RoundThing() { Name = "R2", Radius = 20, Position = new Point(250, 50) },
new RoundThing() { Name = "R3", Radius = 30, Position = new Point(50, 250) },
new RoundThing() { Name = "R4", Radius = 40, Position = new Point(250, 250) },
new ComplexThing() { Name = "CX", Position = new Point(150, 150)},
};
}
private void Canvas_PreviewMouseMove(object sender, MouseEventArgs e)
{
mousePos.Text = Mouse.GetPosition(this).ToString();
}
}
public abstract class Thing : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string property = null)
{
#if DEBUG
var pi = GetType().GetProperty(property);
if (pi == null)
throw new ArgumentException($"Property {property} not found on {this}");
#endif
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}

public string Name
{
get => name;
set
{
if (name == value)
return;
name = value;
RaisePropertyChanged();
}
}
private string name;
public Point Position
{
get => point;
set
{
if (point == value)
return;
point = value;
RaisePropertyChanged();
}
}
private Point point;

public Point Offset
{
get => offset;
set
{
if (offset == value)
return;
offset = value;
RaisePropertyChanged();
}
}
Point offset;

public Geometry Geometry
{
get => geometry ??= CreateGeometry();
set
{
if (geometry == value)
return;
geometry = value;
RaisePropertyChanged();
}
}
private Geometry geometry;
protected abstract Geometry CreateGeometry();
}
public class Marker : Thing
{
protected override Geometry CreateGeometry()
{
return g;
}
readonly static Geometry g;
static Marker()
{
g = new EllipseGeometry();
if (g.CanFreeze)
g.Freeze();
}
}
public class RoundThing : Thing
{
public double Radius
{
get => radius;
set
{
if (radius == value)
return;
radius = value;
RaisePropertyChanged();
Geometry = null;
}
}
protected double radius;
protected override Geometry CreateGeometry()
{
Offset = new Point(radius, radius);
var g = new EllipseGeometry(new Point(radius, radius), radius, radius);
if (g.CanFreeze)
g.Freeze();
return g;
}
}
public class ComplexThing : Thing
{
protected override Geometry CreateGeometry()
{
Offset = new Point(25, 10);
var grp = new GeometryGroup();
grp.Children.Add(new EllipseGeometry(new Point(10, 10), 10, 10));
grp.Children.Add(new EllipseGeometry(new Point(40, 10), 10, 10));
grp.Children.Add(new EllipseGeometry(new Point(25, 30), 10, 10));
if (grp.CanFreeze)
grp.Freeze();
return grp;
}
}
}
<Window x:Class="WpfApp2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp2"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<DataTemplate DataType="{x:Type local:Thing}">
<Border BorderThickness="1" BorderBrush="Red">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Path Grid.Column="1" Data="{Binding Geometry}" Stroke="Black" StrokeThickness="2"
local:PositionContainer.Position="{Binding Position}"
local:PositionContainer.Offset="{Binding Offset}"
local:PositionContainer.Container="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ContentPresenter}}}"/>
<TextBlock Text="some text"/>
<TextBlock Grid.Row="1" Grid.ColumnSpan="2" Text="some rather long text"/>
<TextBlock Grid.Column="2" Text="{Binding Name}" FontWeight="Bold"/>
</Grid>
</Border>
</DataTemplate>
<DataTemplate DataType="{x:Type local:RoundThing}">
<Border BorderThickness="1" BorderBrush="Red">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Path Grid.Column="1" Data="{Binding Geometry}" Stroke="Black" StrokeThickness="2"
local:PositionContainer.Position="{Binding Position}"
local:PositionContainer.Offset="{Binding Offset}"
local:PositionContainer.Container="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ContentPresenter}}}"/>
<TextBlock Text="some text"/>
<TextBlock Grid.Row="1" Grid.ColumnSpan="2" Text="some rather long text"/>
<StackPanel Grid.Column="2">
<TextBlock Text="{Binding Name}" FontWeight="Bold"/>
<TextBlock Text="{Binding Radius, StringFormat=f2}"/>
</StackPanel>
<Slider Grid.Row="2" Grid.ColumnSpan="3" Minimum="0" Maximum="100" Value="{Binding Radius}"/>
</Grid>
</Border>
</DataTemplate>
<DataTemplate DataType="{x:Type local:Marker}">
<Ellipse Width="6" Height="6" Margin="-3" Fill="DarkViolet" Canvas.Left="{Binding Position.X}" Canvas.Top="{Binding Position.Y}"/>
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ItemsControl ItemsSource="{Binding}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas IsItemsHost="True" Background="Transparent" PreviewMouseMove="Canvas_PreviewMouseMove"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter"> <!--so that the markers are placed at their positions-->
<Setter Property="Canvas.Left" Value="{Binding Position.X}"/>
<Setter Property="Canvas.Top" Value="{Binding Position.Y}"/>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
<TextBlock x:Name="mousePos" Grid.Row="1"/>
</Grid>
</Window>
using System.Windows;
using System.Windows.Controls;
namespace WpfApp2
{
public class PositionContainer
{
public static readonly DependencyProperty PositionProperty =
DependencyProperty.RegisterAttached("Position", typeof(Point), typeof(PositionContainer),
new PropertyMetadata(default(Point), OnPositionChanged));
public static void SetPosition(FrameworkElement element, Point value)
{
element.SetValue(PositionProperty, value);
}
public static Point GetPosition(FrameworkElement element)
{
return (Point)element.GetValue(PositionProperty);
}
public static readonly DependencyProperty OffsetProperty =
DependencyProperty.RegisterAttached("Offset", typeof(Point), typeof(PositionContainer),
new PropertyMetadata(default(Point), OnOffsetChanged));
public static void SetOffset(FrameworkElement element, Point value)
{
element.SetValue(OffsetProperty, value);
}
public static Point GetOffset(FrameworkElement element)
{
return (Point)element.GetValue(OffsetProperty);
}
public static readonly DependencyProperty ContainerProperty =
DependencyProperty.RegisterAttached("Container", typeof(FrameworkElement), typeof(PositionContainer),
new PropertyMetadata(default(FrameworkElement), OnContainerChanged));
public static void SetContainer(FrameworkElement element, FrameworkElement value)
{
element.SetValue(ContainerProperty, value);
}
public static FrameworkElement GetContainer(FrameworkElement element)
{
return (FrameworkElement)element.GetValue(ContainerProperty);
}

public static readonly DependencyProperty SourceProperty =
DependencyProperty.RegisterAttached("Source", typeof(FrameworkElement), typeof(PositionContainer),
new PropertyMetadata(default(FrameworkElement)));
public static void SetSource(FrameworkElement element, FrameworkElement value)
{
element.SetValue(SourceProperty, value);
}
public static FrameworkElement GetSource(FrameworkElement element)
{
return (FrameworkElement)element.GetValue(SourceProperty);
}

static void OnPositionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Apply((FrameworkElement)d);
}
static void OnOffsetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Apply((FrameworkElement)d);
}
static void OnContainerChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var source = (FrameworkElement)d;
if (e.OldValue is FrameworkElement oldContainer)
{
oldContainer.SizeChanged -= OnSizeChanged;
SetSource(oldContainer, null);
}
if (e.NewValue is FrameworkElement newContainer)
{
newContainer.SizeChanged += OnSizeChanged;
SetSource(newContainer, source);
Apply(source);
}
}
static void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
Apply(GetSource((FrameworkElement)sender));
}
static void Apply(FrameworkElement element)
{
var container = GetContainer(element);
if (container == null)
return;
var offset = GetOffset(element);
if (container != element)
offset = element.TranslatePoint(offset, container);
var position = GetPosition(element);
position -= (Vector)offset;
container.SetValue(Canvas.LeftProperty, position.X);
container.SetValue(Canvas.TopProperty, position.Y);
}
}
}

最新更新