Основи Java

Розробник курсу Л. В. Іванов

  Лабораторні роботи:

 

Контрольні запитання з курсу

Лабораторна робота 5

Робота з графічними засобами JavaFX

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

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

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

Для створення графіку застосувати засоби класу Canvas.

Варіанти функцій, які треба обирати для побудови графіку та обов'язкові елементи наведені в таблиці:

Таблиця 1.1 - Індивідуальні завдання

№№
Функція, яку необхідно побудувати
Компонент для вибору масштабу
Компонент для вибору товщини ліній
№№
Функція, яку необхідно побудувати
Компонент для вибору масштабу
Компонент для вибору товщини ліній
1
a / (b sin x)
Slider
ScrollBar
9
a sin x + b x Spinner ScrollBar
2
ax + b cos x
ComboBox
Slider
10
(a cos x + b) / x Slider ScrollBar
3
(cos x + a) / (b sin x)
Slider
Spinner
11
a + cos(b / x) ScrollBar Spinner
4
a / (b x sin x)
ScrollBar
Slider
12
sin(a / x) + cos(b / x) Spinner Slider
5
a sin(b / x)
ComboBox
ScrollBar
13
ax + b ctg x Spinner ScrollBar
6
ax - b tg x
ScrollBar
ComboBox
14
a cos(b / x) ComboBox ScrollBar
7
a + sin(b / x)
ScrollBar
Spinner
15
a + tg(b / x) Slider ComboBox
8
(a sin x + b) / x
ScrollBar
ComboBox
16
a sin x + b cos x ComboBox Spinner

1.2 Тестування знаходження суми елементів масиву

Спроектувати клас підсумовування елементів масиву ArraySum, що містить статичний метод sum(), який приймає масив як параметр. Розробити клас ArraySumTest для тестування ArraySum. Застосувати засоби JUnit для тестування. Додати в клас ArraySumTest метод, який тестує поведінку класу ArraySum при передачі в його статичний метод sum() значення null (протестувати очікуваний виняток).

1.3 Робота з геометричними фігурами

Розробити програму зображення стандартних графічних примітивів - квадрата, прямокутника, кола та еліпса. Користувач здійснює вибір необхідної фігури за допомогою кнопок RadioButton

1.4 Використання LineChart

Реалізувати індивідуальне завдання через використання класу LineChart.

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

2.1 Тестування застосунків

2.1.1 Основні концепції тестування

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

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

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

2.1.2 Засоби Java для діагностики помилок під час виконання

Багато сучасних мов програмування, зокрема Java, включають синтаксичні механізми перевірки тверджень (assertions). Ключове слово assert з'явилося в Java починаючи з версії JDK 1.4 (Java 2). Роботу assert можна вмикати або вимикати. Якщо виконання діагностичних тверджень увімкнено, робота assert полягає у такому: виконується вираз типу boolean і якщо результат дорівнює true, робота програми продовжується далі, в протилежному випадку виникає виняток java.lang.AssertionError. Припустимо, відповідно до логіки програми змінна c повинна завжди бути додатною. Виконання такого фрагменту програми не призведе до будь-яких наслідків (винятків, аварійної зупинки програми тощо):

int a = 10;
int b = 1;
int c = a - b;
assert c > 0;

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

int a = 10;
int b = 11;
int c = a - b;
assert c > 0; // генерація винятку

Після твердження можна поставити двокрапку, після якої вказати деякий рядок повідомлення. Наприклад:

int a = 10;
int b = 11;
int c = a - b;
assert c > 0 : "c cannot be negative";

В цьому випадку відповідний рядок є рядком повідомлення винятку.

Робота діагностичних тверджень зазвичай вимкнена в інтегрованих середовищах. Для того, щоб увімкнути виконання assert в середовищі Eclipse слід скористатися функцією меню Run | Run Configurations, у відповідному діалоговому вікні на закладці Arguments в області введення VM arguments слід увести -ea і виконати програму.

Примітка: у середовищі IntelliJ IDEA є аналогічна функція меню Run | Edit Configurations. У вікні Run/Debug Configurations уводимо -ea в рядку введення VM Options.

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

public static void main(String[] args) {
    //...
    assert f() : "failed";
    //...
}

public static boolean f() {
    // Very important calculations
    return true;
}

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

2.1.3 Основи використання JUnit

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

Найбільш розповсюдженим засобом підтримки модульного тестування для програмного забезпечення мовою Java є JUnit - відкрита бібліотека модульного тестування. JUnit дозволяє:

Для використання JUnit необхідна наявність в системі JDK. Середовище IntelliJ IDEA забезпечує вбудовану підтримку JUnit, проте можна встановити бібліотеку JUnit і вручну, скачавши дистрибутиви JUnit за посиланням https://github.com/junit-team/junit/wiki/Download-and-Install (junit-4.12.jar і hamcrest-core-1.3.jar).

Для створення тесту необхідно створити клас, який необхідно тестувати, а також створити відкритий клас для тестування з набором методів, що реалізують конкретні тести. Кожен тестовий метод повинен бути public, void, без параметрів. Метод повинен бути маркованим анотацією @Test:

import org.junit.*;
public class MyTestCase { 
    ...
    @Test
    public void testXXX() { 
    ...
    } 
    ...
}

Усередині таких методів можна використовувати методи перевірки:

assertTrue(вираз);                // Якщо false - завершує тест невдачею
assertFalse(вираз);               // Якщо true - завершує тест невдачею
assertEquals(expected, actual);       // Якщо не еквівалентні - завершує тест невдачею
assertNotNull(new MyObject(params));  // Якщо null - завершує тест невдачею
assertNull(new MyObject(params));     // Якщо не null - завершує тест невдачею
assertNotSame(вираз1, вираз2);// Якщо обидва посилання посилаються на один об'єкт - завершує тест невдачею
assertSame(вираз1, вираз2);   // Якщо об'єкти різні - завершує тест невдачею  
fail(повідомлення)            // Негайно завершує тест невдачею з виведенням повідомлення.

