JavaFX는 다양한 애니메이션 기능이 포함되어 있다. Transition와 Timeline 같은 클래스를 사용하여 그래픽을 움직여 보자.


TranslateTransition 사용

JavaFX가 Swing 등에 비해 뛰어난 점 중 하나는 "애니메이션"이다. Swing에서 그래픽을 애니메이션하려고 하면 스레드와 타이머를 만들고 위치를 자주 이동하고 다시 그리고 등을 반복해야만 한다. 하지만 JavaFX라면 쉽게 애니메이션을 만들 수 있다.

애니메이션 관계의 기능은 javafx.animation 패키지에 여러가지 준비되어 있다. 우선 가장 많이 사용되는 "표시 위치를 이동"하는 것부터 사용해 보자.

이것은 "TranslateTransition"라는 클래스로 제공되고 있다. 이것은 애니메이션 관련 클래스의 가장 기본이다. 이 클래스의 사용법을 알면 다른 것도 대체로 동일하게 다를 수 있게 될 것이다.

이 TranslateTransition은 다음과 같은 형태로 인스턴스를 만듭니다.

new TranslateTransition();
new TranslateTransition("Duration");
new TranslateTransition("Duration", "Node");

인수는 아무것도 없는 것부터, Duration와 Node를 가진 것까지 여러가지를 갖추고 있다. 인수에 있는 "Node"는 움직이는 부품의 인스턴스이다. 그래픽라면 셰이프와 관련된 인스턴스를 여기에 지정한다.

또 다른 한개의 "Duration"은 javafx.util 패키지에 포함되어 있는 클래스로써, 애니메이션의 경과 시간을 처리하기위한 것이다. 이것은 다음과 같이 인스턴스를 만든다.

new Duration(밀리 초);

이것으로 인수에 지정된 밀리 초 단위의 경과 시간을 나타내는 Duration 인스턴스를 사용할 수 있다. 이제 "어떤 모양을 일정 시간에 애니메이션으로 움직일까"라는 애니메이션의 기본 설정이 있다.

또한 인수로 Node와 Duration을 설정하지 않은 경우 나중에 메소드를 사용하여 이를 지정해야 한다.

"TranslateTransition".setNode ( "Node");
"TranslateTransition".setDuration ( "Duration");

이것들이 없으면 애니메이션 자체가 되지 않기 때문에, new 할 때 처음부터 인수로 지정해 두는 것이 좋다.




애니메이션 설정 메소드

TranslateTransition 인스턴스를 만들고 나서, 이어서 메소드를 호출하여 애니메이션에 대한 섬세한 설정을 실시하고 있다. 여기에는 다양한 방법이 준비되어 있지만, 우선 필요한 것은 다음과 같다.

이동하기 전의 위치를 설정하는 메소드

"TranslateTransition".setFromX("double");
"TranslateTransition".setFromY("double");
"TranslateTransition".setFromZ("double");

이동하는 위치를 설정하는 메소드

"TranslateTransition".setToX("double");
"TranslateTransition".setToY("double");
"TranslateTransition".setToZ("double");

오토 리버스 설정

"TranslateTransition".setAutoReverse("boolean");

반복 횟수 설정

"TranslateTransition".setCycleCount ( "int");

대충 설정이 되면 마지막으로 TranslateTransition의 "play" 메소드를 호출하여 애니메이션을 시작한다. 또한 도중에 중지하는 "stop" 이나 "pause"라는 메소드도 준비되어 있다. 모두 인수없이 단지 호출만 할 뿐이다.

간단한 사용 예는 다음과 같다.

// import javafx.animation.*;
// import javafx.util.Duration;
 
public void createShape(Pane root){
    Rectangle r = new Rectangle(20, 20, 50, 50);
    r.setFill(Color.CYAN);
    root.getChildren().add(r);
    new TranslateTransition();
     
    TranslateTransition tt = new TranslateTransition(new Duration(1000), r);
    tt.setFromX(20);
    tt.setToX(100);
    tt.setAutoReverse(true);
    tt.setCycleCount(10);
    tt.play();
}

이전까지 동일한 클래스를 이용하여 도형 만들기 위한 메소드 createShape를 다시 바꾸는 형태로 정리하였다. 실행하면 눈에 보이는 사각형이 좌우로 왕복 운동을 한다.



애니메이션을 위한 주요 클래스

이 TranslateTransition와 같은 애니메이션을 위한 클래스는 그 밖에도 다양하게 준비되어 있다. 여기에서 중요한 것은 정리해 둔다.

리사이즈 애니메이션

new ScaleTransition("Duration", "Node");

설정 메소드

"ScaleTransition".setFromX("double");
"ScaleTransition".setFromY("double");
"ScaleTransition".setFromZ("double");

"ScaleTransition".setToX("double");
"ScaleTransition".setToY("double");
"ScaleTransition".setToZ("double");

회전 애니메이션

new RotateTransition ( "Duration", "Node");

설정 메소드

"RotateTransition".setFromAngle ( "double");
"RotateTransition".setToAngle ( "double");

색상의 애니메이션

new FillTansition("Duration", "Node");
new FillTansition("Duration", "Node", "Color1", "Color2");

new StrokeTransition("Duration", "Node");
new StrokeTransition("Duration", "Node", "Color1", "Color2");

설정 메소드

"StrokeTransition".setFromValue("Color");
"StrokeTransition".setToValue("Color");

페이드 애니메이션

new FadeTransition("Duration", "Node");

설정 용 메소드

"FadeTransition".setFromValue("double");
"FadeTransition".setToValue("double");

기본적인 사용법은 모두 TranslateTransition과 같다. Duration와 Node를 인수로 지정하여 인스턴스를 만들고, 거기에 있는 메소드를 호출하여 애니메이션을 수행 전후의 값을 각각 설정하고, 그것부터 setAutoReverse과 setCount 등의 조정을 한 후 "play"로 시작 실행한다는 흐름이다.

TranslateTransition를 사용할 수 있게 되면, 이러한 클래스도 거의 같은 감각으로 사용할 수 있게 될 알 수 있다.



Timeline 사용


Timeline으로 애니메이션 표시

만든 Timeline에는 애니메이션에 관한 다양한 설정을 위한 메소드가 제공되고 있다. 그 메소드들을 사용하여 필요한 정보를 조립하고 있다. 주요 메소드가 정리해 두자.

오토 리버스 설정

"Timeline".setAutoReverse("boolean");

애니메이션을 왕복 재생하는 것디다. true로 하면 오토 리버스가 설정된다.

재생 횟수 설정

"Timeline".setCycleCount("int");

애니메이션의 재생 횟수를 정수로 지정한다.

KeyFrame을 관리하는 List 얻기

"Timeline".getKeyFrames ();

Timeline에 설정되어 있는 KeyFrame는 List (ObservableList) 인스턴스에 정리해 Timeline에 보관되어 있다. 이 List를 얻는하는 것은 getKeyFrames이다.

KeyFrame 추가

"ObservableList".add("KeyFrame");

Timeline에 KeyFrame을 설정하려면, getKeyFrames에서 List를 얻어 "add"메소드로 추가한다. 이것으로 KeyFrame이 추가된다.

그럼, 실제로 Timeline을 사용하여 애니메이션을 샘플을 만들어 보자. 아래에 createShape 메소드를 재 작성하였다.

// import javafx.animation.*;
// import javafx.util.Duration;
 
public void createShape(Pane root){
    Rectangle r = new Rectangle(20, 20, 100, 50);
    r.setFill(Color.CYAN);
    root.getChildren().add(r);
     
    Timeline tl = new Timeline();
    tl.setAutoReverse(true);
    tl.setCycleCount(10);
 
    // 가로 작업 KeyFrame 작성
    KeyFrame key_a1 = new KeyFrame(
        new Duration(0),
        new KeyValue(r.widthProperty(),100));
    KeyFrame key_a2 = new KeyFrame(
        new Duration(2500),
        new KeyValue(r.widthProperty(),200));
 
    // 채우기 색 작업 KeyFrame 작성
    KeyFrame key_b1 = new KeyFrame(
        new Duration(0),
        new KeyValue(r.fillProperty(),Color.rgb(255, 0, 0)));
    KeyFrame key_b2 = new KeyFrame(
        new Duration(2500),
        new KeyValue(r.fillProperty(),Color.rgb(0, 0, 255)));
    tl.getKeyFrames().add(key_a1);
    tl.getKeyFrames().add(key_a2);
    tl.getKeyFrames().add(key_b1);
    tl.getKeyFrames().add(key_b2);
    tl.play();
}

이것을 실행해 보면, 사각형의 색상이 빨강 ~ 파랑 사이에서 변화하면서 폭이 늘었다가 줄여든다.

여기에서는 Rectangle의 폭 속성을 얻기 위하여 "widthProperty"라는 메소드를 사용한다. 이것은 "DoubleProperty"라는 double 값의 속성을 얻어온다.

