Одним из важных преиму­ществ библиотеки JavaFX является исполь­зо­вание стилей, прине­сенное из мира Web. Т.е. вместо того, чтобы быть зависимым от библиотек Look&Feel, пользо­ватель сам определяет внешний вид, причем это действи­тельно гибко и красиво, а также может изменяться динами­чески, «на лету», содержать анимацию и 3D графику. Используя концепцию стилей можно делать прило­жения с так называ­емыми «скинами», когда внешний вид полностью отделен от бизнес-логики, и даже разра­ба­ты­вается отдельно, например, профес­си­о­нальными дизайнерами.
Создадим простой пример диалога с кнопкой:

public class JavaFXDialog1 extends Application {
    @Override
    public void start(Stage stage) {
        final VBox vbox = new VBox();
        final Button button = new Button("test");
        vbox.getChildren().addAll(button);
        final Scene scene = new Scene(vbox, 150, 100);
        stage.setScene(scene);
        stage.show();
    }
  
    public static void main(String[] args) {
        launch(args);
    }

avafx and css styles 1
Применять стили можно несколькими способами:
1) Непосред­ственно в коде, например, изменить цвет фонта кнопки:

button.setStyle("-fx-text-fill: red");

avafx and css styles 2

2) Посред­ством CSS-файла, на который должен быть настроен класс Scene:
Для этого нужно создать файл с раширением .css и положить его в папку проекта, например — /css/styles.css.
Содер­жимое файла:

.button {
    -fx-text-fill: blue;
}

Да, тут главное не забыть настроить среду разра­ботки, чтобы она копировала эти файлы в сборку, иначе можно долго разби­раться, почему стиль не подключается
Например, в IntelliJ IDEA это делается так:
avafx and css styles 3

Теперь все готово, чтобы подключить к сцене стиль:

scene.getStylesheets().add((getClass().getResource("/css/styles.css")).toExternalForm());

Далее запустим проект и получим такой диалог:
avafx and css styles 4

Инструкция .button в CSS-файле говорит, что теперь все кнопки будут с голубым цветом:

final Button button1 = new Button("button1");
final Button button2 = new Button("button2");
vbox.getChildren().addAll(button1, button2);

avafx and css styles 5

А что если это не то, что нам надо? Что если нужно определить конкретную кнопку?
3) На помощь приходит опреде­ление пользо­ва­тель­ского стиля кнопки:
В styles.css пишем:

.button1 {
    -fx-text-fill: green;
}

А в коде:

button1.getStyleClass().add("button1");

Получаемый диалог выглядит так:
avafx and css styles 6

Теперь все кнопки, присо­еди­ненные к этому классу стиля, будут с зеленым текстом, причем метод add() говорит, что подобных стилей можно добавлять несколько, тем самым расширяя, переопре­деляя или перекрывая разные свойства элемента.
4) Определить пользо­ва­тельский стиль можно также через, так называемый, ID:
В styles.css пишем:

#button2 {
    -fx-text-fill: yellow;
}

А в коде:

button2.setId("button2");

Получаемый диалог:

avafx and css styles 7

Т.е. все элементы с одина­ковым ID будут выглядеть одинаково.
Что же еще интересного можно сделать с помощью стилей?

Стили могут обраба­тывать так называемые триггеры поведения, привне­сенные из мира XAML.
На примере кнопки к ним относятся такие события интер­фейса пользо­вателя, как получение фокуса, выделение, нажатие, пронос курсора мыши над кнопкой, и пр. – все то, что так хочется вынести из кода диалога, чтобы разгрузить его, оставив лишь бизнес-логику. Тем более, если клиент хочет не то, и не так, то это не отразиться на коде диалога – все будет определять динами­чески подклю­чаемый CSS-файл.
Например с помощью следующей CSS-инструкции можно изменить цвет кнопки, когда пользо­ватель проносит над ней курсор мыши:

.button:hover {
    -fx-background-color: orange;
}

Получаемый диалог при наведении курсора мыши:
avafx and css styles 8

