一、第一步创建一个winform空项目,自己绘制了一个简单的计算器界面,如下图:
文本框添加label,按钮添加button,设置好按钮属性,双击按钮添加点击事件。
二、代码实现计算器的难点分析
1.对于没有运算逻辑的表达式的处理。例如5++3*+、23/...12等。
2.运算符的优先级判断。这里只涉及到简单的加减乘除,没有设置括号,也不涉及负数,相对比较简单。
三、具体实现
1.表达式处理
对于没有运算逻辑的表达式的处理可以有两种方式:第一种方法是对表达式的输入进行规范,尽可能避免掉表达式无法计算的情况,保证点击等号之后获取的表达式可以直接进行运算。第二种方法是在点击等号之后,在运算之前先对表达式逻辑进行判断,如果无法计算,可以显示报错信息,合规再计算结果。两种思路的最大区别是,第一种方式是人为的判断好之后,直接告诉计算器哪些情况允许,哪些不被允许,第二种方式是通过代码来让计算器自行判断。显然,第一种方式更为简单。以下是此计算器的一些输入规则(一切输入规则的作用都是在保障计算功能的前提下尽可能排除干扰):
(1)首位输入不可以是除了“-”和“.”以外的其余符号,“-”和“.”出现在首位,在前补零即可。
(2)表达式不可以出现连续的符号,会有歧义,例如“+-”,“/-”,可以理解为加负数和除以负数,但是如果是“+/”,“+--”等,再去纠结其意义就没必要了,所以为了方便,统一不允许连续符号的输入即可,如果连续输入符号,直接用后输入的符号替换掉前一个符号。
(3)表达式最后一位是符号时点击等号的情况,这种情况无法避免,在点击等号之后判断出最后最后一位是符号的话将其忽略掉即可。
下面是定义的输入的方法:
public void AppendNummber(string num)
{
if (labelShow.Text == "0")
{
labelShow.Text = "";
}
string vaildstr = "1234567890";
//如果当前输入的是数字
if (vaildstr.IndexOf(num) > -1)
{
labelShow.Text += num;
}
//如果当前输入的是符号
else{
//如果输入框不为空
if(!string.IsNullOrEmpty(labelShow.Text))
{
//定义上一个字符,如果是符号,不可以继续输入符号
string endstr = labelShow.Text.Substring(labelShow.Text.Length-1,1);
//上一个输入的字符是符号,新输入的符号将替换掉上一个符号
//如果上一个字符是符号,并且符号位之前有内容,就将当前输入的符号替换掉上一个输入的符号,长度>1是为了
//防止开局直接输入允许的“-”或“.”后,又输入其他不合规符号替换掉原本合规的符号
if (vaildstr.IndexOf(endstr) == -1 && labelShow.Text.Length >1)
{
labelShow.Text = labelShow.Text.Remove(labelShow.Text.Length - 1, 1).Insert(labelShow.Text.Length - 1, num);
}
if(vaildstr.IndexOf(endstr) > -1)
{
labelShow.Text += num;
}
}
//开头输入小数点,前面补0
else if(num == "." && string.IsNullOrEmpty(labelShow.Text))
{
labelShow.Text = "0" + num;
}
//开头输入“-”
else if(num == "-" && string.IsNullOrEmpty(labelShow.Text))
{
labelShow.Text += num;
}
else
{
//开头直接输入其他符号将无法输入
labelShow.Text = "0";
}
}
}
除了归零、删除、等于这三个事件外,其余所有点击事件中调用此方法进行输入。下图只展示部分代码。
private void btnAdd_Click(object sender, EventArgs e)
{
AppendNummber("+");
}
private void btnSub_Click(object sender, EventArgs e)
{
AppendNummber("-");
}
归零事件:
private void btnC_Click(object sender, EventArgs e)
{
labelShow.Text = "0";
}
删除事件:
private void btnDelete_Click(object sender, EventArgs e)
{
if (labelShow.Text != "0")
{
if (labelShow.Text.Length > 0)
{
labelShow.Text = labelShow.Text.Remove(labelShow.Text.Length - 1);
}
if(labelShow.Text.Length == 0)
{
labelShow.Text = "0";
}
}
}
2.运算符优先级判断
当我们的运算表达式输入完成后,点击等于号就要开始进行后续一系列操作了。首先要确保字符串的头尾都有运算所需要的操作数,其次,对字符串进行遍历。截取其中的操作数和运算符,分别存入两个栈中。运算符优先级的问题,可以在存的过程中进行解决。截取的数字依次入栈等待计算,截取的第一个运算符直接入栈,后面截取的运算符,先和栈顶的运算符进行优先级比较,如果当前准备入栈的运算符优先级低于或等于栈顶的运算符,则拿出数字栈中的两个数字和符号栈中的栈顶符号进行运算,将结果压入数字栈中,新的运算符入符号栈中,遍历完成后,再重复刚才的操作,两个数字一个运算符依次参与运算,结果压入数字栈,直到数字栈中只剩最后一个操作数时,这个操作数就是表达式的运算结果。
等于事件:
private void btnEquals_Click(object sender, EventArgs e)
{
string s = labelShow.Text;
if (s[0] == '-')
{
s = '0' + s;
}
//如果表达式结尾是运算符号或者小数点,直接忽略。
if (s[s.Length - 1] == '+' || s[s.Length - 1] == '-' || s[s.Length - 1] == '*' || s[s.Length - 1] == '/' ||
s[s.Length - 1] == '.')
{
s = s.Remove(s.Length - 1,1);
}
Stack<float> numstack = new Stack<float>(); //数字栈
Stack<char> symstack = new Stack<char>(); //符号栈
//遍历字符串,目的是截取所有操作数和运算符并将它们分别入栈准备参与计算
for (int i = 0; i < s.Length; i++)
{
//遍历到数字就开始截取操作数
if (s[i] != '+' && s[i] != '-' && s[i] != '*' && s[i] != '/')
{
//j的作用是继续往后遍历判断这个操作数有多长
for (int j = i; j < s.Length; j++)
{
//遍历遇到符号,表示当前操作读取完毕
if (s[j] == '+' || s[j] == '-' || s[j] == '*' || s[j] == '/')
{
//截取这个操作数存入数字栈里
numstack.Push(float.Parse(s.Substring(i, j - i)));
//i重新开始从下一个操作数的起始位开始遍历,直到读取完字符串中所有的操作数
i = j - 1;
break;
}
//如果遍历到字符串最后一位
if (j == s.Length - 1)
{
//将最后一个操作数压入栈
numstack.Push(float.Parse(s.Substring(i, j + 1 - i)));
//代表遍历结束
i = s.Length;
}
}
}
//遍历到运算符
else
{
//如果符号栈为空,当前的运算符直接入栈,也就是第一个运算符直接入栈
if (symstack.Count == 0)
{
symstack.Push(s[i]);
}
//后续需要进行优先级判断
//如果当前准备入栈的运算符的优先级低于或者等于栈顶元素的优先级时
//需要先从栈中拿出两个数字和一个符号进行结果计算后再将此符号入栈
else if (((s[i] == '+' || s[i] == '-') && (symstack.Peek() == '*' || symstack.Peek() == '/'))
|| ((s[i] == '+' || s[i] == '-') && (symstack.Peek() == '+' || symstack.Peek() == '-'))
|| ((s[i] == '*' || s[i] == '/') && (symstack.Peek() == '*' || symstack.Peek() == '/')))
{
//拿出数字栈最上面的两个操作数
float a1 = 0, b1 = 0;
a1 = numstack.Pop();
b1 = numstack.Pop();
//定义一个运算符为当前栈顶的运算符
char op1 = symstack.Peek();
switch (op1)
{
case '+':
{
numstack.Push(b1 + a1);
break;
}
case '-':
{
numstack.Push(b1 - a1);
break;
}
case '*':
{
//新结果入数字栈
numstack.Push(a1 * b1);
break;
}
case '/':
{
numstack.Push(b1 / a1);
break;
}
}
//运算完毕将之前栈顶的运算符丢掉,存入新的运算符
symstack.Pop();
symstack.Push(s[i]);
}
//其他情况不涉及到运算符优先级,所以直接入栈
else
{
symstack.Push(s[i]);
}
}
} //for循环结束,所有操作数入栈,所有运算符排好优先级入栈,准备剩下的计算
while (symstack.Count != 0)
{
char op2 = symstack.Pop();
float a = 0, b = 0;
a = numstack.Pop();
b = numstack.Pop();
switch (op2)
{
case '+':
{
numstack.Push(a + b);
break;
}
case '-':
{
numstack.Push(b - a);
break;
}
case '*':
{
numstack.Push(b * a);
break;
}
case '/':
{
numstack.Push(b / a);
break;
}
}
}
//循环结束,结果输出
s = numstack.Peek().ToString();
labelShow.Text = s;
}
四、结果展示
输入表达式:
点击等于运算结果: