ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Java] JavaFX - 이벤트 처리, 속성 감시, 바인딩
    CSE/Java 2015. 12. 21. 11:56

    JavaFX는 여러 절로 구성되어 있습니다.





    Intro

    JavaFX 레이아웃(Layout)

    JavaFX 컨테이너(Container)

    JavaFX 이벤트 처리 & 속성 감시, 바인딩

    JavaFX 컨트롤(Control)

    JavaFX 메뉴바와 툴바 & 다이얼로그

    JavaFX 스레드 동시성



    JavaFX 이벤트 처리

     UI 어플리케이션은 사용자와 상호작용을 하면서 코드를 실행합니다. 사용자가 UI 컨트롤을 사용하면 이벤트(event)가 발생하고 프로그램은 이벤트를 처리하기 위해 코드를 실행합니다.



     이벤트 핸들러(EventHandler)

      JavaFX는 이벤트 발생 컨트롤과 이벤트 핸들러를 분리하는 위임형(Delegation) 방식을 사용합니다.


      위임형 방식이란 컨트롤에서 이벤트가 발생하면, 컨트롤이 직접 처리하지 않고 이벤트 핸들러에게 이벤트 처리를 위임하는 방식입니다. 예를 들어 사용자가 Button을 클릭하면 ActionEvent가 발생하고, Button에 등록된 EventHandler가 ActionEvent를 처리합니다.


      EventHandler는 컨트롤에서 이벤트가 발생하면, 자신의 handle() 메소드를 실행시킵니다. handle() 메소드에는 윈도우 닫기, 컨트롤 내용 변경, 다이얼로그 띄우기 등의 다양한 코드를 작성할 수 있습니다. 


      EventHandler는 제네릭 타입이기 때문에 타입 파라미터는 발생된 이벤트의 타입이 됩니다. 


      EventHandler가 컨트롤에서 발생된 이벤트를 처리하려면 먼저 컨트롤에 EventHandler를 등록해야 합니다. 컨트롤은 발생되는 이벤트에 따라서 EventHandler를 등록하는 다양한 메소드가 있는데, 이 메소드들은 setOnXXX() 이름을 가지고 있습니다. ActionEvent를 처리하는 EventHandler<ActionEvent>를 등록하려면 다음과 같이 SetOnAction() 메소드를 사용합니다.





    1
    2
    3
    4
    5
    6
    Button button = new Button();
    button.setOnAction(new EventHandler<ActionEvent>() {
        @Override
        public void handle(ActionEvent event) { ... }
    }
     
    cs






     

      EventHandler는 하나의 메소드를 가진 함수적 인터페이스이므로 람다식을 이용하면 보다 적은 코드로 EventHandler를 등록할 수 있습니다.



    1
    2
    3
    4
    button.setOnAction( event -> { ... } );
    tableView.setOnMouseClicked( event -> { ... } );
    stage.setOnCloseRequest( event -> { ... } );
     
    cs





      다음은 프로그램적 레이아웃을 작성하고, 버튼의 ActionEvent를 처리한 것입니다. 첫 번째 버튼은 직접 EventHandler 객체를 생성한 후 등록했고, 두 번째 버튼은 람다식을 이용해서 등록했습니다.




     * ButtonActionEventExam.java


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
     
    package javaFX;
     
    import javafx.application.Application;
    import javafx.event.ActionEvent;
    import javafx.event.EventHandler;
    import javafx.scene.Scene;
    import javafx.scene.control.Button;
    import javafx.scene.layout.HBox;
    import javafx.stage.Stage;
     
    public class ButtonActionEventExam extends Application {
        @Override
        public void start(Stage primaryStage) throws Exception {
            HBox root = new HBox();
            root.setPrefSize(20050);
            root.setSpacing(20);
            
            Button btn1 = new Button("Button1");
            btn1.setOnAction(new EventHandler<ActionEvent>() {
                @Override
                public void handle(ActionEvent event) {
                    System.out.println("Button1 Clicked");
                }
            });
            
            Button btn2 = new Button("Button2");
            btn2.setOnAction( event -> System.out.println("Button2 Clicked"));
            
            root.getChildren().addAll(btn1, btn2);
            Scene scene = new Scene(root);
            
            primaryStage.setTitle("ButtonActionEventExam");
            primaryStage.setScene(scene);
            primaryStage.show();
        }
        
        public static void main(String[] args) {
            launch(args);
        }
    }
     
    cs





     







     FXML 컨트롤러

      프로그램적 레이아웃은 레이아웃 코드와 이벤트 처리 코드를 모두 자바 코드로 작성해야 하므로 코드가 복잡해지며, 유지보수하기가 힘들어집니다. FXML 레이아웃은 FXML 파일당 별도의 컨트롤러(controller)를 지정해서 이벤트를 처리할 수 있기 때문에 FXML 레이아웃과 이벤트 처리 코드를 완전히 분리할 수 있습니다.




      fx:controller 속성과 컨트롤러 클래스

       FXML 파일의 루트 태그에서 fx:controller 속성으로 컨트롤러를 지정하면 UI 컨트롤에서 발생하는 이벤트를 컨트롤러가 처리합니다.



    1
    2
    3
    4
    <RootContainer xmlns:fx="http://javafx.com/fxml"
        fx:controller="packageName.ControllerName">
        ...
    </RootContainer>
    cs




       컨트롤러는 다음과 같이 Initializable 인터페이스를 구현한 클래스로 작성하면 됩니다.



    1
    2
    3
    4
    5
    public class ControllerName implements Initializable {
        @Override
        public void initialize(URL location, ResourceBundle resources) { ... }
    }
     
    cs





       initialize() 메소드는 컨트롤러 객체가 생성되고 나서 호출되는데, 주로 UI 컨트롤의 초기화, 이벤트 핸들러 등록, 속성 감시 등의 코드가 작성됩니다.





      fx:id 속성과 @FXML 컨트롤 주입

       컨트롤러는 이벤트 핸들러를 등록하기 위해서, 그리고 이벤트 처리 시 UI를 변경하기 위해서 FXML 파일에 포함된 컨테이너 및 컨트롤의 참조가 필요합니다. 이를 위해서 FXML 파일에 포함된 컨트롤들은 fx:id 속성을 가질 필요가 있습니다.



     * root.fxml


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
     
    <?xml version="1.0" encoding="UTF-8"?>
     
    <?import javafx.scene.layout.HBox?>
    <?import javafx.scene.control.Button?>
     
    <HBox xmlns:fx="http://javafx.com/fxml/1"
        fx:controller="JavaFX.javaFX.RootController"
        prefHeight="50" prefWidth="200"
        alignment="CENTER" spacing="20">
        <children>
            <Button fx:id="btn1" text="button1" />
            <Button fx:id="btn2" text="button2" />
            <Button fx:id="btn3" text="button3" />
        </children>
        
    </HBox>
     
     
    cs




     



       fx:id 속성을 가진 컨트롤들은 컨트롤러의 @FXML 어노테이션이 적용된 필드에 자동 주입됩니다. 주의할 점은 fx:id 속성값과 필드명은 동일해야 합니다.



    1
    2
    3
    4
    5
    6
    7
    public class ControllerName implements Initializable {
        @FXML private Button btn1;
        @FXML private Button btn2;
        @FXML private Button btn3;
        @Override
        public void initialize(URL location, ResourceBundle resources) {...}
    }
    cs





      FXMLLoader가 FXML 파일을 로딩할 때, 태그로 선언된 컨트롤 객체가 생성되고, 아울러 컨트롤러 객체도 함께 생성됩니다. 그리고 나서 @FXML 어노테이션이 적용된 필드에 컨트롤 객체가 자동 주입됩니다. 주입이 완료되면 비로소 initialize() 메소드가 호출되기 때문에 initialize() 내부에서 필드를 안전하게 사용할 수 있습니다.









     EventHandler 등록

      컨트롤에서 발생하는 이벤틀르 처리하려면 컨트롤러의 initialize() 메소드에서 EventHandler를 생성하고 등록해야 합니다. 다음은 세 개의 Button에서 발생하는 ActionEvent를 처리하는 방법을 보여줍니다.



     * RootController.java


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    package javaFX;
     
    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<ActionEvent>() {
     
                @Override
                public void handle(ActionEvent arg0) {
                    handleBtn1Action(arg0);
                }
            });
     
            btn2.setOnAction(event -> handleBtn2Action(event));
            btn3.setOnAction(event -> handleBtn3Action(event));
        }
     
        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 클릭");
        }
     
    }
    cs




     * AppMain.java


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    package javaFX;
     
    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);
        }
    }
     
    cs








      이벤트 처리 메소드 매핑

       컨트롤러에서 EventHandler를 생성하지 않고도 바로 이벤트 처리 메소드와 연결할 수 있는 방법이 있습니다. Button 컨트롤을 작성할 때 다음과 같이 onAction 속성값으로 "#메소드명"을 주면 내부적으로 EventHandler 객체가 생성되기 때문에 컨트롤러에서는 해당 메소드만 작성하면 됩니다.


       [FXML 파일]


    1
    <Button fx:id="btn" text="button" onAction="#handleBtnAction"/>
    cs




       [Controller 클래스]


    1
    public void handleBtnAction(ActionEvent event) { ... }
    cs









    JavaFX 속성 감시와 바인딩

     JavaFX는 컨트롤의 속성(Property)을 감시하는 리스너를 설정할 수 있습니다. 예를 들어 Slider의 value 속성값을 감시하는 리스너를 설정해서, value 속성값이 변경되면 리스너가 다른 컨트롤러의 폰트나 이미지의 크기를 변경할 수 있습니다.




     속성 감시

      JavaFX 컨트롤 속성은 세 가지 메소드로 구성됩니다. Getter와 Setter 그리고 Property 객체를 리턴하는 메소드입니다. 예를 들어 text 속성은 getText(), setText(String str), textProperty()를 가지고 있습니다.



    1
    2
    3
    4
    5
    private StringProperty text = new SimpleStringProperty();
    public void setText(String str) { text.set(str); }
    public String getText() { return text.get(); }
    public StringProperty textProperty() { return text; }
     
    cs



      StringProperty는 get()과 set() 메소드 이외에 리스너를 관리하는 메소드를 가지고 있습니다. 따라서 text 속성을 감시하는 리스너는 textProperty()가 리턴하는 StringProperty에서 설정합니다. 다음은 text 속성값을 감시하는 ChangeListener를 설정하는 코드입니다.



    1
    2
    3
    4
    5
    6
    7
    8
    textProperty().addListener(new ChangeListener<String>() {
        @Override
        public void changed(ObservableValue<extends String> observable,
            String oldValue, String newValue) {
     
        }
    });
     
    cs






      addListener() 메소드가 ChangeListener를 Property 객체에 설정하면, text 속성이 변경되었을 때 ChangeListener의 changed() 메소드가 자동으로 실행됩니다. 속성의 이전 값은 oldValue에, 새로운 값은 newValue 파라미터로 전달됩니다. ChangeListener는 제네릭 타입인데, 타입Property<String>을 구현하고 있기 때문에 타입 파라미터는 String이 됩니다. 따라서 oldValue와 newValue의 타입은 String이 됩니다. 다른 예를 보면, Slider의 value 속성에 리스너를 설정하려면 다음과 같이 작성하면 됩니다.



    1
    2
    3
    4
    5
    6
    7
    8
    Slider slider = new Slider();
    slider.valueProperty().addListener(new ChangeListener<Number>() {
        @Override
        public void changed(ObservableValue<extends Number> observable,
            Number oldValue, Number newValue) {
     
        }
    });
    cs




      다음 예제는 Slider의 value 속성을 감시해서 value 속성값이 변경되면 Label의 폰트 크기를 변경하도록 리스너를 설정했습니다.




     * root.fxml


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <?xml version="1.0" encoding="UTF-8"?>
     
    <?import javafx.scene.layout.BorderPane?>
    <?import javafx.scene.control.Label?>
    <?import javafx.scene.text.Font?>
    <?import javafx.scene.control.Slider?>
     
    <BorderPane xmlns:fx="http://javafx.com/fxml/1"
        fx:controller="javaFX.RootController"
        prefHeight="250" prefWidth="350">
        <center>
            <Label fx:id="label" text="JavaFX">
                <font>
                    <Font size="0" />
                </font>
            </Label>
        </center>
        <bottom>
            <Slider fx:id="slider" />
        </bottom>
        
    </BorderPane>
     
     
    cs





      * RootController.java


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    package javaFX;
     
    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<Number>() {
                @Override
                public void changed(ObservableValue<extends Number> observable, Number oldValue, Number newValue) {
                    label.setFont(new Font(newValue.doubleValue()));
                }
            });
        }
     
    }
    cs


     

     








     속성 바인딩
      JavaFX 속성은 다른 속성과 바인딩될 수 있습니다. 바인딩된 속성들은 하나가 변경되면 자동적으로 다른 하나도 변경됩니다. 예를 들어, 두 개의 TextArea 컨트롤이 있고 text 속성들을 바인딩하면 사용자가 한쪽의 TextArea에 내용을 입력했을 때 다른쪽 TextArea도 동일한 내용으로 자동 입력됩니다.

      속성 바인딩을 하기 위해서는 xxxProperty() 메소드가 리턴하는 Property 구현 객체의 bind() 메소드를 이용하면 됩니다. 예를 들어, textArea1에서 입력된 내용이 textArea2에 자동으로 입력되도록 하려면 다음과 같이 작성하면 됩니다.



    1
    2
    3
    4
    TextArea textArea1 = new TextArea();
    TextArea textArea2 = new TextArea();
     
    textArea2.textProperty().bind(textArea1.textProperty());
    cs



      bind() 메소드는 단방향인데, textArea1에서 입력된 내용만 textArea2로 자동 입력되고, 반대로 textArea2에 입력된 내용은 textArea1로 자동 입력되지 않습니다. 아예 textArea2는 입력조차 할 수 없습니다. 만약 양방향으로 바인딩하고 싶다면 bind() 메소드 대신 bindBidirectional() 메소드를 이용하거나 Bindings.bindBidirectional() 메소드를 이용하면 됩니다.


    1
    2
    3
    textArea2.textProperty().bindBidirectional(textArea1.textProperty());
    Bindings.bindBidirectional(textArea1.textProperty(), textArea2.textProperty());
     
    cs



      바인딩된 속성을 언바인드하려면 다음 메소드를 이용하면 됩니다.


    1
    2
    3
    textArea2.textProperty().unbind();
    textArea2.textProperty().unbindBidirectional(textArea1.textProperty());
    Bindings.unbindBidirectional(textArea1.textProperty(), textArea2.textProperty());
    cs



      다음 예제는 text 속성으로 두 개의 TextAr 컨트롤을 양뱡향으로 바인딩하였습니다.



     * root.fxml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <?xml version="1.0" encoding="UTF-8"?>
     
    <?import javafx.scene.layout.VBox?>
    <?import javafx.geometry.Insets?>
    <?import javafx.scene.control.Label?>
    <?import javafx.scene.control.TextArea?>
     
    <VBox xmlns:fx="http://javafx.com/fxml/1"
        fx:controller="javaFX.RootController"
        prefHeight="200" prefWidth="300" spacing="10">
        <padding>
            <Insets bottom="10" left="10" right="10" top="10" />
        </padding>
        <children>
            <Label text="textArea1" />
            <TextArea fx:id="textArea1" />
            <Label text="textArea2" />
            <TextArea fx:id="textArea2" />
        </children>
     
    </VBox>
     
     
    cs




     * RootController.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    package javaFX;
     
    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 loc, ResourceBundle resources) {
            Bindings.bindBidirectional(textArea1.textProperty(), textArea2.textProperty());
        }
        
    }
    cs



     








     Bindings 클래스
      두 속성이 항상 동일한 값과 타입을 가질 수는 없습니다. 한쪽 속성값이 다른쪽 송성값과 동일해지기 위해서는 연산 작업이 필요할 수도 있습니다. 

      예를 들어 윈도우의 크기에 상관없이 항상 화면 정중앙에 원을 그린다고 가정해봅시다. 루트 컨테이너 폭의 1/2이 원의 X 좌표가 되고, 루트 컨테이너 높이의 1/2이 원의 Y 좌표가 될 것입니다. 따라서 루트 컨테이너의 폭과 높이를 원의 중심과 바인딩하기 위해서는 1/2이라는 연산이 필요합니다. 

      이때 사용할 수 있는 것이 Bindings 클래스가 제공하는 정적 메소드들입니다. Bindings의 정적 메소드는 속성을 연산하거나, 다른 타입으로 변환한 후 바인딩하는 기능을 제공합니다. 다음은 Bindings 클래스가 제공하는 정적 메소드들을 설명한 표입니다.










      다음은 윈도우 창의 크기가 변경되더라도 항상 화면 정중앙에 원을 그리는 예제입니다. 루트 컨테이너의 폭과 높이를 원의 중심과 바인딩하기 위해 1/2 연산을 해야 하므로 Bindings.divide() 메소드를 이용했습니다.



     * root.fxml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <?xml version="1.0" encoding="UTF-8"?>
     
    <?import javafx.scene.layout.AnchorPane?>
    <?import javafx.scene.shape.Circle?>
     
    <AnchorPane xmlns:fx="http://javafx.com/fxml/1"
        fx:controller="javaFX.RootController"
        fx:id="root"
        prefHeight="200" prefWidth="200">
        <children>
            <Circle fx:id="circle" fill="DODGERBLUE" radius="50.0" stroke="RED" />
        </children>    
    </AnchorPane>
     
     
    cs



     * RootController.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
     
    package javaFX;
     
    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 loc, ResourceBundle resources) {
            circle.centerXProperty().bind(Bindings.divide(root.widthProperty(), 2));
            circle.centerYProperty().bind(Bindings.divide(root.heightProperty(), 2));
        }
        
    }
    cs





     

     



     * 이 포스트은 서적 '이것이 자바다' 를 참고하여 작성한 포스트입니다.


    'CSE > Java' 카테고리의 다른 글

    [Java] JavaFX - 스레드 동시성  (0) 2016.01.13
    [Java] JavaFX - 메뉴바, 툴바, 다이얼로그  (0) 2016.01.03
    [Java] JavaFX - 컨트롤  (0) 2015.12.29
    [Java] JavaFX - 컨테이너(Container)  (0) 2015.12.14
    [Java] JavaFX - 레이아웃(Layout)  (0) 2015.12.14
    [Java] JavaFX - Intro  (0) 2015.12.14

    댓글

Designed by Tistory.