c# - WPF 自定义 ChartLine 性能问题

标签 c# wpf c#-4.0 charts linechart

我们开发了一个简单的 WPF UserControl,它是一个 ChartLine,通常应该显示 -100 到 100 范围内的 512 个值。 该图表可以工作,但是,该图表需要每 1 秒清除和更新一次其值,并且需要花费一秒(1.4~~秒)来简单地呈现其所有值。 在这次失败的尝试之后,我尝试使用 Microsoft 的旧 DynamicDataDisplay (D3),它应该更快,但性能影响完全相同,更新屏幕上的 512 值也花费了一秒多的时间。

下面是我的代码,我相信可能有一些缓存技术、较低的位图分辨率或有助于实现我的目标的东西。

XAML:

<UserControl x:Class="IHM.OsciloscopeGraphic"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         xmlns:local="clr-namespace:IHM"
         mc:Ignorable="d" 
         d:DesignHeight="300" d:DesignWidth="740" Loaded="UserControl_Loaded">
<Grid x:Name="gdMain">
    <Grid.RowDefinitions>
        <RowDefinition Height="30"/>
        <RowDefinition />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition />
        <ColumnDefinition />
    </Grid.ColumnDefinitions>

    <Label Content="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:OsciloscopeGraphic}}, Path=TitleGreen}" HorizontalAlignment="Right" HorizontalContentAlignment="Center" Margin="10, 0" FontSize="18" Width="115" Background="Green"/>
    <Label Content="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:OsciloscopeGraphic}}, Path=TitleLightBlue}" FontSize="18" Margin="10, 0" HorizontalAlignment="Left" HorizontalContentAlignment="Center" Background="LightBlue" Grid.Column="1" Width="115"/>

    <Grid Name="gdChartArea" Grid.Row="1" Grid.ColumnSpan="2" >
        <Border BorderBrush="Black" BorderThickness="1" Margin="30, 10, 10, 30"/>
        <Canvas x:Name="cnvChart" Margin="30, 10, 10, 30">
        <Canvas.Background>
            <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                <GradientStop Color="#4C000080" Offset="1"/>
                <GradientStop Color="#4C7F7FFF"/>
            </LinearGradientBrush>
        </Canvas.Background>
    </Canvas>
    </Grid>
</Grid>

C# 代码:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Shapes;

namespace IHM
{
    public partial class OsciloscopeGraphic : UserControl
    {
        #region Properties
        /// <summary>
        /// If steps Lines are 0, will divide the grid equally by the number in lines grid
        /// </summary>
        public int LinesGrid { get; set; }
        /// <summary>
        /// If steps Columns are 0, will divide the grid equally by the number in lines grid
        /// </summary>
        public int ColumnsGrid { get; set; }

        public int StepsLines { get; set; }
        public int StepsColumns { get; set; }

        public int MaxHorizontal { get; set; }

        public int MaxVertical { get; set; }

        public int MinHorizontal { get; set; }
        public int MinVertical { get; set; }

        public static readonly DependencyProperty TitleGreenProperty =
            DependencyProperty.Register("TitleGreen", typeof(string), typeof(BarGraphicSplitted), new UIPropertyMetadata("TRS"));

        [Bindable(true)]
        public string TitleGreen
        {
            get { return (string)GetValue(TitleGreenProperty); }
            set { SetValue(TitleGreenProperty, value); }
        }
        public static readonly DependencyProperty TitleLightBlueProperty =
            DependencyProperty.Register("TitleLightBlue", typeof(string), typeof(BarGraphicSplitted), new UIPropertyMetadata("FRT"));

        [Bindable(true)]
        public string TitleLightBlue
        {
            get { return (string)GetValue(TitleLightBlueProperty); }
            set { SetValue(TitleLightBlueProperty, value); }
        }
        #endregion Properties

        #region Local Fields/Variables

        private bool initialized = false;

        private int Quantidade
        {
            get { return (Math.Abs(this.MaxHorizontal - this.MinHorizontal) + 1); }
        }
        #endregion Local Fields/Variables

        public OsciloscopeGraphic()
        {
            InitializeComponent();

            this.MaxHorizontal = 255;
            this.MinHorizontal = 0;
            this.MaxVertical = 100;
            this.MinVertical = -100;
            this.LinesGrid = 0;
            this.ColumnsGrid = 0;

            this.StepsColumns = 10;
            this.StepsLines = 10;
        }

        #region Private Local/Methods

