본문 바로가기
공부/JAVA

[JAVA] GUI 계산기 만들기

by yeaseul912 2022. 5. 21.
728x90

웹개발을 위해 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을 활용해서 우선순위를 구현하여 주는거였다.

  1. numStack과 opStack 에 각각 숫자와 연산자를 따로 넣어준다.
  2. 반복문을 돌면서 우선순위가 높은 *, /, %가가 나오면 우선처리하고, 그 값을 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. 보완 & 추가 할 부분

  맥북 계산기를 완벽하게 따라하지 못한 부분이 아쉽다.

  일단 맥북이랑 똑같이 하기 위해서는

  1. inputSpace에 현재 입력한 숫자만 나오게 설정 ( ex)53 , 사칙연산값은 뒤에서 처리하는것같고, 사칙연산을 누른뒤에 숫자가 나오면 입력값이 또 초기화 되고 현재 입력한 값만 나온다.)
  2. 입력값을 바로바로 처리하기 때문에 맥북은 우선순위 기능이 없다..
  3. 플러스마이너스 기능 구현
  4. % 기능 구현 => %가 나머지 인줄 알았는데 퍼센테이지로 바꿔주는거였다 ㅋㅋ큐ㅠㅠ 
  5. 처음에 0으로 초기화 시키는거
  6. C(취소)와 AC(전체취소) 버튼 구현
  7. 버튼눌렀을 때 색 더 비슷하게 예쁘게 표현하기
  8. 결과 값이 정수일땐 정수로만 표현
  9. inputSpace에 값이 많아지면 font작아지게 설정
  10. 파일 하나에 코드 량이 많은것 같은데 나눌수 있을지
  11. 정규표현식 사용하여 조건문 줄일 수 있을지

근데 맥북 계산기가 생각보다 좋은것 같진 않다. 

한번 입력을 마치면 돌아갈수 없다는거(?)

그래서 나중에는 모든 수식이 inputSpace에 나오면서 위 기능들을 다 구현할수 있으면 좋을것같다..^^


9. 마치며

Java GUI에 대해 이해할 수 있는 시간이었다.

우리가 실행하는 os 소프트웨어 프로그램들도 이런식으로 다 코딩이 되어 있는거라고 생각하니까 뭐랄까..

컴퓨터와 한층 더 가까워진 느낌이다.!! ㅋㅋㅋ

계산기라는 보여지는 결과물이 있으니 또 재밌게 할수 있었던것 같다.

 

Reference

[JAVA] JAVA의 GUI 기초, awt, swing 작성 절차, 컨테이너와 컴포넌트, 배치관리자

JAVA-AWT-JPanel-07(GridBagLayout)03(계산기화면)

스택계산기 구현 : 연산자 우선순위와 결합 순서만 생각해 봅시다.

 

반응형

댓글