Причем, как уже указы­валось ранее, это приведет к одина­ковому поведению всех классов button, а такой код:

.button1:hover {
    -fx-background-color: orange;
}

будет применять триггер только для тех, кто ссылается на класс «button1».

Подобный подход можно применять для многих триггеров, например для кнопок кроме hover есть еще foused, selected, pressed.
Жаль, что подобное нельзя применить непосред­ственно в коде, например:

button.setStyle(":hover -fx-text-fill: red");

Может быть в дальнейшем разра­ботчики JavaFX предо­ставят нам такую возможность.
Для чего мы все это изучаем? Ведь в интернете можно найти массу гораздо более мощных примеров, и цель статьи была не в их копиро­вании. Дело в том, что концепцию стилей и триггеров можно так же расширять для своих нужд, а вот это уже представляет интерес.
Мне нужно было сделать визуальный компонент на JavaFX, который бы служил для выбора размер­ности встав­ляемой в текст таблицы, причем внешний вид компо­нента, его цветовая схема, размеры и пр. не должны были бы быть его частью, а настра­и­вались бы внешними CSS-файлами. Выглядеть он должен примерно так:

avafx and css styles 9

Пользо­ватель проносит курсор мыши над сеткой и видит выделение, в данном случае для таблиц 7х8 ячеек, при клике на компо­ненте это выделение должно переда­ваться в программу, чтобы вставилась таблица 7х8 ячеек.
Конечно, можно отсле­живать выделение в коде и окрашивать ячейки опреде­ленным цветом, но что если одному клиенту нравится один цвет, другому – другой, либо цветовая схема опреде­ляется общим механизмом «скина», или еще как-нибудь – что делать тогда?

Тут нужен простой компонент для выделения ячеек, а внешний вид – не его забота.

Очевидно, напра­ши­вается взять преды­дущий пример триггером «hover» для кнопки, но он сработает для каждой ячейки при проносе над ней курсора мыши, те же ячейки, что теряют курсор мыши, перестают соответ­ствовать триггеру и теряют цвет выделения, как же оставить выделенным весь диапазон?
Для решения задачи нужно сделать свой триггер, сраба­ты­вающий на какое-то свойство объекта, например – «нахожусь в диапазоне выделения», а короче – inRage, которое можно было бы исполь­зовать в CSS-файле подобным образом:

.MyCell:inRange {
    -fx-border-width: 0.5;
    -fx-border-color: #ffffff;
    -fx-background-color: lightskyblue
}

Забегая вперед скажу, что задача доста­точно непростая, да еще и решение для версий JavaFX 1.7 и 1.8 – принци­пи­ально различные, и, например, для 1.7 придется исполь­зовать массу deprecated-методов.
Для начала рассмотрим сам компонент:

public class JavaFXDialog2  extends Application {
    @Override
    public void start(Stage stage) {
        final VBox vbox = new VBox();
        final GridPaneEx table = new GridPaneEx();
        table.init(10, 10);
        final Label label = new Label();
        label.setMaxWidth(Double.MAX_VALUE);
        label.setAlignment(Pos.CENTER);
        label.setTextAlignment(TextAlignment.CENTER);
        label.setStyle("-fx-padding: 3 0 5 0");
        label.textProperty().bind(table.text);
        vbox.getChildren().addAll(label, table);
        final Scene scene = new Scene(vbox, 350, 300);
        scene.getStylesheets().add((getClass().getResource("/css/styles.css")).toExternalForm());
        scene.setFill(null);
        stage.setScene(scene);
        stage.show();
    }
 
 
    public static void main(String[] args) {
        launch(args);
    }
 
 
    private void fireCreateTable(final int cols, final int rows){
        System.out.println("cols = " + cols + ", rows = " + rows);
    }
 
 
    protected class GridPaneEx extends GridPane {
  
        public final StringProperty text = new SimpleStringProperty("cancel");
        private int cols;
        private int rows;
  
        public GridPaneEx(){
            this.setOnMouseExited(new EventHandler() {
                @Override
                public void handle(MouseEvent mouseEvent) {
                    text.setValue("cancel");
                    deselectAll();
                }
            });
        }
  
        public void init(final int cols, final int rows){
            getChildren().clear();
            this.cols = cols;
            this.rows = rows;
            for (int col = 0; col < cols; col++){
                for (int row = 0; row < rows; row++){
                    final Button rect = new Button();
                    rect.setMinSize(30, 10);
                    add(rect, col, row);
                    final int selectedCol = col;
                    final int selectedRow = row;
                    rect.setOnMouseMoved(new EventHandler() {
                        @Override
                        public void handle(MouseEvent mouseEvent) {
                            selectRange(selectedCol, selectedRow);
                            text.setValue((selectedCol + 1) + " x " + (selectedRow + 1));
                        }
                    });
                    rect.setOnAction(new EventHandler() {
                        @Override
                        public void handle(ActionEvent actionEvent) {
                            fireCreateTable(selectedCol + 1, selectedRow + 1);
                            deselectAll();
                        }
                    });
                }
            }
            deselectAll();
        }
  
        private Node getNodeFromGridPane(int col, int row) {
            for (Node node : getChildren()) {
                if (GridPane.getColumnIndex(node) == col && GridPane.getRowIndex(node) == row) {
                    return node;
                }
            }
            return null;
        }
  
        private void selectCell(int col, int row, final boolean select){
            final Node node = getNodeFromGridPane(col, row);
            if (select){
                node.setStyle("-fx-border-width: 0.5; -fx-border-color: #ffffff; -fx-background-color: lightskyblue");
            } else {
                node.setStyle("-fx-border-width: 0.5; -fx-border-color: #000000; -fx-background-color: #ffffff");
            }
        }
  
        public void deselectAll(){
            for (int col = 0; col < cols; col++){
                for (int row = 0; row < rows; row++){
                    selectCell(col, row, false);
                }
            }
        }
        private void selectRange(int selectedCol, int selectedRow){
            deselectAll();
            for (int col = 0; col <= selectedCol; col++){
                for (int row = 0; row <= selectedRow; row++){
                    selectCell(col, row, true);
                }
            }
        }
    }
}

Интерес представляет метод selectCell, в котором окраска ячеек осуществ­ляется непосред­ственно в коде:
для обычных ячеек:

node.setStyle("-fx-border-width: 0.5; -fx-border-color: #000000; -fx-background-color: #ffffff");

для ячеек в выбранном диапазоне:

node.setStyle("-fx-border-width: 0.5; -fx-border-color: #ffffff; -fx-background-color: lightskyblue");

Т.к. в описании задачи указано, что чётко прописать цвета никак нельзя, то попробуем задать их с помощью собственного стиля, пометим в styles.css такой код:

#MyCellNormal {
    -fx-border-width: 0.5;
    -fx-border-color: #000000;
    -fx-background-color: #ffffff;
}
 
 
#MyCellInRange {
    -fx-border-width: 0.5;
    -fx-border-color: #ffffff;
    -fx-background-color: lightskyblue
}

А в метод selectCell:

private void selectCell(int col, int row, final boolean select){
            final Node node = getNodeFromGridPane(col, row);
            if (select){
                                node.setId("MyCellNormal");
            } else {
                                node.setId("MyCellInRange");
            }
        }

Уже лучше, не правда ли? Т.е. если клиента не устра­ивают цвета, то он меняет их прямо в styles.css, компонент же остается неизменным.
Но оказы­вается, что есть еще более элегантное решение – поместим в styles.css два стиля:

.MyCell {
    -fx-border-width: 0.5;
    -fx-border-color: #000000;
    -fx-background-color: #ffffff;
}
 
 
.MyCell:inRange {
    -fx-border-width: 0.5;
    -fx-border-color: #ffffff;
    -fx-background-color: lightskyblue
}

