我有一个具有动态宽度的画布。我需要在此画布上绘制图表。由于测量值的数量和业务领域的特殊性,无法使用第三方控件和组件。
该图显示了一些离散测量的级别。 X轴表示测量时间。在最坏的情况下,图表的每个像素都可能具有不同的级别。所以看起来像这样:
我要做的第一个方法就是为每个像素画一条线。所以我的代码看起来像这样:
MyCanvas.Children.Clear();
var random = new Random();
for (var i = 0; i < MyCanvas.Width; i++)
{
var line = new Line()
{
X1 = i,
X2 = i,
Y1 = MyCanvas.ActualHeight,
Y2 = MyCanvas.ActualHeight - random.Next(0, (int)MyCanvas.ActualHeight),
Stroke = Brushes.Blue
};
MyCanvas.Children.Add(line);
}
但是,这似乎并不是执行此类操作的最佳方法。我的图表应支持平移和缩放,并且在每个用户请求上重绘图表大约需要200-350ms。太高了(1000/350 = 2.85fps)。
我在WPF中经验不足,所以我的问题是-绘制此类图表的最佳方法是什么?也许我需要使用路径和几何对象,但是我不能肯定地说,在实现之前,它会带来很多性能上的提升。我也不知道我需要使用哪种几何。看来LineGeometry符合我的期望。
谢谢。
答案 0 :(得分:1)
我将回答这个问题,而这次没有数据绑定。鉴于需要高性能,最好的方法可能是使用自定义FrameworkElement。这是一个示例,该示例绘制了10,000个样本,并且能够在我的笔记本电脑上保持60fps的速度,每个样本每帧更新一次:
public class FastChart : FrameworkElement
{
private Pen ChartPen;
private const int MaxSampleVal = 1000;
private const int NumSamples = 10000;
private int[] Samples = new int[NumSamples];
public FastChart()
{
this.ChartPen = new Pen(Brushes.Blue, 1);
if (this.ChartPen.CanFreeze)
this.ChartPen.Freeze();
ClipToBounds = true;
this.SnapsToDevicePixels = true;
CompositionTarget.Rendering += CompositionTarget_Rendering;
var rng = new Random();
for (int i = 0; i < NumSamples; i++)
this.Samples[i] = MaxSampleVal / 2;
}
private void CompositionTarget_Rendering(object sender, EventArgs e)
{
// update all samples
var rng = new Random();
for (int i = 0; i < NumSamples; i++)
this.Samples[i] = Math.Max(0, Math.Min(MaxSampleVal, this.Samples[i] + rng.Next(11) - 5));
// force an update
InvalidateVisual();
}
protected override void OnRender(DrawingContext drawingContext)
{
// hacky clip to parent scroller
var minx = 0;
var maxx = NumSamples;
var scroller = this.Parent as ScrollViewer;
if (scroller != null)
{
minx = Math.Min((int)scroller.HorizontalOffset, NumSamples - 1);
maxx = Math.Min(NumSamples, minx + (int)scroller.ViewportWidth);
}
for (int x = minx; x < maxx; x++)
drawingContext.DrawLine(this.ChartPen, new Point(0.5 + x, 1000), new Point(0.5 + x, 1000 - this.Samples[x]));
}
}
这里需要注意的几件事:
CompositionTarget.Rendering
来更新图表值并强制渲染。有时每帧它被称为多次,但是有一些解决方法,因此我将留给您查找。无论如何,这是您如何使用此控件的示例(我已经在此处对宽度进行了硬编码,显然您希望将其绑定到视图模型中的属性或其他内容):
<ScrollViewer x:Name="theScrollViewer" HorizontalScrollBarVisibility="Auto">
<controls:FastChart Width="10000" />
</ScrollViewer>
这是(动画)结果的静止图像:
唯一的问题是它不使用Canvas,但我认为为了获得所需的性能,您必须放弃该要求。
答案 1 :(得分:0)
非常容易做到,但是我强烈建议您使用数据绑定。
因此,基本上,您的主视图模型应如下所示(通过演示的方式生成随机数据点):
public class MainViewModel : ObservableObject
{
private IEnumerable<DataPointViewModel> _DataPoints;
public IEnumerable<DataPointViewModel> DataPoints
{
get { return this._DataPoints; }
set { Set(() => this.DataPoints, ref this._DataPoints, value); }
}
public int ChartWidth { get; } = 1000;
public int ChartHeight { get; } = 1000;
public MainViewModel()
{
var rng = new Random();
this.DataPoints = Enumerable.Range(0, this.ChartWidth)
.Select(x => new DataPointViewModel {X1 = x, Y1 = this.ChartHeight-1, X2 = x, Y2 = this.ChartHeight - 1 - rng.Next(this.ChartHeight) });
}
}
集合中项目的视图模型需要提供用于渲染它们的线的点:
public class DataPointViewModel : ObservableObject
{
private double _X1;
public double X1
{
get { return this._X1; }
set { Set(() => this.X1, ref this._X1, value); }
}
private double _Y1;
public double Y1
{
get { return this._Y1; }
set { Set(() => this.Y1, ref this._Y1, value); }
}
private double _X2;
public double X2
{
get { return this._X2; }
set { Set(() => this.X2, ref this._X2, value); }
}
private double _Y2;
public double Y2
{
get { return this._Y2; }
set { Set(() => this.Y2, ref this._Y2, value); }
}
}
然后您的XAML仅使用这些为Canvas控件上的每个数据点绘制一行:
<Viewbox>
<ItemsControl ItemsSource="{Binding DataPoints}" Width="{Binding ChartWidth}" Height="{Binding ChartHeight}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Line Stroke="Blue" StrokeThickness="1" X1="{Binding X1}" Y1="{Binding Y1}" X2="{Binding X2}" Y2="{Binding Y2}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Viewbox>
这将依次产生以下输出: