我想对给定点进行函数图的交互式绘制。对于绘图,我使用 Canvas 和 Path,对于点操作,我使用 DataGrid 和 ObservableCollection。问题是,当我更改点时,画布不会重新绘制。
用户界面:
<Window x:Class="WpfAppCharts.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:WpfAppCharts"
xmlns:viewmodels="clr-namespace:WpfAppCharts.ViewModels"
xmlns:converters="clr-namespace:WpfAppCharts.Converters"
mc:Ignorable="d"
ResizeMode="NoResize"
WindowStartupLocation="CenterScreen"
Title="WPFCharts" Height="450" Width="800">
<Window.DataContext>
<viewmodels:MainWindowViewModel/>
</Window.DataContext>
<Window.Resources>
<Style TargetType="{x:Type DataGridCell}">
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="LightBlue"></Setter>
<Setter Property="Foreground" Value="Black"></Setter>
</Trigger>
</Style.Triggers>
</Style>
<converters:DataToPathConverter x:Key="DataToPathConverter"/>
</Window.Resources>
<Grid x:Name="MainGrid">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<Grid x:Name="ControlPanelGrid" Grid.Column="0">
<Grid.RowDefinitions>
<RowDefinition Height="2*"/>
<RowDefinition Height="2*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<DataGrid x:Name="DatasDataGrid" Grid.Row="0"
ItemsSource="{Binding Path=Datas}"
AutoGenerateColumns="False"
CanUserAddRows="False"
CanUserDeleteRows="False"
CanUserResizeColumns="False"
CanUserSortColumns="False"
IsReadOnly="True"
Margin="5, 5, 5, 5"
SelectedIndex="0">
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Width="*"
Binding="{Binding Path=Name, Mode=OneWay}"/>
<DataGridTemplateColumn Header="Type" Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox SelectedIndex="{Binding Path=Type,
UpdateSourceTrigger=PropertyChanged,
Mode=TwoWay}">
<ComboBoxItem Content="Lines"/>
<ComboBoxItem Content="Spline"/>
</ComboBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<DataGrid x:Name="RecordsDataGrid" Grid.Row="1"
ItemsSource="{Binding ElementName=DatasDataGrid,
Path=SelectedItem.Records,
Mode=TwoWay}"
AutoGenerateColumns="False"
CanUserAddRows="False"
CanUserSortColumns="False"
CanUserDeleteRows="False"
CanUserResizeColumns="False"
Margin="5, 5, 5, 5">
<DataGrid.Columns>
<DataGridTextColumn Header="X" Width="*"
Binding="{Binding Path=X,
Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}" />
<DataGridTextColumn Header="Y" Width="*"
Binding="{Binding Path=Y,
Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}" />
</DataGrid.Columns>
</DataGrid>
</Grid>
<ItemsControl x:Name="ItemsControl" Grid.Column="1" Margin="5, 5, 5, 5" ItemsSource="{Binding Path=Datas}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas x:Name="ChartCanvas"
Background="Aquamarine"
ClipToBounds="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Path Data="{Binding Mode=OneWay,
Converter={StaticResource DataToPathConverter}}"
Stroke="Black"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
数据类:
using System.Collections.ObjectModel;
namespace WpfAppCharts.Models
{
public class Data
{
enum Types { LINES, SPLINE }
public string Name { get; set; }
public int Type { get; set; }
public ObservableCollection<Record> Records { get; set; }
public Data()
{
this.Name = "No name";
this.Type = (int)Types.SPLINE;
this.Records = new ObservableCollection<Record>();
}
}
}
录制课程:
namespace WpfAppCharts.Models
{
public class Record
{
public double X { get; set; }
public double Y { get; set; }
public Record(double X, double Y)
{
this.X = X;
this.Y = Y;
}
}
}
视图模型:
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using WpfAppCharts.Models;
namespace WpfAppCharts.ViewModels
{
public class MainWindowViewModel : INotifyPropertyChanged
{
public ObservableCollection<Data> Datas { get; set; }
public MainWindowViewModel()
{
Datas = new ObservableCollection<Data>();
Datas.Add(new Data());
Datas[0].Records.Add(new Record(0, 0));
Datas[0].Records.Add(new Record(20, 0));
Datas[0].Records.Add(new Record(45, -47));
Datas[0].Records.Add(new Record(53, 335));
Datas[0].Records.Add(new Record(57, 26));
Datas[0].Records.Add(new Record(62, 387));
Datas[0].Records.Add(new Record(74, 104));
Datas[0].Records.Add(new Record(89, 0));
Datas[0].Records.Add(new Record(95, 100));
Datas[0].Records.Add(new Record(100, 0));
Datas[0].Records.Add(new Record(115, 100));
Datas[0].Records.Add(new Record(120, 200));
Datas[0].Records.Add(new Record(130, 300));
Datas[0].Records.Add(new Record(135, 300));
Datas[0].Records.Add(new Record(140, 200));
Datas[0].Records.Add(new Record(145, 50));
Datas[0].Records.Add(new Record(150, 50));
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
转换器:
using System.Globalization;
using System.Windows.Data;
using WpfAppCharts.Models;
namespace WpfAppCharts.Converters
{
class DataToPathConverter : IValueConverter
{
enum Types { LINES, SPLINE }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
Data data = (Data)value;
string result = String.Format("M {0},{1}", data.Records[0].X, data.Records[0].Y);
switch ((int)data.Type)
{
case (int)Types.SPLINE:
case (int)Types.LINES:
for (int i = 1; i < data.Records.Count; i++)
result += String.Format("L {0},{1}", data.Records[i].X,
data.Records[i].Y);
break;
}
return result;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
当向
Path
项的 Records
集合中添加或删除记录时,没有会更新 ItemTemplate 中的 Data
元素的更改通知。为了实现这样的更新机制,您必须绑定到 Records
属性,并且只有当整个 Records
集合发生更改时才会通知此类绑定。
此修改要求
Data
类实现 INotifyPropertyChanged
,视图模型现在如下所示:
public class Record
{
public double X { get; }
public double Y { get; }
public Record(double x, double y)
{
X = x;
Y = y;
}
}
public class Data : INotifyPropertyChanged
{
public enum Types { LINES, SPLINE }
private string name = "No name";
public string Name
{
get => name;
set
{
name = value;
OnPropertyChanged(nameof(Name));
}
}
private Types type = Types.LINES;
public Types Type
{
get => type;
set
{
type = value;
OnPropertyChanged(nameof(Type));
}
}
private IEnumerable<Record> records;
public IEnumerable<Record> Records
{
get => records;
set
{
records = value;
OnPropertyChanged(nameof(Records));
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class MainWindowViewModel
{
public ObservableCollection<Data> Datas { get; } = new ObservableCollection<Data>();
public MainWindowViewModel()
{
Datas.Add(new Data
{
Records = new List<Record>()
{
new Record(0, 0),
new Record(20, 0),
new Record(45, -47),
new Record(53, 335),
new Record(57, 26),
new Record(62, 387),
new Record(74, 104),
new Record(89, 0),
new Record(95, 100),
new Record(100, 0),
new Record(115, 100),
new Record(120, 200),
new Record(130, 300),
new Record(135, 300),
new Record(140, 200),
new Record(145, 50),
new Record(150, 50),
}
});
}
}
由于您无法直接绑定到数据对象,并且您还想评估
Type
属性,因此您需要一个带有多绑定转换器的 MultiBinding
,如下所示。请注意,它不会返回 string
,而是返回 Geometry
- 这是 Path.Data
属性的类型。
public class DataToPathConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length != 2 ||
!(values[0] is Data.Types type) ||
!(values[1] is IEnumerable<Record> records) ||
records.Count() == 0)
{
return null;
}
var points = records.Select(r => new Point(r.X, r.Y));
var figure = new PathFigure { StartPoint = points.First() };
switch (type)
{
case Data.Types.LINES:
figure.Segments.Add(new PolyLineSegment(points.Skip(1), true));
break;
case Data.Types.SPLINE:
// figure.Segments.Add(new PolyLineSegment(points.Skip(1), true));
break;
}
var geometry = new PathGeometry();
geometry.Figures.Add(figure);
return geometry;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
ItemsControl
中的绑定现在看起来像这样:
<ItemsControl ItemsSource="{Binding Path=Datas}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Background="Aquamarine"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Path Stroke="Black" StrokeThickness="1">
<Path.Data>
<MultiBinding Converter="{StaticResource DataToPathConverter}">
<Binding Path="Type"/>
<Binding Path="Records"/>
</MultiBinding>
</Path.Data>
</Path>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>