또한 채우기 색을 얻기 위하여 "fillProperty"라는 메소드를 사용한다. 이것은 "ColorProperty"이라는 Color를 값으로 하는 Property이다. 인수에는 Color 클래스의 "rgb"라는 메소드를 사용하여 RGB 각 밝기를 인수로 지정한 색을 설정하고 있다.

Timeline을 잘 다룰 수있게 되면, 다양한 속성의 값을 자유롭게 조작하고 움직일 수 있게 한다. Node에 어떤 조작 가능한 속성이 있는지 알아 보면 재미있을 것이다.


JavaFX에는 그래픽 작업용 기능이 여러가지 제공되어 있다. 표시 위치나 방향 등을 변환하거나 모양을 투과하거나 흐림과 그림자 효과 등을 적용해 보고, 이러한 특수 효과에 대해 정리하고 설명한다.


트랜스포메이션(Transformation)

모양의 도형은 기본 형상은 매우 간단하다. 예를 들어 사각형이라면, 가로, 세로, 수평, 수직 선뿐이다. "조금 기울어 진 사각형"같은 컨포넌트는 없다. 그렇다면 이러한 것은 어떻게 만들면 될까?

그런 경우에는 "트랜스포메이션(transformation)"을 이용하면 된다. 이것은 모양의 표시를 이동, 회전, 확대, 축소하는 기능이다. 모양의 슈퍼 클래스에 해당하는 Node 클래스에는이를 위한 메소드가 다음과 같이 제공되고 있다.

평행 이동

"Node".setTranslateX(이동률);
"Node".setTranslateY(이동률);
"Node".setTranslateZ(이동률);

회전

"Node".setRotate(회전각도);

확대 축소

"Node".setScaleX(확대율);
"Node".setScaleY(확대율);
"Node".setScaleY(확대율);

평행 이동은 X, Y, Z 축이 각각의 방향으로 평행 이동하는 것이다 (왜 Z 축까지 있지? 라고 생각 하겠지만, 사실 JavaFX는 3D 그래픽 기능도 포함되어 있기 때문이다).

회전은 회전 각도를 지정한다. "역 회전은?"인 경우에는 마이너스의 각도를 지정하면 된다.

확대 축소도 역시 X, Y, Z 축이 각각의 방향으로 준비되어 있다. 이것은 확대율이므로, 1.0보다 크면 확대, 작으면 축소한다.

간단한 사용 예제는 다음과 같다. 지난번 만든 모양 생성 createShape 방법을 재작성하는 형태로 작성되어 있다.

public void createShape(Pane root){
    for(int i = 0;i < 100;i++){
        Rectangle r = new Rectangle(10, 10, 25, 25);
        r.setFill(Color.YELLOW);
        r.setStroke(Color.GREEN);
        r.setStrokeWidth(3);
        r.setRotate(10 * i);
        r.setTranslateX(5 * i);
        r.setTranslateY(2.5 * i);
        r.setScaleX(1 + 0.05 * i);
        root.getChildren().add(r);
    }
}

실행하면 사각형 모양이 조금씩 변화하면서 그려진다 것을 알 수 있다.

Rectangle를 작성하고 평행 이동, 회전, 확대하여 도형의 표시를 조금씩 밀어간다. new Rectangle로 만든 도형의 위치와 크기는 모두 동일하지만 표시는 조금씩 변화 해 나가는 것이다.



도형의 투과

지금까지의 도형은 모든 도형의 내부를 정해진 색으로 채워진 형태로 되어 있었다. 도형을 포개면, 물론 아래에 있는 도형은 숨겨져 보이지 않게 되었다. 그럼 도형을 반투명하고 아래의 것이 비쳐 보이는 용의 표시를하고 싶다면 어떻게 하면 될까?

그런 경우에는 "setOpacity"라는 메소드를 사용한다. 이것은 도형의 투과율을 설정하는 메소드로 다음과 같이 사용한다.

"Node".setOpacity(투과율);

인수는 0 ~ 1 사이의 실수(double)를 지정한다. 제로라면 투명하게, 1이면 불투명하다.

이것으로 도형을 반투명하게 하는 것은 간단하게 할 수 있다. 다만, 실제로 해보면 알 수 있지만, 이 방법은 "윤곽선만 그려서 도형의 내부는 반투명하게 한다"는 것은 되지 않는다. 이것은 도형 전체를 동일하게 투과하는 것이다. 내부뿐만 아니라 윤곽선도 마찬가지로 투명하게 한다.

도형의 내부를 투과하고 싶은 경우는, setFill을 사용하면 된다. 이것으로 Color.TRANSPARENT를 지정하면 내부가 투명하게 윤곽선만의 도형이 그려진다.

아래 소스 코드는 사용 예제 소스 코드이다.

public void createShape(Pane root){
    for(int i = 0;i < 20;i++){
        Rectangle r = new Rectangle(10, 10, 50, 50);
        r.setFill(Color.BLUE);
        r.setStroke(Color.WHITE);
        r.setTranslateX(20 * i);
        r.setTranslateY(10 * i);
        r.setOpacity(1 - 0.05 * i);
        root.getChildren().add(r);
        Rectangle r2 = new Rectangle(10, 10, 50, 50);
        r2.setStroke(Color.RED);
        r2.setFill(Color.TRANSPARENT);
        r2.setTranslateX(20 * i);
        r2.setTranslateY(10 * i);
        root.getChildren().add(r2);
    }
}

실행하면 사각형이 가로로 나란히 표시된다. 윤곽선만은 빨간색으로 표시되고 그 내부는 조금씩 투명하게 바뀐다.

여기에서는 setOpacity 전체를 투과한 도형과 setFill(Color.TRANSPARENT) 내부를 투과한 도형을포개어서 "윤곽선은 그대로 두고, 내부만 조금씩 투과하는 모양"을 표현하고 있다.



흐림(Blur) 효과

셰이프에는 시각 효과를 위한 기능도 포함되어 있다. 시각 효과라고 하면 무슨 뜻인지 알기 어렵겠지만, 예를 들면 '흐림'등이 대표적이다.

시각 효과는 shape(Node의 서브 클래스)에 포함되어 있는 "setEffect"라는 메소드를 사용하여 설정한다. 이것은 다음과 같이 실행한다.

"Node".setEffect("Effect");

인수에는 시각 효과의 내용을 나타내는 "Effect"라는 클래스 (또는 그 서브 클래스)의 인스턴스를 지정한다. 시각 효과는 많은 것이 제공되어 있으며, 그것들은 모두 Effect 클래스의 서브 클래스로 사용할 수 있다.

흐림 관계된 시각 효과 클래스로는 다음과 같은 것들이 있다. 또한 생성자는 시각 효과를 표현하는데 필요한 설정 값 인수에 대해 설명하겠다.

Gaussian Blur

new GaussianBlur(반경)

Gaussian Blur는 가우스 곡선을 이용하여 흐림 효과를 내는 것이다. 넓은 범위에 흐림을 매끄럽게 적용할 시에 사용된다. 인수에는 흐름 반경이 되는 실수를 지정한다.

Motion Blur

new MotionBlur(각도, 반경)

Motion Blur는 고속으로 이동하는 모습을 촬영한 것 같은 흐림 효과를 내는 것이다. 인수에는 흐림 방향을 나타내는 각도와 흐림의 강도를 나타내는 반경을 각각 실수로 지정한다.

Box Blur

new BoxBlur(폭, 높이, 반복)

Box Blur는 인접 픽셀의 평균값을 바탕으로 흐림 효과를 내는 것이다. 인수는 박스의 가로 폭과 높이, 시각 효과의 반복 수를 지정한다. 폭과 높이는 double 값, 반복 수는 int 값이다.

간단한 사용 예는 다음과 같다.

public void createShape(Pane root){
    for(int i = 0;i < 10;i++){
        Rectangle r = new Rectangle(20, 20, 50, 50);
        r.setTranslateX(25 * i);
        r.setTranslateY(15 * i);
        r.setFill(Color.BLUE);
        r.setStroke(Color.RED);
        r.setStrokeWidth(5);
        r.setEffect(new GaussianBlur(2.0 * i));
        root.getChildren().add(r);
    }
}

소스 코드를 실행해 보면 조금씩 Gaussian Blur으로 흐림 효과를 강화하게 사각형을 그려간다.



그림자 효과와 반사

흐림 효과와 함께 많이 사용되는 시각 효과라고 하면 "그림자" 효과일거다. 그림자에는 몇 가지 종류가 있다. 여기서 간단하게 정리해 보겠다.

드롭 섀도우

new DropShadow(반경, 가로 오프셋, 수직 오프셋, 색상);

