AtlantaFX: Сучасні теми для JavaFX додатків
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 доступний через 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.
Стилізація та теми у JavaFX: CSS та User Experience
Від функціональності до естетики: JavaFX CSS (синтаксис, селектори, псевдокласи), створення Light та Dark тем, CSS Variables, стилізація TableView, підключення Font Awesome, responsive design.
Пакування та розповсюдження JavaFX-додатків
Від вихідного коду до готового інсталятора: створення Fat JAR, використання jpackage для генерації нативних інсталяторів під Windows, macOS та Linux, публікація релізів у GitHub Releases.