Лабораторна робота 4

Багатопотоковість. Створення GUI-застосунків

1 Завдання на лабораторну роботу

1.1 Індивідуальне завдання

Необхідно реалізувати мовою Java за допомогою засобів JavaFX застосунок графічного інтерфейсу користувача, в якому здійснюється обробка даних індивідуальних завдань попередніх лабораторних робіт. Головне вікно повинно містити меню, в якому необхідно реалізувати такі функції:

  • створення нового набору даних;
  • завантаження (десеріалізація) даних з XML-документа для редагування;
  • зберігання змінених даних в XML-документі за допомогою засобів серіалізації XStream;
  • пошук за ознаками, визначеними в лабораторній роботі № 2 Базового курсу програмування Java попереднього семестру;
  • здійснення сортування за ознаками, визначеними в лабораторній роботі № 2 Базового курсу програмування Java попереднього семестру;
  • отримання вікна "Про програму" з даними про програму й автора.

У лівій частині вікна слід розташувати рядки для введення скалярних даних, область відображення для результатів пошуку, а також кнопки, які забезпечують виконання основних функцій програми. В середній частині вікна слід розташувати таблицю для відображення та редагування даних.

1.2 Обчислення π в окремому потоці виконання

Реалізувати програму обчислення π с точністю до заданого ε як суму послідовності:

Обчислення здійснювати в окремому потоці виконання. Під час виконання обчислення надавати користувачеві можливість уводити запит про кількість обчислених елементів суми.

1.3 Використання ObservableList

Створити список (ObservableList) дійсних чисел типу Double. Змінити початковий порядок розташування чисел так, щоб спочатку були розташовані додатні числа (без зміни їх відносного порядку), а потім – від'ємні у порядку, протилежному початковому. Кожна зміна стану списку повинна обумовлювати виведення елементів списку в консольне вікно.

1.4 Створення застосунку графічного інтерфейсу користувача для отримання простих множників чисел (додаткове завдання)

За допомогою засобів JavaFX розробити застосунок графічного інтерфейсу користувача, в якому користувач вводить діапазон чисел (від і до), а у вікні відображаються числа і їх прості множники. Реалізувати можливість призупинення, відновлення потоку, а також повного припинення і повторного обчислення з новими даними.

2 Методичні вказівки

2.1 Робота з потоками виконання. Загальні концепції

Процес (process) – це екземпляр комп'ютерної програми, яка завантажена в пам'ять виконується. В сучасних операційних системах процес може виконуватися паралельно з іншими процесами. Процесу виділяється окремий адресний простір, причому цей простір фізично недоступний для інших процесів.

Більшість реалізацій віртуальної машини Java виконується у межах одного процесу. Програма Java може створити додаткові процеси та організувати взаємодію між ними за допомогою об'єкта ProcessBuilder. У наведеному нижче найпростішому прикладі здійснюється запуск програми notepad.exe (блокнот):

package ua.inf.iwanoff.java.advanced.fourth;

public class StartNotepad {

    static void main() throws java.io.IOException {
        String[] command = {"notepad"}; // у масиві також можуть бути аргументи
        ProcessBuilder processBuilder = new ProcessBuilder(command);
        processBuilder.start();
    } 
}

Нитка (thread), або потік виконання (потік управління) – це окрема підзадача, яка може виконуватися паралельно з іншими підзадачами (нитками) в межах одного процесу. Кожен процес містить як мінімум один потік виконання, іменований головним (main thread). Всі потоки виконання, створені процесом, виконуються в адресному просторі цього процесу і мають доступ до ресурсів процесу. Створення потоків виконання – істотно менш ресурсомістка операція, ніж створення нових процесів. Потоки виконання іноді називають легковажними процесами (lightweight processes).

Якщо процес створив кілька потоків, то всі вони виконуються паралельно, при чому час центрального процесора (або декількох центральних процесорів в мультипроцесорних системах) розподіляється між цими потоками. Розподілом часу центрального процесора займається спеціальний модуль операційної системи – планувальник (scheduler). Планувальник по черзі передає управління окремим потокам, так що навіть в однопроцесорній системі створюється ілюзія паралельної роботи запущених потоків. Розподіл часу виконується за перериваннями системного таймера.

Потоки виконання використовують для реалізації незалежних підзадач у межах одного процесу з метою реалізації фонових процесів, моделювання паралельного виконання певних дій або підвищення зручності користувацького інтерфейсу.

2.2 Низькорівневі засоби роботи з потоками виконання

2.2.1 Створення потоків виконання

Є два підходи до створення об'єкта-нитки з використанням класу Thread:

  • успадкування нового класу від класу java.lang.Thread і створення об'єкта нового класу;
  • створення класу, який реалізує інтерфейс java.lang.Runnable; об'єкт такого класу передається конструктору класу java.lang.Thread.

Під час створення похідного класу від Thread необхідно перекрити його метод run(). Після створення об'єкта-потоку його треба запустити за допомогою методу start(). Цей метод здійснює певну ініціалізацій роботу і викликає run(). У наведеному нижче прикладі створюється окремий потік виконання, який здійснює виведення чергового цілого числа від 1 до 40:

package ua.inf.iwanoff.java.advanced.fourth;

public class ThreadTest extends Thread {

    @Override
    public void run() {
        for (int counter = 1; counter <= 40; counter++) {
            try {
                Thread.sleep(1000);
            }
            catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(counter);
        }
    }

    static void main() {
        new ThreadTest().start();
        // Сюди можна додати дії, що виконуються паралельно з методом run()
    }
}

Виклик методу sleep() зумовлює припинення потоку на вказану кількість мілісекунд. Виклик методу sleep() вимагає перехоплення винятку InterruptedException, який генерується в разі переривання цього потоку виконання іншим потоком. Аналогічний приклад зі створенням об'єкта класу, що реалізує інтерфейс Runnable. У цьому випадку також необхідно запустити потік за допомогою методу start():

package ua.inf.iwanoff.java.advanced.fourth;

public class RunnableTest implements Runnable {
  
    @Override
    public void run() {
        for (int counter = 1; counter <= 40; counter++) {
            try {
                Thread.sleep(1000);
            }
            catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(counter);
        }
    }  

    static void main() {
        new Thread(new RunnableTest()).start();
        // Сюди можна додати дії, що виконуються паралельно з методом run()
    }
}

Другий підхід є кращим, оскільки в цьому випадку ми вільні у виборі базового класу.

Окремим потокам можна давати імена (другий параметр конструктора) і отримувати ці імена за допомогою функції getName().

За допомогою методу setPriority()можна змінювати пріоритети потоків. Не рекомендується встановлювати вищий пріоритет (константа Thread.MAX_PRIORITY).

2.2.2 Стани потоку

Будь-який потік виконання може знаходитися в декількох стандартних станах. Стан "новий" (Thread.State.NEW) потік отримує, коли створюється об'єкт потоку. Виклик методу start() переводить потік зі стану "новий" в стан "працює" (Thread.State.RUNNABLE). Існують стани "заблокований" (Thread.State.BLOCKED), "заблокований за часом", або "в режимі очікування" (Thread.State.TIMED_WAITING), "очікує", або "непрацездатний" (Thread.State.WAITING) і "завершений" (Thread.State.TERMINATED). Під час створення потоку він отримує стан "новий" і не виконується. Отримати значення стану потоку можна викликом методу getState(). Наведений нижче приклад демонструє деякі стани потоку в межах його життєвого циклу:

package ua.inf.iwanoff.java.advanced.fourth;

public class StateTest {

    static void main() throws InterruptedException {
        Thread testThread = new Thread();
        System.out.println(testThread.getState()); // NEW
        testThread.start();
        System.out.println(testThread.getState()); // RUNNABLE
        testThread.interrupt(); // перериваємо потік
        Thread.sleep(100);      // потрібен час для завершення потоку
        System.out.println(testThread.getState()); // TERMINATED
    }
}

Метод wait(long timeout), як і метод sleep(long timeout), дозволяє призупинити роботу потоку на вказану кількість мілісекунд. Під час виконання цього методу також може бути згенеровано виняток InterruptedException. Застосування цих методів дозволяє перевести потік в режим очікування (TIMED_WAITING). На відміну від sleep(), працездатність після виклику методу wait() можна відновити методами notify() або notifyAll().

Метод wait() може бути викликаний без параметрів. Потік при цьому переходить в стан "непрацездатний" (WAITING).

Менш надійним альтернативним способом припинення роботи потоку є виклик методу yield(), який повинен призводити до припинення потоку на деякий квант часу, для того, щоб інші потоки могли виконувати свої дії.

Після завершення виконання методу run() потік закінчує свою роботу, потік переходить в пасивний стан (TERMINATED).

У ранніх версіях Java для примусової зупинки, а також для тимчасового призупинення роботи потоку з подальшим відновленням передбачалося використання методів класу Thread - stop(), suspend() і resume(). Методи stop(), suspend() і resume() вважаються небажаними для використання (deprecated-методи), оскільки вони провокують створення ненадійного коду, який важко зневаджувати. Крім того, використання suspend() і resume() може спровокувати взаємні блокування (deadlock).

Перервати потік, який знаходиться в стані очікування (Thread.State.TIMED_WAITING), можна за допомогою виклику функції interrupt(). В цьому випадку генерується виняток InterruptedException. Можна модифікувати попередній приклад так, щоб він дозволяв перервати потік виконання:

package ua.inf.iwanoff.java.advanced.fourth;

import java.util.Scanner;

public class InterruptTest implements Runnable {

    @Override
    public void run() {
        for (int counter = 1; counter <= 40; counter++) {
            try {
                Thread.sleep(1000);
            }
            catch (InterruptedException e) {
                System.out.println("Потік перерваний.");
                break;
            }
            System.out.println(counter);
        }
    }

    static void main() {
        Thread thread = new Thread(new InterruptTest());
        thread.start();
        System.out.println("Для переривання натисніть Enter");
        new Scanner(System.in).nextLine();
        thread.interrupt();
    }
}

2.2.3 Об'єднання та переривання потоків

Метод join() дозволяє одному потоку дочекатися завершення іншого. Виклик цього методу всередині потоку t1 для потоку t2 призведе до припинення поточного потоку (t1) до завершення t2, як показано в наведеному нижче прикладі:

package ua.inf.iwanoff.java.advanced.fourth;

public class JoinTest {
    static Thread t1, t2;

