Лабораторна робота 3
Створення програм графічного інтерфейсу користувача
1 Завдання на лабораторну роботу
1.1 Індивідуальне завдання
Необхідно реалізувати мовою Java за допомогою засобів JavaFX застосунок графічного інтерфейсу користувача, в якому здійснюється обробка даних індивідуальних завдань попередніх лабораторних робіт. Головне вікно повинно містити меню, в якому необхідно реалізувати такі функції:
- створення нового набору даних
- завантаження даних з XML-документу для редагування
- зберігання змінених даних в XML-документі
- пошук за ознаками, визначеними в лабораторній роботі № 5 курсу "Алгоритмізація та програмування" попереднього семестру
- здійснення сортування за ознаками, визначеними в лабораторній роботі № 1
- отримання вікна "Про програму" з даними про програму і автора.
У лівій частині вікна слід розташувати рядки для введення скалярних даних, область відображення для результатів пошуку, а також кнопки, які забезпечують виконання основних функцій програми. В середній частині вікна слід розташувати таблицю для відображення та редагування даних.
1.2 Перелік для опису місяців року
Створити перелік "Місяць". Необхідно визначати у конструкторі і зберігати кількість днів (для невисокосного року). Додати методи отримання попереднього та наступного місяця, а також функцію, яка повертає сезон для кожного місяця. Передбачити виведення місяців українською (російською) мовою. Створити статичну функцію виведення даних про усі місяці. Протестувати переліку в функції main()
тестового класу.
1.3 Дані про користувачів
Представити дані про користувачів у вигляді асоціативного масиву (ім'я / пароль) з припущенням, що всі імена користувачів різні. Вивести дані про користувачів з довжиною пароля більше 6 символів.
1.4 Міні-калькулятор
Створити застосунок графічного інтерфейсу користувача, в якому після введення чисел у двох рядках типу TextField
виконується одна з чотирьох арифметичних дій (залежно від вибраної кнопки RadioButton
). Результат виводиться в інше текстове поле.
1.5 Словник (додаткове завдання)
Розробити програму графічного інтерфейсу користувача перегляду слів невеличкого англо-українського (англо-російського) словника. Реалізувати функції пошуку слова, додавання нових слів. Для зберігання даних використати Map
.
1.6 Пошук різних слів у реченні (додаткове завдання)
Увести речення, створити колекцію (SortedSet
) різних слів речення та вивести ці слова в алфавітному порядку.
2 Методичні вказівки
2.1 Переліки
Починаючи з версії JDK 1.5 (Java 5), реалізовано новий тип-посилання - перелік - список можливих значень, які може отримувати змінна цього типу. У найпростішій своїй формі переліки Java аналогічні відповідним конструкціям C++ і C#.
enum DayOfWeek { SUNDAY,
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY } ... DayOfWeek d = DayOfWeek.WEDNESDAY;
Перелічені константи вважаються відкритими (public
). Тип enum
, як і клас, може бути відкритим або пакетним. Для імен можливих значень використовують великі літери, оскільки фактично це константи. З константами зв'язані цілі значення, у попередньому прикладі - відповідно від 0 до 6. Можна отримати ці цілі значення за допомогою функції ordinal()
, а ім'я константи - за допомогою метода name()
. Наприклад:
DayOfWeek d = DayOfWeek.WEDNESDAY; System.out.println(d.name() + " " + d.ordinal());
За допомогою статичної функції values()
можна отримати масив елементів переліку:
for (int i = 0; i < DayOfWeek.values().length; i++) { System.out.println(DayOfWeek.values()[i]); }
Статична функція valueOf()
дозволяє отримати елемент переліку за його ім'ям. Наприклад, нам необхідно отримати ціле значення, пов'язане з певним елементом:
System.out.println(DayOfWeek.valueOf("FRIDAY").ordinal());
У загальному випадку переліки Java надають можливості по визначенню і перевантаженню методів, створенню додаткових полів тощо. Наприклад, до переліку DayOfWeek
можна додати статичну функцію printAll()
:
static void printAll() { for (DayOfWeek d : values()) { System.out.println(d); } }
Можна перевантажити виведення переліку через визначення функції toString()
:
enum Gender { MALE, FEMALE; @Override public String toString() { switch (this) { case MALE: return "чоловіча стать"; case FEMALE: return "жіноча стать"; } return "щось неможливе!"; } } public class GenderTest { public static void main(String[] args) { Gender g = Gender.FEMALE; System.out.println(g); } }
Константи, можна зв'язати з відповідними значеннями. Наприклад, перелік "Супутник Марса" містить поле "Відстань від центру Марса". У нашому прикладі необхідно також додати конструктор та додаткові елементи:
package ua.inf.iwanoff.oop.third; enum MoonOfMars { PHOBOS(9377), DEIMOS(23460); private double distance; private MoonOfMars(double distance) { this.distance = distance; } double getDistance() { return distance; } @Override public String toString() { return name() + ". " + distance + " km. from Mars"; } } public class MoonsOfMarsTest { public static void main(String[] args) { MoonOfMars m = MoonOfMars.PHOBOS; System.out.println(m); // PHOBOS. 9377.0 km from Mars } }
Як видно з тексту, наявність конструктора обумовлює опис констант з визначенням фактичних параметрів.
2.2 Черги, стеки, множини і асоціативні контейнери
2.2.1 Робота з чергами та стеками
Черга в широкому сенсі є структурою даних, яку заповнюють поелементно, та отримують з неї об'єкти за певним правилом. У вузькому сенсі цим правилом є "першим прийшов - першим вийшов" (FIFO, First In - First Out). У черзі, організованій за принципом FIFO, додавання елемента можливо лише в кінець черги, отримання - тільки з початку черги.
У бібліотеці контейнерів черга представлена інтерфейсом Queue
. Методи, оголошені в цьому інтерфейсі, наведені в таблиці:
Тип операції | Генерує виняток | Повертає спеціальне значення |
---|---|---|
Додавання | add(e) |
offer(e) |
Видалення з отриманням елемента | remove() |
poll() |
Отримання елемента без видалення | element() |
peek() |
Метод offer()
повертає false
, якщо не вдалося додати елемент, наприклад, якщо реалізована черга з обмеженою кількістю елементів. У цьому випадку метод add()
генерує виняток. Аналогічно remove()
і element()
генерують виняток, якщо черга порожня, а poll()
і peek()
в цьому випадку повертають null
.
Для реалізації черги найзручніше використовувати клас LinkedList
, який реалізує інтерфейс Queue
. Наприклад:
package ua.inf.iwanoff.oop.third; import java.util.LinkedList; import java.util.Queue; public class SimpleQueueTest { public static void main(String[] args) { Queue<String> queue = new LinkedList<>(); queue.add("First"); queue.add("Second"); queue.add("Third"); queue.add("Fourth"); String s; while ((s = queue.poll()) != null) { System.out.print(s + " "); // First Second Third Fourth } } }
Клас PriorityQueue
впорядковує елементи відповідно до компаратору (об'єкта класу, що реалізує інтерфейс Comparator
), заданого в конструкторі як параметр. Якщо об'єкт створити за допомогою конструктора без параметрів, елементи будуть упорядковані в природному порядку (для чисел - за зростанням, для рядків - за абеткою). Наприклад:
package ua.inf.iwanoff.oop.third; import java.util.PriorityQueue; import java.util.Queue; public class PriorityQueueTest { public static void main(String[] args) { Queue<String> queue = new PriorityQueue<>(); queue.add("First"); queue.add("Second"); queue.add("Third"); queue.add("Fourth"); String s; while ((s = queue.poll()) != null) { System.out.print(s + " "); // First Fourth Second Third } } }
Інтерфейс Deque
(дек, double-ended-queue) надає можливість додавати й видаляти елементи з обох кінців. Методи, оголошені в цьому інтерфейсі, наведені в таблиці:
Тип операції | Робота з першим елементом | Робота з останнім елементом |
---|---|---|
Додавання | addFirst(e) |
addLast(e) |
Видалення з отриманням елемента | removeFirst() |
removeLast() |
Отримання елемента без видалення | getFirst() |
getLast() |
Кожна з пар представляє відповідно функцію, яка генерує виняток, і функцію, яка повертає спеціальне значення. Є також методи, що дозволяють видалити перше або останнє входження заданого елемента (removeFirstOccurence()
і removeLastOccurence()
відповідно).
Для реалізації інтерфейсу можна використовувати спеціальний клас ArrayDeque
, або зв'язний список (LinkedList
).
Стек - це структура даних, організована за принципом "останній прийшов - перший вийшов" (LIFO, last in - first out). Можливі три операції зі стеком: додавання елементу (push), видалення елементу (pop) і читання головного елемента (peek).
У JRE 1.1 стек представлений класом Stack
. Наприклад:
package ua.inf.iwanoff.oop.third; import java.util.Stack; public class StackTest { public static void main(String[] args) { Stack<String> stack = new Stack<>(); stack.push("First"); stack.push("Second"); stack.push("Third"); stack.push("Fourth"); String s; while (!stack.isEmpty()) { s = stack.pop(); System.out.print(s + " "); // Fourth Third Second First } } }
Цей клас в даний час не рекомендований до використання. Замість нього можна використовувати інтерфейс Deque
, який оголошує аналогічні методи. Наприклад:
package ua.inf.iwanoff.oop.third; import java.util.ArrayDeque; import java.util.Deque; public class AnotherStackTest { public static void main(String[] args) { Deque<String> stack = new ArrayDeque<>(); stack.push("First"); stack.push("Second"); stack.push("Third"); stack.push("Fourth"); String s; while (!stack.isEmpty()) { s = stack.pop(); System.out.print(s + " "); // Fourth Third Second First } } }
Стеки часто використовуються в різних алгоритмах. Зокрема, за допомогою стеку в деяких задачах можна позбутися рекурсії.
2.2.2 Робота з множинами
Множина - це колекція, що не містить однакових елементів. Три основних реалізації інтерфейсу Set
-
HashSet
, LinkedHashSet
і TreeSet
. Як і списки, множини є узагальненими типами. Класи HashSet
і LinkedHashSet
використовують хеш-коди для ідентифікації елемента. Клас TreeSet
використовує двійкове дерево для збереження елементів і гарантує їх певний порядок.
Метод add()
додає елемент до множини і повертає true
якщо елемент раніше був відсутній. В іншому випадку елемент не додається, а метод add()
повертає false
. Усі елементи множини видаляються за допомогою методу clear()
.
Set<String> s = new HashSet<String>(); System.out.println(s.add("First")); // true System.out.println(s.add("Second")); // true System.out.println(s.add("First")); // false System.out.println(s); // [First, Second] s.clear(); System.out.println(s); // []
Метод remove()
видаляє зазначений елемент множини, якщо такий є. Метод contains()
повертає true
, якщо множина містить зазначений елемент.
У наведеному нижче прикладі до множини цілих чисел додається десять випадкових значень у діапазоні від -9 до 9:
package ua.inf.iwanoff.oop.third; import java.util.*; public class SetOfIntegers { public static void main(String[] args) { Set<Integer> set = new TreeSet<Integer>(); Random random = new Random(); for (int i = 0; i < 10; i++) { Integer k = random.nextInt() % 10; set.add(k); } System.out.println(set); } }
Результуюча множина як правило містить менш, ніж 10 чисел, оскільки окремі значення можуть повторюватися. Оскільки ми використовуємо TreeSet
, числа зберігаються та виводяться в упорядкованому (за зростанням) вигляді. Для того, щоб додати саме десять різних чисел, програму можна модифікувати, наприклад із застосуванням циклу while
замість for
:
while (set.size() < 10) { . . . }
Можна створити масив, який містить копії елементів множини. В такий спосіб можна звертатися до елементів за індексом. Наприклад, так можна вивести елементи множини в зворотному порядку:
Set<Integer> set = new HashSet<>(Arrays.asList(1, 2, 4)); Object[] arr = set.toArray(); for (int i = set.size() - 1; i >= 0; i--) { System.out.println(arr[i]); }
Оскільки множина може містити тільки різні елементи, її можна використати для підрахунку різних слів, літер, цифр тощо - створюється множина та викликається метод size()
. Застосовуючи TreeSet
, можна виводити слова та літери в алфавітному порядку. У наведеному нижче прикладі вводиться речення та виводяться всі різні літери речення (не враховуючи роздільників) в алфавітному порядку:
package ua.inf.iwanoff.oop.third; import java.util.*; public class Sentence { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); // Функція nextLine() читає рядок до кінця: String sentence = scanner.nextLine(); // Створюємо множину роздільників: Set<Character> delimiters = new HashSet<Character>( Arrays.asList(' ', '.', ',', ':', ';', '?', '!', '-', '(', ')', '\"')); // Створюємо множину літер: Set<Character> letters = new TreeSet<Character>(); // Додаємо всі літери крім роздільників: for (int i = 0; i < sentence.length(); i++) { if (!delimiters.contains(sentence.charAt(i))) { letters.add(sentence.charAt(i)); } } System.out.println(letters); } }
Порядок сортування елементів TreeSet
можна задати, реалізувавши інтерфейс Comparable
, або передавши в конструктор TreeSet
посилання на об'єкт класу, який реалізує інтерфейс Comparator
. Наприклад, так можна відсортувати дерево у зворотному порядку:
package ua.inf.iwanoff.oop.third; import java.util.*; public class CompTest { public static void main(String args[]) { TreeSet<String> ts = new TreeSet<String>(new Comparator<String>() { @Override public int compare(String s1, String s2) { return s2.compareTo(s1); } }); ts.add("C"); ts.add("E"); ts.add("D"); ts.add("B"); ts.add("A"); ts.add("F"); for (String element : ts) System.out.print(element + " "); } } }
2.2.3 Робота з асоціативними масивами
Асоціативні масиви можуть зберігати пари посилань на об'єкти. Асоціативні масиви теж є узагальненими типами. Асоціативні масиви у Java представлені узагальненим інтерфейсом Map
, який реалізовано, зокрема, класом HashMap
. Інтерфейс SortedMap
, похідний від Map
, вимагає впорядкованого за ключем зберігання пар. Інтерфейс NavigableMap
, що з'явився в java SE 6, розширює SortedМap
і додає нові можливості пошуку за ключем. Цей інтерфейс реалізовано класом TreeMap
.
Кожне значення (об'єкт), яке зберігається в асоціативному масиві, зв'язується з конкретним значенням іншого об'єкта (ключа). Метод put(key, value)
додає значення (value
) і асоціює з ним ключ (key
). Якщо асоціативний масив раніше містив пари з вказаним ключем, нове значення заміщає старе. Метод put()
повертає попереднє значення, пов'язане з ключем, або null
, якщо ключ був відсутній. Метод get(Object key)
повертає по об'єкт за заданим ключем. Для перевірки знаходження ключа та значення застосовуються методи containsKey()
і containsValue()
.
На логічному рівні можна представити асоціативний масив через три допоміжні колекції:
keySet
-
множина значень ключа;values
-
список значений;entrySet
-
множина пар ключ-значення.
Через відповідні функції keySet()
, values()
та entrySet()
можна здійснювати певні дії, в першу чергу послідовне проходження елементів.
У наведеному нижче прикладі обчислюється кількість входжень різних слів у речення. Слова і відповідні кількості зберігаються в асоціативному масиві. Використання класу TreeMap
гарантує алфавітний порядок слів (ключів).
package ua.inf.iwanoff.oop.third; import java.util.*; public class WordsCounter { public static void main(String[] args) { Map<String, Integer> m = new TreeMap<String, Integer>(); String s = "the first men on the moon"; StringTokenizer st = new StringTokenizer(s); while (st.hasMoreTokens()) { String word = st.nextToken(); Integer count = m.get(word); m.put(word, (count == null) ? 1 : count + 1); } for (String word : m.keySet()) { System.out.println(word + " " + m.get(word)); } } }
Використання keySet()
передбачає окремий пошук кожного значення за ключем. Більш рекомендованим є обхід асоціативного масиву через множину пар:
for (Map.Entry<?, ?> entry : m.entrySet()) System.out.println(entry.getKey() + " " + entry.getValue());
Тут метод entrySet()
дозволяє одержати представлення асоціативного масиву у вигляді колекції Set
.
Порядок сортування елементів TreeMap
також можна змінити, вказавши як параметр конструктора TreeMap
об'єкт класу, який реалізує інтерфейс Comparator
, або задавши ключ як об'єкт класу, який реалізує інтерфейс Comparable
:
package ua.inf.iwanoff.oop.third; import java.util.*; public class TreeMapKey implements Comparable<TreeMapKey> { private String name; public String getName() { return name; } public TreeMapKey(String name) { super(); this.name = name; } @Override public int compareTo(TreeMapKey o) { return name.substring(o.getName().indexOf(" ")).trim() .compareToIgnoreCase(o.getName().substring(o.getName().indexOf(" ")).trim()); } public static void main(String args[]) { TreeMap<TreeMapKey, Integer> tm = new TreeMap<TreeMapKey, Integer>(); tm.put(new TreeMapKey("Петро Іванов"), new Integer(1982)); tm.put(new TreeMapKey("Іван Петров"), new Integer(1979)); tm.put(new TreeMapKey("Василь Сидоров"), new Integer(1988)); tm.put(new TreeMapKey("Сидор Васильєв"), new Integer(1980)); for (Map.Entry<TreeMapKey, Integer> me : tm.entrySet()) { System.out.print(me.getKey().getName() + ": "); System.out.println(me.getValue()); } System.out.println(); } }
Клас Hashtable
- одна з реалізацій інтерфейсу Map
. Hashtable
крім розміру має ємність (розмір буфера, виділеного під елементи масиву). Крім цього він характеризується показником завантаженості - часткою буфера, після заповнення якої ємність автоматично збільшується. Конструктор Hashtable()
без параметрів створює порожній об'єкт з ємністю в 101 елемент і показником завантаженості 0.75.
Клас Properties
, похідний від Hashtable
, замість пар довільних об'єктів зберігає пари рядків. Якщо в конкретній задачі і ключі і значення елементів асоціативного масиву мають тип String
, зручніше скористатися класом Properties
. У класі Properties
визначені методи getProperty(String key)
і setProperty(String key, String value)
.
2.3 Використання Java для створення застосунків графічного інтерфейсу користувача (GUI Applications)
2.3.1 Загальні концепції
Інтерфейс користувача - це набір технічних та програмних засобів, за допомогою яких людина взаємодіє з комп'ютером. Далі йтиметься про програмні засоби інтерфейсу комп'ютеру.
Інтерфейс командного рядку - це метод взаємодії з програмою за допомогою інтерпретатору команд, які користувач уводить, як правило, у текстовому режимі, або у спеціальному консольному вікні. Результат виконання команд також відображається у консольному вікні. Програми такого типу також називають консольними застосунками. Консольні застосунки не беруть участі в обміні системними повідомленнями, а також не можуть відсилати повідомлення іншим застосункам.
Графічний інтерфейс користувача (Graphical user interface, GUI) дає можливість користувачеві взаємодіяти з комп'ютером за допомогою графічних елементів управляння (вікон, піктограм, меню, кнопок, списків тощо) та технічних пристроїв позиціонування, таких як маніпулятор "миша" Програми, які реалізують цей тип інтерфейсу, мають назву застосунків графічного інтерфейсу користувача.
Реалізація застосунків графічного інтерфейсу користувача базується на механізмі отримання та обробки подій. Уся програма складається з ініціалізації (реєстрації візуальних елементів управління) та основного циклу отримання та обробки подій. Події - це переміщення або натискання кнопок миші, клавіатурне введення, тощо. Кожний зареєстрований візуальний елемент управління може отримувати події, які до нього стосуються, та виконувати функції обробки цих подій.
Засоби розробки графічного інтерфейсу користувача є складовою частиною Java-технологій від початку існування Java. Першою бібліотекою Java, яка надавала засоби створення програм графічного інтерфейсу, була бібліотека Abstract Window Toolkit (AWT). AWT є частиною Java Foundation Classes (JFC) - стандартного API для реалізації графічного інтерфейсу Java-програми. У перші роки існування Java бібліотека AWT використовувалася переважно для створення аплетів.
Основним недоліком бібліотеки AWT є орієнтація на графічні діалогові компоненти, які надають конкретні операційні системи та графічні оболонки. Це призводить, з одного боку до певних проблем з розгортанням програми на різних програмних платформах, з іншого боку, обмежує виразні засоби застосунків, оскільки необхідно орієнтуватися тільки на ті візуальні компоненти, які присутні на всіх платформах. Такі візуальні компоненти прийнято називати "великоваговими" (highweigh). Цей та інші недоліки AWT виправлені в бібліотеці Swing. Бібліотека Swing також надає деякі додаткові візуальні компоненти, такі як панель із закладками, списки що випадають, таблиці, дерева та ін.
Наразі стандартними засобами розробки додатків графічного інтерфейсу користувача в Java є бібліотеки AWT і Swing, а також платформа JavaFX, засоби якої є альтернативою Swing. Крім того, різні розробники надають альтернативні нестандартні бібліотеки, такі як Qt Jambi, Standard Window Toolkit (SWT), XML Window Toolkit (XWT). Дві останні бібліотеки, поряд з AWT і Swing, підтримуються Eclipse.
2.3.2 Створення та використання аплетів
Аплети - це програмні компоненти Java, які зберігаються на web-сервері, завантажуються на клієнтський комп'ютер, та виконуються за допомогою віртуальної машини Java web-браузера клієнта.
Все необхідне для виконання аплету міститься в тегу <applet>
у тексті HTML-файлу. Аплет зазвичай відповідає за певну прямокутну область у вікні браузера. Координати цієї області також можуть бути задані в теґу <applet>
. Наприклад:
<applet codebase = "." code = "test.Applet1.class" width = 400 height = 300> </applet>
Атрибут codebase
задає місце розміщення класу, що реалізує аплет (в наведеному прикладі - поточна тека). Атрибут code
- ім'я класу. Атрибути width
і height
задають розміри вікна аплета.
Для реалізації аплетів як базові використовують класи java.applet.Applet
(Java 1) та javax.swing.JApplet
(Java 2). Життєвий цикл визначається такими методами класу Applet
:
public void
init()
викликається браузером одразу після завантаження аплета перед першим викликом методуstart()
; цей метод потрібно перевизначати практично завжди, якщо в аплеті потрібна хоч якась ініціалізація;public void
start()
викликається браузером під час кожного "відвідування" сторінки;public void stop()
викликається браузером під час деактивізації сторінки;public void
destroy()
завжди викликається під час виходу з браузера і під час перезавантаження аплету.
З міркувань безпеки на аплети накладаються певні обмеження:
- аплетам, завантаженим з мережі, заборонені операції читання і запису файлів з локальної файлової системи;
- аплети не повинні виконувати мережеві з'єднання з усіма хостами, крім того, з якого був отриманий аплет;
- аплетам не дозволено запускати програми на клієнтській системі;
- аплетам заборонено завантажувати нові бібліотеки і викликати програми, зовнішні по відношенню до Java-машини.
Аплет можна переглянути за допомогою програми appletviewer.exe, що входить до складу JDK. Практично будь-який діалоговий застосунок на Java може бути реалізовано так, що він зможе працювати і як програма і як аплет. Однак аплету не потрібна функція main()
.
Проблеми використання аплетів пов'язані з необхідністю забезпечення наявності відповідної версії Java, яку підтримує Web-браузер клієнта.
2.3.3 Застосування бібліотеки javax.swing
Бібліотека javax.swing
пропонує розробникові низку стандартних класів, які можна використовувати для проектування графічного інтерфейсу користувача. Ця бібліотека розширила попередню менш вдалу бібліотеку AWT (Abstract Window Toolkit) засоби якої використовує для обробки подій, роботи з графікою тощо. Для ідентифікації приналежності до бібліотеки javax.swing
до імен класів візуальних компонентів додана літера J
(наприклад, JButton
, JPanel
і т. д.).
На відміну від компонентів AWT, компоненти Swing є "легковагими" (lightweight). Це означає, що компоненти Swing використовують засоби Java для відображення елементів графічного інтерфейсу користувача на поверхні вікна, без використання компонентів операційної системи.
Як і більшість бібліотек графічного інтерфейсу користувача, бібліотека javax.swing
підтримує концепцію головного вікна застосунку. Це головне вікно створюється як об'єкт класу JFrame
, або похідного від нього. Далі до головного вікна додають візуальні компоненти - мітки (JLabel
), кнопки (JButton
), рядки введення (JTextField
) тощо.
Для того, щоб створити найпростішу програму графічного інтерфейсу користувача, необхідно створити новий клас з функцією main()
, а потім у вихідному тексті додати твердження import
:
import javax.swing.*;
У функції main()
створюємо нове вікно та вказуємо його заголовок:
. . . public class HelloWorldSwing { public static void main(String[] args) { JFrame frame = new JFrame("Привіт"); . . . } }
Далі додаємо нову мітку до компоненту, який відповідає за вміст вікна. Створюємо новий об'єкт типу JLabel
:
frame.getContentPane().add(new JLabel("Привіт, світ!"));
За допомогою методу pack()
здійснюється припасування розмірів вікна. Після виклику функції setVisible(true)
вікно з'являється на екрані. Можна навести весь текст програми:
package ua.inf.iwanoff.oop.third; 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.4 Використання засобів JavaFX
2.4.1 Основні концепції платформи JavaFX
Від початку засоби JavaFX були представлені як програмна платформа для створення так званих насичених інтернет-застосунків (rich internet applications), визначаючи архітектуру, каркас і стиль розробки застосунку. Оскільки JavaFX надає велику кількість інтерфейсів і класів для розробки застосунків графічного інтерфейсу користувача, засоби JavaFX фактично є сучасною альтернативою бібліотеці javax.swing. Перша версія платформи (JavaFX 1.0) вийшла у 2008 році та включала спеціальну скриптову мову JavaFX Script для опису графічного інтерфейсу. У 2011 році вийшла версія JavaFX 2.0 під орудою Oracle. Розробники цієї версії відмовилися від спеціальної скриптової мови на користь Java. Версія JavaFX 8, яка вийшла у 2014 році, розроблена у відповідності до можливостей та стилю Java 8, надає можливості роботи з 3D-графікою, а також пропонує нові візуальні компоненти. Номер версії відповідає Java 8, тому версії 3, 4, 5, 6 і 7 відсутні.
Основними рисами, що відрізняють JavaFX від попередніх бібліотек підтримки графічного інтерфейсу користувача є такі:
- вбудована підтримка патерну проектування MVC (Model-View-Controller);
- можливість декларативного опису візуальних компонентів (мова FXML);
- сучасний стиль візуальних компонентів;
- підтримка розширених можливостей взаємодії користувача з застосунком;
- можливість використання css-стилів для стилізації елементів користувацького інтерфейсу;
- можливість використання 3D-графіки;
- спрощена модель розгортання застосунків.
Є також низка додаткових можливостей, пов'язаних з графікою, текстом, взаємодією з раніше створеними бібліотеками і т. д.
Платформу JavaFX можна розглядати як альтернативу попереднім бібліотекам графічного інтерфейсу користувача, яка покликана надалі повністю їх замінити.
2.4.2 Створення найпростішого застосунку. Структура програми JavaFX
Найпростіший застосунок графічного інтерфейсу користувача, що використовує бібліотеку JavaFX, можна створити, включивши всі необхідні компоненти безпосередньо в Java-коді. Наприклад, наведений нижче клас, похідний від javafx.application.Application
, дозволяє створити вікно з кнопкою посередині:
package ua.inf.iwanoff.oop.third; 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.4.3 Застосування засобів JavaFX у середовищі Eclipse
Для розробки JavaFX-застосунків у середовищі Eclipse необхідно виконати деякі підготовчі дії:
- встановити на комп'ютері JDK 1.8 (останню версію)
- встановити плагін e(fx)lipse
- встановити застосунок Scene Builder
Якщо засоби JDK не встановлені на комп'ютері, їх можна завантажити зі сторінки http://www.oracle.com/technetwork/java/javase/downloads/index-jsp-138363.html#javasejdk. Після того, як JDK 1.8 встановлена на комп'ютері, слід налаштувати середовище Eclipse на роботу зі встановленою версією JDK. В опціях Eclipse (Window | Preferences) обираємо Java | Installed JREs, далі Add... | Next | Directory... Необхідно вибрати встановлену раніше JDK і натиснути Finish. Після цього в списку встановлених JRE обираємо JDK, яку слід встановити за умовчанням і натиснути OK.
Якщо проект було створено раніше, йому слід встановити JDK як JRE за умовчанням в опціях проекту. В головному меню викликаємо функцію Project | Properties | Java Build Path, далі на закладці Libraries обираємо JRE System Library, натискуємо кнопку Edit... , переключаємо System Library в Alternate JRE, у відповідному рядку вибираємо встановлену раніше JDK.
Примітка: слід також перевірити, щоб також був визначений необхідний рівень відповідності компілятора (Compiler compliance level): Window | Preferences головного меню, далі у гілці дерева Java | Compiler проти Compiler compliance level встановлюємо 1.8
.
Для встановлення плагіну e(fx)lipse у середовищі Eclipse слід скористатися функцією головного меню Help | Install New Software..., далі у вікні Install в рядку Work With: увести адресу http://download.eclipse.org/efxclipse/updates-released/1.2.0/site
. У середній частині вікна з'явиться список засобів програмного забезпечення серед яких слід відселектувати e(fx)clipse - install і натиснути Next. Через деякий час у вікні будуть відображені деталі встановлення (Install Details) і ми знову натискаємо кнопку Next. Далі слід погодитися з умовами ліцензії та натиснути Finish. Після встановлення необхідного програмного забезпечення слід перезавантажити IDE Eclipse.
Тепер можна створити перший застосунок JavaFX. Створюємо новий проект JavaFX (File | New | Project... далі у вікні New Project обираємо JavaFX | JavaFX Project та натискаємо Next). Уводимо назву проекту, наприклад "HelloFX", та натискаємо Finish. Цей проект можна завантажити на виконання та отримати порожнє вікно посередині екрану.
2.4.4 Налаштування середовища IntelliJ IDEA для створення застосунків JavaFX
Інтегроване середовище IntelliJ IDEA надає спеціальний плагін, що дозволяє створювати JavaFX-проекти. Для цього у середовищі IntelliJ IDEA необхідно виконати деякі підготовчі дії:
- переконатися, що версія JDK не нижче 1.8.4
- перевірити, чи включений необхідний JavaFX-плагін; це можна здійснити в такий спосіб:
- у вікні Settings (команда меню File | Settings...) IntelliJ IDEA вибрати позицію Plugins
- у списку плагінів відшукати плагін JavaFX; якщо він не включений, включити його.
Новий проект JavaFX можна створити, вибравши в лівій частині вікна майстра проектів Java FX. На наступній сторінці майстра вказуємо ім'я проекту, наприклад, FirstFX
. Автоматично створюється програма, що зображує порожнє вікно з заголовком Hello World
. Проект містить пакет sample
з трьома файлами:
Main.java
містить класMain
, похідний відjavafx.application.Application
. Цей клас містить функіцюmain()
.Controller.java
містить спочатку порожній клас, в який будуть додаватися функції - оброблювачі подій.sample.fxml
містить опис елементів інтерфейсу користувача. Файл містить посилання на класController
.
Імена створеного пакету і файлів можна змінити за допомогою засобів рефакторингу.
Якщо застосунок JavaFX потрібно додати до вже існуючого проекту, це можна зробити в такий спосіб: вибрати в дереві необхідний пакет і скористатися функцією контекстного меню New | JavaFXApplication. Далі у вікні New JavaFXApplication вводимо назву застосунку. При цьому IntelliJ IDEA обмежується тільки генерацією класу, похідного від javafx.application.Application
.
2.4.5 Використання мови FXML для розмічення елементів графічного інтерфейсу користувача
Сучасні уявлення про проектування графічного інтерфейсу користувача передбачають деклративний спосіб визначення складу вікон (фреймів, активностей) і властивостей візуальних компонентів. Найбільш популярний підхід - використання мови XML, яка забезпечує адекватний опис ієрархії візуальних об'єктів через механізм вкладених тегів і визначення властивостей компонентів через використання атрибутів. Різні варіанти мов, побудованих на XML, використовуються в Android, .NET (WPF) тощо. Використання декларативного опису користувацького інтерфейсу дозволяє більш окреслено відокремити дані й алгоритми (model) від елементів користувацького інтерфейсу (view) та забезпечити їхній зв'язок через спеціальний клас - так званий контролер (controller), в якому зазвичай розміщують методи-обробники подій, пов'язані з візуальними елементами. Крім того, декларативна мова опису зовнішнього вигляду та елементів управління дозволяє залучити до проектування GUI дизайнерів, для яких XML-подібна декларативна мова більш прийнятна, ніж мови програмування.
Перша версія JavaFX включала окрему скриптову мову JavaFX Script, яка дозволяла декларативно описувати компоненти користувацького інтерфейсу. Починаючи з другої версії JavaFX автори платформи відмовилися від JavaFX Script і додали до специфікації мову опису елементів користувацького інтерфейсу FXML, яка базується на XML. Використання мови FXML є не єдиним, але рекомендованим підходом.
Для того, щоб скористатися FXML, під час створення нового проекту на третій сторінці майстра нового проекту JavaFX Project на панелі Declarative UI у списку Language вказати FXML. До проекту буде додано FXML-документ Sample.fxml
і клас-контролер SampleController
(ці імена можна змінити). Усталений вміст файлу Sample.fxml
буде таким (якщо ім'я пакету application
не було змінене):
<?xml version="1.0" encoding="UTF-8"?> <?import javafx.scene.layout.BorderPane?> <BorderPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.SampleController"> <!-- TODO Add Nodes --> </BorderPane>
Клас SampleController
також поки порожній:
package application; public class SampleController { }
Вміст класу Main
буде дещо іншим, ніж у попередніх прикладах. Тепер об'єкт-корінь ієрархії компонентів не створюється за допомогою new
, а завантажується з FXML-файлу:
package application; import javafx.application.Application; import javafx.stage.Stage; import javafx.scene.Scene; import javafx.scene.layout.BorderPane; import javafx.fxml.FXMLLoader; public class Main extends Application { @Override public void start(Stage primaryStage) { try { BorderPane root = (BorderPane)FXMLLoader.load(getClass().getResource("Sample.fxml")); Scene scene = new Scene(root,400,400); scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm()); primaryStage.setScene(scene); primaryStage.show(); } catch(Exception e) { e.printStackTrace(); } } public static void main(String[] args) { launch(args); } }
Тепер до FXML-файлу можна додавати візуальні елементи. Наприклад, можна додати кнопку:
<?xml version="1.0" encoding="UTF-8"?> <?import javafx.scene.control.*?> <?import javafx.scene.layout.BorderPane?> <BorderPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.SampleController"> <top> <Button text="Press me" /> </top> </BorderPane>
Крім безпосередньо кнопки необхідно також додати директиву імпорту (<?import javafx.scene.control.*?>
) і вказати, що кнопка буде додана в верхній частині панелі (всередині тегу <top>
) . Як видно з наведеного вище прикладу, візуальні компоненти, описані в FXML-файлі, можуть бути безіменними. Якщо ми хочемо додати ім'я (ідентифікатор), це можна зробити в коді FXML-файлу:
<Button text="Press me" fx:id="button"/>
Для того, щоб не ідентифікувалася помилка, слід додати опис відповідного поля в клас-контролер. Це можна зробити за допомогою функції контекстного меню Quick Fix. Аналогічно можна додати посилання на функцію-оброблювач події onAction
:
<Button text="Press me" fx:id="button" onAction="#doPress"/>
Клас SampleController
тепер буде таким:
package application; import javafx.fxml.FXML; import javafx.scene.control.Button; import javafx.event.ActionEvent; public class SampleController { @FXML Button button; @FXML public void doPress(ActionEvent event) {} }
Анотацію @FXML
використовують для позначення змінних і функцій, пов'язаних з тегами й атрибутами FXML-файлу.
Всередині тіла функції doPress()
можна додати необхідний код, наприклад:
@FXML public void doPress(ActionEvent event) { button.setText("Do not press me"); }
Новий документ FXML можна також додати до існуючого проекту за допомогою головного меню (File | New | Other... | JavaFX | New FXML Document). Його можна розташувати в поточному пакеті.
2.4.6 Компонування в 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
- проміжок між дочірніми вузлами. Детальний приклад використання різних панелей можна знайти в офіційному підручнику Oracle з JavaFX.
2.4.7 Використання елементів управління (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.3 показано роботу з текстовими полями і кнопкою, у прикладі 3.4 - робота з кнопками RadioButton
, у прикладі 3.6 - також з меню та областю редагування тексту TextArea
.
2.4.8 Дочірні та діалогові вікна
Як і інші бібліотеки для створення застосунків графічного інтерфейсу користувача, 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
<T>
.
Модальні діалогові вікна дуже часто використовують для вибору файлів. Клас javafx.stage.FileChooser
надає можливість вибору файлів для читання або запису. Наприклад, якщо поточні підмостки мають назву stage
, так можна створити діалогове вікно вибору файлів для відкриття та отримати об'єкт типу java.io.File
:
FileChooser chooser = new FileChooser(); File file; // Показуємо вікно вибору файлів та перевіряємо, чи підтвердив користувач свій вибір: if ((file = chooser.showOpenDialog(stage)) != null) { // Читаємо з файлу }
Параметр функцій showOpenDialog()
і showSaveDialog()
- вікно, по центру якого розташовується діалогове вікно роботи з файлом. Якщо вказано null
, діалогове вікно відображається по центру екрану. Для з'ясування механізмів використання класу FileChooser
та подій можна скористатися прикладом 3.5.
2.5 Робота з табличними даними в JavaFX
Робота з табличними даними здійснюється за допомогою компоненту TableView
. Основна робота цього компоненту - відображення властивостей об'єктів, які зберігаються в списку узагальненого типу javafx.collections.ObservableList
. Головна відмінність цього списку від традиційних списків - наявність механізму автоматичного сповіщення візуальних компонетів, пов'язаних з ObservableList
, про зміну значень елементів. Крім інтерфейсу ObservableList
, також надається інтерфейс ObservableMap
. Для створення об'єкта, який реалізує інтерфейс ObservableList
, слід скористатися статичною функцією FXCollections.observableList()
з параметром типу "традиційного" списку.
Використання TableView
розглянемо на прикладі. Припустимо, створено клас City
:
package ua.inf.iwanoff.oop.third; 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 (File | New | Other... | JavaFX | JavaFX Project). У класі Main
ініціалізуємо декілька об'єктів типу City
, створюємо таблицю, додаємо колонки, зв'язуємо їх з властивостями класу City
і додаємо таблицю до головного вікна. Вихідний код буде таким:
package ua.inf.iwanoff.oop.third; 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
. Це можна зробити за допомогою функції меню Refactor | Convert Local Variable to Field. Перед застосуванням цієї функції слід помітити код опису відповідного визначення. У діалоговому вікні Convert Local Variable to Field усі значення можна залишити без змін. Код ініціалізації списку, який залишився всередині методу start()
, можна перенести в окремий метод також за допомогою функцій рефакторингу. Цей код також слід помітити, а потім скористатися функцією Refactor | Extract Method.... У діалоговому вікні Extract Method уводимо ім'я методу, наприклад, initList
, і натискаємо OK. Метод додано до опису класу, а його виклик автоматично додається у коді функції start()
, де раніше здійснювалася ініціалізація списку. Аналогічно локальну змінну table
слід перетворити на поле, а заповнення таблиці слід винести в окрему функцію, наприклад, initTable()
. Тепер код класу Main
має такий вигляд:
package ua.inf.iwanoff.oop.third; 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.third; 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 Візуальне проектування програм графічного інтерфейсу користувача за допомогою Scene Builder
На відміну від засобів візуального розташування й налагодження компонентів Swing, вбудованих в середовище Eclipse як плагін (наприклад, Window Builer), для візуального редагування вікон і компонентів JavaFX різні середовища використовують окремий застосунок - Scene Builder. Цей програмний продукт надається Oracle і його можна завантажити з сайту http://www.oracle.com/technetwork/java/javafxscenebuilder-1x-archive-2199384.html. Завантажуємо програму інсталяції, погоджуємося з умовами ліцензії, обираємо версію 2 (або вище) для своєї операційної системи та встановлюємо застосунок. Після встановлення Scene Builder у середовищі Eclipse (зі встановленим плагіном e(fx)clipse) слід вказати шлях до цього застосунку. В меню Eclipse обираємо Window | Preferences, далі у пункті JavaFX дерева вказуємо повне ім'я (зі шляхом) файлу застосунку (SceneBuilder executable), наприклад C:\Program Files (x86)\Oracle\JavaFX Scene Builder 2.0\JavaFX Scene Builder 2.0.exe
, та натискаємо OK.
Завантажити редактор можна через контекстне меню, пов'язане у Package Explorer з FXML-документом (функція Open with SceneBuilder). Головне вікно редактору складається з трьох колонок: ієрархія компонентів у лівій частині вікна (Library), головне підвікно редагування сцени і підвікно редагування властивостей у правій частині вікна (Inspector).
У колонках ліворуч розташовані закладки (вкладки) відповідно до груп компонентів, праворуч - вкладки Properties, Layout і Code. На форму можна перетягувати компоненти і визначати їх властивості. Частину властивостей можна змінювати безпосередньо мишкою (перетягувати, міняти розмір і ін.), частину можна задати у вкладках Properties (спеціальні властивості) і Layout (розташування). У вкладці Code можна, якщо треба, визначити ім'я об'єкта (fx:id
), а також для кожного компонента можна задати реакцію на події.
У режимі попереднього перегляду (Preview | Show Preview in Window) можна взаємодіяти з макетом з метою перевірки функціональності майбутнього коду.
Примітка: для того, щоб працювати з застосунком JavaFX Scene Builder у середовищі IntelliJ IDEA, у вікні налаштувань на закладці Languages & Frameworks | JavaFX вказати шлях до застосунка.
3 Приклади програм
3.1 Опис та використання переліку
Припустимо, необхідно створити перелік, який описує дні тижня. Раніше був наведений приклад такого переліку. До нього слід додати функцію отримання наступного дня, при чому після суботи знов повинна бути неділя тощо. Крім того, необхідна функція перевірки, чи відноситься день до вікенду. Під час виведення також необхідно отримувати номер дня (0 - неділя, 1 - понеділок тощо. д.). У функції main()
, починаючи з понеділка необхідно пройтися по днях тижня, вивести дані про день, а також перевірити, чи це вікенд. Програма матиме такий вигляд:
package ua.inf.iwanoff.oop.third; enum DayOfWeek { SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY; @Override public String toString() { return name() + " " + ordinal(); } DayOfWeek next() { DayOfWeek day = values()[(ordinal() + 1) % values().length]; return day; } boolean isWeekend() { switch (this) { case SATURDAY: case SUNDAY: return true; default: return false; } } } public class EnumTest { public static void main(String[] args) { DayOfWeek d = DayOfWeek.MONDAY; for (int i = 0; i < 7; i++) { d = d.next(); System.out.println(d + " " + d.isWeekend()); } } }
Як і в інших випадках, формою виведення даних керує перекритий метод toString()
.
3.2 Дані про країни в асоціативному масиві
Дані про країни (назва і територія) можна зберігати в асоціативному масиві. Виведення здійснюється за алфавітом країн:
package ua.inf.iwanoff.oop.third; import java.util.Map; import java.util.SortedMap; import java.util.TreeMap; public class Countries { public static void main(String[] args) { SortedMap<String, Double> countries = new TreeMap<>(); countries.put("Україна", 603700.0); countries.put("Німеччина", 357021.0); countries.put("Франція", 547030.0); for (Map.Entry<?, ?> entry : countries.entrySet()) System.out.println(entry.getKey() + " " + entry.getValue()); } }
3.3 Текстові поля і кнопки
Припустимо, необхідно створити програму графічного інтерфейсу користувача, у якій у двох рядках уведення користувач задає два цілих числа і після натискання кнопки одержує в третьому рядку введення суму цих чисел.
Кореневим контейнером нашого застосунку буде FlowPane.
Отже, досить створити три текстових поля й одну кнопку і послідовно додати їх до панелі. Одержимо таку програму:
package ua.inf.iwanoff.oop.third; 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.4 Робота з кнопками RadioButton
У наведеному нижче прикладі одночасно з вибором кнопки RadioButton
у мітці (Label
) відображається текст вибраної кнопки. Для того, щоб робота кнопок була узгодженою, їх об'єднують у групу ToggleGroup
:
package ua.inf.iwanoff.oop.third; 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.5 Робота з діалоговими вікнами вибору файлів
Припустимо, необхідно прочитати з текстового файлу два числа, а в інший текстовий файл записати їх суму. Вікно міститиме дві кнопки - для вибору відповідно вихідного і результуючого файлів. Можна запропонувати таку програму:
package ua.inf.iwanoff.oop.third; 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.6 Програма графічного інтерфейсу користувача для роботи з даними про переписи населення
Припустимо, необхідно створити програму графічного інтерфейсу користувача, в якій користувачеві надається можливість уведення та редагування даних про переписи населення, зберігання даних в XML-файлі, читання даних з раніше створених XML-файлів, редагування даних, пошук даних за певною ознакою, сортування даних, зберігання даних у новому файлі.
До створеного раніше проекту додаємо новий пакет, у якому містимуться класи графічного інтерфейсу користувача, зокрема головний клас-контролер, FXML-документи та додаткові контролери.
У середовищі Eclipse за допомогою функції меню New | Other... | JavaFX | Classes | JavaFX Main Class створюємо новий клас з ім'ям CensusesFX
. Його вміст буде таким:
package ua.inf.iwanoff.oop.third; 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-документ (New | Other... | JavaFX | New FXML Document) з ім'ям 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.third; 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.third; public class CensusesConroller { }
Подальша робота передбачає додавання і налагодження візуальних компонентів за допомогою застосунку SceneBuilder. Завантажуємо застосунок за допомогою контекстного меню файлу CensusesForm.fxml
у Package Explorer (Open with SceneBuilder). Спочатку додаємо посилання на клас-контролер. На закладці Controller у лівій частині вікна SceneBuilder у рядку ControllerClass вводимо ім'я класу-контолеру, вказуючи всі вкладені пакети. В нашому випадку це рядок ua.inf.iwanoff.oop.third.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.third.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.third; 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 Вправи для контролю
- Створити перелік "День тижня". Додати методи отримання дня "позавчора" та "післязавтра". Протестувати перелік у функції
main()
тестового класу. - Створити перелік "Сезон". Описати метод отримання попереднього та наступного сезону. Протестувати перелік у функції
main()
тестового класу. - Створити перелік "Частина доби". Описати метод отримання попередньої та наступної частини доби. перелік у функції
main()
тестового класу. - Створити перелік "Континент". Перевантажити метод
toString()
так, щоб він показував назву континенту українською (російською) мовою. Протестувати перелік у функціїmain()
тестового класу. - Створити стек символів. Ввести символи й вивести у зворотному порядку.
- Увести кількість елементів майбутньої множини цілих чисел та діапазон чисел. Сформувати цю множину з випадкових значень. Вивести елементи множини, відсортовані за збільшенням.
- Увести кількість елементів майбутньої множини дійсних чисел та діапазон чисел. Сформувати цю множину з випадкових значень. Вивести елементи множини у порядку зменшення.
- Заповнити множину цілих випадковими додатними парними значеннями (не більше визначеного числа). Вивести результат.
- Увести слово та вивести всі різні літери слова в алфавітному порядку.
- Увести речення та обчислити кількість різних літер, з яких речення складається. Не враховувати пропусків та розділових знаків.
- Увести речення та обчислити кількість різних слів у реченні.
- Створити програму графічного інтерфейсу користувача, у якій у двох рядках уведення користувач задає два рядки і після натискання кнопки одержує в третьому рядку результат зшивання вихідних рядків.
- Створити програму графічного інтерфейсу користувача, у якій у двох рядках уведення користувач задає два дійсних числа і після натискання кнопки одержує в третьому рядку введення добуток цих чисел.
- Створити програму графічного інтерфейсу користувача, у якій у двох рядках уведення користувач задає два цілих числа і після натискання кнопки одержує в окремому діалоговому вікні введення добуток цих чисел.
5 Контрольні запитання
- Для чого можуть бути застосовані переліки?
- Для чого визначається конструктор переліку?
- Як у циклі отримати усі константи, описані в переліку?
- Чим стек відрізняється від черги?
- Якими стандартними контейнерами реалізовано чергу і стек?
- З якою метою методи роботи з чергою реалізовані в двох варіантах – з генерацією винятку і без?
- Чим множина відрізняється від списку за властивостями та реалізацією?
- Наведіть приклади використання асоціативних масивів.
- У чому відміни інтерфейсів
Map
таSortedMap
та класівHashSet
іTreeSet
? - Які стандартні бібліотеки Java використовують для реалізації застосунків графічного інтерфейсу користувача?
- Що таке аплет?
- У чому полягає ідея програмування, керованого подіями?
- Що таке JavaFX? Які переваги має JavaFX?
- Що таке MVC?
- Що таке FXML? Які переваги надає FXML?
- Що таке компонування і як воно реалізоване в JavaFX?
- Які стандартні контейнери надає JavaFX і чим вони відрізняються?
- Як здійснюється робота з кнопками
RadioButton
? - У чому полягають особливості модальних діалогових вікон?
- Як у JavaFX використовують стандартні вікна вибору файлів?
- Як здійснюється робота з табличними даними в JavaFX?
- Що таке
ObservableList
? - Як зв'язати колонки таблиці з властивостями об'єктів списку?
- Як забезпечити редагування комірок таблиці в JavaFX?
- Як здійснюється візуальне редагування вікна застосунку графічного інтерфейсу користувача?