        private Line CreateGridLine()
        {
            Line lm = new Line();
            lm.Stroke = Brushes.Black;
            lm.StrokeThickness = 1;
            lm.StrokeDashArray = new DoubleCollection() { 1, 4 };
            lm.SetValue(RenderOptions.EdgeModeProperty, EdgeMode.Aliased);

            return lm;
        }

        private Line CreateHorizontalGridLine(Point start, double length)
        {
            Line ln = CreateGridLine();
            //It has the same value because the line will be a vertical line
            ln.X1 = start.X;
            ln.X2 = start.X + length;
            ln.Y1 = start.Y;
            ln.Y2 = start.Y;

            return ln;
        }

        private Line CreateHorizontalScaleLine(Point start)
        {
            Line l = CreateScaleLine();
            l.X1 = start.X;
            l.X2 = start.X - 5;
            l.Y1 = start.Y;
            l.Y2 = start.Y;

            return l;
        }

        private Line CreateScaleLine()
        {
            Line l = new Line();
            l.Stroke = Brushes.Black;
            l.SetValue(RenderOptions.EdgeModeProperty, EdgeMode.Aliased);

            return l;
        }

        private Line CreateVerticalGridLine(Point start, double length)
        {
            Line ln = CreateGridLine();
            //It has the same value because the line will be a vertical line
            ln.X1 = start.X;
            ln.X2 = start.X;
            ln.Y1 = start.Y;
            ln.Y2 = start.Y + length;
            return ln;
        }
        private Line CreateVerticalScaleLine(Point start)
        {
            Line l = CreateScaleLine();
            l.X1 = start.X;
            l.X2 = start.X;
            l.Y1 = start.Y;
            l.Y2 = start.Y + 5;
            return l;
        }

        private void DrawGrid(Grid grid, Canvas chart)
        {
            bool makeBySteps = true;
            if ((this.StepsColumns == 0) || (this.StepsLines == 0))
            {
                makeBySteps = false;
                if ((this.LinesGrid == 0) || (this.ColumnsGrid == 0))
                    throw new DivideByZeroException();
            }

            //get canvas absolute position
            var getPos = chart.TransformToVisual(grid);
            Point XYpos = getPos.Transform(new Point(0, 0));

            //draw the lines
            double actualWidth = (chart.ActualWidth);
            double initialPosition = (XYpos.X + 1);
            double length = this.MaxHorizontal - this.MinHorizontal + 1;
            double stepLegend = (makeBySteps) ? this.StepsColumns : length / Convert.ToDouble(this.ColumnsGrid);
            int counter = (makeBySteps) ? ((int)length) / this.StepsColumns : this.ColumnsGrid;
            double step = (makeBySteps) ? (actualWidth / length) * this.StepsColumns : (actualWidth / this.ColumnsGrid);
            length = Math.Abs(length);
            double remainder = 0d;


            for (int i = 0; i <= counter; i++)
            {
                //vertical gridlines
                double steps = i * step;
                Point start = new Point(initialPosition + steps, XYpos.Y);
                Line Lm = CreateVerticalGridLine(start, chart.ActualHeight);
                grid.Children.Add(Lm);

                //vertical scale lines
                Point startScale = new Point(initialPosition + steps, XYpos.Y + chart.ActualHeight);
                Line LineScale = CreateVerticalScaleLine(startScale);
                grid.Children.Add(LineScale);


                //bottom labels
                Label lb = new Label();
                lb.Width = 20;
                lb.Height = 20;
                lb.Padding = new Thickness(0);
                lb.HorizontalContentAlignment = HorizontalAlignment.Center;
                lb.ClipToBounds = false;
                //this garantes that it will consider the reminder of divisions
                double numero = this.MinHorizontal + (i * stepLegend);
                remainder += numero - Math.Round(numero);
                numero = Math.Round(numero);
                if (remainder > 1)
                {
                    remainder -= 1;
                    numero += 1;
                }
                else if (remainder < -1)
                {
                    remainder += 1;
                    numero -= 1;
                }
                lb.Content = numero;
                grid.Children.Add(lb);
                lb.HorizontalAlignment = HorizontalAlignment.Left;
                lb.VerticalAlignment = VerticalAlignment.Top;
                //TODO: big coment explaining in details the line bellow
                lb.Margin = new Thickness((XYpos.X - 10) + steps, XYpos.Y + chart.ActualHeight + 5, 0, 0);
            }

            initialPosition = XYpos.Y;
            double actualHeight = (chart.ActualHeight);
            length = this.MaxVertical - this.MinVertical + 1;
            stepLegend = (makeBySteps) ? this.StepsLines : length / Convert.ToDouble(this.LinesGrid);
            counter = (makeBySteps) ? ((int)length) / this.StepsLines : this.LinesGrid;
            step = (makeBySteps) ? (actualHeight / length) * this.StepsLines : (actualHeight / this.LinesGrid);
            //initialPosition = (makeBySteps) ? initialPosition + ((actualHeight / length) * (length % this.StepsLines)) : initialPosition;
            length = Math.Abs(length);
            remainder = 0d;

            for (int i = 0; i <= counter; i++)
            {
                double steps = i * step;
                Point start = new Point(XYpos.X, actualHeight + initialPosition - steps);
                //horizontal gridlines
                Line lm = CreateHorizontalGridLine(start, actualWidth);
                grid.Children.Add(lm);
                //horizontal scale lines
                Line l = CreateHorizontalScaleLine(start);
                grid.Children.Add(l);
                //side labels
                Label lb = new Label();
                lb.Width = 30;
                lb.Height = 20;
                lb.HorizontalContentAlignment = System.Windows.HorizontalAlignment.Right;
                lb.Padding = new Thickness(0);
                lb.VerticalContentAlignment = VerticalAlignment.Center;
                lb.ClipToBounds = false;
                //this garantes that it will consider the reminder of divisions
                double numero = this.MinVertical + (i * stepLegend);
                remainder += numero - Math.Round(numero);
                numero = Math.Round(numero);
                if (remainder > 1)
                {
                    remainder -= 1;
                    numero += 1;
                }
                else if (remainder < -1)
                {
                    remainder += 1;
                    numero -= 1;
                }
                lb.Content = numero;

                grid.Children.Add(lb);
                lb.HorizontalAlignment = HorizontalAlignment.Left;
                lb.VerticalAlignment = VerticalAlignment.Top;
                //TODO: big coment explaining in details the line bellow
                lb.Margin = new Thickness(XYpos.X - 37, start.Y - 10, 0, 0);
            }
        }


