Java 이벤트 처리

1. 이벤트 기반 프로그래밍


이벤트 기반 프로그래밍은 이벤트의 발생에 의해 프로그램 실행 흐름이 결정되는 방식의 프로그래밍 패러다임이다. 이벤트는 키 입력, 마우스 클릭, 마우스 드래그 등 사용자의 액션이나, 센더 등 외부 장치로부터의 입력, 네트워크를 통한 데이터 수신, 다른 스레드나 프로그램으로부터의 메시지 수신 등에 의해 발생한다. 이벤트 기반 응용프로그램은 각 이벤트를 처리하는 이벤트 리스터들을 보유하며, 이벤트가 발생할 때마다 리스너가 실행된다.

이벤트 기반 GUI 응용프로그램 구조


GUI 응용프로그램에서 New 라는 버튼을 클릭하여 이벤트가 발생하고 처리되는 과정은 다음과 같다.

  1. 사용자가 new 버튼 클릭
  2. 버튼 클릭은 운영체제의 마우스 드라이버를 거쳐 자바 가상 기계에 전달
  3. 자바 가상 기계는 이벤트 분배 스레드에게 마우스 클릭에 관한 정보를 전달
  4. 이벤트 분배 스레드는 이벤트 객체를 생성, 이벤트 객체는 이벤트에 관한 여러 정보를 담은 객체로 담은 정보 중 이벤트를 발생시킨 컴포넌트를 이벤트 소스라고 부름. 여기서 이벤트 소스는 new 글자가 새겨진 JButton 컴포넌트
  5. 이벤트 분배 스레드는 JButton에 연결된 이벤트 리스터를 찾아 호출
  6. 이벤트 분배 스레드는 이벤트 리스터로부터 리턴한 후 다음 이벤트 기다림
  • 이벤트 소스 : 이벤트를 발생시킨 GUI 컴포넌트
  • 이벤트 객체 : 발생한 이벤트에 대한 정보를 담는 객체
  • 이벤트 리스터 : 이벤트를 처리하는 코드로서 컴포넌트에 등록되어야 작동 가능
  • 이벤트 분배 스레드 : 이벤트 기반 프로그래밍의 핵심 요소로서 무한 루프를 실행하는 스레드, 자바 가상 기계로부터 이벤트의 발생을 통지받아, 이벤트 소스와 이벤트 종류를 결정하고 이에 따라 적절한 이벤트 객체를 생성하고 이벤트 리스너를 찾아 호출


2. 이벤트 객체


현재 발생한 이벤트에 관한 정보를 가진 객체로 이벤트 리스너에게 전달된다. 발생한 이벤트에 따라 다르지만 대략적으로 다음 정보를 준다.

  • 이벤트 종류
  • 이벤트 소스
  • 화면 내 이벤트가 발생항 마우스 좌표
  • 컴포넌트 내 이벤트가 발생한 마우스 좌표
  • 버튼이나 메뉴 아이템에 이벤트가 발생한 경우 버튼이나 메뉴 아이템의 문자열
  • 클릭된 마우스 버튼 번호
  • 마우스 클릭 횟수
  • 키가 눌러졌다면 키의 코드 값과 문자 값
  • 체크박스, 라디오버튼에 이벤트가 발생하였다면 체크 상태

이벤트 객체는 메서드를 통해 이벤트 정보를 제공한다. 객체마다 여러 메서드들이 있는데 이벤트 리스너에서는 getSource() 메서드가 가장 많이 사용된다. getSource는 현재 발생한 이벤트의 소스 컴포넌트의 레퍼런스를 리턴한다. 리턴타입이 Object라 캐스팅해서 사용해야 한다.

JButton b = (JButton)이벤트객체.getSource();

이벤트 객체와 이벤트 소스 그리고 이벤트가 발생하는 경우는 책 p.542 표를 참고하자.

3. 이벤트 리스너


