最近把计算器完善了一下,添加了变量的支持,添加了更多的函数,把逻辑短路操作也实现了,并修正了一些小错误。想起来以前在一本书里看到过一个示例,输入函数表达式,就可以绘制函数的波形。最开始学VB的时候,就喜欢用函数来画图。再加上对电子技术有点兴趣,很多波形都可以用函数来表示,很自然就想到用程序来模拟示波器显示波形。但是因为函数都需要在代码里面写死,如果需要新增函数或者进行修改,需要修改程序代码再编译运行。既然现在可以做到对表达式进行计算,也可以支持变量,那么让变量的值变化就可以计算得到不同的值,再把这些值组合成坐标点,连接起来就成了波形。于是乎,咱也试试做一个显示函数波形的小程序玩玩,效果如下:
先说说新添加的变量支持功能。这里的变量并不需要声明,只要不是保留的关键字,程序就把它作为变量。在以前的版本中遇到不认识的字符串会报错,现在是在分析关键字的时候做了特殊处理,遇到非关键字字符串则添加到一个静态的变量字典中。变量字典的Key是该变量的字符串表示,Value是一个TokenValue对象。在添加到字典之后,如果再遇到相同的字符串,则返回变量字典中对应的TokenValue对象。下面给个例子:
从例子可以看出,在未赋值之前,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
/// <summary>
/// 绘图信息
/// </summary>
public class DrawInfo
{
#region 字段和属性声明
private const string CategoryName = "绘图信息";
private string m_Name = "未命名项";
[Category(CategoryName), DisplayName("名称"), DefaultValue("未命名项"), MergableProperty(false), Description("绘图信息的名称。")]
public string Name
{
get { return m_Name; }
set
{
if (m_Name != value && value.Trim().Length > 0)
m_Name = value;
}
}
private Color m_LineColor = Color.Black;
[Category(CategoryName), DisplayName("线条颜色"), DefaultValue(typeof(Color), "Black"), Description("绘制线条的颜色。")]
public Color LineColor
{
get { return m_LineColor; }
set { m_LineColor = value; }
}
private float m_LineWidth = 1.0f;
[Category(CategoryName), DisplayName("线条宽度"), DefaultValue(1.0f), Description("绘制线条的宽度(以像素为单位)。")]
public float LineWidth
{
get { return m_LineWidth; }
set { m_LineWidth = value; }
}
private string m_ExpressionX = "n";
[Category(CategoryName), DisplayName("表达式X"), DefaultValue("n"), Description("对应坐标轴X的表达式。")]
[Editor("System.ComponentModel.Design.MultilineStringEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))]
public string ExpressionX
{
get { return m_ExpressionX; }
set
{
if (m_ExpressionX != value && value.Trim().Length > 0)
m_ExpressionX = value.Trim();
}
}
private string m_ExpressionY = "n";
[Category(CategoryName), DisplayName("表达式Y"), DefaultValue("n"), Description("对应坐标轴Y的表达式。")]
[Editor("System.ComponentModel.Design.MultilineStringEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))]
public string ExpressionY
{
get { return m_ExpressionY; }
set
{
if (m_ExpressionY != value && value.Trim().Length > 0)
m_ExpressionY = value.Trim();
}
}
private List<PointF> m_PointList = new List<PointF>();
/// <summary>
/// 坐标列表
/// </summary>
[Browsable(false)]
public List<PointF> PointList
{
get { return m_PointList; }
}
#endregion 字段和属性声明
/// <summary>
/// 构造函数
/// </summary>
public DrawInfo()
{ }
private TokenRecord m_TokenX;
private TokenRecord m_TokenY;
/// <summary>
/// 初始化记号对象
/// </summary>
/// <param name="Analyser">表达式分析计算类的实例</param>
public void InitialToken(SyntaxAnalyse Analyser)
{
if (Analyser != null)
{
m_TokenX = Analyser.Analyse(m_ExpressionX.ToLower());
m_TokenY = Analyser.Analyse(m_ExpressionY.ToLower());
this.m_PointList.Clear();
}
}
/// <summary>
/// 执行计算
/// </summary>
public void Execute()
{
m_TokenX.Execute();
m_TokenY.Execute();
m_PointList.Add(new PointF(Convert.ToSingle(m_TokenX.TokenValue), Convert.ToSingle(m_TokenY.TokenValue)));
}
public override string ToString()
{
return m_Name;
}
}//class DrawInfo
在界面上添加相关控件,用来操作绘图信息。点击绘图按钮之后,按照界面上的PictureBox的尺寸创建一个Bitmap对象,然后把它作为参数调用绘图代码,代码如下:
Code
private void Draw(Bitmap myImage)
{
try
{
Graphics g = Graphics.FromImage(myImage);
SyntaxAnalyse.DicVariable.Clear();
int intMin = this.numMin.Value < this.numMax.Value ? (int)this.numMin.Value : (int)this.numMax.Value;
int intMax = this.numMin.Value < this.numMax.Value ? (int)this.numMax.Value : (int)this.numMin.Value;
TokenRecord TokenN = m_Analyse.Analyse("n");
TokenN.TokenValueType = typeof(double);
TokenN.TokenValue = intMin;
//初始化
foreach (DrawInfo item in this.m_DrawInfoList)
{
item.InitialToken(m_Analyse);
}
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
//绘制X轴和Y轴
g.DrawLine(Pens.Black, 0, this.picImage.Height / 2, this.picImage.Width, this.picImage.Height / 2);
g.DrawLine(Pens.Black, this.picImage.Width / 2, 0, this.picImage.Width / 2, this.picImage.Height);
g.TranslateTransform(Convert.ToSingle(myImage.Width / 2), Convert.ToSingle(myImage.Height / 2));
//计算表达式
for (int intIndex = intMin; intIndex <= intMax; intIndex++)
{
TokenN.TokenValue = (double)intIndex;
foreach (DrawInfo item in this.m_DrawInfoList)
{
item.Execute();
}
}
//绘制图像
foreach (DrawInfo item in this.m_DrawInfoList)
{
g.DrawLines(new Pen(item.LineColor, item.LineWidth), item.PointList.ToArray());
}
myImage.RotateFlip(RotateFlipType.Rotate180FlipX);
//绘制刻度
SolidBrush myBrush = new SolidBrush(Color.Black);
for (int intX = 0; intX < myImage.Width / 2; intX += (int)(this.numScale.Value))
{
g.DrawLine(Pens.Black, intX, 0, intX, -3);
g.DrawString(intX.ToString(), this.Font, myBrush, intX + 1, 1);
if (intX == 0)
continue;
g.DrawLine(Pens.Black, -intX, 0, -intX, -3);
g.DrawString("-" + intX.ToString(), this.Font, myBrush, -intX + 1, 1);
}
for (int intY = 0; intY < myImage.Height / 2; intY += (int)this.numScale.Value)
{
g.DrawLine(Pens.Black, 0, -intY, 3, -intY);
g.DrawString(intY.ToString(), this.Font, myBrush, 1, -intY + 1);
if (intY == 0)
continue;
g.DrawLine(Pens.Black, 0, intY, 3, intY);
g.DrawString("-" + intY.ToString(), this.Font, myBrush, 1, intY + 1);
}
//绘制图示Legend
g.TranslateTransform(Convert.ToSingle(myImage.Width / 2 * -1), Convert.ToSingle(myImage.Height / 2 * -1));
int intOffsetX = 10;
int intOffsetY = 10;
int intLegendHeight = (int)g.MeasureString("123",this.Font).Height;
foreach (DrawInfo item in m_DrawInfoList)
{
using (SolidBrush LegendBrush = new SolidBrush(item.LineColor))
{
g.FillRectangle(LegendBrush, intOffsetX, intOffsetY, 30, intLegendHeight);
g.DrawString(item.Name, this.Font, LegendBrush, intOffsetX + 30 + 5, intOffsetY);
intOffsetY += intLegendHeight + 5;
}
}
}
catch (Exception ex)
{
MessageBox.Show("错误信息为:" + ex.Message, "运算发生错误", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
}
这里的代码还有不少可以改进的地方,比如可以设置图片尺寸、图片背景、坐标原点、背景网格,甚至可以让波形一段一段慢慢的显示出来,更好的了解波形的绘制过程。如果有需要可以自行完善。
本文开头给出的示例的各个设置如下表:
名称
|
表达式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)。通过声明变量的方法可以很容易让波形之间关联起来,也可以减少计算量。
有时候胡乱输入一些函数,会有一些很好玩的波形出来,下面给一些例子。
绘制波形需要一些GDI+的基础知识,并不难理解。掌握足够的GDI+知识之后还可以做出统计图之类的控件,根据输入的数据绘制折线图或者柱状图之类,和这里的波形图类似。这里贴几张我做的统计图控件绘制的图吧,虽然没法和Dundas之类的相比,但一般应用足够了。
折线图
柱状图
横道图
饼图
下图是统计图中需要绘制的区域注释,实际绘图时根据数据分析,然后计算出相关的坐标就可以进行绘图了。
Author:Alex Leo
Email:conexpress@qq.com
Blog:http://conexpress.cnblogs.com/