        private void DrawGrid()
        {
            this.DrawGrid(gdChartArea, cnvChart);
        }

        private void DrawLine(List<int> p_values, SolidColorBrush cor)
        {
            Polyline cl = new Polyline();
            cl.Stroke = cor;
            cl.StrokeThickness = 2;
            cl.StrokeLineJoin = PenLineJoin.Round;
            //cl.SetValue(RenderOptions.EdgeModeProperty, EdgeMode.Aliased);
            double stepHorizontal = cnvChart.ActualWidth / ((this.MaxHorizontal - this.MinHorizontal) + 1);
            double stepVertical = cnvChart.ActualHeight / ((this.MaxVertical - this.MinVertical) + 1);

            for (int i = 0; i < p_values.Count; i++)
            {
                int val = p_values[i];
                double x = (stepHorizontal * i);
                double y = cnvChart.ActualHeight - ((val - this.MinVertical) * stepVertical);

                cl.Points.Add(new Point(x, y));
            }

            cnvChart.Children.Add(cl);
        }

        private void DrawLineGreen(List<int> p_values)
        {
            DrawLine(p_values, Brushes.Green);
        }

        private void DrawLineLightBlue(List<int> p_values)
        {
            DrawLine(p_values, Brushes.LightBlue);
        }

        private List<int> GetRandomValues()
        {
            int quantidade = this.Quantidade;
            List<int> lsValues = new List<int>(quantidade);
            int seed = 0;
            long ticks = DateTime.Now.Ticks;
            while (ticks > int.MaxValue)
            {
                ticks -= int.MaxValue;
            }
            seed = Convert.ToInt32(ticks);
            Random ran = new Random(seed);

            for (int i = 0; i < quantidade; i++)
            {
                int randomValue = ran.Next(this.MinVertical, this.MaxVertical);
                lsValues.Add(randomValue);
            }

            return lsValues;
        }

        #endregion Private Local/Methods

        #region Public Methods

        public void Clear()
        {
            this.cnvChart.Children.Clear();
        }

        public void UpdateGraphValues()
        {
            UpdateGraphValues(GetRandomValues(), GetRandomValues());
        }

        public void UpdateGraphValues(List<int> p_frontValues, List<int> p_backValues)
        {
            //Clear current graphic values.
            Clear();

            DrawLineGreen(p_frontValues);
            DrawLineLightBlue(p_backValues);
        }

        #endregion Public Methods

        #region Window Events

