JavaFX

Займемся теперь интерфейсам. Часть из вас уже работало с Java Swing который является своего рода аналогом Windows форм, выглядит ужасно и в целом сильно устарел.

Java предлагает альтернативный способ для разработки интерфейсов, использование фреймворка JavaFX. В отличие от swing приложение тут есть возможность более тонко настраивать вид приложения, а так же изначально архитектура приложения строго отделяет описание интерфейса от логики.

В общем все это очень здорово, поэтому будем с ним работать.

Устанавливаем SceneBuilder

В отличие от Swing где можно править код прямо в среде. Для JavaFX надо установить отдельное приложение, чтобы можно было редактировать интерфейс в режиме WYSIWYG.

Для этого идем сюда https://gluonhq.com/products/scene-builder/#download и скачиваем версию под свою систему.

Хотя есть и разные версии под разные java, желательно выбрать соответствующую вашей. Например, у нас в компьютерном классе всюду java 8, поэтому лучше скачать 8 версию.

Imgur

Восьмая версия еще особенно хороша тем что ей не надо админских прав и можно ставить в компьютерных классах

Imgur

Imgur

Imgur

по завершению установки запустится SceneBuilder

Imgur

но он нам пока не нужен, так что можно его закрыть.

Создаем JavaFX приложение

Делаем новый проект типа JavaFX

Imgur

назову его CustomTypeGUI

Imgur

получаем такую структуру проекта:

Imgur

Принцип организации javafx интерфейса

Кликнем дважды на файлик sample.fxml чтобы открыть его и увидим там что-то такое:

<?import javafx.geometry.Insets?>
<?import javafx.scene.layout.GridPane?>

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<GridPane fx:controller="sample.Controller"
          xmlns:fx="http://javafx.com/fxml" alignment="center" hgap="10" vgap="10">
</GridPane>

fxml – это такой аналог html, только в рамках java, тут даже есть всякие импорты. И есть тэги, определяющие что будет отображаться. Тут к нас пока только один тэг GridPane, которые используется для выравнивания компонент по сетке.

Правда пока выравнивать нечего, так что пока это по сути пустой контейнер.

Особо стоит обратить внимание на атрибут fx:controller в нем указан класс вместе с полным путем, в котором мы будем писать код для интерфейса.

И так, попробуем запустить программу. Увидим что-то такое

Imgur

Откуда же это все взялось? А на самом деле ничего хитрого нет. Открываем файлик Main.java и видим

package sample;

// просто импорты
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

// важное отличие, класс Main должен наследовать Application
public class Main extends Application {
    
    // это метод который вызывается при запуске javafx приложения
    // в нем принято прописывать инициализацию приложения
    @Override
    public void start(Stage primaryStage) throws Exception {
        // этой строчкой просим создать интерфейс из файлика разметки sample.fxml
        Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
        
        // primaryStage -- это по сути главное окно приложения
        // оно уже создано до нас где-то внутри Application
        // мы указываем окну имя Hello World 
        primaryStage.setTitle("Hello World");
        
        // а тут привязываем интерфейс, который мы загрузили
        // и положили в переменную root, к нашему окну
        // плюс указываем размеры окна 300x275
        primaryStage.setScene(new Scene(root, 300, 275));
        
        // ну и показываем окно пользователю
        primaryStage.show();
    }


    public static void main(String[] args) {
        // а вот собственно и точка входа
        // это обыкновенный main, только тут вызывается метод launch из класса Application
        // который инициализирует создание окна JavaFX приложения
        // и заодно вызывает метод start (который выше)
        launch(args);
    }
}

Добавляем кнопку

Вернемся к файлу sample.fxml. Теперь запустим SceneBuidler, выбираем Open Project

Imgur

и открываем наш файлик. Увидим что-то такое

Imgur

Перетянем теперь кнопку на форму. Так как пока у нас пустой GridPane наша форма выглядит как пустота. Но тянуть можно:

Imgur

Сохраним перетянутое Ctrl+S и переключимся обратно в Idea. И глянем что у нас выросло с файлике sample.fxml. Там у нас будет такое:

