About

11 мар. 2016 г.

Custom Control JavaFx: part 1


Пример создания собственного контролла в JavaFx. Например такой кнопки:

Часть 1: основные классы и обработчик событий

Основные классы

За функционирование контролла ответственны два класса - сам класс контролла с обработчиками событий и класс скина - наследники Control и BaseSkin соответственно:
Они соотносятся один-к-одному.
public class StarButtonControl extends Control {
    @Override
    protected Skin<?> createDefaultSkin() {
        return new StarButtonSkin(this);
    }
}
В скин передается класс контролл к которому он относится:
public class StarButtonSkin extends SkinBase<StarButtonControl> {
    public StarButtonSkin(StarButtonControl control) {
        super(control);
    }
}
Эти классы и определяют контролл.

Добаление внешнего вида

1. В скине добавлю метод рисующий звезду через набот точек в Path. А так же перегрузку некоторых методов. computePrefHeight, computePrefWidth - размеры, layoutChildren - перерисовка самого элемента.
public class StarButtonSkin extends SkinBase<StarButtonControl> {
   private Shape star;

   // ...........

   public void updateStar(double w, double h) {
        if (star != null) {
            getChildren().remove(star);
        }

        double x = w / 2.0;
        double y = 0;

        Color color = Color.ORANGE;
        
        Path starPath = new Path();
        starPath.setFill(color);
        starPath.setFillRule(FillRule.NON_ZERO);
        starPath.getElements().add(new MoveTo(x, y));
        starPath.getElements().add(new LineTo((5.0 / 6.0) * w, w));
        starPath.getElements().addAll(new LineTo(0, w / 3),
                new LineTo(w, w / 3),
                new LineTo(w / 6.0, w),
                new LineTo(w / 2, 0),
                new LineTo(x, y));

        star = Path.intersect(starPath, starPath);
        star.setFill(color);
        
        getChildren().add(star);
    }

    @Override
    protected void layoutChildren(double contentX, double contentY, 
double contentWidth, double contentHeight) {
        updateStar(contentWidth, contentHeight);
        layoutInArea(star, contentX, contentY, contentWidth, contentHeight, -1, 
HPos.CENTER, VPos.CENTER);
    }

    @Override
    protected double computePrefHeight(double width, double topInset,
 double rightInset, double bottomInset, double leftInset) {
        return 150 + topInset + bottomInset;
    }

    @Override
    protected double computePrefWidth(double height, double topInset, 
double rightInset, double bottomInset, double leftInset) {
        return 150 + topInset + bottomInset;
    }
}

Рисует фигуру:

2. Возможность сменить цвет. Пусть например будет менять цвет при нажатии.
2.1. Сначала добавить поле проперти хранящее цвет в контроле:
public class StarButtonControl extends Control {

private final ObjectProperty<Color> backgroundFill; 
public StarButtonControl() {
        backgroundFill = new SimpleObjectProperty<>();
    }

    public ObjectProperty<Color> backgroundFillProperty() {
        return backgroundFill;
    }

    public Color getBackgroundFill() {
        return backgroundFill.get();
    }

    public void setBackgroundFill(Color color) {
        backgroundFill.set(color);
    }

   @Override
    protected Skin<?> createDefaultSkin() {
        return new StarButtonSkin(this);
    }

}

2.2.  В скине: добавить листенер на изменение добавленного выше поля backgrondFill. В листенере вызывается setFill() с новым цветом для существующей фигуры.
А метод getSkinnable() из родительского SkinBase - возвращает сам контрол к которому привязан скин (сейчас это StarButtonControl). Оттуда и берется для заливки измененный цвет.
public class StarButtonSkin extends SkinBase {

   private Shape star;

   private boolean invalidStar = true;

   public StarButtonSkin(StarButtonControl control) {
     super(control);
     control.backgroundFillProperty().addListener(observable -> updateStarColor());
   }

   private void updateStarColor() {
        if (star != null) {
            star.setFill(getSkinnable().getBackgroundFill());
            getSkinnable().requestLayout();
        }
    }

   // updateStarMethod....

   @Override
   protected void layoutChildren(double contentX, double contentY, double contentWidth,
 double contentHeight) {
       if (invalidStar) {
         updateStar(contentWidth, contentHeight);
         invalidStar = false;
       }
       layoutInArea(star, contentX, contentY, contentWidth, contentHeight, -1, 
HPos.CENTER, VPos.CENTER);
   }

   // compute pref width/height methods...
}
Так же добавлен флаг invalidStar для предотвращения лишних перерисовок.

2.3. Проверка что получилось. Добавить сам контрол и экшен на нажатие в контейнер:
public class FXMLController implements Initializable {

    @FXML
    private AnchorPane container;
    
    @FXML
    private Label messageLabel;
    

    @Override
    public void initialize(URL url, ResourceBundle rb) {
        StarButtonControl sb = new StarButtonControl();

        sb.setOnMouseClicked((MouseEvent event) -> {
            sb.setBackgroundFill(Color.BROWN);
            messageLabel.setText("Clicked");
        });
        container.getChildren().add(sb);
    }
}

В результате нажатия кнопка меняет цвет:

Теперь нужно доделать обработку нажатий - что бы только клик по самой фигуре срабатывал, а не по области в которую она вписана. Для этого нужно добавить поле хранящее EventHandler и вызывать ивент только по клику на самой фигуре, Shape .

Обработчик событий

1. В контроле добавить поле-проперти для хранения EventHandler:
public class StarButtonControl extends Control {
  
    // ....

    private final ObjectProperty<EventHandler<ActionEvent>> onAction = 
new ObjectPropertyBase<EventHandler<ActionEvent>>() {

        @Override
        protected void invalidated() {
            setEventHandler(ActionEvent.ACTION, get());
        }

        @Override
        public Object getBean() {
            return StarButtonControl.this;
        }

        @Override
        public String getName() {
            return "onAction";
        }
    };

    public ObjectProperty<EventHandler<ActionEvent>> onActionProperty() {
        return onAction;
    }

    public void setOnAction(EventHandler<ActionEvent> value) {
        onAction.set(value);
    }

    public EventHandler<ActionEvent> getOnAction() {
        return onAction.get();
    }
}

2. В скине - вызов экшена по событию mouseClick:
public class StarButtonSkin extends SkinBase {

    private Shape star;

    //....

    public void updateStar(double w, double h) {

        // ...

        star.setOnMouseClicked(e -> getSkinnable().fireEvent(new ActionEvent()));
        getChildren().add(star);
    }
}

 3. Проверка. При создании - добавить новый обработчик событий - onAction:
public class FXMLController implements Initializable {

    //........

    @Override
    public void initialize(URL url, ResourceBundle rb) {
        StarButtonControl sb = new StarButtonControl();

        sb.setOnAction(e -> {
           // .....
        });
        container.getChildren().add(sb);
    }
}
В результате клик работает только на самом контролле, а не вокруг него:


Книга Mastering JavaFx Controls
svn