open 키워드가 추가된 이유는 JavaFX 애플리케이션이 실행될 때 클래스 리플렉션을 수행하기 때
문에 프로젝트 내부의 모든 클래스에 대해 리플렉션을 허용하기 위해서이다. java.se 집합 모듈은
JDK가 제공하는 모든 모듈을 사용하기 위해 추가하였다.
메인 클래스 작성
JavaFX 애플리케이션을 개발하려면 제일 먼저 메인 클래스를 작성해야 한다. 메인 클래스는 JavaFX
애플리케이션을 시작하는 관문이다. 메인 클래스는 추상 클래스인 Application을 상속받고, start ( )
메소드를 재정의해야 한다.
그리고 main ( ) 메소드에서는 Application의 launch ( ) 메소드를 호출해야 한다. launch ( ) 메
소드는 메인 클래스의 객체를 생성하고, 메인 윈도우를 생성한 다음 start ( ) 메소드를 호출하는 역할
을 한다.
AppMain.java
packagesec02.exam01_application_start;importjavafx.application.Application;importjavafx.stage.Stage;publicclassAppMainextendsApplication{@Overridepublicvoidstart(StageprimaryStage)throwsException{primaryStage.show();//윈도우 보여주기}publicstaticvoidmain(String[]args){launch(args);//AppMain 객체 생성 및 메인 윈도우 생성}}
실행 결과
JavaFX는 윈도우를 무대(Stage )로 표현한다. launch ( )는 start ( ) 메소드를 호출할 때 메인 윈도
<!-- 자(cid:2343) 컨트롤 추가 -->
<!-- TextField 선언 -->
<!-- TextField의 폭 설정 -->
<!-- Button 컨트롤 선언 -->
<!-- Button 글자 설정 -->
루트 컨테이너인 HBox는 <HBox> 태그로 작성되고, fx 접두사에 대한 네임스페이스 선언
(xmlns:fx="http://javafx.com/fxml")이 추가되어 있는 것을 볼 수 있다. 추후 이것은 FXML
파일에 <fx:XXX> 형태의 태그 및 fx:xxx="값" 형태의 속성을 작성할 수 있다는 뜻이다.
HBox 컨테이너에 들어갈 TextField와 Button은 <children> 태그의 내용으로 각각 <TextField>
와 <Button> 태그로 작성된 것을 볼 수 있다. <padding>은 여백을 말하는데, 곧 이어서 학습한다.
다음은 자바 로직 부분을 작성한 메인 클래스이다.
>>> AppMain.java
```java
package sec03.exam02_fxml_layout;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class AppMain extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
Parent root = FXMLLoader.load(getClass().getResource("root.fxml"));
Scene scene = new Scene(root);
primaryStage.setTitle("AppMain");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
메인 클래스는 start ( ) 메소드에서 FXML 레이아웃 파일을 읽고 선언된 내용을 객체화해야 한다.
이것을 FXML 로딩이라고 한다. 이때 FXMLLoader의 load ( ) 메소드가 사용된다. load ( ) 메소
레이아웃을 작성할 때 컨트롤들을 쉽게 배치할 수 있도록 도와주는 클래스가 컨테이너이다. javafx.
scene.layout 패키지에는 다양한 컨테이너 클래스들이 존재한다. 접미사가 Pane으로 끝나는 클
래스는 모두 컨테이너이고, 그 이외에 Hbox, Vbox가 있다.
설명
컨트롤을 좌표를 이용해서 배치하는 레이아웃
위, 아래, 오른쪽, 왼쪽, 중앙에 컨트롤을 배치하는 레이아웃
행으로 배치하되 공간이 부족하면 새로운 행에 배치하는 레이아웃
그리드로 배치하되 셀의 크기가 고정적이지 않은 레이아웃
컨트롤을 겹쳐 배치하는 레이아웃
그리드로 배치하되 고정된 셀의 크기를 갖는 레이아웃
수평으로 배치하는 레이아웃
수직으로 배치하는 레이아웃
컨테이너
AnchorPane
BorderPane
FlowPane
GridPane
StackPane
TilePane
HBox
VBox
AnchorPane 컨테이너
AnchorPane 컨테이너는 좌표를 이용하여 AnchorPane의 좌상단(0, 0 )을 기준으로 컨트롤을 배
치한다. 컨트롤 좌표는 좌상단(layoutX, layoutY ) 값을 말하는데, (0, 0 )으로부터 떨어진 거리를
의미한다.
(0, 0)
30. (layoutY)
(layoutX)
(50, 30)
AnchorPane
컨트롤
AnchorPane에서 사용할 수 있는 주요 설정은 다음과 같다.
태그 및 속성
prefWidth
prefHeight
layoutX
layoutY
설명
폭을 설정
높이를 설정
컨트롤의 X 좌표
컨트롤의 Y 좌표
컨트롤을 포함
적용
AnchorPane
AnchorPane
컨트롤
컨트롤
AnchorPane
AnchorPane 컨테이너는 JavaFX Scene Builder를 사용해서 디자인하는 것이 좋다. 눈으로 거리
를 확인해서 컨트롤을 드롭시킬 수 있기 때문이다. 프로그램적 레이아웃 방법은 직접 코드상에 좌표
값을 입력해야 하기 때문에 매우 불편하다.
다음 예제는 AnchorPane루트 컨테이너를 사용해서 로그인 레이아웃을 디자인한 것이다.
>>> root.fxml
```java
```
AnchorPane에 포함될 컨트롤은 태그의 내용으로 작성된다. AnchorPane을 사용해
서 컨트롤을 좌표로 배치하면 윈도우 창이 줄거나 늘어날 경우 컨트롤의 재배치가 일어나지 않는다.
따라서 AnchorPane으로 배치할 경우에는 윈도우 창의 크기를 변경할 수 없도록 primaryStage.
setResizable (false );를 추가하는 것이 좋다.
>>> AppMain.java
```java
package sec04.exam01_anchorpane;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class AppMain extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
Parent root = FXMLLoader.load(getClass().getResource("root.fxml"));
Scene scene = new Scene(root);
primaryStage.setTitle("AppMain");
primaryStage.setScene(scene);
primaryStage.show();
primaryStage.setResizable(false);
}
public static void main(String[] args) {
launch(args);
}
}
```
HBox와 VBox 컨테이너
HBox와 VBox는 수평과 수직으로 컨트롤을 배치하는 컨테이너이다. HBox와 VBox는 자식 컨
트롤의 크기를 재조정하는데, HBox는 컨트롤의 높이를 확장하고, 컨트롤의 폭은 유지한다. VBox
는 컨트롤의 폭을 확장하고 컨트롤의 높이는 유지한다.
단, 크기 조정이 가능한 컨트롤만 자동 확장되는데, 기본 Button의 경우는 크기 조정이 되지 않는다.
그 이유는 maxWidth와 maxHeight가 -1.0을 가지기 때문이다. 크기 조정이 가능하도록 하려면
다음과 같이 maxWidth와 maxHeight을 변경하면 된다.
```java
```
HBox에서 컨트롤의 높이를 확장하고 싶지 않다면 fillHeight 속성을 false로 설정하면 되고,
VBox에서 컨트롤의 폭을 확장하고 싶지 않다면 fillWidth 속성을 false로 설정하면 된다. HBox
와 VBox에서 사용할 수 있는 주요 설정은 다음과 같다.
태그 및 속성
prefWidth
prefHeight
alignment
spacing
fillWidth
fillHeight
```java
```
설명
폭을 설정
높이를 설정
컨트롤의 정렬을 설정
컨트롤의 간격을 설정
적용
HBox, VBox
HBox, VBox
HBox, VBox
HBox, VBox
컨트롤의 폭 확장 여부 설정
컨트롤의 높이 확장 여부 설정
VBox
HBox
컨트롤을 포함
HBox, VBox
HBox의 남은 폭을 채움
컨트롤
VBox의 남은 높이를 채움
컨트롤
다음 예제는 VBox로 ImageView 컨트롤과 HBox 컨테이너를 수직으로 배치하고, HBox 안에는
두 개의 버튼을 수평으로 배치했다. [다음] 버튼은 HBox의 남은 폭을 채우도록 HBox의 hgrow
속성을 설정했다.
>>> root.fxml
```java
```
그림의 비율에 맞게
높이를 설정
현재 경로를 기준으로
상대 경로로 파일 지정
오른쪽 남은 공간을 버튼
이 모두 채우도록 설정
```java
```
버튼의 폭을 자동으로
확장하기 위해 설정
>>> AppMain.java
```java
package sec04.exam02_hbox_vbox;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class AppMain extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
VBox root = (VBox)FXMLLoader.load(
getClass().getResource("root.fxml")
);
Scene scene = new Scene(root);
primaryStage.setTitle("AppMain");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
```
BorderPane 컨테이너
BorderPane은 top, bottom, left, right,
center 셀에 컨트롤을 배치하는 컨테이너이다.
컨트롤만 배치하는 것이 아니라 다른 컨테이너도
배치할 수 있기 때문에 다양한 레이아웃을 만들
어 낼 수 있다. 주의할 점은 각 셀에는 하나의 컨
트롤 또는 컨테이너만 배치할 수 있다.
top
left
center
right
bottom
다음은 BorderPane에서 사용할 수 있는 태그 및 속성들이다.
태그 및 속성
prefWidth
prefHeight
설명
폭을 설정
높이를 설정
top에 배치될 컨트롤을 포함
bottom에 배치될 컨트롤을 포함
right에 배치될 컨트롤을 포함
left에 배치될 컨트롤을 포함
center에 배치될 컨트롤을 포함
적용
BorderPane
BorderPane
BorderPane
BorderPane
BorderPane
BorderPane
BorderPane
BorderPane의 특징은 top, bottom, left, right에 컨트롤을 배치하지 않으면 center에 배치된
컨트롤이 top, bottom, left, right까지 확장된다.
다음은 BorderPane의 top에 ToolBar를 배치하고 center에는 TextArea를, bottom에는 다시
BorderPane을 배치한 것이다.
>>> root.fxml
```java
```
left, right까지 확장
```java
```
top, bottom, left까지 확장
>>> AppMain.java
```java
package sec04.exam03_borderpane;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class AppMain extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
Parent root = FXMLLoader.load(getClass().getResource("root.fxml"));
Scene scene = new Scene(root);
primaryStage.setTitle("AppMain");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
```
FlowPane 컨테이너
FlowPane은 행으로 컨트롤을 배치하되 공간이 부족하면 새로운 행에 배치하는 컨테이너이다.
FlowPane
다음은 FlowPane에서 사용할 수 있는 태그와 속성들이다.
태그 및 속성
prefWidth
prefHeight
hgap
vgap
설명
폭을 설정
높이를 설정
컨트롤의 수평 간격을 설정
컨트롤의 수직 간격을 설정
컨트롤을 포함
적용
FlowPane
FlowPane
FlowPane
FlowPane
FlowPane
다음 예제는 FlowPane 컨테이너에 여섯 개의 Button을 배치한 것인데, 버튼 간의 수평 간격과 수
직 간격을 주기 위해 hgap과 vgap을 10으로 설정하였다.
>>> root.fxml
```java
>>> AppMain.java
package sec04.exam04_flowpane;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class AppMain extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
Parent root = FXMLLoader.load(getClass().getResource("root.fxml"));
Scene scene = new Scene(root);
primaryStage.setTitle("AppMain");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
```
FlowPane을 잘 이해하려면 윈도우 창을 늘렸다가 줄여 보면 되는데, 오른쪽에 배치될 공간이 부족
할 경우에는 새로운 행에 컨트롤이 배치되는 것을 볼 수 있다.
TilePane 컨테이너
TilePane은 그리드로 컨트롤을 배치하되 고정된 셀(타일) 크기를 갖는 컨테이너이다. FlowPane
과 마찬가지로 오른쪽에 컨트롤을 배치할 공간이 부족하면 새로운 행에 컨트롤을 배치한다.
TilePane
다음은 TilePane에서 사용할 수 있는 태그와 속성들이다.
태그 및 속성
prefWidth
prefHeight
prefTileWidth
prefTileHeight
설명
폭을 설정
높이를 설정
타일의 폭을 설정
타일의 높이를 설정
컨트롤을 포함
적용
TilePane
TilePane
TilePane
TilePane
TilePane
다음은 여러 개의 ImageView를 TilePane에 배치한 예제이다. 셀의 크기를 100×100으로 지정
하기 위해 prefTileHeight="100" prefTileWidth="100"으로 지정했다.
>>> root.fxml
```java
>>> AppMain.java
package sec04.exam05_tilepane;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class AppMain extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
Parent root = FXMLLoader.load(getClass().getResource("root.fxml"));
Scene scene = new Scene(root);
primaryStage.setTitle("AppMain");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
```
GridPane 컨테이너
GridPane은 그리드로 컨트롤을 배치하되 셀의 크기가 고정적이지 않고 유동적인 컨테이너이다. 셀
병합이 가능하기 때문에 다양한 입력폼 화면을 만들 때 매우 유용하게 사용할 수 있다. 각 컨트롤은
자신이 배치될 행 인덱스와 컬럼 인덱스를 속성으로 가지며, 몇 개의 셀을 병합할 것인지도 지정할
수 있다.
다음은 GridPane에 적용 가능한 속성들이다.
태그 및 속성
prefWidth
prefHeight
hgap
vgap
설명
폭을 설정
높이를 설정
수평 컨트롤 간격을 설정
수직 컨트롤 간격을 설정
컨트롤을 포함
GridPane.rowIndex
컨트롤이 위치하는 행 인덱스를 설정
GridPane.columnIndex
컨트롤이 위치하는 컬럼 인덱스를 설정
GridPane.rowSpan
행 병합 수를 설정
GridPane.columnSpan
컬럼 병합 수를 설정
GridPane.hgrow
GridPane.vgrow
수평 빈 공간 채우기를 설정
수직 빈 공간 채우기를 설정
GridPane.halignment
컨트롤의 수평 정렬을 설정
GridPane.valignment
컨트롤의 수직 정렬을 설정
다음은 로그인 화면을 GridPane으로 배치한 것이다.
적용
GridPane
GridPane
GridPane
GridPane
GridPane
컨트롤
컨트롤
컨트롤
컨트롤
컨트롤
컨트롤
컨트롤
컨트롤
>>> root.fxml
```java
```
```java
>>> AppMain.java
package sec04.exam06_gridpane;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class AppMain extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
Parent root = FXMLLoader.load(getClass().getResource("root.fxml"));
Scene scene = new Scene(root);
primaryStage.setTitle("AppMain");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
```
StackPane 컨테이너
StackPane은 컨트롤을 겹쳐 배치하는 컨테이너이다. 흔히 카드 레이아웃이라고 하는데, 그림과 같
이 카드가 겹쳐 있는 것처럼 컨트롤도 겹쳐질 수 있다. 만약 위에 있는 컨트롤이 투명이라면 밑에 있
는 컨트롤이 겹쳐 보이게 된다.
StackPane
컨트롤 또는 컨테이너
다음은 두 개의 ImageView를 StackPane에 겹치도록 배치한 예제이다. 하단 이미지는 설경이고
상단 이미지는 투명한 배경을 가지고 있는 듀크이다. 실행해보면 듀크와 설경이 하나의 이미지처럼
보이지만, 상하로 겹쳐 있다.
>>> root.fxml
```java
```
가로비와 세로비 상관없이
고정 길이로 설정
가로비와 세로비를 유지
>>> AppMain.java
```java
package sec04.exam07_stackpane;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class AppMain extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
Parent root = FXMLLoader.load(getClass().getResource("root.fxml"));
Scene scene = new Scene(root);
primaryStage.setTitle("AppMain");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
```
## 05. JavaFX 이벤트 처리
UI 애플리케이션은 사용자와 상호작용하면서 코드를 실행한다. 사용자가 UI 컨트롤을 사용하면 이
벤트event가 발생하고, 프로그램은 이벤트를 처리하기 위해 코드를 실행한다.
이벤트 핸들러
JavaFX는 이벤트 발생 컨트롤과 이벤트 처리를 분리하기 위해 위임형Delegation 방식을 사용한다. 위
임형 방식이란 컨트롤에서 이벤트가 발생하면 컨트롤이 직접 처리하지 않고 이벤트 핸들러에게 이벤
트 처리를 위임하는 방식이다.
예를 들어 사용자가 Button을 클릭하면 ActionEvent가 발생하고, Button에 등록된 EventHandler
가 ActionEvent를 처리한다.
이벤트 발생 컨트롤
(Button)
①
②
ActionEvent
이벤트 발생
④ 이벤트 처리 효과
1. 윈도우 닫기
2. 컨트롤 내용 변경
3. 다이얼로그 띄우기
이벤트 핸들러
(EventHandler)
이벤트 처리 메소드
실행
(cid:81)(cid:86)(cid:67)(cid:77)(cid:74)(cid:68)(cid:1)(cid:87)(cid:80)(cid:74)(cid:69)(cid:1)(cid:73)(cid:66)(cid:79)(cid:69)(cid:77)(cid:70)(cid:9)(cid:106)(cid:10)(cid:1)(cid:92)(cid:1)
③ 이벤트 처리
(cid:94)
EventHandler는 컨트롤에서 이벤트가 발생하면 자신의 handle ( ) 메소드를 실행시킨다. handle ( )
메소드에는 윈도우 닫기, 컨트롤 내용 변경, 다이얼로그 띄우기 등의 코드를 작성할 수 있다.
EventHandler는 제네릭 타입이기 때문에 타입 파라미터는 발생된 이벤트의 타입이 된다. 예를 들
어 ActionEvent를 처리하는 핸들러는 EventHandler가 되고, MouseEvent를
처리하는 핸들러는 EventHandler가 된다.
EventHandler가 컨트롤에서 발생된 이벤트를 처리하려면 먼저 컨트롤에 EventHandler를 등록
해야 한다. 컨트롤은 발생되는 이벤트에 따라서 EventHandler를 등록하는 다양한 메소드가 있는
데, 이 메소드들은 setOnXXX ( ) 이름을 가지고 있다. XXX는 보통 이벤트 이름과 동일하다.
몇 가지 예를 들어보자. Button을 클릭할 때 발생하는 ActionEvent를 처리하는 EventHandler
를 등록하려면 다음과 같이 setOnAction ( ) 메소드를 사용한다.
Button button = new Button();
button.setOnAction(new EventHandler() {
```java
@Override
public void handle(ActionEvent event) { … }
});
```
TableView의 행을 클릭할 때 발생하는 MouseEvent를 처리하는 EventHandler
를 등록하려면 다음과 같이 setOnMouseClicked ( ) 메소드를 사용한다.
TableView tableView = new TableView();
tableView.setOnMouseClicked(new EventHandler() {
```java
@Override
public void handle(MouseEvent event) { … }
});
```
윈도우Stage의 우측 상단 닫기(×) 버튼을 클릭했을 때 발생하는 WindowEvent를 처리하는
EventHandler를 등록하려면 다음과 같이 setOnCloseRequest ( ) 메소드를
사용한다.
stage.setOnCloseRequest(new EventHandler() {
```java
@Override
public void handle(WindowEvent event) { … }
});
```
EventHandler는 하나의 메소드를 가진 함수적 인터페이스이므로 람다식을 이용하면 보다 적은 코
드로 EventHandler를 등록할 수 있다.
button.setOnAction( event->{ … } );
tableView.setOnMouseClicked( event->{ … } );
stage.setOnCloseRequest( event->{ … } );
다음은 프로그램적 레이아웃을 작성하고 버튼의 ActionEvent를 처리한 것이다. 첫 번째 버튼은 직
접 EventHandler 객체를 생성한 후 등록했고, 두 번째 버튼은 람다식을 이용해서 EventHandler
를 등록했다.
>>> AppMain.java
```java
package sec05.exam01_event_handler;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class AppMain extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
HBox root = new HBox();
root.setPrefSize(200, 50);
root.setAlignment(Pos.CENTER);
root.setSpacing(20);
```
Button btn1 = new Button("버튼1");
btn1.setOnAction(new EventHandler() {
```java
@Override
public void handle(ActionEvent event) {
System.out.println("버튼1 클릭");
}
});
```
Button btn2 = new Button("버튼2");
btn2.setOnAction(event -> System.out.println("버튼2 클릭"));
root.getChildren().addAll(btn1, btn2);
Scene scene = new Scene(root);
primaryStage.setTitle("AppMain");
primaryStage.setScene(scene);
primaryStage.setOnCloseRequest(event -> System.out.println("종료 클릭"));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
FXML 컨트롤러
프로그램적 레이아웃은 레이아웃 코드와 이벤트 처리 코드를 모두 자바 코드로 작성해야 하므로 코
드가 복잡해지고 유지보수도 힘들어지며, 디자이너와 협력해서 개발하는 것도 쉽지 않다. FXML 레
이아웃은 FXML 파일당 별도의 컨트롤러Controller를 지정해서 이벤트를 처리할 수 있기 때문에 FXML
레이아웃과 이벤트 처리 코드를 완전히 분리할 수 있다.
1) fx:controller 속성과 컨트롤러 클래스
FXML 파일의 루트 태그에서 fx:controller 속성으로 컨트롤러를 지정하면 UI 컨트롤에서 발생하
는 이벤트를 컨트롤러가 처리한다.
(cid:29)(cid:1814)트컨테이너 xmlns:fx = "http://javafx.com/fxml"
fx:controller = "package...RootController" >
…
(cid:29)/(cid:1814)트컨테이너(cid:31)
컨트롤러는 다음과 같이 Initializable 인터페이스를 구현한 클래스로 작성하면 된다.
```java
public class RootController implements Initializable {
@Override
public void initialize(URL location, ResourceBundle resources) { }
}
```
initialize ( ) 메소드는 컨트롤러 객체가 생성되고 나서 호출되는데, 주로 UI 컨트롤의 초기화, 이벤
트 핸들러 등록, 속성 감시 등의 코드가 작성된다.
2) fx:id 속성과 @FXML 컨트롤 주입
컨트롤러는 이벤트 핸들러를 등록하기 위해, 그리고 이벤트 처리 시 FXML 파일에 포함된 컨테이너
및 컨트롤의 참조가 필요하다. 이를 위해서 FXML 파일에 포함된 컨트롤들은 fx:id 속성을 가질 필
요가 있다.
>>> root.fxml
```java
```
fx:id 속성을 가진 컨트롤들은 컨트롤러의 @FXML 어노테이션이 적용된 필드에 자동 주입된다. 주
의할 점은 fx:id 속성값과 필드명은 동일해야 한다.
```java
public class RootController implements Initializable {
@FXML private Button btn1;
@FXML private Button btn2;
@FXML private Button btn3;
@Override
public void initialize(URL location, ResourceBundle resources) { }
}
```
FXMLLoader가 FXML 파일을 로딩하면, 태그로 선언된 컨트롤들과 컨트롤러는 함께 객체로 생성
된다. 그리고 나서 컨트롤러의 @FXML 어노테이션이 적용된 필드에 컨트롤 객체가 자동 주입된다.
주입이 완료되면 비로소 initialize ( ) 메소드가 호출되기 때문에 initialize ( ) 내부에서 필드를 안전
하게 사용할 수 있다.
3) EventHandler 등록
컨트롤에서 발생하는 이벤트를 처리하려면 컨트롤러의 initialize ( ) 메소드에서 EventHandler를
생성하고 등록해야 한다. 다음은 세 개의 Button에서 발생하는 ActionEvent를 처리하는 방법을
보여준다.
>>> RootController.java
```java
package sec05.exam02_fxml_controller;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
public class RootController implements Initializable {
@FXML private Button btn1;
@FXML private Button btn2;
@FXML private Button btn3;
@Override
public void initialize(URL location, ResourceBundle resources) {
btn1.setOnAction(new EventHandler() {
@Override
public void handle(ActionEvent event) {
handleBtn1Action(event);
```
직접 EventHandler
생성 후 등록
}
});
btn2.setOnAction(event->handleBtn2Action(event));
btn3.setOnAction(event->handleBtn3Action(event));
람다식 이용
}
```java
public void handleBtn1Action(ActionEvent event) {
System.out.println("버튼1 클릭");
}
public void handleBtn2Action(ActionEvent event) {
System.out.println("버튼2 클릭");
}
public void handleBtn3Action(ActionEvent event) {
System.out.println("버튼3 클릭");
}
}
```
4) 이벤트 처리 메소드 매핑
컨트롤러에서 EventHandler를 생성하지 않고도 바로 이벤트 처리 메소드와 연결할 수 있는 방법
이 있다. Button 컨트롤을 작성할 때 다음과 같이 onAction 속성값으로 "#메소드명"을 주면 내부
적으로 EventHandler 객체가 생성되기 때문에 컨트롤러에서는 해당 메소드만 작성하면 된다.
FXML 파일
```java
```
Controller 클래스
```java
public void handleBtnAction(ActionEvent event) { … }
```
## 06. JavaFX 속성 감시와 바인딩
JavaFX는 컨트롤의 속성property을 감시하는 리스너를 설정할 수 있다. 예를 들어 Slider의 value 속
성값을 감시하는 리스너를 설정해서 value 속성값이 변경되면 리스너가 다른 컨트롤러의 폰트나 이
미지의 크기를 변경할 수 있다.
속성 감시
컨트롤의 모든 속성은 XXXProperty 객체로 생성된다. 그리고 Getter와 Setter 그리고
XXXProperty를 리턴하는 메소드로 구성된다. 예를 들어 TextField, TextArea 컨트롤의 text 속
성은 StringProperty 객체로 생성되고 다음과 같은 메소드로 구성된다.
//StringProperty 타입의 text 속성 선언
private StringProperty text = new SimpleStringProperty();
//Geter와 Setter 선언
```java
public void setText(String newValue) { text.set(newValue); }
public String getText() { return text.get(); }
```
//StringProperty 타입의 text 속성을 리턴하는 메소드
public StringProperty textProperty() { return text; }
StringProperty는 get ( )과 set ( ) 메소드 이외에 리스너를 관리하는 textProperty ( ) 메소드를 가
지고 있다. text 속성을 감시하는 리스너는 textProperty ( )가 리턴하는 StringProperty에서 설정
한다. javafx.beans.property 패키지에는 StringProperty 이외에도 다양한 XXXProperty 클래
스가 존재한다.
다음은 TextField 컨트롤의 text 속성값을 감시하는 ChangeListener를 설정하는 코드이다.
StringProperty stringProperty = textField.textProperty();
stringProperty.addListener( new ChangeListener() {
```java
@Override
public void changed(ObservableValue extends String> observable,
String oldValue, String newValue) { … }
} );
```
addListener ( ) 메소드로 ChangeListener를 StringProperty 객체에 설정하면, text 속성이
변경되었을 때 ChangeListener의 changed ( ) 메소드가 자동으로 실행된다. 속성의 이전 값은
oldValue에, 새로운 값은 newValue로 전달된다.
ChangeListener는 제네릭 타입인데, 타입 파라미터는 속성의 타입이 된다. 예를 들어
textProperty ( )가 리턴하는 StringProperty는 Property을 구현하고 있기 때문에 타입
파라미터는 String이 된다. 따라서 oldValue와 newValue의 타입은 String이다.
다른 예를 보자. Slider의 value 속성에 리스너를 설정하려면 다음과 같이 작성하면 된다.
Slider slider = new Slider();
DoubleProperty doubleProperty = slider.valueProperty();
doubleProperty.addListener( new ChangeListener() {
```java
@Override
public void changed(ObservableValue extends Number> observable,
Number oldValue, Number newValue) { … }
} );
```
valueProperty ( )가 리턴하는 DoubleProperty가 Property를 구현하고 있기 때문
에 ChangeListener의 타입 파라미터는 Number가 된다. 따라서 oldValue와 newValue의 타
입도 Number가 된다.
다음 예제는 Slider의 value 속성을 감시해서 value 속성값이 변경되면 Label의 폰트 크기를 변경
하도록 리스너를 설정했다.
>>> root.fxml
```java
>>> RootController.java
```java
package sec06.exam01_property_listener;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.text.Font;
public class RootController implements Initializable {
@FXML private Slider slider;
@FXML private Label label;
@Override
public void initialize(URL location, ResourceBundle resources) {
slider.valueProperty().addListener(new ChangeListener() {
@Override
public void changed(ObservableValue extends Number> observable,
Number oldValue, Number newValue) {
label.setFont( new Font( newValue.doubleValue() ) );
}
```
Label의 폰트 변경
Label의 폰트 변경
});
}
}
value 속성 감시 리스너 등록
>>> AppMain.java
```java
package sec06.exam01_property_listener;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class AppMain extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
Parent root = (Parent)FXMLLoader.load(getClass().getResource
("root.fxml"));
Scene scene = new Scene(root);
primaryStage.setTitle("AppMain");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
```
속성 바인딩
컨트롤의 속성은 다른 컨트롤의 속성과 바인딩될 수 있다. 바인딩된 속성들은 하나가 변경되면 자동
적으로 다른 하나도 변경된다. 예를 들어 두 개의 TextArea 컨트롤의 text 속성들을 바인딩하여 한
쪽 text 속성이 변경되면 다른 쪽 text 속성도 자동 변경된다.
속성을 바인딩하기 위해서는 컨트롤의 xxxProperty ( ) 메소드가 리턴하는 XXXProperty 객체의
bind ( ) 메소드를 이용하면 된다. 예를 들어 textArea1에서 입력된 내용이 textArea2에 자동으로
입력되도록 하려면 다음과 같이 작성하면 된다.
TextArea textArea1 = new TextArea();
TextArea textArea2 = new TextArea();
textArea2.textProperty().bind(textArea1.textProperty());
bind ( ) 메소드는 단방향인데, textArea1에서 입력된 내용만 textArea2로 자동 입력되고 반대로
textArea2에서 입력된 내용은 textArea1로 자동 입력되지 않는다. 아예 textArea2는 입력조차
할 수 없다. 만약 양방향으로 바인딩하고 싶다면 bind ( ) 메소드 대신 bindBidirectional ( ) 메소
드를 이용하거나 Bindings.bindBidirectional ( ) 메소드를 이용하면 된다.
//양방(cid:3313) 바인딩 방법1
textArea2.textProperty().bindBidirectional(textArea1.textProperty());
//양방(cid:3313) 바인딩 방법2
Bindings.bindBidirectional(textArea1.textProperty(), textArea2.textProperty());
바인딩된 속성을 언바인드하려면 다음 메소드를 이용한다.
//단방(cid:3313) 해제
textArea2.textProperty().unbind();
//양방(cid:3313) 해제 방법1
textArea2.textProperty().unbindBidirectional(textArea1.textProperty());
//양방(cid:3313) 해제 방법2
Bindings.unbindBidirectional(textArea1.textProperty(), textArea2.textProperty());
다음 예제는 text 속성으로 두 개의 TextArea 컨트롤을 양방향으로 바인딩하였다.
>>> root.fxml
```java
>>> RootController.java
package sec06.exam02_property_bind;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.beans.binding.Bindings;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.TextArea;
public class RootController implements Initializable {
@FXML private TextArea textArea1;
@FXML private TextArea textArea2;
@Override
public void initialize(URL location, ResourceBundle resources) {
Bindings.bindBidirectional(textArea1.textProperty(),
textArea2.textProperty());
}
}
>>> AppMain.java
package sec06.exam02_property_bind;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class AppMain extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
Parent root = (Parent)FXMLLoader.load(getClass().getResource("root.fxml"));
Scene scene = new Scene(root);
primaryStage.setTitle("AppMain");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
```
Bindings 클래스
두 속성이 항상 동일한 값과 타입을 가질 수는 없다. 한쪽 속성값이 다른 쪽 속성값과 바인딩하기 위
해서는 연산 작업이 필요할 수도 있다.
예를 들어 윈도우의 크기에 상관없이 항상 화면 정중앙에 원을 그린다고 가정해 보자. 루트 컨테이
너 폭의 1/2이 원의 X좌표가 되고, 루트 컨테이너 높이의 1/2이 원의 Y좌표가 될 것이다. 따라서
루트 컨테이너의 폭과 높이를 원의 중심과 바인딩하기 위해서는 1/2이라는 연산이 필요하다.
이때 사용할 수 있는 것이 Bindings 클래스가 제공하는 정적 메소드들이다. Bindings의 정적 메소
드는 속성을 연산하거나, 다른 타입으로 변환한 후 바인딩하는 기능을 제공한다. 다음은 Bindings
클래스가 제공하는 정적 메소드들을 설명한 표이다.
메소드
설명
add, substract, multiply, divide 속성값을 덧셈, 뺄셈, 곱셈, 나눗셈 연산을 수행하고 바인딩함
max, min
속성값과 어떤 수를 비교해서 최대, 최소값을 얻고 바인딩함
greaterThan,
greaterThanOrEqual
속성값이 어떤 값보다 크거나, 같거나 큰지를 조사해서 true/false로 변환하여
바인딩함
lessThan, lessThanOrEqual
속성값이 어떤 값보다 적거나, 같거나 적은지를 조사해서 true/false로 변환하
여 바인딩함
equal, notEquals
속성값이 어떤 값과 같은지, 다른지를 조사해서 true/false로 변환하여 바인딩함
equalIgnoreCase,
notEqualIgnoreCase
대소문자와 상관없이 속성값이 어떤 문자열과 같은지, 다른지를 조사해서 true/
false로 변환하여 바인딩함
isEmpty, isNotEmpty
속성값이 비어있는지, 아닌지를 조사해서 true/false로 변환하여 바인딩함
isNull, isNotNull
속성값이 null 또는 not null인지를 조사해서 true/false로 변환하여 바인딩함
length
size
and, or
not
convert
valueAt
속성값이 문자열일 경우 문자 수를 얻어 바인딩함
속성 타입이 배열, List, Map, Set일 경우 요소 수를 얻어 바인딩함
속성값이 boolean일 경우, 논리곱, 논리합을 얻어 바인딩함
속성값이 boolean일 경우, 반대값으로 바인딩함
속성값을 문자열로 변환해서 바인딩함
속성이 List, Map일 경우 해당 인덱스 또는 키의 값을 얻어 바인딩함
다음은 윈도우 창의 크기가 변경되더라도 항상 화면 정중앙에 원을 그리는 예제이다. 루트 컨테이너
의 폭과 높이를 원의 중심과 바인딩하기 위해 1/2 연산을 해야 하므로 Bindings.divide ( ) 메소드
를 이용하였다.
>>> root.fxml
```java
>>> RootController.java
package sec06.exam03_bindings;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.beans.binding.Bindings;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.layout.AnchorPane;
import javafx.scene.shape.Circle;
public class RootController implements Initializable {
@FXML private AnchorPane root;
@FXML private Circle circle;
@Override
public void initialize(URL location, ResourceBundle resources) {
circle.centerXProperty().bind( Bindings.divide(root.widthProperty(), 2) );
circle.centerYProperty().bind( Bindings.divide(root.heightProperty(), 2) );
}
}
```
윈도우 창의 width, height 속성에 1/2 연산후 Circle의 centerX, centerY 속성과 바인딩
>>> AppMain.java
```java
package sec06.exam03_bindings;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class AppMain extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
Parent root = (Parent)FXMLLoader.load(getClass().getResource("root.fxml"));
Scene scene = new Scene(root);
primaryStage.setTitle("AppMain");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
```
## 07. JavaFX 컨트롤
JavaFX는 다양한 UI 컨트롤을 제공하고 있다. 이번 절에서는 사용 빈도가 높은 버튼 컨트롤, 입력
컨트롤, 뷰 컨트롤, 미디어 컨트롤, 차트 컨트롤에 대해 살펴보자.
버튼 컨트롤
버튼 컨트롤은 마우스로 클릭할 수 있는 컨트롤로 ButtonBase를 상속하는 하위 컨트롤을 말한다.
Button, CheckBox, RadioButton, ToggleButton, Hyperlink 등이 있다.
Button
CheckBox
RadioButton
ToggleButton
기본 Button은 단순한 글자로 구성되지만 setGraphic (ImageView ) 메소드로 아이콘을 넣을 수
도 있다. 다음은 아이콘 버튼을 FXML로 작성하는 방법을 보여준다.
```java
<Button text = "아이콘버튼"(cid:31)
</Button>
```
CheckBox, RadioButton, ToggleButton 컨트롤은 선택과 미선택 두 가지 상태를 가질 수 있다.
selected 속성의 값이 true이면 선택이고, false이면 미선택이다. 다음은 CheckBox 컨트롤을
FXML로 선언한 것이다. text 속성은 사용자에게 보여주는 문자열이고, userData 속성은 프로그
램에서 처리하는 데이터이다.
RadioButton, ToggleButton에는 toggleGroup 속성이 있는데, 이 속성의 값은 $groupName
이다. $groupName은 의 fx:id를 참조한다. 같은
groupName을 참조하는 버튼들은 하나의 그룹으로 묶이며, 같은 그룹 내에서는 하나의 버튼만 선
택된다.
<RadioButton … toggleGroup = "$group" />
<RadioButton … toggleGroup = "$group" />
<RadioButton … toggleGroup = "$group" />
CheckBox, RadioButton, ToggleButton 컨트롤은 사용자가 클릭하면 ActionEvent가 발생하
기 때문에 EventHandler로 처리가 가능하고, onAction 속성을 작성해서 컨트롤러의 이벤트 처리
메소드로 연결할 수도 있다.
<CheckBox … onAction = "#handleChkAction"/>
만약 같은 그룹 내에서 RadioButton 또는 ToggleButton의 선택 변경을 감시하고 싶다면
ToggleGroup의 selectedToggle 속성에 다음과 같이 감시자를 등록하면 된다.
groupName.selectedToggleProperty().addListener(new ChangeListener() {
```java
@Override
public void changed(ObservableValue extends Toggle> observable,
Toggle oldValue, Toggle newValue) { … }
});
```
선택이 변경되면 changed ( ) 메소드가 실행되고 세 번째 매개값인 newValue에 마지막으로 선택
된 버튼이 대입된다. 다음 예제는 CheckBox와 RadioButton의 이벤트 처리를 어떻게 하는지 보
여준다.
>>> root.fxml
```java
>>> RootController.java
package sec07.exam01_button;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
public class RootController implements Initializable {
@FXML private CheckBox chk1;
@FXML private CheckBox chk2;
@FXML private ImageView checkImageView;
@FXML private ToggleGroup group;
@FXML private ImageView radioImageView;
@FXML private Button btnExit;
@Override
public void initialize(URL location, ResourceBundle resources) {
group.selectedToggleProperty().addListener(new
ChangeListener() {
@Override
public void changed(ObservableValue extends Toggle> observable,
Toggle oldValue, Toggle newValue) {
Image image = new Image(getClass().getResource(
"images/" + newValue.getUserData().toString() + ".png").
toString());
radioImageView.setImage(image);
}
});
}
public void handleChkAction(ActionEvent e) {
if(chk1.isSelected() && chk2.isSelected()) {
checkImageView.setImage(new Image(getClass().getResource(
"images/geek-glasses-hair.gif").toString()));
} else if(chk1.isSelected()) {
checkImageView.setImage(new Image(getClass().getResource(
"images/geek-glasses.gif").toString()));
} else if(chk2.isSelected()) {
checkImageView.setImage(new Image(getClass().getResource(
"images/geek-hair.gif").toString()));
} else {
checkImageView.setImage(new Image(getClass().getResource(
"images/geek.gif").toString()));
}
}
public void handleBtnExitAction(ActionEvent e) {
Platform.exit();
}
}
>>> AppMain.java
package sec07.exam01_button;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class AppMain extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
Parent root = (Parent)FXMLLoader.load(getClass().getResource("root.
fxml"));
Scene scene = new Scene(root);
primaryStage.setTitle("AppMain");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
```
입력 컨트롤
입력 컨트롤에는 한 줄 입력을 위한 TextField, 다중 행 입력을 위한 TextArea, 패스워드 입력을
위한 PasswordField, 제한된 항목에서 선택하는 ComboBox가 있다. 또한 날짜를 선택할 수 있
는 DatePicker, 색상을 선택할 수 있는 ColorPicker, HTML을 입력하기 위한 HTMLEditor도
입력 컨트롤이라고 볼 수 있다. Label은 입력 컨트롤은 아니지만 입력 컨트롤의 제목을 표시할 때
사용된다.
Label & TextField
PasswordField
TextArea
ComboBox
DatePicker
ColorPicker
HTMLEditor
다음은 입력 컨트롤을 FXML로 선언하는 방법을 보여준다.