<GridPane alignment="center" hgap="10" vgap="10" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/8.0.171" fx:controller="sample.Controller">
   <columnConstraints>
      <ColumnConstraints />
   </columnConstraints>
   <rowConstraints>
      <RowConstraints />
   </rowConstraints>
   <children>
      <Button mnemonicParsing="false" text="Button" />
   </children>
</GridPane>

Видим, что тут много чего поменялось. Появились какие-то Constraints (это так называем ограничения на размеры колонок и строк, нам они пока не интересны).

Ну и главное, что тут появился тег children и внутри него наша кнопочка

<Button mnemonicParsing="false" text="Button" />
  • text – это то что написано на кнопке
  • mnemonicParsing – это специальный тег для обработки горячих клавиш, но нам это сейчас неинтересно, так что можно игнорировать

Можно попробовать запустить:

Imgur

ура, есть кнопочка! =)

Добавляем реакцию на нажатие

Но кнопка — это не кнопка если нажимая на нее ничего не происходит. Попробуем добавить реакцию на клик.

Переключимся на файл Controller.java:

package sample;

public class Controller {
}

Помните я упомянул атрибут fx:controller=”sample.Controller” внутри sample.fxml, так вот мы сейчас в том самом классе на который указывал этот атрибут. За счет него происходит связывание интерфейса и логики (контроллера).

Чтобы добавить реакцию на клик достаточно сделать следующие операции.

Сначала надо добавить функцию. Такую, например, пусть она просто выводит сообщение в консоль

public class Controller {
    public void showMessage() {
        System.out.println("pressed");
    }
}

Можно даже попробовать запустить. И потыкать кнопку

Imgur

хехе, очевидно ничего не произойдет. Мы ж пока функцию не привязали =)

А теперь переключимся на SceneBuilder и привяжем кнопку к функции

Imgur

не забудем сохранить, и запустим проект по-новому

Ура, заработало! =)

Кстати можно открыть файлик sample.fxml и глянуть что там произошло:

<children>
  <Button mnemonicParsing="false" onAction="#showMessage" text="Button" />
</children>

видите, появился атрибут onAction="#showMessage" в котором указана функцию через решетку, которую надо вызвать при клике

Читаем значение из текстового поля

Ну тыкать на кнопочки и выполнять всякие функции — это конечно важное умение. Но не менее важно уметь считать значение из поля, например, текстового.

Делается это в некотором роде проще чем привязка кнопки.

Например, хочу я добавить поле для ввода текста, и чтобы по нажатию на кнопку выводилось содержимое этого поля.

Что я сделаю? Сначала создам в классе поле, через которое образуется связь

package sample;

// импорты добавил
import javafx.fxml.FXML;
import javafx.scene.control.TextField; // следим чтобы тут был правильный импорт

public class Controller {
    /** 
     * Вот оно, родимое, под текстовое поле с именем txtInput
     * а использование так называемого декоратора @FXML
     * позволит ссылаться на это поле из fxml файла
     */
    @FXML
    private TextField txtInput;

    public void showMessage() {
        System.out.println("pressed");
    }
}

теперь переключимся на SceneBuilder, перетащим текстовое поле на форму, а затем привяжем его к нашему классу, путем указания свойства fx:id:

если вдруг у вас в этом поле ничего не высветилось, то можете смело вписать туда вручную txtInput.

Сохраним в SceneBuilder и вернемся обратно в Idea. Если глянуть в файлик fxml то увидим

Imgur

теперь вернемся в наш Controller и подправим метод showMessage:

public class Controller {
    @FXML
    private TextField txtInput;

    public void showMessage() {
        // подставил сюда txtInput.getText()
        System.out.println(txtInput.getText());
    }
}

запускаем:

Уииии!=)

Меняем значения текстового поля

О, давайте еще научимся изменять значение в списке, но тут все просто

public class Controller {
    @FXML
    private TextField txtInput;

    public void showMessage() {
        // меняем текст
        txtInput.setText("Меня подменили! (~˘▾˘)~");
    }
}

вжух

Imgur

В принципе вот и вся наука =)

  • добавил поле в контроллер
  • добавил объект в SceneBuilder
  • связал через fx:id, либо связал с методом.

Теперь можно пилить что-нибудь посложнее.

