65.9K
CodeProject 正在变化。 阅读更多。
Home

您的项目的 2D 数学曲线演示

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (19投票s)

2004年10月4日

7分钟阅读

viewsIcon

101888

downloadIcon

5071

带注释的代码

引言

也许你的程序需要对任何二维数学表达式进行图形呈现。在这种情况下,你可能需要一套能够轻松完成此任务的类。通常,你必须考虑缩放、曲线布局、如何设置轴上的刻度、无穷大问题等,而图形表示可能很容易成为项目中重要且繁琐的一部分。

Plothelp 的创建就是为了以最简单的方式为你完成所有这些。你只需要在代码中引入 4 行:一行定义公式作为字符串,另外两行定义 x 范围的起点和终点,最后你可以指定形成曲线的点数。

其余部分都是自动的。Plothelp 将缩放两个轴,设置相应的刻度,绘制相应的 xy 网格线,最后绘制曲线。

此外,为了完成函数的呈现,通常需要使用不同的符号绘制数学函数。

y = sqrt(x) 就是一个这样的例子。Plothelp 提供了使用 ± 运算符在同一图表中绘制两种选项的可能性。只需编写 ±sqrt(x) 而不是 sqrt(x) 即可。你可以在公式中随意使用此运算符。

诸如 y = ±x ±1 的公式将生成一个包含 4 条曲线的图表。y 轴的缩放将同时适应所有曲线。

可下载的演示 exe 允许你将公式写入控制台,并由 Plothelp 进行表示。这让你对表示如何适应你的项目有一个印象。

使用代码

Plothelp 仅嵌入在三个类中,这些类专门用于窗体布局、绘图和数学运算。此外,还需要 info.lundin.Math.dll 来解析公式字符串。请执行以下步骤:

  1. 在你的项目中,在 Project 菜单下,添加以下 3 个文件:
    • CurveCalc.cs
    • GraphPlot.cs
    • Plotter.cs
  2. 选择 Add Reference 选项,并使用 Browser 选项查找文件 info.lundin.Math.dll,然后单击 OK。这将包含公式解析器的功能。
  3. 添加指令
    using DataPlotter;
    using System.Windows.Forms;
    using System.Drawing;

    到你将调用 Plothelp 的类的头部。

  4. 现在你就可以在项目中使用 Plothelp 了。只需在你需要的地方添加以下行,例如:
    // string funcstr="x^2+3*x-4";//defines the function as a string 
    // double xs=-8.0; //starting value for the x-range at –8 
    // double xe= 3.0; //final value for the x-range at 3 
    // int nump=200; //number of points to be drawn //
    // Application.Run(new GraphPlot(funcstr,xs,xe,nump)); 

首先,公式被定义为一个字符串,只取等式的右侧。在上面的例子中,

使用了 y = x^2 + 3*x - 4。

其次,定义范围,将起点和终点指示为 double

第三,定义要表示的点数作为 int。通常,100 或 200 个点就足够了。在某些情况下,你可能希望通过增加此数字来研究细节,以获得更好的分辨率。

最后,调用 GraphPlot,并精确地传递变量,如所示。你应该看到下面所示的图表。

接下来的图表显示了 ± 运算符在函数中的多重使用:

y = +sqrt(x)+sin(x)

在范围 –1 到 8 内,当 + 号依次被 ± 运算符替换时。

图 2:y = sqrt(x) + sin(x) y = sqrt(x) ± sin(x) y = ±sqrt(x) ± sin(x)

工作原理

Plothelp 生成一个函数 n 个点的矩阵

x0 , y0

x1 , y1

: :

: :

xn-1 , y n-1

然而,首先检查公式字符串中是否存在 ± 运算符。

如果存在,其在字符串中的位置将被存储在一个数组中,然后该运算符依次被“–”和“+”替换,每次在扩展矩阵中计算一个新的列

x0, y10 …. yk0

x1, y11 …. yk1

: :

: :

xn, y1n .... ykn

其中 k 是公式中存在的 ± 运算符数量的两倍。

基于该矩阵,首先针对 x 范围,然后针对所有 y 范围,计算并存储轴的刻度线(ticmarks)。然后绘制并标记带有刻度线的轴。

每个刻度线的值首先通过一个特殊方法 Rndhlp 进行检查,以避免舍入误差(例如 2.3650000000001*E-13),并且也适用于指数表示法。基本上,数字被转换为字符串,指数部分(如果有的话)被截断,并处理剩余的数字。如果该值 >1,则数字序列在逗号后面的第 5 位被截断,并再次添加指数部分。然而,如果该值 <1,则找到第一个非 0 数字,并将逗号放在其后面。现在,序列再次在逗号后面的第 5 位被截断。由于逗号的移位等于乘以 10n,因此在再次添加可能的指数部分时会考虑这一点。

要绘制曲线,使用 `g.DrawLine(penDraw,Point1,Point2)`。这意味着整个曲线实际上是微小直线从点到点的构造,这可能会在某些曲线的表示中产生一些问题。例如,想象一个由函数 y = sqrt(1-x2) 在 -2 到 +2 范围内表示的圆。由于曲线是从左到右绘制的,因此首先需要识别曲线的起点。

这通过 CurveCalc 类的 `FirstNumber()` 方法完成,该方法逐点检查矩阵,返回第一个给出有限 y 值的索引位置。在我们的圆形示例中,这应该对应于点 -1,0。

