Лабораторна робота 4
Створення GUI-застосунків
1 Завдання на лабораторну роботу
1.1 Індивідуальне завдання
Необхідно реалізувати мовою Java за допомогою засобів JavaFX застосунок графічного інтерфейсу користувача, в якому здійснюється обробка даних індивідуальних завдань попередніх лабораторних робіт. Головне вікно повинно містити меню, в якому необхідно реалізувати такі функції:
- створення нового набору даних;
- завантаження (десеріалізація) даних з XML-документа для редагування;
- зберігання змінених даних в XML-документі за допомогою засобів серіалізації XStream;
- пошук за ознаками, визначеними в лабораторній роботі № 3 курсу "Основи програмування Java " попереднього семестру;
- здійснення сортування за ознаками, визначеними в лабораторній роботі № 4 курсу "Основи програмування Java " попереднього семестру;
- отримання вікна "Про програму" з даними про програму й автора.
У лівій частині вікна слід розташувати рядки для введення скалярних даних, область відображення для результатів пошуку, а також кнопки, які забезпечують виконання основних функцій програми. В середній частині вікна слід розташувати таблицю для відображення та редагування даних.
1.2 Використання ObservableList
Створити список (ObservableList
) дійсних чисел типу Double
. Змінити початковий порядок
розташування чисел так, щоб спочатку були розташовані додатні числа (без зміни їх відносного порядку), а потім – від'ємні
у порядку, протилежному початковому. Кожна зміна стану списку повинна обумовлювати виведення елементів списку в
консольне вікно.
1.3 Міні-калькулятор
Створити застосунок графічного інтерфейсу користувача, в якому після введення чисел у двох рядках типу TextField
виконується одна з чотирьох арифметичних дій (залежно від вибраної кнопки RadioButton
). Результат
виводиться в інше текстове поле.
1.4 Словник (додаткове завдання)
Розробити програму графічного інтерфейсу користувача перегляду слів невеличкого англо-українського
словника. Реалізувати функції пошуку слова, додавання нових слів. Для зберігання даних
використати Map
.
2 Методичні вказівки
2.1 Використання Java для створення GUI-застосунків
2.1.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.1.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.1.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 { public static void main(String[] args) { 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 { public static void main(String[] args) { 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.2 Огляд платформи JavaFX
2.2.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.2.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(); } public static void main(String[] args) { launch(args); } }
У наведеному вище прикладі функція, що обробляє подію, пов'язану з натисканням кнопки, реалізована за допомогою лямбда-виразу.
В окремому каталозі проєкту можна розмістити ресурсні файли, наприклад, побітові зображення.
Компоненти JavaFX утворюють ієрархію об'єктів. Застосунок має містити як мінімум один об'єкт типу
Stage
(підмостки сцени). Підмостки сцени визначають властивості вікна, що є їх власником:
стиль вікна, його тип (наприклад, модальне / немодального), заголовок і т. д. Об'єкт типу Stage
– контейнер верхнього рівня, що містить сцену (Scene
). Сцена являє собою контейнер для інших
візуальних компонентів.
2.2.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
. Цей клас містить функціюmain()
.HelloController.java
містить оброблювач подіїonHelloButtonClick()
.
Крім того, у гілці resources
в аналогічному пакеті розташовано файл hello-view.fxml
,
який містить опис елементів інтерфейсу користувача. Файл містить посилання на клас HelloController
.
Після того, як автоматично буде завантажено необхідні засоби, можна запустити програму на виконання і натиснути кнопку. З'явиться текст "Welcome to JavaFX Application!".
Іноді необхідно додати засоби JavaFX до проєкту, який було створено раніше. Окрім безпосереднього додавання коду,
в якому використані компоненти JavaFX, необхідно додати залежності до файлу
pom.xml
:
<dependency> <groupId>org.openjfx</groupId> <artifactId>javafx-controls</artifactId> <version>21</version> </dependency> <dependency> <groupId>org.openjfx</groupId> <artifactId>javafx-fxml</artifactId> <version>21</version> </dependency> <dependency> <groupId>org.openjfx</groupId> <artifactId>javafx-graphics</artifactId> <version>21</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-21.0.3
(відповідна
тека є в архіві). Для роботи застосунку необхідна тека lib
, яка знаходиться всередині вказаної, наприклад, c:\javafx-sdk-21.0.3\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-21.0.3\lib" --add-modules javafx.controls,javafx.fxml
Далі слід зберегти створену конфігурацію і користуватися нею для виконання програми.
2.3 Теоретичні засади створення застосунків JavaFX
2.3.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>17.0.2</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 { public static void main(String[] args) { 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.3.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 { public static void main(String[] args) { 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 { public static void main(String[] args) { 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.3.3 Робота з колекціями JavaFX
Окрім властивостей і зв'язування, JavaFX надає спеціальні колекції, головна відмінність яких – наявність
механізму автоматичного сповіщення про зміну стану елементів. Узагальнений інтерфейс javafx.collections.ObservableList
,
який розширює інтерфейси java.util.List
і javafx.beans.Observable
, об'єднує функції
для роботи зі стандартними списками з можливостями відстежування змін інформації в списках. Можливості
реагування на зміни в стані списку показані в наведеному нижче прикладі:
package ua.inf.iwanoff.java.advanced.fourth; import javafx.collections.*; public class ObservableListDemo { public static void main(String[] args) { 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.4 Робота з візуальними компонентами JavaFX
2.4.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.4.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.4.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.4.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.4.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.5 Робота з табличними даними в 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(); } } public 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) ); } public 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) ); } public static void main(String[] args) { launch(args); } }
2.6 Візуальне проєктування програм графічного інтерфейсу користувача
Для візуального редагування вікон і компонентів 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) можна взаємодіяти з макетом з метою перевірки функціональності майбутнього коду.
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 { 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++; } } } public static void main(String[] args) { 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(); } } public 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()); } public 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(); } } public static void main(String[] args) { launch(args); } }
3.5 Програма графічного інтерфейсу користувача для роботи з даними про переписи населення
Припустимо, необхідно створити програму графічного інтерфейсу користувача, в якій користувачеві надається можливість уведення та редагування даних про переписи населення, зберігання даних в 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) { } public 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(); } } public 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 майбутнє
головне вікно застосунку матиме такий вигляд:
Тепер можна перейти до проєктування контролеру. Раніше створений клас CountryWithStreams
виконуватиме роль
моделі. Посилання на нього можна додати до класу-контролера. Іменованим компонентам повинні відповідати приватні
поля, які можна згенерувати автоматично. Механізм генерації коду за допомогою функції контекстного меню 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.advanced.first.CensusWithStreams; import ua.inf.iwanoff.java.advanced.first.CountryWithStreams; import ua.inf.iwanoff.java.advanced.third.FileUtils; 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 CountryWithStreams country = new CountryWithStreams(); // Список, вміст якого відображатиметься в таблиці: private ObservableList<CensusWithStreams> 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<CensusWithStreams> tableViewCensuses; @FXML private TableColumn<CensusWithStreams, Integer> tableColumnYear; @FXML private TableColumn<CensusWithStreams, Integer> tableColumnPopulation; @FXML private TableColumn<CensusWithStreams, Number> tableColumnDensity; @FXML private TableColumn<CensusWithStreams, 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 CountryWithStreams(); 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 { updateSourceData(); // оновлюємо дані в моделі nameChanged(event); areaChanged(event); FileUtils.serializeToXML(country, 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.size() > 0) { observableList.remove(observableList.size() - 1); } // Якщо немає рядків, вказуємо, що дані відсутні: if (observableList.size() <= 0) { observableList = null; } } @FXML private void doSortByPopulation(ActionEvent event) { updateSourceData(); country.sortByPopulation(); updateTable(); } @FXML private void doSortByComments(ActionEvent event) { updateSourceData(); country.sortByComments(); 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) { // Оновлюємо дані: updateSourceData(); textAreaResults.setText(""); for (int i = 0; i < country.censusesCount(); i++) { CensusWithStreams c = (CensusWithStreams) country.getCensus(i); if (c.containsWord(textFieldText.getText())) { showResults(c); } } } @FXML private void doSearchBySubstring(ActionEvent event) { // Оновлюємо дані: updateSourceData(); textAreaResults.setText(""); for (int i = 0; i < country.censusesCount(); i++) { CensusWithStreams c = (CensusWithStreams) country.getCensus(i); if (c.containsSubstring(textFieldText.getText())) { showResults(c); } } } private void showResults(CensusWithStreams census) { textAreaResults.appendText("Перепис " + census.getYear() + " року.\n"); textAreaResults.appendText("Коментар:" + census.getComments() + "\n"); textAreaResults.appendText("\n"); } private void updateSourceData() { // Переписуємо дані в модель з observableList country = new CountryWithStreams(); for (CensusWithStreams c : observableList) { country.addCensus(c); } } private void setDensity() { // Визначаємо механізм автоматичного перерахування комірок // стовпця tableColumnDensity, коли змінюються інші дані: tableColumnDensity.setCellFactory(cell -> new TableCell<CensusWithStreams, Number>() { @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<CensusWithStreams, Integer> t) { // Оновлюємо дані про рік: CensusWithStreams c = t.getTableView().getItems().get(t.getTablePosition().getRow()); c.setYear(t.getNewValue()); } private void updatePopulation(CellEditEvent<CensusWithStreams, Integer> t) { // Оновлюємо дані про населення: CensusWithStreams c = t.getTableView().getItems().get(t.getTablePosition().getRow()); c.setPopulation(t.getNewValue()); setDensity(); // перераховуємо щільність населення } private void updateComments(CellEditEvent<CensusWithStreams, String> t) { // Оновлюємо коментарі: CensusWithStreams c = t.getTableView().getItems().get(t.getTablePosition().getRow()); c.setComments(t.getNewValue()); } private void updateTable() { // Заповнюємо observableList: List<CensusWithStreams> list = new ArrayList<>(); observableList = FXCollections.observableList(list); for (int i = 0; i < country.censusesCount(); i++) { list.add((CensusWithStreams) country.getCensus(i)); } tableViewCensuses.setItems(observableList); // Вказуємо для колонок зв'язану з ними властивість і механізм редагування // залежно від типу комірок: tableColumnYear.setCellValueFactory(new PropertyValueFactory<>("year")); tableColumnYear.setCellFactory( TextFieldTableCell.forTableColumn(new IntegerStringConverter())); tableColumnYear.setOnEditCommit(t -> updateYear(t)); tableColumnPopulation.setCellValueFactory(new PropertyValueFactory<>("population")); tableColumnPopulation.setCellFactory( TextFieldTableCell.forTableColumn(new IntegerStringConverter())); tableColumnPopulation.setOnEditCommit(t -> updatePopulation(t)); tableColumnDensity.setSortable(false); // цю колонку не можна сортувати автоматично setDensity(); tableColumnComments.setCellValueFactory(new PropertyValueFactory<>("comments")); tableColumnComments.setCellFactory(TextFieldTableCell.forTableColumn()); tableColumnComments.setOnEditCommit(t -> updateComments(t)); } }
Роботу програми можна вдосконалити, переключаючи доступність окремих елементів за допомогою методу setDisable()
залежно від стану програми.
4 Вправи для контролю
- Створити програму графічного інтерфейсу користувача, у якій у двох рядках уведення користувач задає два рядки й після натискання кнопки одержує в третьому рядку результат зшивання вихідних рядків.
- Створити програму графічного інтерфейсу користувача, у якій у двох рядках уведення користувач задає два дійсних числа і після натискання кнопки одержує в третьому рядку введення добуток цих чисел.
- Створити програму графічного інтерфейсу користувача, у якій у двох рядках уведення користувач задає два цілих числа і після натискання кнопки одержує в окремому діалоговому вікні введення добуток цих чисел.
5 Контрольні запитання
- Які стандартні бібліотеки Java використовують для реалізації застосунків графічного інтерфейсу користувача?
- Що таке аплет?
- У чому полягає ідея програмування, керованого подіями?
- Що таке JavaFX? Які переваги має JavaFX?
- Що таке властивості JavaFX, як вони реалізовані і які можливості вони надають?
- Як реалізоване зв'язування (Binding) між об'єктами JavaFX?
- Що таке
ObservableList
? - Що таке FXML? Які переваги надає FXML?
- Що таке MVC?
- Що таке компонування і як воно реалізоване в JavaFX?
- Які стандартні контейнери надає JavaFX і чим вони відрізняються?
- Як здійснюється робота з кнопками
RadioButton
? - У чому полягають особливості модальних діалогових вікон?
- Як у JavaFX використовують стандартні вікна вибору файлів?
- Як здійснюється робота з табличними даними в JavaFX?
- Як зв'язати колонки таблиці з властивостями об'єктів списку?
- Як забезпечити редагування комірок таблиці в JavaFX?
- Як здійснюється візуальне редагування вікна застосунку графічного інтерфейсу користувача?