        private void UserControl_Loaded(object sender, RoutedEventArgs e)
        {
            if (!initialized)
            {
                DrawGrid();
                UpdateGraphValues();
                initialized = true;
            }
        }

        #endregion Window Events
    }
}

要在我希望的条件下测试图表,您可以简单地实例化`

    private OsciloscopeGraphic graphicOscNormal = new OsciloscopeGraphic() 
    {
        MinHorizontal = 0,
        MaxHorizontal = 255,
        MinVertical = -100,
        MaxVertical = 100
    };

在计时器内,您可以调用`graphicOscNormal.UpdateGraphValues()`,它将用随机值填充图形以用于测试目的。 稍后这些值将来自已实现的串行端口。

注意:我还尝试替换 DrawingVisual 和 DrawingContext.DrawLine 的高级 PolyLine,但性能没有改变!

注意2:我使用的是 C#/WPF 和 .NET 4.0 (VS 2010)。

提前致谢,路易斯。

最佳答案

(最大的)问题是你的随机数生成器 - 它效率极低。尝试:

    private Random ran = new Random(0);
    private List<int> GetRandomValues()
    {
        int quantidade = this.Quantidade;
        List<int> lsValues = new List<int>(quantidade);

        for (int i = 0; i < quantidade; i++)
        {
            int randomValue = ran.Next(this.MinVertical, this.MaxVertical);
            lsValues.Add(randomValue);
        }

        return lsValues;
    }

优化时,分析是值得的。

如果您想要真正、真正快速渲染,那么您几乎必须回到 GDI。例如 - 更新您的 Canvas (cnvChart) 以使用此 FastCanvas :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows;
using System.Windows.Interop;
using System.Runtime.InteropServices;
using System.Drawing.Drawing2D;

namespace WpfApplication1
{
    class FastCanvas : Canvas
    {
        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern IntPtr CreateFileMapping(IntPtr hFile,
                                                       IntPtr lpFileMappingAttributes,
                                                       uint flProtect,
                                                       uint dwMaximumSizeHigh,
                                                       uint dwMaximumSizeLow,
                                                       string lpName);

        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern IntPtr MapViewOfFile(IntPtr hFileMappingObject,
                                                   uint dwDesiredAccess,
                                                   uint dwFileOffsetHigh,
                                                   uint dwFileOffsetLow,
                                                   uint dwNumberOfBytesToMap);
        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern bool UnmapViewOfFile(IntPtr lbBaseAddress);


        protected System.Drawing.Graphics GDIGraphics;
        protected InteropBitmap interopBitmap = null;
        protected InteropBitmap buffBitmap = null;

        private const uint FILE_MAP_ALL_ACCESS = 0xF001F;
        private const uint PAGE_READWRITE = 0x04;

        private int bpp = PixelFormats.Bgra32.BitsPerPixel / 8;
        protected IntPtr MapViewPointer;

        public struct ScopeLine
        {
            public SolidColorBrush lineBrush;
            public List<Point> linePoints;
        }

        public List<ScopeLine> Lines = new List<ScopeLine>();

        protected override void OnRender(DrawingContext dc)
        {
            base.OnRender(dc);
            if (Lines.Count() > 0)
            {              
                    ImageSource drIs = null;

                    if (interopBitmap == null)
                    {
                        uint byteCount = (uint)((int)this.ActualWidth * (int)this.ActualHeight * bpp);

                        var fileMappingPointer = CreateFileMapping(new IntPtr(-1), IntPtr.Zero, PAGE_READWRITE, 0, byteCount, null);
                        this.MapViewPointer = MapViewOfFile(fileMappingPointer, FILE_MAP_ALL_ACCESS, 0, 0, byteCount);
                        PixelFormat format = PixelFormats.Bgra32;
                        var stride = (int)((int)this.ActualWidth * format.BitsPerPixel / 8);
                        this.interopBitmap = Imaging.CreateBitmapSourceFromMemorySection(fileMappingPointer,
                                                                                         (int)this.ActualWidth,
                                                                                         (int)this.ActualHeight,
                                                                                         format,
                                                                                         stride,
                                                                                         0) as InteropBitmap;

                        this.GDIGraphics = GetGdiGraphics(MapViewPointer);
                    }

                    GDIGraphics.FillRectangle(System.Drawing.Brushes.Transparent,
                                                    new System.Drawing.Rectangle(0, 0,
                                                    (int)this.ActualWidth,
                                                    (int)this.ActualHeight));

                    foreach (ScopeLine dLine in Lines)
                    {
                        var pointCount = dLine.linePoints.Count();
                        Color lpColour;
                        lpColour = dLine.lineBrush.Color;
                        System.Drawing.Color lp2Colour;
                        lp2Colour = System.Drawing.Color.FromArgb(lpColour.A,
                                                                  lpColour.R,
                                                                  lpColour.G,
                                                                  lpColour.B);


                        System.Drawing.Pen lpPen = new System.Drawing.Pen(lp2Colour, 1.5f);
                        System.Drawing.PointF newPoint = new System.Drawing.PointF((float)dLine.linePoints[0].X,
                                                                                   (float)dLine.linePoints[0].Y);


                        for (int i = 0; i < pointCount - 1; i++)
                        {
                            System.Drawing.PointF newPoint1 = new System.Drawing.PointF((float)dLine.linePoints[i + 1].X,
                                                                                            (float)dLine.linePoints[i + 1].Y);
                            GDIGraphics.DrawLine(lpPen, newPoint, newPoint1);
                            newPoint = newPoint1;
                        }

                    }

                    var bmpsrc = interopBitmap.GetAsFrozen();
                    if (bmpsrc == null || bmpsrc.CheckAccess())
                    {
                        drIs = (System.Windows.Media.Imaging.BitmapSource)bmpsrc;
                    }
                    else
                    {
                        //Debug.WriteLine("No access to TheImage");
                    }                                    

                    dc.DrawImage(drIs, new Rect(this.RenderSize));
            }
        }

        private System.Drawing.Graphics GetGdiGraphics(IntPtr mapViewPointer)
        {
            System.Drawing.Graphics gdiGraphics;
            System.Drawing.Bitmap gdiBitmap;
            gdiBitmap = new System.Drawing.Bitmap((int)this.ActualWidth,
                                                  (int)this.ActualHeight,
                                                  (int)this.ActualWidth * bpp,
                                                  System.Drawing.Imaging.PixelFormat.Format32bppArgb,
                                                  mapViewPointer);

            gdiGraphics = System.Drawing.Graphics.FromImage(gdiBitmap);
            gdiGraphics.CompositingMode = CompositingMode.SourceCopy;
            gdiGraphics.CompositingQuality = CompositingQuality.HighSpeed;
            gdiGraphics.SmoothingMode = SmoothingMode.HighSpeed;

            return gdiGraphics;
        }
    }
}

并将您的 DrawLine 更改为:

    private void DrawLine(List<int> p_values, SolidColorBrush cor)
    {            
        double stepHorizontal = cnvChart.ActualWidth / ((this.MaxHorizontal - this.MinHorizontal) + 1);
        double stepVertical = cnvChart.ActualHeight / ((this.MaxVertical - this.MinVertical) + 1);

        List<Point> pts = new List<Point>();

        for (int i = 0; i < p_values.Count; i++)
        {
            int val = p_values[i];
            double x = (stepHorizontal * i);
            double y = cnvChart.ActualHeight - ((val - this.MinVertical) * stepVertical);
            pts.Add(new Point(x,y));                       
        }

        FastCanvas.ScopeLine newLine;
        newLine.lineBrush = cor;
        newLine.linePoints = pts;

        cnvChart.Lines.Add(newLine);            
    }

更新值到:

    public void UpdateGraphValues(List<int> p_frontValues, List<int> p_backValues)
    {            
        cnvChart.Lines.Clear();
        DrawLineGreen(p_frontValues);
        DrawLineLightBlue(p_backValues);
        cnvChart.InvalidateVisual();
    }

使用像这样的 GDI,相同的图形可以实时更新(对于 512 点,轻松 > 30fps),而使用 WPF 渲染的速度约为 5-7fps。

关于c# - WPF 自定义 ChartLine 性能问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/17969146/

相关文章:

c# - 在另一个进程运行时启用按钮单击

c# - 在 VS 2015 和 EF7 从模型生成 SQLite 数据库

c# - WPF 将按钮绑定(bind)到按下的键

.net - 在 Oracle Merge 语句的 Using 子句中指定参数

c# - 用于检查修改状态的数据类型或数据结构?

c# - 将 System.Array 转换为字符串 []

javascript - 如何将文件和属性发送到 ASP.NET MVC 中的 Controller 并接收动态生成的 PDF 作为二进制作为响应?

c# - WPF 选择值,显示成员路径与列表框和组合框不一致

WPF 访问路径 'C:\Users\...\Documents\My Music' 被拒绝

dynamic - 你如何实现 C#4 的 IDynamicObject 接口(interface)?