이벤트 리스너란 이벤트를 처리하는 자바 프로그램 코드로서 클래스로 만든다. JDK는 이벤트 리스너 인터페이스를 제공하며, 개발자는 이 인터페이스를 상속받고 추상 메서더를 모두 구현하여 이벤트 리스너를 작성한다. 제공하는 리스너 인터페이스는 책 p544의 표를 참고하자.
컴포넌트는 여러 개의 이벤트 리스너를 가질 수 있다. 이런 경우 등록된 반대순으로 리스너가 실행된다.

이벤트 리스터 작성 과정

  1. 이벤트와 이벤트 리스터 선택 - 목적에 적합한 이벤트와 리스너 인터페이스 선택
  2. 이벤트 리스너 클래스 작성 - 리스너 인터페이스를 상속받은 클래스를 작성하고 추상 메서드 모두 구현
  3. 이벤트 리스너 등록 - 이벤트를 받을 스윙 컴포넌트에 이벤트 리스너 등록
    • 컴포넌트에 이벤트 리스너를 등록하기 위해서는 컴포넌트.addXXXListener() 메서드를 사용한다.

버튼을 클릭하면 버튼 문자열이 바뀌도록 하는 이벤트 리스너를 만들어 보자.

// 인터페이스 구현 클래스 만들기
public class MyActionListener implements ActionListener{	

	@Override
	public void actionPerformed(ActionEvent e) {
		JButton b = (JButton)e.getSource(); // 사용자가 클릭한 버튼 컴포넌트(이벤트 소스) 알아내기
		if (b.getText().equals("Action"))
			b.setText("액션");
		else
			b.setText("Action");		
	}
}

// 컨텐트팬 만들기
public class ContentPaneEx extends JPanel{
	
	public ContentPaneEx() {
		JButton a = new JButton("ok"); // 버튼 만들기
		MyActionListener m = new MyActionListener(); // 이벤트 리스너 객체 만들기
				
		a.addActionListener(m);	 // 컴포넌트에 이벤트리스터 삽입
		
		add(a); // 팬에 부착
		add(new JButton("cancel"));
		add(new JButton("ignore"));
	}
}

// 프레임 만들기
public class MyFrame extends JFrame{
	public MyFrame() {		
		setTitle("350*350 스윙 프레임 만들기");
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		
		setContentPane(new ContentPaneEx()); // 컨텐트 팬 새걸로 갈아끼기
		
		
		setSize(300,300); // 프레임 크기 설정
		setVisible(true); // 프레임 화면에 출력
	}
	

	public static void main(String[] args) {
		MyFrame frame = new MyFrame();		
	}

}

위와 같은 코드가 독립 클래스로 Action 이벤트 리스너를 작성한 것이다. 이 방법은 리스너 코드가 길 때 적합하다. 하지만 MyActionListener에서 다른 클래스의 멤버에 접근하기 어려운 단점이 있다. 내부 클래스로 이벤트 리스너를 작성하면 프레임에도 접근이 가능해져서 간단한 경우라면 내부 클래스로 작성하는게 좋다.

public class Test1 extends JFrame{

	public Test1() {
		setTitle("내부 클래스로 이벤트 리스너 작성");
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		Container contentPane = getContentPane();
		contentPane.setLayout(new FlowLayout());
		JButton btn = new JButton("Action");
		btn.addActionListener(new MyActionListener());
		contentPane.add(btn);
		
		setSize(350,150);
		setVisible(true);	
		
	}
	// 내부 클래스로 action 리스너 작성
	private class MyActionListener implements ActionListener{

		@Override
		public void actionPerformed(ActionEvent e) {
			JButton b = (JButton)e.getSource();
			if (b.getText().equals("Action"))
				b.setText("액션");
			else
				b.setText("Action");
			
			// MyActionListener이 Test1의 멤버와 같으므로
			// Test1의 멤버나 슈퍼클래스인 JFrame에 접근할 수 있다.
			Test1.this.setTitle(b.getText());
      // 그냥 바로 settitle 써도 상관없음
			
		}	
	}
	public static void main(String[] args) {
		new Test1();
	}
}