Ремарка

Наверное, не будем рассматривать как делать конкретно приложение под вашу задачку. Все-таки в прошлом году мы уже пилили интерфейсы. И принцип должен быть понятен.

Но, очевидно, полезно рассмотреть, как можно добавлять разные компоненты и заставить их реагировать на ваши данные.

К тому же в JavaFX есть много хитрых особенностей, которые в WindowsForms не встречалось.

Добавление выпадающего списка (ComboBox)

И так, попробуем добавить выпадающий список. Действуем по выше предложенному алгоритму. Сперва добавим поле в контроллер:

// ...
import javafx.scene.control.ComboBox; // добавили import

public class Controller {
    // ...    

    @FXML
    private ComboBox<String> comboBox; // тут важно указать в треугольных скобках что у нас будут список со строками

    public void showMessage() { /* ... */ }
}

теперь добавляем на форму

Imgur

затем подключаем по fx:id

Imgur

не забываем сохраниться в SceneBuilder.

Запускаем и проверяем:

Imgur

Ура! Есть список! Правда пустой пока…

Заполняем список (интерфейс Initializable)

И вот незадача, как же его заполнить? У нас по сути голый класс Controller, без ничего. Давайте попробуем в конструкторе:

public class Controller {
    @FXML
    private TextField txtInput;

    @FXML
    private ComboBox<String> comboBox;

    public void showMessage() {
        txtInput.setText("Меня подменили! (~˘▾˘)~");
    }

    
    // добавил конструктор
    public Controller() {
        // добавляю элемент списка
        comboBox.getItems().add("Осень");
    }
}

запускаем и… бдыщь

Imgur

суть стенаний виртуальной машины заключается в том, что comboBox нулевой. А нулевой он потому что привязка полей помеченных декоратором @FXML происходит уже после вызова конструктора, где-то в терньях фреймворка JavaFX.

Поэтому добавление элемента в конструкторе не прокатит.

Что ж делать? Оказывается все просто! Надо наследоваться от некого интерфейса Initializable и переопределить одну функцию, которую используется как своего конструктор формы. Делается это так

//...

import javafx.fxml.Initializable; // добавили импорт интерфейса 
import java.net.URL; // и еще класс
import java.util.ResourceBundle; // и второй


public class Controller implements Initializable { // наследуем интерфейс
    // ...

    /* это убрали
    public Controller() {
        comboBox.getItems().add("Осень");
    }
    */

    // вот она функция инициализации формы, в нее все писать будем
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        
    }
}

загоним теперь в метод initialize наш вызов добавления элемента

@Override
public void initialize(URL location, ResourceBundle resources) {
    comboBox.getItems().add("Осень");
}

проверяем

Imgur

Ура!!! =)

Добавляем реакцию на переключения списка

Давайте сначала добавим еще несколько элементов

@Override
public void initialize(URL location, ResourceBundle resources) {
    // можно по одному добавлять, а можно сразу несколько добавить
    comboBox.getItems().setAll(
            "Осень",
            "Зима",
            "Весна",
            "Лето"
    );
}

Есть как минимум два способа добавить реакцию на переключения

Через SceneBuilder

1) Сначала:

Imgur

не забываем сохраниться.

2) Затем в IDEA тыкаем правой кнопкой мыши на название метода

Imgur

выбираем первый пункт Show Context Actions, а затем:

Imgur

3) Нас перекинет в контроллер где мы увидим новодобавленный метод

public class Controller implements Initializable { // наследуем интерфейс
    /*...*/

    public void onComboBoxChanged(ActionEvent actionEvent) {
        // появился этот метод
    }
}

в принципе, его можно было и руками добавить >_>

Этот метод будет вызываться каждый раз, когда мы будем переключать элемент списка. Давайте чего-нибудь там будем выводить:

public void onComboBoxChanged(ActionEvent actionEvent) {
    // это вот так страшно хитро получается узнать какое значение выбрано в combobox
    String selectedValue = comboBox.getSelectionModel().getSelectedItem();
    // а это я его просто вывожу в консольку
    System.out.println(selectedValue);
}

чик-чик-чик

Imgur

Через лямбда-функцию

