一.实验要求
1.能通过设计的按钮控件输入并实现算术表达式,表达式在文本框中显示,运算结果输出显示;保存和浏览历史运算记录;
2.能够检验算术表达式的合法性;
3.能够实现混合运算的求解,算术表达式中包含加、减、乘、除、括号等运算符;
4.要求交互界面友好,程序健壮。
二.基本流程:
三.实验思路:
1.利用java语言编写程序实现算术表达式求解。
提示:①参与运算的数据(操作数)可以为整数或者实数,操作符为+、-、*、/等。
②运算顺序是先乘除、后加减、括号内运算优先。
思路:
方法一:单栈
(1)利用一个字符可变序列StringBuilder和操作符栈(opStack)将键盘输入的中缀表达式转换为后缀表达式。其中,StringBuilder存放后缀表达式的结果,操作符栈(opStack)用于存储运算符。
- 算法思想:
把中缀表达式当作一个字符串,表达式从左到右一个个字符判断,规则如下:
① 如果是数字,直接存进StringBuilder。
② 如果字符是±* /( , 加减乘除和左括号:
A.栈为空,直接将操作符入栈;
B.假设现在的操作符是c,栈顶的操作符是s
a. 若c的优先级大于s,直接入栈;
b.若c的优先级小于s,将栈顶操作符弹出到StringBuilder,知道栈顶元素s优先级低于c或栈为空。弹出完这些元素后,再将c压入到栈中;
c.若栈顶元素是(,则直接入栈;
③如果字符是) 右括号,则依次将栈顶元素添加进StringBuilder,直到遇到左括号,左括号不加进StringBuilder,直接丢弃。
④字符判断完了之后,如果栈不为空,则依次输出栈的元素到StringBuilder。
(2)利用栈来进行后缀表达式值的求解,此栈用于存放计算的中间过程和最终结果。
- 算法思想:
把后缀表达式看作是一个字符串,然后从左到右一个一个字符进行判断,规则如下:
①如果是数字,直接进栈。
②如果是+或*,则弹出栈顶两个元素,进行相加或相乘,将计算结果压入栈中。
③ 如果是-或/,则先弹出一个数字为x,再弹出一个数字为y,然后y-x或y/x,最后将计算结果压入栈中。
(3)输出计算结果。
方法二:双栈运算符优先级法
双栈法运算符优先级法为了实现表达式求值需要设置两个栈:
①运算符栈,用于寄存运算符OP
②操作数栈,用于寄存运算数和运算结果OPND
求值的处理过程是自左至右扫描表达式的每一个字符:
①当扫描到的是运算数,则将其压入栈OPND;
②当扫描到的是运算符:
如果这个运算符比OP栈顶运算符优先级高,则入栈;
如果这个运算符比OP栈顶运算符优先级低,则从OPND栈中弹出两个运算数,从栈OP中弹出栈顶运算符进行运算,并将运算结果压入栈OPND;
③继续处理当前字符,直到遇到结束符为止;
2.学习GUI图形界面的设计,设计计算器的界面
第一部分放数字与操作符组件(0-9,+ - * / . =),4*4网格布局;第二部分放文本框,置于顶部;第三部分放 '( ' 与 ‘ )’以及清空按钮和撤销按钮;第四部分为历史记录按钮。
所谓GUI,全称为(Graphical User Interface),又称图形用户接口,或者是图形用户界面,其实就是指采用图形方式显示的计算机操作用户界面。知道了GUI,然后我们就自然联想到了各种按钮,各种窗口图形,而这些东西,全部都包含在了java自带的Swing中,Swing 使用纯粹的 Java 代码来模拟各种控件(使用 Java 自带的作图函数绘制出各种控件),没有使用本地操作系统的内在方法,所以 Swing 是跨平台的。也正是因为 Swing 的这种特性,人们通常把 Swing 控件称为轻量级控件。
JFrame :它是屏幕上window的对象,能够最大化、最小化、关闭
其常用构造方法如下所示:
- JFrame():构造一个初始时不可见的新窗体。
- JFrame(String title):创建一个具有 title 指定标题的不可见新窗体。
JPanel :是一种轻量级容器,可以加入到JFrame窗体中
其常用的构造方法如下。
- JPanel():使用默认的布局管理器创建新面板,默认的布局管理器为 FlowLayout。
- JPanel(LayoutManagerLayout layout):创建指定布局管理器的 JPanel 对象。
JTextField:单行文本框组件
其常用构造方法如下。
- JTextField():创建一个默认的文本框。
- JTextField(String text):创建一个指定初始化文本信息的文本框。
- JTextField(int columns):创建一个指定列数的文本框。
- JTextField(String text,int columns):创建一个既指定初始化文本信息,又指定列数的文本框。
JTextArea :是一个文本框,可以显示文本
其常用构造方法如下。
- JTextArea():创建一个默认的文本域。
- JTextArea(int rows,int columns):创建一个具有指定行数和列数的文本域。
- JTextArea(String text):创建一个包含指定文本的文本域。
- JTextArea(String text,int rows,int columns):创建一个既包含指定文本,又包含指定行数和列数的多行文本域。
JButton : 是Java中的按钮,可以实现一些功能。
常用构造方法如下。
- JButton():创建一个无标签文本、无图标的按钮。
- JButton(Icon icon):创建一个无标签文本、有图标的按钮。
- JButton(String text):创建一个有标签文本、无图标的按钮。
- JButton(String text,Icon icon):创建一个有标签文本、有图标的按钮。
3.功控件响应事件处理
① 首先需要为按钮添加监控器,这样才能响应按键事件,另外我定义了两个字符串exp和outcome(初值都为空串),分别用来存储运算式和运算结果。
② 对“C”键的响应
“C”键的功能是清空算式内容,因此只需要存储算式的字符串exp设置为“0”即可。
③对“D”键的响应
由于按下此键的功能是删除一个数字或算符等,因此我使用字符串提取子串函数substring来提起从字符串开始到字符串倒数第二个字符为止的子串,然后将其显示在JTextField控件中即可,但需要注意的是,需要考虑到空串的情况,当存储算式的字符串exp为空串时,将该字符串设置为“0”然后在JTextField控件上显示0。
④对运算符(+、-、*、/)键的响应
由于运算符必须要在数字的后面,因此需要判别当前存储算式的字符串是否为空串,只有不为空串且串的最后一个字符不为运算符和“.”的情况下才能将当前按下的运算符加入到存储算式的字符串exp中去。
⑤对“=”键的响应
当按下等号按键时,在算式后加上“#”,然后利用中缀式求值来计算结果,并将double类型的结果转化为字符串并被outcome引用,在获取到outcome后,需要将算式字符串exp末尾的“#”去掉,然后将算式显示在第一个JTextField控件中,并将结果显示在第二个JTextField控件之中,最后为了下次计算,需要将exp串置为空串。
⑥对“.”键的响应
小数点必须要在数字的后面,且两个小数点间不能都是数字,即不能出现2.33.3这种情况,因此,我先在算式中寻找最后一个小数点的位置pos:当算式中存在小数点时,在算式最后一个字符为数字且算式从pos+1开始到末尾的子串不都是数字的情况下,才能将小数点加入到算式中去。
当算式中不存在小数点时,只要算式最后一个字符为数字时即可将小数点加入算式。
⑦对数字按键的响应
对于数字按键,一般是直接将当前按下的数字加入到算式中即可,但需要注意不能在算式为“0”的情况下直接将按下的数字直接加入到算式中去,即不能出现0123这种类似的情况,因此当exp为“0”时先将其置为空串,然后将按下的数字加入。
四.具体功能实现:
1.基础控件与模块的添加初始化及为各按键添加响应
包括20个4×5分布的功能按钮(0-9,+,-,*,/,=,C,DEL,(,),.),1个历史记录按钮(HIS)以及三行文本框区域
代码实现:
public class Calculator implements ActionListener {
private JFrame f;
private JPanel p;
//定义两个文本框用来显示算式和结果
private JTextField show, showexp;
//定义数字按钮
private JButton zero, one, two, three, four, five, six, seven, eight, nine;
//定义控制按钮
private JButton H, C, DEL, point, LK, RK;
//定义运算符号按钮
private JButton div, mul, sum, minus, eql;
//定义算式的字符串表示
private String input, output;
控件及变量初始化代码:
Calculator() {
f = new JFrame("Calculator By Lny");
Image image = new ImageIcon("C:\\Users\\李宁宇\\Pictures\\Saved Pictures\\1.jpg").getImage(); //添加背景图片
p = new BackgroundPanel(image);
show = new JTextField("0");//创建单行文本控件
showexp = new JTextField();
zero = new JButton("0");//创建按钮
one = new JButton("1");
two = new JButton("2");
three = new JButton("3");
four = new JButton("4");
five = new JButton("5");
six = new JButton("6");
seven = new JButton("7");
eight = new JButton("8");
nine = new JButton("9");
H = new JButton("HIS");
C = new JButton("C");
DEL = new JButton("DEL");
sum = new JButton("+");
minus = new JButton("-");
mul = new JButton("*");
div = new JButton("/");
point = new JButton(".");
LK = new JButton("(");
RK = new JButton(")");
eql = new JButton("=");
input = output = "";//初始设置存储算式和结果的字符串为空串
}
计算器界面及按键监听设置:
//计算器界面设置
public void display() {
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//设置关闭就退出程序
//设置显示结果的单行文本框相关属性
show.setFont(new Font(Font.SERIF, Font.BOLD, 40));//设置字体样式
show.setBorder(BorderFactory.createEmptyBorder());//设置单行文本控件无边框
show.setHorizontalAlignment(SwingConstants.RIGHT);//设置文本靠右显示
show.setEnabled(false);//设置单行文本框不能点击
showexp.setFont(new Font(Font.SERIF, Font.BOLD, 16));//设置字体样式
showexp.setBorder(BorderFactory.createEmptyBorder());//设置单行文本控件无边框
showexp.setHorizontalAlignment(SwingConstants.RIGHT);//设置文本靠右显示
showexp.setEnabled(false);//设置文本框不能点击
H.setForeground(Color.red);//设置按钮字体颜色为蓝色
H.setFont(new Font(Font.SERIF, Font.BOLD, 16));//设置按钮字体样式
H.setContentAreaFilled(false);//设置按钮为透明效果
C.setForeground(Color.red);
C.setFont(new Font(Font.SERIF, Font.BOLD, 16));
C.setContentAreaFilled(false);
DEL.setForeground(Color.red);
DEL.setFont(new Font(Font.SERIF, Font.BOLD, 16));
DEL.setContentAreaFilled(false);
LK.setForeground(Color.red);//设置按钮字体颜色为蓝色
LK.setFont(new Font(Font.SERIF, Font.BOLD, 16));//设置按钮字体样式
LK.setContentAreaFilled(false);//设置按钮为透明效果
RK.setForeground(Color.red);//设置按钮字体颜色为蓝色
RK.setFont(new Font(Font.SERIF, Font.BOLD, 16));//设置按钮字体样式
RK.setContentAreaFilled(false);//设置按钮为透明效果
sum.setForeground(Color.BLUE);
sum.setFont(new Font(Font.SERIF, Font.BOLD, 16));
sum.setContentAreaFilled(false);
minus.setForeground(Color.BLUE);
minus.setFont(new Font(Font.SERIF, Font.BOLD, 16));
minus.setContentAreaFilled(false);
mul.setFont(new Font(Font.SERIF, Font.BOLD, 16));
mul.setForeground(Color.BLUE);
mul.setContentAreaFilled(false);
div.setForeground(Color.BLUE);
div.setFont(new Font(Font.SERIF, Font.BOLD, 16));
div.setContentAreaFilled(false);
point.setForeground(Color.BLUE);
point.setFont(new Font(Font.SERIF, Font.BOLD, 16));
point.setContentAreaFilled(false);
eql.setForeground(Color.red);
eql.setFont(new Font(Font.SERIF, Font.BOLD, 16));
eql.setContentAreaFilled(false);
zero.setForeground(Color.BLUE);
zero.setFont(new Font(Font.SERIF, Font.BOLD, 16));
zero.setContentAreaFilled(false);
one.setForeground(Color.BLUE);
one.setFont(new Font(Font.SERIF, Font.BOLD, 16));
one.setContentAreaFilled(false);
two.setForeground(Color.BLUE);
two.setFont(new Font(Font.SERIF, Font.BOLD, 16));
two.setContentAreaFilled(false);
three.setForeground(Color.BLUE);
three.setFont(new Font(Font.SERIF, Font.BOLD, 16));
three.setContentAreaFilled(false);
four.setForeground(Color.BLUE);
four.setFont(new Font(Font.SERIF, Font.BOLD, 16));
four.setContentAreaFilled(false);
five.setForeground(Color.BLUE);
five.setFont(new Font(Font.SERIF, Font.BOLD, 16));
five.setContentAreaFilled(false);
six.setForeground(Color.BLUE);
six.setFont(new Font(Font.SERIF, Font.BOLD, 16));
six.setContentAreaFilled(false);
seven.setForeground(Color.BLUE);
seven.setFont(new Font(Font.SERIF, Font.BOLD, 16));
seven.setContentAreaFilled(false);
eight.setForeground(Color.BLUE);
eight.setFont(new Font(Font.SERIF, Font.BOLD, 16));
eight.setContentAreaFilled(false);
nine.setForeground(Color.BLUE);
nine.setFont(new Font(Font.SERIF, Font.BOLD, 16));
nine.setContentAreaFilled(false);
//为按钮添加监听事件
H.addActionListener(this);
C.addActionListener(this);
DEL.addActionListener(this);
sum.addActionListener(this);
minus.addActionListener(this);
mul.addActionListener(this);
div.addActionListener(this);
point.addActionListener(this);
eql.addActionListener(this);
zero.addActionListener(this);
one.addActionListener(this);
two.addActionListener(this);
three.addActionListener(this);
four.addActionListener(this);
five.addActionListener(this);
six.addActionListener(this);
seven.addActionListener(this);
eight.addActionListener(this);
nine.addActionListener(this);
LK.addActionListener(this);
RK.addActionListener(this);
//GridBagLayout(网格包布局管理器)
GridBagLayout gblayout = new GridBagLayout();
//创建GridBagLayout布局管理器
p.setLayout(gblayout);
//使用GridBagLayout布局管理器
GridBagConstraints g = new GridBagConstraints();
g.fill = GridBagConstraints.BOTH;//设置当某个单元格未填满时填满整个空间
//fill
//指定组件填充网格的方式,可以是如下值:GridBagConstraints.NONE(默认值)、
// GridBagConstraints.HORIZONTAL(组件横向充满显示区域,但是不改变组件高度)、
// GridBagConstraints.VERTICAL(组件纵向充满显示区域,但是不改变组件宽度)
// GridBagConstraints.BOTH(组件横向、纵向充满其显示区域)。
g.weightx = 1.0;//设置窗口变大时缩放比例
g.weighty = 1.0;
//weightx 和 weighty
//用来指定在容器大小改变时,增加或减少的空间如何在组件间分配,默认值为 0,
// 即所有的组件将聚拢在容器的中心,多余的空间将放在容器边缘与网格单元之间。
// weightx 和 weighty 的取值一般在 0.0 与 1.0 之间,数值大表明组件所在的行或者列将获得更多的空间。
g.gridx = 0;//定位在第一行第一列
g.gridy = 0;
//gridx 和 gridy
//用来指定组件左上角在网格中的行和列。容器中最左边列的 gridx 为 0,最上边行的 gridy 为 0。
// 这两个变量的默认值是 GridBagConstraints.RELATIVE,表示对应的组件将放在前一个组件的右边或下面。
g.gridwidth = GridBagConstraints.REMAINDER;//填满整行
g.gridheight = 1;//占一行网格
gridwidth 和 gridheight
//用来指定组件显示区域所占的列数和行数,以网格单元而不是像素为单位,默认值为 1
g.insets = new Insets(5, 5, 0, 5);
//设置该组件与其它组件的距离
// insets指定组件显示区域的外部填充,即组件与其显示区域边缘之间的空间,默认组件没有外部填充。
gblayout.setConstraints(showexp, g);//设置文本控件位置
g.gridx = 0;
g.gridy = 1;//设置在第二行第一列
g.gridheight = 2;//占两行网格
g.insets = new Insets(0, 5, 5, 5);
gblayout.setConstraints(show, g);
g.gridwidth = 1;
g.gridheight = 1;//占一行网格且填满整行
g.insets = new Insets(5, 5, 5, 5);
g.gridy = 4;//第五行
g.gridx = 0;
gblayout.setConstraints(C, g);
g.gridx = 1;
gblayout.setConstraints(DEL, g);
g.gridx = 2;
gblayout.setConstraints(LK, g);
g.gridx = 3;
gblayout.setConstraints(RK, g);
g.gridy = 5;
g.gridx = 0;
gblayout.setConstraints(seven, g);
g.gridx = 1;
gblayout.setConstraints(eight, g);
g.gridx = 2;
gblayout.setConstraints(nine, g);
g.gridx = 3;
gblayout.setConstraints(div, g);
g.gridy = 6;
g.gridx = 0;
gblayout.setConstraints(four, g);
g.gridx = 1;
gblayout.setConstraints(five, g);
g.gridx = 2;
gblayout.setConstraints(six, g);
g.gridx = 3;
gblayout.setConstraints(mul, g);
g.gridy = 7;
g.gridx = 0;
gblayout.setConstraints(one, g);
g.gridx = 1;
gblayout.setConstraints(two, g);
g.gridx = 2;
gblayout.setConstraints(three, g);
g.gridx = 3;
gblayout.setConstraints(sum, g);
g.gridy = 8;
g.gridx = 0;
gblayout.setConstraints(point, g);
g.gridx = 1;
gblayout.setConstraints(zero, g);
g.gridx = 2;
gblayout.setConstraints(eql, g);
g.gridx = 3;
gblayout.setConstraints(minus, g);
p.add(showexp);
p.add(show);
p.add(H);
p.add(C);
p.add(DEL);
p.add(div);
p.add(seven);
p.add(eight);
p.add(nine);
p.add(mul);
p.add(four);
p.add(five);
p.add(six);
p.add(minus);
p.add(one);
p.add(two);
p.add(three);
p.add(sum);
p.add(zero);
p.add(point);
p.add(eql);
p.add(LK);
p.add(RK);
f.setContentPane(p);
f.setSize(450, 550); //设置窗口尺寸
f.setLocationRelativeTo(null);
f.setVisible(true); //设置窗口是否可见
}
@Override
public void actionPerformed(ActionEvent e) {
output = "";
if ((e.getSource()) == C) {//清空算式
input = "0";
show.setText(input); //显示文本框内容
} else if ((e.getSource()) == DEL) {
//提取字符串开始到倒数第二个字符
input = input.substring(0, input.length() - 1);
if (input.length() == 0) input = "0";
show.setText(input);
} else if ((e.getSource()) == sum || (e.getSource()) == minus || (e.getSource()) == mul || (e.getSource()) == div) {
if (input.length() != 0 && (!isOperator(input.charAt(input.length() - 1))))//确认必须有数字才能输入运算符
input += e.getActionCommand(); //getActionCommand():返回于此动作相关的命令字符串
show.setText(input);
} else if ((e.getSource()) == point) {
int pos = input.lastIndexOf('.');//找到最后一个小数点的位置
if (pos >= 0)//前后两个小数点间不能都是数字,即不能2.33时又添加一个小数点变为2.33.
{
if (isDigit(input.charAt(input.length() - 1)) && !isDigitSring(input.substring(pos + 1)))
input += e.getActionCommand();
} else {//小数点前一个必须是数字
if (isDigit(input.charAt(input.length() - 1)))
input += e.getActionCommand();
}
show.setText(input);
} else if ((e.getSource()) == eql) {
input += '#';//算式末尾添加’#’
//从算式中拆分出数字
String[] nums = input.split("[^.0-9]"); //将字符串分割为子字符串,然后结果以字符串数组返回
//任何字符除了.、0-9
List<Double> numList = new ArrayList<>();
for (int i = 0; i < nums.length; i++) {//将每个数字串转化为Double类型
if (!"".equals(nums[i]))
numList.add(Double.parseDouble(nums[i]));
}
double out = getValueOfMid(input, numList);//利用中缀式求值
output = "" + out;//将求得的结果转为字符串
input = input.substring(0, input.length() - 1);//去除算式后的’#’
showexp.setText(input);//第一个单行文本框展示算式
show.setText(output);//第二个单行文本框显示结果
input = "";//存储算式的字符串清空
} else {
if (input == "0") input = "";
input += e.getActionCommand();
show.setText(input);
}
}
3.对输入的算式求解
这里使用的是双栈运算符优先级法:
//对输入的算式(中缀式)进行求值
public static double getValueOfMid(String input, List<Double> numList) {
Stack<Character> OPTR = new Stack<>();//定义算符栈
Stack<Double> OPND = new Stack<>();//定义操作数栈
double output = 0;//最终结果
double a, b;//定义两个操作数
char sym;//定义运算符
OPTR.push('#');
int i = 0, j = 0;
while (input.charAt(i) != '#' || OPTR.peek() != '#') //peek():返回栈顶元素但不移除它
{
if (isDigit(input.charAt(i)))//数字直接入OPND栈
{//此字符是数字
while (isDigit(input.charAt(i)) || input.charAt(i) == '.')//此字符是数字或者此字符是·
{
i++;//跳过此字符
if (i == input.length())
break;
}
i--;
OPND.push(numList.get(j)); //操作数入栈
j++;
} else {
//扫描到是操作符时,如这个运算符比OPTR栈顶运算符的优先级高,则入栈;
// 如这个运算符比OPTR栈顶运算符优先级低,则从OPND栈中弹出两个运算符,从栈OPTR中弹出栈顶运算符进行运算,并将运算结果压入栈OPND。
sym = OPTR.peek();
int m = isp(sym); //m为栈内运算符
int n = icp(input.charAt(i));//n为栈外元素
if (m < n || n == '(') OPTR.push(input.charAt(i));//栈内算符优先级小于栈外,则栈外运算符压栈,左括号直接入栈
else if (m > n)//栈内运算符优先级大于栈外运算符优先级
{
sym = OPTR.peek();
OPTR.pop();//取出栈内的运算符
if (sym != '(' && m == n || m > n) {
b = OPND.peek();//从操作数栈取出一个操作数
OPND.pop();
if (!OPND.empty() && OPTR.peek() != '(')//当操作数栈不为空且不为’(’时继续取出栈中的数进行运算
{
a = OPND.peek();
OPND.pop();
OPND.push(compute(a, sym, b));
continue;
} else if (sym == ')') {
while (sym != '(') {
b = OPND.peek();
OPND.pop();
a = OPND.peek();
OPND.pop();
OPND.push(compute(a, sym, b));
}
} else//处理负数
{
switch (sym)//实现一元运算符的运算
{
case '+':
OPND.push(b);
break;
case '-':
OPND.push(-b);
break;
}
continue;
}
}
if (sym == '(') OPTR.pop();
}
}
i++;
}
output = OPND.peek();
return output;
}
}
参考资料:Java Swing JFrame和JPanel:窗口容器和面板容器 (biancheng.net)http://c.biancheng.net/view/1209.html