도형의 그림자가 그 아래에 떨어지는 효과를 "드롭 섀도우"라고 한다. 이것은 "DropShadow" 클래스로 제공된다. 생성자에는 여러 가지가 있지만, 가장 자주 사용되는 것은 그림자가 적용되는 반경 (그림자의 폭), 그림자의 가로 세로 방향의 편차 폭 (오프셋), 그림자 색 (Color 인스턴스)을 대충 인수로 지정하여 작성하는 것이 일반적이다.

내부의 섀도우

new InnerShadow(반경, 가로 오프셋, 수직 오프셋, 색상);

도형의 내부가 움푹 패인 것처럼 안쪽에 그림자를 그리는 것이다. 이것도 DropShadow와 마찬가지로 인수에는 반경, 가로 세로 오프셋, 그림자 색을 지정한다.

반사

new Reflection(상단 오프셋,  프랙션(fraction),  상부 투과폭,  바닥 투과폭);

이것은 그림자와는 조금 다르지만, 그림자처럼 빛의 효과로 표현한다. 리플렉션(reflection)은 얼음이나 물 등 위에 도형이 있듯이, 그 아래에 반전된 이미지를 표시하는 것이다. 인수에는 그 도형과 반사 도형과의 간격, 반사 도형이 그려진 비율 (프랙션, 0 ~ 1의 실수), 도형의 위와 아래의 지정된 비율을 투과하기 위한 설정 (0 ~ 1의 실수)등을 지정한다.

실제 사용 예제는 아래와 같다.

public void createShape(Pane root){
    Rectangle r = new Rectangle(20, 20, 100, 50);
    r.setFill(Color.BLUE);
    r.setStroke(Color.RED);
    r.setStrokeWidth(5);
    r.setEffect(new DropShadow(20.0, 10.0, 5.0, Color.BLACK));
    root.getChildren().add(r);
     
    Ellipse e = new Ellipse(220, 50, 70, 30);
    e.setFill(Color.YELLOW);
    e.setStroke(Color.GREEN);
    e.setStrokeWidth(5);
    e.setEffect(new Reflection(5.0, 2.0, 0.5, 0));
    root.getChildren().add(e);
     
    Text t = new Text(50, 250, "Hello!");
    t.setFont(new Font(80));
    t.setFill(Color.YELLOW);
    t.setStroke(Color.GREEN);
    t.setStrokeWidth(1);
    t.setEffect(new InnerShadow(2.0, 2.0, 1.0, Color.BLACK));
    root.getChildren().add(t);
}

DropShadow와 InnerShadow은 그 만큼 어려워 아니라고 생각한다. Reflection는 반사 도형의 표시 폭과 상하가 스치는 느낌 등을 설정할 수 있기 때문에, 각각의 인자의 역할을 알면 재미있는 효과를 얻을 수있게 된다.



효과 체인 여러 시각 효과를 적용

다양한 시각 효과를 소개했지만, 이 모든 Node의 "setEffect"로 설정을 했었다. 결국은 동시에 여러 Effect를 설정할 수 없다는 것이다.

하지만 실제에 여러 시각 효과를 동시에 사용하고 싶은 경우가 있다. 이런 경우는 어떻게 해야 할까?

이것은 좀 발상을 바꾸어 생각해야할 필요가 한다. setEffect에서 설정할 수 있는 Effect는 1개뿐이다. 이것은 바꾸려는 것이 아니다. 주목해야 하는 것은 적용하는 Node가 아니라 시각적 효과가 될 Effect 클래스이다.

Effect 클래스는 입력된 이미지에 효과를 적용하여 출력하는 역할을 한다. 즉 "입력 이미지"을 변환해서 "출력 이미지"를 생성하여 전달하는 것이다. 결국은 어떤 시각 효과의 출력을 다른 시각 효과의 입력에 전달할 수 있다면, 여러 시각 효과를 사용할 수있을 것이다.

즉, "시각 효과 A" → "시각 효과 B"→ "시각 효과 C"......로 차례로 시각 효과를 적용해 가고, 최종 결과를 setEffect로 설정하는 것이다. 이것을 "효과 체인(effect chain)"이라고 한다. 이 효과 체인은 "setInput "라는 메소드를 이용하여 설정할 수 있다.

"Effect".setInput("Effect");

setInput는 시각 효과가 있는 Effect 클래스의 메소드이다. 인수에는 다른 Effect 인스턴스를 지정한다. 따라서 인수로 설정된 Effect의 결과를 바탕으로 더욱 시각적 효과를 설정할 수 있다.

실제 사용 예제는 아래와 같다.

public void createShape(Pane root){
    InnerShadow is = new InnerShadow(10.0, 2.0, 2.0, Color.rgb(0, 100, 100));
    DropShadow ds = new DropShadow(20.0, 10.0, 5.0, Color.BLACK);
    ds.setInput(is);
    Reflection rf = new Reflection(0.0, 2.0, 0.5, 0);
    rf.setInput(ds);
 
    Rectangle r = new Rectangle(20, 20, 100, 50);
    r.setFill(Color.CYAN);
    r.setEffect(rf);
    root.getChildren().add(r);
}

여기서 InnerShadow, DropShadow, Reflection 3가지 Effect를 setInput에 이어 시각 효과를 생성한다. 효과 체인 사용법을 알면 얼마든지 시각 효과를 조합해 사용할 수 있다.

JavaFX에서는 GUI 부품처럼 화면에 배치 할 수 있는 벡터 그래픽 부분이 있다. 이를 이용한 그래픽의 생성을 설명한다.

FXML 셰이프를 사용하기

Canvas를 사용한 그래픽 그리기는 Swing, AWT 등과 감각적으로는 비슷하다. 바꿔 말하자면, 그래픽 컨텍스트를 얻어서 렌더링 메소드를 호출하는 화면에 그리는 방식이다. 그려지는 그래픽은 단순한 비트맵 그래픽이다. 그 자체가 그리게 되면 그것으로 끝이다.

이러한 비트맵 그래픽과는 별도로, 벡터 그래픽 작업용 기능도 JavaFX에 포함되어 있다. 벡터 그래픽이라는 것은 위치나 크기 등의 그래픽 정보를 보유하고있는 도형이다. 그리고 난 후에 그것을 변경하고 위치 나 크기 등을 조작할 수 있다. 또한 데이터로 만들어 필요에 따라 그리고 고칠수 있기 때문에, 확대 축소해도 비트맵 그래픽처럼 도형이 거칠거나 하지 않는다.

이 벡터 그래픽은 javafx.scene.shape라는 패키지에 포함되어 있다. 이것은 FXML 태그로 사용할 수 있다. 그러면 실제로 간단한 사용 예를 살펴 보자.

<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.shape.*?>
<Pane xmlns="http://javafx.com/javafx"
    xmlns:fx="http://javafx.com/fxml"
    fx:controller="com.tuyano.libro.AppController">
    <Rectangle x="50" y="50" width="100" height="100"
        fill="RED" stroke="BLUE" strokeWidth="5"/>
    <Circle centerX="150" centerY="150" radius="50"
        fill="GREEN" stroke="CYAN" strokeWidth="5"/>
</Pane>

위에 예제는 사각형과 원형 모양을 표시하는 것이다. 이를 FXML 파일에 작성하고 Java에서 로드 Scne에 표시한다. 예를 들어, app.fxml라는 파일로 생성했다면 아래와 같은 식으로 실행하면 된다.

Scene scene = new Scene(FXMLLoader.load(getClass().getResource("app.fxml")),300,300);
stage.setScene(scene);
stage.show();

여기에서는 빨간색 사각형과 녹색의 원형이 윈도우에 표시된다. 이번에는 셰이프(Shape)을 사용하여 윈도우에서 자유롭게 그래픽을 배치하기 때문에 <BorderLayout> 같은 레이아웃 컨테이너는 사용 의미가 없을 것이다. 그래서 <Pane>라는 컨테이너를 루트 태그로 지정하고 있다. <Pane>은 어떤 레이아웃 기능도 없는 가장 단순한 컨테이너이다. 셰이프처럼 레이아웃이 필요가 없는 컨포넌트를 배치할 때 이용하면 좋을 것이다.



주요 셰이프 FXML 태그

여기에서는 사각형과 원형 모양을 만들었다. 이것들 외에도 다른 모양도 있다. 여기에서 중요한 것은에 대해 정리해 보도록 하겠다.

사각형 모양 "Rectangle"

예제로 사용했던 사각형 모양이다. 여기에는 다음과 같은 속성이 포함되어 있다.

속성설명
x가로 위치를 지정한다.
y세로 위치를 지정한다.
width폭을 지정한다.
height높이 (세로 폭)을 지정한다.

원 모양 "Circle"

이것도 예제로 사용했었다. 둥근 원를 표시하는 셰이프이다. 사각형과는 특성이 약간 다르다.

속성설명
centerX중심의 가로 위치를 지정한다.
centerY중심의 세로 위치를 지정한다.
radius반경을 지정한다.

