项目简介
本计算器采用Java Swing实现,支持高精度运算、多表达式计算和现代UI设计,主要特性包括:
- Windows 11深色风格圆角UI组件
- 右键菜单支持复制/粘贴/全选
- 自适应窗口字体缩放
- 利用栈实现多行表达式计算
- 使用BigDecimal实现高精度计算
项目演示图片
运行界面
表达式计算
高精度计算
核心代码解析
1. 主框架初始化
/**
• 现代风格计算器主类
• 实现功能:
• - 界面初始化
• - 事件监听绑定
• - 组件布局管理
*/
public class ModernCalculator {
// 界面组件
private JFrame frame; // 主窗口容器
private JTextArea display; // 多行显示区域(支持历史记录)
// 计算逻辑相关
private StringBuilder currentInput = new StringBuilder(); // 用户输入缓存(支持多行)
private Stack<BigDecimal> operandStack = new Stack<>(); // 操作数栈(存储BigDecimal数值)
private Stack<Character> operatorStack = new Stack<>(); // 运算符栈(存储+-*/%等)
// 构造函数
public ModernCalculator() {
initialize(); // 执行界面初始化
addContextMenu(); // 添加右键上下文菜单
}
/**
◦ 初始化主界面
◦ 实现步骤:
◦ 1. 窗口基础设置
◦ 2. 显示区域配置
◦ 3. 按钮面板布局
◦ 4. 事件监听绑定
*/
private void initialize() {
// 主窗口设置 --------------------------------------------------
frame = new JFrame("计算器");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // 关闭时退出程序
frame.getContentPane().setBackground(new Color(0x202020)); // 设置暗色背景
// 图标加载(需在项目res目录放置icon.png)
try {
ImageIcon icon = new ImageIcon(getClass().getResource("/res/icon.png"));
frame.setIconImage(icon.getImage()); // 设置任务栏/窗口图标
} catch (Exception e) {
System.err.println("图标加载失败: " + e.getMessage());
}
// 窗口尺寸设置
frame.setSize(428, 600); // 初始尺寸(模拟手机计算器比例)
frame.setMinimumSize(new Dimension(428, 600)); // 最小尺寸限制(防止UI变形)
// 显示区域初始化 ----------------------------------------------
display = new JTextArea("", 6, 26); // 6行26列的多行文本框
display.setEditable(false); // 禁止直接编辑(仅通过按钮输入)
display.setBackground(new Color(0x202020)); // 暗色背景
display.setForeground(Color.WHITE); // 白色文字
display.setLineWrap(true); // 启用自动换行
frame.add(display, BorderLayout.NORTH); // 添加到窗口顶部区域
// 按钮面板布局 ------------------------------------------------
JPanel buttonPanel = new JPanel(new GridLayout(5, 4, 1, 1)); // 5行4列网格布局
buttonPanel.setBackground(new Color(0x202020)); // 统一背景色
// 按钮标签定义(按计算器物理布局顺序)
String[] buttonLabels = {
"%", "C", "CE", "÷", // 第一行:特殊功能键
"7", "8", "9", "×", // 第二行:数字及乘法
"4", "5", "6", "−", // 第三行:数字及减法
"1", "2", "3", "+", // 第四行:数字及加法
"±", "0", ".", "=" // 第五行:符号/数字/等于
};
// 动态创建按钮
for (String label : buttonLabels) {
ModernWebButton button = createStyledButton(label); // 创建风格化按钮
button.addActionListener(new ButtonClickListener()); // 绑定点击事件监听器
buttonPanel.add(button); // 添加到按钮面板
}
frame.add(buttonPanel, BorderLayout.CENTER); // 将按钮面板添加到窗口中心区域
// 窗口缩放监听器(实现字体自适应)-------------------------------
frame.addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent evt) {
adjustFontSize(); // 当窗口大小改变时动态调整字体
}
});
// 显示窗口
frame.setLocationRelativeTo(null); // 居中显示
frame.setVisible(true); // 设为可见
}
/**
◦ 创建风格化按钮(工厂方法)
◦ @param label 按钮文字
◦ @return 配置完成的ModernWebButton实例
*/
private ModernWebButton createStyledButton(String label) {
// 运算符按钮特殊配色
if (label.matches("[%C÷×−+]|CE")) {
return new ModernWebButton(label,
new Color(0x323232), // 默认颜色
new Color(0x3a3d3d), // 悬停颜色
new Color(0x323232) // 点击颜色
);
}
// 等号按钮特殊配色
else if (label.equals("=")) {
return new ModernWebButton(label,
new Color(0x4cc2ff), // 默认蓝
new Color(0x47b1e8), // 悬停蓝
new Color(0x42a1d3) // 点击蓝
);
}
// 数字按钮默认配色
return new ModernWebButton(label);
}
2. 现代风格按钮组件(完整实现)
/**
• 自定义现代风格按钮组件
• 继承JButton并实现:
• 1. 圆角矩形外观
• 2. 三态颜色变化(默认/悬停/点击)
• 3. 抗锯齿渲染
*/
public static class ModernWebButton extends JButton {
// 颜色配置
private final Color DEFAULT_COLOR; // 默认状态颜色
private final Color HOVER_COLOR; // 鼠标悬停颜色
private final Color CLICKED_COLOR; // 点击状态颜色
private boolean isClicked = false; // 点击状态标记
/**
◦ 全参数构造函数
◦ @param text 按钮文字
◦ @param defaultColor 默认状态颜色
◦ @param hoverColor 悬停状态颜色
◦ @param clickedColor 点击状态颜色
*/
public ModernWebButton(String text, Color defaultColor,
Color hoverColor, Color clickedColor) {
super(text);
this.DEFAULT_COLOR = defaultColor;
this.HOVER_COLOR = hoverColor;
this.CLICKED_COLOR = clickedColor;
// 基础设置 ------------------------------
setOpaque(false); // 透明背景(需要自定义绘制)
setBorderPainted(false); // 隐藏默认边框
setFocusPainted(false); // 去除焦点虚线框
setContentAreaFilled(false); // 禁用默认填充
setFont(new Font("Arial", Font.BOLD, 26)); // 统一字体样式
// 鼠标事件监听 ---------------------------
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
setBackground(CLICKED_COLOR); // 按下时显示点击色
isClicked = true;
}
@Override
public void mouseReleased(MouseEvent e) {
// 根据是否悬停恢复颜色
setBackground(getModel().isRollover() ? HOVER_COLOR : DEFAULT_COLOR);
isClicked = false;
}
@Override
public void mouseEntered(MouseEvent e) {
if (!isClicked) setBackground(HOVER_COLOR); // 非点击状态悬停变色
}
@Override
public void mouseExited(MouseEvent e) {
if (!isClicked) setBackground(DEFAULT_COLOR); // 恢复默认颜色
}
});
setBackground(DEFAULT_COLOR); // 初始颜色设置
}
// 默认构造函数(数字按钮使用)
public ModernWebButton(String text) {
this(text,
new Color(0x3b3b3b), // 默认灰
new Color(0x313333), // 悬停灰
new Color(0x262929) // 点击灰
);
}
/**
◦ 自定义绘制方法(实现圆角效果)
◦ @param g 图形上下文
*/
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
// 启用抗锯齿
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY);
// 计算圆角半径(基于按钮高度)
int cornerRadius = getHeight() / 8;
// 绘制圆角矩形背景
RoundRectangle2D rect = new RoundRectangle2D.Double(
0, 0,
getWidth()-1, // 宽度留1像素边框
getHeight()-1, // 高度留1像素边框
cornerRadius,
cornerRadius
);
g2.setColor(getBackground());
g2.fill(rect);
// 调用父类方法绘制文本
super.paintComponent(g2);
g2.dispose();
}
}
(因篇幅限制,表达式解析、右键菜单等核心模块解析请见完整版文档)
实现亮点(技术细节)
// 字体自适应算法示例
private void adjustFontSize() {
int frameHeight = frame.getHeight(); // 获取当前窗口高度
float baseSize = frameHeight / 30f; // 基础字体大小(按窗口高度的1/30计算)
float fontSize = Math.max(20, baseSize); // 设置最小20pt
Font currentFont = display.getFont();
Font newFont = currentFont.deriveFont(Font.BOLD, fontSize);
display.setFont(newFont); // 应用新字体
// 触发文本重排
display.revalidate();
display.repaint();
}
项目完整代码
import javax.swing.*;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.event.*;
import java.awt.geom.RoundRectangle2D;
import java.math.BigDecimal;
import java.math.MathContext;
import java.util.Stack;
public class ModernCalculator {
private JFrame frame;
private JTextArea display;
private StringBuilder currentInput = new StringBuilder();
private Stack<BigDecimal> operandStack = new Stack<>();
private Stack<Character> operatorStack = new Stack<>();
public ModernCalculator() {
initialize();
addCopyAndSelectAllAndPasteMenuToDisplay(); // 新增:为文本区域添加右键菜单,包含粘贴功能
}
private void initialize() {
frame = new JFrame("计算器");
frame.getContentPane().setBackground(new Color(0x202020)); // 设置内容面板的背景颜色
ImageIcon icon = new ImageIcon(getClass().getResource("/res/icon.png"));
if (icon.getImageLoadStatus() == MediaTracker.ERRORED) {
System.err.println("Failed to load icon.");
} else {
frame.setIconImage(icon.getImage()); // 设置图标
}
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 设置初始大小
int defaultWidth = 428;
int defaultHeight = 600;
frame.setSize(defaultWidth, defaultHeight);
// 设置最小尺寸,防止窗口缩小到默认大小之下
frame.setMinimumSize(new Dimension(defaultWidth, defaultHeight));
frame.setLayout(new BorderLayout());
display = new JTextArea("", 6, 26); // 使用JTextArea支持多行显示
display.setEditable(false);
display.setBackground(new Color(0x202020)); // 设置JTextArea的背景颜色
display.setForeground(Color.WHITE); // 设置文本颜色以便于阅读
frame.add(display, BorderLayout.NORTH);
JPanel buttonPanel = new JPanel();
buttonPanel.setLayout(new GridLayout(5, 4, 1, 1)); // 调整为5行4列,水平和垂直间距设为1
buttonPanel.setBackground(new Color(0x202020)); // 设置JPanel的背景颜色
String[] buttonLabels = {"%", "C", "CE", "÷",
"7", "8", "9", "×",
"4", "5", "6", "−",
"1", "2", "3", "+",
"±", "0", ".", "="};
for (String label : buttonLabels) {
ModernWebButton button;
if (label.equals("%") || label.equals("C") || label.equals("CE") || label.equals("÷") || label.equals("×") || label.equals("−") || label.equals("+")) {
// 为特殊按钮设置自定义颜色
button = new ModernWebButton(label, new Color(0x323232), new Color(0x3a3d3d), new Color(0x323232));
button.setForeground(Color.WHITE); // 设置按钮文字颜色为白色
}
else if(label.equals("="))
{
button = new ModernWebButton(label, new Color(0x4cc2ff), new Color(0x47b1e8), new Color(0x42a1d3));
button.setForeground(Color.BLACK); // 设置等号按钮的文字颜色为黑色
}
else {
// 普通按钮使用默认颜色
button = new ModernWebButton(label);
button.setForeground(Color.WHITE); // 设置按钮文字颜色为白色
}
button.addActionListener(new ButtonClickListener());
button.setPreferredSize(new Dimension(80, 60)); // 设置按钮的首选大小
buttonPanel.add(button);
}
frame.add(buttonPanel, BorderLayout.CENTER);
frame.addComponentListener(new ComponentAdapter() {
public void componentResized(ComponentEvent evt) {
adjustFontSize();
}
});
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
private void addCopyAndSelectAllAndPasteMenuToDisplay() {
TransparentPopupMenu popupMenu = new TransparentPopupMenu(new Color(255, 255, 255, 100)); // 设置透明背景色
JMenuItem copyItem = new JMenuItem("复制");
copyItem.setFont(new Font("黑体", Font.PLAIN, 16)); // 设置字体、样式和大小
copyItem.addActionListener(e -> {
String selectedText = display.getSelectedText();
if (selectedText != null && !selectedText.isEmpty()) {
StringSelection selection = new StringSelection(selectedText);
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(selection, selection);
}
});
popupMenu.add(copyItem);
JMenuItem pasteItem = new JMenuItem("粘贴");
pasteItem.setFont(new Font("黑体", Font.PLAIN, 16)); // 设置字体、样式和大小
pasteItem.addActionListener(e -> {
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
try {
String data = (String) clipboard.getData(DataFlavor.stringFlavor);
if (data != null) {
int caretPosition = display.getCaretPosition();
String oldText = display.getText();
display.setText(oldText.substring(0, caretPosition) + data + oldText.substring(caretPosition));
currentInput.append(data);
}
} catch (Exception ex) {
ex.printStackTrace();
}
});
popupMenu.add(pasteItem);
JMenuItem selectAllItem = new JMenuItem("全选");
selectAllItem.setFont(new Font("黑体", Font.PLAIN, 16)); // 设置字体、样式和大小
selectAllItem.addActionListener(e -> display.selectAll());
popupMenu.add(selectAllItem);
display.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
if (SwingUtilities.isRightMouseButton(e)) {
showPopup(e, popupMenu);
}
}
@Override
public void mouseReleased(MouseEvent e) {
if (SwingUtilities.isRightMouseButton(e)) {
showPopup(e, popupMenu);
}
}
private void showPopup(MouseEvent e, JPopupMenu menu) {
menu.show(display, e.getX(), e.getY());
}
});
}
static class TransparentPopupMenu extends JPopupMenu {
private final Color backgroundColor;
public TransparentPopupMenu(Color backgroundColor) {
this.backgroundColor = backgroundColor;
}
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D) g.create();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setColor(backgroundColor);
g2d.fillRect(0, 0, getWidth(), getHeight());
super.paintComponent(g2d);
g2d.dispose();
}
}
private class ButtonClickListener implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
String command = e.getActionCommand();
switch (command) {
case "=":
evaluateExpression();
break;
case "C":
clearAll();
break;
case "CE":
clearCurrentLine();
break;
case "±":
toggleSign();
break;
case "%":
moduloOperation();
break;
default:
updateDisplay(command);
break;
}
}
private void updateDisplay(String text) {
if (text.equals("−") && (currentInput.length() == 0 || currentInput.charAt(currentInput.length() - 1) == '−')) {
currentInput.append(text);
} else if (Character.isDigit(text.charAt(0)) && currentInput.length() > 0 && currentInput.charAt(currentInput.length() - 1) == '−') {
currentInput.append(text);
} else {
currentInput.append(text);
}
display.setText(currentInput.toString());
}
private BigDecimal evaluateInfixExpression(String expression) throws Exception {
int i = 0;
boolean lastWasOperatorOrOpenParenthesis = true;
while (i < expression.length()) {
char c = expression.charAt(i);
if (Character.isWhitespace(c)) {
i++;
continue;
}
if ((c == '-' || c == '+') && lastWasOperatorOrOpenParenthesis) {
StringBuilder numBuilder = new StringBuilder("" + c);
i++;
while (i < expression.length() && (Character.isDigit(expression.charAt(i)) || expression.charAt(i) == '.')) {
numBuilder.append(expression.charAt(i));
i++;
}
operandStack.push(new BigDecimal(numBuilder.toString()));
lastWasOperatorOrOpenParenthesis = false;
} else if (Character.isDigit(c) || c == '.') {
int numStart = i;
while (i < expression.length() && (Character.isDigit(expression.charAt(i)) || expression.charAt(i) == '.')) {
i++;
}
operandStack.push(new BigDecimal(expression.substring(numStart, i)));
lastWasOperatorOrOpenParenthesis = false;
} else if (isOperator(c)) {
while (!operatorStack.isEmpty() && precedence(operatorStack.peek()) >= precedence(c)) {
processAnOperator();
}
operatorStack.push(c);
i++;
lastWasOperatorOrOpenParenthesis = true;
} else if (c == '(') {
operatorStack.push(c);
i++;
lastWasOperatorOrOpenParenthesis = true;
} else if (c == ')') {
while (operatorStack.peek() != '(') {
processAnOperator();
}
operatorStack.pop();
i++;
lastWasOperatorOrOpenParenthesis = false;
} else {
throw new Exception("Unexpected character encountered: " + c);
}
}
while (!operatorStack.isEmpty()) {
processAnOperator();
}
return operandStack.pop();
}
private boolean isOperator(char ch) {
return ch == '+' || ch == '-' || ch == '*' || ch == '/' || ch == '%';
}
private int precedence(char op) {
if (op == '+' || op == '-') {
return 1;
} else if (op == '*' || op == '/' || op == '%') {
return 2;
}
return -1;
}
private void processAnOperator() throws Exception {
if (operandStack.size() < 2) {
throw new Exception("Invalid expression");
}
BigDecimal b = operandStack.pop();
BigDecimal a = operandStack.pop();
char op = operatorStack.pop();
operandStack.push(applyOperation(a, b, op));
}
private BigDecimal applyOperation(BigDecimal a, BigDecimal b, char op) {
switch (op) {
case '+':
return a.add(b);
case '-':
return a.subtract(b);
case '*':
return a.multiply(b);
case '/':
if (b.compareTo(BigDecimal.ZERO) == 0) throw new ArithmeticException("Cannot divide by zero.");
return a.divide(b, MathContext.DECIMAL128); // 使用 MathContext.DECIMAL128
case '%':
return a.remainder(b);
default:
throw new IllegalArgumentException("Unknown operator: " + op);
}
}
private void evaluateExpression() {
try {
String standardExpression = currentInput.toString()
.replace("÷", "/")
.replace("×", "*")
.replace("−", "-");
BigDecimal result = evaluateInfixExpression(standardExpression.replaceAll("\\s+", ""));
currentInput.append("\n\n=").append(result.toPlainString());
display.setText(currentInput.toString());
adjustFontSize();
} catch (Exception ex) {
display.setText("Error");
currentInput.setLength(0);
}
}
private void clearAll() {
currentInput.setLength(0);
display.setText("");
}
private void clearCurrentLine() {
currentInput.setLength(Math.max(0, currentInput.lastIndexOf("\n") + 1));
display.setText(currentInput.toString());
}
private void toggleSign() {
String text = currentInput.toString().trim();
if (!text.isEmpty()) {
int len = currentInput.length();
if (currentInput.charAt(len - 1) == '−') {
currentInput.deleteCharAt(len - 1);
} else {
currentInput.insert(0, '−');
}
display.setText(currentInput.toString());
}
}
private void moduloOperation() {
if (currentInput.length() > 0 && Character.isDigit(currentInput.charAt(currentInput.length() - 1))) {
currentInput.append('%');
display.setText(currentInput.toString());
}
}
}
private void adjustFontSize() {
int height = frame.getHeight();
float fontSize = Math.max(20, height / 30f);
Font font = new Font("Arial", Font.BOLD, (int) fontSize);
display.setFont(font);
}
public static class ModernWebButton extends JButton {
// 颜色常量,默认颜色可以用于普通按钮
private static final Color DEFAULT_COLOR = new Color(0X3b3b3b); // 默认背景颜色
private static final Color HOVER_COLOR = new Color(0x313333); // 鼠标悬停时的背景颜色
private static final Color CLICKED_COLOR = new Color(0x262929); // 鼠标点击时的背景颜色
private boolean isClicked = false;
private Color defaultColor;
// 默认构造函数
public ModernWebButton(String text) {
this(text, DEFAULT_COLOR, HOVER_COLOR, CLICKED_COLOR);
}
// 带有自定义颜色的构造函数,适用于特殊按钮
public ModernWebButton(String text, Color defaultColor, Color hoverColor, Color clickedColor) {
super(text);
this.defaultColor = defaultColor;
setOpaque(false);
setContentAreaFilled(false);
setBorderPainted(false);
setFocusPainted(false);
setFont(new Font("Arial", Font.BOLD, 26));
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
setBackground(clickedColor);
isClicked = true;
}
@Override
public void mouseReleased(MouseEvent e) {
if (getModel().isRollover()) {
setBackground(hoverColor);
} else {
setBackground(defaultColor);
}
isClicked = false;
}
@Override
public void mouseEntered(MouseEvent e) {
if (!isClicked) {
setBackground(hoverColor);
}
}
@Override
public void mouseExited(MouseEvent e) {
if (!isClicked) {
setBackground(defaultColor);
}
}
});
setBackground(defaultColor);
}
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
int w = getWidth();
int h = getHeight();
RoundRectangle2D rect = new RoundRectangle2D.Double(0, 0, w - 1, h - 1, h / 8, h / 8);
g2.setColor(getBackground());
g2.fill(rect);
super.paintComponent(g2);
g2.dispose();
}
@Override
public void setBackground(Color bg) {
if (bg == null) {
super.setBackground(defaultColor);
} else {
super.setBackground(bg);
}
}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(ModernCalculator::new);
}
}
使用说明(操作示例)
示例输入:
15×3+20%
=45.2
功能说明:
1. 连续运算:支持多行表达式自动保留历史记录
2. 特殊符号:
• ±:切换当前数值正负号
• %:百分比计算(20% = 0.2)
3. 错误处理:
• 除以零时会显示"Error"并清空输入