在Canvas上用离散值绘制图表的最佳方法是什么?

时间:2018-12-27 09:38:51

标签: c# wpf performance canvas charts

我有一个具有动态宽度的画布。我需要在此画布上绘制图表。由于测量值的数量和业务领域的特殊性,无法使用第三方控件和组件。

该图显示了一些离散测量的级别。 X轴表示测量时间。在最坏的情况下,图表的每个像素都可能具有不同的级别。所以看起来像这样: Chart

我要做的第一个方法就是为每个像素画一条线。所以我的代码看起来像这样:

        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);
        }

此代码可以执行我想要的操作。它绘制了这样一个图表: Chart

但是,这似乎并不是执行此类操作的最佳方法。我的图表应支持平移和缩放,并且在每个用户请求上重绘图表大约需要200-350ms。太高了(1000/350 = 2.85fps)。

我在WPF中经验不足,所以我的问题是-绘制此类图表的最佳方法是什么?也许我需要使用路径和几何对象,但是我不能肯定地说,在实现之前,它会带来很多性能上的提升。我也不知道我需要使用哪种几何。看来LineGeometry符合我的期望。

谢谢。

2 个答案:

答案 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]));
    }

}

这里需要注意的几件事:

  1. 用于渲染线条的笔需要冻结,否则将导致性能大幅下降。
  2. 如果您的图表位于ScrollViewer中(例如),则需要剪切到屏幕的可见部分。在上面的示例中,我只是通过检查父级是否为ScrollViewer来破解它,您可能需要根据具体情况采取不同的操作。
  3. 如您所见,我正在订阅CompositionTarget.Rendering来更新图表值并强制渲染。有时每帧它被称为多次,但是有一些解决方法,因此我将留给您查找。

无论如何,这是您如何使用此控件的示例(我已经在此处对宽度进行了硬编码,显然您希望将其绑定到视图模型中的属性或其他内容):

<ScrollViewer x:Name="theScrollViewer" HorizontalScrollBarVisibility="Auto">
    <controls:FastChart Width="10000" />
</ScrollViewer>

这是(动画)结果的静止图像:

enter image description here

唯一的问题是它不使用Canvas,但我认为为了获得所需的性能,您必须放弃该要求。

答案 1 :(得分:0)

非常容易做到,但是我强烈建议您使用数据绑定。

  1. 创建一个ItemsControl。
  2. 给ItemsControl一个Canvas类型的ItemsPanel。
  3. 将“画布宽度和高度”绑定到视图模型中的元素,以指定每个轴的范围。
  4. 将整个内容包装在Viewbox中,以使其自动缩放,而与宽度和高度无关。
  5. 为ItemsControl列表中的每个元素创建一个包含X1 / Y1 / X2 / Y2的视图模型,然后使用DataTemplate使用这些值绘制垂直线来渲染这些项目。

因此,基本上,您的主视图模型应如下所示(通过演示的方式生成随机数据点):

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>

这将依次产生以下输出:

enter image description here