C# 中的折线图组件






4.83/5 (32投票s)
折线图是可集成到 Winforms 中的用户控件。X 点相对于 Y 绘制。
 
 
引言
本文解释了如何通过考虑折线图控件来创建功能通用的用户控件。折线图用户控件是控件组,包括面板、标签、网格等。该控件可以添加到工具箱中,并可以拖放到 Winform 上。它包含图表的所有功能,可以像组件一样使用。
背景
我们创建的任何 UI 功能都不应特定于使用它的系统。始终考虑其可重用性,因为 UI 实现需要大量精力。例如,如果您需要在 Winform 上绘制图表,那么我们大多数人只在表单上实现图表功能。相反,考虑实现可重用的功能。要重用该功能,请创建一个用户控件,任何系统都可以以最小的努力重用该控件。用户控件的创建需要大量的背景工作。这就像在我们深入实际实现之前收集需求一样。从使用控件的最终用户的角度考虑,您将获得控件需要满足的所有需求。
规划图表控件
开发任何图表组件都需要以下设计选项
- X 轴(支持 x 轴值,指示标签)
- Y 轴(支持 y 轴值,包括不同的缩放功能,如对数、线性等。指示标签)
- 绘图区域(支持网格,包括水平和垂直网格的缩放)
- 注意:必须处理所有与颜色相关的选项(如网格颜色、绘图线颜色、背景颜色)。应提供按需更改颜色的选项。
Using the Code
考虑到上述设计选项,将解释控件实现中的技术方面。
Y 轴的实现
Y 轴包含要绘制的点的最小和最大范围。最小和最大范围的选择取决于使用它的应用程序。图表支持两种类型的缩放,如下所示
- 线性图:此图由具有固定增量值的范围组成。这里增量因子被添加到先前的值中。
 示例:增量因子=2,最小值=2,值=2,4,6,8,10,12 等。
- 对数图:这里增量因子乘以先前的值。
 示例:增量因子=4,最小值=1,值=1,4,16,64,256,1024 等。
        //Method is called when we need to paint the y axis values
        private void mpanelYAxisPaint(object sender, PaintEventArgs e)
        {
            try
            {
                //Take the graphics object to consideration
                Graphics g = e.Graphics;
                pnlYAxis.BackColor = Color.FromName(YAxisBackColor.Name);
                int l = 0;
                float k = LogarithmicMin;//Assign the minlogarithmic value
                int plen = 0;
                //Calculate the length of each grid
                int temp = plotArea.Height / NoOfYGrids;
                //Font object to paint Y axis
                Font mfYAxisFont = new Font("Arial", 7f, FontStyle.Regular);
                for (int i = 0; i <= NoOfYGrids; i++)
                {
                    //If linear scale
                    if (!IsLogarithmic)
                    {
                        using (Pen p = new Pen(Color.Black))
                        {
                            //For correct alignment of characters to right 
                            //we are padding it.
                            g.DrawString(l.ToString().PadLeft(l.ToString().Length),
                            mfYAxisFont, Brushes.Black,
                            new PointF(4f, this.plotArea.Height + 
					panelYExtra.Height - plen-7));
                        }
                        plen = plen + temp;
                        //Adding the increment factor
                        l = l + YGridInterval;
                    }
                        //if logarithmic
                    else
                    {
                        using (Pen p = new Pen(Color.Black))
                        {
                            //For correct alignment of characters to right,
                            //we are padding it.
                            g.DrawString(k.ToString().PadLeft(k.ToString().Length),
                            mfYAxisFont, Brushes.Black,
                            new PointF(0f, this.plotArea.Height + 
					panelYExtra.Height - plen-7));
                        }
                        plen = plen + temp;
                        //multiply the increment factor
                        k = k * LogarithmicIncFactor;
                    }
                }
                //Dispose the fone object as graphics object are very costly operations.
                mfYAxisFont.Dispose();
            }
            catch (Exception ex)
            {
                //Log the error
            }
        }
