Перейти к основному содержимому

JavaFX и GUI

Разработчику Архитектору

JavaFX и GUI

Desktop GUI в Java — JavaFX, Swing и альтернативы

После консольных программ (первая программа, основные конструкции) часто нужен графический интерфейс — окно, кнопки, формы, диалоги. В Java для этого есть несколько поколений библиотек: Swing в составе JDK, JavaFX (OpenJFX) как рекомендуемый современный стек, плюс специализированные фреймворки.

Эта статья — обзор экосистемы и примеры виджетов; пошаговый старт — 3111.md, рецепты по каждому элементу UI3112.md. Swing без Maven — практические примеры. Общая картина платформы — Экосистема Java-приложений. Теория десктопа — Архитектура десктопных приложений.

Если вы только переходите из консольных программ в GUI, полезно читать материал слоями: сначала понять архитектуру и цикл событий, затем освоить базовые контролы, после этого перейти к компоновке и обработке событий. Такой порядок даёт прочный фундамент и снимает ощущение «случайного набора API».


Быстрый маршрут по статье

  1. Пройти первую программу на JavaFX или минимальный пример с кнопкой ниже.
  2. Разобраться, зачем нужен Application.launch и поток JavaFX.
  3. Освоить контейнеры компоновки (VBox, HBox, GridPane, BorderPane).
  4. Добавить обработчики через setOnAction и свойства с привязкой.
  5. Сравнить тот же сценарий на Swing (EDT, ActionListener).
  6. Закрепить на мини-проектах в конце статьи.

Связанные главы: Практические примеры — Swing, Экосистема Java-приложений, JVM, память и потоки, Maven и структура проекта, Особенности десктопа.


GUI в Java

Если задача требует полноценного приложения с кнопками, полями ввода, меню и другими элементами управления, используют специализированные библиотеки для построения GUI.

Графический пользовательский интерфейс (Graphical User Interface) — система, через которую пользователь взаимодействует с программой через визуальные компоненты: окна, кнопки, списки, диаграммы. В Java доступны несколько основных направлений:

  • Swing (javax.swing) — библиотека в составе JDK; рисует «лёгкие» компоненты на Java, без нативных peer-объектов AWT.
  • JavaFX (javafx.*, проект OpenJFX) — современный стек с CSS, FXML, анимациями и аппаратным ускорением; с Java 11 подключается отдельной зависимостью.
  • AWT (java.awt) — первая графическая библиотека Java на нативных виджетах ОС; сегодня служит основой для Swing и системных диалогов.
  • SWT (Eclipse) — нативные контролы Windows/macOS/Linux; часто встречается в Eclipse IDE.
  • Compose Multiplatform (JetBrains) — декларативный UI на Kotlin; для чистого Java-стека реже, но полезен при миграции на Kotlin.
  • Vaadin — веб-интерфейс на Java без HTML/JS в коде приложения; сервер рендерит UI в браузер.

Любое GUI-приложение следует событийно-ориентированной архитектуре:

  • создаётся главное окно;
  • добавляются виджеты (элементы управления);
  • регистрируются обработчики событий (клик, ввод текста, изменение размера);
  • запускается главный цикл UI, который принимает сообщения от ОС и распределяет их по обработчикам.

JavaFX и JDK
JavaFX входил в Oracle JDK до версии 10 включительно. Начиная с Java 11 модуль вынесен в проект OpenJFX и добавляется через Maven/Gradle. Swing по-прежнему в JDK и не требует отдельных зависимостей.


Сравнение основных стеков

SwingJavaFXSWT
В составе JDKДаНет (OpenJFX)Нет
СтилизацияLook-and-Feel, UIManagerCSS, темыНативный вид ОС
Разметка UIКод + LayoutManagerКод, FXMLКод
АнимацииОграниченноTimeline, TransitionМинимально
Типичный стартУчебные проекты, legacyНовые desktop-приложенияEclipse, RCP

