WPF 使用DrawingVisual绘制高性能曲线图

本文介绍了如何利用WPF的DrawingVisual实现高性能的大数据曲线图绘制,通过局部渲染解决大数据量时的性能问题。作者创建了一个自定义控件,实现了数据更新和滚动条联动,确保即使数据量达到百万级别也能流畅显示。
摘要由CSDN通过智能技术生成

一、前言

  • 项目中涉及到了心率监测,而且数据量达到了百万级别,通过WPF实现大数据曲线图时,尝试过最基础的Canvas来实现,但是性能堪忧,而且全部画出来也不实际。同时也尝试过找第三方的开源库,但是因为曲线图涉及到很多细节功能,第三方的肯定也没法满足。没办法,只能自己实现,上网查找后发现DrawingVisual这个玩意可以实现高性能画图,同时再搭配局部显示,这样就能实现自己想要的效果。话不多说,今天把大致的实现思路写一下,就不直接把项目的源码贴出来,写个简单的Demo就好了。

二、正文

1、首先新建个项目,然后创建个自定义控件,命名CurveChartDrawingVisual,然后让它继承FrameworkElement。因为要使用DrawingVisual对象的话,需要为它创建一个主机容器。关于其他相关DrawingVisual的细节这里不做过多阐述,不明白的可以去微软官网看。

2、实现的具体代码如下,相关细节有备注标注了。这里记得要重写一下VisualChildrenCount属性和重写 GetVisualChild() 方法,不然图画不出来

public class CruveChartDrawingVisual : FrameworkElement
{
    private List<Visual> visuals = new List<Visual>();
    private DrawingVisual Layer;

    private double offset_x = 0;//滑动条偏移值
    private double y_scale;//y轴方向缩放比例

    private List<int> list_points;//曲线数据

    private static int Top_Val_Max = 100;//y轴最大值
    private static int Top_Val_Min = 0;//y轴最小值
    private static int X_Sex = 20;//x轴分度值
    private static int Y_Sex = 20;//y轴分度值
    private static int Bottom = 30;//底部x轴坐标显示高度

    Pen pen = new Pen(Brushes.Green, 2);
    Pen primarygrid_pen = new Pen(Brushes.Black, 1);
    Pen secondgrid_pen = new Pen(Brushes.Gray, 1);

    public CruveChartDrawingVisual()
    {
        pen.Freeze();//冻结笔,提高性能关键所在
        primarygrid_pen.Freeze();
        secondgrid_pen.Freeze();

        Layer = new DrawingVisual();

        visuals.Add(Layer);
    }

    public void SetupData(List<int> points)
    {
        list_points = points;
        offset_x = 0;
        DrawContent();
    }

    public void OffsetX(double offset)
    {
        offset_x = offset;
        DrawContent();
        InvalidateVisual();
    }

    private void DrawContent()
    {
        var dc = Layer.RenderOpen();
        y_scale = (RenderSize.Height - Bottom) / (Top_Val_Max - Top_Val_Min);

        var mat = new Matrix();
        mat.ScaleAt(1, -1, 0, RenderSize.Height / 2);

        mat.OffsetX = -offset_x;
        dc.PushTransform(new MatrixTransform(mat));

        //横线
        for (int y = 0; y <= Top_Val_Max - Top_Val_Min; y += 10)
        {
            Point point1 = new Point(offset_x, y * y_scale + Bottom);
            Point point2 = new Point(offset_x + RenderSize.Width, y * y_scale + Bottom);
            if (y % Y_Sex == 0)
            {
                dc.DrawLine(primarygrid_pen, point1, point2);
                continue;
            }
            dc.DrawLine(secondgrid_pen, point1, point2);
        }

        //竖线与文字
        for (int i = 0; i <= (offset_x + RenderSize.Width); i += X_Sex * 2)
        {
            if (i < offset_x)
            {
                continue;
            }
            var point1 = new Point(i, Bottom);
            var point2 = new Point(i, (Top_Val_Max - Top_Val_Min) * y_scale + Bottom);


            //y轴文字
            if (i % 100 == 0)
            {
                var text1 = new FormattedText(i + "", CultureInfo.CurrentCulture, FlowDirection.LeftToRight, new Typeface("Verdana"), 16, Brushes.Black);
                var mat3 = new Matrix();
                mat3.ScaleAt(1, -1, i - text1.Width / 2, 8 + text1.Height / 2);
                dc.PushTransform(new MatrixTransform(mat3));
                dc.DrawText(text1, new Point(i - text1.Width / 2, 8));
                dc.Pop();
            }

            //表格刻度文字
            if (i % 100 == 0)
            {
                for (int y = Top_Val_Min; y <= Top_Val_Max; y += 10)
                {
                    if (y % Y_Sex == 0)
                    {
                        var text1 = new FormattedText(y + "", CultureInfo.CurrentCulture, FlowDirection.LeftToRight, new Typeface("Verdana"), 12, Brushes.Black);
                        var mat3 = new Matrix();
                        mat3.ScaleAt(1, -1, i + 1, (y - Top_Val_Min) * y_scale + Bottom + text1.Height / 2);
                        dc.PushTransform(new MatrixTransform(mat3));
                        dc.DrawText(text1, new Point(i + 1, (y - Top_Val_Min) * y_scale + Bottom));
                        dc.Pop();
                    }
                }
                //深色竖线
                dc.DrawLine(primarygrid_pen, point1, point2);
                continue;
            }
            //浅色竖线
            dc.DrawLine(secondgrid_pen, point1, point2);
        }

        if (list_points != null)
        {
            for (int i = (int)offset_x; i < list_points.Count - 1; i++)
            {
                if (i > offset_x + RenderSize.Width)
                {
                    break;
                }
                dc.DrawLine(pen, new Point(i, list_points[i] * y_scale + Bottom), new Point(i + 1, list_points[i + 1] * y_scale + Bottom));
            }
        }

        dc.Pop();
        dc.Close();
    }

