在Canvas上用离散值绘制图表的最佳方法是什么? [英] What's the best way to draw chart with discrete values on Canvas?
问题描述
我有一个具有动态宽度的画布.我需要在此Canvas上绘制图表.由于测量值的数量和业务领域的特殊性,无法使用第三方控件和组件.
I have a Canvas with dynamic width. I need to draw a chart on this Canvas. Due to the amount of the measured values and the business area specificity it's not possible to use third-party controls and components.
该图显示了一些离散测量的级别.X轴表示测量时间.在最坏的情况下,图表中的每个像素都可能具有不同的级别.所以看起来像这样:
The chart shows the level of some measurement that is discrete. The X axis indicates the time of the measurement. In the worst case every pixel of the chart can have different level. So it looks like this:
我要做的第一个方法就是为每个像素画一条线.所以我的代码看起来像这样:
My first approach just to make it working was to draw a line for every pixel. So my code looks like this:
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);
}
此代码可以执行我想要的操作.它绘制了这样一个图表:
This code does what I want it to do. It draws a chart like this one:
但是,这似乎并不是执行此操作的最佳方法.我的图表应支持平移和缩放,并且在每个用户请求上重绘图表大约需要200-350ms.太高了(1000/350 = 2.85fps).
But it seems that it is not an optimal way to do things like this one. My chart should support panning and zooming and it takes around 200-350ms to redraw a chart on every user request. That's too much (1000/350 = 2.85fps).
我在WPF中经验不足,所以我的问题是-绘制此类图表的最佳方法是什么?也许我需要使用路径"和几何"对象,但是我不能肯定地说,在实现之前,它会带来很大的性能提升.我也不知道我需要使用哪种几何.看来LineGeometry符合我的期望.
I don't have much experience in WPF so my question is - what is the most optimal way to draw such chart? Maybe I need to use Paths and Geometry objects but I can't say for sure that it will give much perfomance gain until I implement it. Also I don't know what kind of Geometry I need to use. It seems that LineGeometry matches my expectations.
谢谢.
推荐答案
我将回答这个问题,而这次没有数据绑定.鉴于需要高性能,最好的方法可能是使用自定义FrameworkElement.这是一个示例,该示例绘制10,000个样本并能够在我的笔记本电脑上保持60fps的速度,每个样本每帧更新一次:
I'll have another shot at answering this, without data binding this time. Given the need for high performance the best approach would probably be to use a custom FrameworkElement. Here's an example that draws 10,000 samples and is able to maintain 60fps on my laptop, with each sample being updated once per frame:
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]));
}
}
这里需要注意的几件事:
A few things to note here:
- 用于渲染线条的笔需要冻结,否则将导致性能大幅下降.
- 如果您的图表位于ScrollViewer中(例如),则需要剪切到屏幕的可见部分.在上面的示例中,我只是通过检查父级是否为ScrollViewer来破解它,您可能需要根据具体情况采取不同的操作. 如您所见,
- 我正在订阅
CompositionTarget.Rendering
来更新图表值并强制渲染.有时每帧它被称为多次,但是有一些解决方法,因此我将留给您查找.
- the pen you use to render the lines needs to be frozen, failing to do this will result in a huge performance decrease.
- if your chart is inside a ScrollViewer (say) you'll want to clip to the visible portion of the screen. In my example above I've just hacked it in by checking if the parent is a ScrollViewer, you may need to do this differently depending on your circumstances.
- as you can see, I'm subscribing to
CompositionTarget.Rendering
to update the chart values and force a render. this is known to sometimes be called more than once per frame, but there are workarounds for this so I'll leave it to you to find one.
无论如何,这是您如何使用此控件的示例(我在此处对宽度进行了硬编码,显然您希望将其绑定到视图模型中的属性或其他内容):
In any case, here's an example of how you would use this control (I've hard-coded the width here, obviously you'd want to binding it to a property in your view model or something):
<ScrollViewer x:Name="theScrollViewer" HorizontalScrollBarVisibility="Auto">
<controls:FastChart Width="10000" />
</ScrollViewer>
这是(动画)结果的静止图像:
And here's a still from the (animating) result:
唯一的问题是它不使用Canvas,但我认为为了获得所需的性能,您必须放弃该要求.
The only problem with this is that it doesn't use Canvas, but I think in order to get the performance you want you're going to have to ditch that requirement.
这篇关于在Canvas上用离散值绘制图表的最佳方法是什么?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!