Тут MyObject - клас, який тестується. Доступ до цих методів класу Assert здійснюється за допомогою статичного імпорту import static org.junit.Assert.*;. Ці методи існують також з додатковим першим параметром message типу String, який задає повідомлення, що буде відображатися в разі невдалого виконання тесту.

Якщо перед виконанням функції тестування необхідно зробити деякі дії, наприклад, форматувати значення змінних, то така ініціалізація виноситься в окремий метод, якому передує анотація @Before:

@Before
public void setup(){
      ...
}

Наприклад:

import org.junit.*;
import static org.junit.Assert.*;
         
public class MyMathTest {
    private MyMath a;
         
    @Before
    public void setUp(){
        a = new MyMath();
    }
         
    @Test
    public void testAdd(){
        assertEquals(5, a.add(4, 1));
    }
         
    @Test
    public void testSub(){
        assertEquals(-1, a.sub(0, 1));
    }
}

Аналогічно методам, в яких виконуються дії, необхідні після тестування, передує анотація @After. Відповідні методи повинні бути public static void.

Анотація @Test(timeout=time), де time - час в мілісекундах, дозволяє задати максимальну кількість часу, відведеного на виконання даного тесту. Якщо ліміту часу перевищено, тест завершується невдачею.

Анотацію @Test(expected=SomeException.class) використовують, якщо потрібно перевірити, чи приводить спроба виконання тесту до винятку SomeException.

Якщо необхідно поки проігнорувати якийсь тестовий метод, використовують анотацію @Ignore.

@Ignore
@Test(timeout = 1000)
public void testAdd() {
    //код
}   

JUnit надає механізм логічного групування тестів для їх подальшого спільного виконання. Для цього використовуються анотація @RunWith, в якій треба вказати клас під назвою Suite (виконавець наборів тестів), а також анотація @SuiteClasses, параметром якої є список класів, що представляють набір тестів:

import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;

@RunWith(Suite.class)
@SuiteClasses({ TestSample1.class, TestSample2.class })
public class AllSuite {
}

Для запуску набору тестів не з середовища IntelliJ IDEA потрібен такий код:

import org.junit.runner.JUnitCore;
import org.junit.runner.Result;
import org.junit.runner.notification.Failure;

public class TestRunner {
    public static void main(String[] args) {
        Result result = JUnitCore.runClasses(AllSuite.class);
            for (Failure failure : result.getFailures()) {
                System.out.println(failure.toString());
            }
            System.out.println(result.wasSuccessful());
        }
    }

Припустимо, треба протестувати клас Sample з двома методами:

package ua.in.iwanoff.java.fifth;

public class Sample {
    private String msg = "Hello";
    public static int add(int a, int b) { return a + b; } //статичний метод
    public String getMsg() { return msg; } //нестатичний метод
}

Тестовий клас матиме вигляд:

package ua.in.iwanoff.java.fifth;

import org.junit.Test;
import static org.junit.Assert.*;
public class TestSample {

    // перевірка статичного методу складання
    @Test
    public void testAdd() {
        assertEquals(Sample.add(4,4), 8);  
    }
    // перевірка ініціалізації класу
    @Test
    public void testInit() {
        assertEquals(new Sample().getMsg(),"Hello");
    }
}

Під час перевірки методу getMsg() тестується в першу чергу не код цього методу, а створення екземпляра класу з коректною ініціалізацією поля msg.

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

void setValue(into value) {
    this.value = value;
}

...

public void testSetValue() {
    someObject.setValue(123);
    assertEquals(123, someObject.getValue());
}

Однак, як правило, тестувати найпростіші методи доступу до властивостей і установки властивостей (сеттери і геттери) представляється надмірним і не рекомендується.

Якщо зробити в коді помилки:

public class Sample {
    private String msg = "Hello";
    public static int add(int a, int b) { return a - b; }
    public String getMsg() { return msg; }
}

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

2.2 Стандартні графічні засоби JavaFX

2.2.1 Основні концепції

Стандартизація графічних засобів у свій час обумовила істотне прискорення розвитку відеоігор і віртуальної реальності. Стандартизація графіки на низькому апаратно-програмному рівні вперше була здійснена у 1992 році й пов'язана з появою OpenGL. OpenGL - (англ. Open Graphics Library - відкрита графічна бібліотека) - специфікація, що визначає незалежний від мови програмування крос-платформний програмний інтерфейс (API) для написання застосунків, що використовують 2D та 3D комп'ютерну графіку. Але й після появи OpenGL проблема стандартизації залишилася на більш вискому рівні, де існують труднощі, пов'язані з відмінностями архітектури, технічних пристроїв та операційних систем. Великою перевагою Java є можливість роботи з крос-платформними графічними засобами. Графіка Java не залежить від особливостей операційних систем і конкретних пристроїв. Вона включає як можливості низькорівневої роботи з графічними об'єктами і файлами, так і високорівневі операції з обробки зображень, побудови графіків і діаграм тощо.

Перша версія Java включала обмежений набір графічних примітивів без можливостей визначення товщини й властивостей ліній, мінімальні виразні засоби, необхідні для реалізації аплетів. Можливості графіки були інкапсульовані в класі Graphics, отримати об'єкт якого можна було як параметр методу paint(), перевизначеного для певного компоненту, наприклад, панелі (Panel) бібліотеки AWT.

Програмний інтерфейс Java 2D API надає користувачеві розширений набір засобів двовимірної графіки. Починаючи з версії JDK 1.2 компанія Sun включила до складу пакету розробника більш потужний графічний пакет java.awt.Graphics2D. Цей набір інструментів для графічного виведення, що підтримує дизайн різних ліній, тексту, картинок, що має більш багатий і повнофункціональний інтерфейс для користувача.

Зокрема, API підтримує:

