有感于windows自带计算器的不好使,最近心血来潮按自己需求开发了一个多功能计算器。其核心的一部分来自于网上的学习资料,因此我也不敢私藏代码,特拿出来和大家分享心得。
计算器功能如下:
1.支持运行时更换界面皮肤,水印和按钮形状等外观显示。
2.支持sin, cos, tan, cot, arcsin, arccos, arctan, arccot, !, log, ln, +, -, *, /, %, 乘方等基本运算,支持连续运算并自带PI,E的精确值。
3.支持表达式计算,支持设置变量,可以轻松的实现公式运算。
4.无限的结果保存,完全对用户透明,用户可以轻松的存储和读取先前的操作结果.
5.能够分析用户操作尤其是表达式中的语法错误,提醒用户而不是返回错误结果。


设计思路:
整个程序的
GUI
由两部分组成。结果存储由
JDialog
完成,
JDialog
内含一个
JList
存放结果。主界面由
JMenuBar
和
JTabbedPane
组成,
JTabbedPane
的两个子界面分别用作按键式计算器和表达式式计算器。
关于界面的构建工作全部由
CalculatorFace
类完成,同时按键式计算器的后台计算工作也有该类完成。
Parser
类则负责解析表达式返回计算结果。
CalculatorFace
调用
Parser
进行表达式计算工作。
在处理按键式计算时,各种计算操作符号显然可以分为两类:单操作数和双操作数。单操作数计算只需要取出显示框中内容,进行计算再将结果显示即可。双操作数计算则显得比较麻烦。因为需要实现连续计算,所以必须保存上次的操作符号和上次输入的结果。为此增加lastOperator字段和number1,number2字段分别存储前次操作符和对应的操作数。
运行机制如下:
当按下新键时,将number2->number1,显示框中内容取出作为number2,同时根据lastOperator中内容进行计算(number1 lastOperator number2),计算完成后,将这次按键内容保存到lastOperator中。要注意的是,当“=”按下时,将取消连续运算。因此添加boolean字段continued用来判断是否进行连续运算。
考虑到用户的误操作,即可能不小心按错了按钮,因此每次运算应以输入新数字前最后一个操作符号为准。因此,continued事实上也控制运算是否进行,每次双操作运算后,continued赋值为false,输入数字后才重新为真。当continued为false时,程序只是简单的把按键值赋予lastOperator而不进行运算工作。
而对于数字按键,唯一需要注意的是按下操作键后,再输入数据需要将显示框清空后从头输入;否则新数字将在显示框原有内容尾部添加。completed字段用于控制该情形。上述例外在于,第一次使用计算器时,即使没有按下操作键,也应该重新显示数字。First字段将处理这一情形。
表达式解析则通过Parser类完成。getToken方法负责将表达式中基本的单元取出。这些基本单元包括:变量,数字和操作符号。运行时,首先将表达式中变量替换成其值,对处理好的表达式从左至右取出单元进行计算。计算采用递归下降的流式处理,调用按计算优先级排列好的各种函数,每种函数处理同一优先级的运算。
优先级列表为:
括号,阶乘,正负号,三角函数/对数函数/反三角函数,乘方,乘/除/模运算,加减运算。
下面举例说明表达式的解析过程。表达式
10 + 5 * B
有两个项:10和5*B,第二项包括两个因数:5和B,分别是一个数字和一个变量
再看另外一个例子。表达式
14 * (7 – C)
有两个因数:14和(7-C),分别是一个数字和一个圆括号表达式。圆括号表达式包括两个项:一个数字和一个变量。
上述过程形成了递归下降解析器的基础。递归下降解析器是一组互相递归的方法,这些方法以一种链式方式实现生成规则。在每个适当的步骤上,解析器以代数学规定的正确顺序执行指定的操作。为了解释如何使用生成规则来解析表达式,采用下面的表达式来跟踪解析过程:
9/3 – (100 + 56)
整个解析过程如下:
(1) 获得第一项9/3;
(2) 获得第一项的两个因数并完成除法运算,得到结果3;
(3) 获得第二项(100+56)。在这一步启动递归分析过程处理括号内的子表达式;
(4) 获得其中的两项并完成加法运算,得到结果156;
(5) 从第二项的递归计算过程中返回;
(6) 3减去156,答案是-153。
Bugs:
经过同学的友情测试,现发现bug如下:
(1)无法将――2形式识别为2
(2)乘方运算时,当数字和’!’中出现空格或者括号时无法处理
(3)类似sinlog10形式无法识别,只能识别sin(log10)
以上将在下一版本中修订。
Uncompleted features:
(1)任意进制的计算和转换
(2)角度与弧度的转换
(3)扩大计算范围(即应该使用BigDemical而不是double)
以上同样会在下一版本中实现。
下面是程序的源代码:
package
cn.com.w4t.calculator;