...
Y 轴上的字符串绘制是使用 Y 轴面板的图形对象实现的。找到字符串要绘制的 x 值和 y 值。
X 轴的实现
X 轴包含相对于 Y 绘制点的那些值。在 X 轴上绘制字符串时需要考虑许多技术方面,如下所示
- 要移动的值与图表绘制的同步
- 轴上显示的两个值之间的间隔
- 当滚动条事件发生时,值必须相应更新
以下代码显示了图形对象的处理以及当任何事件发生时相应更新的值。
//Method is called to point the x axis values i.e., time
private void mpanelXaxisPaint(object sender, PaintEventArgs e)
{
    try
    {
        int temp = 0;
        //Consider the graphics object of the x axis panel.
        Graphics g = e.Graphics;
        pnlXAxis.BackColor = Color.FromName(XAxisBackColor.Name);
        //The smoothing mode is set to Antialias which gives very smooth effect
        g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
        int j = 0;
        //If scroll bar visible then calculate the number of 
        //moves the scroll bar as scrolled.
        if (!scrollBar.Visible)
            temp = Convert.ToInt32(totalPointTime) * GridSpacingX;
        else
            temp = Convert.ToInt32(totalPointTime) * GridSpacingX -
            (((this.scrollBar.Maximum - this.scrollBar.LargeChange) -
            noOfMoves) * GridSpacingX);
        if (j < 0) return;
        for (int i = (this.pnlXAxis.Width - temp - this.panelExtra.Width);
        i <= this.pnlXAxis.Width && j >= 0; i += GridSpacingX, j++)
        {
            if (j >= this.values.timeCount)
                return;
            //Draw the string by calculating the x and y values.
            if (Convert.ToInt32(this.values.GetTIMEIndex(j, "Time"))
            	% XValueDisplayInterval == 0)
            {
                g.DrawString(this.values.GetTIMEIndex(j, "Time"),
                new Font("Arial", 8f, FontStyle.Regular), Brushes.Black,
                new PointF(i-3, this.pnlXAxis.Height / 12));
            }
        }
    }
    catch (Exception ex)
    {
        //Log the error
    }
}
滚动条事件中的移动次数被考虑用于相应更新值。
绘图区域的实现
绘图区域由网格以及相对于 X 和 Y 绘制的值组成。在绘图区域上定位点之前,必须绘制网格(水平和垂直)。绘制具有所需刻度的水平和垂直网格是用户可配置的。水平线的数量由 NoOfYGrids 属性确定,垂直线之间的间隔由 GridSpacingX 属性确定。实现如下所示
 
 
在这里,我们将绘图区域划分为 NoOfYGrids,这样我们就可以得到每个网格的高度,在那里可以绘制水平线。要计算垂直线,我们从右角开始绘制线,并以 GridSpacingX 值递增,当递增值超出绘图区域时停止。
//This method paints the grid each time called
private void PaintGrid(Graphics g)
{
    try
    {
        //For smoothing.
        g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
        using (Pen p = new Pen(Color.FromName(XGridColor.Name)))
        {
            // Draw all visible, vertical gridlines
            for (int i = this.plotArea.Width - gridScrollOffset;
            	i >= 0; i -= GridSpacingX)
            {
                g.DrawLine(p, i, 0, i, this.plotArea.Height);
            }
        }
        //Draw all horizontal lines.
        using (Pen p = new Pen(Color.FromName(YGridColor.Name)))
        {
            // Draw all visible, horizontal gridlines
            for (int i = this.plotArea.Height; i >= 0; i -= GridSpacingY)
            {
                g.DrawLine(p, 0, i, this.plotArea.Width, i);
            }
        }
    }
    catch (Exception ex)
    {
        //Log Error
    }
}
接下来是绘图区域上绘制点的有趣方面。点相对于 X 和 Y 值放置在绘图区域上。这些 X 和 Y 值是相对于像素的。以下代码解释了确定点所使用的方法
//This method paints the graph i.e., points on the graph
void PaintGraph(Graphics g)
{
    try
    {
        //For smooth drawing.
        g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
        #region general
            float dx = this.DX;
            float x = this.plotArea.Width;
            int t = this.values.timeValCount;
            int i = this.values.PointCount;
            int pointtracker = 0;
            #region Point
            if (this.values.PointCount == 0)
                return;
            if (--t <= 0)
                return;
                float distance = this.plotArea.Width;
                //To determine the distance after the plot area.
                if(!this.scrollBar.Visible)
                    distance -= Convert.ToInt32(totalPointTime) * GridSpacingX;
                else
                    distance -= Convert.ToInt32(totalPointTime) * GridSpacingX -
                    (((this.scrollBar.Maximum - this.scrollBar.LargeChange) -
                    noOfMoves) * GridSpacingX);
                //To determine the distance in the plot area.
                if (!this.scrollBar.Visible)
                    x -= Convert.ToInt32(totalPointTime) * GridSpacingX;
                else
                    x -= Convert.ToInt32(totalPointTime) * GridSpacingX -
                    (((this.scrollBar.Maximum - this.scrollBar.LargeChange) -
                    noOfMoves) * GridSpacingX);
                //Assign the x and y points here
                p.X=x;
                //Here the ScaleY function is called where the
                //function returns the height in terms of pixels.
                p.Y=ScaleY(this.values.GetIndex(0, "Point"));
                //For Graph2
                if (NoOfGraphs == 2)
                {
                    p1.X=x;
                    p1.Y = ScaleY(this.values.GetIndex2(0, "Point")) ;
                }
            pointtracker =  1;
            //Continuous looping till all the points are plotted from the buffer.
            for (; ; )
            {
                if (pointtracker == i || i <= 0)
                    break;
                //Scale X is called here to determine the x point
                //where the point has to be plotted on to the plot area.
                ScaleX((Convert.ToInt64(this.values.GetTIMEIndex
			(pointtracker, "PointTime"))), ref x);
                q.X=x;
                q.Y=ScaleY(this.values.GetIndex(pointtracker, "Point"));
                //Actual drawing takes place here
                using (Pen pen = new Pen
			(Color.FromName(PlotLine1Color.Name), PlotLine1Width))
                {
                    g.DrawLine(pen, p, q);
                }
                //Here the first point become the previous point.
                p = q;
                if (NoOfGraphs == 2)
                {
                    q1.X=x;
                    q1.Y=ScaleY(this.values.GetIndex2(pointtracker, "Point"));
                    using (Pen pen = new Pen(Color.FromName(PlotLine2Color.Name), 
				PlotLine2Width))
                    {
                        g.DrawLine(pen, p1, q1);
                    }
                    p1 = q1;
                }
                //Incremented to check whether all the points from the buffer 
                //have been plotted.
                pointtracker++;
            }
            #endregion
        #endregion
    }
    catch (Exception ex)
    {
        //Log the error
    }
}
在绘图区域上绘制点的过程中,需要动态计算两个点,如下所示
- X 轴点:X 轴包含毫秒时间。因此,图表中的每个网格代表一秒。每个网格的划分方式是,秒应该在 x 值方面精确指向网格。例如
 采集样本 = 1100 毫秒
 每个网格 = 1 秒(1000 毫秒)
 每个网格的划分 = 10 部分(100 毫秒 * 10)= 1000 毫秒
 因此 1100 落在如下所示的点上
  上述计算的代码如下所示 //For scaling the x axis point for 60sec and 10 min graph. private void ScaleX(float source, ref float x) { try { double tempsource = Math.Ceiling(source); //Divide the grid to ten equal parts float temp = (float)this.GridSpacingX / 10; //Difference is divided by 100 to make it 100 equal parts double d = tempsource / 100; //The difference is multiplied with gridspacing ten equal parts double ans = d * temp; //Round the obtained result. ans = Math.Round(ans, 2); //Add the result to the reference variable for further action. x += (float)ans; } catch (Exception ex) { //Log the error } } 
- Y 轴点:Y 轴点包含动态计算的实际点。Y 点表示绘图区域内的像素(点)。相对于线性和对数的点计算如下代码所示
//This method scales the point based on the height of the plot area
float ScaleY(float y)
{
    try
    {
        //Calculate the height of each grid.
        float eachgrid = this.plotArea.Height / NoOfYGrids;
        //Calculate the extra space if any
        float extraspace = this.plotArea.Height - (eachgrid * NoOfYGrids);
        if (!IsLogarithmic)
        {
            int h = this.plotArea.Height - 2;
            float t = h - ((y - 0) / ((NoOfYGrids * YGridInterval) - Min)) * h;
            if (t < 0)//if t exceeds the limit of graph then make it saturated point
                t = 1;
            //Return the point in terms of pixels.
            return t + extraspace;
        }
        else
        {
            //If the point is 0 then return the height of the plot area
            //which represents a point on the plot area as 0.
            if (y == 0)
                return this.plotArea.Height;
            double ans = y;
            //Assign the Min and Max value for logarithmic scale.
            double min = LogarithmicMin;
            double max = LogarithmicMax;
            if (ans <= min)
                return this.plotArea.Height - extraspace - 1;
            //logarthmic formula to calculate Y-point on logarthmic scale
            float t = (float)((this.plotArea.Height-extraspace) *
            ((Math.Log10(ans) - Math.Log10(min)) / 
			(Math.Log10(max) - Math.Log10(min))));
            float ypoint = t;
            //if Calculated point is less or equal to 0 then ypoiny is 0
            if (ypoint < 0)
                ypoint = 0;
            //If its more than the plot area than return the height - extra space
            if (ypoint > (this.plotArea.Height - extraspace))
                ypoint = (this.plotArea.Height - extraspace);
            //Return the actual point.
            return (this.plotArea.Height - ypoint);
        }
    }
    catch (Exception ex)
    {
        //Log the error
    }
    return 0f;
}
折线图组件支持的属性

