1、本篇要实现的内容
最近,大家讨论计算器的实现比较热,今天我也来用C++和Visual Studio实现一个计算器的小程序。这里使用逆波兰算法,能够根据当前用户输入的算式表达式字符串,计算出所要的结果,算式字符串可以包括加、减、乘、除和括号,支持整数、小数,鼠标和键盘均可操作,实现了一个较为经典的计算器功能。后期如果有时间我们再实现一些更多的计算器功能。本篇实现的效果如下:
2、设计目标
我们今天想制作一个计算器,需要基本上能达到日常使用的需求。首先它得有可操作的图形窗口界面,它要能够满足我们一些基本的计算需求,如整数和小数的加、减、乘、除,顺便再把括号功能附加上。同时我们在设计的时候,还允许用户输入算式表达式字符串,程序能根据用户输入算式表达式字符串,经过一些智能纠错后,对纠错后的算式表达式进行实时计算,并最终显示出结果。
2.1 、运行环境
操作系统:Windows10操作系统
编译环境:Microsoft Visual Studio 2010(VC6.0也可以直接编译运行)
其它事项:源代码仅仅包括一个cpp源文件,新建项目可直接编译运行,无需在资源编辑器中额外创建按键、显示框等控件资源。
2.2、实现图形化界面
首先计算器要方便使用,我们必须为它创建一个友好的图形界面。我们首先为他创建一个应用窗口,并为窗口添加相应的控件。控间最主要包括两大部分,一部分是用于用户输入的响应按键,另一部分是用于反馈用户输入和计算结果的显示控件。为了简化项目,我们这里采用系统CreateWindow函数创建的按键BUTTON控件和STATIC控件,分别来响应用户输入和输出。图形的区域分布如下:
2.3、实现字符串算式自动识别计算
通过字符串算式自动识别数学表达式有两个优点。第一个优点,可以方便用户随时校对自己输入算式表达式的正确性。在计算时我不仅仅需要看到的是计算后的得数,有时候我还需要看到我们已经输入的算术表达式,方便我校对输入的式子是否正确,如发现错误还可以及时修改。第二个优点,字符串算式表达式可以考虑到算式计算的优先级。在普通没有字符串表达式的计算器中,我们每输入一个算术符号和数字,就必须要计算出这一步的结果。如此循环操作,再往下继续输入运算符号和数字,屏幕只显示当前的结果。那么这样就势必无法考虑到加减乘除运算规则,只能根据用户输入算式的先后顺序计算,更没有办法考虑到括号的优先运算。那么字符串算数表达式就可以完美解决这个问题,这里还要用到逆波兰算法。
在这个算式中我们需要先计算3*13=39的乘法,在计算12+39=51的加法。
2.4、支持加、减、乘、除和括号
由于使用了逆波兰算法,这里运算我们支持加减乘除,还添加了对括号的支持。我们采用了字符串算式格式,我们可以方便的对加减乘除和括号的运算规则进行支持。因为在日常的运算中,如果拿着计算器还需要自己去考虑一个算式的运算顺序的话,会是一个很糟糕的体验。
2.5、实时更新运算结果
我们在我们在制作计算机前期构想的时候,借鉴了手机上自带计算器功能的一些创意,用户每输入一个字符都会更新并影响到最终结果。在使用计算器的时候,当用户每输入一个数字或者符号时,计算器都会根据当前已经输入的算式表达式,进行智能分析,预估出用户可能需要的结果,随即实时计算出结果并显示。
2.6、智能运算符号校验
我们是采用对字符串进行逆波兰法计算,并且是实时(每输入一个数字或字符都会影响到结果)计算,因此对算式字符串的规范性检测要求较高。但是我们日常在输入字符串表达式的时候,难免会存在一些手误,比如说连续输入两个乘号等等,那么这类的错误操作就需要我们用用户输入逻辑去加以规范或限制。同时还有用户在输入括号时,表达式中的左右括号数量不一致等问题,将会导致计算出现错误。我们这里通过输入逻辑检测解决了用户输入表达式的规范性。
2.7、错误判断提示
在遇到除数为零的特殊情况时,我们需要在结果中输出错误提示,否则计算会出现意外。如下图:
2.8、支持整数、小数运算
这里我们要双精度数据类型进行计算,确保计算的准确性。对小数的计算是我们日常生活中不可少的,部分计算器并没有增加对小数的支持。本次在程序设计的开始,就考虑到了这一点。这里包括对有限小数的计算,包括对循环小数的计算,以及无限循环小数结果的显示逻辑。
2.9、使用逆波兰算法计算数学表达式
一. 波兰式(前缀表达式)
波兰逻辑学家J.Lukasiewicz于1929年提出的表示表达式的一种方式,即二元运算符至于运算数之前的一种表达方式。
二.中缀表达式
普通的表示表达式的一种方法,将二元运算符置于运算数中间,也是大多数情况下使用的一种方法。
三.逆波兰式(后缀表达式)
与波兰式相反,是二元运算符置于运算数之后的一种表达方式。每一运算符都置于其运算对象之后,故称为后缀表示。
三种表达式的形象实例如下:
逆波兰式的应用——算术表达式求值
逆波兰式,也称逆波兰记法(Reverse Polish Notation)。在数据结构中,使用栈的概念完成表达式的求值操作,在计算机系统处理表达式的计算过程中,将中缀表达式转换为后缀表达式的形式进行解析转换并实施计算,这就是逆波兰算法的应用。
具体实现方法大致为:
- 设两个栈,操作数栈和运算符栈;
- 操作数依次入操作数栈;
- 运算符入栈前与运算符的栈顶运算符比较优先级;
- 优先级高于栈顶运算符,压入栈,读入下一个符号;
- 优先级低于栈顶运算符,栈顶运算符出栈,操作数栈退出两个操作数,进行运算,结果压入操作数栈;
- 优先级相等,左右括号相遇,栈顶运算符出栈即可;
- 后缀表达式读完,栈顶为运算结果。
2.10、支持背景图片
程序设计了一个简单的游戏背景设定,程序当前文件夹中放置名为bg.bmp的图片文件后,程序会自动加载并居中显示背景图片,大家可以放上自己喜欢的背景图片。
2.11、支持鼠标、键盘操作
在这里我们除了支持鼠标点击数字和运算符操作以外,还支持用键盘进行输入数字和运算符进行计算。由于我们在设计之初就采用了将鼠标输入和键盘输入的数据命令转换成统一格式命令,然后再计算结果的方式。因此我们可以很方便的统一鼠标和键盘的操作。同时这一特性也对后期我们添加更多的按键操作带来了可能,减少了程序收入和操作之间的耦合度,提高了程序可扩展性。
3、源码下载
该源码可以在VS2010和VC6.0中无差异运行,因此就上传了两个版本的源码,方便运行。
3.1、VS2010源码下载
优快云下载地址:Calculator20241207-15-vs2010.rar
3.2、VC6.0源码下载
优快云下载地址:Calculator20241207-15-vc6.0.rar
4、源代码实现过程
我们根据实现功能的不同,可以大致将整个项目分为以下各个模块。
4.1、链表栈的实现
由于逆波兰法会要用到栈操作,我们预先定义一个链栈,在字符串表达式计算过程中会频繁出栈和进栈,已经栈的初始化和销毁,要注意内存泄露。
//加载系统头文件
#include "windows.h"
#include "stdio.h"
#include "math.h"
//节点统计数字
int st_StackNodeNum=0;
//链栈
template<typename Type>
struct Stack
{
Type num;
Stack<Type>* ptNext;
};
//初始化栈
template<typename Type>
void InitStack(Stack<Type>*& Node)
{
Node = (Stack<Type>*)malloc(sizeof(Stack<Type>));
Node->ptNext = NULL;
st_StackNodeNum++;
}
//头插法入栈
template<typename Type>
void PushStack(Stack<Type>*& Node, Type value)
{
Stack<Type>* pt = (Stack<Type>*)malloc(sizeof(Stack<Type>));
pt->num = value;
pt->ptNext = Node->ptNext;
Node->ptNext = pt;
st_StackNodeNum++;
}
//头插法出栈
template<typename Type>
void PopStack(Stack<Type>*& Node, Type& value)
{
Stack<Type>* pt = Node->ptNext;
value = pt->num;
Node->ptNext = pt->ptNext;
delete pt;
st_StackNodeNum--;
}
//头插法出栈
template<typename Type>
void DestroyStack(Stack<Type>*& Node)
{
if(Node->ptNext == NULL)
{
delete Node;
Node=NULL;
st_StackNodeNum--;
}
}
//判断栈是否为空,除去没有存数据的首个节点外
template<typename Type>
bool IsStackEmpty(Stack<Type>* Node)
{
return Node->ptNext == NULL;
}
//获取栈顶部节点的数据
template<typename Type>
Type GetStackTopValue(Stack<Type>* Node)
{
if(Node->ptNext !=NULL){
return Node->ptNext->num;}else{
return 0;}
}
4.2、字符串操作函数
在字符表达式的输入和处理过程中,会遇到一些必须的字符处理函数,我们在这里定义。
//省略掉数字的小数点后末尾多余的零
void TrimBackZero(char *szString)
{
//标记小数点的位置
int iDotPos=-1;
//先找到小数点的位置
for(int i=0;i<lstrlen(szString);i++)
{
if(szString[i]=='.'){
iDotPos=i;break;}
}
//寻找末尾多余的零
for(int j=lstrlen(szString)-1;j>=iDotPos;j--)
{
if(szString[j]=='.' || szString[j]=='0')
{
szString[j]='\0';
}
else
{
break;
}
}
}
//获取字符串中某个字符的个数
int GetCharAmount(char *szString,char sign)
{
int iAmount=0;
for(int i=0;i<lstrlen(szString);i++)
{
if(szString[i]==sign)iAmount++;
}
return iAmount;
}
//判断是否为数字
bool IsNumber(char *szString)
{
if(strcmp(szString,"0")==0)return true;
if(strcmp(szString,"1")==0)return true;
if(strcmp(szString,"2")==0)return true;
if(strcmp(szString,"3")==0)return true;
if(strcmp(szString,"4")==0)return true;
if(strcmp(szString,"5")==0)return true;
if(strcmp(szString,"6")==0)return true;
if(strcmp(szString,"7")==0)return true;
if(strcmp(szString,"8")==0)return true;
if(strcmp(szString,"9")==0)return true;
return false;
}
//判断是否为运算符号
bool IsOperator(char *szString)
{
if(strcmp(szString,"+")==0)return true;
if(strcmp(szString,"-")==0)return true;
if(strcmp(szString,"*")==0)return true;
if(strcmp(szString,"/")==0)return true;
return false;
}
4.3、计算器类
为了实现计算器的各个功能,我们集成到一个计算器类中进行操作。
//按键最大数量
#define BUTTONMAXNUM 20
//计算器类
class Calculator
{
public:
//用于保存算式表达式字符串
char szExpression[1024];
//用于保存经过校验的算式表达式字符串
char szCheckedExpression[1024];
//用于保存计算结果的字符串
char szResult[1024];
//控件字体设置
HFONT hCtlFont;
//用于存储双精度格式的结果
double ResultDate;
//标记是否出现错误
bool tagError;
//记录错误信息
char szErrorMessage[1024];
//背景图片
HBITMAP hBackGroundBitmap;
public:
Calculator();
~Calculator();
//初始化,用于创建按键控件和显示控件
void Initialize(HWND hWnd);
//相应键盘输入转换成统一的指令(鼠标点击按键)
void OnCommand(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam);
//相应键盘输入转换成统一的指令(数字按键和运算符号按键)
void OnChar(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam);
//相应键盘输入转换成统一的指令(其他特殊按键)
void OnKeyDown(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam);
//根据用户的输入指令进行相应的处理
void OnExcuteString(HWND hWnd,char *szCommand);
//屏幕显示内容
void OnPaint(HWND hWnd,HDC hDC);
//根据字符串计算结果
double GetResultValueByString();
//计算分步结果
void CalValue(Stack<double> *&ptNumStack,Stack<char> *&ptOperatorStack);
//逆波兰算法实现
double Polish(char *String, int len);
};
//自定义计算器类实例
Calculator Calculators;
Calculator::Calculator()
{
hCtlFont=NULL;
strcpy(szExpression,"");
strcpy(szCheckedExpression,"");
strcpy(szResult,"");
ResultDate=0;
tagError=false;
strcpy(szErrorMessage,"");
hBackGroundBitmap=NULL;
hBackGroundBitmap=(HBITMAP)LoadImage(NULL,"bg.bmp",IMAGE_BITMAP,0,0,LR_LOADFROMFILE);
}
Calculator::~Calculator()
{
//删除字体资源
DeleteObject(hCtlFont);
}
4.3.1、初始化及界面初始化
我们这里分别采用系统CreateWindow函数创建的按键BUTTON控件和STATIC控件,分别来响应用户输入和输出。
void Calculator::Initialize(HWND hWnd)
{
//控件字体设置
HFONT hCtlFont=CreateFont(22,0,0,0,1000,0,0,0,0,0,0,PROOF_QUALITY,0,"宋体");
//获取窗口的大小
RECT tempClientRect;
GetClientRect(hWnd,&tempClientRect);
//自定义按键的文字标题
char szButtonTitle[BUTTONMAXNUM][1024]={
".","0","C","+","1","2","3","-","4","5","6","*","7","8","9","/","(",")","DEL","="};
//创建按键控件,并设置按键的位置和标题
for(int i=0;i<BUTTONMAXNUM;i++)
{
//设置按键的宽和高
int w=60,h=35,gap=10;
//设置按键的坐标位置
int x=10+(i%4)*(w+gap),y=tempClientRect.bottom-h-gap-(i/4)*(h+gap);
//创建按键子控件
CreateWindow("BUTTON",szButtonTitle[i],WS_CHILD|WS_VISIBLE|WS_CLIPCHILDREN|WS_CLIPCHILDREN|WS_CLIPSIBLINGS,x,y,60,35,hWnd,(HMENU)i,NULL,NULL);
//设置字体记大小
SendMessage(GetDlgItem(hWnd,i),WM_SETFONT,(WPARAM)hCtlFont,1);
}
//创建显示子控件,算式显示屏幕
CreateWindowEx(WS_EX_CLIENTEDGE,"STATIC","",WS_CHILD|WS_VISIBLE|SS_RIGHT|SS_CENTERIMAGE,10,10,tempClientRect.right-20,50,hWnd,(HMENU)51,NULL,NULL);
//创建显示子控件,结果显示屏幕
CreateWindowEx(WS_EX_CLIENTEDGE,"STATIC","",WS_CHILD|WS_VISIBLE|SS_RIGHT|SS_CENTERIMAGE,10,70,tempClientRect.right-20,50,hWnd,(HMENU)53,NULL,NULL);
//设置字体记大小
SendMessage(GetDlgItem(hWnd,51),WM_SETFONT,(WPARAM)hCtlFont,1);
SendMessage(GetDlgItem(hWnd,52),WM_SETFONT,(WPARAM)hCtlFont,1);
SendMessage(GetDlgItem(hWnd,53),WM_SETFONT,(WPARAM)hCtlFont,1);
}
4.3.1、计算器消息处理逻辑
在这里,我们设计鼠标操作和键盘同时可以操作计算器,因此我们需要统一两种操作的模式。我们将WM_CHAR、WM_KEYDOWN和WM_COMMAND的消息统一转换成Calculator类的指令。
void Calculator::OnCommand(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam)
{
char szButtonTitle[1024]="";
GetWindowText(GetDlgItem(hWnd,LOWORD(wParam)),szButtonTitle,1024);
OnExcuteString(hWnd,szButtonTitle);
}
void Calculator::OnChar(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam)
{
//判断是否响应相应的按键
bool tagResponseStatus=false;
//当用户按下数字键,包括小键盘的数字键
if('0'<=LOWORD(wParam) && LOWORD(wParam)<='9'){
tagResponseStatus=