仿查询分析器的C#计算器——6.函数波形绘制

最近把计算器完善了一下,添加了变量的支持,添加了更多的函数,把逻辑短路操作也实现了,并修正了一些小错误。想起来以前在一本书里看到过一个示例,输入函数表达式,就可以绘制函数的波形。最开始学VB的时候,就喜欢用函数来画图。再加上对电子技术有点兴趣,很多波形都可以用函数来表示,很自然就想到用程序来模拟示波器显示波形。但是因为函数都需要在代码里面写死,如果需要新增函数或者进行修改,需要修改程序代码再编译运行。既然现在可以做到对表达式进行计算,也可以支持变量,那么让变量的值变化就可以计算得到不同的值,再把这些值组合成坐标点,连接起来就成了波形。于是乎,咱也试试做一个显示函数波形的小程序玩玩,效果如下:

先说说新添加的变量支持功能。这里的变量并不需要声明,只要不是保留的关键字,程序就把它作为变量。在以前的版本中遇到不认识的字符串会报错,现在是在分析关键字的时候做了特殊处理,遇到非关键字字符串则添加到一个静态的变量字典中。变量字典的Key是该变量的字符串表示,Value是一个TokenValue对象。在添加到字典之后,如果再遇到相同的字符串,则返回变量字典中对应的TokenValue对象。下面给个例子:
变量支持1

从例子可以看出,在未赋值之前,n的值为空,和其他值运算不会发生错误。下面是语法树分析的图:
变量支持语法树
从图上可以看出变量n是引用的,在第一句中n的值是空,类型为未初始化类型,但是在PropertyGrid中显示的信息是最后一次赋值的结果。而且这里把赋值操作符"="作为赋值操作的根节点,并没有像左括号"("一样处理。比如最后一个表达式sin(n+20)的语法树中,TokenSin的下级是TokenPlus,而不是TokenLeftBracket。对于赋值操作符"="之所以这保留了原始结构,是因为这样可以在修改下级节点的值之后继续调用Execute方法进行计算,否则如果把值直接指定给变量,下次调用Execute的时候就没法执行了。左括号只是分割表达式,但赋值操作符是有真正的运算过程,所以必须用不一样的分析方法。这一点对于下面要实现的函数波形非常重要。在绘制波形的时候需要改变变量,如果采用变量字符串替换的方法,每次都需要分析表达式,而变量的值域可能很大,这样会把大量时间消耗在分析上。但是如果能保留完整的语法树,只需要将变量对应的TokenRecord的值改变,再次调用顶级节点的Execute方法,这时候只需要逐级向下调用计算方法即可,不需要重新分析表达式了。
接下来就介绍怎么实现函数波形绘制的吧。首先这里引入了一个变量n,在进行计算之前在程序里面进行初始化,然后根据设置的范围用for递增。定义两个表达式X和Y,分别对应坐标点的X和Y,这两个表达式中包含n,在对n进行递增之前调用语法分析类进行分析,得到顶级节点,这时候语法树已经分析完成了。在对n进行递增的时候,计算X和Y,形成一系列坐标点。调用Graphics类的DrawLines方法,把计算得到的一系列坐标点作为参数传递给该方法,这样就可以看到特定的波形。
比如阿基米德螺旋线用伪代码表示如下:

 
for(int n = 1; n < 360; n++)
{
    X 
= n*sin(n);
    Y 
= n*cos(n);
    PointCollection.Add(
new Point(X, Y));
}

myGraphics.DrawLines(myPen, PointCollection);
在本程序里阿基米德螺旋线的伪代码可以表示如下:
strN = "n=0";
strX 
= "n*sin(n)";
strY 
= "n*cos(n)";
TokenN 
= mySyntaxAnalyse.Analyse(strN);
TokenX 
= mySyntaxAnalyse.Analyse(strX);
TokenY 
= mySyntaxAnalyse.Analyse(strY);

for(int index = 1; n < 360; n++)
{
    TokenN.TokenValue 
= index;
    TokenX.Execute();
    TokenY.Execute();
    PointCollection.Add(
new Point(TokenX.TokenValue, TokenY.TokenValue));
}
myGraphics.DrawLines(myPen, PointCollection);
 
从伪代码中可以看到X和Y的表达式可以由用户输入,这样就不需要修改程序再编译才能显示要绘制的波形图了。
为了同时支持多个波形图,这里用一个类来记录一个函数对,以及线条颜色、线条宽度等信息。该类的代码如下:
Code

在界面上添加相关控件,用来操作绘图信息。点击绘图按钮之后,按照界面上的PictureBox的尺寸创建一个Bitmap对象,然后把它作为参数调用绘图代码,代码如下:

Code

这里的代码还有不少可以改进的地方,比如可以设置图片尺寸、图片背景、坐标原点、背景网格,甚至可以让波形一段一段慢慢的显示出来,更好的了解波形的绘制过程。如果有需要可以自行完善。
本文开头给出的示例的各个设置如下表: 

名称

表达式X

表达式Y

相位1

n

a=100*sin(n)

相位2

n

b=100*sin(n+120)

相位3

n

c=100*sin(n-120)

三相整流波形

n

abs(a)+abs(b)+abs(c)

李沙育图

100*sin(n*2)

100*cos(n*3+90)-200

阿基米德螺旋线

n*sin(abs(n))/20-240

n*cos(abs(n))/20-180


波形示例
相位1、相位2、相位3是模拟三相电的波形,都是标准正弦波,只是相位差120度。这里用一个赋值操作声明了三个变量a, b, c,这样在三相整流波形中就可以直接操作这三个变量了,所以三相整流波形的表达式Y的值是abs(a)+abs(b)+abs(c)。通过声明变量的方法可以很容易让波形之间关联起来,也可以减少计算量。
有时候胡乱输入一些函数,会有一些很好玩的波形出来,下面给一些例子。
示例波形2
绘制波形需要一些GDI+的基础知识,并不难理解。掌握足够的GDI+知识之后还可以做出统计图之类的控件,根据输入的数据绘制折线图或者柱状图之类,和这里的波形图类似。这里贴几张我做的统计图控件绘制的图吧,虽然没法和Dundas之类的相比,但一般应用足够了。
折线图
折线图
柱状图
柱状图
横道图
横道图
饼图
饼图
下图是统计图中需要绘制的区域注释,实际绘图时根据数据分析,然后计算出相关的坐标就可以进行绘图了。
绘图注释
本文就到此结束了,下面是源代码下载,做的还不是很完善,有需要的朋友可以自行修改一下。
源代码下载:https://files.cnblogs.com/conexpress/ConExpress_MyCalculator_Wave.rar
原文地址:https://www.cnblogs.com/conexpress/p/MyCalculator_06.html