타원 모양 "Ellipse"

이것은 타원을 그리는 셰이프이다. 둥근 원형과 달리 수직 및 수평 반경을 각각 별도로 지정할 수 있다.

속성설명
centerX중심의 가로 위치를 지정한다.
centerY중심의 세로 위치를 지정한다.
radiusX가로 반경을 지정한다.
radiusY세로 방향의 반경을 지정한다.

원호 모양 "Arc"

원형의 일부만을 자른거 같은 원호를 그리는 것이다. 타원의 특성 이외에 호 크기(각도)에 관한 것이 추가되어 있다.

속성설명
centerX중심의 가로 위치를 지정한다.
centerY중심의 세로 위치를 지정한다.
radiusX가로 반경을 지정한다.
radiusY세로 방향의 반경을 지정한다.
startAngle호의 시작점의 각도 (0 ~ 360의 값)을 지정한다.
length원호의 크기를 각도로 지정한다.
type호 유형이다. ROUND (중심에서 자르기), CHORD (원호의 끝을 직선으로 연결), OPEN (원호의 끝을 닫지) 중 하나를 지정한다.

직선 모양 "Line"

2점을 연결하는 직선을 그리는 셰이프이다. 시작 지점과 끝 지점의 속성이 필요하다.

속성설명
startX시작 지점의 수평 위치를 지정한다.
startY시작 지점의 수직 위치를 지정한다.
endX종료 지점의 수평 위치를 지정한다.
endY끝점의 세로 위치를 지정한다.

모양 전반에 관한 특성

이 밖에 모든 셰이프에 공통적으로 포함되는 속성도 있다. 기본적으로 채우기나 선 정보가 포함되어 있다. 선은 뾰족한 끝의 셰이프의 상태 등 아주 많은 속성이 있는데 ,우선 아래의 3개만 기억해두도록 하자.

속성설명
fill채우기 색을 지정합니다.
stroke선의 색상을 지정합니다.
strokeWidth선 두께를 지정합니다.

우선, 이런 것들이 사용할 수 있게 되면, 기본 도형은 그릴 수 있게 된다. 실제로 태그를 써서 연습을 해보길 바란다.



직선, 곡선의 셰이프

원형과 사각형은 아주 간단한 도형이지만, 더 복잡한 도형을 필요로 할 수도 있다. 이러한 경우에 사용되는 것이 "직선의 다각형(polygon)"이나 "곡선"같은 도형이다.

이러한 도형은 위치 정보에 대한 특성이 매우 많이 필요하며 그 만큼 작성도 복잡하게 된다. 다음 사용법을 대해 정리해보자.

직선 (다각형)의 모양 "Polygon", "Polyline"

여러 점을 직선으로 연결하는 다각형을 그리기 위한 것이다. Polygon은 시작 지점과 끝 지점을 연결하는 "닫힌 도형"을 그리고, Polyline은 양 끝을 맺지 않는 "개방 된 도형"을 그린다. 기본적인 사용법은 모두 동일하다.

이러한 FXML로 작성하는 경우에는 시작 태그와 종료 태그 사이에 <points>라는 태그를 제공하고, 나아가 그 안에 <Double< 태그를 사용하여 각 위치의 가로 세로 위치 정보를 작성해 간다.

곡선 모양 "QuadCurve", "CubicCurve"

곡선은 두 가지 모양이 있다. "QuadCurve"는 2차 곡선을 그리기 위한 것이다. 이것은 시작과 끝 지점의 2점 외에 1개의 컨트롤 포인트를 사용해 그려진다. Arc으로 그리는 원호 같은 곡선이다.

"CubicCurve"은 3차 곡선을 그리기 위한 것이다. 이것은 시작과 끝 지점의 다른 두 개의 컨트롤 포인트를 사용해 그려진다. 이른바 베지어 곡선(Bezier curves)이라고 불리는 것이 이에 해당된다.

이들은 시작점과 끝점, 그리고 컨트롤 포인트의 위치를 모든 속성으로 작성한다.

속성설명
startX시작 지점의 수평 위치를 지정한다.
startY시작 지점의 수직 위치를 지정한다.
endX종료 지점의 수평 위치를 지정한다.
endY끝점의 세로 위치를 지정한다.

QuadCurve의 경우

속성설명
controlX컨트롤 포인트의 수평 위치를 지정한다.
controlY컨트롤 포인트의 세로 위치를 지정한다.

CubicCurve의 경우

속성설명
controlX1제 1 컨트롤 포인트의 수평 위치를 지정한다.
controlY1제 1 제어 포인트의 세로 위치를 지정한다.
controlX2제 2 컨트롤 포인트의 수평 위치를 지정한다.
controlY2제 2 제어 포인트의 세로 위치를 지정한다.

아래 Polygon과 CubicCurve을 표시하는 FXML 샘플이다.

<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.shape.*?>
<?import javafx.scene.shape.Ellipse?>
<?import javafx.scene.shape.Line?>
<?import javafx.scene.shape.Polygon?>
<?import javafx.scene.shape.CubicCurve?>
<Pane xmlns="http://javafx.com/javafx"
    xmlns:fx="http://javafx.com/fxml"
    fx:controller="com.devkuma.javafx.AppController">
 
    <Polygon fill="RED">
    <points>
        <Double fx:value="110.0" />
        <Double fx:value="10.0" />
        <Double fx:value="210.0" />
        <Double fx:value="20.0" />
        <Double fx:value="150.0" />
        <Double fx:value="100.0" />
    </points>
    </Polygon>
 
    <CubicCurve fill="YELLOW" stroke="BLUE" strokeWidth="5"
        startX="50" startY="50" endX="200" endY="200"
        controlX1="200" controlY1="50"
        controlX2="50" controlY2="200" />
 
</Pane>

<Polygon> 태그 안에 <points> 태그가 있고, 또 안에는 <Double> 태그를 사용하여 각 정점의 가로와 세로를 순서대로 작성되어 있다. 작성 방법이 조금 이해하기 어렵기 때문에 잘 태그의 구조를 이해하고 사용해야 한다.




Java 소스 코드로 셰이프 사용

FXML를 사용하지 않고, Java 소스 코드에서 직접 모양의 개체를 만들고, 스테이지에 설정 표시 할 수 있다. 실제로 해보록 하자.

아래에 샘플을 적어 보겠습니다.

package com.devkuma.javafx;
     
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.stage.Stage;
 
public class App extends Application {
     
    public static void main(String[] args) {
        launch(args);
    }
 
    @Override
    public void start(Stage stage) {
        Pane root = new Pane();
        createShape(root);
        Scene scene = new Scene(root,300,300);
        stage.setScene(scene);
        stage.show();
    }
     
    public void createShape(Pane root){
        Rectangle r = new Rectangle(50, 50, 100, 100);
        r.setFill(Color.RED);
        r.setStroke(Color.BLUE);
        r.setStrokeWidth(3);
        root.getChildren().add(r);
        Circle c = new Circle(150, 150, 50);
        c.setFill(Color.YELLOW);
        c.setStroke(Color.GREEN);
        c.setStrokeWidth(10);
        root.getChildren().add(c);
    }
}

여기에서는 createShape라는 메소드를 사용하고, 그 속에서 사각형과 원형 모양을 그릴 수 있다. 먼저 FXML로 만든 샘플과 같은 것이기 때문에, 양자를 비교해 보면 차이를 잘 알 수 있다.

인스턴스 만들기

모양의 클래스는 Rectangle와 Circle 모두 FXML 태그와 같은 이름이다. 인스턴스의 생성은 모두 필수 속성을 인수로 지정하는 형태로 되어 있다.

new Rectangle(가로 위치, 세로 위치, 폭, 높이)
new Circle(중심 가로 위치, 중심 세로 위치, 반경)

이 클래스에는 인수가 없는 디폴트 생성자에서 여러 종류의 생성자가 존재하며, 다양한 방식으로 인스턴스를 생성할 수 있다. 필수 항목이 되는 속성의 값을 모두 인수로 넣은 모양이 가장 잘 알기 쉽다.

속성 설정

작성 후, fill, stroke, strokeWidth의 속성을 설정하는 메소드를 호출한다. Rectangle라면 다음과 같다.

r.setFill (Color.RED);
r.setStroke (Color.BLUE);
r.setStrokeWidth (3);

FXML 태그에 포함되어 있던 속성은 "set 속성 이름"라는 메소드로 값을 설정할 수 있다. 또한 이번에는 사용하지 않았지만 "get 속성 이름" 또는 "is 속성 이름"라는 메소드로 값을 얻을 수 있다.

색상 값에 대해

이번 setFill과 setStroke에서는 Color 클래스의 필드를 지정하고 있다. 이들은 먼저 Canvas에서 이용을 하였다. 채우기 및 선 색상을 Color로 설정하는 등 기본적인 아이디어는 Canvas 그리기와 거의 같은 것이다.



