JavaFX Canvas 그래픽 그리기

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 배열에 정리 한 것과 정점의 수를 지정한다. OOPolylineOOPolygon 두 종류가 있는데, 전자는 닫혀 있지 않은 도형, 후자는 닫힌 도형 (시작 지점과 종료 지점이 연결되어있는)를 그리는 것이다.

텍스트 그리기

"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을 준비해서 실행해야 한다.




최종 수정 : 2017-09-19