JFC(Swing과 2D 그래픽)
1999년 4월 14일 첫 출판, 글/윤경구
Copyright © 1999 Yoon Kyung Koo. All Rights Reserved.
무단 전재를 금합니다.
이 글은 월간지 마이크로소프트웨어 1999년 5월호의 마소 주니어에
실린 자바 이야기의 초고에 기반을 하고 있습니다.
자바 이야기를 포함한 1999년 1월부터 여섯 달동안 연재되었던 마소 주니어 원고들은
"초보자를 위한 프로그래밍 마스터"란 한 권의 단행본으로 묶여 북마크
출판사에서 출간되었습니다.
참고하시기 바랍니다.
[1. Swing] - [2. 2D Graphics]
여기에서 다룰 내용은 자바의 멋진 외양에 대한 것입니다. 아무리 맛있는 빵이라 하더라도 겉보기에 먹음직스럽지 않으면 쉽게 손이 가지 않습니다. 훌륭한 요리를 평가할 때에 맛뿐만 아니라 색상과 향 등 겉모양도 중요한 기준이 되듯이, 좋은 프로그래밍 도구라면 멋지고 편리한 그래픽 사용자 인터페이스를 제공하여 사용자들의 호감을 얻을 수 있어야 합니다.
자바는 단순한 프로그래밍 언어가 아니라 프로그래밍 개발 도구로서의 특성도 가지는 복합적인 개발 플랫폼입니다. AWT라는 자바의 독특한 윈도우 인터페이스가 있지만 여기에서 소개하는 녀석은 훨씬 더 멋진 모습을 하고 있습니다. 댄스 음악의 한 장르이자, 그네라는 뜻도 가진 스윙이 바로 이 멋진 녀석의 이름입니다.
AWT 윈도우 컴포넌트들은 모두 각 운영체제에 고유한 윈도우 컴포넌트들을 빌어쓰는 방식으로 구현되어 있습니다.
예를 들어 버튼을 나타내는 Button 컴포넌트는 마이크로소프트 윈도우 운영체제에서는 마이크로소프트 윈도우가 제공하는 버튼 컴포넌트를 빌어쓰고, 유닉스 운영체제에서는 X 윈도우 시스템이 제공하는 버튼 위젯을 빌어씁니다. 그래서, 자바 프로그램이 실행되는 운영 체제에 따라 그 모양이 조금씩 다르게 나타나는 단점이 있었습니다.
스윙 컴포넌트는 AWT 윈도우 컴포넌트의 이런 한계를 극복하여 자바에 고유한 컴포넌트로 설계되었습니다. 자바 코드로 각 컴포넌트들의 외양과 기능을 완전히 새롭게 설계하였기 때문에 각 운영체제가 제공하는 윈도우 시스템에 대한 의존도가 없으며 아주 잘생긴 데다가 기능도 다양해졌습니다.
<그림> 스윙 컴포넌트를 사용한 예제 프로그램
스윙 컴포넌트는 흔히 JFC라고도 불려지는데 여기에서 JFC는 자바 파운데이션 클래스(Java Foundation Class)의 머리글자입니다.
사실 JFC는 스윙 컴포넌트 기술만을 지칭하는 이름이 아니라, JDK 1.0 버전(현재는 1.2버전입니다)의 AWT 클래스 기능을 대체할 새로운 다중 플랫폼 사용자 인터페이스인 스윙 컴포넌트를 핵심으로, 개선된 그래픽 기능인 2D 그래픽, 신체부자유자에 대한 지원, 그리고 기존의 AWT 등 새로운 자바의 그래픽 사용자 인터페이스와 그래픽 기능에 관련된 핵심 기반 기술을 제공하는 클래스들을 포괄하는 이름입니다. JFC의 인터넷 홈페이지는 http://java.sun.com/products/jfc 이며 여기에서 JFC에 대한 최근의 소식을 접할 수 있습니다.
여기에서는 자바의 아름다운 스윙 컴포넌트와 그래픽 기능을 크게 확장한 2D 그래픽을 다룹니다. 스윙 컴포넌트에 대한 설명은 AWT 컴포넌트를 이해하고 있다는 전제 하에서 진행할 것이므로, 혹시 AWT 윈도우 시스템에 대해서 생소하신 독자들이 계시다면 먼저 AWT 윈도우 시스템에 대하여 공부하실 필요가 있습니다.
1. 새로운 얼굴, 스윙 셋
스윙 셋은 자바 2에 포함된 새로운 사용자 인터페이스 컴포넌트들의 집합을 가리키는 말입니다.
자바 2가 등장하기 이전에는 AWT 컴포넌트들이 제공되었는데 이들은 자바가 실행되는 운영 체제에서 제공하는 윈도우 시스템의 컴포넌트들을 빌어서 자바의 윈도우 기능을 실현하는 방식을 채택하고 있었습니다. 이 방식에서는 자바의 각 윈도우 컴포넌트에 해당하는 운영 체제 윈도우 시스템의 컴포넌트를 필요로 하였기 때문에 수행 성능이나 구현의 이점이 있었지만, 기존 컴포넌트들의 제약으로 인해 다양한 기능을 제공할 수 없었고, 실행되는 운영 체제에 따라 실행되는 프로그램의 모습이 조금씩 달라지는 단점이 있었습니다.
스윙 컴포넌트들은 자바가 직접 각 컴포넌트들을 렌더링하고 기능을 구현하는 새로운 기술인 경량(lightweight) 컴포넌트 기법을 적용하여 만든 컴포넌트들로 기반 운영 체제의 윈도우 시스템에 대응하는 컴포넌트가 있을 필요가 없어, 편리하고 다양한 사용자 인터페이스를 제공할 수 있습니다. 스윙 컴포넌트를 기본으로 포함한 자바 2 플랫폼이 개발도구인 JDK 1.2 버전과 함께 1998년 말에 소개되면서, 스윙 컴포넌트는 기존의 AWT 컴포넌트들을 제치고 자바의 표준 인터페이스로 각광받고 있습니다. 이제 스윙 컴포넌트들을 만나볼까요?
1.1 스윙 컴포넌트들
javax.swing 패키지에는 다양한 스윙 컴포넌트 클래스들이 들어 있습니다. 먼저 다음과 같은 AWT 컴포넌트에 대응하는 스윙 컴포넌트 클래스들이 존재합니다. 이들 스윙 컴포넌트의 이름은 대부분 AWT의 컴포넌트 클래스들 이름 앞에 자바의 첫 영문자인 'J'를 붙인 모양을 하고 있습니다.
JButton, JMenuItem, JMenu, JCheckBox, JLabel, JList, JMenuBar, JPanel, JPopupMenu, JScrollBar, JScrollPane, JTextArea, JTextField, JApplet, JDialog, JFrame, JWindow
스윙 컴포넌트들은 단순히 AWT 컴포넌트들을 다시 구현하는 데 그치지 않고 기능을 대폭 개선하였으며 뿐만 아니라 유용한 새로운 컴포넌트들을 여럿 선보였습니다. AWT 컴포넌트에 대응하는 컴포넌트들도 대부분 AWT 컴포넌트의 사용 방법을 지원하긴 하지만 외양도 많이 달라지고 메쏘드들이 많이 추가되었으므로 JDK 1.2 API 문서를 꼭 읽어봐야 합니다.
새로 추가된 컴포넌트들을 간단히 소개하면 다음과 같습니다.
● JCheckBoxMenuItem, JRadioButtonMenuItem : 체크박스 혹은 라디오 버튼 형식의 메뉴 항목 컴포넌트
<그림> JCheckBoxMenuItem과 JRadioButtonMenuItem
● JRadioButton : 라디오 버튼 컴포넌트. AWT 컴포넌트에서는 CheckboxGroup 클래스를 사용하여 Checkbox 컴포넌트로 라디오 버튼을 구현하지만 스윙 컴포넌트에서는 별도의 라디오 버튼 컴포넌트를 제공
<그림> JRadioButton
● JColorChooser : 원하는 색깔을 직접 보면서 선택할 수 있는 대화상자 컴포넌트
<그림> JColorChooser
● JComboBox : JComboBox 컴포넌트는 AWT 컴포넌트의 Choice에 해당하는 스윙 컴포넌트. 여러 개 중의 하나를 선택할 수도 있으며 현재 선택된 문자열을 편집할 수도 있습니다.
<그림> JComboBox
● JFileChooser : AWT 컴포넌트의 FileDialog에 해당하는 스윙 컴포넌트입니다. 파일을 선택할 때 사용하며 이미지 파일의 경우 미리 보기 기능을 지원합니다.
<그림> JFileChooser
● JInternalFrame : 컨테이너 내부에 프레임을 만들 수 있는 컴포넌트
<그림> JInternalFrame
● JProgressBar : 진행 상황을 그래픽으로 보여주는 컴포넌트
<그림> JProgressBar
● JSlider : 눈금이나 크기를 나타내는 컴포넌트.
<그림> JSlider
● JSplitPane : 윈도우 내부를 분할하여 사용할 수 있는 컴포넌트
<그림> JSplitPane 컴포넌트 사용 예제
● JTabbedPane : 여러 개의 윈도우 중 선택된 한 화면만 보여주는 윈도우로 AWT의 CardLayout을 사용하여 구현한 윈도우들과 유사한 기능을 가지고 있습니다.
<그림> JTabbedPane
● JTable : 표를 표현하는 컴포넌트
<그림> JTable
● JTextPane : 서식을 가진 텍스트와 이미지를 사용하여 표현할 수 있는 문서 창 컴포넌트. 서식 있는 문서, RTF 문서, HTML 문서 등을 표현할 수 있습니다.
<그림> JTextPane
● JPasswordField : 패스워드를 입력받는 텍스트필드 컴포넌트
<그림> JPasswordField
● JToolBar : 명령 아이콘 박스 컴포넌트로 메뉴 아래에 붙을 수도 있고 프레임으로부터 떼낼 수도 있습니다.
<그림> JToolBar
● JToolTip : 마우스 움직임에 따라 현재 마우스가 위치한 부분에 대한 풍선 도움말을 제공하는 컴포넌트
<그림> JToolTip
● JTree : 나무 모양의 계층 구조를 표현하는 컴포넌트
<그림> JTree
1.2 AWT 코드를 스윙 코드로 바꾸기
스윙 컴포넌트를 사용하는 방법은 기본적인 메쏘드들의 경우에는 AWT의 대응하는 컴포넌트들의 사용법과 거의 동일합니다. 그래서, AWT 컴포넌트들로 작성한 프로그램들을 스윙 컴포넌트를 사용하도록 바꾸는 데에는 몇 가지 점만 염두에 두면 큰 어려움은 없습니다.
첫째, java.awt, java.awt.event 패키지와 더불어 스윙 컴포넌트 클래스들이 포함된 패키지들 즉, javax.swing 패키지와 javax.swing.event 패키지를 임포트합니다.
둘째, 각 AWT 컴포넌트 클래스 이름들을 대응하는 스윙 컴포넌트 클래스 이름으로 변경합니다. 많은 경우에는 클래스 이름 앞에 'J'만 붙여주면 될 것입니다.
셋째, 메쏘드들 중 이름이나 사용법이 다른 경우에는 찾아서 변경해주어야 합니다.
간단한 예제를 바꾸어 보면서 스윙 컴포넌트를 사용할 때 AWT 컴포넌트와 다르게 처리해야 하는 부분들이 무엇인지 짚어봅시다.
먼저 간단한 AWT 예제입니다.
import java.awt.*; import java.awt.event.*; public class AWTComponent extends Frame implements ActionListener { // 버튼의 실행 이벤트 처리 // 프레임의 생성자 AWTComponent(String title) { super(title); // 메뉴바 생성 MenuBar menuBar=new MenuBar(); // 파일 메뉴를 생성하여 메뉴바에 추가 Menu fileMenu = new Menu("File"); menuBar.add(fileMenu); // 파일 메뉴의 각 메뉴 항목을 생성 fileMenu.add(new MenuItem("Open")); fileMenu.add(new MenuItem("Save")); fileMenu.addSeparator(); fileMenu.add(new MenuItem("Print")); fileMenu.addSeparator(); // 종료 메뉴 항목은 단축키를 만들고 실행 이벤트 처리 객체를 등록한다. MenuItem exitItem = new MenuItem("Exit", new MenuShortcut('X')); fileMenu.add(exitItem); exitItem.setActionCommand("Exit"); exitItem.addActionListener(this); // 메뉴바를 프레임에 추가한다. setMenuBar(menuBar); // 버튼과 텍스트영역을 각각 프레임에 추가한다. Button button=new Button("Exit"); add(button, "South"); button.addActionListener(this); TextArea text=new TextArea(); add(text, "Center"); text.setText("AWT 컴포넌트를 사용하였습니다."); } // main() 메쏘드 public static void main(String args[]) { AWTComponent frame=new AWTComponent("AWT UI Program"); frame.setSize(300, 300); frame.setVisible(true); } // 실행 이벤트가 발생하면 종료한다. public void actionPerformed(ActionEvent evt) { String cmd=evt.getActionCommand(); if (cmd.equals("Exit")) System.exit(0); } }<예제> AWTComponent 프로그램
이 프로그램을 위의 방법에 따라 스윙 컴포넌트 프로그램으로 변경하면 다음과 같이 바꿀 수 있습니다. 볼드체로 표시된 부분이 변경된 부분들입니다.
import java.awt.*; import java.awt.event.*; import javax.swing.*; public class SwingComponent extends JFrame implements ActionListener { // 생성자 SwingComponent(String title) { super(title); JMenuBar menuBar=new JMenuBar(); JMenu fileMenu = new JMenu("File"); menuBar.add(fileMenu); fileMenu.add(new JMenuItem("Open")); fileMenu.add(new JMenuItem("Save")); fileMenu.addSeparator(); fileMenu.add(new JMenuItem("Print")); fileMenu.addSeparator(); JMenuItem exitItem = new JMenuItem("Exit", 'X'); fileMenu.add(exitItem); exitItem.setActionCommand("Exit"); exitItem.addActionListener(this); setJMenuBar(menuBar); JButton button=new JButton("Exit"); getContentPane( ).add(button, "South"); button.addActionListener(this); JTextArea text=new JTextArea(); getContentPane( ).add(text, "Center"); text.setText("Swing 컴포넌트를 사용하였습니다."); } // main 메쏘드 public static void main(String args[]) { SwingComponent frame=new SwingComponent("Swing UI Program"); frame.setSize(300, 300); frame.setVisible(true); } public void actionPerformed(ActionEvent evt) { String cmd=evt.getActionCommand(); if (cmd.equals("Exit")) System.exit(0); } }<예제> 스윙 컴포넌트로 변경된 코드
getContentPane() AWT 컴포넌트를 사용하는 코드를 스윙 컴포넌트를 사용하도록 변경한 SwinComponent.java 코드를 보면 위에서 제시된 세 가지 규칙을 따르지 않은 것은 거의 없습니다. 하지만 조금 더 자세히 보면 중요한 다른 점이 있습니다. 바로 버튼과 텍스트영역을 프레임에 추가하는 코드에서 getContentPane()이라는 조금 생소한 메쏘드를 호출합니다.
getContentPane( ).add(button, "South");
AWT와 마찬가지로 스윙에도 컴포넌트들을 추가하여 배치할 수 있는 컨테이너 역할을 하는 컴포넌트들이 존재합니다. JFrame, JApplet, JDialog, JWindow가 바로 스윙 컴포넌트들의 컨테이너 컴포넌트들입니다. 이들 컨테이너 컴포넌트들은 실제로는 자식 윈도우 컴포넌트들을 배치하고 관리하는 일을 별도의 컨테이너 컴포넌트에게 위임하는 방식을 채택하고 있습니다. 그래서 윈도우 컴포넌트들을 이들 스윙 컨테이너 컴포넌트에 추가할 때에는 보이지 않는 이 별도의 컨테이너 컴포넌트에게 추가해야 합니다. 이 컨테이너 컴포넌트를 contentPane이라고 합니다. contentPane 객체에게 요청해야 것에는 컴포넌트 추가뿐만 아니라 컴포넌트들의 레이아웃 관리자 지정도 포함됩니다. 즉, 스윙 컨테이너 컴포넌트의 레이아웃 관리자를 FlowLayout으로 변경하고 싶다면 다음과 같은 방법으로 사용하면 됩니다. Container contentPane = getContentPane( ); contentPane.setLayout(new FlowLayout( )); contentPane.add(new JButton("버튼")); |
스윙 컴포넌트와 AWT 컴포넌트를 섞으면? 스윙 컴포넌트와 AWT 컴포넌트를 섞어서 사용하면 어떻게 될까요? 처음 스윙 컴포넌트를 사용한다면 익숙해져 있던 AWT 컴포넌트를 계속 사용하고 AWT 컴포넌트가 없는 부분만 스윙 컴포넌트를 사용하면 편리할 것 같습니다. 하지만, 이것은 곤란한 문제를 안고 있습니다. AWT 컴포넌트들은 불투명한 윈도우 시스템의 컴포넌트들을 빌어 사용하는 컴포넌트들이고 스윙 컴포넌트들은 자바로 직접 렌더링하고 기능을 처리하는 투명한 컴포넌트들입니다. 이들 AWT 컴포넌트와 스윙 컴포넌트를 함께 배치할 경우 컴포넌트를 추가한 순서에 관계없이 항상 AWT 컴포넌트들이 스윙 컴포넌트를 덮어써버리게 됩니다. 다음 그림은 간단한 예를 보여줍니다. 세 개의 버튼 중 두 개(라벨이 Light와 Light2)는 JButton이며 하나(라벨이 Heavy)는 Button입니다. 버튼을 추가한 순서는 Light, Heavy, Light2 순서이지만 실제로 디스플레이되기는 Heavy 레이블을 가진 AWT Button 컴포넌트가 두 스윙 버튼 컴포넌트들 위에 나타나게 됩니다. 섞어서 사용하지 않는 것이 최선이라는 것을 알 수 있습니다.
<그림> AWT 컴포넌트와 스윙 컴포넌트를 함께 사용한 예 |
1.3 스윙 컨테이너들
모든 스윙 컴포넌트가 경량 컴포넌트인 것은 아닙니다. 스윙 컴포넌트를 담는 다음 네 가지 스윙 컨테이너 클래스는 중량인 AWT 컴포넌트 위에 특별한 설계를 하여 스윙 컴포넌트들을 추가할 수 있도록 하였습니다.
중량 컴포넌트인 네 가지 스윙 컨테이너 컴포넌트는 다음과 같습니다.
● JFrame : Frame 클래스를 상속하여 스윙 컴포넌트를 추가할 수 있게 해줍니다.
● JDialog : Dialog 클래스를 상속하여 여러 가지 대화 상자를 스윙 컴포넌트들로 구현하게 해줍니다.
● JWindow : Window 클래스를 상속하여 스윙 컴포넌트를 추가할 수 있게 해줍니다.
● JApplet : Applet 클래스를 상속하여 애플릿에 스윙 컴포넌트를 추가할 수 있게 해줍니다. 스윙 컴포넌트를 사용하는 애플릿을 작성할 때에는 반드시 Applet 클래스가 아니라 JApplet 클래스를 상속하여 만들어야 함을 잊어서는 안됩니다.
이들 네 가지 스윙 컨테이너 클래스들은 모두 RootPaneContainer라는 공통의 인터페이스를 구현하여 스윙 컴포넌트를 추가하도록 설계되었습니다. 이 외에도 프레임 안에 내부 프레임 윈도우를 구현하게 해주는 스윙 컨테이너인 JInternalFrame 클래스도 동일한 인터페이스를 구현하여 스윙 컨테이너들은 통일된 방식으로 스윙 컴포넌트들을 추가, 관리합니다.
<그림> 스윙 컨테이너 클래스 계층 상속 구조
RootPaneContainer 인터페이스는 JRootPane 객체를 사용하여 스윙 컴포넌트들을 관리하게 하므로, JRootPane 객체를 살펴보면 스윙 컨테이너가 스윙 컴포넌트를 관리하는 기본적인 방법을 이해할 수 있습니다.
JRootPane은 스윙 컨테이너들의 모든 동작을 위임받은 스윙 컴포넌트이며 다음 네 가지 서로 다른 기능을 하는 컴포넌트들로 구성됩니다.
<그림> JRootPane을 구성하는 윈도우들의 윈도우 계층 구조
● 자식 컴포넌트 윈도우를 담는 부분 (contentPane) : 모든 자식 스윙 컴포넌트 윈도우들의 부모 윈도우가 됩니다. rootPane에 직접 자식 컴포넌트를 추가해서는 안되며 반드시 contentPane에 추가해야 합니다.
● 투명한 부분 (glassPane) : 이 부분은 전체 스윙 컨테이너의 모든 이벤트를 가로챌 수 있도록 특별히 설계된 투명한 컴포넌트입니다. 항상 존재하며 대부분의 경우 보이지 않습니다.
● 층을 구성하는 부분 (layeredPane) : JLayeredPane 클래스의 인스턴스 객체이며 여러 스윙 컨포넌트들이 겹쳐질 때 각 컴포넌트의 상하 위치를 결정합니다. 항상 존재하며 contentPane과 menuBar는 이 컴포넌트의 자식 윈도우로 구성됩니다.
● 메뉴바 (menuBar) : JMenuBar 클래스의 인스턴스 객체이며 항상 존재하지는 않습니다.
1.4 BoxLayout 사용하기
javax.swing 패키지에는 스윙 컴포넌트 외에도 반가운 클래스가 하나 들어 있습니다. 새로운 레이아웃 관리자인 BoxLayout 클래스입니다. 물론 스윙 컴포넌트들을 기존의 레이아웃 관리자를 사용하여 레이아웃하더라도 어려움이 없긴 하지만 BoxLayout 클래스는 좀더 편리하고 원하는 모양으로 컴포넌트를 레이아웃할 수 있게 해주는 재간둥이 클래스입니다.
BoxLayout 클래스는 가로 방향, 혹은 세로 방향으로 컴포넌트를 정렬시키는데, 예를 들어 패널에 두 개의 버튼을 차례로 가로 방향으로 추가시키는 경우엔 다음과 같이 BoxLayout을 사용할 수 있습니다.
JPanel panel = new JPanel(); // 첫 번째 인자에는 레이아웃할 대상 컴포넌트를, // 두 번째 인자에는 가로 방향 정렬의 경우에는 BoxLayout.X_AXIS, // 세로 방향 정렬의 경우에는 BoxLayout.Y_AXIS를 지정한다. panel.setLayout(new BoxLayout(panel, BoxLayout.X_AXIS)); panel.add(new JButton("버튼 1")); panel.add(new JButton("버튼 2"));
BoxLayout을 세로 방향으로도 정렬이 가능한 FlowLayout으로 생각하면 이해가 쉽습니다. 하지만 BoxLayout의 능력은 FlowLayout의 단순한 확장이 아니라 섬세한 제어까지 가능하게 해줍니다. BoxLayout의 레이아웃 기능을 돕기 위해 Box라는 별도의 클래스가 존재합니다.
Box 클래스는 몇 개의 static 메쏘드를 제공하여 컴포넌트 사이에 간격을 주거나 빈 부분을 채울 수 있는 보이지 않는 컴포넌트들을 만들어줍니다.
메쏘드 이름 |
역할 |
createGlue( ) |
'glue'는 아교를 뜻합니다. 이 아교는 각 컴포넌트를 적당한 크기로 배치하고 났을 때 남는 부분을 채우는 일을 합니다. 이 보이지 않는 아교 컴포넌트를 만드는 Box의 클래스의 static 메쏘드들은 다음 셋으로, Box.createHorizontalGlue() 메쏘드는 가로 방향, Box.createVerticalGlue() 메쏘드는 세로 방향, Box.createGlue() 메쏘드는 가로, 세로 방향의 아교 컴포넌트를 만듭니다. |
createHorizontalGlue( ) |
|
createVerticalGlue( ) |
|
createRigidArea( ) |
'rigid area'는 말 그대로 고정된 영역을 나타냅니다. 이 고정 영역은 지정한 크기만큼 자리를 차지하게 됩니다. |
createHorizontalStrut( ) |
'strut'은 버팀목이라는 뜻을 가지고 있습니다. 이 버팀목은 가로 혹은 세로 방향의 고정 간격을 지정합니다. 한쪽 방향만 있는 고정 영역과 비슷하지만, 다른 점은 RigidArea의 경우 가로, 세로 모두 고정된 크기를 지정하지만 Strut의 경우에는 가로, 혹은 세로 성분만 고정 크기를 지정하기 때문에 지정되지 않은 부분, 즉 HorizontalStrut의 경우 세로 방향은 마치 Glue처럼 채우는 역할을 수행한다는 것입니다. 지정되지 않은 한 방향의 길이가 제한이 없기 때문에 복잡한 레이아웃에 사용하기에는 예측이 어려워지는 문제가 있습니다. 이 경우에는 RigidArea를 한쪽 길이를 0으로 하여 사용하는 것이 보다 예측 가능하고 확실한 대안이 될 것입니다. |
createVerticalStrut( ) |
import java.awt.*; import java.awt.event.*; import javax.swing.*; class BoxExample extends JFrame { public BoxExample(String title) { super(title); Container contentPane = getContentPane(); // 두 개의 패널은 세로 방향으로 쌓는다. contentPane.setLayout(new BoxLayout(contentPane, BoxLayout.Y_AXIS)); JPanel panel1 = new JPanel(); // 가로 방향의 레이아웃 panel1.setLayout(new BoxLayout(panel1, BoxLayout.X_AXIS)); panel1.setBorder(BorderFactory.createTitledBorder("Panel 1")); panel1.add(Box.createHorizontalGlue()); panel1.add(new JButton("O K")); panel1.add(Box.createRigidArea(new Dimension(5, 0))); panel1.add(new JButton("Cancel")); JPanel panel2 = new JPanel(); panel2.setLayout(new BoxLayout(panel2, BoxLayout.X_AXIS)); panel2.setBorder(BorderFactory.createTitledBorder("Panel 2")); panel2.add(new JButton("O K")); panel2.add(Box.createVerticalStrut(100)); panel2.add(new JButton("Cancel")); contentPane.add(panel1); contentPane.add(panel2); validate(); setSize(300, 200); setVisible(true); } public static void main(String args[]) { new BoxExample("Box Layout Example Program"); } }<예제> BoxLayout을 사용한 레이아웃 예제 프로그램
<그림> BoxLayout을 사용한 레이아웃
룩앤필 스윙 컴포넌트들은 AWT 컴포넌트와 달리 순수하게 자바 코드로 만들어진 컴포넌트들이기 때문에 여러 가지 새로운 자바만의 기능을 설계 때부터 고려하여 추가할 수 있었습니다. 룩앤필(Look and Feel)은 스윙 컴포넌트들의 전체적인 외양과 느낌을 지정하는 클래스입니다. 룩앤필을 사용하면 동일한 스윙 컴포넌트들이 현재 지정된 룩앤필에 따라 전혀 다른 느낌을 주게 됩니다. 다음 그림은 룩앤필에 따라 서로 다른 모습으로 나타나는 동일한 스윙 컴포넌트들을 보여줍니다.
<그림> 자바 룩앤필
<그림> 윈도우 룩앤필
<그림> 모티프 룩앤필
별도로 룩앤필을 지정하지 않으면 자바 룩앤필이 기본으로 지정됩니다. 룩앤필을 지정할 때에는 다음과 같이 UIManager 클래스를 사용합니다. try { UIManager.setLookAndFeel( UIManager.getCrossPlatformLookAndFeelClassName()); } catch (Exception e) { System.err.println("Cannot set look and feel:"+e.getMessage()); } UIManager 클래스의 getCrossPlatformLookAndFeelClassName() 메쏘드는 자바 룩앤필 클래스 이름을 리턴합니다. getSystemLookAndFeelClassName() 메쏘드는 현재 자바 스윙 컴포넌트가 실행되고 있는 환경에 따라 다른 클래스 이름을 리턴합니다. 즉, 마이크로소프트 윈도우 시스템에서 실행되고 있다면 윈도우 룩앤필 클래스 이름을 리턴하고, 유닉스의 X 윈도우 시스템에서 실행되고 있다면 모티프 룩앤필 클래스 이름을 리턴할 것입니다. |
다중 쓰레드 환경의 스윙 컴포넌트 AWT 컴포넌트와는 달리 스윙 컴포넌트들을 다중 쓰레드 환경에서 사용할 때에는 주의해야 할 사항이 있습니다. AWT 컴포넌트에만 익숙해있던 독자라면 많이 놀랄 것입니다.
"일단 스윙 컴포넌트가 구현된 다음에는 스윙 컴포넌트의 상태를 변경한다거나 변경된 상태에 따라 어떤 일을 수행하고자 할 때에는 반드시 이벤트 처리 쓰레드에서 수행해야 합니다."
스윙 컴포넌트는 setVisible(true), show(), pack() 등의 메쏘드를 사용하면 구현됩니다. 이 때, 구현된다는 표현은 화면에 보여져서 paint() 메쏘드가 자동으로 호출되고 이벤트를 받을 준비가 된다는 뜻입니다. 그런 의미에서 단순히 컴포넌트 안에 포함된 컴포넌트들을 원하는 크기로 배치시키는 일만 하는 pack() 메쏘드가 호출될 때에도 스윙 컴포넌트들이 구현된다는 점은 꼭 기억해야 합니다. 이런 메쏘드들을 호출한 다음에는 해당하는 스윙 컴포넌트와 그 안에 포함된 스윙 컴포넌트들에 setText(), getText() 등과 같이 컴포넌트의 상태를 변경하는 메쏘드를 호출하면 이벤트 처리 쓰레드와 경쟁 조건에 빠질 위험이 있습니다. 이벤트 처리 쓰레드는 AWT 윈도우 시스템의 이벤트를 처리하는 쓰레드로 paint() 메쏘드를 비롯하여 actionPerformed() 등 모든 이벤트 처리 메쏘드를 호출하는 별도의 쓰레드입니다. 이 이벤트 처리 쓰레드에게 원하는 일을 수행하도록 요청하려면 javax.swing 패키지의 SwingUtilities 클래스가 제공하는 static 메쏘드인 invokeLater() 혹은 invokeAndWait() 메쏘드를 사용해야 합니다. public static void invokeLater(Runnable obj); public static void invokeAndWait(Runnable obj) throws InterruptedException, InvocationTargetException; 이 두 메쏘드는 invokeLater() 메쏘드가 실행을 요청한 다음 바로 다음으로 실행이 넘어가는 대신에 invokeAndWait() 메쏘드는 지정한 Runnable의 run() 메쏘드 실행이 종료할 때까지 기다린다는 점을 제외하면 동일한 역할을 합니다. 이 메쏘드들을 사용해봅시다. JFrame이 화면에 보여지고 난 다음에 프레임에 있는 JTextField의 값을 변경할 때에는 다음과 같이 할 수 있습니다. JFrame frame = new JFrame("test frame"); ... frame.setVisible(true); // 여기부터는 GUI 상태를 변경시키는 메쏘드를 사용해서는 안된다. Runnable doSetText = new Runnable() { public void run() { text.setText("Setting text after setVisible"); } }; // 나중에 이벤트 쓰레드에서 해당 작업을 수행하도록 요청한다. SwingUtilities.invokeLater(doSetText); |
1.5 스윙 다중 문서 인터페이스
다중 문서 인터페이스(MDI, Multiple Document Interface)는 하나의 주 프레임 윈도우 안에 여러 개의 문서 윈도우를 관리할 수 있는 인터페이스로 대부분의 윈도우용 워드프로세서나 스프레드시트 프로그램들이 제공하는 인터페이스입니다.
자바 스윙 컴포넌트는 JInternalFrame 컴포넌트 클래스를 제공하여 프레임 안에 내부 프레임 윈도우 컴포넌트를 구현할 수 있도록 지원합니다. AWT 윈도우 시스템에서는 가능하지 않던 다중 문서 인터페이스가 JInternalFrame 컴포넌트를 사용하여 손쉽게 구현할 수 있게 된 것입니다.
1.5.1 JLayeredPane의 층들
JRootPane 컴포넌트는 내부에 추가할 스윙 컴포넌트의 상하 포개짐을 처리할 수 있는 JLayeredPane 컴포넌트를 가집니다. 각 스윙 컴포넌트는 자신이 속한 층에 따라 상하 위치가 정해지는데 이 층의 값은 정수로 지정할 수 있습니다. 높은 정수값을 가진 층에 속한 컴포넌트가 낮은 정수값을 가진 층에 속한 컴포넌트보다 윗쪽에 위치합니다. 다음 예제에서 버튼은 레이블보다 위에 위치할 것입니다.
JFrame frame = new JFrame(); JLayeredPane layeredPane = frame.getLayeredPane(); JButton button = new JButton("버튼"); JLabel label = new JLabel("레이블"); // 버튼은 20의 값을 가진 층에 속한다. layeredPane.add(button, new Integer(20)); // 레이블은 10의 값을 가진 층에 속한다. layeredPane.add(label, new Integer(10));
이와 같이 직접 정수값을 지정하여 각 컴포넌트들이 속할 층을 지정할 수도 있겠지만 JLayeredPane에서는 자주 사용되는 층을 미리 다섯 가지로 분류하여 상수로 제공합니다. 대부분의 경우에는 이 상수값들을 사용하게 될 것입니다.
● JLayeredPane.DEFAULT_LAYER : 기본 층. 특별히 층을 지정하지 않으면 모두 기본값으로 이 층에 속하게 됩니다. 일반적인 경우에는 이 층을 사용합니다.
● JLayeredPane.PALETTE_LAYER : 기본 층 위에 위치하는 층입니다. 다른 컴포넌트들보다 윗쪽에 위치하게 해주므로 메뉴에서 분리된 툴바 등에 사용합니다.
● JLayeredPane.MODAL_LAYER : 모덜 대화상자에 주로 사용하는 층입니다.
● JLayeredPane.POPUP_LAYER : 툴팁이나 콤보상자 등의 팝업 윈도우를 나타내는 데 주로 사용하는 층입니다.
● JLayeredPane.DRAG_LAYER : 어떤 컴포넌트를 마우스로 드래깅할 때에는 드래깅되는 컴포넌트가 다른 모든 컴포넌트들보다 앞으로 나와보여야 합니다. 컴포넌트를 드래깅하는 도중에 사용되는 층입니다.
대부분의 컴포넌트들은 층을 별도로 지정하지 않을 때 사용되는 기본 층(DEFAULT_LAYER)에 속하게 됩니다. 같은 층에 속한 컴포넌트들 간에도 서로의 위치 관계를 지정할 수 있는데 JLayeredPane 클래스의 moveToFront(), moveToBack(), setPosition() 메쏘드는 각각 같은 층 안에서 컴포넌트들 간의 상대 위치를 변경시킵니다.
7.2 JInternalFrame 사용 방법
이제 JInternalFrame 컴포넌트 사용법을 알아봅시다. 여러 개의 내부 프레임 컴포넌트는 어떤 층에 속하여 그 층에 따라 포개짐이 결정됩니다. 내부 프레임 컴포넌트를 추가할 컴포넌트는 JLayeredPane을 상속하는 JDesktopPane 컴포넌트입니다. JDesktopPane 클래스는 JLayeredPane을 상속하면서 JInternalFrame 컴포넌트만을 전담해서 관리하는 특수한 컴포넌트 클래스입니다.
다음 예제 코드는 내부 프레임을 생성해서 프레임에 추가하는 아주 간단한 프로그램입니다.
import java.awt.*; import javax.swing.*; class IFrame { public static void main(String args[]) { // 프레임 생성 JFrame frame = new JFrame("내부 프레임 테스트 프로그램"); // JDesktopPane 생성 JDesktopPane desktop = new JDesktopPane(); // 내부 프레임 생성 JInternalFrame iframe = new JInternalFrame("첫 번째 내부 프레임", true /* 크기변경 가능 */, true /* 종료 가능 */, true /* 최대 크기로 가능 */, true /* 아이콘화 가능 */ ); desktop.add(iframe); // JDesktopPane 컴포넌트에 내부 프레임 추가 // 프레임 윈도우의 contentPane을 JDesktopPane 컴포넌트로 지정 frame.setContentPane(desktop); // 내부 프레임의 위치와 크기 지정 iframe.setBounds(50, 50, 200, 100); iframe.setVisible(true); // JDK 1.3.x부터는 기본값이 invisible. JDK 1.2.x버전까지는 기본값이 visible. frame.setSize(300,300); // 프레임의 크기 지정 frame.setVisible(true); } }<예제> 내부 프레임을 만드는 아주 간단한 프로그램
<그림> 첫 번째 내부 프레임 윈도우 프로그램
7.3 내부 프레임의 윈도우 이벤트 처리
내부 프레임을 아이콘화하거나 종료하는 등 윈도우 상태를 변화시키면 일반 윈도우의 WindowEvent 이벤트에 해당하는 InternalFrameEvent 이벤트가 발생합니다. 이 InternalFrameEvent를 처리하려면 InternalFrameListener 인터페이스를 구현하거나 InternalFrameAdapter 클래스를 상속하면 됩니다. WindowEvent 이벤트를 처리하는 방법과 크게 다르지 않습니다.
InternalFrameListener 인터페이스에 정의된 내부 프레임 이벤트 처리 메쏘드들은 다음과 같습니다.
① public void internalFrameActivated(InternalFrameEvent e) : 내부 프레임이 활성화될 때
② public void internalFrameClosed(InternalFrameEvent e) : 내부 프레임이 닫겼을 때
③ public void internalFrameClosing(InternalFrameEvent e) : 내부 프레임이 닫길 때
④ public void internalFrameDeactivated(InternalFrameEvent e) : 내부 프레임이 비활성화될 때
⑤ public void internalFrameDeiconified(InternalFrameEvent e) : 내부 프레임이 아이콘화되었다가 복구될 때
⑥ public void internalFrameIconified(InternalFrameEvent e) : 내부 프레임이 아이콘화될 때
⑦ public void internalFrameOpened(InternalFrameEvent e) : 내부 프레임이 열릴 때
2. 모나리자 그리기, 자바 2D
JDK 1.2 버전이 나오기 전에도 자바는 그래픽 기능을 제공했지만, 지나치게 기본적인 기능에 한정되어 자바를 사용한 고급 그래픽 프로그램은 거의 불가능했습니다. 하지만 자바 2 즉, JDK 1.2의 등장과 함께 이러한 불만을 거의 해소할 수 있게 되었습니다. 자바 2에 새로 포함된 2D 기술은 기존 그래픽 기능을 크게 확장, 개선하여 드로잉, 이미지 처리, 텍스트 렌더링, 색깔 정의에 이르기까지 광범위한 기능을 제공합니다.
<그림> 2D를 사용한 그래픽 프로그램 예제
2.1 그래픽 컨텍스트 - Graphics와 Graphics2D
그림을 그리기 위해서는 그래픽 장치 컨텍스트라는 것이 필요합니다. 자바에서는 java.awt 패키지의 Graphics 클래스에서 그래픽 장치 컨텍스트와 여러 가지 렌더링에 필요한 상태 정보를 저장하는 기능을 제공하고 있습니다. 따라서, 자바에서 그림을 그릴 때에는 반드시 Graphics 객체에 대한 참조를 구해야 합니다.
Graphics 객체에 대한 참조는 각 컴포넌트에서 getGraphics() 메쏘드를 호출하여 얻을 수 있으며 자바의 AWT 시스템이 인자로 Graphics 객체에 대한 참조를 인자로 넘겨주는 paint() 등의 메쏘드 안에서 바로 사용하면 됩니다.
즉, JPanel 컴포넌트 안에 선을 그으려면 다음과 같이 사용할 수 있습니다.
JPanel panel = new JPanel(); Graphics g = panel.getGraphics(); if (g==null) // 그래픽 컨텍스트를 얻는 데 실패한 경우를 처리 return; try { g.drawLine(10, 10, 10, 50); // 패널 위에 선을 그린다. } finally { // getGraphics()로 얻은 그래픽 컨텍스트는 반드시 리소스 해지 g.dispose(); }
그래픽 컨텍스트는 시스템이 제공하는 한정된 리소스이므로 사용 후에는 dispose()를 명시적으로 호출해줘야 합니다. 다만 paint()나 update() 메쏘드에서처럼 AWT 시스템이 인자로 넘겨주는 그래픽 컨텍스트는 dispose()를 호출할 필요가 없습니다.
Graphics 클래스는 기본적인 드로우 기능을 제공하는 그래픽 컨텍스트 클래스입니다. 2D 그래픽의 확장된 기능을 사용하기 위해서는 Graphics2D 클래스 객체를 구해야 하는데, Graphics2D 객체를 구하는 방법은 아주 간단합니다.
먼저 Graphics 객체를 구한 다음 명시적으로 형변환을 해주면 됩니다. 즉, 다음과 같이 사용합니다.
JPanel panel = new JPanel(); // 명시적으로 형 변환 Graphics2D g2 = (Graphics2D) panel.getGraphics(); if (g2==null) // 그래픽 컨텍스트를 얻는 데 실패한 경우를 처리 return; try { BasicStroke stroke = new BasicStroke(10.0f); g2.setStroke(stroke); g2.drawLine(10, 10, 10, 50); // 패널 위에 굵기가 10인 선을 그린다. } finally { // getGraphics()로 얻은 그래픽 컨텍스트는 반드시 리소스 해지 g2.dispose(); }
팁 : 자바 2D 프로그램을 사용하면 윈도우 95가 죽어요 윈도우 95/98 운영 체제 위에서 자바 2D 예제 프로그램을 실행시키다 보면 이유없이 시스템이 먹통이 되는 경우가 간혹 있습니다. 이것은 JDK 1.2의 버그 때문이므로 JDK 1.2.1 버전으로 업그레이드하면 조금 더 안정됩니다. 하지만, 그래도 종종 시스템이 다운되는 것을 만나게 됩니다. 이때에는 사용하는 비디오 카드 제조회사의 웹페이지를 찾아가 최신 버전의 비디오 카드 드라이버를 다운로드하여 설치하면 조금 개선됩니다. 2D 그래픽 기능이 마이크로소프트 윈도우 운영 체제에서는 마이크로소프트 사의 DirectDraw 기술을 사용하여 구현되어 있기 때문에 마이크로소프트 사로부터 한글 DirectX 최신 버전을 다운로드하여 설치하면 조금 더 안정될 수 있습니다. 글을 쓸 당시(1999년 4월) 필자의 컴퓨터 환경은 펜티엄 75MHz, 주 메모리 32MB인데 메모리 부족과 비디오 카드의 문제 등이 복합적으로 얽혀 아직도 JDK에 포함되어 있는 자바 2D 예제 프로그램과 스윙 예제 프로그램을 제대로 실행시키지 못했습니다. 지금은 업그레이드했지만, 소프트웨어의 발전이 요구하는 하드웨어의 비용을 쫓아가는 지불하는 것이 기분 좋은 일만은 아니더군요. |
2.2 도형 그리기
2D 그래픽 기능을 사용하여 그림을 그려봅시다. 그림을 그릴 때에는 도형의 모양과 스트로크, 채움 등의 요소가 필요합니다.
2.2.1 도형의 모양
먼저 2D 그래픽에서는 그려질 도형의 일반적인 모양을 정의하는 Shape 인터페이스를 제공합니다. 이 Shape 인터페이스를 구현하는 여러 가지 도형 클래스들이 java.awt 패키지와 java.awt.geom 패키지에 정의가 되어 있습니다.
⼗ GeneralPath - 일반적인 기하학적 도형
⼗ Line2D - 2D API를 사용해서 그릴 수 있는 선. Line2D.Float 및 Line2D.Double에 대한 부모 클래스
⼗ Rectangle - 직사각형을 정의하는 java.awt 클래스
⼗ RectangularShape - 직사각형으로 둘러싸여질 수 있는 도형. Arc2D, Ellipse2D, Rectangle2D, RoundRectangle2D의 부모 클래스
⼗ Polygon - 다각형을 정의하는 java.awt 클래스
⼗ CubicCurve2D - 3차 파라미터 커브의 일부분. CubicCurve2D.Float 및 CubicCurve2D.Double의 부모 클래스
⼗ QuadCurve2D - 2차 파라미터 커브의 일부분. QuadCurve2D.Float 및 QuardCurve2D.Double에 대한 부모 클래스
⼗ Area - 다른 도형 객체들로 구성된 임의의 영역
이름이 2D로 끝나는 도형 클래스들은 내부 클래스로 각각 Float와 Double을 가지고 있으며 이들 중 하나를 통해 객체를 생성할 수 있습니다. Double 클래스를 사용하면 Float보다 좀더 섬세하게 도형을 정의할 수 있지만 속도가 느려지는 것을 감수해야 합니다. 가로, 세로 10의 크기를 가지고 좌표가 (0, 0)인 2차원 사각형 객체는 다음과 같이 생성할 수 있습니다.
Rectangle2D rect = new Rectangle2D.Float(0.0f, 0.0f, 10.0f, 10.0f);
2.2.2 스트로크와 채움 속성
스트로크(Stroke)는 도형의 외곽선을 그리는 데 사용되는 여러 가지 속성들을 나타내는 표현입니다. 주로 두께나 점선 패턴, 선들이 만날 때의 처리, 선의 끝 부분 처리 등을 지정할 수 있습니다. 그래픽 컨텍스트에 스트로크를 지정할 때에는 setStroke() 메쏘드를 사용합니다. 다음은 선 두께를 10으로 지정합니다.
BasicStroke stroke = new BasicStroke(10.0f); ((Graphics2D) g).setStroke(stroke);
채움 속성(Paint)은 도형의 내부를 채우는 방법을 말합니다. 동일한 색으로 채우는 방법과, 그라디언트를 주는 방법, 지정된 패턴을 사용하여 채우는 방법 등이 지원됩니다. 동일한 색으로 채울 때에는 setColor() 메쏘드를 사용하며 다른 경우에는 setPaint() 메쏘드를 사용합니다. 다음은 그라디언트 방식으로 채움 속성을 지정하여 사각형을 그리는 예입니다.
GradientPaint gp = new GradientPaint(0.0f, 0.0f, Color.blue, 50.0f, 100.0f, Color.yellow); ((Graphics2D) g).setPaint(gp); g.fillRect(0, 0, 50, 100);
2.2.3 도형 변환
2D 그래픽부터는 도형의 변환을 지원합니다. 대칭 변환, 회전 변환, 경사 주기 등의 변환을 할 수 있습니다. 2D 그래픽은 도형의 변환을 그려질 도형의 속성처럼 사용하도록 설계되어 있으므로 도형을 그리기 전에 setTransform() 메쏘드를 사용하여 변환 방식을 지정해주면 됩니다. 다음은 사각형을 45도 회전시키는 변환입니다.
Rectangle2D rect = new Rectangle2D.Float(10.0, 10.0, 20.0, 30.0); AffineTransform rotate = AffineTransform.getRotateInstance(Math.PI/4.0, 0.0, 0.0); ((Graphics2D) g).setTransform(rotate); ((Graphics2D) g).fill();
2.2.4 도형 그리기
도형을 실제 그리려면 먼저 도형의 모양과 각 속성을 지정한 다음, 도형 내부를 채울 것인지 여부에 따라 외곽선만 그리는 draw() 메쏘드와 내부를 채우는 fill() 메쏘드를 호출하면 됩니다.
java.awt.Graphics 클래스는 2D 기술이 소개되기 전에 나온 drawRect(), fillRect()와 같은 여러 가지 메쏘드들을 제공합니다. 이들은 모두 draw()나 fill() 메쏘드를 사용해서도 구현할 수 있습니다.
즉,
fillRect(10, 10, 20, 30);
과
Rectangle2D rect = new Rectangle2D.Float(10.0, 10.0, 20.0, 30.0); ((Graphics2D) g).fill();은 거의 같은 일을 수행하게 됩니다.
2.3 2D 드로잉 프로그램 만들기
이제 드로잉 기능을 적용하여 프로그램을 만들어볼 시간입니다.
모양, 스트로크, 채움 방식, 그리고 색깔을 지정하여 마우스로 그림을 그리도록 하는 간단한 2D 드로잉 프로그램을 만들어봅시다.
이 프로그램은 크게 네 개의 클래스로 나눠볼 수 있습니다.
∙ Draw2D : JFrame 클래스를 상속한 주 클래스. 드로잉 속성을 지정할 메뉴를 제공.
∙ DrawCanvas : JPanel 클래스를 상속하며 실제 그림이 그려지는 캔버스 역할을 합니다.
∙ DrawCanvas.UIController : Draw2D에서 메뉴 선택 이벤트가 발생하면 처리하여 캔버스에 그려질 드로잉 속성을 변경합니다.
∙ DrawCanvas.MouseController : 캔버스 위의 마우스 움직임을 처리하는 클래스.
먼저 주 클래스인 Draw2D 클래스는 다음과 같이 만들 수 있습니다.
class Draw2D extends JFrame { ExitController exiter = new ExitController(); // 윈도우 종료 메시지 처리 객체 DrawCanvas canvas = new DrawCanvas(); // 캔버스 컴포넌트 // 생성자 Draw2D(String title) { super(title); // 메뉴바 JMenuBar menuBar = new JMenuBar(); setJMenuBar(menuBar); JMenu shapeMenu = menuBar.add(new JMenu("Shape")); shapeMenu.setMnemonic('S'); ButtonGroup shapeBG = new ButtonGroup(); ... // 여기에 메뉴를 만드는 코드들이 생략되어 있습니다. // 프레임의 기본 레이아웃 관리자는 BorderLayout canvas.setBackground(Color.white); getContentPane().add(canvas, BorderLayout.CENTER); // 윈도우 종료 이벤트 처리 addWindowListener(exiter); setSize(600, 500); pack(); setVisible(true); } // pack이 호출될 때 사용되는 크기 public Dimension getPreferredSize() { return new Dimension(500, 400); } // main 메쏘드 public static void main(String args[]) { new Draw2D("Draw2D Program"); } // 윈도우 종료 메시지를 처리하는 내부 클래스 class ExitController extends WindowAdapter implements ActionListener { public void windowClosing(WindowEvent we) { Draw2D.this.dispose(); System.exit(0); } public void actionPerformed(ActionEvent ae) { Draw2D.this.dispose(); System.exit(0); } } // Draw2D.ExitController 클래스 끝 }<예제> Draw2D 클래스
실제 그림을 그리는 캔버스 클래스는 도형에 관련된 현재 정보들을 저장합니다.
class DrawCanvas extends JPanel { // 도형의 모양에 관련된 상수 final static int SHAPE_LINE = 0; final static int SHAPE_RECT = 1; final static int SHAPE_ROUND_RECT = 2; final static int SHAPE_ELLIPSE = 3; final static int SHAPE_ARC = 4; int shape=SHAPE_LINE; // 기본 도형 모양 // 스트로크 final static int STROKE_NORMAL = 0; // 선 두께 5 final static int STROKE_THIN = 1; // 선 두께 1 final static int STROKE_THICK = 2; // 선 두께 10 int stroke = STROKE_NORMAL; // 기본 스트로크 boolean dashed = false; // 기본 값은 실선 스트로크 // 채움 방식 final static int FILL_SOLID = 0; final static int FILL_GRADIENT = 1; final static int FILL_PATTERN = 2; int fill = FILL_SOLID; // 기본 채움 방식 boolean filled = true; // 기본 값으로 채우기 모드 // 마우스 위치 이동에 따라 도형의 시작점과 끝점을 저장 Point startPoint=new Point(), savePoint=new Point(), endPoint=new Point(); Color fgColor=Color.black; // 전경색 Color bgColor=Color.white; // 배경색 UIController controller = new UIController(); // 버퍼링을 위해 사용하는 오프스크린 이미지 BufferedImage offImage = null; Graphics2D offG2 = null; // 오프스크린 이미지의 그래픽 컨텍스트 // 생성자 DrawCanvas() { // 마우스 이벤트 처리기 등록 MouseController mouseController = new MouseController(); addMouseMotionListener(mouseController); addMouseListener(mouseController); } public void paint(Graphics g) { int width = getSize().width; int height = getSize().height; if (width <=0 || height <= 0) return; // 오프스크린 이미지가 없으면 생성 if (offImage == null || offImage.getWidth() != width || offImage.getHeight() != height) createBufferImage(width, height); // 오프스크린 이미지가 존재하면 오프스크린 이미지를 캔버스에 디스플레이 if (offImage != null) ((Graphics2D) g).drawImage(offImage, 0, 0, this); } // 오프스크린 이미지와 그래픽 컨텍스트 생성 void createBufferImage(int width, int height) { offImage = (BufferedImage) createImage(width, height); if (offImage != null) { offG2 = offImage.createGraphics(); offG2.setBackground(getBackground()); offG2.clearRect(0, 0, width, height); } } // 실제 2D를 사용하여 그림을 그리는 루틴 void draw(Graphics2D g, Point startPoint, Point endPoint) { // 스트로크 지정 BasicStroke newStroke = null; float dash[]={10.0f}; switch (stroke) { case STROKE_THIN : if (dashed) newStroke = new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, dash, 0.0f); else newStroke = new BasicStroke(1.0f); break; case STROKE_THICK : if (dashed) newStroke = new BasicStroke(5.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, dash, 0.0f); else newStroke = new BasicStroke(5.0f); break; case STROKE_NORMAL : default : if (dashed) newStroke = new BasicStroke(3.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, dash, 0.0f); else newStroke = new BasicStroke(3.0f); break; } g.setStroke(newStroke); // 전경색 지정 g.setColor(fgColor); // 채우기를 사용할 경우 채움 방식 지정 if (filled) { switch (fill) { case FILL_GRADIENT : // 그라디언트 채우기 GradientPaint gp = new GradientPaint(startPoint, fgColor, endPoint, bgColor); g.setPaint(gp); break; case FILL_PATTERN : // 패턴 채우기 // 작은 오프스크린 이미지에 패턴으로 사용할 이미지를 그립니다. BufferedImage bi = new BufferedImage(5, 5, BufferedImage.TYPE_INT_RGB); Graphics2D big = bi.createGraphics(); try { big.setColor(fgColor); big.fillRect(0, 0, 5, 5); big.setColor(bgColor); big.fillOval(0, 0, 5, 5); } finally { big.dispose(); } Rectangle r = new Rectangle(0,0,5,5); TexturePaint texture = new TexturePaint(bi, r); g.setPaint(texture); break; case FILL_SOLID : // 앞에서 setColor() 메소드를 호출했으므로 처리할 필요가 없습니다. default : break; } } Shape newShape = null; // 그릴 도형의 모양 지정 switch (shape) { case SHAPE_RECT : newShape = new Rectangle2D.Float( (float) startPoint.x, (float) startPoint.y, (float) endPoint.x-(float) startPoint.x, (float) endPoint.y-(float) startPoint.y); break; case SHAPE_ROUND_RECT : newShape = new RoundRectangle2D.Float( (float) startPoint.x, (float) startPoint.y, (float) endPoint.x-(float) startPoint.x, (float) endPoint.y-(float) startPoint.y, (endPoint.x-startPoint.x)/10.0f, (endPoint.y-startPoint.y)/10.0f ); break; case SHAPE_ELLIPSE : newShape = new Ellipse2D.Float( (float) startPoint.x, (float) startPoint.y, (float) endPoint.x-(float) startPoint.x, (float) endPoint.y-(float) startPoint.y); break; case SHAPE_ARC : newShape = new Arc2D.Float( (float) startPoint.x, (float) startPoint.y, (float) endPoint.x-(float) startPoint.x, (float) endPoint.y-(float) startPoint.y, 0.0f, 90.0f, Arc2D.PIE); break; case SHAPE_LINE : default : newShape = new Line2D.Float( (float) startPoint.x, (float) startPoint.y, (float) endPoint.x, (float) endPoint.y); break; } // 채우기 모드이면 fill() 메쏘드 호출 if (filled) g.fill(newShape); // 외곽선은 항상 그립니다. g.draw(newShape); } ... // 다른 내부 클래스 선언이 여기에 들어갑니다. }<예제> DrawCanvas 클래스
다음은 마우스 이벤트를 처리하여 그림을 그리는 DrawCanvas.MouseController 클래스입니다.
class MouseController extends MouseInputAdapter { // 드래깅할 때 움직이는 잔상을 보여줍니다. public void mouseDragged(MouseEvent me) { Graphics2D g=(Graphics2D) getGraphics(); try { g.setXORMode( new Color(255-fgColor.getRed(), 255-fgColor.getGreen(), 255-fgColor.getBlue())); draw(g, startPoint, savePoint); endPoint = me.getPoint(); draw(g, startPoint, endPoint); savePoint=endPoint; } finally { g.dispose(); } } // 마우스가 눌려지면 시작점을 저장합니다. public void mousePressed(MouseEvent me) { startPoint = me.getPoint(); savePoint = startPoint; } // 마우스가 놓여지면 끝점을 저장하고 // 현재 캔버스와 오프스크린 모두에 도형을 그립니다. public void mouseReleased(MouseEvent me) { endPoint = me.getPoint(); Graphics2D g=(Graphics2D) getGraphics(); try { draw(g, startPoint, endPoint); if (offG2 != null) draw(offG2, startPoint, endPoint); } finally { g.dispose(); } } }<예제> DrawCanvas.MouseController 클래스
메뉴 이벤트를 처리하는 DrawCanvas.UIController 클래스는 생략합니다. Draw2D.java의 소스 코드를 참조하기 바랍니다.
<그림> Draw2D 프로그램 실행 모습
자바 2D는 단순한 드로잉 기능 뿐만 아니라 글꼴 처리, 이미지 처리 기능, 인쇄 처리 기능 등 다양한 부분에 걸쳐 있지만 여기에서는 드로잉 기능에만 국한하였습니다. 관심있는 독자들은 JFC 홈페이지(http://java.sun.com/products/jfc)에서 많은 정보를 만날 수 있습니다.
핵심 체크 ◈ 스윙 컴포넌트는 순수한 자바 코드로 작성된 경량 컴포넌트들입니다. ◈ 스윙 컴포넌트는 AWT 컴포넌트에서 찾아볼 수 없는 다양하고 편리한 인터페이스를 제공합니다. ◈ 이벤트 처리 쓰레드가 아닌 쓰레드에서 컴포넌트의 상태를 변화시키고자 할 때에는 SwingUtilities.invokeLater() 등의 메쏘드를 사용해야 합니다. ◈ 2D 그래픽은 자바 그래픽의 기능을 크게 확장시켜 보다 자유롭고 섬세한 그래픽 표현을 가능하게 해줍니다. |
필자 연락처 : yoonforh@yahoo.com