클리핑(clipping)으로 오려내기

기본 도형을 만드는 방법은 대체로 알게 되었다. 하지만, 좀 더 복잡한 도형을 만드는 방법에 대한 배우고 싶은 기능에 대해서도 몇 가지 보충 하도록 하자.

우선 '클리핑(clipping)'에 대해서이다. 클리핑라는 것은 윈도우에 '창'을 열고, 거기에서 표시하는 것이다. 요약하면, 그려진 그래픽 일부분만 잘라내어 표시할 수 있다.

이 클리핑은 "Node"라는 클래스에 있는 "setClip"라는 메소드로 설정할 수 있다. Node 클래스는 BorderLayout과 Pane 등의 컨테이너 종류이며, 나아가서는 여기에서 다룬 모양 종류의 슈퍼 클래스이기도 하다.

"Node".setClip("Node");

이 setClip는 인수로 Node 인스턴스를 지정한다. 그거로 부터 그 부품에 인수 노드의 형상을 클리핑 영역으로 설정한다. 그 부품의 표시는 인수에 지정된 노드 형상의 모양으로 잘라낸 것이 표시된다. 인수 노드의 형상은 외부에 아무것도 그려지지 않는다.

그럼 실제로 해 봅시다. 아래에 예제 코드와 같이 작성해 보자. 이전에 createShape 방법을 수정하여 사용하자.

public void createShape(Pane root){
    Rectangle r = new Rectangle(50, 50, 100, 100);
    r.setFill(Color.RED);
    r.setStroke(Color.BLUE);
    r.setStrokeWidth(3);
    root.getChildren().add(r);
    Circle c = new Circle(150, 150, 50);
    c.setFill(Color.YELLOW);
    c.setStroke(Color.GREEN);
    c.setStrokeWidth(10);
    root.getChildren().add(c);
    Circle clip = new Circle(120, 120, 75);
    root.setClip(clip);
}

여기에서는 Rectangle와 Circle을 작성하여 root에 설정한 후, 클리핑 용 Circle을 만들었다. 그리고 그것을

root.setClip(clip);

으로 클리핑으로 설정하고 있다. 그러면 setClip한 Circle 원형 모양으로 잘라 내서 그래픽이 표시가 된다.



경로(Path)로 복잡한 도형 생성

또 다른 복잡한 도형 그리는 방법으로 "경로(Path)"를 이용할 있다. 경로는 단번에 그리는 것과 같이 직선과 곡선을 그려 나가기로 도형을 작성하는 것이다.

이것은 "Path"클래스를 이용하여 작성한다. Path 인스턴스를 만들고 거기에 선 그래픽이 되는 인스턴스를 생성하고 설정하여 도형을 만들다. Path에 설정 할 수 있는 그래픽 부분은 "PathElement"라는 클래스의 서브 클래스로 다음과 같은 것이 있다.

드로잉 위치를 이동하는 MoveTo

new MoveTo(가로 위치, 세로 위치)

단번에 그려 그리기 위치를 이동하는 것이다. 인수로 지정된 위치에 렌더링 위치를 이동한다.

직선을 그리는 LineTo

new LineTo(종단 가로 위치, 종단 세로 위치)

현재 드로잉 위치에서 지정된 지점까지 직선을 그린다. 인수로 선의 종단이 되는 위치를 지정한다.

2 차 곡선을 그리는 QuadCurveTo

new new QuadCurveTo(CP 가로 위치, CP 세로 위치, 종단 가로 위치, 종단 세로 위치)

현재 드로잉 위치에서 2차 곡선을 그린다. 인수에는 컨트롤 포인트의 위치와 종료 위치를 지정한다.

3 차 곡선을 그리는 CubicCurveTo

new new QuadCurveTo(CP 가로 위치1, CP 세로 위치1, CP 가로 위치2, CP 세로 위치2, 종단 가로 위치, 종단 세로 위치)

현재 드로잉 위치에서 3차 곡선을 그린다. 인수에는 2개의 컨트롤 포인트의 위치와 종료 위치를 지정한다.

그럼, 실제 사용 예제를 보도록 하자. 아래의 목록은 아까의 샘플 createShape 메소드를 수정 한 것이다. 이것으로 두 개의 직선과 1개의 3차 곡선으로 이루어진 경로 도형을 만들고 표시한다.

public void createShape(Pane root){
    Path path = new Path();
    MoveTo mt1 = new MoveTo(50, 50);
    path.getElements().add(mt1);
    LineTo lt1 = new LineTo(250, 50);
    path.getElements().add(lt1);
    CubicCurveTo cc1 = new CubicCurveTo(250, 250,50, 50, 50, 250);
    path.getElements().add(cc1);
    LineTo lt2 = new LineTo(50, 50);
    path.getElements().add(lt2);
    path.setFill(Color.RED);
    root.getChildren().add(path);
}

여기에서는 Path 인스턴스를 만들고 MoveTo, LineTo, CubicCurveTo와 같은 인스턴스를 생성하지 포함되어 있다. 도형의 연결은 아래와 같이 한다.

path.getElements().add(mt1);

Path의 "getElements"메소드는 Path에 설정되어 있는 List(ObserbableList) 인스턴스를 얻기 위해 사용된다.

Path에는 이 List에 그리는 도형의 PathElement를 관리하고 여기에 add 메소드에서 인스턴스를 추가해 나갈 것으로 그리는 도형이 추가되어 간다. 단순한 도형의 조합으로 그릴 수 없는 복잡한 형상의 모양도 Path를 이용하면 그릴 수 있다.



JavaFX의 그래픽 그리기, AWT와 Swing과는 전혀 다르다. 그 다른 시스템을 이해하면 렌더링 처리 자체는 쉽게 수행 할 수 있다. 여기에서는 그 기본이 되는 Canvas 컨트롤과 GraphicsContext 클래스의 사용법에 대해 설명한다.


Canvas와 GraphicsContext

그래픽 그리기는 AWT에도 Swing에서도 기본적인 아이디어는 동일했었다. 구성 요소에는 표시를 업데이트 할 때 호출되는 paint 메소드 (또는 paintComponent)가 제공되어 있으며, 이 메서드를 오버라이딩하면 자동으로 호출되어 그리기가 실행되었다.

각각의 구성 요소에는 렌더링 처리를 관리하는 Graphics (또는 Graphics2D) 클래스의 인스턴스가 포함되어 그것이 paint 메소드에 인수로 전달된다. 이 Graphics에 있는 드로잉 메서드를 호출하여 그리기를 할 수 있었다.

하지만 JavaFX는 상당히 사정이 다르다. 먼저, 컨트롤에는 표시를 업데이트 때에 호출되는 메소드가 없다. 이 시점에서 "어? 그럼 어떻게 하는 거야?"라고 머리 하얗게 되어 버리는 Java 프로그래머도 많을지도 모르겠다. 메소드가 없으니 당연히 Graphics 전달되지 않는다. 그렇다면 그리기 수단이 없다 ...?

아니다. 그것은 반대다. 즉, "표시를 업데이트 때에 호출되는 메소드를 오버라이드(override)하지 않으면 그릴 수 없다"가 아니라, "언제 어디서나 필요할 때 그릴 수 있게 됐다"것이다. "이 메소드를 오버라이드하고 렌더링 처리를 작성하지 않으면 안된다"는 제한이 없고, 어디에서든 그려도 항상 그것이 컨트롤에 표시되며 이를 마음대로 끄거나 할 수 없게 되는 것이다.

그래픽 그리기를 하려면 "Canvas"라는 컨트롤를 준비해야 된다. AWT에도 같은 이름의 Canvas 클래스가 있었지만, 그것과는 전혀 다른 것이다. JavaFX용은 javafx.scene.canvas 패키지에 포함되어 있다.

Canvas에는 "GraphicsContext"라는 클래스의 인스턴스가 포함되어 있다. 이것이 AWT/Swing의 Graphics 클래스에 해당된다. 이 인스턴스를 얻어서 거기에 있는 메소드를 호출하는 것으로, Canvas에 도형 등을 그릴 수 있게 된다.

그럼 실제로 해보자. 우선은 FXML에서 Canvas를 추가하도록 하자. 아래와 같이 FXML 파일을 만들고 작성한다.

<?xml version="1.0" encoding="UTF-8"?>
 
<?import java.lang.*?>
<?import javafx.scene.canvas.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.layout.*?>
<BorderPane xmlns="http://javafx.com/javafx"
    xmlns:fx="http://javafx.com/fxml"
    fx:controller="com.devkuma.javafx.AppController">
    <center>
        <Canvas fx:id="canvas" width="300" height="300" />
    </center>
</BorderPane>

이 FXML을 로드하고 표시하는 Application 클래스는 별도 준비하도록 하자 (이전까지 작성했던 것을 그대로 사용해도 된다).