    protected override int VisualChildrenCount => visuals.Count;
    protected override Visual GetVisualChild(int index)
    {
        return visuals[index];
    }

    protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
    {
        DrawContent();
        base.OnRenderSizeChanged(sizeInfo);
    }

    protected override void OnRender(DrawingContext drawingContext)
    {
        drawingContext.DrawRectangle(Brushes.White, null, new Rect(0, 0, RenderSize.Width, RenderSize.Height));
        base.OnRender(drawingContext);
    }
}

3、接着测试一下,打开MainWindow,添加我们的自定义控件,这里局部显示需要搭配一个ScrollViewer来实现,记得这里没有将我们的自定义控件嵌入ScrollViewer,而是放一个Canvas来填充,代码如下

<Grid>
    <local:CruveChartDrawingVisual x:Name="curve" Margin="0,15,0,20" />
    <ScrollViewer
        Name="scroll"
        HorizontalScrollBarVisibility="Auto"
        ScrollChanged="ScrollViewer_ScrollChanged"
        VerticalScrollBarVisibility="Disabled">
        <Canvas x:Name="canvas" Height="1" />
    </ScrollViewer>
</Grid>

4、接着就是后台代码,比较简单,就是自动生成一个List,然后传给自定义控件,Canvas的宽度直接设置为List的长度,因为这里是水平方向一个像素点画一个点,然后滑动条滑动时再将对应的偏移值传递到控件,再通过偏移值更新视图

public partial class MainWindow : Window
{
    private bool isAdd = true;

    public MainWindow()
    {
        InitializeComponent();
    }

    private void Window_Loaded(object sender, RoutedEventArgs e)
    {
        List<int> lists = new List<int>();
        int temp = 20;
        for (int i = 0; i < 60 * 60; i++)
        {
            if (isAdd)
            {
                lists.Add(temp);
                temp ++;
            }
            else
            {
                lists.Add(temp);
                temp --;
            }

            if (temp == 90) isAdd = false;
            if (temp == 10) isAdd = true;
        }

        canvas.Width = lists.Count;

        curve.SetupData(lists);
    }

    private void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
    {
        curve.OffsetX(scroll.HorizontalOffset);
    }
}

5、运行效果如下,滑动条拖到哪里就显示哪里,这样就算数据量再大也没问题,这种曲线图跟常规的曲线图有点差别,这里更多的是提供一种思路。

567ad062bfb54a2ede0db6b8ea9c7ce5.gif

[1]

参考资料

[1]

链接: https://www.cnblogs.com/cong2312/p/15921146.html

WPF(Windows Presentation Foundation)是一种用于创建Windows应用程序的框架,可用于创建各种各样的图形用户界面(GUI),包括实时曲线图。 要实现实时曲线图中x轴的自动滚动,可以采取以下步骤: 1. 创建一个WPF应用程序并设置其主窗口。 2. 在XAML中定义一个Canvas元素,它将用于绘制实时曲线图。在Canvas上,您可以添加其他需要的元素,例如坐标轴等。 3. 在代码中,创建一个定时器(例如使用System.Timers.Timer类),以固定的时间间隔触发更新曲线图的操作。 4. 在定时器的Tick事件处理程序中,更新实时曲线图的数据,并重新绘制曲线图。 5. 在绘制曲线图时,可以通过调整x轴的坐标值来实现自动滚动的效果。例如,可以记录最后一个数据点的x轴坐标值,并将所有数据点的x轴坐标值减去该值,以实现滚动效果。 6. 然后,根据新的x轴坐标值重新绘制曲线图。 以下是一个简单示例代码: ```csharp private System.Timers.Timer timer; private double xOffset = 0; public MainWindow() { InitializeComponent(); timer = new System.Timers.Timer(100); // 设置定时器间隔为100毫秒 timer.Elapsed += Timer_Tick; timer.Start(); } private void Timer_Tick(object sender, ElapsedEventArgs e) { // 更新曲线图数据 // 调整x轴坐标值 double lastX = GetLastDataPointXValue(); xOffset += lastX; // 重新绘制曲线图 Dispatcher.BeginInvoke(new Action(() => { DrawCurve(); })); } private void DrawCurve() { // 清除Canvas上的所有图形元素 // 根据新的x轴坐标值重新绘制曲线图 double xOffset = 0; foreach (DataPoint dataPoint in dataPoints) { // 计算新的x轴坐标值 double newX = dataPoint.X - xOffset; // 绘制曲线图上的数据点 // 更新x轴坐标值为最新的值 dataPoint.X = newX; } } ``` 这样,每当定时器触发时,曲线图的x轴坐标值都会根据最新的数据进行调整,从而实现自动滚动的效果。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值