Стилізація та теми у JavaFX: CSS та User Experience
Стилізація та теми у JavaFX: CSS та User Experience
Вступ: Від функціональності до естетики
Ваш MVVM-додаток працює ідеально: ViewModel з Properties та Commands, Controller з Bindings, Guice для DI, валідація, навігація, тести. Але він виглядає як Windows 95. Сірі кнопки, стандартні шрифти, відсутність кольорової схеми. Користувачі оцінюють не лише функціональність, а й зовнішній вигляд.
Перше враження має значення. Дослідження показують, що користувачі формують думку про додаток за перші 50 мілісекунд. Якщо інтерфейс виглядає застарілим або непрофесійним, користувач може закрити додаток, навіть не спробувавши його функції.
Стилізація — це не лише краса. Це також usability: правильні кольори покращують читабельність, правильні відступи полегшують навігацію, правильні розміри кнопок зменшують помилки. Це brand identity: унікальний стиль робить додаток впізнаваним.
JavaFX підтримує стилізацію через CSS (Cascading Style Sheets) — той самий CSS, що використовується у веб-розробці, але адаптований для desktop. Це означає, що веб-розробники можуть легко перейти до JavaFX, а JavaFX-розробники можуть використовувати знання CSS.
Ця стаття про те, як створити професійно виглядаючий JavaFX-додаток. Ми розглянемо:
- JavaFX CSS: основи синтаксису, відмінності від веб-CSS.
- Селектори та псевдокласи (
:hover,:pressed,:focused). - Створення Light та Dark тем.
- CSS Variables для централізованого керування кольорами.
- Стилізацію складних компонентів (TableView, ListView).
- Підключення Font Awesome для іконок.
- Responsive design: адаптація під різні розміри вікна.
Але перш за все, потрібно зрозуміти фундаментальний принцип: CSS у JavaFX — це розділення структури (FXML) та стилізації (CSS). FXML описує, що відображається, CSS описує, як воно виглядає. Це дозволяє змінювати зовнішній вигляд без зміни коду.
-fx- для всіх властивостей, специфічні властивості для JavaFX-компонентів (-fx-background-radius, -fx-effect), відсутність деяких веб-властивостей (float, position: absolute).JavaFX CSS: Основи синтаксису
JavaFX CSS схожий на веб-CSS, але має свої особливості.
Структура CSS-правила
.button {
-fx-background-color: #3498db;
-fx-text-fill: white;
-fx-font-size: 14px;
-fx-padding: 10px 20px;
-fx-background-radius: 5px;
}
Селектор (.button) — вказує, до яких елементів застосовувати стилі.
Властивості (-fx-background-color, -fx-text-fill) — що змінювати.
Значення (#3498db, white) — на що змінювати.
Префікс -fx-
Всі JavaFX CSS властивості мають префікс -fx-:
/* JavaFX */
-fx-background-color: red;
-fx-font-size: 16px;
/* Web CSS (не працює у JavaFX) */
background-color: red;
font-size: 16px;
Це запобігає конфліктам з веб-CSS та явно вказує, що це JavaFX-специфічна властивість.
Основні властивості
Колір фону:
.button {
-fx-background-color: #3498db; /* Hex */
-fx-background-color: rgb(52, 152, 219); /* RGB */
-fx-background-color: rgba(52, 152, 219, 0.8); /* RGBA з прозорістю */
}
Колір тексту:
.label {
-fx-text-fill: white;
-fx-text-fill: #ecf0f1;
}
Шрифт:
.label {
-fx-font-family: "Segoe UI", Arial, sans-serif;
-fx-font-size: 14px;
-fx-font-weight: bold; /* normal, bold */
-fx-font-style: italic; /* normal, italic */
}
Відступи та поля:
.button {
-fx-padding: 10px 20px; /* top/bottom left/right */
-fx-padding: 10px 20px 15px 25px; /* top right bottom left */
}
.vbox {
-fx-spacing: 10px; /* Відстань між дочірніми елементами */
}
Рамка:
.text-field {
-fx-border-color: #3498db;
-fx-border-width: 2px;
-fx-border-radius: 5px;
}
Заокруглення кутів:
.button {
-fx-background-radius: 5px; /* Всі кути */
-fx-background-radius: 10px 10px 0 0; /* top-left top-right bottom-right bottom-left */
}
Підключення CSS до Scene
У коді:
Scene scene = new Scene(root, 800, 600);
scene.getStylesheets().add(getClass().getResource("/css/styles.css").toExternalForm());
У FXML:
<BorderPane xmlns:fx="http://javafx.com/fxml"
stylesheets="@../css/styles.css">
<!-- ... -->
</BorderPane>
@../css/styles.css — відносний шлях від FXML-файлу до CSS-файлу.
src/main/resources/css/ для всіх CSS-файлів. Використовуйте окремі файли для різних тем (light-theme.css, dark-theme.css) та компонентів (buttons.css, tables.css). Імпортуйте їх у головний styles.css через @import.Селектори та псевдокласи
Селектори визначають, до яких елементів застосовувати стилі.
Type Selector: За типом компонента
Button {
-fx-background-color: #3498db;
}
Label {
-fx-text-fill: #2c3e50;
}
TextField {
-fx-border-color: #bdc3c7;
}
Застосовується до всіх кнопок, Label, TextField у додатку.
Class Selector: За CSS-класом
.primary-button {
-fx-background-color: #3498db;
-fx-text-fill: white;
}
.danger-button {
-fx-background-color: #e74c3c;
-fx-text-fill: white;
}
.secondary-button {
-fx-background-color: #95a5a6;
-fx-text-fill: white;
}
Встановлення CSS-класу у FXML:
<Button text="Save" styleClass="primary-button"/>
<Button text="Delete" styleClass="danger-button"/>
<Button text="Cancel" styleClass="secondary-button"/>
Встановлення CSS-класу у коді:
button.getStyleClass().add("primary-button");
ID Selector: За унікальним ідентифікатором
#saveButton {
-fx-background-color: #27ae60;
}
#deleteButton {
-fx-background-color: #e74c3c;
}
Встановлення ID у FXML:
<Button fx:id="saveButton" id="saveButton" text="Save"/>
Різниця між fx:id та id:
fx:id— для доступу з Controller (Java-код).id— для доступу з CSS.
Зазвичай вони співпадають, але це не обов'язково.
Псевдокласи: Стани елемента
Псевдокласи дозволяють стилізувати елементи у різних станах.
.button:hover {
-fx-background-color: #2980b9;
}
.button:pressed {
-fx-background-color: #1f618d;
-fx-scale-x: 0.98;
-fx-scale-y: 0.98;
}
.text-field:focused {
-fx-border-color: #3498db;
-fx-border-width: 2px;
}
.button:disabled {
-fx-opacity: 0.5;
-fx-cursor: not-allowed;
}
.table-row-cell:selected {
-fx-background-color: #3498db;
-fx-text-fill: white;
}
Комбінування селекторів
Нащадок (descendant):
.vbox Label {
-fx-text-fill: #2c3e50;
}
Застосовується до всіх Label всередині VBox.
Дочірній елемент (child):
.vbox > Label {
-fx-text-fill: #2c3e50;
}
Застосовується лише до Label, що є безпосередніми дочірніми елементами VBox.
Кілька класів:
.button.primary-button {
-fx-background-color: #3498db;
}
Застосовується до елементів, що мають обидва класи: button та primary-button.
Теми: Light та Dark Mode
Створимо дві теми: світлу (Light) та темну (Dark).
Light Theme: light-theme.css
/* ===== Глобальні кольори ===== */
.root {
/* Базові кольори */
-fx-base: #ffffff;
-fx-background: #f5f5f5;
-fx-control-inner-background: #ffffff;
/* Акцентні кольори */
-fx-accent: #3498db;
-fx-default-button: #3498db;
-fx-focus-color: #3498db;
/* Текст */
-fx-text-base-color: #2c3e50;
-fx-text-fill: #2c3e50;
}
/* ===== Кнопки ===== */
.button {
-fx-background-color: #ecf0f1;
-fx-text-fill: #2c3e50;
-fx-font-size: 14px;
-fx-padding: 8px 16px;
-fx-background-radius: 4px;
-fx-cursor: hand;
}
.button:hover {
-fx-background-color: #bdc3c7;
}
.button:pressed {
-fx-background-color: #95a5a6;
}
.primary-button {
-fx-background-color: #3498db;
-fx-text-fill: white;
}
.primary-button:hover {
-fx-background-color: #2980b9;
}
.danger-button {
-fx-background-color: #e74c3c;
-fx-text-fill: white;
}
.danger-button:hover {
-fx-background-color: #c0392b;
}
/* ===== Текстові поля ===== */
.text-field {
-fx-background-color: white;
-fx-border-color: #bdc3c7;
-fx-border-width: 1px;
-fx-border-radius: 4px;
-fx-padding: 8px;
}
.text-field:focused {
-fx-border-color: #3498db;
-fx-border-width: 2px;
}
/* ===== Таблиці ===== */
.table-view {
-fx-background-color: white;
}
.table-row-cell {
-fx-background-color: white;
}
.table-row-cell:odd {
-fx-background-color: #f9f9f9;
}
.table-row-cell:selected {
-fx-background-color: #3498db;
-fx-text-fill: white;
}
.table-row-cell:hover {
-fx-background-color: #ecf0f1;
}
.column-header {
-fx-background-color: #ecf0f1;
-fx-text-fill: #2c3e50;
-fx-font-weight: bold;
}
Dark Theme: dark-theme.css
/* ===== Глобальні кольори ===== */
.root {
/* Базові кольори */
-fx-base: #2c3e50;
-fx-background: #1a252f;
-fx-control-inner-background: #34495e;
/* Акцентні кольори */
-fx-accent: #3498db;
-fx-default-button: #3498db;
-fx-focus-color: #3498db;
/* Текст */
-fx-text-base-color: #ecf0f1;
-fx-text-fill: #ecf0f1;
}
/* ===== Кнопки ===== */
.button {
-fx-background-color: #34495e;
-fx-text-fill: #ecf0f1;
-fx-font-size: 14px;
-fx-padding: 8px 16px;
-fx-background-radius: 4px;
-fx-cursor: hand;
}
.button:hover {
-fx-background-color: #4a5f7f;
}
.button:pressed {
-fx-background-color: #2c3e50;
}
.primary-button {
-fx-background-color: #3498db;
-fx-text-fill: white;
}
.primary-button:hover {
-fx-background-color: #2980b9;
}
.danger-button {
-fx-background-color: #e74c3c;
-fx-text-fill: white;
}
.danger-button:hover {
-fx-background-color: #c0392b;
}
/* ===== Текстові поля ===== */
.text-field {
-fx-background-color: #34495e;
-fx-text-fill: #ecf0f1;
-fx-border-color: #4a5f7f;
-fx-border-width: 1px;
-fx-border-radius: 4px;
-fx-padding: 8px;
}
.text-field:focused {
-fx-border-color: #3498db;
-fx-border-width: 2px;
}
/* ===== Таблиці ===== */
.table-view {
-fx-background-color: #2c3e50;
}
.table-row-cell {
-fx-background-color: #2c3e50;
-fx-text-fill: #ecf0f1;
}
.table-row-cell:odd {
-fx-background-color: #34495e;
}
.table-row-cell:selected {
-fx-background-color: #3498db;
-fx-text-fill: white;
}
.table-row-cell:hover {
-fx-background-color: #4a5f7f;
}
.column-header {
-fx-background-color: #34495e;
-fx-text-fill: #ecf0f1;
-fx-font-weight: bold;
}
Перемикання теми у runtime
public class ThemeManager {
private final Scene scene;
private Theme currentTheme = Theme.LIGHT;
public ThemeManager(Scene scene) {
this.scene = scene;
applyTheme(currentTheme);
}
public void switchTheme() {
currentTheme = (currentTheme == Theme.LIGHT) ? Theme.DARK : Theme.LIGHT;
applyTheme(currentTheme);
}
private void applyTheme(Theme theme) {
scene.getStylesheets().clear();
String themePath = getClass().getResource(theme.getCssPath()).toExternalForm();
scene.getStylesheets().add(themePath);
}
public enum Theme {
LIGHT("/css/light-theme.css"),
DARK("/css/dark-theme.css");
private final String cssPath;
Theme(String cssPath) {
this.cssPath = cssPath;
}
public String getCssPath() {
return cssPath;
}
}
}
Використання:
ThemeManager themeManager = new ThemeManager(scene);
// Кнопка для перемикання теми
Button themeToggle = new Button("Toggle Theme");
themeToggle.setOnAction(e -> themeManager.switchTheme());
Preferences API для збереження вибраної теми між запусками додатку:Preferences prefs = Preferences.userNodeForPackage(AudiobookApp.class);
prefs.put("theme", currentTheme.name());
// Завантаження при старті
String savedTheme = prefs.get("theme", Theme.LIGHT.name());
currentTheme = Theme.valueOf(savedTheme);
CSS Variables: Централізоване керування кольорами
CSS Variables (також відомі як Custom Properties) дозволяють визначити колір один раз та використовувати його у багатьох місцях.
Визначення змінних
.root {
/* Основні кольори */
-primary-color: #3498db;
-secondary-color: #2ecc71;
-danger-color: #e74c3c;
-warning-color: #f39c12;
-info-color: #9b59b6;
/* Фонові кольори */
-background-color: #f5f5f5;
-surface-color: #ffffff;
/* Текстові кольори */
-text-primary: #2c3e50;
-text-secondary: #7f8c8d;
-text-disabled: #bdc3c7;
/* Відступи */
-spacing-xs: 4px;
-spacing-sm: 8px;
-spacing-md: 16px;
-spacing-lg: 24px;
-spacing-xl: 32px;
}
Використання змінних
.primary-button {
-fx-background-color: -primary-color;
-fx-padding: -spacing-sm -spacing-md;
}
.danger-button {
-fx-background-color: -danger-color;
-fx-padding: -spacing-sm -spacing-md;
}
.label {
-fx-text-fill: -text-primary;
}
.label-secondary {
-fx-text-fill: -text-secondary;
}
Переваги CSS Variables:
- Централізація: Змінити колір у всьому додатку — змінити одну змінну.
- Консистентність: Всі кнопки використовують один колір, всі відступи однакові.
- Легкість створення варіацій: Створити нову тему — змінити значення змінних.
Приклад: Варіації теми через змінні
/* Light Theme */
.root {
-primary-color: #3498db;
-background-color: #f5f5f5;
-text-primary: #2c3e50;
}
/* Dark Theme */
.root.dark-theme {
-primary-color: #3498db;
-background-color: #1a252f;
-text-primary: #ecf0f1;
}
Перемикання теми через додавання CSS-класу:
if (isDarkTheme) {
scene.getRoot().getStyleClass().add("dark-theme");
} else {
scene.getRoot().getStyleClass().remove("dark-theme");
}
Стилізація TableView: Детальний контроль
TableView — один з найскладніших компонентів для стилізації. Він складається з багатьох частин: заголовки колонок, рядки, комірки, скролбари.
Анатомія TableView
TableView
├── column-header-background (контейнер заголовків)
│ └── column-header (окремий заголовок колонки)
│ └── label (текст заголовка)
├── table-row-cell (рядок таблиці)
│ └── table-cell (комірка)
│ └── text (текст комірки)
└── virtual-flow (контейнер для скролу)
└── scroll-bar (вертикальний скролбар)
Стилізація заголовків
.table-view .column-header-background {
-fx-background-color: #ecf0f1;
}
.table-view .column-header {
-fx-background-color: #ecf0f1;
-fx-text-fill: #2c3e50;
-fx-font-weight: bold;
-fx-font-size: 13px;
-fx-padding: 10px;
-fx-border-color: #bdc3c7;
-fx-border-width: 0 0 2px 0;
}
.table-view .column-header:hover {
-fx-background-color: #d5dbdb;
}
Стилізація рядків: Зебра-ефект
.table-view .table-row-cell {
-fx-background-color: white;
-fx-padding: 5px;
}
.table-view .table-row-cell:odd {
-fx-background-color: #f9f9f9;
}
.table-view .table-row-cell:selected {
-fx-background-color: #3498db;
-fx-text-fill: white;
}
.table-view .table-row-cell:hover {
-fx-background-color: #ecf0f1;
}
.table-view .table-row-cell:selected:hover {
-fx-background-color: #2980b9;
}
Стилізація комірок
.table-view .table-cell {
-fx-padding: 8px;
-fx-alignment: CENTER_LEFT;
}
/* Вирівнювання для числових колонок */
.table-view .table-cell.numeric-column {
-fx-alignment: CENTER_RIGHT;
}
/* Кольорове кодування для статусів */
.table-view .table-cell.status-active {
-fx-text-fill: #27ae60;
-fx-font-weight: bold;
}
.table-view .table-cell.status-inactive {
-fx-text-fill: #e74c3c;
}
Встановлення CSS-класу для комірки у коді:
durationColumn.setCellFactory(column -> new TableCell<AudiobookViewModel, Integer>() {
@Override
protected void updateItem(Integer duration, boolean empty) {
super.updateItem(duration, empty);
if (empty || duration == null) {
setText(null);
getStyleClass().remove("numeric-column");
} else {
setText(formatDuration(duration));
getStyleClass().add("numeric-column");
}
}
});
Стилізація скролбарів
.table-view .scroll-bar:vertical {
-fx-background-color: transparent;
}
.table-view .scroll-bar:vertical .thumb {
-fx-background-color: #bdc3c7;
-fx-background-radius: 5px;
}
.table-view .scroll-bar:vertical .thumb:hover {
-fx-background-color: #95a5a6;
}
.table-view .scroll-bar:vertical .track {
-fx-background-color: #ecf0f1;
}
Практичні завдання
Рівень 1: Базова стилізація
Завдання 1.1: Створіть CSS-файл з стилізацією кнопок: primary (синя), secondary (сіра), danger (червона). Кожна кнопка має мати hover-ефект (темніший колір) та pressed-ефект (ще темніший + зменшення розміру).
Завдання 1.2: Створіть стилі для TextField: білий фон, сіра рамка, при фокусі — синя рамка товщиною 2px. Додайте стиль для помилки: червона рамка.
Завдання 1.3: Створіть стилі для Label: основний текст (#2c3e50), вторинний текст (#7f8c8d), текст помилки (червоний). Використайте CSS-класи .label-primary, .label-secondary, .label-error.
Рівень 2: Теми та змінні
Завдання 2.1: Створіть Light та Dark теми для вашого додатку. Визначте CSS Variables для основних кольорів (primary, background, text). Реалізуйте перемикання теми через кнопку.
Завдання 2.2: Додайте збереження вибраної теми через Preferences API. При запуску додатку завантажуйте збережену тему.
Завдання 2.3: Створіть третю тему "High Contrast" для користувачів з порушеннями зору: високий контраст між текстом та фоном, великі шрифти, товсті рамки.
Рівень 3: Складні компоненти
Завдання 3.1: Стилізуйте TableView: зебра-ефект для рядків, hover-ефект, стилізовані заголовки колонок, кастомні скролбари. Додайте кольорове кодування для статусів (зелений для "Active", червоний для "Inactive").
Завдання 3.2: Створіть кастомний стиль для діалогів: заокруглені кути, тінь, кольорові кнопки (OK — зелена, Cancel — сіра). Використайте CSS для стилізації .dialog-pane.
Завдання 3.3: Реалізуйте responsive design: при ширині вікна менше 800px змініть розмір шрифтів (зменшити на 2px), відступи (зменшити на 25%), приховайте деякі колонки TableView. Використайте Bindings у коді для динамічної зміни стилів.
Підсумок
У цій статті ми розглянули стилізацію та теми у JavaFX. Ключові висновки:
JavaFX CSS — це адаптація веб-CSS для desktop. Схожий синтаксис (селектори, властивості, значення), але з префіксом -fx- для всіх властивостей. Підтримує більшість веб-CSS можливостей: селектори, псевдокласи, каскадність.
Селектори визначають, до яких елементів застосовувати стилі. Type Selector (за типом компонента), Class Selector (за CSS-класом), ID Selector (за унікальним ідентифікатором). Псевдокласи для різних станів: :hover, :pressed, :focused, :disabled, :selected.
Light та Dark теми через окремі CSS-файли. Визначаємо глобальні кольори у .root, стилізуємо всі компоненти. Перемикання теми через scene.getStylesheets().clear() та додавання нового CSS-файлу. Збереження вибраної теми через Preferences API.
CSS Variables для централізованого керування. Визначаємо змінні у .root (-primary-color, -spacing-md), використовуємо у стилях. Зміна однієї змінної оновлює всі місця використання. Легко створювати варіації тем.
TableView потребує детальної стилізації. Складається з багатьох частин: заголовки (.column-header), рядки (.table-row-cell), комірки (.table-cell), скролбари (.scroll-bar). Зебра-ефект через :odd, hover-ефект через :hover, вибір через :selected.
Розділення структури та стилізації. FXML описує структуру (що відображається), CSS описує стилізацію (як воно виглядає). Це дозволяє змінювати зовнішній вигляд без зміни коду, перевикористовувати стилі, створювати теми.
Стилізація покращує UX. Правильні кольори покращують читабельність, правильні відступи полегшують навігацію, правильні розміри зменшують помилки. Перше враження має значення — професійний вигляд додатку впливає на сприйняття користувачів.
Це завершує наш цикл статей про JavaFX MVVM + Guice. Ми пройшли шлях від основ JavaFX (Properties, Bindings) через архітектурні патерни (MVC, MVP, MVVM) до повноцінної інтеграції з Dependency Injection, валідації, навігації, тестування та стилізації. Тепер у вас є всі знання для побудови професійних, масштабованих, красивих JavaFX-додатків.
Тестування JavaFX MVVM-додатків
Від unit-тестів ViewModel до UI-автоматизації: тестування з Mockito, інтеграційні тести з H2 database, TestFX для автоматизованого тестування JavaFX UI, Test Doubles (Mock vs Stub vs Fake), організація тестів для різних шарів.
AtlantaFX: Сучасні теми для JavaFX додатків
Від стандартного Modena до сучасного дизайну: AtlantaFX як колекція готових тем (Primer, Nord, Cupertino, Dracula), додаткові контроли, CSS Variables, utility classes, інтеграція з MVVM-додатками.