    static class FirstThread implements Runnable {
        @Override
        public void run() {
            try {
                System.out.println("Потік First запущений.");
                Thread.sleep(1000);
                System.out.println("Основна робота потоку First завершена.");
                t2.join();
                System.out.println("Потік First завершено.");
            }
            catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    static class SecondThread implements Runnable {
        @Override
        public void run() {
            try {
                System.out.println("Потік Second запущений.");
                Thread.sleep(3000);
                System.out.println("Потік Second завершено.");
            }
            catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    static void main() {
        t1 = new Thread(new FirstThread());
        t2 = new Thread(new SecondThread());
        t1.start();
        t2.start();
    }
}

Порядок виведення повідомлень буде таким:

Потік First запущений.
Потік Second запущений.
Основна робота потоку First завершена.
Потік Second завершено.
Потік First завершено.

Як і sleep(), join() реагує на переривання, отже, необхідно перехоплювати виняток InterruptedException.

Можна створити потік виконання, робота якого переривається при завершенні потоку, який його породив. Для того, щоб створити такий потік, викликається метод setDaemon setDaemon() з параметром true. Метод setDaemon() необхідно викликати після створення потоку, але до моменту його запуску, тобто перед викликом методу start().За допомогою методу isDaemon() можна перевірити, є потік демоном, чи ні. Якщо потік-демон створює інші потоки, то вони також отримають статус потоку-демона.

package ua.inf.iwanoff.java.advanced.fourth;

public class DaemonThread extends Thread {
    public void run() {
        try {
            if (isDaemon()) {
                System.out.println("старт потоку-демона");
                sleep(1000);
            }
            else {
                System.out.println("старт звичайного потоку");
            }
        }
        catch (InterruptedException e) {
            System.err.print("Error" + e);
        }
        finally {
            if (!isDaemon())
                System.out.println("завершення звичайного потоку");
            else
                System.out.println("завершення потоку-демона");
        }
    }

    static void main() {
        DaemonThread usual = new DaemonThread();
        DaemonThread daemon = new DaemonThread();
        daemon.setDaemon(true);
        daemon.start();
        usual.start();
        System.out.println("останній рядок main");
    }
}

Після компіляції й запуску, можливо, буде виведено:

старт потоку-демона
останній рядок main
старт звичайного потоку
завершення звичайного потоку

Потік-демон (через виклик методу sleep(10000)) не встиг завершити виконання свого коду до завершення основного потоку застосунку, пов'язаного з методом main(). Базова властивість потоків-демонів полягає в можливості основного потоку застосунку завершити виконання з закінченням коду методу main(), не звертаючи уваги на те, що потік-демон ще працює. Якщо поставити час затримки також для потоку main(), то потік-демон може встигнути завершити своє виконання до закінчення роботи основного потоку:

старт потоку-демона
старт звичайного потоку
завершення звичайного потоку
завершення потоку-демона
останній рядок main

2.2.4 Синхронізація потоків

Якщо запущено декілька потоків виконання, потрібен спосіб для виключення можливості використання одного ресурсу двома потоками. Якщо елементи даних класу оголошені як private і доступ до цієї області пам'яті можливий тільки за допомогою методів, то можна уникнути колізій, оголосивши ці методи як synchronized. Одночасно тільки один процес може викликати synchronized метод для певного об'єкта. Такий метод або фрагмент коду іменується критичною секцією.

Для забезпечення доступу до такого коду використовується концепція монітора. Під монітором розуміють деякий об'єкт, що забезпечує блокування коду під час виконання його деяким потоком. Якщо ключове слово synchronized розташоване перед заголовком функції, монітором є об'єкт, для якого викликаний цей метод (this). Після того, як викликаний хоча б один синхронізований метод, блокування поточним об'єктом поширюється на всі методи з модифікатором synchronized.

Розглянемо такий приклад. Клас Adder дозволяє додавати цілі числа до деякого накопичувача. Клас AdderThread реалізує потік виконання, що забезпечує додавання послідовних п'яти цілих значень. У функції main() класу AdderTest створюємо два потоки й запускаємо їх:

package ua.inf.iwanoff.java.advanced.fourth;

class Adder {
    long sum = 0;

    public void add(long value) {
        this.sum += value;
        System.out.println(Thread.currentThread().getName() + " " + sum);
    }
}

class AdderThread extends Thread {
    private Adder counter = null;

    public AdderThread(String name, Adder counter) {
        super(name);
        this.counter = counter;
    }

    public void run() {
        try {
            for (int i = 0; i < 5; i++) {
                counter.add(i);
                Thread.sleep(10);
            }
        }
        catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class AdderTest {

    static void main() {
        Adder adder = new Adder();
        Thread threadA = new AdderThread("A", adder);
        Thread threadB = new AdderThread("B", adder);
        threadA.start();
        threadB.start();
    }
}

Завантажена декілька разів на виконання програма буде продукувати різні проміжні результати, що пов'язано з некоректною роботою двох потоків з одним об'єктом. Для виправлення ситуації перед методом add() слід додати модифікатор synchronized:

synchronized public void add(long value) {
    this.sum += value;
    System.out.println(Thread.currentThread().getName() + " " + sum);
}

Іноді оголошувати весь метод як synchronized незручно, оскільки одночасно інші нитки могли б виконувати інші заблоковані методи. Для розв'язання цієї проблеми використовують блок, що починається зі слова synchronized і містить після цього слова в дужках ім'я об'єкта, який відповідає за блокування. Монітором може бути будь-який об'єкт, похідний від java.lang.Object, що підтримує необхідні для цього методи. У наведеному нижче прикладі створюється спеціальний об'єкт для блокування:

public void add(long value) {
    Object lock = new Object();
    synchronized (lock) {
        this.sum += value;
        System.out.println(Thread.currentThread().getName() + " " + sum);
    }
}

Примітка: в нашому випадку такий підхід може не дати бажаного результату; краще здійснити блокування на рівні всього методу.

Синхронізації не вимагають процеси запису і читання об'єктів, розміри яких не перевищують 32 біт. Такі об'єкти називаються атомарними.

Під час обміну даних через поля слід використовувати ключове слово volatile перед описом відповідного поля. Це пов'язано з наявністю механізму кешування даних окремо для кожного потоку, і значення з цього кешу може відрізнятися для кожного з них. Оголошення поля з ключовим словом volatile відключає для нього таке кешування:

volatile long sum = 0;

Метод wait(), викликаний всередині синхронізованого блоку або методу, зупиняє виконання поточного потоку і звільняє від блокування захоплений об'єкт, зокрема об'єкт lock. Повернути блокування об'єкта потоку можна викликом методу notify() для конкретного потоку або notifyAll() для всіх потоків. Виклик може бути здійснений тільки з іншого потоку, який заблокував, своєю чергою, зазначений об'єкт.

2.3 Використання Java для створення GUI-застосунків

2.3.1 Загальні концепції

Інтерфейс користувача – це набір технічних та програмних засобів, за допомогою яких людина взаємодіє з комп'ютером. Далі йтиметься про програмні засоби інтерфейсу комп'ютера.

Інтерфейс командного рядка – це метод взаємодії з програмою за допомогою інтерпретатору команд, які користувач уводить, як правило, у текстовому режимі, або у спеціальному консольному вікні. Результат виконання команд також відображається у консольному вікні. Програми такого типу також називають консольними застосунками. Консольні застосунки не беруть участі в обміні системними повідомленнями, а також не можуть відсилати повідомлення іншим застосункам.

Графічний інтерфейс користувача (Graphical user interface, GUI) дає можливість користувачеві взаємодіяти з комп'ютером за допомогою графічних елементів управляння (вікон, піктограм, меню, кнопок, списків тощо) та технічних пристроїв позиціювання, таких як маніпулятор "миша" Програми, які реалізують цей тип інтерфейсу, мають назву застосунків графічного інтерфейсу користувача.

Реалізація застосунків графічного інтерфейсу користувача базується на механізмі отримання та обробки подій. Уся програма складається з ініціалізації (реєстрації візуальних елементів управління) та основного циклу отримання та обробки подій. Події – це переміщення або натискання кнопок миші, клавіатурне введення тощо. Кожний зареєстрований візуальний елемент управління може отримувати події, які до нього стосуються, та виконувати функції обробки цих подій.

Засоби розробки графічного інтерфейсу користувача є складовою частиною Java-технологій від початку існування Java. Першою бібліотекою Java, яка надавала засоби створення програм графічного інтерфейсу, була бібліотека Abstract Window Toolkit (AWT). AWT є частиною Java Foundation Classes (JFC) – стандартного API для реалізації графічного інтерфейсу Java-програми. У перші роки існування Java бібліотека AWT використовувалася переважно для створення аплетів.

Основним недоліком бібліотеки AWT є орієнтація на графічні діалогові компоненти, які надають конкретні операційні системи та графічні оболонки. Це призводить, з одного боку до певних проблем з розгортанням програми на різних програмних платформах, з іншого боку, обмежує виразні засоби застосунків, оскільки необхідно орієнтуватися тільки на ті візуальні компоненти, які присутні на всіх платформах. Такі візуальні компоненти заведено називати "великоваговими" (high weight). Цей та інші недоліки AWT виправлені в бібліотеці Swing. Бібліотека Swing також надає деякі додаткові візуальні компоненти, такі як панель із закладками, списки що випадають, таблиці, дерева та ін.

Наразі стандартними засобами розробки додатків графічного інтерфейсу користувача в Java є бібліотеки AWT і Swing, а також платформа JavaFX, засоби якої є альтернативою Swing. Крім того, різні розробники надають альтернативні нестандартні бібліотеки, такі як Qt Jambi, Standard Window Toolkit (SWT), XML Window Toolkit (XWT). Дві останні бібліотеки, поряд з AWT і Swing, підтримуються Eclipse.

2.3.2 Створення та використання аплетів

Аплети – це програмні компоненти Java, які зберігаються на web-сервері, завантажуються на клієнтський комп'ютер, та виконуються за допомогою віртуальної машини Java web-браузера клієнта.

Все необхідне для виконання аплету міститься в тегу <applet> у тексті HTML-файлу. Аплет зазвичай відповідає за певну прямокутну область у вікні браузера. Координати цієї області також можуть бути задані в теґу <applet>. Наприклад:

<applet codebase = "." code = "test.Applet1.class" width = 400 height = 300>
</applet>

Атрибут codebase задає місце розміщення класу, що реалізує аплет (в наведеному прикладі – поточна тека). Атрибут code – ім'я класу. Атрибути width і height задають розміри вікна аплету.

Для реалізації аплетів як базові використовують класи java.applet.Applet (Java 1) та javax.swing.JApplet (Java 2). Життєвий цикл визначається такими методами класу Applet:

  • public void init() викликається браузером одразу після завантаження аплету перед першим викликом методу start(); цей метод потрібно перевизначати практично завжди, якщо в аплеті потрібна хоч якась ініціалізація;
  • public void start() викликається браузером під час кожного "відвідування" сторінки;
  • public void stop() викликається браузером під час деактивізації сторінки;
  • public void destroy() завжди викликається під час виходу з браузера і під час перезавантаження аплету.

З міркувань безпеки на аплети накладаються певні обмеження:

  • аплетам, завантаженим з мережі, заборонені операції читання і запису файлів з локальної файлової системи;
  • аплети не повинні виконувати мережеві з'єднання з усіма хостами, крім того, з якого був отриманий аплет;
  • аплетам не дозволено запускати програми на клієнтській системі;
  • аплетам заборонено завантажувати нові бібліотеки й викликати програми, зовнішні відносно Java-машини.

Аплет можна переглянути за допомогою програми appletviewer.exe, що входить до складу JDK. Практично будь-який діалоговий застосунок на Java може бути реалізовано так, що він зможе працювати і як програма і як аплет. Однак аплету не потрібна функція main().

Проблеми використання аплетів пов'язані з необхідністю забезпечення наявності відповідної версії Java, яку підтримує Web-браузер клієнта.

2.3.3 Застосування бібліотеки javax.swing

Бібліотека javax.swing пропонує розробникові низку стандартних класів, які можна використовувати для проєктування графічного інтерфейсу користувача. Ця бібліотека розширила попередню менш вдалу бібліотеку AWT (Abstract Window Toolkit) засоби якої використовує для обробки подій, роботи з графікою тощо. Для ідентифікації приналежності до бібліотеки javax.swing до імен класів візуальних компонентів додана літера J (наприклад, JButton, JPanel і т. д.).

На відміну від компонентів AWT, компоненти Swing є "легковагими" (lightweight). Це означає, що компоненти Swing використовують засоби Java для відображення елементів графічного інтерфейсу користувача на поверхні вікна, без використання компонентів операційної системи.

Як і більшість бібліотек графічного інтерфейсу користувача, бібліотека javax.swing підтримує концепцію головного вікна застосунку. Це головне вікно створюється як об'єкт класу JFrame, або похідного від нього. Далі до головного вікна додають візуальні компоненти – мітки (JLabel), кнопки (JButton), рядки введення (JTextField) тощо.

Для того, щоб створити найпростішу програму графічного інтерфейсу користувача, необхідно створити новий клас з функцією main(), а потім у вихідному тексті додати інструкцію import:

import javax.swing.*;

У функції main() створюємо нове вікно та вказуємо його заголовок:

// ...

public class HelloWorldSwing {
    static void main() {
       JFrame frame = new JFrame("Привіт");
       // ...
    }
}

Далі додаємо нову мітку до компонента, який відповідає за вміст вікна. Створюємо новий об'єкт типу JLabel:

frame.getContentPane().add(new JLabel("Привіт, світ!"));

За допомогою методу pack() здійснюється припасування розмірів вікна. Після виклику функції setVisible(true) вікно з'являється на екрані. Можна навести весь текст програми:

package ua.inf.iwanoff.java.advanced.fourth;

import javax.swing.*;

public class HelloWorldSwing {
    static void main() {
        JFrame frame = new JFrame("Привіт");
        frame.getContentPane().add(new JLabel("Привіт, світ!"));
        frame.pack();
        frame.setVisible(true);
    }
}

Далі до програми можна додавати інші візуальні компоненти та оброблювачі подій.

Візуальні компоненти бібліотеки Swing успадковуються від класу javax.swing.JComponent, спадкоємця класу java.awt.Container. Своєю чергою, цей клас є спадкоємцем java.awt.Component. Клас java.awt.Component – базовий клас, який визначає відображення на екрані й поведінку кожного елемента інтерфейсу під час взаємодії з користувачем. Методи класу, що відповідають за управління подіями, дозволяють задати розмір, колір, шрифт та інші атрибути елементів управління. Наприклад, метод setBackground(Color) встановлює колір фону компонентів, setFont(Font) – шрифт (java.awt.Color і java.awt.Font – класи, що дозволяють визначити певний колір і шрифт). Клас JComponent розширює можливості базових класів підтримкою механізму налаштування зовнішнього вигляду (Look & Feel), використанням гарячих клавіш, вікон підказки, і деякими іншими можливостями. Зокрема, змінювати зовнішній вигляд інтерфейсу користувача відповідно до стилю (Look & Feel), який прийнято у певній операційній системі або графічній оболонці. Для керування стилем використовують спеціальний клас UIManager. Усталено встановлено кросплатформений стиль ("metal"). Для того, щоб встановити стиль, прийнятий у конкретній операційній системі, до ініціалізації вікна слід додати такий код:

try {
    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
}
catch (Exception ex){
    ex.printStackTrace();
}

Використання візуальних компонентів базується на застосуванні Java Beans.

2.4 Огляд платформи JavaFX

2.4.1 Основні концепції платформи JavaFX

Від початку засоби JavaFX були представлені як програмна платформа для створення так званих насичених інтернет-застосунків (rich internet applications), визначаючи архітектуру, каркас і стиль розробки застосунку. Оскільки JavaFX надає велику кількість інтерфейсів і класів для розробки застосунків графічного інтерфейсу користувача, засоби JavaFX фактично є сучасною альтернативою бібліотеці javax.swing. Перша версія платформи (JavaFX 1.0) вийшла у 2008 році та включала спеціальну скриптову мову JavaFX Script для опису графічного інтерфейсу. У 2011 році вийшла версія JavaFX 2.0 під орудою Oracle. Розробники цієї версії відмовилися від спеціальної скриптової мови на користь Java. Версія JavaFX 8, яка вийшла у 2014 році, розроблена відповідно до можливостей та стилю Java 8, надає можливості роботи з 3D-графікою, а також пропонує нові візуальні компоненти. Номер версії відповідає Java 8, тому версії 3, 4, 5, 6 і 7 відсутні.

Основними рисами, що відрізняють JavaFX від попередніх бібліотек підтримки графічного інтерфейсу користувача є такі:

  • вбудована підтримка патерну проєктування MVC (Model-View-Controller);
  • можливість декларативного опису візуальних компонентів (мова FXML);
  • сучасний стиль візуальних компонентів;
  • підтримка розширених можливостей взаємодії користувача з застосунком;
  • можливість використання css-стилів для стилізації елементів користувацького інтерфейсу;
  • можливість використання 3D-графіки;
  • спрощена модель розгортання застосунків.

Є також низка додаткових можливостей, пов'язаних з графікою, текстом, взаємодією з раніше створеними бібліотеками тощо.

Платформу JavaFX можна розглядати як альтернативу попереднім бібліотекам графічного інтерфейсу користувача, яка покликана надалі повністю їх замінити.

2.4.2 Створення найпростішого застосунку. Структура програми JavaFX

Найпростіший застосунок графічного інтерфейсу користувача, що використовує бібліотеку JavaFX, можна створити, включивши всі необхідні компоненти безпосередньо в Java-коді. Наприклад, наведений нижче клас, похідний від javafx.application.Application, дозволяє створити вікно з кнопкою посередині:

package ua.inf.iwanoff.java.advanced.fourth;

import javafx.application.Application;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.layout.FlowPane;
import javafx.geometry.Pos;
import javafx.scene.control.Button;

public class Main extends Application {

    @Override
    public  void start(Stage primaryStage) throws Exception {
        // Встановлюємо заголовок вікна:
        primaryStage.setTitle("First Java FX Application");

        // Створюємо кореневий контейнер і встановлюємо центрування
        // дочірніх елементів
        FlowPane rootNode = new FlowPane();
        rootNode.setAlignment(Pos.CENTER);

        // Створюємо сцену і встановлюємо її в "підмостки"
        Scene scene = new Scene(rootNode, 300, 200);
        primaryStage.setScene(scene);

        // Створюємо кнопку, визначаємо дію при її натисканні
        // і вставляємо кнопку в кореневий контейнер:
        Button button = new Button("Press me");
        button.setOnAction(event -> button.setText("Do not press me"));
        rootNode.getChildren().add(button);

        // Показуємо вікно:
        primaryStage.show();
    }

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

У наведеному вище прикладі функція, що обробляє подію, пов'язану з натисканням кнопки, реалізована за допомогою лямбда-виразу.

Компоненти JavaFX утворюють ієрархію об'єктів. Застосунок має містити як мінімум один об'єкт типу Stage (підмостки сцени). Підмостки сцени визначають властивості вікна, що є їх власником: стиль вікна, його тип (наприклад, модальне / немодального), заголовок і т. д. Об'єкт типу Stage – контейнер верхнього рівня, що містить сцену (Scene). Сцена являє собою контейнер для інших візуальних компонентів.

На жаль у новітніх версіях JDK компіляцію та виконання цієї програми ускладнено, оскільки починаючи з Java 11, JavaFX не входить у JDK і розвивається як окремий проєкт OpenJFX. Його необхідно завантажувати й налагоджувати окремо.

2.4.3 Застосування засобів JavaFX у середовищі IntelliJ IDEA

Інтегроване середовище IntelliJ IDEA надає засоби для створення проєктів JavaFX. Спочатку необхідно виконати деякі підготовчі дії:

  • переконатися, що версія JDK не нижче 11;
  • перевірити, чи включений необхідний JavaFX-плагін; це можна здійснити в такий спосіб:
    • у вікні Settings (команда меню File | Settings...) IntelliJ IDEA вибрати позицію Plugins
    • у списку плагінів відшукати плагін JavaFX; якщо він не включений, включити його.

Тепер можна створити проєкт JavaFX. В лівій частині вікна майстра проєктів вибираємо JavaFX. Вказуємо ім'я проєкту, наприклад, FirstFX. Можна змінити параметри проєкту Maven (Group та Artifact). На другій сторінці майстра можна додати сторонні бібліотеки, пов'язані з JavaFX. Для першої програми додавання цих бібліотек не є доцільним. Автоматично створюється програма, що зображує вікно з заголовком Hello! і кнопкою всередині.

Проєкт містить у гілці "Java" пакет з трьома файлами:

  • HelloApplication.java містить клас HelloApplication, похідний від javafx.application.Application.
  • HelloController.java містить оброблювач події onHelloButtonClick().
  • Launcher.java містить клас Launcher з функцією main().

Код файлу HelloApplication.java усталено буде таким:

package ua.inf.iwanoff.java.advanced.fourth.demo;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;

import java.io.IOException;

public class HelloApplication extends Application {
    @Override
    public void start(Stage stage) throws IOException {
        FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("hello-view.fxml"));
        Scene scene = new Scene(fxmlLoader.load(), 320, 240);
        stage.setTitle("Hello!");
        stage.setScene(scene);
        stage.show();
    }
}

Код файлу HelloController.java буде таким:

package ua.inf.iwanoff.java.advanced.fourth.demo;

import javafx.fxml.FXML;
import javafx.scene.control.Label;

public class HelloController {
    @FXML
    private Label welcomeText;

    @FXML
    protected void onHelloButtonClick() {
        welcomeText.setText("Welcome to JavaFX Application!");
    }
}

Код файлу Launcher.java буде таким:

package ua.inf.iwanoff.java.advanced.fourth.demo;

import javafx.application.Application;

public class Launcher {
    static void main(String[] args) {
        Application.launch(HelloApplication.class, args);
    }
}

Крім того, у гілці resources в аналогічному пакеті розташовано файл hello-view.fxml, який містить опис елементів інтерфейсу користувача. Файл містить посилання на клас HelloController. Вміст цього файлу такий:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.VBox?>

<?import javafx.scene.control.Button?>
<VBox alignment="CENTER" spacing="20.0" xmlns:fx="http://javafx.com/fxml"
      fx:controller="ua.inf.iwanoff.java.advanced.fourth.demo.HelloController">
    <padding>
        <Insets bottom="20.0" left="20.0" right="20.0" top="20.0"/>
    </padding>

    <Label fx:id="welcomeText"/>
    <Button text="Hello!" onAction="#onHelloButtonClick"/>
</VBox>

Завдяки такому розділенню на файли у проєкті підтримується патерн MVC, принаймні дві його складові частини – View (hello-view.fxml) і Controller (клас HelloController).

У файлі pom.xml автоматично створені додаткові залежності:

<dependency>
    <groupId>org.openjfx</groupId>
    <artifactId>javafx-controls</artifactId>
    <version>25.0.1</version>
</dependency>
<dependency>
    <groupId>org.openjfx</groupId>
    <artifactId>javafx-fxml</artifactId>
    <version>25.0.1</version>
</dependency>

Примітка: як і в усіх подібних випадках, версія артефактів може змінитися.

Після того, як автоматично буде завантажено необхідні засоби, можна запустити програму на виконання і натиснути кнопку. З'явиться текст "Welcome to JavaFX Application!".

Примітка: у новітніх версіях JDK виконання програми може спричинити появу попередження, аналогічні попередженням, які спричинило використання XStream; методи боротьби з цими попередженнями були описані в попередній роботі.

Іноді необхідно додати засоби JavaFX до проєкту, який було створено раніше. Окрім безпосереднього додавання коду, в якому використані компоненти JavaFX, необхідно додати залежності до файлу pom.xml:

<dependency>
    <groupId>org.openjfx</groupId>
    <artifactId>javafx-controls</artifactId>
    <version>25.0.1</version>
</dependency>
<dependency>
    <groupId>org.openjfx</groupId>
    <artifactId>javafx-fxml</artifactId>
    <version>25.0.1</version>
</dependency>
<dependency>
    <groupId>org.openjfx</groupId>
    <artifactId>javafx-graphics</artifactId>
    <version>25.0.1</version>
    <classifier>win</classifier>
</dependency>

Якщо в проєкті використовується файл з розміткою (*.fxml), у теці проєкту для нього слід створити теку src\main\resources\ і далі відтворити ієрархію пакетів, включаючи пакет з класом-застосунком. У створену в такий спосіб теку записують файл розмітки.

Але навіть після успішної компіляції застосунок під час роботи вимагатиме наявності бібліотеки часу виконання. На консоль буде виведено повідомлення про помилку:

Error: JavaFX runtime components are missing, and are required to run this application

Необхідні компоненти часу виконання можна отримати, завантаживши й вручну розгорнувши SDK JavaFX на своєму комп'ютері. Необхідні засоби для вашої операційної системи можна завантажити зі сторінки https://gluonhq.com/products/javafx/. Завантажений архів слід розкрити в будь-яку теку на комп'ютері, наприклад, c:\javafx-sdk-25 (відповідна тека є в архіві). Для роботи застосунку необхідна тека lib, яка знаходиться всередині вказаної, наприклад, c:\javafx-sdk-25\lib.

Тепер для роботи застосунку в середовищі IntelliJ IDEA можна створити нову конфігурацію часу виконання через головне меню: Run | Edit Configurations, далі у вікні Run/Debug Configurations додаємо нову конфігурацію (Add New Configuration), натиснувши "плюс". Далі вибираємо Application і у вікні налаштовуємо опції конфігурації: "Ім'я" (Name) і "Головний клас" (Main class). Найголовніше – вказати додаткові опції віртуальної машини Java. Зі списку Modify options вибираємо Add VM options. З'являється додатковий рядок введення. Для вказаного раніше розташування SDK JavaFX в цьому рядку слід вказати

--module-path "c:\javafx-sdk-25\lib" --add-modules javafx.controls,javafx.fxml

Далі слід зберегти створену конфігурацію і користуватися нею для виконання програми.

2.5 Теоретичні засади створення застосунків JavaFX

2.5.1 Використання властивостей JavaFX. Використання Observable

В широкому сенсі властивість об'єкта – це атрибут його даних, під час зміни значення якого (а іноді й під час читання) можуть автоматично виконуватися певні дії. Деякі мови програмування, такі як Visual Basic, Object Pascal, C# тощо, підтримують властивості на синтаксичному рівні. В цих мовах синтаксис звернення до властивостей збігається з синтаксисом роботи з полями класу.

В технологіях Java властивості на логічному рівні представлені в компонентах Java Beans, синтаксис яких було описано раніше. Розширюючи модель Java Beans, JavaFX надає спеціальний узагальнений інтерфейс Property і велику кількість абстрактних класів, таких як BooleanProperty, DoubleProperty, FloatProperty, IntegerProperty, StringProperty тощо. Існують також усталені реалізації цих класів, такі як, наприклад, SimpleBooleanProperty, SimpleDoubleProperty, SimpleFloatProperty, SimpleIntegerProperty, SimpleStringProperty тощо.

Робота з властивостями вимагає підключення до проєкту засобів JavaFX. Відповідну залежність можна додати до файлу pom.xml:

        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-controls</artifactId>
            <version>25.0.1</version>
        </dependency>

Для того, щоб визначити властивість у деякому класі, треба визначити приватне поле відповідного типу, а також створити один сетер і два гетери:

package ua.inf.iwanoff.java.advanced.fourth;

import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;

public class LiveNumber {
    private IntegerProperty number = new SimpleIntegerProperty();

    public void setNumber(int number) {
        this.number.set(number);
    }

    public int getNumber() {
        return number.get();
    }

    public IntegerProperty numberProperty() {
        return number;
    }
}

Примітка: для стандартних властивостей JavaFX середовище IntelliJ IDEA сформує необхідні сетер і гетери автоматично (Code | Generate | Getter and Setter).

Таким чином, нам нічого не заважає створювати класи з властивостями JavaFX замість даних стандартних типів для моделювання сутностей реального світу. Але найголовніше – використання Property надає можливість отримувати повідомлення про зміну значення властивості. Іншими словами, властивості JavaFX реалізують вбудовану підтримку патерну проєктування Observer.

Observable (спостережуваний) – це сутність, яка обгортає вміст і дозволяє спостерігати за вмістом з точки зору втрати його актуальності. Відповідний базовий інтерфейс визначено в пакеті javafx.beans. Цей інтерфейс оголошує два методи:

public interface Observable {
    void addListener(InvalidationListener listener);
    void removeListener(InvalidationListener listener);
}

Класи, які реалізують інтерфейс Property, також реалізують інтерфейс Observable. Це дозволяє відстежувати зміни значення можна через механізм обробки подій.

package ua.inf.iwanoff.java.advanced.fourth;

import javafx.beans.value.ObservableValue;

public class LiveNumberDemo {
    static void main() {
        LiveNumber liveNumber = new LiveNumber();
        liveNumber.numberProperty().addListener(LiveNumberDemo::listen);
        liveNumber.setNumber(100);
        liveNumber.setNumber(200);
    }

    private static void listen(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
        System.out.printf("Old: %d  \tNew: %d\n", oldValue.intValue(), newValue.intValue());
    }
}

Наведений механізм можна застосовувати в різних задачах, в тому числі не пов'язаних з JavaFX.

2.5.2 Використання зв'язування

Зв'язування (binding) в JavaFX побудоване на можливостях властивостей і дозволяє оновлювати об'єкти синхронно з даними зв'язаних з ними об'єктів. Наприклад, можна змінювати розміри візуальних компонентів залежно від розмірів інших компонентів, автоматично оновлювати дані в таблицях тощо.

Існує два підходи до реалізації механізму зв'язування – низькорівневий і високорівневий. Більш універсальним є низькорівневе зв'язування. В наведеному нижче прикладі змінна sum автоматично отримує значення суми двох цілих чисел:

package ua.inf.iwanoff.java.advanced.fourth;

import javafx.beans.binding.IntegerBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;

public class LowLevelBinding {
    static void main() {
        final IntegerProperty m = new SimpleIntegerProperty(1);
        final IntegerProperty n = new SimpleIntegerProperty(2);
        IntegerBinding sum = new IntegerBinding() {
            {
                super.bind(m, n);
            }
            @Override
            protected int computeValue() {
                return (m.get() + n.get());
            }
        };
        System.out.println(sum.get());
        n.set(3);
        System.out.println(sum.get());
    }
}

Для більшості простих обчислень застосовують високорівневий підхід. Існують статичні функції класу Bindings, які забезпечують необхідний механізм зв'язування. Попередній приклад можна реалізувати за допомогою високорівневих механізмів зв'язування:

package ua.inf.iwanoff.java.advanced.fourth;

import javafx.beans.binding.Bindings;
import javafx.beans.binding.NumberBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;

public class HighLevelBinding {
    static void main() {
        final IntegerProperty m = new SimpleIntegerProperty(1);
        final IntegerProperty n = new SimpleIntegerProperty(2);
        NumberBinding sum = Bindings.add(n, m);
        System.out.println(sum.getValue());
        n.set(3);
        System.out.println(sum.getValue());
    }
}

Існують також різні варіанти функцій subtract(), multiply(), divide(), equal(), greaterThan(), lessThan(), min(), max() та багато інших. Крім того, відповідні нестатичні методи оголошені у класі NumberExpressionBase, з якого опосередковано походять класи IntegerProperty, DoubleProperty та інші "обгортки" для числових значень. Для визначення послідовності дій можна використовувати суперпозицію функцій, наприклад Bindings.add(a.multiply(b), c.multiply(d)) тощо.

Можливості зв'язування дуже часто використовують у програмах графічного інтерфейсу користувача для синхронних змін декількох об'єктів або для підтримки синхронізації представлення з даними моделі.

2.5.3 Робота з колекціями JavaFX

Окрім властивостей і зв'язування, JavaFX надає спеціальні колекції, головна відмінність яких – наявність механізму автоматичного сповіщення про зміну стану елементів. Узагальнений інтерфейс javafx.collections.ObservableList, який розширює інтерфейси java.util.List і javafx.beans.Observable, об'єднує функції для роботи зі стандартними списками з можливостями відстежування змін інформації в списках. Можливості реагування на зміни в стані списку показані в наведеному нижче прикладі:

package ua.inf.iwanoff.java.advanced.fourth;

import javafx.collections.*;

public class ListChangeDemo {
    static void main() {
        final ObservableList<Integer> list = FXCollections.observableArrayList(1, 2, 4, 8);
        list.addListener(new ListChangeListener() {
            @Override
            public void onChanged(ListChangeListener.Change change) {
                while (change.next()) {
                    if (change.wasAdded()) {
                        System.out.println("Item was added " + list);
                        return;
                    }
                    if (change.wasRemoved()) {
                        System.out.println("Item was removed " + list);
                        return;
                    }
                    System.out.println("Detected changes " + list);
                }
            }
        });
        list.add(16);   // Item was added [1, 2, 4, 8, 16]
        list.remove(0); // Item was removed [2, 4, 8, 16]
    }
}

Як видно з прикладу, для аналізу події необхідно створити цикл.

Для створення об'єкта, який реалізує інтерфейс ObservableList, можна також скористатися статичною функцією FXCollections.observableList() з параметром типу "традиційного" списку:

List<String> list = new ArrayList<String>();
ObservableList<String> observableList = FXCollections.observableList(list);

Крім інтерфейсу ObservableList, також надаються інтерфейси ObservableSet, ObservableMap, ObservableArray, а також інтерфейси-обгортки для звичайних масивів типів float та int (ObservableFloatArray та ObservableIntegerArray).

2.6 Робота з візуальними компонентами JavaFX

2.6.1 Загальні концепції

Компоненти графічного інтерфейсу користувача JavaFX представлені класами, які опосередковано походять від javafx.scene.Node. Цей клас реалізує базову поведінку компонентів, яка включає стандартні перетворення (transformations), такі як зсув (translation), обертання (rotation), масштабування (scaling) та зсув (shearing), а також підтримку стилізації. Похідний клас javafx.scene.Parent підтримує механізм створення ієрархії об'єктів і надає список дочірніх компонентів у вигляді списку (ObservableList). Клас javafx.scene.layout.Region є базовим для всіх візуальних компонентів JavaFX. Найбільш вживані похідні класи – javafx.scene.layout.Pane і Control. Клас Pane є базовим для всіх типів панелей. Клас Control є базовим для візуальних компонентів, які розташовують всередині панелей.

Один з підходів до проєктування графічного інтерфейсу користувача полягає в створенні у коді об'єктів необхідних типів та додаванні їх у відповідні контейнери програмним шляхом. Такий підхід провокує створення дуже великих класів, в яких буде змішано створення компонентів, обробку подій і обчислення. Альтернативний підхід до проєктування графічного інтерфейсу користувача – використання FXML.

2.6.2 Використання мови FXML для розмічення елементів графічного інтерфейсу користувача

Сучасні уявлення про проєктування графічного інтерфейсу користувача передбачають декларативний спосіб визначення складу вікон (фреймів, активностей) і властивостей візуальних компонентів. Найпопулярніший підхід – використання мови XML, яка забезпечує адекватний опис ієрархії візуальних об'єктів через механізм вкладених тегів і визначення властивостей компонентів через використання атрибутів. Різні варіанти мов, побудованих на XML, використовуються в Android, .NET (WPF) тощо.

Окрім переваг декларативного розмічення, використання FXML має велике значення з точки зору виконання вимог патерну проєктування Model-View-Controller. Головна ідея цього патерну полягає у відокремленні структур даних предметної області та алгоритмів їх обробки (модель, model) від засобів взаємодії з користувачем (вигляд, view) з видаленням залежностей між цими частинами. Контролер (controller) – це спеціальний модуль (клас), який забезпечує зв'язок між моделлю і виглядом через реалізацію обробки подій, які виникають під час взаємодії користувача з програмою і виклик функцій класів моделі. Архітектура застосунку JavaFX, яка спирається на використання FXML, включає представлення вигляду через файли FXML і стилів (*.css) і контролера – класу, код якого може бути згенеровано автоматично. До контролера додаються посилання на окремо розташовані класи моделі, які представляють сутності предметної області

Крім того, декларативна мова опису зовнішнього вигляду та елементів управління дозволяє залучити до проєктування GUI дизайнерів, для яких XML-подібна декларативна мова більш прийнятна, ніж мови програмування.

Перша версія JavaFX включала окрему скриптову мову JavaFX Script, яка дозволяла декларативно описувати компоненти користувацького інтерфейсу. Починаючи з другої версії JavaFX автори платформи відмовилися від JavaFX Script і додали до специфікації мову опису елементів користувацького інтерфейсу FXML, яка базується на XML. Використання мови FXML є не єдиним, але рекомендованим підходом.

Використання FXML автоматично передбачається під час створення нового проєкту JavaFX у середовищі IntelliJ IDEA.

2.6.3 Компонування в JavaFX

JavaFX надає механізми компонування візуальних компонентів (вузлів), багато в чому схожі на відповідні механізми бібліотеки javax.swing, але на відміну від Swing, елементи компонування є вузлами в дереві елементів користувацького інтерфейсу. Існує декілька типів стандартних контейнерів (панелей), які відрізняються правилами розташування і форматування дочірніх візуальних компонентів. У попередніх прикладах вже були використані контейнери BorderPane і FlowPane. Взагалі до стандартних контейнерів можна віднести такі:

  • Pane – найпростіша панель з абсолютним позиціюванням;
  • BorderPane працює аналогічно BorderLayout бібліотеки Swing; можна додати п'ять компонентів – відповідно зверху (top), знизу (bottom), ліворуч (left), праворуч (right) і по центру (center); в останньому випадку компонент намагається зайняти весь вільний простір;
  • FlowPane автоматично додає елементи у горизонтальний (вертикальний) ряд з продовженням у наступному рядку (стовпці);
  • HBox вишикує вузли у горизонтальний ряд;
  • VBox вишикує вузли у вертикальний ряд;
  • AnchorPane дозволяє прив'язати вузли до різних сторін контейнера, або до його центру, визначаючи відповідні відстані;
  • TilePane розташовує елементи в сітці з однаковими розмірами
  • StackPane поміщає кожен новий вузол поверх попереднього вузла; дозволяє створювати складені форми, які підтримують динамічну зміну свого вмісту;
  • GridPane дозволяє розташувати вузли в динамічній сітці, яка дозволяє об'єднувати сусідні комірки; за своїми можливостями нагадує GridBagLayout бібліотеки javax.swing.

Усі панелі підтримують властивість padding (відступ), панелі GridPane, HBox і VBox підтримують також властивість spacing – проміжок між дочірніми вузлами.

2.6.4 Використання елементів управління (controls). Обробка подій

Класи візуальних елементів, які використовують у JavaFX, походять від класу javafx.scene.layout.Region, вище якого за ієрархією є абстрактний клас javafx.scene.Parent, який походить від абстрактного класу javafx.scene.Node. Саме властивості цього класу визначають розміри й розташування компонентів на контейнері, можливість створення ієрархії об'єктів.

Більшість візуальних елементів управління за своїми іменами та базовою функціональністю схожі на відповідні компоненти бібліотеки javax.swing. На відміну від Swing, властивості елементів управління можна визначати не тільки в коді, але й у FXML-документі.

Наприклад, найпростіший елемент управління – кнопка, для створення якої слід застосувати клас javafx.scene.control.Button. Заголовок кнопки можна визначити у конструкторі, або встановити потім за допомогою методу setText(). Властивості можна визначити в коді функції або в FXML-документі.

Найбільш вживаними елементами користувацького інтерфейсу, визначеними в пакеті javafx.scene.control, є такі:

  • панелі ToolBar, Accordion, SplitPane, TabPane, ScrollPane, TitledPane, MenuBar
  • мітка Label
  • кнопки Button, MenuButton, SplitMenuButton, ToggleButton
  • текстові елементи TextField, TextArea, PasswordField
  • перемикачі CheckBox, RadioButton (використовують разом з групою ToggleGroup)
  • списки ChoiceBox, ComboBox, ListView
  • елементи меню Menu, ContextMenu, MenuItem
  • діалогові вікна вибору ColorPicker (вибір кольору), DatePicker (вибір дати)
  • таблиці TableView, TreeTableView
  • індикатор ProgressBar, ProgressIndicator
  • гіперпосилання Hyperlink
  • роздільник Separator
  • бігунок Slider
  • дерево TreeView.

Додаткові елементи можна знайти в пакетах javafx.scene.chart, javafx.scene.image, javafx.scene.media тощо. Пакет javafx.scene.shape забезпечує малювання геометричних форм.

Як практично всі бібліотеки графічного інтерфейсу користувача, JavaFX підтримує роботу з головним і з контекстним меню. Головне меню розташовують всередині компоненту javafx.scene.control.MenuBar. Для створення окремих підменю використовують клас javafx.scene.control.Menu. Окремі позиції меню можна створювати за допомогою класу javafx.scene.control.MenuItem.

Всі бібліотеки підтримки програм графічного інтерфейсу користувача Java SE підтримують схожі механізми обробки подій. Усі класи подій JavaFX походять від класу java.util.EventObject. Найбільш розповсюдженим є похідний клас ActionEvent – подія, пов'язана з основним використанням елементу управління (наприклад, натисканням кнопки). Для того, щоб обробити подію, слід спочатку зареєструвати оброблювач, викликавши метод setOnAction(). Параметром цього методу є об'єкт класу, який реалізує інтерфейс javafx.event.EventHandler<T extends Event>. Єдиний метод, який необхідно реалізувати – void handle(T eventObj). Оскільки цей інтерфейс є функціональним, для створення безіменних класів, які його реалізують, у більшості випадків використовують лямбда-вирази.

Іноді для тонкішої обробки події доцільно отримати її джерело (об'єкт, який ініціалізував подію). Цей об'єкт можна отримати, викликавши метод getSource(), визначений у класі java.util.EventObject.

Найкращий спосіб засвоєння роботи з візуальними компонентами й подіями – аналіз прикладів. У прикладі 3.2 показано роботу з текстовими полями й кнопкою, у прикладі 3.3 – робота з кнопками RadioButton, у прикладі 3.5 – також з меню та областю редагування тексту TextArea.

2.6.5 Дочірні та діалогові вікна

Як і інші бібліотеки для створення застосунків графічного інтерфейсу користувача, JavaFX надає засоби створення дочірніх вікон – вікон, які виникають після виконання певних дій в основному вікні застосунку. Зазвичай такі вікна містять елементи управління для здійснення діалогу з користувачем. Такі вікна мають назву діалогових.

Діалогові вікна – це вікна особливого типу, які дозволяють увести обмежену кількість даних, містять обмежену кількість елементів управління, дозволяють вибрати варіанти виконання дій, або інформують користувача. Діалогові вікна зазвичай відображаються тоді, коли програмі для подальшої роботи потрібна відповідь. На відміну від звичайних вікон, більшість діалогових вікон не можна розгорнути або згорнути, так само як і змінити їх розмір. Особливий тип діалогових вікон – так звані модальні діалогові вікна. Вони не дозволяють продовжувати роботу з застосунком доти, доки модальне вікно не буде зачинене. За допомогою модальних діалогових вікон здебільшого сповіщають користувача про деякі проміжні результати, показують попередження, повідомлення про помилки, уводять окремі рядки даних.

Для створення дочірнього вікна слід створити окремий об'єкт типу Stage. Для цього об'єкта слід викликати метод show(). Для об'єкта типу Stage модального вікна слід викликати функцію showAndWait(). Ця функція показує нове вікно і дозволяє повернутися в попереднє тільки після закриття нового вікна.

У JavaFX 8 (починаючи з версії JDK 8.4) для повідомлень надається клас javafx.scene.control.Alert, який дозволяє створювати стандартні діалогові вікна. Варіанти вікон визначаються у конструкторі за допомогою переліку javafx.scene.control.Alert.AlertType. Визначаються такі типи вікон:

  • AlertType.INFORMATION – стандартне інформаційне вікно з повідомленням;
  • AlertType.WARNING – вікно попередження з відповідною піктограмою;
  • AlertType.ERROR – вікно помилки з відповідною піктограмою;
  • AlertType.CONFIRMATION – вікно запитання з кнопками OK і Cancel (можна також додати інші кнопки).

Існують також спеціальні класи для введення тексту javafx.scene.control.TextInputDialog і javafx.scene.control.ChoiceDialog<T>, який дозволяє користувачеві вибрати зі списку елемент типу T. Усі перелічені класи походять від узагальненого класу javafx.scene.control.Dialog.

Модальні діалогові вікна дуже часто використовують для вибору файлів. Клас javafx.stage.FileChooser надає можливість вибору файлів для читання або запису. Наприклад, якщо поточні підмостки мають назву stage, так можна створити діалогове вікно вибору файлів для відкриття та отримати об'єкт типу java.io.File:

FileChooser chooser = new FileChooser();
File file;
// Показуємо вікно вибору файлів та перевіряємо, чи підтвердив користувач свій вибір:
if ((file = chooser.showOpenDialog(stage)) != null) { 
    // Читаємо з файлу
}

Параметр функцій showOpenDialog() і showSaveDialog() – вікно, по центру якого розташовується діалогове вікно роботи з файлом. Якщо вказано null, діалогове вікно відображається по центру екрана. Для з'ясування механізмів використання класу FileChooser та подій можна скористатися прикладом 3.4.

2.7 Робота з табличними даними в JavaFX

Робота з табличними даними здійснюється за допомогою компоненту TableView. Основна робота цього компоненту – відображення властивостей об'єктів, які зберігаються в списку типу ObservableList. Використання TableView розглянемо на прикладі. Припустимо, створено клас City:

package ua.inf.iwanoff.java.advanced.fourth;

public class City {
    private String name;
    private int population;

    public City(String name, int population) {
        this.name = name;
        this.population = population;
    }

    public String getName() {
        return name;
    }

    public int getPopulation() {
        return population;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setPopulation(int population) {
        this.population = population;
    }
}

Необхідно створити й заповнити список міст і відобразити цей список у головному вікні застосунку.

Створюємо новий застосунок JavaFX. У класі Main ініціалізуємо декілька об'єктів типу City, створюємо таблицю, додаємо колонки, зв'язуємо їх з властивостями класу City і додаємо таблицю до головного вікна. Вихідний код буде таким:

package ua.inf.iwanoff.java.advanced.fourth;
    
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.BorderPane;

public class Main extends Application {

    @Override
    public void start(Stage primaryStage) {
        // Заповнюємо список міст:
        ObservableList<City> list = FXCollections.observableArrayList(
            new City("Харків", 1_451_132), 
            new City("Полтава", 295_950),
            new City("Київ", 2_868_702)
        );
        
        try {
            primaryStage.setTitle("Міста");
            BorderPane root = new BorderPane();
            
            // Створюємо таблицю і додаємо до неї колонки:
            TableView<City> table = new TableView<>();
            table.setItems(list);
            table.getColumns().clear();
            // Колонку columnName зв'язуємо з властивістю name:
            TableColumn<City, String> columnName = new TableColumn<>("Назва");
            columnName.setCellValueFactory(new PropertyValueFactory<>("name"));
            // Колонку columnPopulation зв'язуємо з властивістю population:
            TableColumn<City, Integer> columnPopulation = new TableColumn<>("Населення");
            columnPopulation.setCellValueFactory(new PropertyValueFactory<>("population"));
            // Додаємо колонки до таблиці:            
            table.getColumns().add(columnName);
            table.getColumns().add(columnPopulation);
            // Додаємо таблицю в центр панелі:
            root.setCenter(table);
            
            Scene scene = new Scene(root, 300, 200);
            primaryStage.setScene(scene);
            primaryStage.show();
        } 
        catch(Exception e) {
            e.printStackTrace();
        }
    }
    
    static void main(String[] args) {
        launch(args);
    }
}

Слід зауважити, що натисканням на заголовки відповідних колонок таблиці можна автоматично здійснити сортування даних за відповідною ознакою.

У багатьох застосунках табличні дані необхідно редагувати. Редагування вимагає внесення деяких змін у структуру програми, додавання нових візуальних компонентів.

Нова структура програми передбачає перенесення списку міст з локальної видимості функції start() у множину полів класу Main. Це можна здійснити за допомогою рефакторингу. Код ініціалізації списку, який залишився всередині методу start(), можна перенести в окремий метод також за допомогою функцій рефакторингу. Метод додаємо до опису класу, а його виклик додається у коді функції start(), де раніше здійснювалася ініціалізація списку. Аналогічно локальну змінну table слід перетворити на поле, а заповнення таблиці слід винести в окрему функцію, наприклад, initTable(). Тепер код класу Main має такий вигляд:

package ua.inf.iwanoff.java.advanced.fourth;
    
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.BorderPane;

public class Main extends Application {   
    private ObservableList<City> list;
    private TableView<City> table;

    @Override
    public void start(Stage primaryStage) {
        initList();        
        try {
            primaryStage.setTitle("Міста");
            BorderPane root = new BorderPane();
            initTable();
            root.setCenter(table);
            Scene scene = new Scene(root, 300, 200);
            primaryStage.setScene(scene);
            primaryStage.show();
        } 
        catch(Exception e) {
            e.printStackTrace();
        }
    }

    private void initTable() {
        table = new TableView<>();
        table.setItems(list);
        table.getColumns().clear();
        // Колонку columnName зв'язуємо з властивістю name:
        TableColumn<City, String> columnName = new TableColumn<>("Назва");
        columnName.setCellValueFactory(new PropertyValueFactory<>("name"));
        // Колонку columnPopulation зв'язуємо з властивістю population:
        TableColumn<City, Integer> columnPopulation = new TableColumn<>("Населення");
        columnPopulation.setCellValueFactory(new PropertyValueFactory<>("population"));
        // Додаємо колонки до таблиці:            
        table.getColumns().add(columnName);
        table.getColumns().add(columnPopulation);
    }

    private void initList() {
        list = FXCollections.observableArrayList(
            new City("Харків", 1_451_132), 
            new City("Полтава", 295_950),
            new City("Київ", 2_868_702)
        );
    }
    
    static void main(String[] args) {
        launch(args);
    }
}

До вікна слід додати кнопку для перезавантаження вихідних даних. Можна також додати кнопку, натиснення якої забезпечує додавання нового порожнього рядка таблиці для його подальшого заповнення. Відповідні кнопки можна розташувати зверху і знизу вікна. Код додавання кнопок всередині методу start() може бути таким:

    Button buttonReload = new Button("Перезавантажити дані");
    buttonReload.setMaxWidth(Double.MAX_VALUE);
    buttonReload.setOnAction(event -> reload());
    root.setTop(buttonReload);
    Button buttonAddCity = new Button("Додати місто");
    buttonAddCity.setMaxWidth(Double.MAX_VALUE);
    buttonAddCity.setOnAction(event -> addCity());
    root.setBottom(buttonAddCity);

У наведеному коді кожна з кнопок створюється з текстом, визначеним у конструкторі. Встановлення максимальної ширини в значення Double.MAX_VALUE забезпечує розтягування кнопки на всю ширину вікна. Параметри функцій setOnAction() – лямбда-вирази, в яких здійснюється виклик окремо реалізованих функцій:

    private void reload() {
        initList();
        initTable();
    }

    private void addCity() {
        list.add(new City("", 0));
        initTable();
    }

Кнопки додаються до панелі відповідно зверху і знизу.

Редагування таблиці здійснюється через використання можливостей редагування, які надають інші візуальні компоненти, наприклад, TextField. Клас javafx.scene.control.cell.TextFieldTableCell дозволяє працювати з коміркою таблиці як з полем текстового введення. Виклик методу setCellFactory() для певної колонки перевизначає механізм здійснення маніпуляцій з комірками колонки. Крім того, слід додати визначення функції зворотно виклику, яка обробляє введені (змінені) дані.

Увесь текст програми може мати такий вигляд:

package ua.inf.iwanoff.java.advanced.fourth;

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.BorderPane;
import javafx.util.converter.IntegerStringConverter;

public class Main extends Application {
    private ObservableList<City> list;
    private TableView<City> table;

    @Override
    public void start(Stage primaryStage) {
        initList();
        try {
            primaryStage.setTitle("Міста");
            BorderPane root = new BorderPane();
            table = new TableView<>();
            initTable();
            root.setCenter(table);
            Button buttonReload = new Button("Перезавантажити дані");
            buttonReload.setMaxWidth(Double.MAX_VALUE);
            buttonReload.setOnAction(event -> reload());
            root.setTop(buttonReload);
            Button buttonAddCity = new Button("Додати місто");
            buttonAddCity.setMaxWidth(Double.MAX_VALUE);
            buttonAddCity.setOnAction(event -> addCity());
            root.setBottom(buttonAddCity);
            Scene scene = new Scene(root, 300, 200);
            primaryStage.setScene(scene);
            primaryStage.show();
        }
        catch(Exception e) {
            e.printStackTrace();
        }
    }

    private void reload() {
        initList();
        initTable();
    }

    private void addCity() {
        list.add(new City("", 0));
        initTable();
    }

    private void initTable() {
        table.setItems(list);
        table.getColumns().clear();
        table.setEditable(true);
        // Колонку columnName зв'язуємо з властивістю name:
        TableColumn<City, String> columnName = new TableColumn<>("Назва");
        columnName.setCellValueFactory(new PropertyValueFactory<>("name"));
        columnName.setCellFactory(TextFieldTableCell.forTableColumn());
        columnName.setOnEditCommit(t ->
                ((City) t.getTableView().getItems().get(t.getTablePosition().getRow())).
                        setName(t.getNewValue()));
        // Колонку columnPopulation зв'язуємо з властивістю population:
        TableColumn<City, Integer> columnPopulation = new TableColumn<>("Населення");
        columnPopulation.setCellValueFactory(new PropertyValueFactory<>("population"));
        columnPopulation.setCellFactory(TextFieldTableCell.<City, Integer>forTableColumn(
                new IntegerStringConverter()));
        columnPopulation.setOnEditCommit(t ->
                ((City) t.getTableView().getItems().get(t.getTablePosition().getRow())).
                        setPopulation(t.getNewValue()));

        // Додаємо колонки до таблиці:
        table.getColumns().add(columnName);
        table.getColumns().add(columnPopulation);
    }

    private void initList() {
        list = FXCollections.observableArrayList(
                new City("Харків", 1_451_132),
                new City("Полтава", 295_950),
                new City("Київ", 2_868_702)
        );
    }

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

2.8 Візуальне проєктування програм графічного інтерфейсу користувача

Для візуального редагування вікон і компонентів JavaFX різні середовища використовують окремий застосунок – Scene Builder. Цей програмний продукт надається Oracle і його можна завантажити з сайту https://www.oracle.com/java/technologies/javafxscenebuilder-1x-archive-downloads.html. Завантажуємо програму інсталяції, погоджуємося з умовами ліцензії, обираємо версію 2 для своєї операційної системи та встановлюємо застосунок.

В середовищі IntelliJ IDEA слід в установках IntelliJ IDEA встановити шлях до застосунку JavaFX Scene Builder; для цього у вікні налаштувань на закладці Languages & Frameworks | JavaFX вибрати шлях до програми, наприклад C:\Program Files\Oracle\JavaFX Scene Builder 2.0\JavaFX Scene Builder 2.0.exe.

Завантажити редактор можна через контекстне меню, пов'язане з FXML-документом (функція Open in SceneBuilder в IntelliJ IDEA). Головне вікно редактору складається з трьох колонок: ієрархія компонентів у лівій частині вікна (Library), головне підвікно редагування сцени й підвікно редагування властивостей у правій частині вікна (Inspector).

У колонках ліворуч розташовані закладки (вкладки) відповідно до груп компонентів, праворуч – вкладки Properties, Layout і Code. На форму можна перетягувати компоненти й визначати їх властивості. Частину властивостей можна змінювати безпосередньо мишкою (перетягувати, міняти розмір і ін.), частину можна задати у вкладках Properties (спеціальні властивості) і Layout (розташування). У вкладці Code можна, якщо треба, визначити ім'я об'єкта (fx:id), а також для кожного компонента можна задати реакцію на події.

У режимі попереднього перегляду (Preview | Show Preview in Window) можна взаємодіяти з макетом з метою перевірки функціональності майбутнього коду.

2.9 Використання потоків виконання у JavaFX-застосунках

Робота застосунків графічного інтерфейсу користувача зазвичай пов'язана з потоками виконання. Після завантаження JavaFX-застосунку на виконання автоматично створюється потік застосунку (application thread), в якому здійснюється обробка подій. Тільки цей потік може мати взаємодію з вікнами й візуальними компонентами. Спроба двох потоків одночасно керувати зовнішнім виглядом компонентів та інформацією у вікні може призвести до хаотичного вигляду й непередбачуваної поведінки візуальних компонентів. Разом з тим, саме завдяки багатопотоковості можна забезпечити керованість застосунком. Крім того, доцільно деякі дії, які виконуються тривалий час, запускати в окремому потоці виконання, що забезпечить можливість відстежувати процес, призупиняти й продовжувати його, змінювати налаштування тощо.

Засоби JavaFX надають можливість виконувати код в потоці виконання, який відповідає за отримання й обробку подій. Метод javafx.application.Platform.runLater() отримує об'єкт, який реалізує функціональний інтерфейс Runnable. Цей об'єкт отримує потік обробки подій, де об'єкт стає в чергу і, коли виникає можливість, виконується його метод run(). Саме в цьому методі доцільно розташувати взаємодію з візуальними компонентами. Приклад 3.3 демонструє можливість виклику цього методу з дочірнього потоку.

3 Приклади програм

3.1 Робота з ObservableList

Припустимо, в деякому списку цілих чисел слід розташувати спочатку непарні, потім парні числа зі збереженням відносного порядку розташування чисел. Можна запропонувати алгоритм, в якому парне число, яке ми знайшли, видаляється зі списку і додається в і кінці списку. Важливо створити два лічильники (i та j), щоб не переносити числа декілька разів.

Програма матиме вигляд:

package ua.inf.iwanoff.java.advanced.fourth;

import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;

public class ObservableListDemo {
    public static void orderList(ObservableList<Integer> list) {
        for (int i = 0, j = 0; j < list.size(); j++) {
            if (list.get(i) % 2 == 0) {
                list.add(list.get(i));
                list.remove(i);
            }
            else {
                i++;
            }
        }
    }

    static void main() {
        ObservableList<Integer> list = FXCollections.observableArrayList();
        list.addListener((ListChangeListener<? super Integer>) c -> System.out.println(list));
        list.addAll(1, 12, 2, 37, 6, 8, 11);
        orderList(list);
        list.clear();
    }
}

Текст у консольному вікні буде таким:

[1, 12, 2, 37, 6, 8, 11]
[1, 12, 2, 37, 6, 8, 11, 12]
[1, 2, 37, 6, 8, 11, 12]
[1, 2, 37, 6, 8, 11, 12, 2]
[1, 37, 6, 8, 11, 12, 2]
[1, 37, 6, 8, 11, 12, 2, 6]
[1, 37, 8, 11, 12, 2, 6]
[1, 37, 8, 11, 12, 2, 6, 8]
[1, 37, 11, 12, 2, 6, 8]
[]

3.2 Текстові поля і кнопки

Припустимо, необхідно створити програму графічного інтерфейсу користувача, у якій у двох рядках уведення користувач задає два цілих числа і після натискання кнопки одержує в третьому рядку введення суму цих чисел.

Кореневим контейнером нашого застосунку буде FlowPane. Отже, досить створити три текстових поля й одну кнопку і послідовно додати їх до панелі. Одержимо таку програму:

package ua.inf.iwanoff.java.advanced.fourth;

import javafx.application.Application;
import javafx.event.Event;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.layout.FlowPane;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.stage.Stage;

public class TextFieldsAndButton extends Application {
    private Button button;
    private TextField field1, field2, field3;
    
    @Override
    public void start(Stage stage) throws Exception {
        stage.setTitle("Сума");
        FlowPane rootNode = new FlowPane(10, 10);// визначаємо розміри горизонтального і
                                                 // вертикального проміжків між елементами
        rootNode.setAlignment(Pos.CENTER);
        Scene scene = new Scene(rootNode, 200, 200); // розміри вікна
        stage.setScene(scene);
        button = new Button("Знайти суму");   // визначаємо напис на кнопці
        button.setOnAction(this::buttonClick);// визначаємо функцію, яка обробляє подію
        field1 = new TextField(); 
        field2 = new TextField();
        field3 = new TextField();
        rootNode.getChildren().addAll(field1, field2, button, field3);
        stage.show();
    }

    private void buttonClick(Event event) {
        try {
            int i = Integer.parseInt(field1.getText());
            int j = Integer.parseInt(field2.getText());
            int k = i + j;
            field3.setText(k + "");
        }
        catch (NumberFormatException e1) {
            Alert alert = new Alert(AlertType.ERROR);
            alert.setTitle("Помилка");
            alert.setHeaderText("Хибні дані!");
            alert.showAndWait();
        }
    }
    
    static void main(String[] args) {
        launch(args);
    }
}

3.3 Робота з кнопками RadioButton

У наведеному нижче прикладі одночасно з вибором кнопки RadioButton у мітці (Label) відображається текст вибраної кнопки. Для того, щоб робота кнопок була узгодженою, їх об'єднують у групу ToggleGroup:

package ua.inf.iwanoff.java.advanced.fourth;

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.RadioButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;


public class ToggleGroupDemo extends Application {
    private Label label = new Label("No button");

    @Override
    public void start(Stage primaryStage) throws Exception {
        primaryStage.setTitle("Toggle Group Demo");

        RadioButton radioButtonFirst = new RadioButton("First");
        radioButtonFirst.setOnAction(this::showButtonText);
        RadioButton radioButtonSecond = new RadioButton("Second");
        radioButtonSecond.setOnAction(this::showButtonText);
        RadioButton radioButtonThird = new RadioButton("Third");
        radioButtonThird.setOnAction(this::showButtonText);

        ToggleGroup radioGroup = new ToggleGroup();
        radioButtonFirst.setToggleGroup(radioGroup);
        radioButtonSecond.setToggleGroup(radioGroup);
        radioButtonThird.setToggleGroup(radioGroup);
        VBox vbox = new VBox(radioButtonFirst, radioButtonSecond, radioButtonThird, label);
        vbox.setSpacing(10);
        vbox.setPadding(new Insets(10, 10, 10, 10));
        Scene scene = new Scene(vbox, 150, 120);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private void showButtonText(ActionEvent actionEvent) {
        label.setText(((RadioButton)actionEvent.getSource()).getText());
    }

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

3.4 Робота з діалоговими вікнами вибору файлів

Припустимо, необхідно прочитати з текстового файлу два числа, а в інший текстовий файл записати їх суму. Вікно міститиме дві кнопки – для вибору відповідно вихідного і вислідного файлів. Можна запропонувати таку програму:

package ua.inf.iwanoff.java.advanced.fourth;

import java.io.File;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.util.Scanner;

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.layout.FlowPane;
import javafx.stage.FileChooser;
import javafx.stage.Stage;

public class SumWriter extends Application {
    private double a, b;
    private FileChooser chooser;
    private File file;
    
    @Override
    public void start(Stage stage) throws Exception {
        stage.setTitle("Сума");
        FlowPane rootNode = new FlowPane(10, 10);// визначаємо розміри горизонтального і
                                                 // вертикального розривів між елементами
        rootNode.setAlignment(Pos.CENTER);
        Scene scene = new Scene(rootNode, 200, 100); // розміри вікна
        stage.setScene(scene);
        Button buttonOpen = new Button("Завантажити дані");   // визначаємо напис на кнопці
        buttonOpen.setOnAction(this::buttonOpenClick);// визначаємо функцію, яка обробляє подію
        Button buttonSave = new Button("Зберегти суму");   // визначаємо напис на кнопці
        buttonSave.setOnAction(this::buttonSaveClick);// визначаємо функцію, яка обробляє подію
        rootNode.getChildren().addAll(buttonOpen, buttonSave);
        chooser = new FileChooser();
        chooser.setInitialDirectory(new File("."));
        stage.show();
    }

    private void buttonOpenClick(ActionEvent event) {
        if ((file = chooser.showOpenDialog(null)) != null) {
            readFromFile();
        }
    }

    private void buttonSaveClick(ActionEvent event) {
        if ((file = chooser.showSaveDialog(null)) != null) {
            writeToFile();
        }
    }

    private void readFromFile() {
        try (Scanner scanner = new Scanner(file)) {
            a = scanner.nextDouble();
            b = scanner.nextDouble();
        }
        catch (Exception e) {
            Alert alert = new Alert(AlertType.ERROR);
            alert.setTitle("Помилка");
            alert.setHeaderText("Помилка читання з файлу!");
            alert.showAndWait();
        }
    }

    private void writeToFile() {       
        try (PrintWriter out = new PrintWriter(new FileWriter(file))) {
            out.println(a + b);
            out.close();
        }
        catch (Exception e) {
            Alert alert = new Alert(AlertType.ERROR);
            alert.setTitle("Помилка");
            alert.setHeaderText("Не можна створити файл!");
            alert.showAndWait();
        }
    }
  
    static void main(String[] args) {
        launch(args);
    }
}

3.5 Створення застосунку графічного інтерфейсу користувача для обчислення і відображення простих чисел

Припустимо, необхідно розробити програму отримання простих чисел у діапазоні від 1 до певного значення, яке може бути досить великим. Для знаходження простих чисел будемо застосовувати найпростіший алгоритм послідовної перевірки усіх чисел від 2 до квадратного кореня з числа, що перевіряється. Така перевірка може вимагати багато часу. Для створення зручного інтерфейсу користувача доцільно використовувати окремий потік виконання, у якому здійснюється перевірка. Завдяки цьому можна буде призупиняти та відновлювати пошук, виконувати стандартні маніпуляції з вікном, включаючи його закриття до завершення процесу пошуку.

Кореневою панеллю нашого застосунку буде BorderPane. Сирцевий код JavaFX-застосунку буде таким:

package ua.inf.iwanoff.java.advanced.fourth;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import java.io.IOException;

public class PrimeApp extends Application {

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

    @Override
    public void start(Stage primaryStage) {
        try {
            BorderPane root = (BorderPane)FXMLLoader.load(getClass().getResource("PrimeAppWindow.fxml"));
            Scene scene = new Scene(root, 700, 500);
            primaryStage.setScene(scene);
            primaryStage.setTitle("Прості числа");
            primaryStage.show();
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Інтерфейс користувача включатиме верхню панель (HBox), до якої послідовно додано мітку з текстом "До:", поле введення тексту textFieldTo, а також чотири кнопки buttonStart, buttonSuspend, buttonResume і buttonStop з текстом відповідно "Стартувати", "Призупинити","Продовжити" і "Завершити". На початку виконання програми лише кнопка buttonStart буде доступною. Середню частину займатиме область введення textAreaResults, для якої вимкнене редагування, нижню частину займатиме компонент progressBar, у якому відображатиметься доля виконання процесу пошуку простих чисел. Файл FXML-документа буде таким:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextArea?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>

<?import javafx.scene.control.ProgressBar?>
<BorderPane prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.121"
            xmlns:fx="http://javafx.com/fxml/1" fx:controller="ua.inf.iwanoff.java.advanced.fourth.PrimeAppController">
    <top>
        <HBox prefHeight="3.0" prefWidth="600.0" spacing="10.0" BorderPane.alignment="CENTER">
            <padding>
                <Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
            </padding>
            <Label text="До:"/>
            <TextField fx:id="textFieldTo" text="1000"/>
            <Button fx:id="buttonStart" minWidth="80.0" text="Стартувати" onAction="#startClick"/>
            <Button fx:id="buttonSuspend" minWidth="80.0" disable="true" text="Призупинити" onAction="#suspendClick"/>
            <Button fx:id="buttonResume" minWidth="80.0" disable="true" text="Продовжити" onAction="#resumeClick"/>
            <Button fx:id="buttonStop" minWidth="80.0" disable="true" text="Завершити" onAction="#stopClick"/>
        </HBox>
    </top>
    <center>
        <TextArea fx:id="textAreaResults" editable="false" wrapText="true" BorderPane.alignment="CENTER"/>
    </center>
    <bottom>
        <ProgressBar fx:id="progressBar" maxWidth="Infinity" progress="0"/>
    </bottom>
</BorderPane>
      

Можна згенерувати порожній клас контролера, додати до нього поля, пов'язані з візуальними компонентами й методи обробки подій:

package ua.inf.iwanoff.java.advanced.fourth;

import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.*;

public class PrimeAppController {
    @FXML private TextField textFieldTo;
    @FXML private Button buttonStart;
    @FXML private Button buttonSuspend;
    @FXML private Button buttonResume;
    @FXML private Button buttonStop;
    @FXML private ProgressBar progressBar;
    @FXML private TextArea textAreaResults;

    @FXML
    private void startClick(ActionEvent actionEvent) {
        
    }

    @FXML
    private void suspendClick(ActionEvent actionEvent) {
        
    }

    @FXML
    private void resumeClick(ActionEvent actionEvent) {
        
    }

    @FXML
    private void stopClick(ActionEvent actionEvent) {
        
    }
}

Після завантаження на виконання вікно програми буде таким:

Тепер окремо від засобів графічного інтерфейсу користувача можна створити клас, який відповідатиме за обчислення простих чисел в окремому потоці. Клас PrimeNumbers реалізуватиме інтерфейс Runnable. Основні "робочі" функції (start(), suspend(), resume() і stop()), які викликатимуться з контролера, змінюють стан об'єкта (поля suspended і stopped). Для забезпечення гнучкості програмі здійснюється оновлення даних про прості числа і відсоток виконання процесу через механізм зворотного виклику – передбачені поля класу типу інтерфейсу Runnable. Для того, щоб виконання методів run() здійснювалося б в іншому потоці, ці функції викликаються за допомогою Platform.runLater(). Сирцевий код класу PrimeNumbers матиме такий вигляд:

package ua.inf.iwanoff.java.advanced.fourth;

import javafx.application.Platform;

public class PrimeNumbers implements Runnable {
    private Thread primesThread;     // нитка обчислення простих чисел
    private int to;                  // кінець діапазону обчислення простих чисел
    private int lastFound;           // останнє знайдене просте число
    private Runnable displayFunc;    // функція, яка викликається для виведення знайденого числа
    private Runnable percentageFunc; // функція, яка оновлює кількість відсотків виконаного процесу
    private Runnable finishFunc;     // функція, яка викликається після закінчення
    private double percentage;
    private boolean suspended;
    private boolean stopped;

    public PrimeNumbers(Runnable addFunc, Runnable percentageFunc, Runnable finishFunc) {
        this.displayFunc = addFunc;
        this.percentageFunc = percentageFunc;
        this.finishFunc = finishFunc;
    }

    public int getTo() {
        return to;
    }

    public void setTo(int to) {
        this.to = to;
    }

    public synchronized int getLastFound() {
        return lastFound;
    }

    private synchronized void setLastFound(int lastFound) {
        this.lastFound = lastFound;
    }

    public synchronized double getPercentage() {
        return percentage;
    }

    private synchronized void setPercentage(double percentage) {
        this.percentage = percentage;
    }

    public synchronized boolean isSuspended() {
        return suspended;
    }

    private synchronized void setSuspended(boolean suspended) {
        this.suspended = suspended;
    }

    public synchronized boolean isStopped() {
        return stopped;
    }

    private synchronized void setStopped(boolean stopped) {
        this.stopped = stopped;
    }

    @Override
    public void run() {
        for (int n = 2; n <= to; n++)
        {
            try {
                setPercentage(n * 1.0 / to);
                // Оновлюємо кількість відсотків:
                if (percentageFunc != null) {
                    Platform.runLater(percentageFunc);
                }
                boolean prime = true;
                for (int i = 2; i * i <= n; i++) {
                    if (n % i == 0) {
                        prime = false;
                        break;
                    }
                }
                Thread.sleep(20);
                if (prime) {
                    setLastFound(n);
                    // Відображаємо знайдене просте число:
                    if (displayFunc != null) {
                        Platform.runLater(displayFunc);
                    }
                }
            }
            catch (InterruptedException e) {
                // залежно від стану об'єкта чекаємо на продовження або завершуємо пошук:
                while (isSuspended()) {
                    try {
                        Thread.sleep(100);
                    }
                    catch (InterruptedException e1) {
                        // Перервали у стані очікування:
                        if (isStopped()) {
                            break;
                        }
                    }
                }
                if (isStopped()) {
                    break;
                }
            }
        }
        if (finishFunc != null) {
            Platform.runLater(finishFunc);
        }
    }

    public void start() {
        primesThread = new Thread(this);
        setSuspended(false);
        setStopped(false);
        primesThread.start();
    }

    public void suspend() {
        setSuspended(true);
        primesThread.interrupt();
    }

    public void resume() {
        setSuspended(false);
        primesThread.interrupt();
    }

    public void stop() {
        setStopped(true);
        primesThread.interrupt();
    }
}

Як видно з коду, методи доступу до даних реалізовані з модифікатором synchronized, що унеможливлює одночасний запис і читання недописаних даних з різних потоків. Тому всі звертання до полів класу з його методів здійснюються тільки через функції доступу.

Тепер можна реалізувати код контролера:

package ua.inf.iwanoff.java.advanced.fourth;

import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.*;

public class PrimeAppController {
    @FXML private TextField textFieldTo;
    @FXML private Button buttonStart;
    @FXML private Button buttonSuspend;
    @FXML private Button buttonResume;
    @FXML private Button buttonStop;
    @FXML private ProgressBar progressBar;
    @FXML private TextArea textAreaResults;

    private PrimeNumbers primeNumbers = new PrimeNumbers(this::addToTextArea, this::setProgress, this::finish);

    @FXML
    private void startClick(ActionEvent actionEvent) {
        try {
            primeNumbers.setTo(Integer.parseInt(textFieldTo.getText()));
            textAreaResults.setText("");
            progressBar.setProgress(0);
            buttonStart.setDisable(true);
            buttonSuspend.setDisable(false);
            buttonResume.setDisable(true);
            buttonStop.setDisable(false);
            primeNumbers.start();
        } catch (NumberFormatException e) {
            Alert alert = new Alert(Alert.AlertType.ERROR);
            alert.setTitle("Помилка");
            alert.setHeaderText("Хибний діапазон!");
            alert.showAndWait();

        }
    }

    @FXML
    private void suspendClick(ActionEvent actionEvent) {
        primeNumbers.suspend();
        buttonStart.setDisable(true);
        buttonSuspend.setDisable(true);
        buttonResume.setDisable(false);
        buttonStop.setDisable(false);
    }

    @FXML
    private void resumeClick(ActionEvent actionEvent) {
        primeNumbers.resume();
        buttonStart.setDisable(true);
        buttonSuspend.setDisable(false);
        buttonResume.setDisable(true);
        buttonStop.setDisable(false);
    }

    @FXML
    private void stopClick(ActionEvent actionEvent) {
        primeNumbers.stop();
    }

    private void addToTextArea() {
        textAreaResults.setText(textAreaResults.getText() + primeNumbers.getLastFound() + " ");
    }

    private void setProgress() {
        progressBar.setProgress(primeNumbers.getPercentage());
    }

    private void finish() {
        buttonStart.setDisable(false);
        buttonSuspend.setDisable(true);
        buttonResume.setDisable(true);
        buttonStop.setDisable(true);
    }
}

Методи addToTextArea(), setProgress() і finish() призначені не для безпосереднього виклику з контролера, а для зворотного виклику. Посилання на ці функції передаються в конструктор об'єкта типу PrimeNumbers. Оброблювачі подій викликають відповідні методи класу PrimeNumbers і змінюють доступність кнопок.

3.6 Програма графічного інтерфейсу користувача для роботи з даними про переписи населення

Припустимо, необхідно створити програму графічного інтерфейсу користувача, в якій користувачеві надається можливість уведення та редагування даних про переписи населення, зберігання даних в XML-файлі, читання даних з раніше створених XML-файлів, редагування даних, пошук даних за певною ознакою, сортування даних, зберігання даних у новому файлі.

До створеного раніше проєкту додаємо новий пакет, у якому будуть розташовані класи графічного інтерфейсу користувача, зокрема головний клас-контролер, FXML-документи та додаткові контролери.

Створюємо новий клас з ім'ям CensusesFX. Його вміст буде таким:

package ua.inf.iwanoff.java.advanced.fourth;

import javafx.application.Application;
import javafx.stage.Stage;

public class CensusesFX extends Application {

    @Override
    public void start(Stage primaryStage) {
        
    }

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

Також до проєкту слід додати новий FXML-документ з ім'ям CensusesForm.fxml. Для того, щоб після компіляції цей файл було скопійовано в необхідне місце, його слід розташувати у теці src\main\resources\ua\inf\iwanoff\java\advanced\fourth (відносно теки проєкту). Вміст файлу може бути таким:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.layout.BorderPane?>

<BorderPane xmlns:fx="http://javafx.com/fxml/1">
    <!-- TODO Add Nodes -->
</BorderPane>

Додаємо до класу посилання на FXML-документ і створюємо сцену (відповідний код можна скопіювати з коду попередніх прикладів):

package ua.inf.iwanoff.java.advanced.fourth;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class CensusesFX extends Application {

    @Override
    public void start(Stage primaryStage) {
        try {
            BorderPane root = (BorderPane)FXMLLoader.load(getClass().getResource("CensusesForm.fxml"));
            Scene scene = new Scene(root, 700, 500);
            primaryStage.setScene(scene);
            primaryStage.setTitle("Переписи населення");
            primaryStage.show();
        }
        catch(Exception e) {
            e.printStackTrace();
        }
    }

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

Слід також додати клас-контролер:

package ua.inf.iwanoff.java.advanced.fourth;

public class CensusesController {

}

Подальша робота передбачає додавання і налагодження візуальних компонентів за допомогою застосунку SceneBuilder. Завантажуємо застосунок за допомогою контекстного меню файлу CensusesForm.fxml у Package Explorer (Open with SceneBuilder). Спочатку додаємо посилання на клас-контролер. На закладці Controller у лівій частині вікна SceneBuilder у рядку ControllerClass вводимо ім'я класу-контролера, вказуючи всі вкладені пакети. В нашому випадку це рядок ua.inf.iwanoff.java.advanced.fourth.CensusesController.

Додаємо головне меню. На кладці Controls в лівій частині вікна (палітрі) знаходимо компонент MenuBar і додаємо його в верхній частині кореневого контейнера (позиція insert TOP у підвікні ієрархії об'єктів). Головне меню, яке ми додали, вже містить при підменю File (с позицією Close), Edit (с позицією Delete) і Help (с позицією About). З закладки Menu можна додати нові підменю (Menu) і окремі позиції (MenuItem). В нашому випадку текст позицій меню можна перекласти з англійської, до головного меню слід також додати підменю "Робота" и отримати таке головне меню:

  • підменю "Файл" з позиціями "Новий", "Відкрити...", "Зберегти..." і "Вийти";
  • підменю "Редагування" з позиціями "Додати рядок" і "Видалити останній рядок";
  • підменю "Робота" з позиціями "Сортувати за кількістю населення" і "Сортувати за алфавітом коментарів";
  • підменю "Допомога" з позицією "Про програму...".

Між позиціями "Зберегти..." і "Вийти" можна додати розділювач (компонент Separator).

В лівій частині кореневої панелі слід розташувати ще одну панель (AnchorPane) для розміщення елементів управління – кнопок, рядків виведення, а також області відображення результатів пошуку. Зокрема, слід додати мітку (Label) з написом "Текст для пошуку:", рядок TextField для введення слів (послідовності літер) для пошуку в коментарях, а також відповідні кнопки з текстом "Шукати слово" і "Шукати послідовність літер". В нижній частині доданої панелі розміщуємо компонент TextArea для виведення результатів пошуку. Компонентам TextField і TextArea слід визначити імена, відповідно textFieldCountry, textFieldArea, textFieldText і textAreaResults, оскільки до вмісту цих компонентів необхідно звертатися в програмі.

Для області виведення результатів textAreaResults слід явно вказати значення властивості AnchorPane.bottomAnchor, що забезпечить автоматичну зміну висоти у випадку зміни розмірів головного вікна. Встановити цю властивість можна безпосередньо в тексті FXML-документа або за допомогою SceneBuilder, визначивши необхідну величину в нижньому текстовому полі символічного зображення Anchor Pane Constraints у закладці Layout у правій частині вікна.

В центральній частині кореневої панелі розташовуємо компонент TableView, в якому здійснюватиметься відображення і редагування даних про переписи населення. Від початку такий компонент містить дві колонки (TableColumn) з заголовками "C1" і "C2". Цей текст слід змінити на "Рік" і "Населення". До таблиці слід додати ще дві колонки ("Щільність населення" і "Коментарі"). Таблиці й колонкам теж слід визначити імена, відповідно tableViewCensuses, tableColumnYear, tableColumnPopulation, tableColumnDensity і tableColumnComments. Для всієї таблиці (tableViewCensuses), а також для всіх колонок, крім tableColumnDensity, властивість editable слід встановити в true, а для tableColumnDensity – у false. У режимі попереднього перегляду (Preview | Show Preview in Window) редактору SceneBuilder майбутнє головне вікно застосунку матиме такий вигляд:

Тепер можна перейти до проєктування контролеру. Раніше створені класи консольного застосунку виконуватимуть роль моделі. Посилання на клас CountryWithArrayList можна додати до класу-контролера. Іменованим компонентам повинні відповідати приватні поля, які можна згенерувати автоматично. Механізм генерації коду за допомогою функції контекстного меню Quick Fix був описаний раніше.

Слід також додати методи-оброблювачі подій, пов'язаних з позиціями меню, кнопками й зміною вмісту рядків тексту. Для додавання події створюємо відповідні функції з анотацією і параметром типу ActionEvent. Посилання на ці функції додаються за допомогою програми SceneBuilder або вручну. Текст файлу CensusesForm.fxml може бути таким:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<BorderPane prefHeight="500.0" prefWidth="700.0"
            xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"
            fx:controller="ua.inf.iwanoff.java.advanced.fourth.CensusesController">
    <top>
        <MenuBar BorderPane.alignment="CENTER">
            <Menu mnemonicParsing="false" text="Файл">
                <MenuItem mnemonicParsing="false" text="Новий" onAction="#doNew"/>
                <MenuItem mnemonicParsing="false" text="Відкрити..." onAction="#doOpen"/>
                <MenuItem mnemonicParsing="false" text="Зберегти..." onAction="#doSave"/>
                <MenuItem mnemonicParsing="false" text="Вийти" onAction="#doExit"/>
            </Menu>
            <Menu mnemonicParsing="false" text="Редагування">
                <MenuItem mnemonicParsing="false" text="Додати рядок" onAction="#doAdd"/>
                <MenuItem mnemonicParsing="false" text="Видалити останній рядок" onAction="#doRemove"/>
            </Menu>
            <Menu mnemonicParsing="false" text="Робота">
                <MenuItem mnemonicParsing="false" text="Сортувати за кількістю населення"
                          onAction="#doSortByPopulation"/>
                <MenuItem mnemonicParsing="false" text="Сортувати за алфавітом коментарів"
                          onAction="#doSortByComments"/>
            </Menu>
            <Menu mnemonicParsing="false" text="Допомога">
                <MenuItem mnemonicParsing="false" text="Про програму..." onAction="#doAbout"/>
            </Menu>
        </MenuBar>
    </top>
    <left>
        <AnchorPane prefHeight="472.0" prefWidth="200.0" BorderPane.alignment="CENTER">
            <Label text="Країна" AnchorPane.leftAnchor="11.0"
                   AnchorPane.rightAnchor="11.0" AnchorPane.topAnchor="14.0"/>
            <TextField fx:id="textFieldCountry" prefHeight="22.0" AnchorPane.leftAnchor="11.0"
                       AnchorPane.rightAnchor="11.0" AnchorPane.topAnchor="35.0" onAction="#nameChanged"/>
            <Label text="Територія" AnchorPane.leftAnchor="11.0"
                   AnchorPane.rightAnchor="11.0" AnchorPane.topAnchor="69.0"/>
            <TextField fx:id="textFieldArea" prefHeight="22.0" AnchorPane.leftAnchor="11.0"
                       AnchorPane.rightAnchor="11.0" AnchorPane.topAnchor="90.0" onAction="#areaChanged"/>
            <Label text="Текст для пошуку:" AnchorPane.leftAnchor="11.0"
                   AnchorPane.rightAnchor="11.0" AnchorPane.topAnchor="154.0"/>
            <TextField fx:id="textFieldText" prefHeight="22.0" AnchorPane.leftAnchor="11.0"
                       AnchorPane.rightAnchor="11.0" AnchorPane.topAnchor="175.0"/>
            <Button mnemonicParsing="false" prefHeight="22.0"
                    text="Шукати слово" AnchorPane.leftAnchor="11.0"
                    AnchorPane.rightAnchor="11.0" AnchorPane.topAnchor="210.0" onAction="#doSearchByWord"/>
            <Button mnemonicParsing="false" prefHeight="22.0"
                    text="Шукати послідовність літер" AnchorPane.leftAnchor="11.0"
                    AnchorPane.rightAnchor="11.0" AnchorPane.topAnchor="245.0" onAction="#doSearchBySubstring"/>
            <TextArea fx:id="textAreaResults" AnchorPane.bottomAnchor="11.0"
                      AnchorPane.leftAnchor="11.0" AnchorPane.rightAnchor="11.0"
                      AnchorPane.topAnchor="280.0"/>
        </AnchorPane>
    </left>
    <center>
        <TableView fx:id="tableViewCensuses" prefHeight="473.0" prefWidth="114.0"
                   BorderPane.alignment="CENTER" editable="true" >
            <columns>
                <TableColumn fx:id="tableColumnYear" prefWidth="50.0" text="Рік" editable="true" />
                <TableColumn fx:id="tableColumnPopulation" prefWidth="100.0" text="Населення"  editable="true" />
                <TableColumn fx:id="tableColumnDensity" prefWidth="140.0" text="Щільність населення"  editable="false" />
                <TableColumn fx:id="tableColumnComments" prefWidth="205.0" text="Коментарі"  editable="true" />
            </columns>
        </TableView>
    </center>
</BorderPane>

Для реалізації оброблювачів подій придадуться деякі допоміжні методи. Можна навести весь код класу CensusesController:

package ua.inf.iwanoff.java.advanced.fourth;

import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.TableColumn.CellEditEvent;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.stage.FileChooser;
import javafx.util.converter.IntegerStringConverter;
import ua.inf.iwanoff.java.third.Census;
import ua.inf.iwanoff.java.fourth.CountryWithArrayList;
import ua.inf.iwanoff.java.advanced.third.FileUtils;
import ua.inf.iwanoff.java.third.CensusUtilities;
import ua.inf.iwanoff.java.third.CountryUtilities;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.ResourceBundle;

/**
 * Клас-контролер, пов'язаний з документом CensusesForm.fxml
 *
 * Реалізація інтерфейсу Initializable забезпечує можливість ініціалізації
 * в методі initialize() візуальних компонентів, описаних у FXML-документі
 *
 */
public class CensusesController implements Initializable {
    // Посилання на клас-модель:
    private CountryWithArrayList country = new CountryWithArrayList();

    // Список, вміст якого відображатиметься в таблиці:
    private ObservableList<Census> observableList;

    /**
     * Діалогове вікно довільного повідомлення
     *
     * @param message - текст повідомлення
     */
    public static void showMessage(String message) {
        Alert alert = new Alert(AlertType.INFORMATION);
        alert.setTitle("");
        alert.setHeaderText(message);
        alert.showAndWait();
    }

    /**
     * Діалогове вікно повідомлення про помилку
     *
     * @param message - текст повідомлення
     */
    public static void showError(String message) {
        Alert alert = new Alert(AlertType.ERROR);
        alert.setTitle("Помилка");
        alert.setHeaderText(message);
        alert.showAndWait();
    }

    /**
     * Створення діалогового вікна вибору файлів
     *
     * @param title - текст заголовка вікна
     */
    public static FileChooser getFileChooser(String title) {
        FileChooser fileChooser = new FileChooser();
        // Починаємо шукати з поточної теки:
        fileChooser.setInitialDirectory(new File("."));
        // Встановлюємо фільтри для пошуку файлів:
        fileChooser.getExtensionFilters().add(
                new FileChooser.ExtensionFilter("XML-файли (*.xml)", "*.xml"));
        fileChooser.getExtensionFilters().add(
                new FileChooser.ExtensionFilter("Усі файли (*.*)", "*.*"));
        // Вказуємо заголовок вікна:
        fileChooser.setTitle(title);
        return fileChooser;
    }

    // Поля, пов'язані з візуальними елементами:

    @FXML
    private TextField textFieldCountry;
    @FXML
    private TextField textFieldArea;
    @FXML
    private TextField textFieldText;
    @FXML
    private TextArea textAreaResults;
    @FXML
    private TableView<Census> tableViewCensuses;
    @FXML
    private TableColumn<Census, Integer> tableColumnYear;
    @FXML
    private TableColumn<Census, Integer> tableColumnPopulation;
    @FXML
    private TableColumn<Census, Number> tableColumnDensity;
    @FXML
    private TableColumn<Census, String> tableColumnComments;

    /**
     * Метод ініціалізації візуальних компонентів, описаних у FXML-документі
     *
     */
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        // Записуємо порожній рядок замість "No content in table":
        tableViewCensuses.setPlaceholder(new Label(""));
    }

    // Методи - оброблювачі подій:

    @FXML
    private void doNew(ActionEvent event) {
        country = new CountryWithArrayList();
        observableList = null;
        textFieldCountry.setText("");
        textFieldArea.setText("");
        textFieldText.setText("");
        textAreaResults.setText("");
        tableViewCensuses.setItems(null);
        tableViewCensuses.setPlaceholder(new Label(""));
    }

    @FXML
    private void doOpen(ActionEvent event) {
        FileChooser fileChooser = getFileChooser("Відкрити XML-файл");
        File file;
        if ((file = fileChooser.showOpenDialog(null)) != null) {
            try {
                country = FileUtils.deserializeFromXML(file.getCanonicalPath());
                // Заповнюємо текстові поля прочитаними даними:
                textFieldCountry.setText(country.getName());
                textFieldArea.setText(country.getArea() + "");
                textAreaResults.setText("");
                // Очищаємо та оновлюємо таблицю:
                tableViewCensuses.setItems(null);
                updateTable();
            }
            catch (IOException e) {
                showError("Файл не знайдено");
            }
            catch (Exception e) {
                showError("Неправильний формат файлу");
            }
        }
    }

    @FXML
    private void doSave(ActionEvent event) {
        FileChooser fileChooser = getFileChooser("Зберегти XML-файл");
        File file;
        if ((file = fileChooser.showSaveDialog(null)) != null) {
            try {
                FileUtils.serializeToXML(createUpdatedCountry(), file.getCanonicalPath());
                showMessage("Результати успішно збережені");
            }
            catch (Exception e) {
                showError("Помилка запису в файл");
            }
        }
    }

    @FXML
    private void doExit(ActionEvent event) {
        Platform.exit(); // коректне завершення застосунку JavaFX
    }

    @FXML
    private void doAdd(ActionEvent event) {
        country.addCensus(0, 0, "");
        updateTable(); // створюємо нові дані
    }

    @FXML
    private void doRemove(ActionEvent event) {
        // Не можемо видалити рядок, якщо немає даних:
        if (observableList == null) {
            return;
        }
        // Якщо є рядки, видаляємо останній:
        if (!observableList.isEmpty()) {
            observableList.removeLast();
        }
        // Якщо немає рядків, вказуємо, що дані відсутні:
        if (observableList.isEmpty()) {
            observableList = null;
        }
    }

    @FXML
    private void doSortByPopulation(ActionEvent event) {
        CountryUtilities.sortByPopulation(country);
        tableViewCensuses.setItems(null);
        updateTable();
    }

    @FXML
    private void doSortByComments(ActionEvent event) {
        CountryUtilities.sortByComments(country);
        tableViewCensuses.setItems(null);
        updateTable();
    }

    @FXML
    private void doAbout(ActionEvent event) {
        Alert alert = new Alert(AlertType.INFORMATION);
        alert.setTitle("Про програму...");
        alert.setHeaderText("Дані про переписи населення");
        alert.setContentText("Версія 1.0");
        alert.showAndWait();
    }

    @FXML
    private void nameChanged(ActionEvent event) {
        // Коли користувач змінив дані в textFieldCountry,
        // автоматично оновлюємо назву:
        country.setName(textFieldCountry.getText());
    }

    @FXML
    private void areaChanged(ActionEvent event) {
        // Коли користувач змінив дані в textFieldArea, автоматично
        // оновлюємо значення території та щільності населення:
        try {
            double area = Double.parseDouble(textFieldArea.getText());
            country.setArea(area);
            setDensity();
        }
        catch (NumberFormatException e) {
            // Якщо помилка, повертаємо, як було:
            textFieldArea.setText(country.getArea() + "");
        }
    }

    @FXML
    private void doSearchByWord(ActionEvent event) {
        textAreaResults.setText("");
        for (int i = 0; i < country.censusesCount(); i++) {
            Census c = country.getCensus(i);
            if (CensusUtilities.containsWord(c, textFieldText.getText())) {
                showResults(c);
            }
        }
    }

    @FXML
    private void doSearchBySubstring(ActionEvent event) {
        textAreaResults.setText("");
        for (int i = 0; i < country.censusesCount(); i++) {
            Census c = country.getCensus(i);
            if (CensusUtilities.containsSubstring(c, textFieldText.getText())) {
                showResults(c);
            }
        }
    }

    private void showResults(Census census) {
        textAreaResults.appendText("Перепис " + census.getYear() + " року.\n");
        textAreaResults.appendText("Коментар:" + census.getComments() + "\n");
        textAreaResults.appendText("\n");
    }

    private CountryWithArrayList createUpdatedCountry() {
        // Переписуємо дані в модель з observableList
        CountryWithArrayList country = new CountryWithArrayList();
        for (Census c : observableList) {
            country.addCensus(c);
        }
        country.setName(textFieldCountry.getText());
        double area = Double.parseDouble(textFieldArea.getText());
        country.setArea(area);
        return country;
    }

    private void setDensity() {
        // Визначаємо механізм автоматичного перерахування комірок
        // стовпця tableColumnDensity, коли змінюються інші дані:
        tableColumnDensity.setCellFactory(cell -> new TableCell<>() {
            @Override
            protected void updateItem(Number item, boolean empty) {
                int current = this.getTableRow().getIndex();
                if (observableList != null && current >= 0 &&
                        current < observableList.size() && country.getArea() > 0) {
                    double population = observableList.get(current).getPopulation();
                    double density = population / country.getArea();
                    setText(String.format("%7.2f", density));
                }
                else {
                    setText("");
                }
            }
        });
    }

    private void updateYear(CellEditEvent<Census, Integer> t) {
        // Оновлюємо дані про рік:
        Census c = t.getTableView().getItems().get(t.getTablePosition().getRow());
        c.setYear(t.getNewValue());
    }

    private void updatePopulation(CellEditEvent<Census, Integer> t) {
        // Оновлюємо дані про населення:
        Census c = t.getTableView().getItems().get(t.getTablePosition().getRow());
        c.setPopulation(t.getNewValue());
        setDensity(); // перераховуємо щільність населення
    }

    private void updateComments(CellEditEvent<Census, String> t) {
        // Оновлюємо коментарі:
        Census c = t.getTableView().getItems().get(t.getTablePosition().getRow());
        c.setComments(t.getNewValue());
    }

    private void updateTable() {
        // Заповнюємо observableList:
        List<Census> list = new ArrayList<>();
        observableList = FXCollections.observableList(list);
        for (int i = 0; i < country.censusesCount(); i++) {
            list.add(country.getCensus(i));
        }
        tableViewCensuses.setItems(observableList);

        // Вказуємо для колонок зв'язану з ними властивість і механізм редагування
        // залежно від типу комірок:
        tableColumnYear.setCellValueFactory(new PropertyValueFactory<>("year"));
        tableColumnYear.setCellFactory(
                TextFieldTableCell.forTableColumn(new IntegerStringConverter()));
        tableColumnYear.setOnEditCommit(this::updateYear);
        tableColumnPopulation.setCellValueFactory(new PropertyValueFactory<>("population"));
        tableColumnPopulation.setCellFactory(
                TextFieldTableCell.forTableColumn(new IntegerStringConverter()));
        tableColumnPopulation.setOnEditCommit(this::updatePopulation);
        tableColumnDensity.setSortable(false); // цю колонку не можна сортувати автоматично
        setDensity();
        tableColumnComments.setCellValueFactory(new PropertyValueFactory<>("comments"));
        tableColumnComments.setCellFactory(TextFieldTableCell.forTableColumn());
        tableColumnComments.setOnEditCommit(this::updateComments);
    }
}

Роботу програми можна вдосконалити, переключаючи доступність окремих елементів за допомогою методу setDisable() залежно від стану програми.

Оскільки бібліотека JavaFX додана до раніше створеного проєкту, необхідно здійснити додаткові налаштування, описані у 2.2.3.

4 Вправи для контролю

  1. Створити програму графічного інтерфейсу користувача, у якій у двох рядках уведення користувач задає два рядки й після натискання кнопки одержує в третьому рядку результат зшивання вихідних рядків.
  2. Створити програму графічного інтерфейсу користувача, у якій у двох рядках уведення користувач задає два дійсних числа і після натискання кнопки одержує в третьому рядку введення добуток цих чисел.
  3. Створити програму графічного інтерфейсу користувача, у якій у двох рядках уведення користувач задає два цілих числа і після натискання кнопки одержує в окремому діалоговому вікні введення добуток цих чисел.

5 Контрольні запитання

  1. Дайте визначення процесу і потоку виконання.
  2. У чому переваги використання потоків виконання?
  3. Які є способи створення потоків виконання в Java?
  4. В яких станах може перебувати потік?
  5. Коли здійснюється припинення і призупинення виконання роботи потоку?
  6. Якими засобами можна здійснити синхронізацію потоків?
  7. У яких випадках робота потоку блокується?
  8. Для чого використовується модифікатор synchronized?
  9. Які стандартні бібліотеки Java використовують для реалізації застосунків графічного інтерфейсу користувача?
  10. Що таке аплет?
  11. У чому полягає ідея програмування, керованого подіями?
  12. Що таке JavaFX? Які переваги має JavaFX?
  13. Що таке властивості JavaFX, як вони реалізовані і які можливості вони надають?
  14. Як реалізоване зв'язування (Binding) між об'єктами JavaFX?
  15. Що таке ObservableList?
  16. Що таке FXML? Які переваги надає FXML?
  17. Що таке MVC?
  18. Що таке компонування і як воно реалізоване в JavaFX?
  19. Які стандартні контейнери надає JavaFX і чим вони відрізняються?
  20. Як здійснюється робота з кнопками RadioButton?
  21. У чому полягають особливості модальних діалогових вікон?
  22. Як у JavaFX використовують стандартні вікна вибору файлів?
  23. Як здійснюється робота з табличними даними в JavaFX?
  24. Як забезпечити редагування комірок таблиці в JavaFX?
  25. Як здійснюється візуальне редагування вікна застосунку графічного інтерфейсу користувача?

 

up