import
java.awt.BorderLayout;
import
java.awt.Color;
import
java.awt.GridLayout;
import
java.awt.event.ActionEvent;
import
java.awt.event.ActionListener;
import
java.util.Vector;

import
javax.swing.JButton;
import
javax.swing.JDialog;
import
javax.swing.JFrame;
import
javax.swing.JLabel;
import
javax.swing.JList;
import
javax.swing.JMenu;
import
javax.swing.JMenuBar;
import
javax.swing.JMenuItem;
import
javax.swing.JPanel;
import
javax.swing.JScrollPane;
import
javax.swing.JTabbedPane;
import
javax.swing.JTextArea;
import
javax.swing.JTextField;
import
javax.swing.UIManager;
import
javax.swing.border.EtchedBorder;
import
javax.swing.border.LineBorder;
import
javax.swing.border.TitledBorder;

import
org.jvnet.substance.SubstanceLookAndFeel;
import
org.jvnet.substance.theme.SubstancePurpleTheme;
import
org.jvnet.substance.watermark.SubstanceBinaryWatermark;


/** */
/**
* Main Application Interface
*
*
@author
td
*
*/
@SuppressWarnings(
"
serial
"
)

public
class
CalculatorFace
extends
JFrame
implements
ActionListener
{


/** */
/**
* 按键计算的结果
*/
private
double
result1;


/** */
/**
* 表达式计算的结果
*/
private
double
result2;


/** */
/**
* 二元计算是否完成,完成后下次输入数据时将从头开始而不是在原有数据后添加
*/
private
boolean
completed
=
false
;


/** */
/**
* 是否是第一次进行操作
*/
private
boolean
first
=
true
;


/** */
/**
* 是否进行连续计算,比如"="将取消连续运算
*/
private
boolean
continued
=
false
;


/** */
/**
* 被保存的先前计算结果集
*/
private
Vector
<
String
>
resultVector
=
new
Vector
<
String
>
();


/** */
/**
* 计算表达式
*/
private
String express;


/** */
/**
* 变量值
*/
private
String variables;


/** */
/**
* 表达式解析式
*/
private
Parser parser
=
new
Parser();


/** */
/**
* 操作按键
*/
private
JButton[] operatorBtns
=
new
JButton[
20
];


/** */
/**
* 操作符号
*/
private
String[] op
=
{
"
+
"
,
"
-
"
,
"
*
"
,
"
/
"
,
"
%
"
,
"
^
"
,
"
!
"
,
"
=
"
,
"
ln
"
,
"
log
"
,
"
sin
"
,
"
cos
"
,
"
tan
"
,
"
cot
"
,
"
arcsin
"
,
"
arccos
"
,
"
arctan
"
,
"
arccot
"
,
"
PI
"
,
"
E
"
}
;


/** */
/**
* 数字按键
*/
private
JButton[] numberBtns
=
new
JButton[
12
];


/** */
/**
* 保持结果的对话框
*/
private
JDialog resultHolder;


/** */
/**
* 保持结果的列表
*/
private
JList resultList;


/** */
/**
* 一些系统功能按键,显示、隐藏结果记录对话框,存储计算结果,清屏,退格
*/
private
JButton clearBtn, saveBtn, saveBtn2, backBtn, shBtn, shBtn2,
submitBtn;


/** */
/**
* 主界面
*/
private
JTabbedPane mainTab;


/** */
/**
* 输入框
*/
private
JTextField resultField1, resultField2, inputField;


/** */
/**
* 错误标签
*/
private
JLabel errorLabel1, errorLabel2;


/** */
/**
* 监听器
*/
private
ActionListener numberListener
=
new
NumberListener(),
oneOperatorListener
=
new
OneOperatorListener(),
twoOperatorListener
=
new
TwoOperatorListener();


/** */
/**
* 菜单
*/
private
JMenu fileM, setM, helpM;


/** */
/**
* 菜单栏
*/
private
JMenuBar menu;


/** */
/**
* 变量输入框
*/
private
JTextArea variableArea;


/** */
/**
* 操作数
*/
private
String number1
=
"
0.0
"
, number2
=
"
0.0
"
;


/** */
/**
* 上一次操作符号
*/
private
String lastOperator;

//
private JMenuItem fileItem, settingsItem, helpItem;
public
CalculatorFace()
{
super
();
init();
}
/** */
/**
* 初始化属性
*
*/
private
void
initReference()
{
//
init all buttons
for
(
int
i
=
0
; i
<
10
; i
++
)
{
numberBtns[i]
=
new
JButton(Integer.toString(i));
numberBtns[i].addActionListener(numberListener);
numberBtns[i].setBorder(
null
);
}
numberBtns[
10
]
=
new
JButton(
"
.
"
);
numberBtns[
10
].addActionListener(oneOperatorListener);
numberBtns[
10
].setBorder(
null
);
numberBtns[
11
]
=
new
JButton(
"
+/-
"
);
numberBtns[
11
].setBorder(
null
);
numberBtns[
11
].addActionListener(oneOperatorListener);

for
(
int
i
=
0
; i
<
op.length; i
++
)
{
operatorBtns[i]
=
new
JButton(op[i]);
if
(i
<
6
)
operatorBtns[i].addActionListener(twoOperatorListener);
else
operatorBtns[i].addActionListener(oneOperatorListener);
operatorBtns[i].setBorder(
null
);
}
clearBtn
=
new
JButton(
"
Clear
"
);
clearBtn.addActionListener(
this
);
clearBtn.setBorder(
new
EtchedBorder(EtchedBorder.LOWERED));
saveBtn
=
new
JButton(
"
Save
"
);
saveBtn.addActionListener(
this
);
saveBtn.setBorder(
new
EtchedBorder(EtchedBorder.LOWERED));
saveBtn2
=
new
JButton(
"
Save
"
);
saveBtn2.addActionListener(
this
);
saveBtn2.setBorder(
new
EtchedBorder(EtchedBorder.LOWERED));
backBtn
=
new
JButton(
"
Back
"
);
backBtn.addActionListener(
this
);
backBtn.setBorder(
new
EtchedBorder(EtchedBorder.LOWERED));
shBtn
=
new
JButton(
"
S/H
"
);
shBtn.addActionListener(
this
);
shBtn.setBorder(
new
EtchedBorder(EtchedBorder.LOWERED));
shBtn2
=
new
JButton(
"
S/H
"
);
shBtn2.addActionListener(
this
);
shBtn2.setBorder(
new
EtchedBorder(EtchedBorder.LOWERED));
submitBtn
=
new
JButton(
"
Submit
"
);
ParserListener p
=
new
ParserListener();
submitBtn.addActionListener(p);
submitBtn.setBorder(
new
EtchedBorder(EtchedBorder.LOWERED));

//
init textfileds
resultField1
=
new
JTextField(
"
0.0
"
);
resultField1.setEditable(
false
);
resultField2
=
new
JTextField(
"
0.0
"
);
resultField2.setEditable(
false
);
inputField
=
new
JTextField();
inputField.addActionListener(p);
inputField.setToolTipText(
"
输入表达式,可以含有变量,但是变量必须在下面定义
"
);
variableArea
=
new
JTextArea();
variableArea.setToolTipText(
"
输入变量值,约定形式为a=1,b=2,请勿使用s,c,t等关键字作为变量名
"
);

//
init labels
errorLabel1
=
new
JLabel();
errorLabel1.setForeground(Color.red);
errorLabel2
=
new
JLabel();
errorLabel2.setForeground(Color.red);

mainTab
=
new
JTabbedPane();

//
init menu
menu
=
new
JMenuBar();
fileM
=
new
JMenu(
"
File
"
);
fileM.setMnemonic(
'
F
'
);
setM
=
new
JMenu(
"
Settings
"
);
setM.setMnemonic(
'
S
'
);
helpM
=
new
JMenu(
"
Help
"
);
helpM.setMnemonic(
'
H
'
);
}
/** */
/**
* 初始化菜单
*
*/
private
void
initMenu()
{
menu.add(fileM);
menu.add(setM);
menu.add(helpM);
fileM.add(
new
JMenuItem(
"
Exit
"
));
setM.add(
new
JMenuItem(
"
二进制
"
));
setM.add(
new
JMenuItem(
"
八进制
"
));
setM.add(
new
JMenuItem(
"
十进制
"
));
setM.add(
new
JMenuItem(