Это значит, что ячейки будут ориен­ти­ро­ваться на стиль «MyCell», а когда у них будет сраба­тывать триггер «inRange», по аналогии «hover» или «pressed» на кнопке, то цвет должен измениться соответ­ствующим образом.
Как «научить» ячейки запускать триггер?
Т.к. в качестве ячеек в нашем компо­ненте исполь­зуется Button , то необходимо переопре­делить его поведение в нашем «наследнике», а точнее переопре­делить поведение так называ­емого «псевдо-класса», для JavaFX 1.7 это делается так:

protected static class RangeButton extends Button {
        public RangeButton(){
            getStyleClass().add("MyCell");
        }
 
 
        private BooleanProperty inRange = new BooleanPropertyBase() {
 
 
            @Override
            protected void invalidated() {
                impl_pseudoClassStateChanged("inRange");
            }
 
 
            @Override
            public Object getBean() {
                return RangeButton.this;
            }
 
 
            @Override
            public String getName() {
                return "inRange";
            }
        };
 
 
        public boolean isInRange() {
            return inRange.get();
        }
 
 
        public void setInRange(boolean value) {
            inRange.set(value);
        }
 
 
        private static final long IN_RANGE_PSEUDOCLASS_STATE = StyleManager.getInstance().getPseudoclassMask("inRange");
 
 
        @Override
        public long impl_getPseudoClassState() {
            long mask = super.impl_getPseudoClassState();
            if (isInRange()) mask |= IN_RANGE_PSEUDOCLASS_STATE;
            return mask;
        }
    }

Как видно, все методы «…PseudoClass…» — deprecated.
Теперь вместо Button используем наш RangeButton и смотрите, как теперь преоб­ра­зится метод selectCell:

private void selectCell(int col, int row, final boolean select){
            final Node node = getNodeFromGridPane(col, row);
            ((RangeButton)node).setInRange(select);
        }

Т.е. изменение состояния поля «InRange» приводит к сраба­ты­ванию триггера из стиля и цвет выделенных в диапазоне ячеек изменится соответ­ствующим образом.
Это именно то, что нам нужно!
Для JavaFX 1.7 это работает, но в JavaFX 1.8 – запрещено, поэтому такой код перестанет компи­ли­ро­ваться, как только будет подключена JVM 1.8.
Что же предлагает нам новая версия? Как и ожидалось deprecated-методы были убраны и упрощена архитектура в целом. Теперь доста­точно сделать так:

protected static class RangeButton extends Button {        protected final PseudoClass pcInRange = PseudoClass.getPseudoClass("inRange");
 
 
        public RangeButton(){
            getStyleClass().add("MyCell");
        }
 
 
        protected final BooleanProperty inRange = new BooleanPropertyBase() {
 
 
            @Override
            protected void invalidated() {
                pseudoClassStateChanged(pcInRange, getValue());
            }
 
 
            @Override
            public Object getBean() {
                return RangeButton.this;
            }
 
 
            @Override
            public String getName() {
                return "inRange";
            }
        };
 
 
        public boolean isInRange() {
            return inRange.get();
        }
 
 
        public void setInRange(boolean value) {
            inRange.set(value);
        }
    }

Все работает, как и прежде.
Можно еще упростить код:

protected static class RangeButton extends Button {
        protected final BooleanProperty inRange;
        public RangeButton(){
            getStyleClass().add("MyCell");
            final PseudoClass pcInRange = PseudoClass.getPseudoClass("inRange");
            inRange = new SimpleBooleanProperty();
            inRange.addListener(new ChangeListener() {
                @Override
                public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
                    pseudoClassStateChanged(pcInRange, newValue);
                }
            });
        }

И изменить метод соответ­ствующим образом:

private void selectCell(int col, int row, final boolean select){
            final Node node = getNodeFromGridPane(col, row);
            ((RangeButton)node).inRange.setValue(select);
        }

Резюме: с помощью описанного способа на JavaFX можно создавать специ­альные пользо­ва­тельские свойства компо­нентов и связывать их со стилями, что очень удобно, если нужно, чтобы бизнес-логика была полностью абстра­ги­ро­ванной от внешнего вида.