Для новых desktop-проектов на Java чаще выбирают JavaFX. Swing остаётся удобным первым шагом: нулевая настройка зависимостей, миллионы строк legacy-кода в enterprise, простые утилиты.


Архитектура JavaFX

JavaFX строит интерфейс как дерево узлов (Node) внутри сцены (Scene), отображаемой в окне (Stage). Это ближе к современным UI-фреймворкам (Qt, WPF), чем к раннему AWT.

КомпонентНазначение
StageГлавное (или дочернее) окно приложения
SceneКонтейнер сцены с корневым узлом и размерами
NodeЛюбой визуальный элемент — Button, Label, TextField, контейнер
ParentУзел, который может содержать дочерние (VBox, GridPane)
ApplicationТочка входа; инициализирует платформу и вызывает start(Stage)
JavaFX Application ThreadЕдинственный поток, где можно менять UI

Пример на JavaFX:


import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class HelloFx extends Application {

@Override
public void start(Stage stage) {
Label prompt = new Label("Введите имя:");
TextField nameField = new TextField();
Label greeting = new Label();
Button greetButton = new Button("Приветствовать");

greetButton.setOnAction(e -> {
String name = nameField.getText().trim();
greeting.setText(name.isEmpty() ? "Введите имя" : "Привет, " + name + "!");
});

VBox root = new VBox(10, prompt, nameField, greetButton, greeting);
root.setStyle("-fx-padding: 20;");

Scene scene = new Scene(root, 360, 200);
stage.setTitle("Пример GUI");
stage.setScene(scene);
stage.show();
}

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

Разбор:

  • Application.launch(args) поднимает JavaFX runtime и вызывает start.
  • VBox — вертикальный контейнер; 10 — расстояние между дочерними узлами.
  • setOnAction связывает кнопку с обработчиком клика (лямбда).
  • Scene задаёт корень и размер окна; stage.show() отображает окно.

launch блокирует main до закрытия последнего окна — аналог mainloop() в Tkinter или app.exec() в Qt.


Иерархия Stage → Scene → Node

Stage (окно)
└── Scene (400×300)
└── Parent (корень, например BorderPane)
├── top: ToolBar
├── center: TableView
└── bottom: HBox с кнопками

Каждый Node имеет свойства (положение, видимость, стиль), которые можно менять из кода или привязать к другим свойствам. Изменения в UI выполняются только в JavaFX Application Thread — прямой вызов из фонового потока приведёт к IllegalStateException.


Пример простого приложения с кнопкой

Для кнопки с обработчиком используют Button и setOnAction. В отличие от Swing, здесь передаётся лямбда или метод, а не экземпляр ActionListener:


import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class ButtonDemo extends Application {

private void onButtonClick() {
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle("Событие");
alert.setHeaderText(null);
alert.setContentText("Кнопка была нажата!");
alert.showAndWait();
}

@Override
public void start(Stage stage) {
Button button = new Button("Нажми меня");
button.setOnAction(e -> onButtonClick());
button.setMaxWidth(Double.MAX_VALUE);
button.setStyle("-fx-font-size: 14px; -fx-padding: 10 20;");

StackPane root = new StackPane(button);
root.setStyle("-fx-padding: 20;");

stage.setTitle("Пример кнопки JavaFX");
stage.setScene(new Scene(root, 320, 180));
stage.show();
}

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

Ключевые аспекты:

  1. setOnAction — обработчик срабатывает при активации кнопки (клик или Enter при фокусе).
  2. Alert — стандартный диалог; showAndWait() блокирует до закрытия (модальный режим).
  3. launch — без него окно не появится; код после launch в main выполнится только после завершения приложения.
  4. Стили — строка CSS с префиксом -fx- (шрифт, отступы, цвета).

Инициализация приложения

Минимальный каркас:


import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.stage.Stage;

public class MinimalApp extends Application {

@Override
public void start(Stage stage) {
stage.setTitle("Приложение");
stage.setScene(new Scene(new Label("Hello"), 400, 300));
stage.show();
}

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

Класс наследует Application и переопределяет start. Альтернатива без наследования — Application.launch() с анонимным подклассом, но для учебных проектов достаточно явного класса.


Подключение JavaFX через Maven

Фрагмент pom.xml для Java 17+ и OpenJFX 21:

<properties>
<javafx.version>21.0.2</javafx.version>
</properties>

<dependencies>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>0.0.8</version>
<configuration>
<mainClass>com.example.HelloFx</mainClass>
</configuration>
</plugin>
</plugins>
</build>

Запуск:

mvn javafx:run

Подробнее о структуре Maven-проекта — Структуры проекта.


Контролы JavaFX

Control — интерактивный узел: кнопка, поле ввода, чекбокс. Label — статический текст или изображение.

Объект Label создаётся так:

Label label = new Label("Приветствие");
label.setStyle("-fx-text-fill: blue; -fx-background-color: white; -fx-padding: 8;");

Таблица частых свойств Label и аналогов:

Свойство / методОписаниеПример
setText(String)Текст метки"Информация"
textProperty()Свойство для привязкиlabel.textProperty().bind(...)
setFont(Font)ШрифтFont.font("Arial", FontWeight.BOLD, 14)
setWrapText(true)Перенос длинных строкдля узких панелей
setGraphic(Node)Иконка рядом с текстомImageView
setAlignment(Pos)Выравнивание внутри LabelPos.CENTER_LEFT
setMaxWidth(Double.MAX_VALUE)Растягивание по ширине контейнерав VBox с setFillWidth(true)

Методы, общие для всех Node:

МетодОписание
setVisible(boolean)Показать или скрыть
setDisable(boolean)Отключить ввод, «серый» вид
setOnMouseClicked(...)Обработчик клика мыши
getStyleClass().add("...")CSS-класс из таблицы стилей

Пример оформленных меток:


import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.stage.Stage;

public class LabelDemo extends Application {

@Override
public void start(Stage stage) {
Label title = new Label("Заголовок окна");
title.setFont(Font.font("Arial", FontWeight.BOLD, 18));
title.setStyle("-fx-text-fill: white; -fx-background-color: black; -fx-padding: 12;");

Label info = new Label("Это информационная метка.\nТекст может быть многострочным.");
info.setWrapText(true);
info.setMaxWidth(280);
info.setAlignment(Pos.CENTER_LEFT);

VBox root = new VBox(16, title, info);
root.setStyle("-fx-padding: 20;");

stage.setScene(new Scene(root, 360, 220));
stage.setTitle("Label");
stage.show();
}

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

Динамическое обновление текста и привязки

Прямой вызов setText работает, но для связи нескольких виджетов удобнее properties и bindings:


import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class BindingDemo extends Application {

@Override
public void start(Stage stage) {
TextField field = new TextField();
field.setPromptText("Введите текст");

Label mirror = new Label();
mirror.textProperty().bind(
Bindings.concat("Вы ввели: ", field.textProperty())
);

Button clear = new Button("Очистить");
clear.setOnAction(e -> field.clear());

stage.setScene(new Scene(new VBox(10, field, mirror, clear), 360, 160));
stage.show();
}

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

textProperty().bind(...) автоматически обновляет метку при каждом изменении поля — без ручного setText в обработчике.


Контейнеры компоновки

Размещение узлов на сцене задают layout-контейнеры (Pane и наследники). В одном родителе используют один тип менеджера; смешивать произвольно нельзя (дочерний узел принадлежит одному Parent).

VBox — вертикальный поток:

VBox box = new VBox(8); // 8 px между детьми
box.getChildren().addAll(label, field, button);
box.setAlignment(Pos.CENTER);
VBox.setVgrow(table, Priority.ALWAYS); // таблица забирает свободное место

HBox — горизонтальный поток; параметры spacing, alignment, HBox.setHgrow.

GridPane — сетка строк и столбцов:

МетодОписание
add(node, column, row)Ячейка сетки
add(node, col, row, colspan, rowspan)Объединение ячеек
setHgap / setVgapРасстояние между ячейками
GridPane.setConstraints(node, ...)Прилипание к краям (GridPane.setHgrow)
GridPane grid = new GridPane();
grid.setHgap(10);
grid.setVgap(8);
grid.add(new Label("Имя:"), 0, 0);
grid.add(nameField, 1, 0);
grid.add(new Label("Пароль:"), 0, 1);
grid.add(passwordField, 1, 1);

BorderPane — зоны top, bottom, left, right, center (классическая форма с меню и центральной областью):

BorderPane root = new BorderPane();
root.setTop(toolBar);
root.setCenter(tableView);
root.setBottom(statusBar);

StackPane — наложение слоёв (кнопка по центру, фон, оверлей загрузки).

AnchorPane, FlowPane, TilePane — для специальных случаев; в формах чаще GridPane или VBox/HBox.


Обработка событий

JavaFX следует event-driven модели. Платформа получает сообщения от ОС, ставит их в очередь и вызывает зарегистрированные обработчики в UI-потоке.

Основные способы подписки:

button.setOnAction(e -> { /* клик / Enter */ });

node.setOnMouseClicked(e -> {
double x = e.getX();
double y = e.getY();
});

field.setOnKeyPressed(e -> {
if (e.getCode() == KeyCode.ENTER) submit();
});

stage.widthProperty().addListener((obs, oldVal, newVal) -> resizeContent());

Для фильтрации и всплытия есть цепочка event filter (сверху вниз) и event handler (снизу вверх). Для большинства учебных задач достаточно setOnAction и setOnMouseClicked.

Controls (кнопки, поля) генерируют высокоуровневые события; для низкоуровневого ввода — MouseEvent, KeyEvent, ScrollEvent.


FXML и контроллер

Интерфейс можно описать в FXML (XML-разметка) и связать с Java-классом controller:

hello.fxml:

<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<VBox xmlns:fx="http://javafx.com/fxml" fx:controller="com.example.HelloController"
spacing="10" style="-fx-padding: 20;">
<Label text="Конвертер °C → °F"/>
<TextField fx:id="celsiusField" promptText="Температура °C"/>
<Button text="Перевести" onAction="#convert"/>
<Label fx:id="resultLabel"/>
</VBox>

HelloController.java:


package com.example;

import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;

public class HelloController {

@FXML
private TextField celsiusField;

@FXML
private Label resultLabel;

@FXML
private void convert() {
try {
double c = Double.parseDouble(celsiusField.getText().trim().replace(',', '.'));
double f = c * 9 / 5 + 32;
resultLabel.setText(String.format("%.1f °C = %.1f °F", c, f));
} catch (NumberFormatException ex) {
resultLabel.setText("Введите число");
}
}
}

Загрузка в Application.start:


FXMLLoader loader = new FXMLLoader(getClass().getResource("/hello.fxml"));
Parent root = loader.load();
stage.setScene(new Scene(root, 360, 200));

Такой подход отделяет разметку от логики — аналог .ui в Qt Designer или отдельного ui.py в Tkinter-проектах.


CSS-стилизация

JavaFX понимает CSS с префиксом -fx-. Стили задают inline (setStyle), через файл styles.css или классы:

scene.getStylesheets().add(getClass().getResource("/app.css").toExternalForm());
.button-primary {
-fx-background-color: #2563eb;
-fx-text-fill: white;
-fx-font-weight: bold;
}
.error-label {
-fx-text-fill: #dc2626;
}

Тема Modena включена по умолчанию; для кастомного вида переопределяют селекторы .root, .button, .text-field.


Swing — встроенный GUI JDK

Swing (javax.swing) рисует компоненты на Java поверх AWT. Архитектура окна:

JFrame (главное окно)
└── contentPane
├── JLabel, JButton, JTextField
└── JPanel + LayoutManager (FlowLayout, BorderLayout, GridLayout, BoxLayout)

Графический интерфейс Swing работает в Event Dispatch Thread (EDT). Создание и изменение компонентов выполняют через SwingUtilities.invokeLater():


import javax.swing.*;

public class SimpleWindow {
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
JFrame frame = new JFrame("Моё первое окно");
frame.setSize(400, 300);
frame.setLocationRelativeTo(null);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new JLabel("Привет, Swing!", SwingConstants.CENTER));
frame.setVisible(true);
});
}
}

Обработка клика — ActionListener (или лямбда):


JButton button = new JButton("Нажми меня");
button.addActionListener(e -> JOptionPane.showMessageDialog(frame, "Клик!"));

Полные примеры счётчика и формы входа — в Практические примеры — Swing.


Геометрические менеджеры Swing

МенеджерПоведениеКогда использовать
FlowLayoutЭлементы в ряд, перенос строкПростые панели кнопок
BorderLayoutNORTH, SOUTH, EAST, WEST, CENTERКлассическое окно с toolbar и центром
GridLayoutРавномерная сеткаФормы с одинаковыми ячейками
GridBagLayoutГибкая сетка с весамиСложные формы
BoxLayoutВертикальный или горизонтальный стекСписки полей

Важно: внутри одного контейнера нельзя смешивать менеджеры — у каждого JPanel один LayoutManager.

Пример BorderLayout:

JPanel panel = new JPanel(new BorderLayout(8, 8));
panel.add(toolbar, BorderLayout.NORTH);
panel.add(scrollPane, BorderLayout.CENTER);
panel.add(statusLabel, BorderLayout.SOUTH);

Swing и потоки

Swing, как и JavaFX, не потокобезопасен. Обновление UI из фонового потока:

  • SwingUtilities.invokeLater(() -> label.setText("Готово"));
  • SwingWorker — для длительных задач с прогрессом и публикацией результата в EDT.

В JavaFX аналог — Platform.runLater(...) и класс Task / Service.

Пример безопасного обновления из фонового потока (JavaFX):


new Thread(() -> {
String result = loadFromNetwork(); // долго
Platform.runLater(() -> statusLabel.setText(result));
}).start();

Для сетевых и файловых операций в GUI сверяйтесь с работой с БД и общими принципами многопоточности.


AWT — кратко

AWT (java.awt) использует peer-объекты — нативные виджеты Windows, macOS или X11. Внешний вид зависит от ОС; кастомизация ограничена. Сегодня AWT применяют там, где Swing/JavaFX опираются на низкий уровень: системный трей, некоторые диалоги, работа с Graphics, Robot, буфер обмена. Новые экранные формы на чистом AWT писать редко — разумнее Swing или JavaFX.


Организация кода в реальном приложении

В учебных примерах всё в одном классе; в утилитах удобнее разделить ответственность:

  • MainApp / Application — запуск, конфигурация Stage;
  • view — FXML или фабрики панелей;
  • controller — обработчики событий, связь view и model;
  • service — логика без UI (файлы, HTTP, БД);
  • model — данные и правила предметной области.

Такой каркас упрощает тестирование сервисов без поднятия UI и повторное использование логики в CLI или REST.

// MainApp.java — только сборка
public class MainApp extends Application {
@Override
public void start(Stage stage) throws Exception {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/main.fxml"));
Parent root = loader.load();
stage.setScene(new Scene(root));
stage.show();
}
public static void main(String[] args) { launch(args); }
}

Частые анти-паттерны

  • Долгая операция прямо в setOnAction / ActionListener без Task, SwingWorker или фонового потока — UI «замирает».
  • Обновление виджетов из worker-потока без Platform.runLater / invokeLater.
  • Глобальные статические ссылки на все контролы вместо явной структуры приложения.
  • Смешивание нескольких layout-менеджеров на одном JPanel без вложенных панелей.
  • Отсутствие валидации ввода перед парсингом Double.parseDouble и SQL.
  • Игнорирование setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE) — процесс JVM остаётся висеть.