만약 이벤트 리스너가 한 군데에서만 사용하는 경우 그냥 익명 클래스를 이용하는게 편하다.

4. 어댑터 클래스


리스너 인터페이스를 상속받아 이벤트 리스너의 구현 클래스를 만들 때 메서드를 하나만 사용하고 싶어도 모두 구현해야한다는 부담이 있다. JDK에서는 이러한 부담을 줄여주기 위해 인터페이스를 미리 구현해 높은 클래스를 제공하는데 이것이 어댑터 클래스이다. 어댑터 클래스는 모든 메서드가 단순 리턴하도록만 작성되어있다. 추상 메서드가 1개인 경우에는 굳이 어댑터 클래스를 사용할 필요가 없으므로 대응하는 어댑터 클래스가 없다. 어댑터 클래스의 유무는 p544를 확인하자.

// MouseListener 인터페이스의 구현 클래스 MouseAdapter에는 빈 깡통으로 메서드들이 구현되어 있다.
// 그러므로 이 클래스를 상속받아서 필요한 메서드만 오버라이딩 해주고 이걸 이벤트 리스너로 사용해서
// 컴포넌트에 add 해주면 된다.
class MyMouseAdapter extends MouseAdapter{

		@Override
		public void mouseClicked(MouseEvent e) {
			// TODO Auto-generated method stub
			super.mouseClicked(e);
		}
		
	}	


5. key 이벤트와 포커스


key 이벤트는 사용자가 키를 입력할 때 발생하는 이벤트이며, 모든 컴포넌트가 key 이벤트를 받을 수 있다. 그러나 응용프로그램 내에 포커스를 가진 컴포넌트가 키 입력을 독점하기 때문에, 현재 포커스를 가진 컴포넌트에만 key 이벤트가 전달된다. 포커스란 키 입력의 독점권을 뜻한다. 즉, 버튼을 누르기 위해 enter을 눌러도 버튼이 포커스를 가지고 있지 않다면 key 이벤트를 받을 수 없는 것이다.
어떤 컴포넌트에 키를 입력하고자 하면 tab이나 마우스 클릭으로 포커스를 해당 컴포넌트에게 이동시켜줘야 한다. 그러기 위해서는 다음과 같은 코드가 반드시 필요하다.

component.setFocusable(true); // component가 포커스를 받을 수 있도록 설정
component.requestFocus(); // component에게 포커스를 주어 키 입력을 받을 수 있도록 설정

어떤 컴포넌트에 포커스를 주고자 한다면, 스윙 프레임에서 setVisible(true) 코드 이후에 작성해야한다. setVisible은 프레임을 출력한 후 포커스를 임의로 움직이기 때문이다.
마우스 클릭으로 포커스를 주는 방법은 다음과 같다.

component.addMouseListener(new MouseAdapter(){
  public void mouseClicked(MouseEvent e){
    Component c = (Component)e.getSource(); // 해당 컴포넌트로 캐스팅
    c.setFocusable(true); // 포커스 가능하도록 설정
    c.requestFocus(); // 마우스 클릭된 컴포넌트에 포커스 설정

  }

});


key 이벤트와 keyListener

keyListener은 3개의 추상 메서드로 구성되어 있다.

  • keyPressed() : 키를 누르는 순간
  • keyReleased() : 누른 키를 떼는 순간
  • keyTyped() : 누른 키가 유니코드이며 떼는 순간
    • 유니코드 입력시 keyPressed, keyTyped, keyReleased 순서로 호출


입력된 키 판별