然而,如果你尝试用例如 103 步来呈现曲线,那么第一个真实点将在 x = -1.0196 的第 25 点和 x = -0.980 的下一个计算点之间,此时函数已经给出了一个值。这意味着你没有找到曲线的真正起点。在这种情况下,你将得到一个在开头(和结尾)“开放”的圆。

为了避免这类问题,使用了 `CurveStart(int column)` 方法。该方法查找第一个给出有限 y 值的 x 值(firstX),并在此点和前一个点之间开始迭代以找到曲线的起点。

 //This method looks for the start of the curve 
 //which is not necessarily at
 //the first point of the range. Also, the start of 
 //the curve may be at infinity;
 //in this case the next point is chosen. 
 //A value of the x-range is returned.
 //"column" corresponds to the number of y-column in the xyMatrix
 public double CurveStart(int column)
 {
      int j,t=column;
     double firstX=0,foreX=0, z=0;
     double increm=(xn-x0)/nump;
     double y;
     bool ynum, yinf; 
     y=curve[0,t]; //y at the start of the range 
     ynum=Double.IsNaN(y); //ynum=false if this value is a number
     yinf=Double.IsInfinity(y); //yinf=true if this value is ±8
 
     if(ynum==false & yinf==false)//if the first value is 
 // a number and not infinit
     {
         return x0; //then no problem exists. Returns x0.
     }
     else //if not, now the first x-value giving
     { //a finit y-value is to be found in this column 
       for(j=1;j<=nump-1;j++)
       {
        y=curve[j,t]; //y-value at jth position of the matrix
        ynum=Double.IsNaN(y);     //is that a number? (false for any number)
        yinf=Double.IsInfinity(y);//is that infinit? (false if not infinit)
 
         if(ynum==false & yinf==false)//if this is a finit number:
         {
          firstX=curve[j,0]; //firstX is the first x-value that
           foreX=curve[j-1,0]; //gives a finit y-value! foreX is the 
           break; //previous x-value in the x-range
         } 
       }
 
    }
    if(yinf==false)   
//The curve must start between foreX and firstX and the 
    {                   //starting point is found by iteration
      for(int k=0;k<50;k++)//starts an iteration for 20 times
       {
        z=foreX+(firstX-foreX)/2;
//calculates an average between both values and 
        if(Double.IsNaN(yValue(z,t))==true)
          {foreX=z;}//proves if an y-value exists
        else {firstX=z;} 
      }
    }
    return firstX; 
//returns the aproximated x-value of the start of the curve
 }

对于某些类型的函数,增加迭代因子 k 可能很有用。

以类似的方式,`CurveEnd``(int column)` 方法用于圆的终点。column 指的是 xy 矩阵中与当前曲线对应的列。

限制

Plothelp 也有一些罕见的情况无法正常工作。例如,当曲线在给定的 x 范围内形成多个封闭区域时,就会发生这种情况。请看以下表示函数的图表:

y = ±sin(x)2/x

图 3:x 范围从 6 到 10 x 范围从 6 到 16

如你所见,寻找区域起点和终点的方法在区域之间不起作用,因此第一个区域的最后一个点通过一条不表示函数的直线连接到第二个区域的第一个点。

关注点

你有没有尝试过编程轴的自动划分?它一开始看起来很简单,但我花了很多时间才让它运行良好。现在它看起来如此简单!

 //returns an ArrayList containing the ticmarks of the axis
 //using the limits of the x-range (xa and xe)
 public ArrayList AxDiv(double xa, double xs) 
 { 
   if(xa==Double.NegativeInfinity){xa=-100000;}
   if(xs==Double.PositiveInfinity){xs= 100000;} 
 
   double diff=xs-xa; //finds the range
   double num=diff/10; //takes the 10th part
   int redw=(int)Math.Floor(Math.Log10(num));    //finds the next minor
   double tlg=Math.Pow(10,redw); //as a power of 10
 
   ArrayList tic=new ArrayList();
   if(diff/tlg<11){tlg=tlg;} //only 10 divisions are allowed
   else if(diff/(2*tlg)<11){tlg=2*tlg;} //this reduces their number by 2
   else if(diff/(5*tlg)<11){tlg=5*tlg;} //this reduces by 5
   else {tlg=10*tlg;} //and if necessary by 10
 
   if(xa/tlg==Math.Round(xa/tlg)) //only if the starting value of
   { //the range is an entire this value
        tic.Add(xa); //is taken as the first ticmark
   }
   else //otherwise the range is amplified
   { //to the left to the next ticmark
        tic.Add(Math.Floor(xa/tlg)*tlg);
   }
 
   while(Convert.ToDouble(tic[tic.Count-1])<xs)
 //now the rest of the ticmark+s
   { //is added
        tic.Add(Convert.ToDouble(tic[tic.Count-1])+tlg);
   }
   return tic;
 } 

历史

这是我的第一个 C# 程序。我确信你会发现很多可以改进的地方。为了缩短工作量,我采用了 Hans-Jürgen Schmidt 编写的一个很棒的程序的一部分:DataPlotter - 2D 数据的线性或对数显示,该程序已通过 CodeProject 提供。

另一个很大的帮助是 Patrik Lundin 编写的公式解析器(参见 http://www.lundin.info),它运行得非常好,为我节省了大量时间。如果你想了解公式解析器的语法规则,最好访问这个网站。

向两位作者致以诚挚的感谢。

© . All rights reserved.