Примеры реализации

Таймер обратного отсчёта (JavaFX)

Timeline планирует периодические обновления в UI-потоке:


import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.util.Duration;

public class CountdownApp extends Application {

private int count = 10;

@Override
public void start(Stage stage) {
Label label = new Label("Осталось: " + count);
label.setStyle("-fx-font-size: 24px;");

Timeline timeline = new Timeline();
timeline.getKeyFrames().add(new KeyFrame(Duration.seconds(1), e -> {
count--;
label.setText(count > 0 ? "Осталось: " + count : "Время вышло!");
if (count <= 0) {
timeline.stop();
}
}));
timeline.setCycleCount(Timeline.INDEFINITE);
timeline.play();

stage.setScene(new Scene(new StackPane(label), 300, 120));
stage.show();
}

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

Форма входа с валидацией (JavaFX)


import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;

public class LoginFormFx extends Application {

@Override
public void start(Stage stage) {
GridPane grid = new GridPane();
grid.setHgap(10);
grid.setVgap(10);
grid.setPadding(new Insets(20));

TextField nameField = new TextField();
PasswordField passwordField = new PasswordField();
Label result = new Label(" ");
Button submit = new Button("Войти");

submit.setOnAction(e -> {
String name = nameField.getText().trim();
String password = passwordField.getText();
if (name.isEmpty()) {
new Alert(Alert.AlertType.ERROR, "Введите имя").showAndWait();
} else if (password.length() < 3) {
new Alert(Alert.AlertType.ERROR, "Пароль слишком короткий").showAndWait();
} else {
result.setText("Привет, " + name + "!");
nameField.clear();
passwordField.clear();
}
});

grid.add(new Label("Имя:"), 0, 0);
grid.add(nameField, 1, 0);
grid.add(new Label("Пароль:"), 0, 1);
grid.add(passwordField, 1, 1);
grid.add(submit, 1, 2);
grid.add(result, 1, 3);

stage.setScene(new Scene(grid, 400, 200));
stage.setTitle("Вход");
stage.show();
}

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

Таблица и меню (Swing)

Для data-heavy UI в legacy-стеке часто используют JTable + TableModel:


import javax.swing.*;
import javax.swing.table.DefaultTableModel;

public class TableDemo {
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
String[] columns = {"Имя", "Возраст"};
Object[][] data = {{"Алиса", 30}, {"Боб", 25}};
JTable table = new JTable(new DefaultTableModel(data, columns));

JFrame frame = new JFrame("Таблица");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new JScrollPane(table));
frame.setSize(400, 200);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}

В JavaFX аналог — TableView с ObservableList и колонками PropertyValueFactory.


Частые ошибки

СимптомПричина
Окно не появляетсяНет launch() / setVisible(true)
IllegalStateException: Not on FX application threadUI меняют из фонового потока без Platform.runLater
Module javafx.controls not foundOpenJFX не добавлен в module path / Maven
JVM не завершается после закрытия окна SwingНет EXIT_ON_CLOSE или остались не-daemon потоки
UI зависаетБлокирующий код в обработчике кнопки
FXML не находит controllerНеверный fx:controller или ресурс не в resources

Что попробовать

  1. Тот же сценарий «приветствие» на Swing и JavaFX — сравните EDT и JavaFX thread.
  2. Вынести разметку в FXML и подключить CSS-файл.
  3. Долгую задачу обернуть в Task с индикатором ProgressIndicator.
  4. Собрать JAR с javafx-maven-plugin и запустить на другой машине с JRE 17+.
  5. Общую теорию окон и mainloop — Архитектура десктопных приложений.

Практика и справочник


Сравнение с Python
Параллель в экосистеме Python — Tkinter и GUI: там mainloop и Tcl/Tk, здесь JavaFX Application Thread и OpenJFX. Оба материала опираются на одну и ту же теорию десктопа.

См. также

Другие статьи этого же раздела в боковом меню (как на странице "О разделе").