키 이벤트가 발생하면 입력된 키 정보가 keyEvent 객체에 담겨 리스너에게 전달된다. keyEvent 객체에서는 다음 2개의 메서드를 통해 입력된 키를 판별한다. 유니코드인 경우 getKeyChar, 아닌 경우 getKeyCode를 사용하면 된다.

  • char KeyEvent.getKeyChar() : 입력된 키의 문자코드(유니코드 값)을 리턴하며, 유니코드 키가 아닌 경우 KeyEvent.CHAR_UNDEFINED를 리턴한다.
  • int KeyEvent.getKeyCode() : 모든 키에 대해 정수형의 키 코드 값을 리턴한다. 키 코드는 운영체제나 하드웨어에 따라 다를 수 있으므로 반드시 getKeyCode가 리턴한 키 코드와 가상 키 값을 비교해야 한다. 가상 키는 KeyEvent 클래스에 VK_ 로 시작하는 static 상수로 선언되어 있다. 가상 키는 p560의 표 10-5를 참고하자.


활용

키 이름 문자열은 KeyEvent 클래스의 static 타입 getKeyText 메서드로부터 얻을 수 있다. 키 코드 값을 인자로 받아 키 이름 문자열을 리턴한다.

static String KeyEvent.getKeyText(int keyCode)


그림1

키 입력받을 수 있도록 컨텐트팬에 포커스를 강제로 설정하고 키 리스너로 입력받은 것을 찍어보자. 위 그림은 실행하고 8을 입력했을 때이다.

public class KeyListenerEx extends JFrame {
	private JLabel [] keyMessage; // 3개의 메시지를 출력할 레이블 컴포넌트 배열
	
	public KeyListenerEx() {
		setTitle("keyListener 예제");
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		
		Container c = getContentPane();
		c.setLayout(new FlowLayout());
		c.addKeyListener(new MyKeyListener()); 		
		
		keyMessage = new JLabel [3]; // 레이블 컴포넌트 배열 생성 후 대입
		keyMessage[0] = new JLabel(" getKeyCode()  ");
		keyMessage[1] = new JLabel(" getKeyChar()  ");
		keyMessage[2] = new JLabel(" getKeyText()  ");
		
		// 만든 레이블 컴포넌트 컨텐트팬에 부착
		for(int i=0; i<keyMessage.length; i++) {
			c.add(keyMessage[i]);
			keyMessage[i].setOpaque(true); // 배경색이 보이도록 불투명 속성 설정
			keyMessage[i].setBackground(Color.YELLOW); // 레이블 배경색 설정
		}
		
		setSize(300,150);
		setVisible(true);
		
		// 프레임 출력 이후 컨텐트팬으로 포커스 설정
		c.setFocusable(true);
		c.requestFocus();
	}
	
	// KeyPressed만 사용할 것이므로 어텝터 클래스 상속
	class MyKeyListener extends KeyAdapter {
    @overried
		public void keyPressed(KeyEvent e) {
			int keyCode = e.getKeyCode(); // 입력 받은 것의 키코드 값
			char keyChar = e.getKeyChar(); // 키 문자 값
      // 레이블 텍스트 수정
			keyMessage[0].setText(Integer.toString(keyCode)); // 키 코드 값 
			keyMessage[1].setText(Character.toString(keyChar)); // 유니코드 값
			keyMessage[2].setText(KeyEvent.getKeyText(keyCode)); //  키의 이름 문자열
			
		}
	}
	
	public static void main(String [] args) {
		new KeyListenerEx();
	}
}


6. 마우스 이벤트


Mouse 이벤트는 사용자의 마우스 조작에 따라 총 8가지 경우에 발생한다. 모든 스윙 컴포넌트가 Mouse 이벤트를 받을 수 있고, 발생하면 MouseEvent 객체나 MouseWheelEvent 객체가 리스너의 메서드에 전달된다.