Ну тут просто в методе инициализации привязываете лямбда-функцию к реакции на изменение свойства.

Я на лекции расскажу что тут происходит на самом деле

@Override
public void initialize(URL location, ResourceBundle resources) {
    // ето не трогаем
    comboBox.getItems().setAll(
            "Осень",
            "Зима",
            "Весна",
            "Лето"
    );
    
    // а тут привязываю реакцию на изменение выбранного элемента
    comboBox.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
        // вывожу новое значение в консоль. 
        // Кстати тут видно, что такое подход более гибкий, я ж могу и oldValue вывести если захочу 
        System.out.println(newValue);
    });
}

Работа с CheckBox

Еще может пригодится чекбокс, для вывода всяких булевых значений. Добавим его на форму

Imgur

добавим в Controller:

// ...
import javafx.scene.control.CheckBox; // добавили пакет

public class Controller implements Initializable {
    // ...

    @FXML
    private CheckBox checkBox; // добавили поле

    // ...
}

теперь подключим checkbox к форме

ну теперь мы можем, например, управлять им кликая на кнопку:

// ...
import javafx.scene.control.CheckBox; // добавили пакет

public class Controller implements Initializable {
    // ...

    @FXML
    private CheckBox checkBox; // добавили поле
    
    public void toggleCheckBox() {
        // читаем значение чекбокса (с галочкой или нет)
        boolean previousValue = checkBox.isSelected();
        // и меняем его на противоположное значение
        checkBox.setSelected(!previousValue);
    }

    // ...
}

проверяем:

Imgur

ой, забыли кнопке указать другую функцию

опять проверяем:

Imgur

Вот теперь красота! =)

кстати если хотите текст поменять то правьте поле Text:

Сохраняем состояние формы

Одним из важных принципов при разработке приложений, а особенно GUI приложений заключается в том, что состояние формы (значения в полях, размеры и положение) после закрытия приложения и при последующем открытии должно сохранятся.

Как правило для хранения состояния приложения используют простые файлы в каком-нибудь распространённом формате типа *.xml, *.ini, *.json

Выбор моментов фиксации самого состояние остается на совести разработчика, это может быть момент сразу после изменения значений в полях, может быть реакцией на клик какой-нибудь кнопки (в идеале чем чаще, тем лучше), а может быть просто момент закрытия формы.

Подключаем обработчик закрытия формы

Мы будем сохранять в момент закрытия формы.

Вот мы недавно уже мучались с поиском момента создания формы, теперь так же помучаемся с определением момента закрытия формы.

К сожалению, тут простым интерфейсом не отделаешься. Вообще в JavaFX нет как такового понятия формы.

Там есть понятие stage (англ. состояние, сцена как место где происходят события), которое представляет собой окно приложения и scene (англ. тоже сцена, но уже в качестве события) которая, если немного упростить, представляет собой fxml файлик + контроллер.

Ну и ясно что именно к scene привязывается класс Controller. И так как scene за окно формы (то бишь stage) не отвечает, то из него и нельзя получить прямого доступа к событию закрытия окна.

Но можно получить не прямой. Для этого надо поймать событие закрытия формы (то бишь stage) и проксировать его в Controller. Делается это так:

Сначала пойдем в контроллер и создадим функцию которая должна будет вывязываться при закрытии формы. Такую:

public class Controller implements Initializable {
    /* ... */

    public void onStageClose() {
        System.out.println("Ура! расходимся! =)");
    }
}

теперь идем в Main и делаем магические пассы:

public class Main extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception{
        // нашу первую строчку мы расщепляем пополам
        // сначала формируем загрузчик и привязываем его к файлу
        FXMLLoader loader = new FXMLLoader(getClass().getResource("sample.fxml"));
        // а затем уже непосредственно вызываем загрузку дерева разметки из файла
        Parent root = loader.load();
        
        // добавляем эту строчку, собственно ради чего мы первую строку и расщепили
        // тут мы вытаскиваем контроллер которые был создан при вызове метода load
        // и сохраняем ссылку на него в переменную
        Controller controller = loader.getController();

        // а тут привязываем событие закрытия приложения к нашей функции onStageClose
        primaryStage.setOnHidden(e -> controller.onStageClose());

