上一篇文章:JAVA Swing开发简易计算器(上)介绍了图形界面的实现,那么这篇文章就来讲下如何实现具体的功能。注意,我们在计算的过程中考虑运算符的优先级。
代码下载:https://github.com/taifus/Java_Calculator。
准备
GUI是基于事件驱动的,即当用户与GUI交互时,这些构件就产生事件。
为了使我们点击计算器按钮时能有反应,我们需要引入事件处理机制:事件源、事件对象、事件监听者。具体一点来说,我们使用ActionListener接口中的actionPerformed()方法来处理按钮事件。
public void actionPerformed(ActionEvent e)
,这就是一个事件监听器,可以处理类似单击鼠标时触发的事件。ActionEvent就是一个事件类,传入的e就是该事件的对象,然后用e.getActionCommand()方法获取按钮上的字符串 。
核心
其实我们只要获取点击的按钮上代表的内容,然后作出对应的处理就行。比如说我们依次点击按键3
、+
、2
、*
、5
,最后点击=
时,就输出结果13。所以其中最重要的就是用getActionCommand()方法来获取单击按键上的内容,然后用字符串保存起来,单击不同的按钮我们作不同的处理:运算、显示。显示的话使用JTextField来处理就行。
运算符优先级
为了处理运算符优先级,我使用了一个double数组num来保存数字,char数组ope来保存“+-*/”运算符。然后计算的话,如果出现-x
,就先把它变成+(-1.0) * x
,如果是出现乘号或者除号x1 * x2
,我们就让x2 *= x1, x1 = 0
,最后把num数组中所有数字累加一遍即可。
/**
* 处理多项式运算,考虑运算符优先级
*/
private double handlePolynomial()
{
double result = 0.0;
for (int i = 0; i < count2; ++i)
{
if (ope[i] == '-') {
num[i + 1] *= (-1.0);
ope[i] = '+';
}
}
for (int i = 0; i < count2; ++i)
{
if (ope[i] == '*')
{
num[i + 1] *= num[i];
num[i] = 0.0;
}
else if (ope[i] == '/')
{
if (num[i] == 0.0)
operateValidFlag = false;
else
num[i + 1] = num[i] / num[i+1];
num[i] = 0.0;
}
}
for (int i = 0; i < count1; ++i)
result += num[i];
return result;
}
其实处理运算度优先级还有很多别的办法,但是这种属于比较简单的暴力,所以就采取这种方式了。
结果文本显示
为了显示连续输入,我使用了两个JTextField对象:displayText和resultText,其中displayText专门用于文本框内容的显示,而resultText用于保存文本中数字。
/**
* 从输入文本中获取数字
*/
private void getNumberFromText()
{
double result = Double.valueOf(resultText.getText());
num[count1++] = result;
}
所以这算是计算器的两个核心模块,计算与显示。
版块
我们先实现之前在图形界面不同版块种不同按键对应的功能。
全局变量
// 文本框内容显示
private JTextField displayText = new JTextField("");
// 保存文本中数字
private JTextField resultText = new JTextField("0");
// 运算中间结果
private double resultNum = 0.0;
// 第一个是否为数字
private boolean firstDigit = true;
// 是否为基本的+-*/运算
private boolean basicOperator = true;
// 运算结果是否合理
private boolean operateValidFlag = true;
// 模式切换
private boolean changeMode = false;
// 文本说明
private boolean sayNotice = false;
// 二元多项式处理
private int index = 0;
// 中间数字
private double num[] = new double[110];
// 基本运算符
private char ope[] = new char[110];
private int count1 = 0, count2 = 0;
功能键
private final String[] COMMANDS = {"(●'◡'●)", "Mode", "Clear", "Backspace"};
为了方便描述,使用String label = e.getActionCommand();
,下同。
- (●'◡'●)
这个完全属于我...自己添加的没用的东西,点击该按钮时,输出计算机的说明。
if (label.equals(COMMANDS[0]))
{
displayText.setText("Hello, are you OK? Welcome to use this smart calculator.");
sayNotice = true;
}
- Mode
这个是用来改变显示模式的,我们有连续显示或者单个数字显示模式。涉及的代码比较多,就不贴了。
- Clear
这个是用来清零的,将所有变量初始化。
/*
* 处理C按键事件
*/
private void handleC()
{
// 初始化
displayText.setText("");
resultText.setText("0");
resultNum = 0.0;
firstDigit = true;
basicOperator = true;
operateValidFlag = true;
changeMode = false;
sayNotice = false;
index = 0;
count1 = count2 = 0;
}
- Backspace
意思很简单,输错了回退,但是有bug,只能处理数字,如果需要处理运算符的话,还需要进行判断,实现起来也不是很复杂,但是我懒,不想写那么多了。
/*
* 处理Backspace按键事件
*/
private void handleBackspace()
{
String distext = displayText.getText(), restext = resultText.getText();
int dislen = distext.length() , reslen = restext.length();
if (reslen > 0)
{
restext = restext.substring(0, reslen - 1);
distext = distext.substring(0, dislen - 1);
if (restext.length() == 0)
{
displayText.setText("0");
resultText.setText("0");
firstDigit = true;
}
else
{
displayText.setText(distext);
resultText.setText(restext);
}
}
}
数字键
因为输入的数字除了有多位之外还可能出现小数,所以需要做专门的处理,具体见代码。
/*
* 处理数字键事件
*/
private void handleNumber(String key)
{
// 处理输入数字
if (firstDigit)
resultText.setText(key);
else if (key.equals(".") && (!resultText.getText().contains(".")))
resultText.setText(resultText.getText() + ".");
else if (!key.equals("."))
resultText.setText(resultText.getText() + key);
firstDigit = false;
}
文本内容显示
两种显示模式,一种是连续输入,一种是单个数字,默认连续输入。
// 改变显示模式
if (changeMode || sayNotice)
{
// 显示数字
if (firstDigit)
displayText.setText(key);
else
displayText.setText(displayText.getText() + key);
sayNotice = false;
}
else
displayText.setText(displayText.getText() + key);
// 显示基本运算符
if ("+-*/%^".contains(key))
displayText.setText(displayText.getText() + key);
输出结果调整
在运算的时候,我们都是使用默认double类型的数,但是实际输出的可能是整数,所以要对最后的
/**
* 输出结果,并判断是浮点数还是整数
*/
private void resultPrint(double result)
{
if (operateValidFlag)
{
int t1;
double t2;
t1 = (int) result;
t2 = result - t1;
if (t2 == 0)
{
// 显示结果
displayText.setText(String.valueOf(t1));
// 保存当前结果用于连续计算
resultText.setText(String.valueOf(t1));
}
else
{
displayText.setText(String.valueOf(result));
resultText.setText(String.valueOf(result));
}
}
else
// 输入非法,可能出现除零错误
displayText.setText("Illegal operation, please enter again.");
count1 = count2 = 0; // 初始化
}
运算符键
运算符键的处理稍微复杂,除了基本的加减乘除运算外,还会有各种简单函数的运算,所以要做判断。此外,当单击一个运算符时,我们就可以判断之前的数字输入已完成,可以保存了。我默认的是基本的加减乘除运算,然后二元运算的话,需要先获取两个数字,所以加了一点特判:当index大于0时表示二元运算。具体看代码吧。
/*
* 处理运算符被按下的事件
*/
private void handleOperator(String key)
{
// 显示基本运算符
if ("+-*/%^".contains(key))
displayText.setText(displayText.getText() + key);
// 读取运算符
if ("+-*/".contains(key))
ope[count2++] = key.charAt(0);
// 每按一次运算符,读取一次数字
getNumberFromText();
// sin函数
if (key.equals("sin"))
{
basicOperator = false;
resultNum = Math.sin(num[0] * Math.PI / 180);
}
// cos函数
else if (key.equals("cos"))
{
basicOperator = false;
resultNum = Math.cos(num[0] * Math.PI / 180);
}
// tan函数
else if (key.equals("tan"))
{
basicOperator = false;
resultNum = Math.sin(num[0] * Math.PI / 180);
}
// 开根号
else if (key.equals("√ ̄"))
{
basicOperator = false;
resultNum = Math.sqrt(num[0]);
}
// 倒数
else if (key.equals("1/x"))
{
basicOperator = false;
resultNum = num[0];
if (resultNum == 0.0) // 除零错误
operateValidFlag = false;
else
resultNum = 1.0 / resultNum;
}
// 取余
else if (key.equals("%"))
{
basicOperator = false;
index = 1;
}
// 幂
else if (key.equals("^"))
{
basicOperator = false;
index = 2;
}
// 平方
else if (key.equals("x^2"))
{
basicOperator = false;
resultNum = num[0] * num[0];
}
// 一元二次函数
else if (key.equals("(x+1)^2"))
{
basicOperator = false;
resultNum = num[0] * num[0] + 2.0 * num[0] + 1.0;
}
// 二元一次函数
else if (key.equals("x+x*y"))
{
basicOperator = false;
index = 3;
displayText.setText(displayText.getText() + " ");
}
// 二元二次函数
else if (key.equals("x^2+y^2"))
{
basicOperator = false;
index = 4;
displayText.setText(displayText.getText() + " ");
}
// 等于输出结果
else if (key.equals("="))
{
// +-*/基本运算
if (basicOperator)
resultNum = handlePolynomial();
// 处理函数
if (index > 0)
{
resultNum = handleindex(index);
index = 0;
}
// 输出结果
resultPrint(resultNum);
basicOperator = true;
}
firstDigit = true;
operateValidFlag = true;
}
/**
* 处理二元式
*/
private double handleindex(int index)
{
double result = 0.0;
if (index == 1)
result = num[0] % num[1];
else if (index == 2)
result = Math.pow(num[0], num[1]);
else if (index == 3)
result = num[0] + num[0] * num[1];
else if (index == 4)
result = num[0] * num[0] + num[1] * num[1];
return result;
}
所有的功能版块就到此结束了,接下来就是一个框架。
框架
其实在初始化的时候我们就应该完成这项工作:给所有按键添加侦听器。
// 给所有按键添加时间侦听器
// 均使用同一个监听器,即本对象
for (int i = 0; i < KEYS.length; ++i)
keys[i].addActionListener(this);
for (int i = 0; i < COMMANDS.length; ++i)
commands[i].addActionListener(this);
for (int i = 0; i < FUN.length; ++i)
fun[i].addActionListener(this);
总框架其实就是处理事件,就是单击不同的按钮时做不同的处理。
/*
* 处理事件
*/
public void actionPerformed(ActionEvent e)
{
// 获取事件源标签
String label = e.getActionCommand();
// 按键为"(●'◡'●)"
if (label.equals(COMMANDS[0]))
{
displayText.setText("Hello, are you OK? Welcome to use this smart calculator.");
sayNotice = true;
}
// 按键为“Mode”,改变显示模式
else if (label.equals(COMMANDS[1]))
{
handleC();
displayText.setText("The mode has changed, please enter again.");
changeMode = true;
}
// Clear
else if (label.equals(COMMANDS[2]))
{
handleC();
displayText.setText("The result has been cleared, please enter again.");
sayNotice = true;
}
// 回退
else if (label.equals(COMMANDS[3]))
handleBackspace();
// 清空屏幕
else if (label.equals(FUN[3]))
handleC();
// 数字键
else if ("0123456789.".contains(label))
handleNumber(label);
// 运算符
else
handleOperator(label);
}
这就是整个框架,具体的方法在功能实现模块讲了。
演示
最后我们把这次的功能实现代码与上次的图形界面代码整理一下就可以运行了,完整版暂时不贴。
演示视频中均为正常输入,至于什么叫正常输入呢...
小结
其实,这样写出来的计算器还会有很多隐藏的bug,使用久了就会慢慢发现。对于出现的“bug”我们基本上是可以修复的,只是有些简单,有些麻烦。除此之外,还会有逻辑问题,这个看你怎么想,有一些我并不觉得是bug,但是你可以当做是bug,比如说连续点击两个小数点需要报错之类的,这些其实都可以处理,但是在正常情况下并不会出现,所以我就懒得管了。总共将近500行的代码量,感觉还是可以的,收获不少。
最后,还是那句话:Fixed some known bugs, and added some new bugs.
对了,还有一句话,有bug欢迎反馈,至于修不修就...
赏
葛一速
我是代码控,看到喜欢的代码就会收藏
太傅
@葛一速 @[真棒] 咦,XP...
RYAN0UP
可以说是很腻害啦!@[doge]
太傅
@RYAN0UP @[太开心]
w候人兮猗
太傅回归了吗
太傅
@w候人兮猗 @[doge] 更新了更新了!
惶心
刚刚扒了你半天的 style.css,硬是没找到你博客的特殊字体是从哪里加载的……不过第一次打开了的时候在 pic.taifua.com 这个域名上停留了挺久的,所以猜是从这里加载的?其实可以把字体文件放在 GitHub Repo 里面然后用 elemecdn.com 加载(背后是阿里云CDN),就可以省下很多流量。我博客目前采用的是仅仅提取了常用字的 PingFang SC 和 Google Fonts 上面的 Titillium Web 字体。
太傅
@惶心 是从pic.taifua.com这里加载的,腾讯云cdn,字体大概1m多,还是比较慢,没办法。不过cdn的流量是足够的。
太傅
Fixed some known bugs, and added some new bugs.