Основний механізм малювання в java.awt.Graphics2D такий же, як в попередніх версіях JDK - графічна система управляє тим, коли і що програма повинна малювати. Коли деякий компонент повинен відобразити себе на екрані, його метод paint() або update() автоматично викликається з відповідним йому контекстом Graphics.

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

Графіка JavaFX реалізована на різних рівнях:

2.2.2 Використання можливостей класу Canvas

Концепція полотна (canvas) притаманна практично всім сучасним графічним системам. Зазвичай полотно інкапсулює в собі можливості малювання графічних примітивів. У JavaFX Canvas - це клас, який представляє вузол у графі сцени. З кожним полотном зв'язаний так званий графічний контекст - об'єкт типу GraphicsContext, який надає функції малювання ліній, геометричних фігур, тексту, а також підтримки візуальних ефектів і графічних перетворень.

Об'єкт типу Canvas можна створити за допомогою двох конструкторів. Окрім конструктора без параметрів є конструктор, який визначає розміри полотна. Якщо ми хочемо малювати на всій панелі (об'єкт panel), об'єкт типу Canvas можна створити в такий спосіб:

Canvas canvas = new Canvas(panel.getWidth(), panel.getHeight());

Для отримання об`єкта типу GraphicsContext можна скористатись методом getGraphicsContext2D(). Це статичний фабричний метод класу Canvas. Наприклад:

GraphicsContext gc = canvas.getGraphicsContext2D();

Концептуально клас Canvas реалізує процедурний підхід до створення графіки. Можна розглядати GraphicsContext як кінцевий автомат, який послідовно виконує малювання графічних примітивів залежно від свого стану. Можна визначити стан для кольору, товщини й типу ліній (відповідно setStroke(), setLineWidth()), а також для кольору заповнення (setFill()). Можна також встановити тип ліній і заповнення. Визначені налаштування діятимуть до встановлення нових значень. Після створення об'єкта типу Canvas встановлюються усталені значення кольорів (чорний), товщини ліній - 1.

Найпростіший спосіб визначити колір - скористатися константами класу javafx.scene.paint.Color, наприклад, Color.RED, Color.GREEN, Color.BROWN тощо. Окрім готових констант, клас Color надає декілька статичних функцій визначення необхідного кольору, зокрема Color.rgb(), Color.hsb() тощо.

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

package ua.in.iwanoff.java.fifth;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.layout.AnchorPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.ArcType;
import javafx.stage.Stage;

public class CanvasDemo extends Application {
    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle("Canvas Demo");
        AnchorPane root = new AnchorPane();
        Scene scene = new Scene(root, 320, 230);
        // Створюємо полотно і графічний контекст
        Canvas canvas = new Canvas(root.getWidth(), root.getHeight());
        GraphicsContext gc = canvas.getGraphicsContext2D();

        // Прямокутник:
        gc.setStroke(Color.DARKCYAN);
        gc.setFill(Color.LIGHTGREEN);
        gc.fillRect(20, 20, 100, 80);
        gc.strokeRect(20, 20, 100, 80);

        // Відрізок прямої:
        gc.setStroke(Color.MAGENTA);
        gc.setLineWidth(3);
        gc.strokeLine(140, 20, 220, 100);

        // Сектор:
        gc.setFill(Color.AQUA);
        gc.fillArc(210, 20, 100, 100, 45, 100, ArcType.ROUND);

        // Еліпс:
        gc.setStroke(Color.BLUE);
        gc.setFill(Color.YELLOW);
        gc.setLineWidth(1);
        gc.fillOval(20, 120, 100, 80);
        gc.strokeOval(20, 120, 100, 80);

        // Багатокутник:
        gc.setFill(Color.RED);
        double[] xPoints = { 150, 170, 190, 170 };
        double[] yPoints = { 135, 155, 120, 170 };
        gc.fillPolygon(xPoints, yPoints, xPoints.length);

        // Ламана:
        gc.setLineWidth(2);
        gc.setFill(Color.DARKBLUE);
        xPoints = new double[] { 250, 270, 290, 270 };
        yPoints = new double[] { 135, 155, 120, 170 };
        gc.strokePolyline(xPoints, yPoints, xPoints.length);

        // Додаємо полотно до панелі:
        root.getChildren().add(canvas);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

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

Головне вікно програми матиме такий вигляд:

2.2.3 Використання стандартних геометричних фігур

Об'єктно-орієнтований підхід до графіки в JavaFX найбільш наочно виявляється у застосуванні об'єктів стандартних класів, які опсують найбільш вживані графічні примітиви. В цьому випадку побудова складних зображень включає створення набору об'єктів різних типів, похідних від javafx.scene.shape.Shape. Необхідні розміри та координати можна визначати як у конструкторі, так і за допомогою окремих сеттерів. Як і у випадку застосування Canvas, для об'єктів можна визначити властивості заповнення (setFill()) і контуру (setStroke()).

Наведена нижче програма демонструє можливості створення й налаштування деяких об'єктів, похідних від Shape:

package ua.in.iwanoff.java.fifth;

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.layout.VBox;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.scene.shape.*;

public class ShapesDemo extends Application {
    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle("Shapes Demo");

        // Прямокутник:
        Rectangle rectangle = new Rectangle(100, 80); // розміри
        rectangle.setStroke(Color.BROWN);
        rectangle.setFill(Color.YELLOWGREEN);

        // Коло:
        Circle circle = new Circle(35.0); // радіус
        circle.setCenterX(80.0);
        circle.setCenterY(80.0);
        circle.setFill(Color.CHOCOLATE);

        // Дуга:
        // В конструкторі визначаємо центр, радіуси, початок і довжину в градусах
        Arc arc = new Arc(100, 100, 50, 50, 45, 135);
        arc.setType(ArcType.ROUND);
        arc.setFill(Color.DARKGRAY);

        // Еліпс:
        Ellipse ellipse = new Ellipse(50, 40);
        ellipse.setStroke(Color.DARKBLUE);
        ellipse.setFill(Color.LIGHTGOLDENRODYELLOW);

        // Багатокутник:
        Polygon polygon = new Polygon();
        polygon.setFill(Color.RED);
        // Додаємо пари координат вершин.
        // Остання вершина з'єднується з першою:
        polygon.getPoints().addAll(10.0, 15.0,
                                   30.0, 35.0,
                                   50.0, 00.0,
                                   30.0, 50.0);

        // Крива Безьє:
        CubicCurve cubic = new CubicCurve();
        cubic.setFill(Color.TRANSPARENT);
        cubic.setStroke(Color.DARKGREEN);
        // Почачток:
        cubic.setStartX(0.0);
        cubic.setStartY(100.0);
        // Точки, від яких залежить кривина кривої Безьє
        cubic.setControlX1(25.0);
        cubic.setControlY1(0.0);
        cubic.setControlX2(75.0);
        cubic.setControlY2(100.0);
        // Кінець:
        cubic.setEndX(100.0);
        cubic.setEndY(50.0);

        HBox hBoxFirst = new HBox();
        hBoxFirst.setPadding(new Insets(10, 10, 10, 10));
        hBoxFirst.setSpacing(10);
        hBoxFirst.getChildren().addAll(rectangle, circle, arc);
        HBox hBoxSecond = new HBox();
        hBoxSecond.setPadding(new Insets(10, 10, 10, 10));
        hBoxSecond.setSpacing(10);
        hBoxSecond.getChildren().addAll(ellipse, polygon, cubic);
        VBox vBox = new VBox();
        vBox.getChildren().addAll(hBoxFirst, hBoxSecond);
        vBox.setSpacing(10);
        vBox.setPadding(new Insets(10, 10, 10, 10));
        Scene scene = new Scene(vBox,  320, 250);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

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

Головне вікно застосунку виглядатиме так:

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

Окрім наведених фігур, пакет javafx.scene.shape також містить примітиви, які використовують як проекції тривимірних об'єктів.

2.2.4 Стандартні засоби створення графіків і діаграм

Стандартні засоби JavaFX надають готові компоненти для побудови графіків і діаграм. Абстрактний клас Chart є базовим для всіх графіків і діаграм і об'єднує в собі властивості всіх компонентів, призначених для візуалізації числових даних. Безпосередньо похідними від нього є класи PieChart (кругова діаграма) та XYChart. Останній також є абстрактним і виступає базовим для всіх інших діаграм: AreaChart, BarChart, BubbleChart, LineChart, ScatterChart, StackedAreaChart і StackedBarChart.

Конструктори класів, похідних від XYChart, вимагають визначення двох об'єктів типу Axis (вісь). Axis - це абстрактний клас для представлення осі на графіку або діаграмі. Похідними від нього є клас CategoryAxis, об'єкти якого використовують, коли вздовж осі розташовують мітки у вигляді набору рядків, а також абстрактний клас ValueAxis. Клас NumberAxis, похідний від ValueAxis, використовують, коли вісь представляє шкалу числових значень.

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

package ua.in.iwanoff.java.fifth;

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.chart.*;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

public class ChartDemo extends Application {

    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle("Chart Demo");

        PieChart pieChart = new PieChart();
        pieChart.setMinWidth(300);
        pieChart.setTitle("Pie Chart");
        pieChart.getData().addAll(new PieChart.Data("two", 2),
                                  new PieChart.Data("three", 3),
                                  new PieChart.Data("one", 1));

        XYChart.Data<String, Number>[] barData = new XYChart.Data[] {
                                  new XYChart.Data<>("one", 2.0),
                                  new XYChart.Data<>("two", 3.0),
                                  new XYChart.Data<>("three", 1.0) };
        BarChart<String, Number> barChart = new BarChart<>(new CategoryAxis(), new NumberAxis());
        barChart.setTitle("Bar Chart");
        barChart.getData().addAll(new Series<>("Bars", FXCollections.observableArrayList(barData)));

        XYChart.Data<Number, Number>[] xyData = new XYChart.Data[] {
                new XYChart.Data<>(1.0, 2.0),
                new XYChart.Data<>(2.0, 3.0),
                new XYChart.Data<>(3.0, 1.0) };
        AreaChart<Number, Number> areaChart = new AreaChart<>(new NumberAxis(), new NumberAxis());
        areaChart.setTitle("Area Chart");
        areaChart.getData().addAll(new Series<>("Points", FXCollections.observableArrayList(xyData)));
        // Використовує той самий набір даних
        BubbleChart<Number, Number> bubbleChart = new BubbleChart<>(
                new NumberAxis(0, 4, 1), new NumberAxis(0, 5, 1));
        bubbleChart.setTitle("Bubble Chart");
        bubbleChart.getData().addAll(new Series<>("Bubbles", FXCollections.observableArrayList(xyData)));

        HBox hBox = new HBox();
        hBox.getChildren().addAll(pieChart, barChart, areaChart, bubbleChart);
        hBox.setSpacing(10);
        hBox.setPadding(new Insets(10, 10, 10, 10));

        Scene scene = new Scene(hBox,  1000, 350);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

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

Головне вікно застосунку виглядатиме так:

Для діаграм, похідних від XYChart, можна визначити кілька наборів даних (Series) для візуального порівняння різних залежностей.

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

3.1 Використання JUnit в середовищі IntelliJ IDEA

Припустимо, в середовищі IntelliJ IDEA створено клас Product:

package ua.in.iwanoff.java.fifth;

public class Product {
    public static int multiply(int a, int b) {
        return a * b;
    }
}

Для того, щоб створити тестовий клас, спочатку вибираємо проект в дереві та створюємо нову теку (File | New | Directory), наприклад, з ім'ям tests. Далі в налаштуваннях структури проекту (File | Project Structure...) для налаштувань Modules у дереві знаходимо нову теку і в списку Mark as вибираємо Tests.

Повернувшись у вікно редагування коду і вибравши ім'я класу натискаємо Alt-Enter. У списку, який з'явився після цього, вибираємо Create Test. З'являється діалогове вікно Create Test, в якому слід вибрати бібіліотеку Testing library, в нашому випадку, JUnit4. Якщо тестування проекту здійснювалося вперше, в наступному рядку з'явиться повідомлення JUnit4 library not found in the module з кнопкою Fix. Ящо натиснути цю кнопку, з'явиться ще одно діалогове вікно, в якому слід вибрати Use 'JUnit4' from IntelliJ IDEA distribution і натиснути OK. Повернувшись у попереднє діалогове вікно, слід вибрати функції, тестування яких здійснюватиметься. В нашому випадку, це метод multiply().

У теці tests буде створено структуру пакетів, які відтворюють структуру пакетів проекту. Також буде згенеровано клас ProductTest з таким вихідним кодом:

package ua.in.iwanoff.java.fifth;

import org.junit.Test;

import static org.junit.Assert.*;

public class ProductTest {

    @Test
    public void multiply() throws Exception {
    }

}

Тепер всередині коду можна розташувати виклик функцій, які здійснюють перевірку тверджень. В нашому випадку код всередині функції multiply() класу ProductTest буде таким:

assertEquals(12, Product.multiply(3, 4));

Функцію тестування можна запустити через головне меню: Run | Run..., далі ProductTest.multiply. Якщо тест пройшов успішно, у заголовку консольного вікна з'являється зелена смуга і напис 1 test passed.

Можна внести помилки в код функції multiply() класу Product, наприклад,

return a * b + 1;

Тепер виконання ProductTest.multiply призведе до появи червоної смуги, тексту 1 test failed, а також повідомлення про помилку в консольному вікні.

3.2 Діаграма скалярних величин

Припустимо, необхідно створити застосунок графічного інтерфейсу користувача для введення п'яти скалярних величин від 0 до 100 і відображення їх у вигляді стовпчастої діаграми. Величину a слід уводити за допомогою компоненту Slider, b слід уводити за допомогою компоненту ComboBox, c - за допомогою компоненту ScrollBar, d - через компонент Spinner, e - через TextField. Робота з програмою повинна полягати в редагуванні даних з одночасним відображенням величин на діаграмі.

Всі необхідні компоненти можна визначити в FXML-файлі:

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

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.control.ComboBox?>
<?import javafx.scene.control.ScrollBar?>
<?import javafx.scene.control.Slider?>


<?import javafx.scene.shape.Rectangle?>
<AnchorPane minHeight="150" minWidth="250" prefHeight="400.0" prefWidth="600.0"
            fx:controller="ua.in.iwanoff.java.fifth.DiagramController"
            xmlns:fx="http://javafx.com/fxml/1"
            xmlns="http://javafx.com/javafx/8">
    <children>
        <VBox minWidth="200.0" prefWidth="200.0" AnchorPane.topAnchor="50">
            <children>
                <HBox prefHeight="40.0" minHeight="40.0">
                    <children>
                        <Label prefWidth="50.0" text="a" alignment="CENTER"/>
                        <Slider fx:id="sliderA" prefWidth="100.0" />
                        <Label fx:id="labelA" prefWidth="50.0" text="50" alignment="CENTER" />
                    </children>
                </HBox>
                <HBox prefHeight="40.0" minHeight="40.0">
                    <children>
                        <Label prefWidth="50.0" text="b" alignment="CENTER"/>
                        <ComboBox fx:id="comboBoxB" prefWidth="100.0" />
                        <Label fx:id="labelB" prefWidth="50.0" text="50" alignment="CENTER" />
                    </children>
                </HBox>
                <HBox prefHeight="40.0" minHeight="40.0">
                    <children>
                        <Label prefWidth="50.0" text="c" alignment="CENTER"/>
                        <ScrollBar fx:id="scrollBarC" prefWidth="100.0" prefHeight="20.0"/>
                        <Label fx:id="labelC" prefWidth="50.0" text="50" alignment="CENTER" />
                    </children>
                </HBox>
                <HBox prefHeight="40.0" minHeight="40.0">
                    <children>
                        <Label prefWidth="50.0" text="d" alignment="CENTER"/>
                        <Spinner fx:id="spinnerD" prefWidth="100.0" />
                        <Label fx:id="labelD" prefWidth="50.0" text="50" alignment="CENTER" />
                    </children>
                </HBox>
                <HBox prefHeight="40.0" minHeight="40.0">
                    <children>
                        <Label prefWidth="50.0" text="e" alignment="CENTER"/>
                        <TextField fx:id="textFieldE" prefWidth="100.0" />
                        <Label fx:id="labelE" prefWidth="50.0" text="50" alignment="CENTER" />
                    </children>
                </HBox>
            </children>
        </VBox>
        <Rectangle fx:id="rectangleA" AnchorPane.leftAnchor="200.0" AnchorPane.bottomAnchor="50"
                   width = "50" height="250" fill="red"/>
        <Rectangle fx:id="rectangleB" AnchorPane.leftAnchor="300.0" AnchorPane.bottomAnchor="50"
                   width = "50" height="250" fill="orange"/>
        <Rectangle fx:id="rectangleC" AnchorPane.leftAnchor="400.0" AnchorPane.bottomAnchor="50"
                   width = "50" height="250" fill="yellow"/>
        <Rectangle fx:id="rectangleD" AnchorPane.leftAnchor="500.0" AnchorPane.bottomAnchor="50"
                   width = "50" height="250" fill="green"/>
        <Rectangle fx:id="rectangleE" AnchorPane.leftAnchor="600.0" AnchorPane.bottomAnchor="50"
                   width = "50" height="250" fill="blue"/>
    </children>
</AnchorPane>

Клас контролера матиме такий вигляд:

package ua.in.iwanoff.java.fifth;

import java.net.URL;
import java.util.ResourceBundle;

import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.*;
import javafx.scene.shape.Rectangle;

public class DiagramController implements Initializable {
    @FXML private Slider sliderA;
    @FXML private ComboBox<Integer> comboBoxB;
    @FXML private ScrollBar scrollBarC;
    @FXML private Spinner<Integer> spinnerD;
    @FXML private TextField textFieldE;

    @FXML private Label labelA;
    @FXML private Label labelB;
    @FXML private Label labelC;
    @FXML private Label labelD;
    @FXML private Label labelE;

    @FXML private Rectangle rectangleA;
    @FXML private Rectangle rectangleB;
    @FXML private Rectangle rectangleC;
    @FXML private Rectangle rectangleD;
    @FXML private Rectangle rectangleE;
    
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        sliderA.setValue(50);
        sliderA.valueProperty().addListener(this::aChanged);
        for (int i = 0; i <= 100; i++) {
            comboBoxB.getItems().add(i);
        }
        comboBoxB.getSelectionModel().select(50);
        comboBoxB.getSelectionModel().selectedItemProperty().addListener(this::bChanged);
        scrollBarC.setValue(50);
        scrollBarC.valueProperty().addListener(this::cChanged);
        SpinnerValueFactory.IntegerSpinnerValueFactory valueFactory =
                new SpinnerValueFactory.IntegerSpinnerValueFactory(0, 100);
        valueFactory.setValue(50);
        spinnerD.setValueFactory(valueFactory);
        spinnerD.valueProperty().addListener(this::dChanged);
        textFieldE.setText("50");
        textFieldE.textProperty().addListener(this::eChanged);
    }


    private void aChanged(ObservableValue<? extends Number> observableValue, Number oldValue, Number newValue) {
        labelA.setText(Math.round(newValue.intValue()) + "");
        rectangleA.setHeight(newValue.intValue() * 5);
    }

    private void bChanged(ObservableValue<? extends Integer> observableValue, Integer oldValue, Integer newValue) {
        labelB.setText(Math.round(newValue.intValue()) + "");
        rectangleB.setHeight(newValue.intValue() * 5);
    }

    private void cChanged(ObservableValue<? extends Number> observableValue, Number oldValue, Number newValue) {
        labelC.setText(Math.round(newValue.intValue()) + "");
        rectangleC.setHeight(newValue.intValue() * 5);
    }

    public void dChanged(ObservableValue<? extends Integer> observable, Integer oldValue, Integer newValue) {
        labelD.setText(Math.round(newValue.intValue()) + "");
        rectangleD.setHeight(newValue.intValue() * 5);
    }

    public void eChanged(ObservableValue<? extends String> observable, String oldValue, String newValue) {
        int value;
        try {
            if (oldValue.length() == 0) {
                oldValue = labelE.getText();
            }
            value = Integer.parseInt(newValue);
            if (value > 100 || value < 0) {
                textFieldE.setText(oldValue);
                return;
            }
            rectangleE.setHeight(value * 5);
            labelE.setText(newValue);
        }
        catch (NumberFormatException e) {
            if (textFieldE.getText().length() > 0) {
                textFieldE.setText(oldValue);
            }
        }
    }
}

Як видно з наведеного вище коду, об'єкт типу Slider слід заздалегідь підготувати, додавши числа від 0 до 100. Всім компонентам встановлюється середнє значення (50). Оброблювачі подій містять оновлення значень відповідних міток і значень висоти відповідних прямокутників (стовпців). Оброблювач події, пов'язаної з текстовим полем, додатково містить код, який забезпечує обмеження можливості введення числами від 0 до 100.

Клас-застосунок JavaFX буде таким:

package ua.in.iwanoff.java.fifth;

import java.io.IOException;

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

public class DiagramWindow extends Application {

    @Override
    public void start(Stage primaryStage) {
        try {
            primaryStage.setTitle("Скалярні величини");
            AnchorPane root = (AnchorPane)FXMLLoader.load(getClass().getResource("DiagramWindow.fxml"));
            Scene scene = new Scene(root, 700, 600);
            primaryStage.setScene(scene);
            primaryStage.show();
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }

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

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

3.3 Використання LineChart для побудови графіків тригонометричних функцій

Серед класів, похідних від XYChart, особливу цікавість представляє клас LineChart, об'єкти якого можуть бути використані для побудови графіків функцій. Для побудови графіку спочатку слід підготувати дані залежності у вигляді набору точок. Точок повинно бути достатньо багато, щоб крива була гладкою, але не надто багато, щоб не уповільнити виконання програми. У наведеній вище програмі здійснюється побудова двох тригонометричних функцій на одному графіку:

package ua.in.iwanoff.java.fifth;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.stage.Stage;

import java.util.function.DoubleUnaryOperator;

public class LineChartDemo extends Application {
    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle("Тригонометричні функції");
        Scene scene = new Scene(fgGraph(-4, 4, 1, -1.5, 1.5, 1,
                "Синус", Math::sin, "Косинус", Math::cos), 800, 350);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private LineChart<Number, Number> fgGraph(double xFrom, double xTo, double xStep,
                                              double yFrom, double yTo, double yStep,
                                              String fName, DoubleUnaryOperator f,
                                              String gName, DoubleUnaryOperator g) {
        NumberAxis xAxis = new NumberAxis(xFrom, xTo, xStep);
        NumberAxis yAxis = new NumberAxis(yFrom, yTo, yStep);
        LineChart<Number, Number> graphChart = new LineChart<>(xAxis, yAxis);
        // Вимикаємо "символи" в точках
        graphChart.setCreateSymbols(false);
        double h = (xTo - xFrom) / 100;
        // Додаємо ім'я і точки першої функції:
        XYChart.Series<Number, Number> fSeries = new XYChart.Series<>();
        fSeries.setName(fName);
        for (double x = xFrom; x <= xTo; x += h) {
            fSeries.getData().add(new XYChart.Data<>(x, f.applyAsDouble(x)));
        }
        // Додаємо ім'я і точки другої функції:
        XYChart.Series<Number, Number> gSeries = new XYChart.Series<>();
        gSeries.setName(gName);
        for (double x = xFrom; x <= xTo; x += h) {
            gSeries.getData().add(new XYChart.Data<>(x, g.applyAsDouble(x)));
        }
        // Додаємо обидві функції:
        graphChart.getData().addAll(fSeries, gSeries);
        return graphChart;
    }

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

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

Недоліком такого підходу можна вважати необхідність підбору набору точок замість використання зворотного виклику. Кількість точок може бути як недостатньою (замість графіку отримаємо ломану) так і надмірною, що призведе до уповільнення побудови графіку.

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

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

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

package ua.in.iwanoff.java.fifth;

import javafx.scene.paint.Paint;

import java.util.function.DoubleUnaryOperator;

public class FunctionData {
    private DoubleUnaryOperator operator;
    private Paint paint;
    private double width;

    public FunctionData(DoubleUnaryOperator operator, Paint paint, double width) {
        this.operator = operator;
        this.paint = paint;
        this.width = width;
    }

    public DoubleUnaryOperator getOperator() {
        return operator;
    }

    public Paint getPaint() {
        return paint;
    }

    public double getWidth() {
        return width;
    }
}

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

Для побудови графіків функцій можна застосувати стандартні геометричні фігури, зокрема класи Line (осі, сітка) і Polyline (безпосередньо графік). Відповідні об'єкти залежно від початку координат і масштабу додаються до панелі.

Клас, який відповідає за побудову графіків, матиме такий вигляд:

package ua.in.iwanoff.java.fifth;

import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Line;
import javafx.scene.shape.Polyline;
import javafx.scene.text.Text;

import java.util.ArrayList;
import java.util.List;
import java.util.function.DoubleUnaryOperator;

public class GraphBuilder {
    // Кількість кроків для обчислення мінімуму й максимуму функцій:
    public static double STEPS = 100;
    // Мінімальний відступ від осей і графіку по вертикалі:
    public static double VERTICAL_GAP = 0.2;
    // Поріг видимості ліній сітки:
    public static double MIN_PIXELS = 20;
    // Поріг зміни кроку сітки:
    public static double SCALE_STEP = 10;
    // Мінімальний відступ від осей:
    public static double MIN_GAP = 2;
    // Відступ від краю малюнка:
    public static double MAX_GAP = 10;
    // Формат виведення чисел:
    public static String FORMAT = "%5.2f";


    // Приблизне обчислення мінімуму функції на деякому інтервалі:
    public static double minY(double from, double to, DoubleUnaryOperator operator) {
        double min = operator.applyAsDouble(from);
        double h = (to - from) / STEPS;
        for (double x = from + h; x <= to; x += h) {
            double y = operator.applyAsDouble(x);
            if (min > y) {
                min = y;
            }
        }
        return min;
    }

    // Приблизне обчислення максимуму функції на деякому інтервалі:
    public static double maxY(double from, double to, DoubleUnaryOperator operator) {
        double max = operator.applyAsDouble(from);
        double h = (to - from) / STEPS;
        for (double x = from + h; x <= to; x += h) {
            double y = operator.applyAsDouble(x);
            if (max < y) {
                max = y;
            }
        }
        return max;
    }

    // Панель, на якій відображатиметься графік:
    private Pane pane;

    // Кольори сітки та осей:
    private Color gridColor = Color.LIGHTGRAY;
    private Color axesColor = Color.BLACK;

    // Функції, які слід відобразити:
    private List<FunctionData> funcs = new ArrayList<>();

    // У конструкторі обов'язково визначаємо панель для побудови графіку:
    public GraphBuilder(Pane pane) {
        this.pane = pane;
    }

    public Color getGridColor() {
        return gridColor;
    }

    public void setGridColor(Color gridColor) {
        this.gridColor = gridColor;
    }

    public Color getAxesColor() {
        return axesColor;
    }

    public void setAxesColor(Color axesColor) {
        this.axesColor = axesColor;
    }

    public void clearFunctions() {
        funcs.clear();
    }

    public void addFunction(DoubleUnaryOperator operator, Paint paint, double width) {
        FunctionData func = new FunctionData(operator, paint, width);
        funcs.add(func);
    }

    // Побудова графіку:
    public void drawGraph(double xMin, double xMax) {
        // Визначаємо та обчислюємо діапазон функції:
        double yMin = -VERTICAL_GAP;
        double yMax = VERTICAL_GAP;
        for (FunctionData func : funcs) {
            double min = minY(xMin, xMax, func.getOperator());
            double max = maxY(xMin, xMax, func.getOperator());
            if (yMin > min) {
                yMin = min;
            }
            if (yMax < max) {
                yMax = max;
            }
        }
        yMin -= VERTICAL_GAP * Math.abs(yMin);
        yMax += VERTICAL_GAP * Math.abs(yMax);
        // Розміри графіку:
        double width = pane.getWidth();
        double height = pane.getHeight();
        // Масштаби:
        double xScale = width / (xMax - xMin);
        double yScale = height / (yMax - yMin);
        // Координати проекції початку коордитат:
        double x0 = -xMin * xScale;
        double y0 = yMax * yScale;
        pane.getChildren().clear();

        // Сітка:
        double xStep = 1; // Крок сітки
        // Змінюємо крок, якщо лінії розташовані надто часто:
        while (xStep * xScale < MIN_PIXELS)
            xStep *= SCALE_STEP;
        // Змінюємо крок, якщо лінії розташовані надто рідко:
        while (xStep * xScale > MIN_PIXELS * SCALE_STEP)
            xStep /= SCALE_STEP;
        // Вертикальні лінії сітки
        for (double dx = xStep; dx < xMax; dx += xStep) {
            double x = x0 + dx * xScale;
            Line line = new Line(x, 0, x, height);
            line.setStroke(gridColor);
            pane.getChildren().add(line);
            pane.getChildren().add(new Text(x + MIN_GAP, MAX_GAP, String.format(FORMAT, dx)));
        }
        for (double dx = -xStep; dx >= xMin; dx -= xStep) {
            double x = x0 + dx * xScale;
            Line line = new Line(x, 0, x, height);
            line.setStroke(gridColor);
            pane.getChildren().add(line);
            pane.getChildren().add(new Text(x + MIN_GAP, MAX_GAP, String.format(FORMAT, dx)));
        }
        double yStep = 1;  // Крок сітки
        // Змінюємо крок, якщо лінії розташовані надто часто:
        while (yStep * yScale < MIN_PIXELS) {
            yStep *= SCALE_STEP;
        }
        // Змінюємо крок, якщо лінії розташовані надто рідко:
        while (yStep * yScale > MIN_PIXELS * SCALE_STEP)
            yStep /= SCALE_STEP;
        // Горизонтальні лінії сітки
        for (double dy = yStep; dy < yMax; dy += yStep) {
            double y = y0 - dy * yScale;
            Line line = new Line(0, y, width, y);
            line.setStroke(gridColor);
            pane.getChildren().add(line);
            pane.getChildren().add(new Text(MIN_GAP, y - MIN_GAP, String.format(FORMAT, dy)));
        }
        for (double dy = -yStep; dy > yMin; dy -= yStep) {
            double y = y0 - dy * yScale;
            Line line = new Line(0, y, width, y);
            line.setStroke(gridColor);
            pane.getChildren().add(line);
            pane.getChildren().add(new Text(MIN_GAP, y - MIN_GAP, String.format(FORMAT, dy)));
        }

        // Осі:
        Line verticalAxis = new Line(x0, 0, x0, height);
        verticalAxis.setStroke(axesColor);
        pane.getChildren().add(verticalAxis);
        Line horizontalAxis = new Line(0, y0, width, y0);
        pane.getChildren().add(horizontalAxis);
        pane.getChildren().add(new Text(MIN_GAP, y0 - MIN_GAP, "0.0"));
        pane.getChildren().add(new Text(MIN_GAP, y0 - MIN_GAP, "0.0"));
        pane.getChildren().add(new Text(width - MAX_GAP, y0 - MIN_GAP, "X"));
        pane.getChildren().add(new Text(x0 + MIN_GAP, MAX_GAP, "Y"));

        // Функції:
        for (FunctionData func : funcs) {
            Polyline polyline = new Polyline();
            polyline.setStroke(func.getPaint());
            polyline.setStrokeWidth(func.getWidth());
            for (double x = 0; x < width; x++) {
                double dx = (x - x0) / xScale;
                double dy = func.getOperator().applyAsDouble(dx);
                double y = y0 - dy * yScale;
                polyline.getPoints().addAll(x, y);
            }
            pane.getChildren().add(polyline);
        }
    }
}

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

У класі GraphBuilder описано поля - посилання на панель, в якій здійснюється побудова графіків, кольори сітки й осей, а також список описів функцій, які ми будуємо - список об'єктів типу FunctionData. Цей список може бути порожнім. В цьому випадку здійснюватиметься малювання осей і сітки без графіків функцій.

Функція drawGraph() отримує параметри - інтервал зміни аргументу xMin і xMax, який буде спільним для всіх функцій. Залежно від інтервалу зміни функцій слід визначити діапазон y так, щоб були повністю відображені графіки всіх функцій. Для визначення діапазону y можна створити окремі методи. Змінні yMin і yMax зберігатимуть початок і кінець діапазону y.

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

У функції побудови графіків drawGraph() здійснюється робота з двома системами координат - реальними світовими координатами, пов'язаними з математичними функціями, і координатами точок всередині панелі. У наведеному нижче коді цієї функції для реальних координат використані імена змінних dx і dy, а для координат точок - x і y. Слід пам'ятати, що вертикальна вісь реальних світових координат і координат точок на панелі спрямовані в протилежні боки. Це впливатиме на визначення формул перерахування координат.

Для побудови графіків визначаються масштаби xScale і yScale (кількість точок в одиниці реальних світових координат). Далі визначаються координати точки на панелі x0 і y0, в яку проектується початок світових координат.

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

Тепер класом GraphBuilder можна скористатися в застосунку JavaFX:

package ua.in.iwanoff.java.fifth;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.AnchorPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

public class ShapesGraph  extends Application {

    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle("Functions");
        AnchorPane pane = new AnchorPane();
        GraphBuilder builder = new GraphBuilder(pane);
        builder.addFunction(Math::sin, Color.CYAN, 1);
        builder.addFunction(Math::cos, Color.BLUE, 2);
        // Дуже важливо будувати графік після визначення розмірів панелі:
        Scene scene = new Scene(pane,  1000, 350);
        builder.drawGraph(-4, 6);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

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

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

Якщо в списку фактичних параметрів методу drawGraph() вказати інтервал більший (наприклад, drawGraph(-50, 100)), або менший (наприклад, drawGraph(-0.1, 0.6)), програма здійснюватиме перерахування кроку сітки. Рекомендовано переконатися в цьому самостійно.

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

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

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

  1. У чому полягає ідея парадигми розробки через тестування?
  2. Для чого використовують механізм перевірки тверджень?
  3. Як налаштувати програмне середовище для того, щоб можна було скористатися перевіркою тверджень?
  4. Що таке модульне тестування?
  5. Що таке JUnit?
  6. Як здійснюється анотування методів тестування в JUnit?
  7. Як здійснити логічне групування тестів?
  8. Як скористатися JUnit у програмному середовищі?
  9. Які стандартні графічні засоби надає Java?
  10. Чим відрізняється декларативний підхід побудови графічних зображень від імперативного?
  11. Які рівні реалізації 2D-графіки існують у JavaFX?
  12. Як скористатися засобами класу Canvas?
  13. Як здійснюється робота зі стандартними графічними примітивами?
  14. Як здійснюється робота з діаграмами і графіками у JavaFX?