        // это не трогаем   
        primaryStage.setTitle("Hello World");
        primaryStage.setScene(new Scene(root, 300, 275));
        primaryStage.show();
    }


    public static void main(String[] args) {
        launch(args);
    }
}

проверяем

Imgur

прекрасно! =)

Сохраняем состояние формы в файл

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

Поэтому мы создадим класс который будет хранить состояние формы, а работу по сохранения/чтения его в файл отдадим на откуп встроенному в java XML сериализатору.

И так создаем новый класс

Imgur

фиксируем в него поля

public class Settings {
    public String txtInputText;
    public Boolean checkBoxSelected;
    public int comboBoxSelectedIndex;
}

теперь идем в Controller и в методе закрытия формы прописываем:

public void onStageClose() {
    // создали экземпляр класс
    Settings settings = new Settings();
    // зафиксировали значения полей
    settings.checkBoxSelected = checkBox.isSelected();
    settings.comboBoxSelectedIndex = comboBox.getSelectionModel().getSelectedIndex();
    settings.txtInputText = txtInput.getText();
}

а теперь добавляем сериализацию в файл. Тут все просто

public void onStageClose() {
    /* ЭТО НЕ ТРОГАЕМ */
    Settings settings = new Settings();
    settings.checkBoxSelected = checkBox.isSelected();
    settings.comboBoxSelectedIndex = comboBox.getSelectionModel().getSelectedIndex();
    settings.txtInputText = txtInput.getText();
    
    // добавляем
    try {
        // создаем поток для записи в файл experiment.xml
        FileOutputStream fos = new FileOutputStream("settings.xml");
        // создали энкодер, которые будет писать в поток
        XMLEncoder encoder = new XMLEncoder(fos);

        // записали настройки
        encoder.writeObject(settings);

        // закрыли энкодер и поток для записи
        // если не закрыть, то файл будет пустой
        encoder.close();
        fos.close();
    } catch (Exception e) {
        // на случай ошибки
        e.printStackTrace();
    }
}

теперь проверяем как работает

Imgur

Так-так, файлик появился. У меня внутри такие строчки:

<?xml version="1.0" encoding="UTF-8"?>
<java version="1.8.0_265" class="java.beans.XMLDecoder">
 <object class="sample.Settings" id="Settings0">
  <void class="sample.Settings" method="getField">
   <string>txtInputText</string>
   <void method="set">
    <object idref="Settings0"/>
    <string>Меня подменили! (~˘▾˘)~</string>
   </void>
  </void>
  <void class="sample.Settings" method="getField">
   <string>checkBoxSelected</string>
   <void method="set">
    <object idref="Settings0"/>
    <boolean>true</boolean>
   </void>
  </void>
  <void class="sample.Settings" method="getField">
   <string>comboBoxSelectedIndex</string>
   <void method="set">
    <object idref="Settings0"/>
    <int>1</int>
   </void>
  </void>
 </object>
</java>

теперь попробуем добавить чтение из файла. Чтение стало быть делаем в инициализаторе:

public void initialize(URL location, ResourceBundle resources) {
    // ЭТО НЕ ТРОГАЕМ
    comboBox.getItems().setAll(/* ... */);
    comboBox.getSelectionModel().selectedItemProperty().addListener(/* ... */);
    
    // добавляем
    try {
        // создаем поток для чтения из файла
        FileInputStream fis = new FileInputStream("settings.xml");
        // создаем xml декодер из файла
        XMLDecoder decoder = new XMLDecoder(fis);

        /**
         * С помощью decoder.readObject() читаем объект из файла
         * а так как джава сама не может определить, что в файле
         * мы ей подсказываем, указывая в скобочках (Settings)
         * ну и просто загоняем в переменную settings
         */
        Settings settings = (Settings) decoder.readObject();

        // а теперь заполняем форму
        checkBox.setSelected(settings.checkBoxSelected);
        comboBox.getSelectionModel().select(settings.comboBoxSelectedIndex);
        txtInput.setText(settings.txtInputText);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

проверяем:

Imgur

красота! =)

Ну все, гоу пилить третью лабу