Mouse 이벤트가 발생하는 경우리스너의 메서드리스너
마우스가 컴포넌트 위에 올라갈 때void mouseEntered(MouseEvent e)MouseListener
마우스가 컴포넌트 위에 내려올 때void mouseExited(MouseEvent e)MouseListener
마우스가 버튼이 눌러졌을 때void mousePressed(MouseEvent e)MouseListener
눌러진 버튼이 떼어질 때void mouseReleased(MouseEvent e)MouseListener
마우스 컴포넌트를 클릭하였을 때void mouseClicked(MouseEvent e)MouseListener
마우스가 드래그되는 동안void mouseDragged(MouseEvent e)MouseMotionListener
마우스가 움직이는 동안void mouseMoved(MouseEvent e)MouseMotionListener
마우스 휠이 구르는 동안void mouseWheelMoved(MouseEvent e)MouseMotionListener
  • 마우스가 눌러진 위치에서 그대로 떼어지면 : mousePressed, mouseReleased, mouseClicked 순서로 호출
  • 마우스가 드래그되면 : mousePressed, mouseDragged … mouseReleased 순으로 호출


MouseEvent 객체

MouseEvent, MouseMotion 이벤트 정보를 제공하는 객체이다. 다음과 같은 메서드를 제공한다.

  • int getX() : 마우스 x 위치 리턴
  • int getY() : 마우스 y 위치 리턴
  • Point getPoint() : 마우스 포인터의 위치를 Point 객체로 리턴
  • int getClickCount() : 마우스 클릭 횟수 리턴, 더블클릭 인식할 때 사용
  • int getButton() : 눌러진 마우스 버튼의 번호를 리턴, 왼쪽 마우스 버튼 BUTTON1, 오른쪽 BUTTON3
public class MouseEventEx extends JFrame {
	private JLabel la = new JLabel("No Mouse Event");

	public MouseEventEx() {
		setTitle("mouseListener, MouseMotionListener 예제");
		setDefaultCloseOperation(EXIT_ON_CLOSE);

		Container contentPane = getContentPane();
		contentPane.setLayout(new FlowLayout());
		
    // 리스너 만들고
		MyMouseListener ml = new MyMouseListener();
    // 컨텐트팬에 붙이고
		contentPane.addMouseListener(ml);
		contentPane.addMouseMotionListener(ml);
		
    // 라벨 컨텐트팬에 붙이고 
		contentPane.add(la);
    // 프레임 크기조정, 화면 출력 보이기 설정
		setSize(300,200);
		setVisible(true);

	}

	// 구현 클래스
	class MyMouseListener implements MouseListener, MouseMotionListener {

		@Override
		public void mouseDragged(MouseEvent e) {
			// 마우스가 드래깅 되는 동안 계속 호출
			la.setText("mousePressed (" + e.getX() + "," + e.getY() + ")");

		}

		@Override
		public void mouseMoved(MouseEvent e) {
			// 마우스가 움직이는 동안 계속 호출
			la.setText("mousePressed (" + e.getX() + "," + e.getY() + ")");

		}

		@Override
		public void mouseClicked(MouseEvent e) {

		}

		@Override
		public void mousePressed(MouseEvent e) {
			// 마우스가 눌러진 위치
			la.setText("mousePressed (" + e.getX() + "," + e.getY() + ")");

		}

		@Override
		public void mouseReleased(MouseEvent e) {
			// 마우스 떼어진 위치
			la.setText("mouseReleased (" + e.getX() + "," + e.getY() + ")");

		}

		@Override
		public void mouseEntered(MouseEvent e) {
			// 마우스가 올라간 컴포넌트를 알아내서 배경색 변경
			Component c = (Component) e.getSource();
			c.setBackground(Color.cyan);

		}

		@Override
		public void mouseExited(MouseEvent e) {
			// 마우스가 내려갔을 때 컴포넌트를 알아내서 배경색 변경
			Component c = (Component) e.getSource();
			c.setBackground(Color.yellow);

		}

	}
	
	public static void main(String[] args) {
		new MouseEventEx();
	}

}



본 포스팅은 ‘명품 JAVA Programming’를 읽고 공부한 내용을 바탕으로 작성하였습니다.


© 2021. By Backtony