Проектування баз даних

AtlantaFX: Сучасні теми для JavaFX додатків

Від стандартного Modena до сучасного дизайну: AtlantaFX як колекція готових тем (Primer, Nord, Cupertino, Dracula), додаткові контроли, CSS Variables, utility classes, інтеграція з MVVM-додатками.

AtlantaFX: Сучасні теми для JavaFX додатків

Вступ: Проблема стандартного вигляду JavaFX

JavaFX постачається зі стандартною темою Modena — це тема, що використовується за замовчуванням з JavaFX 8. Вона функціональна, але виглядає застаріло порівняно з сучасними веб-додатками та desktop-додатками (Electron, Flutter). Сірі кнопки, стандартні шрифти, відсутність темної теми — все це робить JavaFX-додатки схожими на програми з 2010-х років.

Створення власної теми з нуля — це складно. Потрібно стилізувати десятки компонентів (Button, TextField, TableView, ComboBox, DatePicker), підтримувати світлу та темну теми, забезпечити консистентність кольорів, відступів, шрифтів. Це сотні рядків CSS-коду та тижні роботи.

AtlantaFX вирішує цю проблему. Це колекція готових, професійно виглядаючих тем для JavaFX, натхненних сучасними веб-фреймворками та дизайн-системами (GitHub Primer, Nord, Dracula). AtlantaFX надає:

  • 7 готових тем (Primer Light/Dark, Nord Light/Dark, Cupertino Light/Dark, Dracula).
  • Додаткові контроли (ToggleSwitch, PasswordTextField, Popover, Notification, Message, Tile).
  • CSS Variables для легкої кастомізації кольорів.
  • Utility classes для швидкої стилізації (.text-bold, .bg-success, .rounded).
  • Сумісність з JavaFX 11+ та модульною системою Java.

Ця стаття — це повний гід по AtlantaFX: від встановлення до глибокої кастомізації. Ми розглянемо:

  • Встановлення та базове налаштування.
  • Огляд всіх доступних тем.
  • Перемикання тем у runtime.
  • Додаткові контроли та їхнє використання.
  • CSS Variables для кастомізації кольорів.
  • Utility classes для швидкої стилізації.
  • Інтеграцію з MVVM-додатками.
  • Створення власної теми на базі AtlantaFX.
AtlantaFX vs власний CSS: AtlantaFX — це не заміна CSS, а доповнення. Ви можете використовувати AtlantaFX як базову тему та додавати власні стилі поверх неї. Це економить час на стилізації стандартних компонентів та дозволяє зосередитися на унікальних елементах вашого додатку.

Встановлення та базове налаштування

AtlantaFX доступний через Maven Central, тому встановлення зводиться до додавання залежності у pom.xml або build.gradle.

Maven

<dependency>
    <groupId>io.github.mkpaz</groupId>
    <artifactId>atlantafx-base</artifactId>
    <version>2.1.0</version>
</dependency>

Gradle

repositories {
    mavenCentral()
}

dependencies {
    implementation 'io.github.mkpaz:atlantafx-base:2.1.0'
}

Версія 2.1.0 — остання стабільна версія на момент написання статті (травень 2026). Перевірте GitHub Releases для актуальної версії.

Застосування теми у додатку

Після додавання залежності застосуйте тему у методі Application.start():

package dev.kostyl.audiobook;

import atlantafx.base.theme.PrimerLight;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class AudiobookApp extends Application {
    
    @Override
    public void start(Stage primaryStage) {
        // Застосування теми AtlantaFX
        Application.setUserAgentStylesheet(new PrimerLight().getUserAgentStylesheet());
        
        // Створення UI
        Button button = new Button("Hello AtlantaFX!");
        StackPane root = new StackPane(button);
        Scene scene = new Scene(root, 400, 300);
        
        primaryStage.setTitle("AtlantaFX Demo");
        primaryStage.setScene(scene);
        primaryStage.show();
    }
    
    public static void main(String[] args) {
        launch(args);
    }
}

Рядок 14: Application.setUserAgentStylesheet() — це метод JavaFX, що встановлює глобальну тему для всього додатку. Він замінює стандартну тему Modena на AtlantaFX.

Рядок 14: new PrimerLight().getUserAgentStylesheet() — створює екземпляр теми Primer Light та повертає шлях до її CSS-файлу.

Важливо: setUserAgentStylesheet() має викликатися до створення Scene. Якщо викликати після, тема не застосується до вже створених компонентів.

Альтернативний спосіб: Через Scene

Якщо потрібно застосувати тему лише до конкретної Scene (а не глобально):