BorderPane의 center에 단순히 <Canvas> 태그를 배치 했을 뿐이다. <Canvas>에는 width와 height 속성을 추가되어 있다. 이렇게 하면 컨트롤의 크기가 조정된다.


GraphicsContext으로 그리기

그럼 컨트롤러 클래스에 그리기 위한 처리를 작성해 보자. 아래와 같이 간단한 예제를 작성하자.

package com.devkuma.javafx;
 
import java.net.URL;
import java.util.ResourceBundle;
 
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;
 
 
public class AppController implements Initializable {
    @FXML Canvas canvas;
    GraphicsContext gc;
     
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        gc = canvas.getGraphicsContext2D();
        draw();
    }
     
    void draw(){
        gc.setFill(Color.RED);
        gc.setStroke(Color.BLUE);
        gc.fillRect(50, 50, 150, 150);
        gc.strokeOval(100, 100, 50, 50);
    }
}

이걸 실행하면 빨간색 사각형 영역에 청색 선으로 동그라미가 그려진다.

Canvas의 그리기는 "GraphicsContext 얻어 오기", "그리기 설정", "드로잉 메서드 호출"이라는 형태로 수행한다. 각 단계별로 설명을 하겠다.

GraphicsContext 얻어 오기

변수 = canvas.getGraphicsContext2D();

GraphicsContext를 얻어 오는 것은 Canvas 인스턴스의 "getGraphicsContext2D"메소드로 수행한다. 이것으로 인스턴스를 얻을 수 있기 때문에, 이것을 변수 등에 보관해 두었다가 사용한다.

색상 설정

graphicsContext.setFill("Color");
graphicsContext.setStroke("Color");

그리기를 수행할 때 미리 그리는 도형에 관한 상세한 설정을 수행해야 한다. 여기에서는 "색"에 대한 설정 만 준비되어 있다. "setFill"은 채우기의 내용을 설정하는 것이고, "setStroke"는 선 그리기 내용을 설정하는 것이다.

어느 것이나 인수에는 "Paint"라는 인스턴스를 지정한다. 이것은 렌더링 방식을 나타내는데 사용되는 것이다. 정해진 색으로 칠하려면 Paint의 서브 클래스인 'Color'를 사용하는 것이 일반적이다.

Color는 기본 색상 값이 클래스 변수로 사용할 수 있다. 이것들을 지정하여 쉽게 색상을 설정할 수 있다. 기타 색상은

new Color(빨강, 녹색, 파랑)

이런 형태로 인스턴스를 작성할 수 있다. 이 부근은 java.awt.Color와 거의 같기 때문에 어떻게 해서든 알 수 있을 것이다.

드로잉 메서드

다음은 드로잉 메서드를 호출하여 그리기이다. 이번에는 사각형과 타원형 그리기를 수행하는 메소드를 호출하고 있다. 다음 문장이다.

gc.fillRect(50, 50, 150, 150);
gc.strokeOval(100, 100, 50, 50);

GraphicsContext에는 이 외에도 다수의 드로잉 메서드가 포함되어 있다. 많은 드로잉 메소드는 "도형의 내부를 채우기", "도형의 윤곽선을 그리기" 이렇게 2종류가 준비되어 있다. 채우기는 "fillOO", 윤곽선 그리기는 "strokeOO"라는 이름에 되어 있다. 여기에서는 "사각형(Rect) 채우기(fill)", "타원형(Oval) 그리기(stroke)" 2개의 드로잉을 하고 있기 때문에 각각 "fillRect", "strokeOval"라는 메소드를 호출한 거다.



주요 그리기 메소드

그럼 도형 그리기 메소드에는 어떤 것이 있을까? 여기에서 중요한 것은 정리하도록 하겠다.

직선 그리기

"GraphicsContext".strokeLine(시작 가로 위치, 시작 세로 위치, 종료 가로 위치, 종료 세로 위치);

2개의 점을 연결하는 직선을 그리기 위한 것이다. 인수에는 선의 시작점과 끝점을 지정한다.

사각형 그리기

"GraphicsContext".fillRectangle(가로 위치, 세로 위치, 폭, 높이);
"GraphicsContext".strokeRect(가로 위치, 세로 위치, 폭, 높이);

사각형을 그릴 수 있는 것이다. fillRectangle는 채워진 사각형, strokeRectangle은 윤곽선만 그린다. 인수는 각 도형의 오른쪽의 위치와 화면의 너비를 지정한다.

둥근 사각형 그리기

"GraphicsContext".fillRoundRectangle (가로 위치, 세로 위치, 폭, 높이, 각도 폭 모서리의 높이);
"GraphicsContext".stokeRoundRectangle (가로 위치, 세로 위치, 폭, 높이, 각도 폭 모서리의 높이);

모서리 부분이 둥글게 된 사각형을 그리기 위한 것이다. 인수는 위치와 크기 값뿐만 아니라 모서리 부분의 가로 세로 폭을 제공한다.

타원 그리기

"GraphicsContext".fillOval(가로 위치, 세로 위치, 폭, 높이);
"GraphicsContext".strokeOval(가로 위치, 세로 위치, 폭, 높이);

타원을 그리는 것이다. 기본적으로 사각형 그리기와 같고, 그리기 타원의 위치 (오른쪽 위치)과 가로 세로 폭을 지정한다.

원호 그리기

"GraphicsContext".fillArc(가로 위치, 세로 위치, 가로 반지름, 세로 반경, 시작 각도, 길이, 유형);
"GraphicsContext".stokeArc(가로 위치, 세로 위치, 가로 반지름, 세로 반경, 시작 각도, 길이, 유형);

원호를 그리기 위한 것이다. 위치와 크기 외에, 그 원형의 시작 지점과 길이, 유형에 대한 값을 지정한다. 유형은 그려지는 원호 모양의 형태에 관한 것이다. 이것은 ArcType이라는 열거 형 (Enum)의 값을 사용하여 지정한다. 사용할 수 있는 값은 다음과 같다.

  • CHORD : 원호의 끝을 직선으로 연결 한 형태의 도형을 그린다.
  • OPEN : 호 끝을 맺지 않고 열린 모양으로 그린다 (내부는 채워지지 않는다).
  • ROUND : 호 양단과 원형의 중심을 직선으로 연결 한 형태의 도형을 그린다.

· 다각형 그리기

"GraphicsContext".fillPolyline(가로 배열, 세로 배열, 정점 수);
"GraphicsContext".stokePolyline(가로 배열, 세로 배열, 정점 수);
"GraphicsContext".fillPolygon(가로 배열, 세로 배열, 정점 수);
"GraphicsContext".stokePolygon(가로 배열, 세로 배열, 정점 수);

여러 정점을 맺은 다각형을 그리기 위한 것이다. 인수는 각 정점의 가로와 세로를 각각 별도로 double 배열에 정리 한 것과 정점의 수를 지정한다. OOPolyline과 OOPolygon 두 종류가 있는데, 전자는 닫혀 있지 않은 도형, 후자는 닫힌 도형 (시작 지점과 종료 지점이 연결되어있는)를 그리는 것이다.

텍스트 그리기

"GraphicsContext".fillText(그리기 텍스트, 가로 위치, 세로 위치);
"GraphicsContext".stokeText(그리기 텍스트, 가로 위치, 세로 위치);

텍스트를 지정된 위치에 그린다. 그리는 텍스트와 그려지는 위치 값을 지정한다.


그라데이션으로 채우기

도형을 그릴 때 잘 이해하고 싶은 것은 '채우기 방법 "이다. 이전에 setFill 등으로 색상 설정을 했지만, 이것은 "색을 지정하기"가 아니라는 것을 잊지 말아라. "채우기 방식"을 지정하는 것이며, 인수도 Color가 아니라, Color의 슈퍼 클래스인 "Paint"을 지정하도록 되어 있다.

결국은 Paint의 하위 클래스이라면, Color 이외의 것을 지정할 수 있는 것이다. 그 예로 "그라데이션"의 클래스를 지정하여 보기로 하자.

LinearGradient 클래스

이것은 직선적인 그라데이션을 이용하기위한 클래스이다. 한 지점에서 다른 지점으로 점진적으로 색이 변화 해 나가는 것을 만들 수 있다. 이는 다음과 같이 인스턴스를 생성한다.

변수 = new LinearGradient (시작 가로, 시작 세로,  종료 가로,  종료 세로, 부울, 반복 방식, 목록);
  • 시작 가로, 시작 세로 : 그라데이션의 시작 지점이다.
  • 종료 가로, 종료 세로 : 그라데이션의 종료 지점이다.
  • 부울 : 균일하게 색을 변화시켜 나갈 것인지를 나타낸다.
  • 반복 방식 : CycleMethod이라는 열거 형 값을 사용하여 지정한다.
  • 목록 : 색의 변화 정보를 배열로 모은 것이다. "Stop"이라는 클래스의 인스턴스 준비한다.

