Лабораторна робота 4
Програмування, кероване подіями
1 Завдання на лабораторну роботу
1.1 Індивідуальне завдання
Необхідно реалізувати мовою Java за допомогою засобів JavaFX застосунок графічного інтерфейсу користувача, в якому здійснюється обробка даних індивідуальних завдань попередніх лабораторних робіт. Головне вікно повинно містити меню, в якому необхідно реалізувати такі функції:
- створення нового набору даних
- завантаження даних з XML-документу для редагування
- зберігання змінених даних в XML-документі
- пошук за ознаками, визначеними в лабораторній роботі № 5 курсу "Основи програмування" (2 частина) попереднього семестру
- здійснення сортування за ознаками, визначеними в лабораторній роботі № 1
- отримання вікна "Про програму" з даними про програму і автора.
У лівій частині вікна слід розташувати рядки для введення скалярних даних, область відображення для результатів пошуку, а також кнопки, які забезпечують виконання основних функцій програми. В середній частині вікна слід розташувати таблицю для відображення та редагування даних.
1.2 Міні-калькулятор
Створити застосунок графічного інтерфейсу користувача, в якому після введення чисел у двох рядках типу TextField
виконується одна з чотирьох арифметичних дій (залежно від вибраної кнопки RadioButton
). Результат виводиться в інше текстове поле.
1.3 Список файлів усіх підкаталогів (додаткове завдання)
Створити застосунок графічного інтерфейсу користувача, в якому після введення імені певної теки у рядку типу TextField
користувач може натиснути одну з кнопок:
- натискання першої кнопки забезпечує виведення в першій області відображення
TextArea
результатів імен усіх файлів і підкаталогів цієї теки, а також усіх файлів підкаталогів, їхніх підкаталогів тощо, отриманих за допомогою класуjava.io.File
через рекурсивну функцію - натискання другої кнопки забезпечує виведення в другій області
TextArea
відображення аналогічних результатів, отриманих засобами пакетуjava.nio.file
.
Якщо тека не існує, вивести повідомлення про помилку в діалоговому вікні.
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.oop.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.oop.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-проекти. Для цього у середовищі IntelliJ IDEA необхідно виконати деякі підготовчі дії:
- переконатися, що версія JDK не нижче 11
- перевірити, чи включений необхідний JavaFX-плагін; це можна здійснити в такий спосіб:
- у вікні Settings (команда меню File | Settings...) IntelliJ IDEA вибрати позицію Plugins
- у списку плагінів відшукати плагін JavaFX; якщо він не включений, включити його.
Новий проект JavaFX можна створити, вибравши в лівій частині вікна майстра проектів Java FX. На наступній сторінці майстра вказуємо ім'я проекту, наприклад, FirstFX
. Автоматично створюється програма, що зображує вікно з заголовком Hello!
і кнопкою всередині. Якщо програму запустити на виконання і натиснути кнопку, з'явиться текст
"Welcome to JavaFX Application!".
Проект містить у гілці "Java" пакет example
з двома файлами:
HelloApplication.java
містить класHelloApplication
, похідний відjavafx.application.Application
. Цей клас містить функціюmain()
.HelloController.java
містить оброблювач подііonHelloButtonClick()
.
Крім того, у гілці resources
в аналогічному пакети розташовано файл hello-view.fxml
, який містить опис елементів інтерфейсу користувача. Файл містить посилання на клас HelloController
.
Імена створеного пакету і файлів можна змінити за допомогою засобів рефакторингу.
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
тощо.
Для того, щоб визначити властивість у деякому класі, треба визначити приватне поле відповідного типу, а також створити один сеттер і два геттери:
package ua.inf.iwanoff.oop.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.oop.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.oop.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.oop.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.oop.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.1 показано роботу з текстовими полями і кнопкою, у прикладі 3.2 – робота з кнопками RadioButton
, у прикладі 3.4 – також з меню та областю редагування тексту 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.3.
2.5 Робота з табличними даними в JavaFX
Робота з табличними даними здійснюється за допомогою компоненту TableView
. Основна робота цього компоненту – відображення властивостей об'єктів, які зберігаються в списку типу ObservableList
. Використання TableView
розглянемо на прикладі. Припустимо, створено клас City
:
package ua.inf.iwanoff.oop.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.oop.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.oop.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.oop.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) можна взаємодіяти з макетом з метою перевірки функціональності майбутнього коду.
2.7 Робота з файловою системою
2.7.1 Загальні концепції
Java надає можливість роботи не тільки з вмістом файлів, але також з файлової системою в цілому. Файлова система – це спосіб організації даних, який використовується операційною системою для зберігання інформації у вигляді файлів на носіях інформації. Також цим поняттям позначають сукупність файлів і каталогів (папок), які розміщуються на логічному або фізичному пристрої.
До типових функцій взаємодії з файлової системою можна віднести:
- перевірку існування файлу або каталогу
- отримання списку файлів і підкаталогів заданого каталогу
- створення файлів і посилань на файли
- копіювання файлів
- перейменування і переміщення файлів
- управління атрибутами файлів
- видалення файлів
- обхід дерева підкаталогів
- відстеження змін файлів
Для роботи з файловою системою Java надає два підходи:
- використання класу
java.io.File
; - використання засобів пакету
java.nio.file
.
2.7.2 Використання класу File
Пакет java.io
надає можливість роботи як із вмістом файлів, так і з файловою системою в цілому. Цю можливість реалізує клас File
. Для створення об'єкта цього класу як параметр конструктора слід визначити повний або відносний шлях до файлу. Наприклад:
File dir = new File("C:\\Users"); File currentDir = new File("."); // Тека проекту (поточна)
Клас File
містить методи для отримання списку файлів визначеної теки (list()
, listFiles()
), отримання та модифікації атрибутів файлів (setLastModified()
, setReadOnly()
, isHidden()
, isDirectory()
тощо), створення нового файлу (createNewFile()
, createTempFile()
), створення тек (mkdir()
), видалення файлів та тек (delete()
) та багато інших. Роботу деяких з цих методів можна продемонструвати на наведеному нижче прикладі:
package ua.inf.iwanoff.oop.fourth; import java.io.*; import java.util.*; public class FileTest { public static void main(String[] args) throws IOException { Scanner scanner = new Scanner(System.in); System.out.print("Уведіть ім\'я теки, яку ви хочете створити:"); String dirName = scanner.next(); File dir = new File(dirName); // Створюємо нову теку: if (!dir.mkdir()) { System.out.println("Не можна створити теку!"); return; } // Створюємо новий файл всередині нової теки: File file = new File(dir + "\\temp.txt"); file.createNewFile(); // Показуємо список файлів теки: System.out.println(Arrays.asList(dir.list())); file.delete(); // Видаляємо файл dir.delete(); // Видаляємо теку } }
Функція list()
без параметрів дозволяє отримати масив рядків – усіх файлів та підкаталогів теки, визначеної під час створення об'єкта типу File
. Виводяться відносні імена файлів (без шляхів). У наведеному нижче прикладі ми отримуємо список файлів та підкаталогів теки, ім'я якої вводиться з клавіатури:
package ua.inf.iwanoff.oop.fourth; import java.io.File; import java.io.FilenameFilter; import java.util.Scanner; public class ListOfFiles { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.print("Уведіть ім\'я теки:"); String dirName = scanner.next(); File dir = new File(dirName); if (!dir.isDirectory()) { System.out.println("Хибне ім\'я теки!"); return; } String[] list = dir.list(); for(String name : list) { System.out.println(name); } } }
На відміну від list()
, функція listFiles()
повертає масив об'єктів типу File
. Це надає додаткові можливості – отримання імен файлів з повним шляхом, перевірки значень атрибутів файлів, окрему роботу з теками тощо. Ці додаткові можливості продемонструємо на такому прикладі:
File[] list = dir.listFiles(); // Виводяться дані про файли в усталеній формі: for(File file : list) { System.out.println(file); } // Виводиться повний шлях: for(File file : list) { System.out.println(file.getCanonicalPath()); } // Виводяться тільки підкаталоги: for(File file : list) { if (file.isDirectory()) System.out.println(file.getCanonicalPath()); }
Для визначення маски-фільтру необхідно створювати об'єкт класу, який реалізує інтерфейс FilenameFilter
. У наведеному нижче прикладі ми отримуємо список файлів та підкаталогів, імена яких починаються з літери 's'
:
String[] list = dir.list(new FilenameFilter() { @Override public boolean accept(File dir, String name) { return name.toLowerCase().charAt(0) == 's'; } }); for(String name : list) { System.out.println(name); }
Аналогічний параметр типу FilenameFilter
можна застосувати до функції listFiles()
2.7.3 Робота з пакетом java.nio
Пакет java.nio
, який з'явився в JDK 1.4, спочатку включав альтернативні засоби введення-виведення. У порівнянні з традиційними потоками введення-виведення, java.nio
забезпечує більш високу ефективність операцій введення-виведення. Це досягається за рахунок того, що традиційні засоби введення-виведення працюють з даними в потоках, в той час як java.nio
працює з даними в блоках. Центральними об'єктами в java.nio
є "Канал" (Channel
) і "Буфер" (Buffer
). Канали аналогічні потокам в пакеті java.io
. Буфер – це контейнерний об'єкт. Всі дані, які передаються в канал, повинні бути спочатку поміщені в буфер. Будь-які дані, які зчитуються з каналу, зчитуються в буфер. Засоби java.nio
ефективні при роботі з двійковими файлами.
Версія Java 7 надає альтернативний підхід до роботи з файловою системою – набір класів, описаних в пакеті java.nio.files
. Пакет java.nio.files
надає клас Path
, який забезпечує надання шляху в файлової системі. Окремі складові цього шляху можна уявити деякою колекцією імен проміжних підкаталогів і імені самого файлу (підкаталогу). Отримати об'єкт класу Path
можна за допомогою методу get()
класу Path
. Методу get()
передається рядок – шлях::
Path path = Paths.get("c:/Users/Public");
Тепер можна отримати інформацію про шлях:
System.out.println(path.toString()); // c:\Users\Public System.out.println(path.getFileName()); // Public System.out.println(path.getName(0)); // Users System.out.println(path.getNameCount()); // 2 System.out.println(path.subpath(0, 2)); // Users\Public System.out.println(path.getParent()); // c:\Users System.out.println(path.getRoot()); // c:\
Після того, як об'єкт класу Path
створений, його можна використовувати як аргумент статичних функцій класу java.nio.files.Files
. Для перевірки наявності (відсутності) файлу використовують відповідно функції exists()
і notExists()
:
Path dir = Paths.get("c:/Windows"); System.out.println(Files.exists(dir)); // швидше за все, true System.out.println(Files.notExists(dir)); // швидше за все, false
Наявність двох окремих функцій пов'язано з можливістю отримання невизначеного результату (заборонений доступ до файлу).
Для того, щоб переконатися, що програма може отримати необхідний доступ до файлу, можна використовувати методи isReadable(Path)
, isWritable(Path)
і isExecutable(Path)
. Припустимо, створений об'єкт file типу Path
і заданий шлях до файлу. Наведений нижче фрагмент коду перевіряє, чи існує конкретний файл, і чи можна завантажити його на виконання:
boolean isRegularExecutableFile = Files.isRegularFile(file) & Files.isReadable(file) & Files.isExecutable(file);
Для отримання метаданих (даних про файлах і каталогах) клас Files
надає низку статичних методів:
Методи | Пояснення |
---|---|
size(Path) |
Повертає розмір зазначеного файлу в байтах |
isDirectory(Path, LinkOption...) |
Повертає |
isRegularFile(Path, LinkOption...) |
Повертає true , якщо вказаний Path вказує на звичайний файл |
isHidden(Path) |
Повертає true , якщо вказаний Path вказує на прихований файл |
getLastModifiedTime(Path, LinkOption...) setLastModifiedTime(Path, FileTime) |
Повертає / встановлює час останньої зміни зазначеного файлу |
getOwner(Path, LinkOption...) setOwner(Path, UserPrincipal) |
Повертає / встановлює власника файлу |
getAttribute(Path, String, LinkOption...) setAttribute(Path, String, Object, LinkOption...) |
Повертає або встановлює значення атрибуту файлу |
Для ОС Windows різних версій рядок атрибуту повинен починатися з префікса "dos:
". Наприклад, так можна встановити необхідні атрибути деякого файлу:
Path file = ... Files.setAttribute(file, "dos:archive", false); Files.setAttribute(file, "dos:hidden", true); Files.setAttribute(file, "dos:readonly", true); Files.setAttribute(file, "dos:system", true);
Читання необхідних атрибутів може також здійснюватися за допомогою методу readAttributes()
. Його другий параметр – метадані тип результату. Ці метадані можуть бути отримані через значення поля class
(метадані типів будуть розглянуті пізніше). Найбільш відповідний тип результату – це клас java.nio.file.attribute.BasicFileAttributes
. Наприклад, так можна отримати деякі дані про файл:
package ua.inf.iwanoff.oop.fourth; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.util.Scanner; public class Attributes { public static void main(String[] args) throws Exception { System.out.println("Введіть ім\'я файлу або каталогу:"); Path path = Paths.get(new Scanner(System.in).nextLine()); BasicFileAttributes attr = Files.readAttributes(path, BasicFileAttributes.class); System.out.println("Час створення: " + attr.creationTime()); System.out.println("Час останнього доступу: " + attr.lastAccessTime()); System.out.println("Час останньої зміни: " + attr.lastModifiedTime()); System.out.println("Каталог: " + attr.isDirectory()); System.out.println("Звичайний файл: " + attr.isRegularFile()); System.out.println("Розмір: " + attr.size()); } }
Клас DosFileAttributes
, похідний від BasicFileAttributes
, надає також функції isReadOnly()
, isHidden()
, isArchive()
і isSystem()
.
На відміну від java.io
, клас java.nio.files.Files
надає функцію copy()
для копіювання файлів. Наприклад:
Files.copy(Paths.get("c:/autoexec.bat"), Paths.get("c:/Users/autoexec.bat")); Files.copy(Paths.get("c:/autoexec.bat"), Paths.get("c:/Users/autoexec.bat"), StandardCopyOption.REPLACE_EXISTING);
Існують також опції StandardCopyOption.ATOMIC_MOVE
і StandardCopyOption.COPY_ATTRIBUTES
. Опції можна вказувати через кому.
Для переміщення файлів використовують функцію move()
(з аналогічними атрибутами або без них). Перейменування виконується тією ж функцією:
Files.move(Paths.get("c:/Users/autoexec.bat"), Paths.get("d:/autoexec.bat"));// переміщення Files.move(Paths.get("d:/autoexec.bat"), Paths.get("d:/unnecessary.bat"));// перейменування
Створення нових каталогів здійснюється за допомогою функції createDirectory()
класу Files
. Параметр функції має тип Path
.
Path dir = Paths.get("c:/NewDir"); Files.createDirectory(dir);
Для створення каталогу кількох рівнів в глибину, коли один або кілька батьківських каталогів, можливо, ще не існує, можна використовувати метод createDirectories()
:
Path dir = Paths.get("c:/NewDir/1/2"); Files.createDirectories(dir);
Для отримання списку файлів підкаталогу можна скористатися класом DirectoryStream
.
package ua.inf.iwanoff.oop.fourth; import java.io.IOException; import java.nio.file.*; public class FileListDemo { public static void main(String[] args) { Path dir = Paths.get("c:/Windows"); try (DirectoryStream<Path> ds = Files.newDirectoryStream(dir)) { for (Path p : ds) { System.out.println(p.getFileName()); } } catch (IOException e) { e.printStackTrace(); } } }
Видалення файлів і тек здійснюється за допомогою функцій delete()
і deleteIfExists()
:
Files.delete(Paths.get("d:/unnecessary.bat")); Files.deleteIfExists(Paths.get("d:/unnecessary.bat"));
Для обходу дерева каталогів пакет java.nio.files
надає засоби, які потребують реалізації рекурсивних алгоритмів. Існує метод walkFileTree()
класу Files
, що забезпечує обхід дерева підкаталогів. Як параметри необхідно вказати початковий каталог (об'єкт типу Path
), а також об'єкт, який реалізує узагальнений інтерфейс FileVisitor
.
Примітка: існує інший варіант методу, що дозволяє задавати також опції обходу каталогів і обмеження на глибину обходу підкаталогів.
Для реалізації інтерфейсу FileVisitor
треба визначити методи preVisitDirectory()
, postVisitDirectory()
, visitFile()
і visitFileFailed()
. Результат цих функцій – перелік типу FileVisitResult
. Можливі значення цього перерахування – CONTINUE
(продовжувати пошук), TERMINATE
(продовжувати пошук), SKIP_SUBTREE
(пропустити поддерево) і SKIP_SIBLINGS
(пропустити елементи того ж рівня).
Щоб кожного разу не реалізовувати всі методи інтерфейсу FileVisitor
, можна скористатися узагальненим класом SimpleFileVisitor
. Цей клас надає усталену реалізацію функцій інтерфейсу. В цьому випадку необхідно тільки перекрити потрібні функції. У наведеному нижче прикладі здійснюється пошук всіх файлів заданого каталогу і його підкаталогів:
package ua.inf.iwanoff.oop.fourth; import java.io.IOException; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.util.Scanner; public class FindAllFiles { private static class Finder extends SimpleFileVisitor<Path> { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { System.out.println(file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { System.out.println("----------------" + dir + "----------------"); return FileVisitResult.CONTINUE; } } public static void main(String[] args) { String dirName = new Scanner(System.in).nextLine(); try { Files.walkFileTree(Paths.get(dirName), new Finder()); // Поточний каталог } catch (IOException e) { e.printStackTrace(); } } }
Для пошуку файлів можна користуватися масками (так звані "glob"-маски), які активно застосовують в усіх операційних системах. Приклади таких масок – "a*.*
" (Імена файлів починаються з літери a
), "*.txt
" (файли з розширенням *.txt
) тощо. Припустимо, рядок pattern
містить таку маску. Далі створюємо об'єкт PathMatcher
:
PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + pattern);
У наіеденому нижче прикладі у визначеному каталозі здійснюється пошук файлів за вказаною маскою:
package ua.inf.iwanoff.oop.fourth; import java.io.IOException; import java.nio.file.*; import java.util.Scanner; public class FindMatched { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); String dirName = scanner.nextLine(); String pattern = scanner.nextLine(); Path dir = Paths.get(dirName); PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + pattern); try (DirectoryStream<Path> ds = Files.newDirectoryStream(dir)) { for (Path file : ds) { if (matcher.matches(file.getFileName())) { System.out.println(file.getFileName()); } } } catch (IOException e) { e.printStackTrace(); } } }
Маски можуть поєднуватися з обходом дерева каталогів.
Одне із завдань файлової системи – відстеження стану зазначеного каталогу. Наприклад, програма повинна оновлювати дані про файли і підкаталогах деякого каталогу, якщо інші процеси або потоки управління зумовили виникнення, зміну, видалення файлів і папок тощо. Пакет java.nio.files
надає засоби для реєстрації таких каталогів і відстеження їх стану. Для відстеження змін можна реалізувати інтерфейс WatchService
. Відповідну реалізацію можна отримати за допомогою функції FileSystems.getDefault().newWatchService()
). Клас StandardWatchEventKinds
надає необхідні константи для можливих подій.
Спочатку необхідно зареєструвати необхідний каталог, а потім в нескінченному циклі читати інформацію про події пов'язані з його змінами. Інтерфейс WatchEvent
надає опис можливої події. Наприклад, можна запропонувати таку програму:
package ua.inf.iwanoff.oop.fourth; import java.nio.file.*; import java.util.Scanner; import static java.nio.file.StandardWatchEventKinds.*; public class WatchDir { public static void main(String[] args) throws Exception { System.out.println("Введіть ім\'я каталогу:"); Path dir = Paths.get(new Scanner(System.in).nextLine()); // Створюємо об'єкт WatchService: WatchService watcher = FileSystems.getDefault().newWatchService(); // Реєструємо події, які ми відслідковуємо: WatchKey key = dir.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY); while (true) { // нескінчений цикл key = watcher.take(); // чекаємо на наступний набір подій for (WatchEvent<?> event: key.pollEvents()) { WatchEvent<Path> ev = (WatchEvent<Path>)event; System.out.printf("%s: %s\n", ev.kind().name(), dir.resolve(ev.context())); } key.reset(); // оновлюємо стан набору подій } } }
Бібліотека java.nio.files
підтримує роботу як з символьними посиланнями (symlinks, soft links), так і з жорсткими посиланнями (hard links). Метод createSymbolicLink(нове_посилання, об'єкт_що_існує)
класу Files
створює символьне посилання, метод createLink(нове_посилання, файл_що_існує)
створює жорстке посилання. Метод isSymbolicLink()
повертає true
, якщо переданий йому об'єкт – символьне посилання. Метод eadSymbolicLink()
) дозволяє знайти об'єкт, на який посилається символьне посилання.
3 Приклади програм
3.1 Текстові поля і кнопки
Припустимо, необхідно створити програму графічного інтерфейсу користувача, у якій у двох рядках уведення користувач задає два цілих числа і після натискання кнопки одержує в третьому рядку введення суму цих чисел.
Кореневим контейнером нашого застосунку буде FlowPane.
Отже, досить створити три текстових поля й одну кнопку і послідовно додати їх до панелі. Одержимо таку програму:
package ua.inf.iwanoff.oop.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.2 Робота з кнопками RadioButton
У наведеному нижче прикладі одночасно з вибором кнопки RadioButton
у мітці (Label
) відображається текст вибраної кнопки. Для того, щоб робота кнопок була узгодженою, їх об'єднують у групу ToggleGroup
:
package ua.inf.iwanoff.oop.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.3 Робота з діалоговими вікнами вибору файлів
Припустимо, необхідно прочитати з текстового файлу два числа, а в інший текстовий файл записати їх суму. Вікно міститиме дві кнопки – для вибору відповідно вихідного і результуючого файлів. Можна запропонувати таку програму:
package ua.inf.iwanoff.oop.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.4 Програма графічного інтерфейсу користувача для роботи з даними про переписи населення
Припустимо, необхідно створити програму графічного інтерфейсу користувача, в якій користувачеві надається можливість уведення та редагування даних про переписи населення, зберігання даних в XML-файлі, читання даних з раніше створених XML-файлів, редагування даних, пошук даних за певною ознакою, сортування даних, зберігання даних у новому файлі.
До створеного раніше проекту додаємо новий пакет, у якому будуть розташовані класи графічного інтерфейсу користувача, зокрема головний клас-контролер, FXML-документи та додаткові контролери.
Створюємо новий клас з ім'ям CensusesFX
. Його вміст буде таким:
package ua.inf.iwanoff.oop.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
. Його вміст може бути таким:
<?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.oop.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.oop.fourth; public class CensusesConroller { }
Подальша робота передбачає додавання і налагодження візуальних компонентів за допомогою застосунку SceneBuilder. Завантажуємо застосунок за допомогою контекстного меню файлу CensusesForm.fxml
у Package Explorer (Open with SceneBuilder). Спочатку додаємо посилання на клас-контролер. На закладці Controller у лівій частині вікна SceneBuilder у рядку ControllerClass вводимо ім'я класу-контолеру, вказуючи всі вкладені пакети. В нашому випадку це рядок ua.inf.iwanoff.oop.fourth.CensusesConroller
.
Додаємо головне меню. На кладці 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 майбутнє головне вікно застосунку матиме такий вигляд:
Тепер можна перейти до проектування контролеру. Раніше створений клас XMLCountry
виконуватиме роль моделі. Посилання на нього можна додати до класу-контролеру. Іменованим компонентам повинні відповідати приватні поля, які можна згенерувати автоматично. Механізм генерації коду за допомогою функції контекстного меню 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.oop.fourth.CensusesConroller"> <top> <MenuBar BorderPane.alignment="CENTER"> <menus> <Menu mnemonicParsing="false" text="Файл"> <items> <MenuItem mnemonicParsing="false" text="Новий" onAction="#doNew"/> <MenuItem mnemonicParsing="false" text="Відкрити..." onAction="#doOpen"/> <MenuItem mnemonicParsing="false" text="Зберегти..." onAction="#doSave"/> <MenuItem mnemonicParsing="false" text="Вийти" onAction="#doExit"/> </items> </Menu> <Menu mnemonicParsing="false" text="Редагування"> <items> <MenuItem mnemonicParsing="false" text="Додати рядок" onAction="#doAdd"/> <MenuItem mnemonicParsing="false" text="Видалити останній рядок" onAction="#doRemove"/> </items> </Menu> <Menu mnemonicParsing="false" text="Робота"> <items> <MenuItem mnemonicParsing="false" text="Сортувати за кількістю населення" onAction="#doSortByPopulation"/> <MenuItem mnemonicParsing="false" text="Сортувати за алфавітом коментарів" onAction="#doSortByComments"/> </items> </Menu> <Menu mnemonicParsing="false" text="Допомога"> <items> <MenuItem mnemonicParsing="false" text="Про програму..." onAction="#doAbout"/> </items> </Menu> </menus> </MenuBar> </top> <left> <AnchorPane prefHeight="472.0" prefWidth="200.0" BorderPane.alignment="CENTER"> <children> <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" /> </children> </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>
Для реалізації оброблювачів подій придадуться деякі допоміжні методи. Можна навести весь код класу CensusesConroller
:
package ua.inf.iwanoff.oop.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.oop.second.XMLCountry; import ua.inf.iwanoff.oop.first.AbstractCensus; import javax.xml.bind.JAXBException; 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 CensusesConroller implements Initializable { // Посилання на клас-модель: private XMLCountry country = new XMLCountry(); // Список, вміст якого відображатиметься в таблиці: private ObservableList<AbstractCensus> 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<AbstractCensus> tableViewCensuses; @FXML private TableColumn<AbstractCensus, Integer> tableColumnYear; @FXML private TableColumn<AbstractCensus, Integer> tableColumnPopulation; @FXML private TableColumn<AbstractCensus, Number> tableColumnDensity; @FXML private TableColumn<AbstractCensus, 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 XMLCountry(); 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.readFromFile(file.getCanonicalPath()); // Заповнюємо текстові поля прочитаними даними: textFieldCountry.setText(country.getName()); textFieldArea.setText(country.getArea() + ""); textAreaResults.setText(""); // Очищаємо та оновлюємо таблицю: tableViewCensuses.setItems(null); updateTable(); } catch (IOException e) { showError("Файл не знайдено"); } catch (JAXBException e) { showError("Неправильний формат файлу"); } } } @FXML private void doSave(ActionEvent event) { FileChooser fileChooser = getFileChooser("Зберегти XML-файл"); File file; if ((file = fileChooser.showSaveDialog(null)) != null) { try { updateSourceData(); // оновлюємо дані в моделі country.writeToFile(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++) { AbstractCensus c = 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++) { AbstractCensus c = country.getCensus(i); if (c.containsSubstring(textFieldText.getText())) { showResults(c); } } } private void showResults(AbstractCensus census) { textAreaResults.appendText("Перепис " + census.getYear() + " року.\n"); textAreaResults.appendText("Коментар:" + census.getComments() + "\n"); textAreaResults.appendText("\n"); } private void updateSourceData() { // Переписуємо дані в модель з observableList country.clearCensuses(); for (AbstractCensus c : observableList) { country.addCensus(c); } } private void setDensity() { // Визначаємо механізм автоматичного перерахування комірок // стовпця tableColumnDensity, коли змінюються інші дані: tableColumnDensity.setCellFactory(cell -> new TableCell<AbstractCensus, 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<AbstractCensus, Integer> t) { // Оновлюємо дані про рік: AbstractCensus c = t.getTableView().getItems().get(t.getTablePosition().getRow()); c.setYear(t.getNewValue()); } private void updatePopulation(CellEditEvent<AbstractCensus, Integer> t) { // Оновлюємо дані про населення: AbstractCensus c = t.getTableView().getItems().get(t.getTablePosition().getRow()); c.setPopulation(t.getNewValue()); setDensity(); // перераховуємо щільність населення } private void updateComments(CellEditEvent<AbstractCensus, String> t) { // Оновлюємо коментарі: AbstractCensus c = t.getTableView().getItems().get(t.getTablePosition().getRow()); c.setComments(t.getNewValue()); } private void updateTable() { // Заповнюємо observableList: List<AbstractCensus> list = new ArrayList<AbstractCensus>(); 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(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?
- Як здійснюється візуальне редагування вікна застосунку графічного інтерфейсу користувача?
- Як визначити поняття "файлова система"?
- Які можна назвати типові функції для роботи з файловою системою?
- Які засоби надає Java для роботи з файловою системою?
- Як отримати атрибути файлу за допомогою засобів класу
java.io.File
? - Чим відрізняються функції
list()
іlistFiles()
? - Як здійснити копіювання файлів засобами
java.io
? - Як здійснити копіювання файлів засобами
java.nio
?