Scene scene = new Scene(root, 400, 300);
scene.getStylesheets().add(new PrimerLight().getUserAgentStylesheet());

Цей підхід корисний, якщо у додатку кілька вікон з різними темами.


Огляд доступних тем

AtlantaFX постачається з 7 готовими темами, кожна з яких має унікальну колірну палітру та стиль.

1. Primer Light / Primer Dark

Натхнення: GitHub Primer Design System.

Колірна палітра: Нейтральні сірі тони з синім акцентом.

Коли використовувати: Універсальна тема для бізнес-додатків, інструментів розробника, адміністративних панелей.

// Light
Application.setUserAgentStylesheet(new PrimerLight().getUserAgentStylesheet());

// Dark
Application.setUserAgentStylesheet(new PrimerDark().getUserAgentStylesheet());

Особливості:

  • Високий контраст для читабельності.
  • Мінімалістичний дизайн без зайвих деталей.
  • Синій акцент (#0969da) для кнопок та посилань.

2. Nord Light / Nord Dark

Натхнення: Nord Color Palette — арктична, холодна колірна схема.

Колірна палітра: Холодні сині та сірі тони.

Коли використовувати: Додатки для розробників (IDE, редактори коду), творчі інструменти.

// Light
Application.setUserAgentStylesheet(new NordLight().getUserAgentStylesheet());

// Dark
Application.setUserAgentStylesheet(new NordDark().getUserAgentStylesheet());

Особливості:

  • М'які, приглушені кольори (не яскраві).
  • Популярна серед розробників (Nord — одна з найпопулярніших тем для VS Code, Vim).
  • Темна тема має глибокий синьо-сірий фон (#2e3440).

3. Cupertino Light / Cupertino Dark

Натхнення: macOS та iOS дизайн (Apple Human Interface Guidelines).

Колірна палітра: Світлі сірі тони з синім акцентом (#007aff).

Коли використовувати: Додатки для macOS, додатки з Apple-подібним дизайном.

// Light
Application.setUserAgentStylesheet(new CupertinoLight().getUserAgentStylesheet());

// Dark
Application.setUserAgentStylesheet(new CupertinoDark().getUserAgentStylesheet());

Особливості:

  • Заокруглені кути (більш округлі, ніж у Primer).
  • Світлі тіні та м'які переходи.
  • Синій акцент (#007aff) — фірмовий колір Apple.

4. Dracula

Натхнення: Dracula Theme — популярна темна тема для редакторів коду.

Колірна палітра: Темний фіолетовий фон (#282a36) з яскравими акцентами (рожевий, зелений, жовтий).

Коли використовувати: Додатки для розробників, творчі інструменти, нічний режим.

Application.setUserAgentStylesheet(new Dracula().getUserAgentStylesheet());

Особливості:

  • Висока контрастність між фоном та текстом.
  • Яскраві акцентні кольори (рожевий #ff79c6, зелений #50fa7b).
  • Популярна серед розробників (Dracula — одна з найпопулярніших тем для терміналів та редакторів).

Порівняння тем

Primer

Стиль: Мінімалістичний, бізнесовий

Акцент: Синій (#0969da)

Використання: Бізнес-додатки, інструменти розробника

Особливості: Високий контраст, чіткі лінії

Nord

Стиль: Арктичний, холодний

Акцент: Блакитний (#88c0d0)

Використання: IDE, редактори коду

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

Cupertino

Стиль: Apple-подібний

Акцент: Синій (#007aff)

Використання: macOS додатки

Особливості: Заокруглені кути, світлі тіні

Dracula

Стиль: Темний, яскравий

Акцент: Рожевий (#ff79c6)

Використання: Нічний режим, творчі інструменти

Особливості: Висока контрастність, яскраві акценти


Перемикання тем у runtime

Користувачі очікують можливість змінювати тему без перезапуску додатку. AtlantaFX підтримує динамічну зміну тем.

Простий спосіб: Зміна User Agent Stylesheet

public class ThemeManager {
    
    public enum Theme {
        PRIMER_LIGHT(new PrimerLight()),
        PRIMER_DARK(new PrimerDark()),
        NORD_LIGHT(new NordLight()),
        NORD_DARK(new NordDark()),
        CUPERTINO_LIGHT(new CupertinoLight()),
        CUPERTINO_DARK(new CupertinoDark()),
        DRACULA(new Dracula());
        
        private final atlantafx.base.theme.Theme theme;
        
        Theme(atlantafx.base.theme.Theme theme) {
            this.theme = theme;
        }
        
        public String getStylesheet() {
            return theme.getUserAgentStylesheet();
        }
    }
    
    private Theme currentTheme = Theme.PRIMER_LIGHT;
    
    public void setTheme(Theme theme) {
        this.currentTheme = theme;
        Application.setUserAgentStylesheet(theme.getStylesheet());
    }
    
    public Theme getCurrentTheme() {
        return currentTheme;
    }
    
    public void toggleDarkMode() {
        switch (currentTheme) {
            case PRIMER_LIGHT -> setTheme(Theme.PRIMER_DARK);
            case PRIMER_DARK -> setTheme(Theme.PRIMER_LIGHT);
            case NORD_LIGHT -> setTheme(Theme.NORD_DARK);
            case NORD_DARK -> setTheme(Theme.NORD_LIGHT);
            case CUPERTINO_LIGHT -> setTheme(Theme.CUPERTINO_DARK);
            case CUPERTINO_DARK -> setTheme(Theme.CUPERTINO_LIGHT);
            case DRACULA -> setTheme(Theme.PRIMER_LIGHT);
        }
    }
}

Використання у Controller

public class SettingsController {
    
    private final ThemeManager themeManager;
    
    @FXML private ComboBox<ThemeManager.Theme> themeComboBox;
    @FXML private ToggleSwitch darkModeToggle;
    
    @Inject
    public SettingsController(ThemeManager themeManager) {
        this.themeManager = themeManager;
    }
    
    @FXML
    public void initialize() {
        // Заповнення ComboBox темами
        themeComboBox.getItems().addAll(ThemeManager.Theme.values());
        themeComboBox.setValue(themeManager.getCurrentTheme());
        
        // Обробка зміни теми
        themeComboBox.setOnAction(e -> {
            ThemeManager.Theme selected = themeComboBox.getValue();
            themeManager.setTheme(selected);
        });
        
        // Обробка перемикання Dark Mode
        darkModeToggle.setOnAction(e -> {
            themeManager.toggleDarkMode();
        });
    }
}

Збереження вибраної теми

Використовуємо Preferences API для збереження вибраної теми між запусками:

public class ThemeManager {
    
    private static final String PREFS_KEY_THEME = "theme";
    private final Preferences prefs;
    
    public ThemeManager() {
        this.prefs = Preferences.userNodeForPackage(ThemeManager.class);
        loadSavedTheme();
    }
    
    private void loadSavedTheme() {
        String savedTheme = prefs.get(PREFS_KEY_THEME, Theme.PRIMER_LIGHT.name());
        try {
            Theme theme = Theme.valueOf(savedTheme);
            setTheme(theme);
        } catch (IllegalArgumentException e) {
            setTheme(Theme.PRIMER_LIGHT);
        }
    }
    
    public void setTheme(Theme theme) {
        this.currentTheme = theme;
        Application.setUserAgentStylesheet(theme.getStylesheet());
        prefs.put(PREFS_KEY_THEME, theme.name());
    }
}

Тепер при запуску додатку автоматично завантажується остання вибрана тема.


Додаткові контроли AtlantaFX

AtlantaFX надає додаткові контроли, яких немає у стандартному JavaFX.

ToggleSwitch: Перемикач iOS-стилю

ToggleSwitch — це сучасна альтернатива CheckBox, що виглядає як перемикач у мобільних додатках.

import atlantafx.base.controls.ToggleSwitch;

ToggleSwitch darkModeSwitch = new ToggleSwitch("Dark Mode");
darkModeSwitch.setSelected(false);

darkModeSwitch.selectedProperty().addListener((obs, oldVal, newVal) -> {
    if (newVal) {
        themeManager.setTheme(ThemeManager.Theme.PRIMER_DARK);
    } else {
        themeManager.setTheme(ThemeManager.Theme.PRIMER_LIGHT);
    }
});

Стилізація:

.toggle-switch {
    -fx-background-color: -color-bg-subtle;
}

.toggle-switch:selected {
    -fx-background-color: -color-accent-emphasis;
}

PasswordTextField: Поле пароля з кнопкою показу

PasswordTextField — це розширення стандартного PasswordField з кнопкою для показу/приховування пароля.

import atlantafx.base.controls.PasswordTextField;

PasswordTextField passwordField = new PasswordTextField();
passwordField.setPromptText("Enter password");
passwordField.setLeft(new FontIcon(Material2AL.LOCK)); // Іконка зліва

Особливості:

  • Кнопка "показати пароль" (іконка ока) справа.
  • Підтримка іконок зліва та справа через setLeft() та setRight().
  • Автоматична стилізація під поточну тему.

Popover: Спливаюче вікно

Popover — це спливаюче вікно, що прикріплюється до елемента (як tooltip, але з довільним вмістом).

import atlantafx.base.controls.Popover;

Button infoButton = new Button("Info");

Popover popover = new Popover();
popover.setTitle("Information");
popover.setContentNode(new Label("This is additional information about the feature."));
popover.setArrowLocation(Popover.ArrowLocation.TOP_CENTER);

infoButton.setOnAction(e -> {
    popover.show(infoButton);
});

Властивості Popover:

  • setTitle(String) — заголовок у header.
  • setContentNode(Node) — довільний вміст (Label, VBox, форма).
  • setArrowLocation(ArrowLocation) — позиція стрілки (TOP, BOTTOM, LEFT, RIGHT).
  • setDetachable(boolean) — чи можна відкріпити Popover від елемента.
  • setAnimated(boolean) — анімація появи/зникнення.
  • setFadeInDuration(Duration) — тривалість анімації появи.

Приклад: Popover з формою:

VBox content = new VBox(10);
content.setPadding(new Insets(10));

TextField nameField = new TextField();
nameField.setPromptText("Name");

TextField emailField = new TextField();
emailField.setPromptText("Email");

Button submitButton = new Button("Submit");
submitButton.getStyleClass().add(Styles.ACCENT);

content.getChildren().addAll(
    new Label("Contact Form"),
    nameField,
    emailField,
    submitButton
);

Popover popover = new Popover(content);
popover.setTitle("Contact Us");
popover.setDetachable(true);

Message: Повідомлення з іконкою

Message — це компонент для відображення повідомлень (інформація, попередження, помилка).

import atlantafx.base.controls.Message;
import atlantafx.base.theme.Styles;

// Інформаційне повідомлення
Message infoMessage = new Message(
    "Information",
    "Your changes have been saved successfully."
);
infoMessage.getStyleClass().add(Styles.ACCENT);

// Попередження
Message warningMessage = new Message(
    "Warning",
    "This action cannot be undone."
);
warningMessage.getStyleClass().add(Styles.WARNING);

// Помилка
Message errorMessage = new Message(
    "Error",
    "Failed to connect to the server."
);
errorMessage.getStyleClass().add(Styles.DANGER);

Стилі для Message:

  • Styles.ACCENT — синє повідомлення (інформація).
  • Styles.SUCCESS — зелене повідомлення (успіх).
  • Styles.WARNING — жовте повідомлення (попередження).
  • Styles.DANGER — червоне повідомлення (помилка).

Notification: Тимчасове повідомлення

Notification — це toast-повідомлення, що з'являється у куті екрану та автоматично зникає.

import atlantafx.base.controls.Notification;

Notification notification = new Notification(
    "Success",
    "Audiobook added successfully!"
);
notification.getStyleClass().add(Styles.SUCCESS);

// Показ notification у правому верхньому куті
notification.show(scene);

// Автоматичне закриття через 3 секунди
PauseTransition delay = new PauseTransition(Duration.seconds(3));
delay.setOnFinished(e -> notification.hide());
delay.play();

Tile: Інформаційна плитка

Tile — це компонент для відображення інформації у вигляді плитки (як у dashboard).

import atlantafx.base.controls.Tile;

Tile audioBooksTile = new Tile(
    "Total Audiobooks",
    "1,234"
);
audioBooksTile.setDescription("↑ 12% from last month");
audioBooksTile.getStyleClass().add(Styles.ACCENT);

Tile usersTile = new Tile(
    "Active Users",
    "567"
);
usersTile.setDescription("↑ 8% from last month");
usersTile.getStyleClass().add(Styles.SUCCESS);

CSS Variables: Кастомізація кольорів

AtlantaFX використовує CSS Variables для всіх кольорів, що дозволяє легко кастомізувати тему.

Основні змінні кольорів

Foreground (текст):

--color-fg-default: /* Основний текст */
--color-fg-muted: /* Приглушений текст (вторинний) */
--color-fg-subtle: /* Ледь помітний текст (placeholder) */
--color-fg-emphasis: /* Акцентований текст (заголовки) */

Background (фон):

--color-bg-default: /* Основний фон */
--color-bg-overlay: /* Фон для модальних вікон */
--color-bg-subtle: /* Світліший фон (панелі) */
--color-bg-inset: /* Темніший фон (input fields) */

Border (рамки):

--color-border-default: /* Основні рамки */
--color-border-muted: /* Приглушені рамки */
--color-border-subtle: /* Ледь помітні рамки */

Accent (акцентні кольори):

--color-accent-fg: /* Акцентний текст */
--color-accent-emphasis: /* Акцентний фон (кнопки) */
--color-accent-muted: /* Приглушений акцент */
--color-accent-subtle: /* Ледь помітний акцент */

Status (статусні кольори):

/* Success (зелений) */
--color-success-fg: /* Текст успіху */
--color-success-emphasis: /* Фон успіху */
--color-success-muted: /* Приглушений успіх */
--color-success-subtle: /* Ледь помітний успіх */

/* Warning (жовтий) */
--color-warning-fg: /* Текст попередження */
--color-warning-emphasis: /* Фон попередження */
--color-warning-muted: /* Приглушене попередження */
--color-warning-subtle: /* Ледь помітне попередження */

/* Danger (червоний) */
--color-danger-fg: /* Текст помилки */
--color-danger-emphasis: /* Фон помилки */
--color-danger-muted: /* Приглушена помилка */
--color-danger-subtle: /* Ледь помітна помилка */

Перевизначення змінних

Створіть власний CSS-файл та перевизначте змінні:

/* custom-theme.css */

.root {
    /* Зміна акцентного кольору на фіолетовий */
    --color-accent-emphasis: #9b59b6;
    --color-accent-fg: #9b59b6;
    --color-accent-muted: rgba(155, 89, 182, 0.4);
    --color-accent-subtle: rgba(155, 89, 182, 0.1);
    
    /* Зміна кольору успіху на бірюзовий */
    --color-success-emphasis: #1abc9c;
    --color-success-fg: #1abc9c;
    
    /* Зміна фону */
    --color-bg-default: #fafafa;
}

Підключення:

Application.setUserAgentStylesheet(new PrimerLight().getUserAgentStylesheet());
scene.getStylesheets().add(getClass().getResource("/css/custom-theme.css").toExternalForm());

Тепер всі компоненти, що використовують --color-accent-emphasis, матимуть фіолетовий колір.


Utility Classes: Швидка стилізація

AtlantaFX надає utility classes для швидкої стилізації без написання CSS.

Текстові стилі

import atlantafx.base.theme.Styles;

// Розмір тексту
label.getStyleClass().add(Styles.TEXT_SMALL); // Малий текст
label.getStyleClass().add(Styles.TITLE_1); // Великий заголовок
label.getStyleClass().add(Styles.TITLE_2); // Середній заголовок
label.getStyleClass().add(Styles.TITLE_3); // Малий заголовок
label.getStyleClass().add(Styles.TITLE_4); // Найменший заголовок

// Вага шрифту
label.getStyleClass().add(Styles.TEXT_BOLD); // Жирний
label.getStyleClass().add(Styles.TEXT_BOLDER); // Дуже жирний
label.getStyleClass().add(Styles.TEXT_NORMAL); // Звичайний
label.getStyleClass().add(Styles.TEXT_LIGHTER); // Тонкий

// Стиль шрифту
label.getStyleClass().add(Styles.TEXT_ITALIC); // Курсив
label.getStyleClass().add(Styles.TEXT_OBLIQUE); // Похилий
label.getStyleClass().add(Styles.TEXT_UNDERLINED); // Підкреслений
label.getStyleClass().add(Styles.TEXT_STRIKETHROUGH); // Закреслений

// Колір тексту
label.getStyleClass().add(Styles.TEXT_MUTED); // Приглушений
label.getStyleClass().add(Styles.TEXT_SUBTLE); // Ледь помітний

Фонові кольори

// Акцентні фони
pane.getStyleClass().add(Styles.BG_ACCENT_EMPHASIS); // Яскравий акцент
pane.getStyleClass().add(Styles.BG_ACCENT_MUTED); // Приглушений акцент
pane.getStyleClass().add(Styles.BG_ACCENT_SUBTLE); // Ледь помітний акцент

// Статусні фони
pane.getStyleClass().add(Styles.BG_SUCCESS_EMPHASIS); // Зелений
pane.getStyleClass().add(Styles.BG_WARNING_EMPHASIS); // Жовтий
pane.getStyleClass().add(Styles.BG_DANGER_EMPHASIS); // Червоний

// Нейтральні фони
pane.getStyleClass().add(Styles.BG_DEFAULT); // Основний фон
pane.getStyleClass().add(Styles.BG_SUBTLE); // Світліший фон
pane.getStyleClass().add(Styles.BG_INSET); // Темніший фон

Стилі кнопок

// Акцентні кнопки
button.getStyleClass().add(Styles.ACCENT); // Синя кнопка
button.getStyleClass().add(Styles.SUCCESS); // Зелена кнопка
button.getStyleClass().add(Styles.DANGER); // Червона кнопка

// Варіанти кнопок
button.getStyleClass().add(Styles.BUTTON_OUTLINED); // Контурна кнопка
button.getStyleClass().add(Styles.FLAT); // Плоска кнопка (без фону)

// Розміри кнопок
button.getStyleClass().add(Styles.SMALL); // Маленька кнопка
button.getStyleClass().add(Styles.LARGE); // Велика кнопка

// Форми кнопок
button.getStyleClass().add(Styles.ROUNDED); // Заокруглена кнопка
button.getStyleClass().add(Styles.BUTTON_CIRCLE); // Кругла кнопка
button.getStyleClass().add(Styles.BUTTON_ICON); // Іконка-кнопка (без тексту)

Приклад: Форма з utility classes

VBox form = new VBox(15);
form.setPadding(new Insets(20));
form.getStyleClass().add(Styles.BG_DEFAULT);

Label titleLabel = new Label("Add Audiobook");
titleLabel.getStyleClass().addAll(Styles.TITLE_2, Styles.TEXT_BOLD);

TextField titleField = new TextField();
titleField.setPromptText("Title");

TextField authorField = new TextField();
authorField.setPromptText("Author");

HBox buttons = new HBox(10);
Button saveButton = new Button("Save");
saveButton.getStyleClass().addAll(Styles.ACCENT, Styles.LARGE);

Button cancelButton = new Button("Cancel");
cancelButton.getStyleClass().add(Styles.FLAT);

buttons.getChildren().addAll(saveButton, cancelButton);

form.getChildren().addAll(titleLabel, titleField, authorField, buttons);

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


Інтеграція з MVVM-додатками

AtlantaFX легко інтегрується з MVVM-архітектурою, яку ми розглядали у попередніх статтях.

ThemeManager як Singleton через Guice

package dev.kostyl.audiobook.infrastructure;

import atlantafx.base.theme.*;
import com.google.inject.Singleton;
import javafx.application.Application;

import java.util.prefs.Preferences;

@Singleton
public class ThemeManager {
    
    public enum Theme {
        PRIMER_LIGHT("Primer Light", new PrimerLight()),
        PRIMER_DARK("Primer Dark", new PrimerDark()),
        NORD_LIGHT("Nord Light", new NordLight()),
        NORD_DARK("Nord Dark", new NordDark()),
        CUPERTINO_LIGHT("Cupertino Light", new CupertinoLight()),
        CUPERTINO_DARK("Cupertino Dark", new CupertinoDark()),
        DRACULA("Dracula", new Dracula());
        
        private final String displayName;
        private final atlantafx.base.theme.Theme theme;
        
        Theme(String displayName, atlantafx.base.theme.Theme theme) {
            this.displayName = displayName;
            this.theme = theme;
        }
        
        public String getDisplayName() {
            return displayName;
        }
        
        public String getStylesheet() {
            return theme.getUserAgentStylesheet();
        }
        
        public boolean isDark() {
            return theme.isDarkMode();
        }
    }
    
    private static final String PREFS_KEY_THEME = "atlantafx.theme";
    private final Preferences prefs;
    private Theme currentTheme;
    
    public ThemeManager() {
        this.prefs = Preferences.userNodeForPackage(ThemeManager.class);
        this.currentTheme = loadSavedTheme();
        applyTheme(currentTheme);
    }
    
    private Theme loadSavedTheme() {
        String savedTheme = prefs.get(PREFS_KEY_THEME, Theme.PRIMER_LIGHT.name());
        try {
            return Theme.valueOf(savedTheme);
        } catch (IllegalArgumentException e) {
            return Theme.PRIMER_LIGHT;
        }
    }
    
    public void setTheme(Theme theme) {
        this.currentTheme = theme;
        applyTheme(theme);
        prefs.put(PREFS_KEY_THEME, theme.name());
    }
    
    private void applyTheme(Theme theme) {
        Application.setUserAgentStylesheet(theme.getStylesheet());
    }
    
    public Theme getCurrentTheme() {
        return currentTheme;
    }
    
    public boolean isDarkMode() {
        return currentTheme.isDark();
    }
}

Реєстрація у Guice Module

@Override
protected void configure() {
    // Infrastructure
    bind(ThemeManager.class).in(Singleton.class);
    
    // ... інші bindings
}

ViewModel для налаштувань теми

package dev.kostyl.audiobook.viewmodel;

import com.google.inject.Inject;
import dev.kostyl.audiobook.infrastructure.ThemeManager;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;

public class SettingsViewModel {
    
    private final ThemeManager themeManager;
    
    private final ObservableList<ThemeManager.Theme> availableThemes = 
        FXCollections.observableArrayList(ThemeManager.Theme.values());
    
    private final ObjectProperty<ThemeManager.Theme> selectedTheme = 
        new SimpleObjectProperty<>();
    
    private final BooleanProperty darkMode = new SimpleBooleanProperty();
    
    @Inject
    public SettingsViewModel(ThemeManager themeManager) {
        this.themeManager = themeManager;
        
        // Ініціалізація поточної теми
        selectedTheme.set(themeManager.getCurrentTheme());
        darkMode.set(themeManager.isDarkMode());
        
        // Listener для зміни теми
        selectedTheme.addListener((obs, oldTheme, newTheme) -> {
            if (newTheme != null) {
                themeManager.setTheme(newTheme);
                darkMode.set(newTheme.isDark());
            }
        });
    }
    
    public ObservableList<ThemeManager.Theme> getAvailableThemes() {
        return availableThemes;
    }
    
    public ObjectProperty<ThemeManager.Theme> selectedThemeProperty() {
        return selectedTheme;
    }
    
    public BooleanProperty darkModeProperty() {
        return darkMode;
    }
    
    public void toggleDarkMode() {
        ThemeManager.Theme current = selectedTheme.get();
        ThemeManager.Theme newTheme = switch (current) {
            case PRIMER_LIGHT -> ThemeManager.Theme.PRIMER_DARK;
            case PRIMER_DARK -> ThemeManager.Theme.PRIMER_LIGHT;
            case NORD_LIGHT -> ThemeManager.Theme.NORD_DARK;
            case NORD_DARK -> ThemeManager.Theme.NORD_LIGHT;
            case CUPERTINO_LIGHT -> ThemeManager.Theme.CUPERTINO_DARK;
            case CUPERTINO_DARK -> ThemeManager.Theme.CUPERTINO_LIGHT;
            case DRACULA -> ThemeManager.Theme.PRIMER_LIGHT;
        };
        selectedTheme.set(newTheme);
    }
}

Controller з Bindings

package dev.kostyl.audiobook.controller;

import atlantafx.base.controls.ToggleSwitch;
import atlantafx.base.theme.Styles;
import com.google.inject.Inject;
import dev.kostyl.audiobook.infrastructure.ThemeManager;
import dev.kostyl.audiobook.viewmodel.SettingsViewModel;
import javafx.fxml.FXML;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.util.StringConverter;

public class SettingsController {
    
    @FXML private ComboBox<ThemeManager.Theme> themeComboBox;
    @FXML private ToggleSwitch darkModeToggle;
    @FXML private Label currentThemeLabel;
    
    private final SettingsViewModel viewModel;
    
    @Inject
    public SettingsController(SettingsViewModel viewModel) {
        this.viewModel = viewModel;
    }
    
    @FXML
    public void initialize() {
        setupThemeComboBox();
        setupDarkModeToggle();
        setupBindings();
    }
    
    private void setupThemeComboBox() {
        themeComboBox.setItems(viewModel.getAvailableThemes());
        
        // Converter для відображення назв тем
        themeComboBox.setConverter(new StringConverter<>() {
            @Override
            public String toString(ThemeManager.Theme theme) {
                return theme != null ? theme.getDisplayName() : "";
            }
            
            @Override
            public ThemeManager.Theme fromString(String string) {
                return null;
            }
        });
    }
    
    private void setupDarkModeToggle() {
        darkModeToggle.setText("Dark Mode");
    }
    
    private void setupBindings() {
        // Bidirectional binding для вибраної теми
        themeComboBox.valueProperty().bindBidirectional(
            viewModel.selectedThemeProperty()
        );
        
        // Bidirectional binding для dark mode toggle
        darkModeToggle.selectedProperty().bindBidirectional(
            viewModel.darkModeProperty()
        );
        
        // Unidirectional binding для відображення поточної теми
        currentThemeLabel.textProperty().bind(
            viewModel.selectedThemeProperty().asString()
        );
    }
    
    @FXML
    private void onToggleDarkMode() {
        viewModel.toggleDarkMode();
    }
}

FXML з AtlantaFX контролами

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

<?import atlantafx.base.controls.ToggleSwitch?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.geometry.Insets?>

<VBox xmlns:fx="http://javafx.com/fxml"
      fx:controller="dev.kostyl.audiobook.controller.SettingsController"
      spacing="20" padding="20">
    
    <Label text="Theme Settings" styleClass="title-2, text-bold"/>
    
    <VBox spacing="10">
        <Label text="Select Theme:"/>
        <ComboBox fx:id="themeComboBox" prefWidth="300"/>
    </VBox>
    
    <ToggleSwitch fx:id="darkModeToggle" onAction="#onToggleDarkMode"/>
    
    <Label fx:id="currentThemeLabel" styleClass="text-muted"/>
    
</VBox>

Практичні завдання

Рівень 1: Базове використання AtlantaFX

Завдання 1.1: Додайте AtlantaFX до вашого проєкту через Maven/Gradle. Застосуйте тему Primer Light до додатку. Перевірте, що всі стандартні компоненти (Button, TextField, TableView) автоматично стилізовані.

Завдання 1.2: Створіть форму з TextField, PasswordTextField (з AtlantaFX), та кнопками. Використайте utility classes (Styles.ACCENT, Styles.LARGE) для стилізації кнопок.

Завдання 1.3: Додайте Message компонент для відображення повідомлень про успіх/помилку. Використайте Styles.SUCCESS та Styles.DANGER для різних типів повідомлень.

Рівень 2: Перемикання тем та кастомізація

Завдання 2.1: Створіть ThemeManager з підтримкою всіх 7 тем AtlantaFX. Реалізуйте перемикання теми через ComboBox. Додайте збереження вибраної теми через Preferences API.

Завдання 2.2: Створіть власний CSS-файл, що перевизначає акцентний колір AtlantaFX на фіолетовий (#9b59b6). Підключіть його після теми AtlantaFX.

Завдання 2.3: Додайте ToggleSwitch для перемикання між світлою та темною версіями поточної теми (Primer Light ↔ Primer Dark, Nord Light ↔ Nord Dark).

Рівень 3: Інтеграція з MVVM та додаткові контроли

Завдання 3.1: Інтегруйте ThemeManager з Guice як Singleton. Створіть SettingsViewModel з Properties для вибраної теми. Реалізуйте Controller з Bindings між ComboBox та ViewModel.

Завдання 3.2: Використайте Popover для відображення додаткової інформації про аудіокнигу при натисканні на кнопку "Info". Popover має містити опис, рейтинг, та кнопку "Read More".

Завдання 3.3: Створіть dashboard з Tile компонентами для відображення статистики (кількість аудіокниг, активних користувачів, нових додавань за місяць). Використайте різні кольори для різних метрик (Styles.ACCENT, Styles.SUCCESS, Styles.WARNING).


Підсумок

У цій статті ми розглянули AtlantaFX — сучасну бібліотеку тем для JavaFX. Ключові висновки:

AtlantaFX вирішує проблему застарілого вигляду JavaFX. Замість стандартної теми Modena отримуємо 7 професійних тем, натхнених сучасними дизайн-системами (GitHub Primer, Nord, Dracula, Cupertino). Додаток одразу виглядає сучасно без написання CSS.

Встановлення через Maven/Gradle. Одна залежність (io.github.mkpaz:atlantafx-base:2.1.0) та один рядок коду (Application.setUserAgentStylesheet(new PrimerLight().getUserAgentStylesheet())) — тема застосована до всього додатку.

7 готових тем для різних сценаріїв. Primer (універсальна, бізнесова), Nord (арктична, для розробників), Cupertino (Apple-подібна), Dracula (темна, яскрава). Кожна тема має світлу та темну версії (окрім Dracula).

Додаткові контроли розширюють можливості JavaFX. ToggleSwitch (iOS-стиль перемикач), PasswordTextField (з кнопкою показу пароля), Popover (спливаюче вікно), Message (повідомлення з іконкою), Notification (toast), Tile (інформаційна плитка).

CSS Variables для легкої кастомізації. Всі кольори визначені через змінні (--color-accent-emphasis, --color-bg-default). Перевизначте змінні у власному CSS — всі компоненти автоматично оновляться.

Utility classes для швидкої стилізації. Styles.ACCENT, Styles.SUCCESS, Styles.DANGER для кнопок. Styles.TEXT_BOLD, Styles.TITLE_2 для тексту. Styles.BG_SUBTLE для фонів. Професійний вигляд без написання CSS.

Перемикання тем у runtime. Application.setUserAgentStylesheet() дозволяє змінювати тему динамічно. ThemeManager з Preferences API зберігає вибрану тему між запусками.

Інтеграція з MVVM. ThemeManager як Singleton через Guice, SettingsViewModel з Properties для вибраної теми, Controller з Bindings між UI та ViewModel. AtlantaFX не конфліктує з MVVM-архітектурою.

AtlantaFX — це не заміна власного CSS, а потужна основа. Використовуйте AtlantaFX для стилізації стандартних компонентів та додавайте власні стилі для унікальних елементів. Це економить тижні роботи та робить JavaFX-додатки конкурентоспроможними з Electron та Flutter.

Copyright © 2026