RadialGradient 클래스

이것은 원형으로 색상이 변화해 나가는 것이다. 지정한 중심 위치에서 원형으로 색상이 변화해 가는 표현이 가능하다. 이는 다음과 같이 인스턴스를 생성한다.

변수 = new RadialGradient (포커스 방향, 초점 거리,  중심 가로, 중심 세로, 반지름, 부울, 반복 방식, 목록);
  • 포커스 방향 : 원형의 중심에서 본 시작 위치의 방향이다.
  • 초점 거리 : 원형의 중심에서 본 시작 위치의 거리이다.
  • 중심 가로, 중심 세로 : 그라데이션의 원형의 중심 위치이다.
  • 반경 : 그라데이션의 반경이다.
  • 반복 방식 : CycleMethod 열거 형에 의한 반복 방식을 지정하는 것이다.

"포커스"라는 것은 시작 위치를 원형의 중심에서 겹치지 않도록 비켜 내기 위한 것이다. 이것으로 원형의 중심에서 어느쪽으로 얼마나 비켜 내지 여부를 지정하는 것으로, 그라데이션의 시작 위치를 조정할 수 있다.

CycleMethod 열거 형

반복 방식을 지정하는 것이다. 이것은 다음의 값이 포함되어 있으며, 그 중 하나를 지정한다.

  • NO_CYCLE : 반복 색을 변화시키지 않는다.
  • REFRECT : 시작 지점의 색상과 종료 지점의 색상을 왕복하고 반복 변화시킨다.
  • REPEAT : 시작 지점의 색상과 끝 지점의 색상에 있다는 변화를 거듭하고 있다.

이러한 그라데이션의 사용 예제는 아래와 같다.

package com.devkuma.javafx;
 
import java.net.URL;
import java.util.ResourceBundle;
 
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.RadialGradient;
import javafx.scene.paint.Stop;

public class AppController implements Initializable {
    @FXML Canvas canvas;
    GraphicsContext gc;
     
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        gc = canvas.getGraphicsContext2D();
        draw();
    }
     
    void draw(){
        Stop[] stops1 = new Stop[] {
            new Stop(0, Color.RED),
            new Stop(1, Color.YELLOW)
        };
        LinearGradient gradient1 = new LinearGradient(0, 0, 1, 0, true, CycleMethod.NO_CYCLE, stops1);
        gc.setFill(gradient1);
        gc.fillRect(50, 50, 150, 150);
        Stop[] stops2 = new Stop[] {
            new Stop(0, Color.BLUE), 
            new Stop(0.5, Color.CYAN)
        };
        RadialGradient gradient2 = new RadialGradient(0, 0, 125, 125, 100, false, CycleMethod.NO_CYCLE, stops2);
        gc.setFill(gradient2);
        gc.fillOval(75, 75, 100, 100);
    }
}

여기에서는 LinearGradient으로 사각형을 그려서 그 안에 RadialGradient에 동그라미를 그리고 있다. 인수가 많기 때문에 어려울 것 보이지만, 하고 있는 것 자체는 비교적 간단하다.



이미지 파일 사용하기

보다 복잡한 그래픽을 렌더링하려면 미리 렌더링 그래픽을 이미지 파일로 준비해서 이를 로드하고 렌더링하는 것이 좋다. 미리 Image 인스턴스를 준비해두고, 그것을 렌더링하면 된다.

이 경우의 "Image"는 AWT의 Image가 아니다. 이는 javafx.scene.image 패키지에 포함되어 있는 클래스이다. 그러므로 AWT/Swing의 경우와는 차이가 있다.

Image 인스턴스 생성하기

변수 = new Image(경로);

Image 인스턴스의 생성은 여러가지 방법이 있지만, 우선 이 생성하는 방법만 기억해두자. 인수에 가져올 이미지 파일의 경로를 String로 지정하면 이를 로드한 Image 인스턴스가 생성된다.

Image 그리기

이것도 GraphicsContext 클래스에 메소드가 준비되어 있다. 이것은 렌더링 방식에 따라 여러 가지가 있다. 주요 방법을 정리해 두자.

지정 위치에 이미지 그리기

"GraphicsContext".drawImage("Image", 가로 위치, 세로 위치);

지정 영역에 이미지를 변형하여 그리기

"GraphicsContext".drawImage("Image", 가로 위치, 세로 위치, 폭, 높이);

이미지의 일부를 Canvas의 특정 영역에 그리기

"GraphicsContext".drawImage("Image", 가로  위치1, 세로  위치1, 너비1, 높이1, 가로 위치2, 세로 위치2, 너비2, 높이2);

그러면 실제로 이미지 파일을 그려 보자. 예를 들어, "sample.jpg"라는 이미지 파일을 읽어 Canvas에 표시하는 예제는 아래와 같다.

package com.devkuma.javafx;
 
import java.net.URL;
import java.util.ResourceBundle;
 
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.image.Image;

public class AppController implements Initializable {
    @FXML Canvas canvas;
    GraphicsContext gc;
     
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        gc = canvas.getGraphicsContext2D();
        draw();
    }
     
    void draw(){
        Image image = new Image("sample.jpg");
        gc.drawImage(image, 0, 0);
    }
}

여기에서는 'new Image("sample.jpg");'와 같이 해서 sample.jpg를 로드한다. Java 프로그램은 프로그램 파일이 배치되는 위치가 현재 디렉토리가 되므로 같은 위치에 sample.jpg을 준비해서 실행해야 한다.


JavaFX 컨트롤에는 값을 관리하는 속성에 전용 클래스가 제공되어 있으며, 거기에 이벤트 리스너를 설정하여 값이 변경시에 처리할 수 있다. 여기에서는 ToggleGroup, ComboBox, Slider에 대한 속성의 이벤트 처리 방식을 설명한다.


ToggleGroup의 ChangeListener처리

이전에 ListeView 대해 선택 상태가 변경 되었을 때의 이벤트 처리를 만들었다. 이것은 선택 상태를 관리하는 속성에 이벤트 리스너를 설정하였다.

이러한 "속성이 변경되었을 때에 ChangeListener에서 이벤트 처리를 한다"는 방식은, JavaFX 컨트롤의 기본적인 이벤트 처리 방식이다. 이 기본 개념을 알면 다른 컨트롤러에서도 유사한 방식으로 이벤트 처리를 설정할 수 있다.

우선 "라디오 버튼을 조작했을 때의 이벤트 처리"에서 생각해 보자. 라디오 버튼은 ToggleGroup라는 그룹 관리 클래스를 사용하여 여러 라디오 버튼을 하나로 관리하고 있었다. 라디오 버튼이 선택되면 이 ToggleGroup의 선택 상태를 나타내는 속성 값이 변경된다.

이 속성은 "selectedToggleProperty"라는 것으로, ReadOnlyObjectProperty 클래스의 인스턴스가 설정되어 있다. 이에 "addListener"에 ChangeListener를 설정함으로써 선택이 변경되었을 때 처리 할 수 있도록 되어 있다.

그러면 실제로 간단한 샘플을 만들어 보자.

<?xml version="1.0" encoding="UTF-8"?>
 
<?language javascript?>
<?import java.lang.*?>
<?import java.net.URL ?>
<?import javafx.scene.text.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.collections.FXCollections?>
<?import javafx.scene.layout.*?>
<BorderPane xmlns="http://javafx.com/javafx"
    xmlns:fx="http://javafx.com/fxml"
    fx:controller="com.devkuma.javafx.AppController">
    <stylesheets>
        <URL value="@app.css" />
    </stylesheets>
    <top>
        <Label fx:id="label1" text="This is FXML!" />
    </top>
    <center>
        <VBox>
            <fx:define>
                <ToggleGroup fx:id="group1" />
            </fx:define>
            <RadioButton text="Male" toggleGroup="$group1" userData="남자" selected="true" />
            <RadioButton text="Female" toggleGroup="$group1" userData="여자"/>
        </VBox>
    </center>
    <bottom>
    </bottom>
</BorderPane>

먼저 FXML에서 라디오 버튼을 제공한다. 위와 같은 형태로 ToggleGroup을 포함하는 2개의 라디오 버튼이 있다. 여기에서는 "userData"라는 속성이 포함되어 있다. 이것은 라디오 버튼에 자신의 데이터를 갖게 위한 것으로, 나중에 사용하기 때문에 작성해 두자. 그 외에는 특히 아무것도 특별한 것은 하지 않는 일반 라디오 버튼이다.


ChangeListener을ToggleGroup에 설정

그럼, 작성한 FXML를 읽어 들여, ToggleGroup 이벤트 처리를 포함한 컨트롤러 클래스를 만들어 보자.

아래에 간단한 예제 코드를 준비해 두었다.

package com.devkuma.javafx;
 
