웹개발을 위해 JAVA를 공부하고 있지만, 프레임 워크 뿐만 아니라 JAVA 자체 언어관련 책도 한번 쓱 훑어 보고 있었다.
그 중에 JAVA 자체 GUI 프로그램도 구현해 보고 싶어서 계산기를 만들게 되었다.
먼저 완성된 모습이다.
- 목차 -
1. Java 의 GUI
2. 계산기 화면 구현
3. 버튼 이벤트 처리
4. 연산자 우선순위 계산
5. 연산자 후순위 계산
6. 마우스 이벤트 추가
7. 전체 소스 (깃허브 주소)
8. 보완 & 추가 할 부분
9. 마치며
1. JAVA의 GUI
Java의 GUI는 Componenet 로 구성되어 있다.
GUI(Graphical User Interface) 란?
- 사용자가 편리하게 입출력 할 수 있도록 그래픽으로 화면을 구성하고 마우스나 키보드로 입력받을 수 있도록 지원하는 사용자 인터페이스
Component 란?
- GUI에서 각각의 요소들
Java 의 GUI 프로그램
- AWT(Abstract Window Tollkit) 패키지 : java.awt 패키지
운영체제가 제공하는 자원을 이용하여 컴포넌트를 생성.
- Swing 패키지 : javax.swing 패키지, java.awt 를 보완한 것 (확장판)
컴포넌트가 자바로 작성되어 있기 때문에 어떤 플랫폼에서도 일관된 화면 작성 가능.
- JavaFX : Java 11에서 제거
2. 계산기 화면 구현
1. GridBagLayout, GridLayout 을 활용한 화면 구현
package com.cal;
public class Calculator extends JFrame {
public JTextField inputSpace= new JTextField();
private GridBagLayout grid = new GridBagLayout();
public Calculator() {
setLayout(null);
// 값입력하는 공간 설정
inputSpace = new JTextField();
inputSpace.setEditable(false); // 편집 불가능
inputSpace.setBackground(Color.WHITE); // 배경은 화이트
inputSpace.setHorizontalAlignment(JTextField.RIGHT); // input값 정렬 위치
inputSpace.setFont(new Font("Times", Font.BOLD, 50)); // 글씨 체
inputSpace.setBounds(8, 10, 270, 70); // x:8, y:10 위치 270x70 크기
// 버튼 공간 설정
JPanel buttonPanel = new JPanel();
buttonPanel.setLayout(new GridLayout(4, 4, 10, 10)); // 격자형태로 배열
buttonPanel.setBounds(8, 90, 270, 235); // x:8, y:90 위치 270x235 크기
// 버튼에 들어갈 글자 배열
String button_names[] = {"C","÷", "+","=", "7", "8", "9", "x","4", "5", "6", "-", "1", "2", "3","0"};
// 버튼글자배열 개수만큼 버튼 만들기
JButton buttons[] = new JButton[button_names.length];
for(int i = 0; i<button_names.length; i++) {
buttons[i] = new JButton(button_names[i]);
buttons[i].setFont(new Font("Arial", Font.BOLD, 20)); // 폰트 지정
// 버튼색상 지정
if(button_names[i] == "C"){ buttons[i].setBackground(Color.red); }
else if((i >=4 && i<=6) || (i >= 8 && i<=10) || (i >=12 && i<=14)) {
buttons[i].setBackground(Color.black);
}else { buttons[i].setBackground(Color.gray); }
buttons[i].setForeground(Color.white); // 글자 색상 설정
buttons[i].setBorderPainted(false); // 테두리 없애주기
buttonPanel.add(buttons[i]); // 버튼 추가
buttons[i].setOpaque(true); // 맥에서 배경색이 먹지 않는것을 해결!
}
// JFrame에 추가
add(inputSpace);
add(buttonPanel);
setTitle("계산기");
setVisible(true);
setSize(300, 370);
setLocationRelativeTo(null); // 화면 가운데에 띄우기
setResizable(false); // 사이즈조절 불가
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // 창을 닫을때 실행중인 프로그램 도 종료
}
public static void main(String[] args) {
new Calculator();
}
}
2. GridBagLayout, GridBagConstraints 을 활용한 화면 구현
맥에 있는 계산기 디자인과 똑같이 하기 위해서 격자 형태로 만드는 방법에 대해서 찾다가
"JAVA-AWT-JPanel-07(GridBagLayout)03(계산기화면)" 를 참고하여 아래와 같이 격자형태로 화면을 재구현 하였다.
package com.cal;
public class Calculator extends JFrame {
private final int width = 240;
private final int height = 370;
public JTextField inputSpace= new JTextField();
private GridBagLayout grid = new GridBagLayout();
private GridBagConstraints gbc = new GridBagConstraints();
private Color darkColor = new Color(80, 82, 85);
TitledBorder tB = new TitledBorder(new LineBorder(darkColor, 1));
String button_names[] = {"C", "±","%", "÷", "7", "8", "9", "x","4", "5", "6", "-", "1", "2", "3","+","0", ".", "="};
String buttonString = "C±%÷789x456-123+0.=";
JButton buttons[] = new JButton[button_names.length];
public Calculator() {
setLayout(null);
inputSpace.setEditable(false); // 편집 불가능
inputSpace.setBackground(darkColor); // 배경은 화이트
inputSpace.setHorizontalAlignment(JTextField.RIGHT); // 정렬 위치
inputSpace.setFont(new Font("Dialog", Font.PLAIN, 40)); // 글씨 체
inputSpace.setBounds(0, 0, width, 70); // x:8, y:10 위치 270x70 크기
inputSpace.setBorder(new LineBorder(Color.gray, 0));
inputSpace.setForeground(Color.white);
JPanel buttonPanel = new JPanel();
// 버튼 레이어 셋팅
buttonPanel.setLayout(grid);
buttonPanel.setBounds(0, 70, width, 274); // x:0, y:70 위치 240x274 크기
buttonPanel.setBackground(darkColor);
//======
gbc.fill = GridBagConstraints.BOTH; // 꽉 채워줌
gbc.weightx = 1.0; // x축 안 넘어감
gbc.weighty = 1.0;// y축 안 넘어감
//========
int x = 0;
int y = 0;
for(int i = 0; i<button_names.length; i++) {
buttons[i] = new JButton(button_names[i]);
buttons[i].setFont(new Font("Dialog", Font.BOLD, 20));
buttons[i].setForeground(Color.white);
// 버튼 색
if(button_names[i].matches("[÷+=x-]")) {
buttons[i].setBackground(new Color(255, 159, 9));
}else if(button_names[i].matches("[C±%]")) {
buttons[i].setBackground(new Color(97, 99, 102));
}else {
buttons[i].setBackground(new Color(123, 125, 127));
}
// 격자 형태 생성 ======
if(button_names[i] == "0") {
makeFrame(buttons[i], x, y, 2, 1);
x++;
}else {
makeFrame(buttons[i], x, y, 1, 1);
}
x++;
if(x > 3) {
x = 0;
y++;
}
// ====== ======
buttons[i].setBorder(tB);
buttonPanel.add(buttons[i]);
buttons[i].setOpaque(true);
}
add(inputSpace);
add(buttonPanel);
setTitle("계산기");
setVisible(true);
setSize(width, height);
setBackground(Color.DARK_GRAY);
setLocationRelativeTo(null); // 화면 가운데에 띄우기
setResizable(false); // 사이즈조절 불가
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // 창을 닫을때 실행중인 프로그램 도 종료
}
// 버튼의 크기 설정
public void makeFrame(JButton c, int x , int y, int w, int h) {
gbc.gridy = y;
gbc.gridx = x;
gbc.gridheight = h;
gbc.gridwidth = w;
grid.setConstraints(c, gbc);
}
public static void main(String[] args) {
new Calculator();
}
}
3. 버튼 이벤트 처리
버튼을 눌렀을 때 이벤트를 처리하는 클래스를 만들어주었다.
버튼 값에 따라 if문으로 분기하여 처리해 주었다.
열심히 예외 처리도 해주었다.
public class Calculator extends JFrame {
String num = "";
private String prev_operation = "";
public Calculator() {
....
for(int i = 0; i<button_names.length; i++) {
// 버튼 이벤트 추가
buttons[i].addActionListener(new ButtonActionListener());
}
....
}
public void makeFrame(JButton c, int x , int y, int w, int h) {
....
}
class ButtonActionListener implements ActionListener{
// ActionListener가 가지고 있는 메서드를 override
// 버튼 이벤트를 처리해준다.
@Override
public void actionPerformed(ActionEvent e) {
String inputValue = e.getActionCommand(); // 클릭한 버튼의 값
if(inputValue.equals("C")) {
// 취소키면 => 초기화
inputSpace.setText("");
}else if(inputValue.equals("=")) {
// 등호 => calculate함수에서 계산한 결과를 double형으로 반환해서,
// inputSpace에 보여주고, num 초기화
String result = Double.toString(calculate(inputSpace.getText()));
inputSpace.setText(""+result);
num = "";
}else if(inputValue.equals("±")) {
/*
* 플러스 마이너스 버튼이다.
* 숫자면 -1을 곱해주고
* 사칙연산 키가 오면 계산이 된다.
* 하지만 플러스마이너스 키를 누른 다음에 또 숫자가 나오면 inputSpace가 초기화 된다.(맥북에서)
* 맥북은 사칙연산버튼을 누르면 inputSpace에 값이 바로바로 사라진다.
* 근데 현재 내 계산기는 모든 계산식이 inputSpace에서 나온다
* 따라서 플러스 마이너스 키를 쓰기 애매하다.
* 플러스마이너스 키는 나중에 구현하고
* 사칙연산 우선순위를 먼저 구현해보자.
* 마찬가지로 마이너스 처리도 추후에 맥북처럼 바로바로 값을 초기화 시키거나
* 괄호를 넣는 방법으로 처리해야겠다.
*/
}else if(inputValue.matches("[÷+x-]") || inputValue.matches("%") ) {
// .matches안에 조건을 합치기 위해 정규표현식에서 %를 어떻게 넣을 수 있는지 찾아보자
// 사칙연산일 때,
if(inputSpace.getText().equals("") ) {
// 빈칸인데 사칙연산이 들어왔을 경우, 빈칸 유지
inputSpace.setText("");
}else if(prev_operation.matches("[÷+x-]") || prev_operation.matches("%") || prev_operation.matches("±")){
// 사칙연산 다음에 또 사칙연산이 들어왔을 경우,
// 마지막 글자(사칙연산)를 잘라서 새로운 사칙연산으로 대체해준다.
String is = inputSpace.getText();
String lastS = is.substring(0, is.length() -1 );
inputSpace.setText("");
inputSpace.setText(lastS + inputValue);
}else {
// 숫자 다음에 또 사칙연산이 들어왔을 경우 (정상)
inputSpace.setText(inputSpace.getText() + inputValue);
}
} else {
// 숫자일때 => 연결연결
inputSpace.setText(inputSpace.getText() + inputValue);
}
// prev_operation에 다음 키와 비교위해 현재 버튼 값 저장.
prev_operation = inputValue;
}
}
private double calculate(String inputText) {
// 5번에 구현 예정
}
public static void main(String[] args) {
new Calculator();
}
}
4. 연산자 우선순위 계산
calculate 메서드를 통해 계산을 하기 전에, 연산자 우선순위를 처리하여 주기 위하여 preprocess 메서드를 만들었다.
나름 계산기의 핵심이니까 private로 접근을 제어해준다.
사실 이 코드는 너무 민망하다..ㅠㅠ 좀 더 맑은 정신일 때 재 도전해 보도록한다.
"스택계산기 구현 : 연산자 우선순위와 결합 순서만 생각해 봅시다." 를 보고 참고하였다.
일단 설명을 하자면 Stack을 활용해서 우선순위를 구현하여 주는거였다.
- numStack과 opStack 에 각각 숫자와 연산자를 따로 넣어준다.
- 반복문을 돌면서 우선순위가 높은 *, /, %가가 나오면 우선처리하고, 그 값을 numStack에 넣어준다.
여기까지는 좋았다.
근데 마지막 우선순위 연산자가 나오면 캐치를 못해주더라.
연산자를 먼저 stack에 넣어주고 빼서 계산해주면 되잖아?! 했지만 numStack에서 2개를 빼올수가 없어서 할수없었따..
그래서 마지막에 똑같이 한번 더 우선순위 연산자를 실행해 주었다. ㅠ
똑같은 코드를 두번 작성하는게 영 찝찝했지만 일단 잘되는게 목적이었다..
다른 포스팅을 따라해 보려고 해도 앞에 다른 코드들이 너무 달라서 그 부분만 가져다가 쓸수는 없었다. ㅠ
아래 코드는 다음과 같이 동작한다. 최종적으로는 +와 - 연산 할 것만 남아있게 된다.
public class Calculator extends JFrame {
....
Stack<Double> numStack = new Stack<>();
Stack<Character> opStack= new Stack<>();
public Calculator() {
....
}
public void makeFrame(JButton c, int x , int y, int w, int h) {
....
}
class ButtonActionListener implements ActionListener{
....
}
private void preprocess(String inputText) {
numStack.clear();
opStack.clear();
for(int i=0; i<inputText.length(); i++) {
char ch = inputText.charAt(i);
if(ch == '-' || ch == '+' || ch == 'x' || ch== '÷' || ch== '%') {
if(num != "" ) numStack.add(Double.valueOf(num));
if(!opStack.isEmpty() && (opStack.peek().equals('x' ) || opStack.peek().equals('÷') || opStack.peek().equals('%' ))) {
double n1 = numStack.pop();
double n2 = numStack.pop();
Character oper = opStack.pop();
if(oper.equals('x')){
numStack.add(n2*n1);
}else if(oper.equals('%')){
numStack.add(n2%n1);
}else if(oper.equals('÷')) {
numStack.add(n2/n1);
}
}
opStack.add(ch);
num ="";
} else {
num = num + ch;
}
if( i == inputText.length()-1) {
if(ch == '-' || ch == '+' || ch == 'x' || ch== '÷' || ch== '%') {
opStack.pop();
}
if(!opStack.isEmpty() && (opStack.peek().equals('x') || opStack.peek().equals('÷') || opStack.peek().equals('%'))){
double n1 = Double.valueOf(num);
double n2 = numStack.pop();
Character oper = opStack.pop();
if(oper.equals('x')){
numStack.add(n2*n1);
}else if(oper.equals('%')){
numStack.add(n2%n1);
}else if(oper.equals('÷')) {
numStack.add(n2/n1);
}
}else {
if(num != "" ) numStack.add(Double.valueOf(num));
}
}
}
}
private double calculate(String inputText) {
preprocess(inputText);
// 5번에 구현 예정
}
public static void main(String[] args) {
new Calculator();
}
}
5. 연산자 후순위 계산
연산자가 남아 있고, 숫자가 2개 이상이면 while문을 계속 돌면서
숫자가 1개만 되도록(최종 결과 값) 하여 반환해준다.
public class Calculator extends JFrame {
....
public Calculator() {
....
}
public void makeFrame(JButton c, int x , int y, int w, int h) {
....
}
class ButtonActionListener implements ActionListener{
....
}
private void preprocess(String inputText) {
....
}
private double calculate(String inputText) {
preprocess(inputText);
while(!opStack.isEmpty() && numStack.size() >= 2) {
double n1 = numStack.pop();
double n2 = numStack.pop();
Character op = opStack.pop();
if(op == '+') {
numStack.add(n1+n2);
}else if(op == '-'){
numStack.add(n2-n1);
}
}
return numStack.pop();
}
public static void main(String[] args) {
new Calculator();
}
}
6. 마우스 이벤트 추가
마지막으로 버튼이 눌리는걸 좀 더 생동감있게 표현하기 위하여 마우스 이벤트를 주었다.
MouseListener를 implements 하여 마우스 이벤트들을 override하였다.
나는 마우스를 클릭하고 있을때 (mousePressed)와 클릭해제했을 때 (mouseReleased) 배경색상에 변화를 주었다.
맥북 계산기가 누를때 배경색이 바뀌길래 나도 추가해봤다.!
MouseActionListener 객체와 만날수 있는 시간이었다.
public class Calculator extends JFrame {
....
MouseActionListener me = new MouseActionListener();
public Calculator() {
....
for(int i = 0; i<button_names.length; i++) {
....
buttons[i].addMouseListener(me);
}
....
}
public void makeFrame(JButton c, int x , int y, int w, int h) {
....
}
class ButtonActionListener implements ActionListener{
....
}
private void preprocess(String inputText) {
....
}
private double calculate(String inputText) {
....
}
class MouseActionListener implements MouseListener{
@Override
public void mousePressed(MouseEvent e) {
JButton jb = (JButton)e.getSource();
int target = buttonString.indexOf(jb.getText());
buttons[target].setBorder(new LineBorder(Color.black));
if(jb.getText().matches("[÷+=x-]")) {
buttons[target].setBackground(Color.green);
}else if(jb.getText().matches("[C±%]")) {
buttons[target].setBackground(Color.green);
}else {
buttons[target].setBackground(Color.green);
}
}
@Override
public void mouseReleased(MouseEvent e) {
JButton jb = (JButton)e.getSource();
int target = buttonString.indexOf(jb.getText());
buttons[target].setBorder(tB);
// 버튼 색
if(jb.getText().matches("[÷+=x-]")) {
buttons[target].setBackground(new Color(255, 159, 9));
}else if(jb.getText().matches("[C±%]")) {
buttons[target].setBackground(new Color(97, 99, 102));
}else {
buttons[target].setBackground(new Color(123, 125, 127));
}
}
@Override
public void mouseEntered(MouseEvent e) { }
@Override
public void mouseExited(MouseEvent e) { }
@Override
public void mouseClicked(MouseEvent e) { }
}
public static void main(String[] args) {
new Calculator();
}
}
7. 전체 코드
깃허브 주소 참고하세용
https://github.com/KimYeaSeul/Calculator.git
8. 보완 & 추가 할 부분
맥북 계산기를 완벽하게 따라하지 못한 부분이 아쉽다.
일단 맥북이랑 똑같이 하기 위해서는
- inputSpace에 현재 입력한 숫자만 나오게 설정 ( ex)53 , 사칙연산값은 뒤에서 처리하는것같고, 사칙연산을 누른뒤에 숫자가 나오면 입력값이 또 초기화 되고 현재 입력한 값만 나온다.)
- 입력값을 바로바로 처리하기 때문에 맥북은 우선순위 기능이 없다..
- 플러스마이너스 기능 구현
- % 기능 구현 => %가 나머지 인줄 알았는데 퍼센테이지로 바꿔주는거였다 ㅋㅋ큐ㅠㅠ
- 처음에 0으로 초기화 시키는거
- C(취소)와 AC(전체취소) 버튼 구현
- 버튼눌렀을 때 색 더 비슷하게 예쁘게 표현하기
- 결과 값이 정수일땐 정수로만 표현
- inputSpace에 값이 많아지면 font작아지게 설정
- 파일 하나에 코드 량이 많은것 같은데 나눌수 있을지
- 정규표현식 사용하여 조건문 줄일 수 있을지
근데 맥북 계산기가 생각보다 좋은것 같진 않다.
한번 입력을 마치면 돌아갈수 없다는거(?)
그래서 나중에는 모든 수식이 inputSpace에 나오면서 위 기능들을 다 구현할수 있으면 좋을것같다..^^
9. 마치며
Java GUI에 대해 이해할 수 있는 시간이었다.
우리가 실행하는 os 소프트웨어 프로그램들도 이런식으로 다 코딩이 되어 있는거라고 생각하니까 뭐랄까..
컴퓨터와 한층 더 가까워진 느낌이다.!! ㅋㅋㅋ
계산기라는 보여지는 결과물이 있으니 또 재밌게 할수 있었던것 같다.
Reference
[JAVA] JAVA의 GUI 기초, awt, swing 작성 절차, 컨테이너와 컴포넌트, 배치관리자
JAVA-AWT-JPanel-07(GridBagLayout)03(계산기화면)
스택계산기 구현 : 연산자 우선순위와 결합 순서만 생각해 봅시다.
'공부 > JAVA' 카테고리의 다른 글
[JAVA] Extends 와 Super (0) | 2022.06.22 |
---|---|
[JAVA] 제네릭(Generic) (0) | 2022.06.21 |
[JAVA] 배열복사 메서드 ( Object.clone, Arrays.copyOf(Range), System.arrayCopy) (0) | 2022.06.11 |
ArrayList(LinkedList) 를 String으로 변환하는 방법 (JAVA) (0) | 2022.05.24 |
[JAVA] 변수와 자료형 (0) | 2021.10.17 |
댓글