Visual Calc v3.0 - 桌面计算器的新维度






3.62/5 (113投票s)
如何开始编写解析器。
完整的VisualCalc计算器项目(包含GUI + 解析器)
VisualCalc解析器项目(仅计算引擎)
最新更新
- 将解析器分离到自己的DLL中。
- 项目迁移到 Microsoft Visual C++ .NET 2003。
目录
引言
我最初的想法是创建一个解析器来为某些文本着色。由于我不知道如何认真开始,我记得Bjarne Stroustrup的《C++语言》中的一个例子。该示例是一个简单的计算器,它从标准输入流(控制台模式)获取字符,然后解析它们以进行数学表达式计算。我重新实现了它,改进了它,这就是我所做的。请享用...
解析是编程中一个有趣的部分,因为我们总是需要分析一个流来猜测用户希望程序做什么。在“图形窗口”中,这得到了很好的简化,因为通常不需要在输入流上使用完整的句子,例如命令行。无论如何,这就是我的计算器所做的,我将尽力解释其原理。
首先,我将介绍解析器、其成员、其函数以及它们如何相互作用。然后,我将介绍我的图形界面,最后,我将介绍如何在您自己的代码或至少在您的项目中使用解析器。
VisualCalc 最初是用 Microsoft Visual C++ 6 构建的。由于这个编译器已经严重老化,我不得不做出艰难的决定,迁移到 Visual Studio 的新版本,尽管我知道很多用户仍然在使用它。我将尽力支持那些除了 VC6 之外没有其他编译器的用户使用 VisualCalc。如果您只对在代码中使用解析器感兴趣,那么在 Visual C++ 6 下编译它仍然不会有任何问题;也许未来的版本会有问题...
免责声明
VisualCalc 项目越来越大,我仍然有很多想法来改进它。不幸的是,我只在空闲时间进行工作,所以更新以合理的速度进行。然而,自2004年8月首次私下发布以来,我一直在认真工作,我请求您有兴趣地使用我的作品。这就是我为使用它设置一些条件的原因。
This code may be used in compiled form in any way you desire (including commercial use). The code may be redistributed unmodified by any means providing it is not sold for profit without the author's written consent, and providing that this notice and the author's name and all copyright notices remain intact. However, this file and the accompanying source code may not be hosted on a website or bulletin board without the author's written permission. This software is provided "as is" without express or implied warranty. Use it at your own risk! Whilst I have made every effort to remove any undesirable "features", I cannot be held responsible if it causes any damage or loss of time or data.
希望这不算过分要求,考虑到投入的工作量。如果您确实在商业应用程序中使用它,那么请给我发送一封电子邮件,让我知道。
I] 解析器(版本3.0)
为了符合 MVC(模型-视图-控制器)架构,我在解析器 2.3 版本中改进的最重要一点是将解析器方法从主对话框类中分离出来,并创建自己的类。基本上,MVC 架构要求应用程序的主要层(业务对象、后台处理和图形显示)是分离的。现在,解析器不仅存在于自己的类中,而且在物理上也被分离,因为它保留在自己的 DLL 项目中。解析器代码已为 Doxygen 文档工具添加了注释,生成的 HTML 文档可以从本页下载(请参阅文章顶部的下载部分)。
解析器在以下文件中实现
VCalcParser.h: | 解析器定义 |
VCalcParser.cpp: | 解析器实现 |
VCalcParserExceptions.h: | 异常定义 |
VCalcParserExceptions.cpp: | 异常实现 |
VCalcParserTypes.h: | 解析器和用户界面使用的类型 |
以下是CVCalcParser
类的定义
#include "VCalcParserTypes.h" class CVCalcParser { private: /****************** Mathematic constants ******************/ const VALUES_TYPE m_PI; // 3.1415926535897932384626433832795 const VALUES_TYPE m_E; // 2.7182818284590452353602874713527 /************* Members for parsing treatment **************/ // Token extracted from the input stream TokenValue m_CurrentToken; // Reference to the input stream std::string m_Source; // Identifier extracted as an IDENTIFIER token std::string m_strIdentifierValue; // Describe message of the warning std::string m_strWarningMsg; // Number extracted as a NUMBER token VALUES_TYPE m_valNumberValue; // Non-interrupting low level message bool m_bWarningFlag; // The end of the input stream is reached bool m_bEndEncountered; // Index in the input stream int m_iCurrentIndex; // Supported functions std::list<std::string> m_lstFunctions; // User defined variables table mapping the // identifier with a value std::map<std::string, VALUES_TYPE> m_mapVariables; // Answers History mapping a formula with a result std::deque<AnswerItem> m_dqeAnswersHistory; /***** Locally defined/redefined mathematic functions *****/ VALUES_TYPE ffactor (VALUES_TYPE); VALUES_TYPE nCp (VALUES_TYPE, VALUES_TYPE); VALUES_TYPE nAp (VALUES_TYPE, VALUES_TYPE); AnswerItem Ans (VALUES_TYPE valIndex); VALUES_TYPE abs (VALUES_TYPE); VALUES_TYPE cos (VALUES_TYPE); VALUES_TYPE sin (VALUES_TYPE); VALUES_TYPE tan (VALUES_TYPE); VALUES_TYPE cosh (VALUES_TYPE); VALUES_TYPE sinh (VALUES_TYPE); VALUES_TYPE tanh (VALUES_TYPE); VALUES_TYPE Acos (VALUES_TYPE); VALUES_TYPE Asin (VALUES_TYPE); VALUES_TYPE Atan (VALUES_TYPE); VALUES_TYPE deg (VALUES_TYPE); VALUES_TYPE rad (VALUES_TYPE); VALUES_TYPE exp (VALUES_TYPE); VALUES_TYPE ln (VALUES_TYPE); VALUES_TYPE log (VALUES_TYPE); VALUES_TYPE logn (VALUES_TYPE, VALUES_TYPE); VALUES_TYPE sqrt (VALUES_TYPE); VALUES_TYPE sqrtn (VALUES_TYPE, VALUES_TYPE); VALUES_TYPE pow (VALUES_TYPE, VALUES_TYPE); VALUES_TYPE mod (VALUES_TYPE, VALUES_TYPE); VALUES_TYPE sum (std::string expr, std::string var, VALUES_TYPE low, VALUES_TYPE high); VALUES_TYPE product (std::string expr, std::string var, VALUES_TYPE low, VALUES_TYPE high); public: /************* Public interface of the Parser *************/ CVCalcParser(); virtual ~CVCalcParser(); void ResetParserMembers(const std::string); void ResetFunctions(); void ResetVariables(); void ResetAnswersHistory(); const std::list<std::string>& GetFunctions(); const std::map<std::string, VALUES_TYPE>& GetVariables(); const std::deque<AnswerItem>& GetAnswersHistory(); bool HasWarning(); std::string GetWarningMsg(); /** Only function to call to start a calculation process **/ VALUES_TYPE Evaluate(const std::string& Source) throw(CVCalcParserException); private: // Parsing functions following (in the // recursive-descending order)... VALUES_TYPE Level_1 (void); // + , - VALUES_TYPE Level_2 (void); // * , / VALUES_TYPE Level_3 (void); // ^ VALUES_TYPE Level_4 (void); // % VALUES_TYPE Level_5 (void); // ! , ° VALUES_TYPE Primary (void); // ( ) , Unary + , Unary - TokenValue GetToken (void); bool StepIndexForward(void); };
解析器使用纯标准C++,以便可以在任何平台上使用。VCalcParserTypes
定义了多种类型,稍后将在段落中介绍。其中最重要的是VALUES_TYPE
// Type for the handled operands and results typedef long double VALUES_TYPE; // But why not this: //typedef CMyBigDecimalType VALUES_TYPE;
解析器处理并返回结果的值目前是long double
。我选择使用别名,这样我就可以更容易地将类型更改为可以处理大量数字的类型。此外,我正在并行开发自己的“大十进制”数据类型,它将能够存储比原生类型更大的浮点数。这部分的困难在于重新创建对这些十进制数进行操作的数学函数。这也是我重载数学函数(cos、sqrt、log等)的原因,这样可以在使用的数据类型之间轻松切换。
成员
解析器成员的设置允许不同函数之间进行通信。以下是它们的用途
const VALUES_TYPE m_PI;
此成员存储常数pi的值,精度为32位。
const VALUES_TYPE m_E;
此成员存储常数e的值,精度同样为32位。
TokenValue m_CurrentToken;
存储一个令牌,该令牌将由较低级别的
GetToken()
函数切换。TokenValue
是在VCalcParserTypes.h中定义的类型,如下所示// Type for recognized separation tokens typedef enum { TV_NUMBER, TV_IDENTIFIER, TV_END, TV_SEQ = ',', TV_PLUS = '+', TV_MINUS = '-', TV_MUL = '*', TV_DIV = '/', TV_POW = '^', TV_MOD = '%', TV_FACT = '!', TV_DEG = '°', TV_LP = '(', TV_RP = ')', TV_ASSIGN = '=' } TokenValue;
TokenValue
类型允许GetToken()
函数将从输入流中读取的令牌返回给调用函数。当计算器中有新的运算符时,它会相应地增长。std::string m_Source;
包含整个输入字符串。这是将被
GetToken()
读取并解析的有效流。std::string m_strIdentifierValue;
包含有效标识符的名称。无论用户设置新变量、修改变量还是仅调用变量,变量名称的字符串都将存储在此处。此成员还包含用户调用的函数名称。
std::string m_strWarningMsg;
此成员存储一个非阻塞警告描述。它可以由用户界面打印或忽略。我的界面在输出结果后直接打印它(例如,0^0会发出警告,然后被替换为1)。
VALUES_TYPE m_valNumberValue;
当从输入流中读取到有效数字时,存储该数字(整数或浮点数)的值。
bool m_bWarningFlag;
此标志告诉运行解析器的函数,结果是有效/可打印的,但可能会添加一条消息来通知用户他进行了一项可疑的操作。
bool m_bEndEncountered;
这个由
GetToken()
使用的标志并非绝对必要,但相当有用。它表明所有输入流都已读取完毕(相当于文件的eof()
)。int m_iCurrentIndex;
计数器变量,指示流中的读取位置。它可能在 0 到“输入字符串长度”-1 之间。
std::list<std::string> m_lstFunctions;
此成员存储为用户设计的函数列表。
std::map<std::string, VALUES_TYPE> m_mapVariables;
这是用户设置的变量与其值关联的表格。
std::deque<AnswerItem> m_dqeAnswersHistory;
此列表包含一组最新的
AnswerItem
。AnswerItem
是在VCalcParserTypes.h中定义的类型,如下所示// Type for an entry in the answers history typedef struct { std::string m_strFormula; VALUES_TYPE m_valResult; } AnswerItem;
它将用户输入的公式与其答案的实际结果关联起来。
函数
正如我们在类定义中看到的,解析器由七个private
递归下降函数加上一个用于用户界面启动计算的public
函数组成。调用树被称为“递归下降”,因为每个函数都与一个运算符级别相关联;一个函数通常由紧邻的较低级别函数调用,并自身调用紧邻的较高级别函数。调用关系如下图所示。在某些特殊情况下,一个函数可以调用自身或层次结构中级别较低的另一个函数
图1:常规解析器函数调用层次结构。
我们知道,数学运算符具有优先级(例如,基本知识告诉我们乘法优先于加法)。我们还知道使用编程语言处理运算符级别的另一种方法。因此,我必须解析用户输入的内容,并考虑优先级。这部分通过函数的递归性得到了隐式改进。例如,此计算器中开发的运算符按优先级降序排列。分组的运算符具有相同的级别
| ||||||||||||||||||||||||||||||||||||||
图2:支持的运算符级别 |
让我们以运算符=
的优先级为例。由于它几乎是最低级别,它将右侧表达式的结果赋值给变量(左操作数)。请注意这一点。
图3:请注意运算符优先级。
下面介绍的所有函数都以相同的方式运行。首先,它们调用高级别函数并返回其结果。然后,在switch
语句中测试m_CurrentToken
。如果函数识别出该令牌,则执行关联的操作;否则,它返回到低级别函数。
VALUES_TYPE Evaluate(const std::string& Source) throw(CVCalcParserException);
这是用户界面可以调用的唯一
public
函数,用于解析数学表达式。它为新的计算初始化解析器成员。它还确保将最后一个答案(公式 + 结果)推送到答案历史记录deque
的前面。VALUES_TYPE Level_1(void) throw(CVCalcParserException);
此函数是最低级别的函数。它通过将其左操作数加/减到
Level_2()
返回的值来管理加法和减法操作。它还处理用户尝试对字面量使用赋值运算符的情况。VALUES_TYPE Level_2(void) throw(CVCalcParserException);
此函数管理乘法和除法。它将其左操作数与
Level_2()
返回的值相乘/相除(我们可以在这里看到递归调用)。如果除法的被除数设置为0,则会抛出异常。VALUES_TYPE Level_3(void) throw(CVCalcParserException);
此函数管理幂运算。它将其左操作数提升到
Level_4()
返回值的幂。如果用户请求0^0,则会发出警告,提示它被替换为1。在这种情况下,我希望向用户发出警告,告诉他他向计算器请求了一个特殊操作。对于当前版本2.24的VisualCalc,以下情况仍未处理:- 无限^0 被 1 替换,
- 未定义^0 被 1 替换,
- 1^无限 被 1 替换,
- 1^未定义 被 1 替换。
通常,正负无穷大和 NaN 情况尚不受支持。
VALUES_TYPE Level_4(void) throw(CVCalcParserException);
此函数管理模运算。它返回其左操作数除以
Level_5()
返回值的模数。VALUES_TYPE Level_5(void) throw(CVCalcParserException);
此函数管理解析器版本2.1目前实现的两个后缀一元运算符:
!
和°
运算符。!
,返回其左操作数的阶乘。例如,5! = 5 x 4 x 3 x 2 x 1。°
,是一个将度数转换为弧度的运算符。它主要用于需要弧度作为参数的三角函数,是rad()
的快捷方式。例如:cos(45°) = cos(rad(45)) = cos(pi/2)。
VALUES_TYPE Primary(void) throw(CVCalcParserException);
解析器中最大的函数之一。由于它接近最高级函数,其工作变得繁重。一个重要的注意事项是,它不管理任何需要左右操作数的运算符。它调用更高级别的函数(
get_token()
)但不会获取返回的值作为左操作数。它是switch
语句的组合,测试以下m_CurrentToken
可能的值TV_NUMBER
返回从输入字符串中提取的字面量。
TV_IDENTIFIER
它首先尝试在函数列表中识别标识符。如果识别成功,会执行一些测试以检查用户是否正确调用函数(括号,参数数量是否正确 - 取决于调用的函数;不将函数名称用作变量)。然后,它调用关联的函数并返回其值。
其次,标识符可能是一个变量名(已存在,或正在创建)。如果存在这样的变量(已赋值),它将值添加到变量表中。如果标识符被赋值(下一个标记是
=
运算符),它会测试用户是否试图赋值一个常量(pi
或e
),然后赋值给变量或返回错误。如果用户只是回忆之前存储在标识符中的值,则将该值返回给低级别调用函数。如果未找到变量,则抛出异常。也不允许隐式地将变量与表达式相乘。例如,以下情况会抛出异常:2x
而不是2 * x
。foo(3!)
而不是foo * (3!)
。
VisualCalc中用户可用的函数在帮助对话框的“函数”选项卡中详细说明。
TV_PLUS
/TV_MINUS
返回带有相关符号的下一个
Primary()
。TV_LP
当发现左括号开始一个全新的表达式(而不是函数参数列表)时,将返回此标记。调用
Level_1()
来评估表达式,直到在输入流中找到右括号,否则将抛出异常。
TokenValue GetToken(void) throw(CVCalcParserException);
它是解析器最重要的函数,也是最高级别的函数。它逐个读取输入流中的字符,忽略空白符。我们可以在有效字符之间输入空格/制表符,而不会改变所输入表达式的含义。然后,常规的
switch
语句测试以下情况:*
,/
,+
,-
,^
,%
,!
,°
,(
,)
,=
,,
返回与提取字符相关的令牌。读取的令牌也存储在
m_CurrentToken
中。.
,1
,2
,3
,4
,5
,6
,7
,8
,9
,0
如果后面的字符构成一个有效的整数/浮点数,这些字符可能会导致
GetToken()
返回一个常量数字。如果在同一个数字中发现两个点('.
'),则会抛出相关异常。该数字存储在m_valNumberValue
中。_
a
,b
,c
,d
,e
,f
,g
,h
,i
,j
,k
,l
,m
,n
,o
,p
,q
,r
,s
,t
,u
,v
,w
,x
,y
,z
A
,B
,C
,D
,E
,F
,G
,H
,I
,J
,K
,L
,M
,N
,O
,P
,Q
,R
,S
,T
,U
,V
,W
,X
,Y
,Z
如果在流中找到其中一个字符,则提取并存储后续标识符到
m_strIdentifierValue
中。
bool StepIndexForward(void) throw();
此函数是为了将
GetToken()
多次调用的代码片段进行因子分解而创建的。其唯一目的是增加用于读取待解析字符串的索引,然后通过设置m_bEndEncountered
标志来告知字符串末尾已到达。
异常
由于在解析器处理过程中任何地方、任何时候都可能发生错误,因此异常是必须使用的语言功能。解析器可以抛出七种类型的异常,每种异常都继承自CVCalcParserException
。在下面的图中,绿色和蓝色类是abstract
的,这意味着它们不能在代码中直接实例化。
图4:解析器异常层次结构。
CVCalcParserException
实际上提供了在捕获异常时使用的public
成员函数(GetExceptionNumber()
、GetMessage()
、GetErrorPos()
)。然后,每个异常都有自己的类,该类必然继承自七个异常组之一(CSyntaxException
、CMathematicException
、CFunctionException
、CParameterException
、CVariableException
、CDomainException
、CParserException
)。
II] 用户界面
计算器
解析器本身不编辑图形界面的控件。对话框调用返回当前状态列表的public
函数。
这里是四个主要区域
- 输入和结果编辑框。
这是用户编写待解析表达式和显示结果的地方。当要显示错误/警告消息时,它会打印在结果字段中。
- 最近的答案/公式列表框。
当 VisualCalc 返回正确答案(无错误)时,该值和输入的公式将添加到各自的列表中。最新答案插入到列表顶部。可以通过双击来回顾之前计算的结果;它将插入到光标位置,或者如果输入编辑框中选中了多个字符,则会替换选择。还可以通过点击按钮
在两个列表之间切换。
图5:最后答案切换按钮。
- 变量列表框。
当用户分配新的标识符时,变量名称会添加到列表中。如果他只是修改其中一个,则值在内部修改,但此窗口中没有更改。变量按字母顺序存储。在这里,也可以通过双击变量名称在新的公式中重复使用变量。它将添加到光标位置,或者将替换输入编辑框中选定的字符。自 v2.23 起,为变量添加了
按钮,用于在变量名称及其值之间切换。
图6:变量切换按钮。
- 函数列表框
此功能旨在允许用户使用常见的数学函数。当您需要一个函数时,可以单击其名称(它会插入到光标位置)或输入标识符。请注意,解析器是区分大小写的!
cos()
与Cos()
不同。以下是已实现的函数
. 函数 用途 描述 abs
abs(expr)
返回表达式的绝对值。 Acos
Acos(expr)
返回表达式的反余弦。 expr
预期为弧度。Ans
Ans(expr)
返回历史记录中的一个答案。 expr
必须介于1和列表中答案的数量之间。Asin
Asin(expr)
返回表达式的反正弦。 expr
预期为弧度。Atan
Atan(expr)
返回表达式的反正切。 expr
预期为弧度。cos
cos(expr)
返回表达式的余弦。 expr
预期为弧度。cosh
cosh(expr)
返回表达式的双曲余弦。 expr
预期为弧度。deg
deg(expr)
返回表达式的度数等价量。 expr
预期为弧度。exp
exp(expr)
返回表达式的指数。 ln
ln(expr)
返回表达式的自然(纳皮尔,以e为底)对数。 log
log(expr)
返回表达式的十进制(以10为底)对数。 logn
logn(expr, n)
返回表达式的n进制对数。 nAp
nAp(n, p)
返回n个元素中p个的排列数 nCp
nCp(n, p)
返回n个元素中p个的组合数。 rad
rad(expr)
返回表达式的弧度等价量。 expr
预期为度数。与°
运算符相同。sin
sin(expr)
返回表达式的正弦。 expr
预期为弧度。sinh
sinh(expr)
返回表达式的双曲正弦。 expr
预期为弧度。sqrt
sqrt(expr)
返回表达式的平方根。 sqrtn
sqrtn(expr, n)
返回表达式的n阶根。 tan
tan(expr)
返回表达式的正切。 expr
预期为弧度。tanh
tanh(expr)
返回表达式的双曲正切。 expr
预期为弧度。sum
sum(expr(var), var, low, high)
当 var
从low到high时,返回表达式的和。var
必须是整数(尚未实现)。product
product(expr(var), var, low, high)
当 var
从low到high时,返回表达式的乘积。var
必须是整数(尚未实现)。图7:用户接口函数。
我在每个处理函数的末尾添加了一组OnFocus()
调用,以避免用户过多地使用鼠标。这样,当您完成要计算的表达式的编写时,只需按下键盘上的回车键即可。如果发生错误,光标将定位在错误位置(如果可能),否则将选择整个输入文本。当您双击(最近答案/变量)列表或“擦除”(清除列表)按钮时,也会发生这种情况。
所有运算符及其语法都汇总在帮助对话框的右侧选项卡中。使用“帮助”按钮或系统菜单(Alt+空格键)即可打开。
帮助对话框
该对话框以标签页形式展示了用户可用的不同功能。现有三个标签页显示了函数、运算符和错误代码,如下图所示:
图8:帮助对话框选项卡。
此对话框作为信息性工具提示提供,供那些需要快速查找可用函数和运算符、它们的语法和含义而无需打开解析器文档的用户使用。错误代码也进行了总结,并概述了可能发生的各种错误情况。
关于对话框
关于一个无人知晓的对话框,有什么可说的呢?
图9:关于对话框。
它只是向所有应得之人致敬,并维护 VisualCalc 和解析器项目的版本号。
III] 如何在您的应用程序中非常简单地使用解析器
您可以选择在您的项目中重新编译解析器源文件,或者从其 DLL 导入解析器。请跟随指南。
重新编译源文件
如前所述,解析器是由五个文件组成的,需要包含在您的项目中。
- 将VCalcParser.h、VCalcParser.cpp、VCalcParserExceptions.h、VCalcParserExceptions.cpp和VCalcParserTypes.h复制到您的源文件中。
- 然后,在需要调用解析器并启动计算的文件中,只需
#include "VCalcParser.h"
。当然,如果您是在MFC项目中,请务必在stdafx.h之后包含它。
...就是这样。现在您需要做的就是像使用项目中的任何其他类一样使用它(有关示例,请参阅下面的“使用代码”)。
调用 DLL
- The DLL is already built, why not reuse it?
- Yeah, sure, but how?
- Follow me... ready?
- OK, let's go !
同样,第一步是在源文件中包含解析器的头文件(如果是在使用预编译头的 MFC 项目中编译,仍然在 stdafx.h 之后)
#include "stdafx.h" #define VCALCPARSER_DLL #include "VCalcParser.h"
- 嘿,这里定义的这个宏是什么?
嗯,定义VCALCPARSER_DLL
宏是为了方便在“DLL导入”或“源代码使用”环境中重用头文件。它告诉编译器您从DLL导入符号,而不是自己使用一组内部__declspec(dllimport)
来定义它们。由于此宏隐式地使您的项目加载VCalcParser.lib到您的项目中,因此您必须使其在源文件中可用,方法是将其复制到您的项目源文件夹中,或者更改您的项目设置。
使用 DLL 方法时最后一点是在您的项目中设置以下设置
Project settings > C/C++ | Code Generation | runtime library : - Multithreaded DLL (/MD) in release mode compilation - Multithreaded Debug DLL (/MDd) in debug mode compilation
这将防止您的代码在尝试释放堆上由 DLL 分配的内存(例如异常对象)时崩溃。
使用代码
一旦解析器以您选择的任何方式插入到您的项目中,您就可以使用它了。您可以根据自己的喜好使用您的新解析器:作为全局对象(别说是我告诉你的!)、作为需要获取公式结果的函数中的局部变量,或者作为类成员(非常推荐 - 我个人在我的CVisualCalcDlg
对话框类中声明了一个CVCalcParser m_Parser
)。您甚至不必在构造函数(或任何其他地方)中初始化它。
当您需要启动解析器时,只需调用其CVCalcParser::Evaluate(std::string)
成员并向其传递要解析的字符串。
这可以简单地通过以下方式完成
StrDest.Format("%f", m_Parser.Evaluate(" 2 + 2 - 1 " );
然而,我提出异议。解析器可能会抛出异常,必须捕获这些异常以防止程序终止。因此,前面的指令周围至少必须有一个基本的try/catch
语句。最好的方法是测试每个异常类别以及警告。
以下是我的对话框在 VisualCalc 中调用解析器的方式:
void CVisualCalcDlg::OnCalculate() { CString strSource, strDest; m_peInput->GetWindowText(strSource); // Initializes and calls the Parser try { VALUES_TYPE valResult = m_Parser.Evaluate((LPCTSTR)strSource); strDest.Format("%.8f", valResult); strDest = this->RemoveTrailingZeros(strDest); strDest = this->InsertThousandSpaces(strDest); if (m_Parser.HasWarning()) { // Adds the warning after the result strDest.Format("%s ; %s", strDest, m_Parser.GetWarningMsg().c_str()); } m_peInput->SetSel(0, -1); } catch (CSyntaxException& ex) { strDest.Format("Syntax error %d : %s", ex.GetExceptionNumber(), ex.GetMessage().c_str()); m_peInput->SetSel(ex.GetErrorPos(), ex.GetErrorPos()); } catch (CMathematicException& ex) { strDest.Format("Mathematic error %d : %s", ex.GetExceptionNumber(), ex.GetMessage().c_str()); m_peInput->SetSel(ex.GetErrorPos(), ex.GetErrorPos()); } catch (CFunctionException& ex) { strDest.Format("Function error %d : %s", ex.GetExceptionNumber(), ex.GetMessage().c_str()); m_peInput->SetSel(ex.GetErrorPos(), ex.GetErrorPos()); } catch (CParameterException& ex) { strDest.Format("Parameter error %d : %s", ex.GetExceptionNumber(), ex.GetMessage().c_str()); m_peInput->SetSel(ex.GetErrorPos(), ex.GetErrorPos()); } catch (CVariableException& ex) { strDest.Format("Variable error %d : %s", ex.GetExceptionNumber(), ex.GetMessage().c_str()); m_peInput->SetSel(ex.GetErrorPos(), ex.GetErrorPos()); } catch (CDomainException& ex) { strDest.Format("Domain error %d : %s", ex.GetExceptionNumber(), ex.GetMessage().c_str()); m_peInput->SetSel(ex.GetErrorPos(), ex.GetErrorPos()); } catch (CParserException& ex) { strDest.Format("Parser error %d : %s", ex.GetExceptionNumber(), ex.GetMessage().c_str()); m_peInput->SetSel(ex.GetErrorPos(), ex.GetErrorPos()); } catch (...) { strDest.Format("Unknown parser internal error"); } // Prints the result... m_peOutput->SetWindowText(strDest); // Updates the listboxes' contents this->UpdateAnswersList(); this->UpdateVariablesList(); this->UpdateFunctionsList(); // Give the focus back to the input Edit Box m_peInput->SetFocus(); }
我们到了。钥匙就在你手中...
结论和致谢
VisualCalc 从我的版本 1.0(从未发布)发展到现在的版本 3.0,期间经过了一些私人发布。它成为一个模块化项目,可以轻松插入到整个计算项目中。正如副标题所说,它还提供了编写递归下降解析器的起点。
因此,我要感谢所有通过提问、建议、帮助等方式对本项目做出或大或小贡献的人们(感谢Cedric Moonen在/MD编译器选项上的宝贵帮助,感谢VuNic在DLL的迷雾中为我指明了正确的方向)。我邀请大家继续参与这个项目,提供新的想法、新的功能,并提供一些并发实现...
未来功能
- 修复运算符优先级错误。
- 修复未知异常 (2999 导致程序退出而无异常)。
- 管理正/负无穷大。
- 擦除列表按钮上的弹出菜单。
- 修复读取流末尾的问题。
- 将
m_PI
和m_E
的可见性更改为public static const
。 - 用于本地化的新
TV_PERIOD
TokenValue
枚举器。 - “关于”对话框内的超链接。
历史
- 2006年4月16日:VisualCalc v3.0发布。
- 将解析器分离到自己的DLL中。
- 项目迁移到 Microsoft Visual C++ .NET 2003。
- 2006年2月6日:VisualCalc v2.24发布。
- 解析器读取流已修复。
- 解析器异常情况已修复(隐式乘法)。
- 解析器
Pow()
计算已修复(x-n)。 - 删除了结果末尾的零。
- 在结果显示中添加了千位分隔符。
- 新增按钮,可在变量名及其值之间切换。
- 新增
sqrtn(x, n)
函数,用于n阶根。 - 解析器代码已为 Doxygen 文档生成器添加注释。
- 将所有 GUI 字符串移至资源字符串表。
- 2005年10月19日:VisualCalc v2.20发布。
- 解析器已拆分为其自己的类。
- 解析器现在符合标准 C++。
- 值类型已更改为
long double
。 - 错误最终通过异常管理,并重新定义了代码。
- 新增按钮,可在上次答案的结果和公式之间切换。
- 结果字段已更改为编辑框,以便复制其内容。
- 2005年1月26日:VisualCalc v2.13发布。
- 错误现在由代码管理。
- 帮助对话框中新增错误代码选项卡。
- 2004年12月28日:VisualCalc v2.12发布。
- 新增了一些函数。
- 新增了一些运算符。
- 新增帮助对话框,带有函数和运算符的选项卡。
- 2004年11月13日:VisualCalc v2.0发布。
- 增加了函数列表。
- 新的序列运算符(',')。
- 增加了目录(帮助标签的初始版本)。
- 对话框源代码中更好的控件管理。
- 源代码已清理(是的,尚未完成...)。
- 新增“关于”对话框。
- 2004年10月5日:VisualCalc v1.0发布。
- 上次答案列表。
- 变量列表(已赋值 + 常量)。
- “关于”对话框中有关可用运算符的简短文档。