本系列文章介绍如何用C#实现一个类似于查询分析器的计算器。该计算器接受表达式输入,支持多行表达式,可选择部分表达式进行计算,能定位语法错误的位置,并且支持数值、字符串和逻辑值的计算,内置多种运算符和函数,并且可以根据需要扩展出更多的运算符和函数。程序中包含一些细节上的bug,有兴趣的朋友可以完善一下。
本篇介绍如何调用之前实现的词法分析和语法分析类以实现计算,以及如何在界面上实现多行表达式计算、选中部分表达式计算和错误定位。
代码下载:https://files.cnblogs.com/conexpress/ConExpress_MyCalculator.rar
前面几篇文章介绍了各种分析过程,本篇作为完结篇,介绍如何调用之前实现的代码,如何实现多行表达式或者选择部分表达式进行运算,以及如何定位错误。
本程序可以不需要UI界面,独立成一个模块。如果表达式分析与计算功能打包成一个dll,那入口只有一个,SyntaxAnalyse类。new一个SyntaxAnalyse类之后,调用其中的Analyse方法,将要计算的运算表达式作为参数传递进去,返回一个顶级TokenRecord对象,再根据返回的TokenRecord对象的值类型取得结果,整个计算过程就完成了,使用起来非常方便。
Code
/// <summary>
/// 表达式分析计算类,功能入口
/// </summary>
/// <remarks>Author:Alex Leo</remarks>
public class SyntaxAnalyse
{
/// <summary>
/// 构造函数
/// </summary>
/// <remarks>Author:Alex Leo; Date:2007-8-2</remarks>
public SyntaxAnalyse()
{ }
/// <summary>
/// 分析语句并返回记号记录对象
/// </summary>
/// <param name="Code">运算表达式</param>
/// <returns>顶级TokenRecord对象</returns>
public TokenRecord Analyse(string Code)
{
if (Code.Trim().Equals(string.Empty))
{
return new TokenValue(0,1);
}
List<TokenRecord> ListToken = new List<TokenRecord>();//TokenRecord列表
int intIndex = 0;
TokenFactory.LexicalAnalysis(ListToken, Code, ref intIndex);//词法分析,将代码转换为TokenRecord列表
//语法树分析,将Token列表按优先级转换为树
TokenRecord TokenTop = SyntaxTreeAnalyse.SyntaxTreeGetTopTokenAnalyse(ListToken, 0, ListToken.Count - 1);
TokenTop.Execute();
return TokenTop;
}
}
从代码中可以看出,首先是词法分析,得到一个记号对象列表List<TokenRecord>,然后进行语法分析,调用SyntaxTreeAnalyse的SnytaxTreeGetTopTokenAnalyse方法,分析出顶级记号对象,这样一棵树就出来了。接下来执行顶级节点的Execute方法,该方法中首先会执行下级节点的Execute方法,然后再针对下级节点的值执行自身的运算。所有的TokenRecord都是这样的模式,逐级递归调用,最后得到计算结果。TokenRecord基类中包含一个object类型的Value属性和一个Type类型的TokenValueType属性,通过这两个属性可以得到具体的值及其类型,然后做下一步处理。因为这里不只能执行数学运算,还能做字符串和逻辑值运算,所以必须通过TokenValueType来确定值的类型。如果只需要实现数学运算,程序会简单一些。
窗体的调用也很简单,并没有设计漂亮的外观和高级设置等。主要的代码是“计算”按钮的Click事件处理方法,代码如下:
Code
/// <summary>
/// 点击“计算”按钮
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnExecute_Click(object sender, EventArgs e)
{
if (this.rtbInput.Text.Trim().Replace("\n", "").Length == 0)
{
this.rtbOutput.Text = "输入的表达式不能为空,请重新输入。";
}
else
{
string strSource;
int intTotalIndex = 0;
this.rtbOutput.Text = "";
string[] strLines;
this.trvSyntaxTree.Nodes.Clear();//清空语法树
if (this.rtbInput.SelectedText.Trim().Length == 0)//获取选中的代码,如果未选中,则执行全部
{
strSource = this.rtbInput.Text;
}
else
{
strSource = this.rtbInput.SelectedText;
intTotalIndex = this.rtbInput.SelectionStart;
}
if (this.chkAllowMultiLine.Checked)//判断是按多行执行还是单行执行
{
strLines = strSource.Split(new char[] { '\n' });//多行则用换行符分割成多行
}
else
{
strLines = new string[] { strSource.Replace("\n", "") };//单行则移除换行符成一行
}
foreach (string Line in strLines)
{
if (Line.Trim().Length != 0)//避免中间出现空行
{
try
{
TokenRecord TokenTop = myAnalyse.Analyse(Line);//计算表达式
this.rtbOutput.Text += TokenTop.GetValueString() + "\n";//显示计算结果
this.LoadSyntaxTree(TokenTop);//加载语法树到TreeView控件
}
catch (Exception ex)
{
this.rtbOutput.Text += "发生错误\n" + ex.Message + "\n";//显示错误信息
if (ex is SyntaxException)//如果是语法错误,则选中错误的代码
{
SyntaxException myException = (SyntaxException)ex;
this.ActiveControl = this.rtbInput;//设置输入框为激活控件
this.rtbInput.Select(myException.Index + intTotalIndex, myException.Length);//定位发生错误的字符串
}
return;
}//try
}//if
intTotalIndex += Line.Length + 1;
}//foreach
}//else
}//btnExecute_Click
代码中包含详细的注释,这里做简要说明。未选中输入框中的文本则执行全部代码,否则执行选中部分的代码。将要执行的代码根据是否计算多行进行分解,存放在一个字符串数组中。然后对表达式数组循环计算。如此实现了选择部分表达式计算以及多行表达式计算。另外如何实现错误定位,则是通过捕获错误。程序中定义了一个Exception类,但进行词法分析和语法分析的时候,如果发生错误,则会抛出该异常。通过该异常类中的错误序号以及长度,就可以选中输入框中的错误部分。但是这里只能选中第一次发生的错误,不能像VS.NET的IDE一样捕获所有错误。Exception类的定义如下:
Code
/// <summary>
/// 语法错误类,用于发生错误时提示用户并选中错误的操作符
/// </summary>
/// <remarks>Author:Alex Leo; Date:2008-5-21;</remarks>
public class SyntaxException : Exception
{
private int m_Index;
/// <summary>
/// 错误列号
/// </summary>
/// <remarks>Author:Alex Leo; Date:2008-5-21;</remarks>
public int Index
{
get { return m_Index; }
}
private int m_Length;
/// <summary>
/// 错误操作符长度
/// </summary>
/// <remarks>Author:Alex Leo; Date:2008-5-21;</remarks>
public int Length
{
get { return m_Length; }
}
private string m_Message;
/// <summary>
/// 错误信息
/// </summary>
public override string Message
{
get { return m_Message; }
}
/// <summary>
/// 构造函数
/// </summary>
/// <param name="Index">错误处的列号(用于错误时确定错误操作符起始位置)</param>
/// <param name="Length">错误操作符长度(用于错误时选择错误操作符的长度)</param>
/// <param name="ErrorInformation">错误信息</param>
public SyntaxException(int Index, int Length, string ErrorInformation)
{
this.m_Index = Index;
this.m_Length = Length;
this.m_Message = ErrorInformation;
}
}
单行多行切换只需要设置窗体的AcceptButton属性为“计算按钮”即可,这样在单行状态下,用户回车就相当于点击“计算按钮”。而按“F5”键执行计算则是通过检测输入框的KeyUp事件,当释放“F5”键时用代码去执行“计算”按钮的Click操作实现计算。
另外这里有一个语法树分析,是为了显示语法树的结构,用更直观的方法来验证分析是否正确。树节点的文本是调用TokenRecord的ToString方法得到的,如果需要显示为其他信息,也可以自行修改。
到这里本系列文章就结束了,其中包含了一些编程的技巧,希望对看了本系列文章的各位有帮助。
代码下载:https://files.cnblogs.com/conexpress/ConExpress_MyCalculator.rar
Author:Alex Leo
Email:conexpress@qq.com
Blog:http://conexpress.cnblogs.com/