绘制图表背后的实际逻辑是,每当点添加到缓冲区时,plotArea 就会失效,这会导致绘图区域重新绘制。因此,在绘制过程中,缓冲区中的所有点都被绘制,包括最近添加到缓冲区的点,从而产生运行图表的视图。要绘制在 plotArea 上的点使用组件公开的 Add() 方法添加,如下所示
lineGraph1.Add(graph1point,graph2point,time(in milliseconds));
每次使用上述方法添加点都会重新绘制图表,从而使用户体验到运行时图表。如果用户想清除缓冲区中的点,可以使用如下所示的方法。
lineGraph1.Clear();
此方法清除缓冲区中所有放置的点,并使图表保持首次启动时的状态。
折线图支持的功能
- 支持跨线程引用(即,可以从任何线程更新图表)
- 只需增加 Buffersize值即可支持任意数量的点
- 支持同时绘制 2 个图表
- 支持用户可配置的布局和颜色
- 支持两种缩放选项(线性与对数)
- 在 X 轴和 Y 轴方面精确定位点
- 支持所有属性的设计时预览
- 支持缓冲区功能,即出现一个滚动条以查看以前的值
- 向左滚动滚动条会暂停图表的绘制,但点会在后端收集到缓冲区中。双击图表会恢复绘制所有收集的点。
关注点
开发用户控件在可重用性、逻辑思想、满足最终用户需求等方面丰富了编码能力。开发折线图组件后,我开始从可重用性的角度思考任何解决方案。开发该组件非常有趣且具有挑战性,因为它包含许多功能。每个功能都让我非常感兴趣,因为它需要一定量的设计和实现方法。支持设计时预览非常有趣。在开发过程中,我获得了大量关于 GDI+ 以及 UI 设计的信息。感兴趣的人可以从一个小型控件开始,这在大多数情况下对他们非常有用。在开发过程中,您将获得许多概念性信息的实践经验。此组件的下一个版本将是 WPF。
注意:演示中用于绘制图表的值只是从数组中获取的硬编码值。如果您在解决方案中使用该控件,您可以使用 Add() 方法从实时场景计算或获取点。
历史
- 2011 年 10 月 27 日:初始版本


