仿查询分析器的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
    /// <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, 0this.picImage.Height / 2this.picImage.Width, this.picImage.Height / 2);
                g.DrawLine(Pens.Black, 
this.picImage.Width / 20this.picImage.Width / 2this.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 + 11);
                    
if (intX == 0)
                        
continue;
                    g.DrawLine(Pens.Black, 
-intX, 0-intX, -3);
                    g.DrawString(
"-" + intX.ToString(), this.Font, myBrush, -intX + 11);
                }

                
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)。通过声明变量的方法可以很容易让波形之间关联起来,也可以减少计算量。
有时候胡乱输入一些函数,会有一些很好玩的波形出来,下面给一些例子。
示例波形2
绘制波形需要一些GDI+的基础知识,并不难理解。掌握足够的GDI+知识之后还可以做出统计图之类的控件,根据输入的数据绘制折线图或者柱状图之类,和这里的波形图类似。这里贴几张我做的统计图控件绘制的图吧,虽然没法和Dundas之类的相比,但一般应用足够了。
折线图
折线图
柱状图
柱状图
横道图
横道图
饼图
饼图
下图是统计图中需要绘制的区域注释,实际绘图时根据数据分析,然后计算出相关的坐标就可以进行绘图了。
绘图注释
本文就到此结束了,下面是源代码下载,做的还不是很完善,有需要的朋友可以自行修改一下。
源代码下载: http://files.cnblogs.com/conexpress/ConExpress_MyCalculator_Wave.rar

转载于:https://www.cnblogs.com/conexpress/archive/2009/06/29/MyCalculator_06.html

猜你喜欢

转载自blog.csdn.net/weixin_34226182/article/details/93352523
今日推荐