本文在之前實(shí)現(xiàn)的計(jì)算器的基礎(chǔ)上進(jìn)行完善,添加了對(duì)變量的支持,新增了部分函數(shù),并實(shí)現(xiàn)了根據(jù)輸入的函數(shù)繪制波形的功能,支持多個(gè)波形的繪制,且多個(gè)波形之間可以有關(guān)聯(lián)。程序中還有一些可以改進(jìn)的地方,有需要的朋友可以自行完善。
最近把計(jì)算器完善了一下,添加了變量的支持,添加了更多的函數(shù),把邏輯短路操作也實(shí)現(xiàn)了,并修正了一些小錯(cuò)誤。想起來(lái)以前在一本書里看到過一個(gè)示例,輸入函數(shù)表達(dá)式,就可以繪制函數(shù)的波形。最開始學(xué)VB的時(shí)候,就喜歡用函數(shù)來(lái)畫圖。再加上對(duì)電子技術(shù)有點(diǎn)興趣,很多波形都可以用函數(shù)來(lái)表示,很自然就想到用程序來(lái)模擬示波器顯示波形。但是因?yàn)楹瘮?shù)都需要在代碼里面寫死,如果需要新增函數(shù)或者進(jìn)行修改,需要修改程序代碼再編譯運(yùn)行。既然現(xiàn)在可以做到對(duì)表達(dá)式進(jìn)行計(jì)算,也可以支持變量,那么讓變量的值變化就可以計(jì)算得到不同的值,再把這些值組合成坐標(biāo)點(diǎn),連接起來(lái)就成了波形。于是乎,咱也試試做一個(gè)顯示函數(shù)波形的小程序玩玩,效果如下:

先說(shuō)說(shuō)新添加的變量支持功能。這里的變量并不需要聲明,只要不是保留的關(guān)鍵字,程序就把它作為變量。在以前的版本中遇到不認(rèn)識(shí)的字符串會(huì)報(bào)錯(cuò),現(xiàn)在是在分析關(guān)鍵字的時(shí)候做了特殊處理,遇到非關(guān)鍵字字符串則添加到一個(gè)靜態(tài)的變量字典中。變量字典的Key是該變量的字符串表示,Value是一個(gè)TokenValue對(duì)象。在添加到字典之后,如果再遇到相同的字符串,則返回變量字典中對(duì)應(yīng)的TokenValue對(duì)象。下面給個(gè)例子:

從例子可以看出,在未賦值之前,n的值為空,和其他值運(yùn)算不會(huì)發(fā)生錯(cuò)誤。下面是語(yǔ)法樹分析的圖:

從圖上可以看出變量n是引用的,在第一句中n的值是空,類型為未初始化類型,但是在PropertyGrid中顯示的信息是最后一次賦值的結(jié)果。而且這里把賦值操作符"="作為賦值操作的根節(jié)點(diǎn),并沒有像左括號(hào)"("一樣處理。比如最后一個(gè)表達(dá)式sin(n+20)的語(yǔ)法樹中,TokenSin的下級(jí)是TokenPlus,而不是TokenLeftBracket。對(duì)于賦值操作符"="之所以這保留了原始結(jié)構(gòu),是因?yàn)檫@樣可以在修改下級(jí)節(jié)點(diǎn)的值之后繼續(xù)調(diào)用Execute方法進(jìn)行計(jì)算,否則如果把值直接指定給變量,下次調(diào)用Execute的時(shí)候就沒法執(zhí)行了。左括號(hào)只是分割表達(dá)式,但賦值操作符是有真正的運(yùn)算過程,所以必須用不一樣的分析方法。這一點(diǎn)對(duì)于下面要實(shí)現(xiàn)的函數(shù)波形非常重要。在繪制波形的時(shí)候需要改變變量,如果采用變量字符串替換的方法,每次都需要分析表達(dá)式,而變量的值域可能很大,這樣會(huì)把大量時(shí)間消耗在分析上。但是如果能保留完整的語(yǔ)法樹,只需要將變量對(duì)應(yīng)的TokenRecord的值改變,再次調(diào)用頂級(jí)節(jié)點(diǎn)的Execute方法,這時(shí)候只需要逐級(jí)向下調(diào)用計(jì)算方法即可,不需要重新分析表達(dá)式了。
接下來(lái)就介紹怎么實(shí)現(xiàn)函數(shù)波形繪制的吧。首先這里引入了一個(gè)變量n,在進(jìn)行計(jì)算之前在程序里面進(jìn)行初始化,然后根據(jù)設(shè)置的范圍用for遞增。定義兩個(gè)表達(dá)式X和Y,分別對(duì)應(yīng)坐標(biāo)點(diǎn)的X和Y,這兩個(gè)表達(dá)式中包含n,在對(duì)n進(jìn)行遞增之前調(diào)用語(yǔ)法分析類進(jìn)行分析,得到頂級(jí)節(jié)點(diǎn),這時(shí)候語(yǔ)法樹已經(jīng)分析完成了。在對(duì)n進(jìn)行遞增的時(shí)候,計(jì)算X和Y,形成一系列坐標(biāo)點(diǎn)。調(diào)用Graphics類的DrawLines方法,把計(jì)算得到的一系列坐標(biāo)點(diǎn)作為參數(shù)傳遞給該方法,這樣就可以看到特定的波形。
比如阿基米德螺旋線用偽代碼表示如下:
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的表達(dá)式可以由用戶輸入,這樣就不需要修改程序再編譯才能顯示要繪制的波形圖了。
為了同時(shí)支持多個(gè)波形圖,這里用一個(gè)類來(lái)記錄一個(gè)函數(shù)對(duì),以及線條顏色、線條寬度等信息。該類的代碼如下:

Code
/// <summary>
/// 繪圖信息
/// </summary>
public class DrawInfo
{
#region 字段和屬性聲明
private const string CategoryName = "繪圖信息";
private string m_Name = "未命名項(xiàng)";
[Category(CategoryName), DisplayName("名稱"), DefaultValue("未命名項(xiàng)"), 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("表達(dá)式X"), DefaultValue("n"), Description("對(duì)應(yīng)坐標(biāo)軸X的表達(dá)式。")]
[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("表達(dá)式Y(jié)"), DefaultValue("n"), Description("對(duì)應(yīng)坐標(biāo)軸Y的表達(dá)式。")]
[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>
/// 坐標(biāo)列表
/// </summary>
[Browsable(false)]
public List<PointF> PointList
{
get { return m_PointList; }
}
#endregion 字段和屬性聲明
/// <summary>
/// 構(gòu)造函數(shù)
/// </summary>
public DrawInfo()
{ }
private TokenRecord m_TokenX;
private TokenRecord m_TokenY;
/// <summary>
/// 初始化記號(hào)對(duì)象
/// </summary>
/// <param name="Analyser">表達(dá)式分析計(jì)算類的實(shí)例</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>
/// 執(zhí)行計(jì)算
/// </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
在界面上添加相關(guān)控件,用來(lái)操作繪圖信息。點(diǎn)擊繪圖按鈕之后,按照界面上的PictureBox的尺寸創(chuàng)建一個(gè)Bitmap對(duì)象,然后把它作為參數(shù)調(diào)用繪圖代碼,代碼如下:

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));
//計(jì)算表達(dá)式
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("錯(cuò)誤信息為:" + ex.Message, "運(yùn)算發(fā)生錯(cuò)誤", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
}
這里的代碼還有不少可以改進(jìn)的地方,比如可以設(shè)置圖片尺寸、圖片背景、坐標(biāo)原點(diǎn)、背景網(wǎng)格,甚至可以讓波形一段一段慢慢的顯示出來(lái),更好的了解波形的繪制過程。如果有需要可以自行完善。
本文開頭給出的示例的各個(gè)設(shè)置如下表:
|
名稱
|
表達(dá)式X
|
表達(dá)式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是模擬三相電的波形,都是標(biāo)準(zhǔn)正弦波,只是相位差120度。這里用一個(gè)賦值操作聲明了三個(gè)變量a, b, c,這樣在三相整流波形中就可以直接操作這三個(gè)變量了,所以三相整流波形的表達(dá)式Y(jié)的值是abs(a)+abs(b)+abs(c)。通過聲明變量的方法可以很容易讓波形之間關(guān)聯(lián)起來(lái),也可以減少計(jì)算量。
有時(shí)候胡亂輸入一些函數(shù),會(huì)有一些很好玩的波形出來(lái),下面給一些例子。

繪制波形需要一些GDI+的基礎(chǔ)知識(shí),并不難理解。掌握足夠的GDI+知識(shí)之后還可以做出統(tǒng)計(jì)圖之類的控件,根據(jù)輸入的數(shù)據(jù)繪制折線圖或者柱狀圖之類,和這里的波形圖類似。這里貼幾張我做的統(tǒng)計(jì)圖控件繪制的圖吧,雖然沒法和Dundas之類的相比,但一般應(yīng)用足夠了。
折線圖

柱狀圖

橫道圖

餅圖

下圖是統(tǒng)計(jì)圖中需要繪制的區(qū)域注釋,實(shí)際繪圖時(shí)根據(jù)數(shù)據(jù)分析,然后計(jì)算出相關(guān)的坐標(biāo)就可以進(jìn)行繪圖了。
Author:Alex Leo
Email:conexpress@qq.com
Blog:http://conexpress.cnblogs.com/