import java.net.URL;
import java.util.ResourceBundle;
 
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;
 
public class AppController implements Initializable {
    @FXML Label label1;
    @FXML ToggleGroup group1;
     
    @Override
    public void initialize(URL location, ResourceBundle resources) {
 
        group1.selectedToggleProperty().addListener((ObservableValue<? extends Toggle> 
            observ, Toggle oldVal, Toggle newVal)->{
            String oldStr = (String)oldVal.getUserData();
            String newStr = (String)newVal.getUserData();
            label1.setText(oldStr + "->" + newStr);
        });
    }
 
}

실행하여 라디오 버튼을 클릭하여 보자. 그러면 윈도우 상단의 라벨에 "남자 -> 여자"라는 텍스트가 표시된다. 선택 전과 선택 후에 각각의 RadioButton에 설정된 userData을 표시하고 있는 것을 알 수 있다.

여기에서는 ToggleGroup의 selectedToggleProperty에 설정된 ReadOnlyObjectProperty에 다음과 같은 형태로 리스너를 설정하고 있다.

ReadOnlyObjectProperty.addListener((ObservableValue<? extends Toggle> observ,
    Toggle oldVal, Toggle newVal) -> {
    // 수행할 작업
});

여기에서는 람다 식을 사용하여 설정 방법을 적어 보겠다. ChangeListener에는 changed라는 메소드 하나만 정의되어 있다. 이것은 다음과 같은 형태를 하고 있다.

void changed(ObservableValue<? extends T> observable, T oldValue, T newValue)

ObservableValue는 제네릭 형을 설정 할 수 있다. ToggleGroup의 selectedtoggleProperty에 포함 된 경우 "Toggle"라는 클래스를 상속하는 형태로 ObservableValue가 제공된다. 그 외에 oldValue, newValue도 Toggle 인스턴스로 전달된다.

이 Toggle이라는 클래스 (정확하게는 인스턴스이지만)은 ON/OFF로 다루어지는 값을 관리하기 위한 클래스이다. 이 Toggle에서 변경된 값에 대한 정보를 검색한다. 여기에서는 "getUserData"라는 메소드를 사용하고 있다. 이것으로 부터 그 Toggle로 설정되어 있는 UserData 속성의 값을 꺼낸다. 이는 FXML에 추가되어 있는 "userData" 속성의 값이다.

ReadOnlyObjectProperty, ObservableValue, Toggle와 낯선 클래스가 몇개 등장해서 이해하기 어려울지도 모르지만, 기본적인 속성 변경 이벤트 리스너 처리는 모두 유사한 형태로 되어 있기 때문에 이러한 이벤트 처리에 익숙해지면 어떤 것도 유사한 방식으로 처리할 수 있다.



ComboBox의 SelectionModel 이벤트 처리

계속해서 ComboBox에 대해 설명하겠다. 이것은 사실은 ListView 알면 거의 같은 방식으로 처리 할 수 있다.

ComboBox에도 선택 상태를 관리하는 모델 클래스가 설정된 속성이 있는데 이는 "getSelectionModel"으로 선택 모델 클래스를 얻을 수 있다.

그리고 그 selectedItemProperty에서 얻을 수 있는 속성에 addListener으로 이벤트 리스너를 설정하여 선택 변경시의 처리를 설정 할 수 있다.

그럼 이것도 살펴 보도록 하자. 이전에 FXML에 있던 <center> 태그 안에서 다음과 같이 작성한다.

<ComboBox fx:id="combo1">
<items>
    <FXCollections fx:factory="observableArrayList">
        <String fx:value="One" />
        <String fx:value="Two" />
        <String fx:value="Three" />
    </FXCollections>
</items>
</ComboBox>

이것으로 String 값을 항목으로 설정한 ComboBox가 준비되었다. 다음에는 이 이벤트 처리를 컨트롤러 클래스에서 제공하는 뿐이다.

아래와 같이 소스 코드를 작성해 보자.

package com.devkuma.javafx;
 
import java.net.URL;
import java.util.ResourceBundle;
 
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
 
 
public class AppController implements Initializable {
    @FXML Label label1;
    @FXML ComboBox<String> combo1;
     
    @Override
    public void initialize(URL location, ResourceBundle resources) {
 
        combo1.getSelectionModel().selectedItemProperty().
            addListener((ObservableValue<? extends String> observ,
                    String oldVal, String newVal)->{
            label1.setText(oldVal + "->" + newVal);
        });
    }
}

여기에서는 ComboBox에 제네릭 형으로 String를 지정하고 있다. 아래와 같은 형식이다.

@FXML ComboBox<String> combo1;

이것으로 String 값을 항목으로 가진 설정이 된다. 이벤트 리스너는 다음과 같이 설정한다.

combo1.getSelectionModel().selectedItemProperty().addListener ......

getSelectionModel에서 얻어진 선택 모델 selectedItemProperty에 addListener을 설정한다. 조금 까다롭다. 이벤트 리스너 설정은 다음과 같은 형태로 정의된다.

addListener((ObservableValue<? extends String> observ, String oldVal, String newVal) -> {
    // 이벤트 처리
});

첫번째 인수 ObservableValue<? extends String> observ이라는 형태로 정의되어 있다. ComboBox의 정의도 <String>가 지정되어 있기 때문에, ObservableValue도 extends String 클래스로 정의된다. 다음에는 변경 전과 변경 후의 값이 각각 String으로 전달된다.

이 "ObservableValue는 거기에 저장된 값을 extends하는 형태로 작성된다"는 것은 정말 이상한 형태일 것이다. 어쨌든, 실제로 프로그램을 작성한 ObservableValue 슈퍼 클래스가 어떤 결정 때문이다. 조금 이해하기 어렵겠지만, "선택 모델에 설정된 클래스를 상속해서 ObservableValue은 준비된다"는 것을 명심하자.



슬라이더의 valueProperty를 이벤트 처리

슬라이더도 값이 변경되었을 때의 이벤트를 유사한 방식으로 처리 할 수 있다. Slider 클래스는 설정된 값은 "valueProperty"라는 속성으로 처리된다.

이것은 "DoubleProperty"라는 클래스의 인스턴스로 속성을 관리하는 기능이 포함되어 있다. 이 DoubleProperty에 addListener에서 이벤트 리스너를 설정하여 값이 변경되었을 때 처리를 수행 할 수 있다.

그럼 이것도 간단한 샘플을 만들어 사용해 보자. 이전에 FXML에서 <center> 태그를 다음과 같이 작성하자.

<center>
    <Slider fx:id="slider1" min="0" max="100"/>
</center>

이것으로 슬라이더가 하나 표시되도록 되었다. 여기에 이벤트 처리를 설정하여 조작하면 실시간으로 표시가 업데이트 되도록 하자.

아래와 같이 간단한 예제를 작성하자.

package com.devkuma.javafx;
 
import java.net.URL;
import java.util.ResourceBundle;
 
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
 
public class AppController implements Initializable {
    @FXML Label label1;
    @FXML Slider slider1;
     
    @Override
    public void initialize(URL location, ResourceBundle resources) {
 
        slider1.valueProperty().addListener((ObservableValue<? extends Number> 
                observ, Number oldVal, Number newVal)->{
            double oldnum = oldVal.doubleValue();
            double newnum = newVal.doubleValue();
            label1.setText(oldnum + "->" + newnum);
        });
    }
}

이것은 값을 변경 전과 변경 후의 값을 얻어와 표시하는 샘플이다. 슬라이더를 조작하면 "12.34567 -> 98.7654"와 같은 상태로, 이전 값과 새로운 값이 표시된다.

여기에서는 슬라이더에 이벤트 리스너 설정을 다음과 같은 형태로 작성하고 있다.

slider1.valueProperty().addListener ......

Slider 클래스의 valueProperty에서 얻은 DoubleProperty 인스턴스의 addListener에 이벤트 리스너를 설정한다. 이것은 다음과 같은 형태로 되어 있다.

addListener((ObservableValue <? extends Number> observ, Number oldVal, Number newVal) -> {
    // 이벤트 처리
});

ObservableValue는 Number의 서브 클래스로 정의되어 있다. 그리고 전달되는 값은 Number 인스턴스가 있다. 그 다음은 전달된 Number에서 메소드를 호출하여 필요한 값 등을 취득 할뿐이다.

double oldnum = oldVal.doubleValue();
double newnum = newVal.doubleValue();

여기에서는 doubleValue에서 double 값으로 얻어 표시하고 있다. 정수 만들려고 한다면 intValue 메소드를 사용하면 된다.

이벤트 리스너와 ObservableValue의 관계가 이것으로 많이 알게 되었다고 생각한다. ObservableValue를 잘 다룰 수 있게 된다면, 속성의 이벤트 처리는 대체로 마스터하였다고 생각해도 될 것이다.

+ Recent posts