在学习WPF时,我正在努力制作可单击和可拖动的样条曲线。我已经能够成功地处理纯线段,但是要跳到折线却很难。我有一个用于内插WinForms中曾经使用过的样条曲线的类,因此我使用了鼠标的一些输入单击,这些将成为单击和拖动的拇指。插值点具有足够高的分辨率,因此WPF折线应该适合显示。为了澄清,我需要更高分辨率的输出,因此使用WPF Beizer不能正常工作。
我的轮廓设置非常好-但是我遇到的一个特殊问题是,拖动拇指不会或者a)双向绑定设置不正确,或者b)ObservableCollection无法生成通知。我意识到ObservableCollection仅在添加/删除/清除项目等时通知,而不是各个索引都能够产生通知。我花了最后几个小时进行搜索-找到了一些有前途的想法,但未能正确地将它们连接起来。发布了一些代码,以尝试从ObservableCollection继承并重写ObservableCollection中的OnPropertyChanged方法,但这是受保护的虚拟方法。虽然其他人使用OC中的方法调用将PropertyChanged事件处理程序附加到每个对象,但是我不确定在哪里注入该逻辑。所以我有点卡住了。
MainWindow.xaml: mainCanvas中托管有一个ItemsControl。 ItemsControl绑定到ViewModel上的属性
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Menu>
<MenuItem x:Name="menuAddNewPolyline" Header="Add Polyline" Click="MenuItem_Click" />
</Menu>
<Canvas x:Name="mainCanvas" Grid.Row="1">
<ItemsControl x:Name="polylinesItemsControl"
ItemsSource="{Binding polylines}"
>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Canvas />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Canvas>
</Grid>
MainWindow.Xaml.cs: 非常简单-初始化一个新的视图模型,并将其设置为DataContext。有一个带有“添加折线”项的菜单,该菜单依次初始化一个新的PolylineControl,并在窗口的ActualHeight和ActualWidth内生成三个随机点(使用Thread.Sleep,否则它们在调用之间是相同的)。新的PolylineControl被添加到一个ObservableCollection的ViewModel中,这是我可以接受鼠标输入之前的立场。
public partial class MainWindow : Window
{
private ViewModel viewModel;
public MainWindow()
{
InitializeComponent();
viewModel = new ViewModel();
DataContext = viewModel;
}
private Point GetRandomPoint()
{
Random r = new Random();
return new Point(r.Next(0, (int)this.ActualWidth), r.Next(0, (int)this.ActualHeight));
}
private void MenuItem_Click(object sender, RoutedEventArgs e)
{
var newPolyline = new PolylineControl.Polyline();
newPolyline.PolylinePoints.Add(GetRandomPoint());
Thread.Sleep(100);
newPolyline.PolylinePoints.Add(GetRandomPoint());
Thread.Sleep(100);
newPolyline.PolylinePoints.Add(GetRandomPoint());
viewModel.polylines.Add(newPolyline);
}
}
ViewModel.cs: 绝对注意到这里
public class ViewModel
{
public ObservableCollection<PolylineControl.Polyline> polylines { get; set; }
public ViewModel()
{
polylines = new ObservableCollection<PolylineControl.Polyline>();
}
}
** PolylineControl:
Polyline.cs:** 包含折线的ObservableCollection的DP。最终,它还将包含插值点和输入点,但是该演示将使用一个点集合。我确实尝试使用INotifyPropertyChanged接口无济于事。
public class Polyline : Control
{
public static readonly DependencyProperty PolylinePointsProperty =
DependencyProperty.Register("PolylinePoints", typeof(ObservableCollection<Point>), typeof(Polyline),
new FrameworkPropertyMetadata(new ObservableCollection<Point>()));
public ObservableCollection<Point> PolylinePoints
{
get { return (ObservableCollection<Point>)GetValue(PolylinePointsProperty); }
set { SetValue(PolylinePointsProperty, value); }
}
static Polyline()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(Polyline), new FrameworkPropertyMetadata(typeof(Polyline)));
}
}
Generic.xaml 包含具有数据绑定折线的画布,以及包含用于ThumbPoint控件的DataTemplate的ItemsControl。
<local:PointCollectionConverter x:Key="PointsConverter"/>
<Style TargetType="{x:Type local:Polyline}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:Polyline}">
<Canvas Background="Transparent">
<Polyline x:Name="PART_Polyline"
Stroke="Black"
StrokeThickness="2"
Points="{Binding Path=PolylinePoints,
RelativeSource={RelativeSource TemplatedParent},
Converter={StaticResource PointsConverter}}"
>
</Polyline>
<ItemsControl x:Name="thumbPoints"
ItemsSource="{Binding PolylinePoints, RelativeSource={RelativeSource TemplatedParent}}"
>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Canvas>
<tc:ThumbPoint Point="{Binding Path=., Mode=TwoWay}"/>
</Canvas>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Canvas>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
PointsCollectionConverter.cs: 包含一个IValueConverter,用于将ObservableCollection转换为PointsCollection。
public class PointCollectionConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value.GetType() == typeof(ObservableCollection<Point>) && targetType == typeof(PointCollection))
{
var pointCollection = new PointCollection();
foreach (var point in value as ObservableCollection<Point>)
{
pointCollection.Add(point);
}
return pointCollection;
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return null;
}
}
最后, ThumbPointControl:
ThumbPoint.cs: 包含一个用于点中心的DP,以及DragDelta功能。
public class ThumbPoint : Thumb
{
public static readonly DependencyProperty PointProperty =
DependencyProperty.Register("Point", typeof(Point), typeof(ThumbPoint),
new FrameworkPropertyMetadata(new Point()));
public Point Point
{
get { return (Point)GetValue(PointProperty); }
set { SetValue(PointProperty, value); }
}
static ThumbPoint()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(ThumbPoint), new FrameworkPropertyMetadata(typeof(ThumbPoint)));
}
public ThumbPoint()
{
this.DragDelta += new DragDeltaEventHandler(this.OnDragDelta);
}
private void OnDragDelta(object sender, DragDeltaEventArgs e)
{
this.Point = new Point(this.Point.X + e.HorizontalChange, this.Point.Y + e.VerticalChange);
}
}
Generic.xaml: 包含样式和数据绑定的Ellipse边界。
<Style TargetType="{x:Type local:ThumbPoint}">
<Setter Property="Width" Value="8"/>
<Setter Property="Height" Value="8"/>
<Setter Property="Margin" Value="-4"/>
<Setter Property="Background" Value="Gray" />
<Setter Property="Canvas.Left" Value="{Binding Path=Point.X, RelativeSource={RelativeSource Self}}" />
<Setter Property="Canvas.Top" Value="{Binding Path=Point.Y, RelativeSource={RelativeSource Self}}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:ThumbPoint}">
<Ellipse x:Name="PART_Ellipse"
Fill="{TemplateBinding Background}"
Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}"
/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Window after the Add Polyline menu item is pressed
该代码用于添加具有三个随机点的折线。
Thumbs moved away from poly line
但是,一旦移动了拇指,折线将不会随之更新。
我有一个工作示例,该示例仅包含一个线段(单击添加线段按钮时,它会多次添加到视图模型中),因此看起来逻辑上应该都是正确的,但是由于引入了ObservableCollection承载折线所需的多个点。
感谢您的帮助
答案 0 :(得分:0)
根据Clemens的建议,我能够使其正常工作。
我重命名了Polyline.cs控件,以消除与标准WPF折线形状类对DynamicPolyline的混淆。该类现在实现INotifyPropertyChanged,并为PolylinePoints提供了DP,为NotifyingPoint类提供了单独的ObservableCollection,该类也实现了INotifyPropertyChanged。初始化DynamicPolyline时,它将在ObserableCollection上挂接CollectionChanged事件。然后,事件处理程序方法要么将事件处理程序添加到集合中的每个项目,要么根据操作将其删除。每个项目的事件处理程序只需调用SetPolyline,它依次循环遍历InputPoints,将它们添加到新的PointCollection,然后在PART_Polyline上设置Points属性(在OnApplyTemplate方法中创建对它的引用)。
原来,折线上的Points属性不会侦听INotifyPropertyChanged接口,因此无法在Xaml中进行数据绑定。将来可能会最终使用PathGeometery,但是目前可以使用。
要解决Marks非MVVM问题。.这是一个演示应用程序,很抱歉,我有一些代码可以测试后面代码中的内容。关键是能够重用这些控件,并将它们与其他控件组合在一起以用于各种用例,因此与重复代码相比,让它们自己拥有更为有意义。
DynmicPolyline.cs:
public class DynamicPolyline : Control, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string caller = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(caller));
}
public static readonly DependencyProperty PolylinePointsProperty =
DependencyProperty.Register("PoilylinePoints", typeof(PointCollection), typeof(DynamicPolyline),
new PropertyMetadata(new PointCollection()));
public PointCollection PolylinePoints
{
get { return (PointCollection)GetValue(PolylinePointsProperty); }
set { SetValue(PolylinePointsProperty, value); }
}
private ObservableCollection<NotifyingPoint> _inputPoints;
public ObservableCollection<NotifyingPoint> InputPoints
{
get { return _inputPoints; }
set
{
_inputPoints = value;
OnPropertyChanged();
}
}
private void SetPolyline()
{
if (polyLine != null && InputPoints.Count >= 2)
{
var newCollection = new PointCollection();
foreach (var point in InputPoints)
{
newCollection.Add(new Point(point.X, point.Y));
}
polyLine.Points = newCollection;
}
}
private void InputPoints_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
foreach (var item in e.NewItems)
{
var point = item as NotifyingPoint;
point.PropertyChanged += InputPoints_PropertyChange;
}
}
else if (e.Action == NotifyCollectionChangedAction.Remove)
{
foreach (var item in e.OldItems)
{
var point = item as NotifyingPoint;
point.PropertyChanged -= InputPoints_PropertyChange;
}
}
}
private void InputPoints_PropertyChange(object sender, PropertyChangedEventArgs e)
{
SetPolyline();
}
public DynamicPolyline()
{
InputPoints = new ObservableCollection<NotifyingPoint>();
InputPoints.CollectionChanged += InputPoints_CollectionChanged;
SetPolyline();
}
static DynamicPolyline()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(DynamicPolyline), new FrameworkPropertyMetadata(typeof(DynamicPolyline)));
}
private Polyline polyLine;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
polyLine = this.Template.FindName("PART_Polyline", this) as Polyline;
}
NotifyingPoint.cs 从数据绑定的ThumbPoint更新X或Y时,引发属性更改事件的简单类。
public class NotifyingPoint : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string caller = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(caller));
}
public event EventHandler ValueChanged;
private double _x = 0.0;
public double X
{
get { return _x; }
set
{
_x = value;
OnPropertyChanged();
ValueChanged?.Invoke(this, null);
}
}
private double _y = 0.0;
public double Y
{
get { return _y; }
set
{
_y = value;
OnPropertyChanged();
}
}
public NotifyingPoint()
{
}
public NotifyingPoint(double x, double y)
{
X = x;
Y = y;
}
public Point ToPoint()
{
return new Point(_x, _y);
}
}
最后,为了完整起见,这是控件的 Generic.xaml 。唯一的变化是NotifyingPoint的X和Y的绑定。
<Style TargetType="{x:Type local:DynamicPolyline}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:DynamicPolyline}">
<Canvas x:Name="PART_Canvas">
<Polyline x:Name="PART_Polyline"
Stroke="Black"
StrokeThickness="2"
/>
<ItemsControl x:Name="PART_ThumbPointItemsControl"
ItemsSource="{Binding Path=InputPoints, RelativeSource={RelativeSource TemplatedParent}}"
>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Canvas>
<tc:ThumbPoint X="{Binding Path=X, Mode=TwoWay}" Y="{Binding Path=Y, Mode=TwoWay}"/>
</Canvas>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Canvas>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
我将Spline类放到SetPolyline方法中,并得到以下结果: Two working click and drag able spline curves