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

Первая программа на JavaFX

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

Первая программа на JavaFX

Где применяют JavaFX

JavaFX (OpenJFX) — рекомендуемый стек для новых desktop-приложений на Java — CSS, FXML, анимации, TableView, диалоги. С Java 11 библиотека подключается через Maven или Gradle; в JDK "из коробки" её уже нет.

Мы соберём "Конвертер °C → °F" и разберём Application.launch() — без него окно не останется на экране, как mainloop() в Tkinter или SwingUtilities.invokeLater + видимое окно в Swing.

Теория: JavaFX и GUI. Элементы UI по рецептам: Справочник по JavaFX и Swing — элементы UI. Swing без Maven — Lab — окна и кнопки (построчный разбор для лабораторной) или кратко в Простые приложения на Java — Swing.


Что получится

Поле ввода, кнопки "Перевести" и "Выход", метка результата; Enter в поле вызывает расчёт.


Подготовка проекта (Maven)

Минимальный pom.xml для Java 17 и OpenJFX 21:

Код ITЗагрузка примера кода…

Запуск из каталога проекта:

mvn javafx:run

Подробнее о структуре каталогов — Структуры проекта.

"В pom.xml всё написано", а import javafx не находится

Чаще всего Maven не скачивает OpenJFX, потому что в <version>${javafx.version}</version> нет свойства javafx.version в блоке <properties> — в IDEA плейсхолдер подсвечивается красным. Без артефакта javafx-controls компилятор честно пишет package javafx.application does not exist. Это незаданная версия зависимости.

Чеклист pom.xml перед первым Run

ПроверкаДолжно быть
Свойство версии<javafx.version>21.0.7</javafx.version> (или другая стабильная 21.x)
Зависимостьorg.openjfx:javafx-controls с той же версией, без красного &#36;&#123;javafx.version&#125;
Версия Javamaven.compiler.source / target (или release) совпадают с Project SDK в IDEA — не 25, если установлен JDK 21/24
mainClass в javafx-maven-pluginРеальный класс с public static void main, например ru.example.Main, а не шаблон com.example.app.Main
После правокПравый клик по pom.xmlMaven → Reload Project

С JDK 11+ JavaFX нет в стандартном JDK (в Oracle JDK до 10 он был внутри). Фраза "Java установлена" не означает "JavaFX доступен" — нужны зависимости OpenJFX и корректный запуск.


Две точки входа — Main и класс Application

В учебных Maven-проектах удобно разделить роли:

  • HelloApp extends Application — только UI — start(Stage), виджеты, обработчики;
  • Main — одна строка запуска, какой экран открыть.
package ru.example;

import javafx.application.Application;

public class Main {
public static void main(String[] args) {
Application.launch(HelloApp.class, args);
// Application.launch(DemoApp.class, args);
}
}

Переключение между "приветствием" и большим демо — комментарием одной строки, без копирования launch в каждый класс. Альтернатива: public static void main прямо в HelloApp с вызовом launch(args) — тогда Run в IDEA можно повесить на этот класс.

В javafx-maven-plugin укажите тот класс, у которого есть main (часто это Main, а не HelloApp).


Запуск — Maven, IDEA и module-path

Через Maven (плагин сам подставляет module path):

Maven → Plugins → javafx → javafx:run

Через Run в IntelliJ IDEA после Java 11 недостаточно "просто classpath": без module path появляется:

Error: JavaFX runtime components are missing, and application needs to be terminated

В логе видно, что JAR JavaFX лежат на classpath, а runtime ждёт --module-path с платформенными JAR (javafx-controls-*-win.jar на Windows).

Минимальные VM options для конфигурации Application (после сборки проекта):

--module-path "$PROJECT_DIR$/target/javafx-lib"
--add-modules javafx.controls,javafx.graphics
--enable-native-access=javafx.graphics

Чтобы папка target/javafx-lib появилась при mvn compile, в pom.xml можно добавить копирование нативных JAR OpenJFX (классификатор win / linux / mac под вашу ОС):

Код ITЗагрузка примера кода…

Порядок в IDEA: Build Project (или mvn compile) → выбрать сохранённую конфигурацию Main / DemoApp с VM options → Run. Временная конфигурация "Run Main.java" без VM options снова даст runtime components are missing.

Предупреждения JDK 24 про sun.misc.Unsafe из JavaFX в консоли обычно не мешают работе окна — их можно игнорировать на этапе обучения.


Поэтапное расширение одного проекта

Логичный маршрут "от нуля к наглядной галерее" (как в учебном HelloApp):

ШагКлассЧто добавляете
1HelloAppTextField, Button, Label, VBox, setOnAction — одно окно 320×180
2DemoAppTabPane (формы, список, прогресс), BorderPane, Slider + привязка к label, Timeline, строка статуса — подробный разбор
3ButtonDemoStackPane, стили кнопки, Alert со стилизацией через getDialogPane()

На шаге 2 полезно увидеть привязку свойств вместо ручного setText в каждом обработчике:

volumeLabel.textProperty().bind(
Bindings.format("Громкость: %.0f%%", volumeSlider.valueProperty())
);

Стили можно подключить inline (setStyle), через CSS-файл в resources или строкой в scene.getStylesheets().add("data:text/css,...") — для демо без отдельного файла.


Минимальное окно

Код ITЗагрузка примера кода…

Разбор:

  • Application — точка входа JavaFX; платформа вызывает start(Stage).
  • Stage — окно; Scene — содержимое с корневым узлом и размером.
  • launch(args) запускает UI-поток и цикл событий; без show() окно не видно.

Конвертер температуры

Код ITЗагрузка примера кода…

Разбор:

  • convert() читает текст поля, парсит число и обновляет resultLabel.
  • NumberFormatException перехватывает нечисловой ввод; Alert показывает сообщение об ошибке.
  • GridPane выравнивает метки и поля в сетке; HBox группирует кнопки в ряд.
  • setOnAction и setOnKeyPressed с KeyCode.ENTER — два способа вызвать одну логику.

DemoApp — галерея контролов

DemoApp — второй учебный экран после конвертератри мини-сцены во вкладках, общая строка состояния и стили через CSS-классы. Так отрабатываются контейнеры, привязки свойств и анимация в одном окне — без FXML и без отдельного окна на каждый пример.

Переключение в Main:

Application.launch(DemoApp.class, args);
// Application.launch(HelloApp.class, args);

Исходник держите рядом с HelloApp (например, ru.timur.crm.DemoApp). Ниже — как добавляется каждый блок, а не копия всего файла целиком.

Связанные материалы


Зачем отдельный класс после конвертера

Конвертер учит одну форму и обработку ошибок через Alert. DemoApp показывает, как в реальном приложении собирают экран:

  • несколько зон (шапка, центр, статус) вместо одного GridPane;
  • вкладки вместо отдельных окон;
  • общий статус — один Label, на который ссылаются все вкладки;
  • привязки (bind) — UI обновляется сам, без ручного setText на каждый тик слайдера;
  • анимация (Timeline) — прогресс без Thread.sleep в обработчике кнопки.

Это шаг 2 в поэтапном расширении; шаг 3 — ButtonDemo со стилями и диалогами.


Каркас — BorderPane, заголовок, вкладки и статус

BorderPane — контейнер с пятью зонами (top, bottom, left, right, center). Подходит для классической раскладки "шапка — содержимое — статус", описанной в архитектуре десктопа.

TabPane — панель вкладок; каждая Tab хранит свой корневой Node (форму, список, прогресс).

Строка состояния (status bar) — обычно Label внизу окна; показывает подсказки и результат последнего действия. В DemoApp один statusLabel передаётся в formTab, listTab и progressTab, чтобы не дублировать виджет.

Код ITЗагрузка примера кода…

Разбор по строкам

  • statusLabel.getStyleClass().add("status-bar") — класс из CSS, не inline-стиль; оформление в константе STYLESHEET.
  • new TabPane(formTab(...), ...) — varargs: три вкладки создаются фабричными методами; каждый получает ту же ссылку на status.
  • TabClosingPolicy.UNAVAILABLE — у вкладок нет крестика закрытия; для учебного демо проще, чем восстанавливать закрытую вкладку.
  • VBox(4, ...) — вертикальный стек с промежутком 4 px между заголовком и подзаголовком.
  • BorderPane.setMargin(tabs, ...) — отступы только у центра; шапка и статус на всю ширину.
УзелРоль
statusLabelЕдиная обратная связь с пользователем; вкладки вызывают status.setText(...)
TabPaneТри экрана в одном Stage без new Stage()
VBox headerСтатичный заголовок вне вкладок
BorderPane.setMargin"Воздух" по бокам у блока вкладок

setMaxWidth(Double.MAX_VALUE) у статуса заставляет Label растянуться на всю ширину — иначе метка остаётся узкой и "плавает" слева. Подробнее про растягивание — ниже и в Справочник по JavaFX и Swing — элементы UI — StatusBar.


Стили и CSS-классы без отдельного файла

В JavaFX стили — CSS с префиксом -fx- (обзор в JavaFX и GUI). Три способа подключения:

  • inline — node.setStyle("...");
  • файл — scene.getStylesheets().add(url);
  • data-URL — CSS-строка прямо в коде (удобно для лабораторной без resources).

В DemoApp стили в text block и подключаются так:

private static final String STYLESHEET = """
.root { -fx-font-family: "Segoe UI", system-ui, sans-serif; … }
.card { -fx-background-color: white; -fx-border-radius: 10; … }
""";

scene.getStylesheets().add("data:text/css," + STYLESHEET.replace("\n", ""));
  • .root — корень сцены; шрифт и фон всего окна.
  • .card, .header-title, .status-bar — классы на узлах через getStyleClass().add(...).
  • replace("\n", "") — data-URL не любит переводы строк в одной строке запроса.

Вспомогательные методы убирают повторение:

private VBox card(Node content) {
VBox box = new VBox(content);
box.getStyleClass().add("card");
VBox.setVgrow(content, Priority.ALWAYS);
return box;
}

private Label styledLabel(String text, String styleClass) {
Label label = new Label(text);
label.getStyleClass().add(styleClass);
return label;
}
  • card() — обёртка "белая карточка с рамкой" вокруг содержимого вкладки.
  • VBox.setVgrow(content, Priority.ALWAYS) — внутренний блок забирает свободную высоту карточки (важно для ListView).

Для продакшена чаще кладут app.css в src/main/resources — см. стилизацию ниже.


Вкладка "Формы"

Метод formTab(Label status) — типичная экранная форма: подпись слева, контрол справа. Контейнер — GridPane (сетка строк и столбцов), как в конвертере, но с большим числом типов ввода.

TextField и PasswordField

TextField — однострочный ввод текста. PasswordField — тот же API, но символы маскируются (наследник TextField).

TextField nameField = new TextField();
nameField.setPromptText("Имя");
nameField.setMaxWidth(Double.MAX_VALUE);

PasswordField passwordField = new PasswordField();
passwordField.setPromptText("Пароль");
passwordField.setMaxWidth(Double.MAX_VALUE);
  • setPromptText — подсказка-серый placeholder, пока поле пустое; не то же самое, что значение по умолчанию.
  • setMaxWidth(Double.MAX_VALUE) + GridPane.setHgrow(..., Priority.ALWAYS) — поле тянется на ширину ячейки, а не по числу символов.

Чтение при отправке: nameField.getText(), passwordField.getText(); пустое имя в DemoApp заменяют на "(без имени)" через isBlank().

ComboBox

ComboBox — выпадающий список фиксированных вариантов (роль пользователя, категория, статус).

ComboBox<String> roleBox = new ComboBox<>();
roleBox.getItems().addAll("Гость", "Пользователь", "Администратор");
roleBox.setValue("Пользователь");
  • getItems() — изменяемый список (ObservableList); можно add / remove в runtime.
  • getValue() — выбранный элемент; для нередактируемого списка надёжнее, чем getText().
  • setValue до первого показа — начальное выделение без клика пользователя.

Рецепт — Справочник по JavaFX и Swing — элементы UI — ComboBox.

DatePicker

DatePicker — календарь для выбора даты; значение типа LocalDate (java.time), не Date.

DatePicker datePicker = new DatePicker(LocalDate.now());

В обработчике "Применить" дату превращают в строку для статуса:

String date = datePicker.getValue() != null
? datePicker.getValue().format(DateTimeFormatter.ofPattern("dd.MM.yyyy"))
: "—";

Slider и привязка свойств (Bindings)

Slider — ползунок в числовом диапазоне. Привязка (binding) — связь двух свойств JavaFX: при изменении источника цель обновляется сама, без явного setText в коде.

Slider volumeSlider = new Slider(0, 100, 40);
volumeSlider.setShowTickLabels(true);
volumeSlider.setShowTickMarks(true);

Label volumeLabel = new Label();
volumeLabel.textProperty().bind(
Bindings.format("Громкость: %.0f%%", volumeSlider.valueProperty())
);
  • new Slider(min, max, value) — диапазон 0–100, старт 40.
  • valueProperty()ObservableValue<Double>; источник для привязки.
  • Bindings.format(...) — как String.format, но реактивно: метка всегда синхронна со слайдером.

Это контраст с императивным стилем в конвертере, где resultLabel.setText(...) вызывают вручную в convert(). Оба подхода допустимы: привязки — для "зеркала" UI; обработчики — для бизнес-логики по кнопке.

CheckBox и RadioButton

  • CheckBox — независимый флаг (вкл/выкл уведомления).
  • RadioButton — один вариант из группы; без ToggleGroup можно выбрать несколько кнопок сразу — частая ошибка новичка.
CheckBox notifyCheck = new CheckBox("Уведомления");
notifyCheck.setSelected(true);

ToggleGroup themeGroup = new ToggleGroup();
RadioButton lightTheme = new RadioButton("Светлая");
RadioButton darkTheme = new RadioButton("Тёмная");
lightTheme.setToggleGroup(themeGroup);
darkTheme.setToggleGroup(themeGroup);
lightTheme.setSelected(true);
  • isSelected() у чекбокса — boolean для строки статуса ("вкл" / "выкл").
  • lightTheme.isSelected() — какая тема выбрана в группе из двух.

Кнопка "Применить" и сбор данных формы

Кнопка по умолчанию (setDefaultButton(true)) — нажатие Enter в форме активирует её, пока фокус в контроле на сцене.

Код ITЗагрузка примера кода…

Разбор обработчика

  • Один setOnAction собирает снимок всех контролов — паттерн "Прочитать форму → показать в статусе" (позже тот же сбор уйдёт в сохранение или API).
  • status.setText — запись в общую строку состояния, переданную параметром status.
  • Тема и уведомления — разные модели: флаг vs взаимоисключающий выбор.

Размещение в GridPane

GridPane grid = new GridPane(12, 10);
grid.add(new Label("Имя:"), 0, 0);
grid.add(nameField, 1, 0);
grid.add(volumeLabel, 0, 4, 2, 1);
grid.add(new HBox(12, lightTheme, darkTheme), 1, 7);
grid.add(applyButton, 1, 8);

GridPane.setHgrow(nameField, Priority.ALWAYS);
GridPane.setHgrow(passwordField, Priority.ALWAYS);
GridPane.setHgrow(roleBox, Priority.ALWAYS);

return new Tab("Формы", card(grid));
  • GridPane(12, 10)hgap 12 и vgap 10 между ячейками.
  • grid.add(node, col, row, colspan, rowspan)volumeLabel и слайдер на две колонки.
  • HBox(12, lightTheme, darkTheme) — горизонтальный ряд радиокнопок с зазором 12 px.
  • return new Tab("Формы", card(grid)) — вкладка = заголовок + содержимое в "карточке".

Вкладка "Список"

Здесь модель данных — список строк в памяти (ListView), без БД. Паттерн CRUD в миниатюре — добавить, удалить, выделить.

ListView показывает ObservableList; изменения списка сразу видны на экране.

ListView<String> listView = new ListView<>();
listView.getItems().addAll("Java", "Kotlin", "Scala", "Groovy");
listView.getSelectionModel().selectFirst();
  • getItems() — живая коллекция; add / remove / clear обновляют UI.
  • getSelectionModel() — какая строка выделена; нужна и для удаления, и для подсветки в статусе.

Добавление (с проверкой пустоты и дубликата):

addButton.setOnAction(e -> {
String text = itemField.getText().trim();
if (!text.isEmpty() && !listView.getItems().contains(text)) {
listView.getItems().add(text);
listView.getSelectionModel().select(text);
itemField.clear();
status.setText("Добавлено: " + text);
}
});

Удаление выбранного элемента:

removeButton.setOnAction(e -> {
String selected = listView.getSelectionModel().getSelectedItem();
if (selected != null) {
listView.getItems().remove(selected);
status.setText("Удалено: " + selected);
}
});

Слушатель выделения — реакция на клик по строке, без отдельной кнопки "Показать":

listView.getSelectionModel().selectedItemProperty().addListener((obs, oldVal, newVal) -> {
if (newVal != null) {
status.setText("Выбрано: " + newVal);
}
});
  • selectedItemProperty() — свойство JavaFX; addListener — подписка на смену выделения (обработка событий в JavaFX и GUI).
  • Проверка newVal != null — после удаления последнего элемента выделение может стать пустым.

Компоновка вкладки

  • VBox — список сверху, панель действий снизу.
  • VBox.setVgrow(listView, Priority.ALWAYS) — список растягивается по высоте.
  • HBox(8, itemField, addButton, removeButton) — поле и кнопки в один ряд.
  • HBox.setHgrow(itemField, Priority.ALWAYS) — поле "Новый элемент" на всю ширину ряда.

Рецепты — ListView, TabPane.


Вкладка "Прогресс"

Вкладка связывает индикатор прогресса, ручной слайдер и анимацию по таймеру. Главное правило: долгая работа не должна выполняться в setOnAction — иначе окно "зависнет" (Особенности разработки десктопных приложений). Timeline крутит кадры в JavaFX Application Thread, не блокируя очередь событий надолго.

ProgressBar хранит прогресс как дробь 0.0 … 1.0, не проценты:

ProgressBar progressBar = new ProgressBar(0);
progressBar.setMaxWidth(Double.MAX_VALUE);

Слайдер 0…1 синхронизирует полосу и подпись:

manualSlider.valueProperty().addListener((obs, oldVal, newVal) -> {
progressBar.setProgress(newVal.doubleValue());
percentLabel.setText(String.format("%.0f%%", newVal.doubleValue() * 100));
});
  • setProgress(0.42) — 42% на полосе.
  • Один слушатель обслуживает и ручное движение, и анимацию — не дублируйте логику в Timeline.

Имитация загрузки через Timeline

Код ITЗагрузка примера кода…

Разбор анимации

  • KeyFrame(Duration.ZERO, …) — мгновенный сброс слайдера в 0 перед стартом.
  • KeyFrame(Duration.seconds(2.5), new KeyValue(..., 1)) — за 2,5 с плавно довести valueProperty до 1.
  • setOnFinished — вернуть кнопки и слайдер после конца; без этого UI останется отключённым.
  • setDisable(true) на время — пользователь не сбивает анимацию вторым кликом.

Импорты: javafx.animation.Timeline, KeyFrame, KeyValue, javafx.util.Duration. Подробнее — Timeline в справочнике JavaFX и таймер в JavaFX и GUI.

Для реальной загрузки из сети или диска используют Task + Platform.runLater, а не Timeline — см. JavaFX и GUI — потоки и UI.


Сводка по вкладкам

ВкладкаКонтролы и приёмыЧему учит
"Формы"TextField, PasswordField, ComboBox, DatePicker, Slider, CheckBox, RadioButton, GridPane, BindingsВвод и выбор; форма "метка | поле"; привязка свойств
"Список"ListView, TextField, кнопки, selectedItemPropertyМодель в памяти; add/remove/select; слушатель свойства
"Прогресс"ProgressBar, Slider, Timeline, KeyFrame / KeyValueДиапазон 0–1; анимация без блокировки UI

Общее для всего DemoApp

  • BorderPane — каркас окна;
  • TabPane — несколько экранов в одном Stage;
  • один statusLabel — обратная связь;
  • CSS-классы и card() — единый визуальный стиль;
  • фабрики formTab / listTab / progressTab — разбиение start() на читаемые части (при росте проекта то же делают с FXML).
Куда идти после DemoApp

GridPane и VBox

  • GridPane — таблица строк и столбцов; удобен для форм "метка | поле".
  • VBox / HBox — вертикальный или горизонтальный стек элементов.

В одном Parent у каждого дочернего узла одна позиция; для сложных форм вкладывают панели друг в друга.

Позиционирование и компоновка — что путает новичка

JavaFX не расставляет виджеты "как в HTML по умолчанию" и не использует абсолютные x/y на сцене в обычных формах. Положение задаёт контейнер (VBox, HBox, GridPane, BorderPane, StackPane):

ОжиданиеКак в JavaFX
"Кнопка по центру окна"StackPane + кнопка, или VBox/HBox с setAlignment(Pos.CENTER)
"Поле на всю ширину"setMaxWidth(Double.MAX_VALUE) у поля и VBox.setVgrow(field, Priority.ALWAYS) при необходимости
"Меню сверху, таблица в центре, статус снизу"BorderPane: setTop / setCenter / setBottom
"Вкладки с разным содержимым"TabPane, каждая Tab со своим корнем (VBox, GridPane…)

Типичная ошибка — положить всё в VBox без отступов и выравнивания: элементы "прилипают" к левому краю. Помогают setPadding(new Insets(20)), setSpacing(10), setAlignment(Pos.CENTER).

Один узел нельзя одновременно быть в двух родителях. Для сложного экрана вкладывают панели — внешний BorderPane, внутри центра — VBox с формой, рядом — HBox с кнопками.


Стилизация — окно, кнопка и Alert

Inline (быстро для эксперимента):

button.setStyle("-fx-background-color: #e53935; -fx-text-fill: white; -fx-padding: 10 20;");
root.setStyle("-fx-padding: 20; -fx-background-color: #2d3436;");

CSS-классы на сцене (удобнее для нескольких виджетов):

button.getStyleClass().add("primary-button");
scene.getStylesheets().add(getClass().getResource("/app.css").toExternalForm());

Alert — не системный MessageBox Windows — внешний вид ограничен темой JavaFX, но шапку, текст и кнопки можно переопределить через DialogPane:

Alert alert = new Alert(Alert.AlertType.INFORMATION);
DialogPane pane = alert.getDialogPane();
pane.getStylesheets().add("data:text/css," + css.replace("\n", ""));
pane.getStyleClass().add("custom-alert");
alert.showAndWait();

Селекторы вроде .custom-alert .header-panel и .button-bar .button меняют фон диалога и кнопку OK; иконку типа (INFORMATION, ERROR) полностью "перекрасить" сложнее — для полного контроля используют Dialog<> с кастомным содержимым (JavaFX и GUI — CSS и диалоги).


Диалоги и меню

Выбор файла:


import javafx.stage.FileChooser;

FileChooser chooser = new FileChooser();
chooser.setTitle("Выберите файл");
chooser.getExtensionFilters().add(
new FileChooser.ExtensionFilter("Текст", "*.txt")
);
java.io.File file = chooser.showOpenDialog(stage);
if (file != null) {
// чтение file
}

Меню:

Код ITЗагрузка примера кода…


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

СимптомПричинаЧто сделать
package javafx.application does not existНет свойства javafx.version или Maven не перезагруженДобавить <javafx.version>…</javafx.version>, Reload Project
&#36;&#123;javafx.version&#125; красный в IDEAСвойство не объявленоСм. чеклист выше
invalid target release: 24 (или 25)В pom другая версия, чем установленный JDKВыровнять source/target/release с Project SDK
Окно "мигает" и закрываетсяНет launch() или show()Application.launch(...) и stage.show()
Module javafx.controls not foundOpenJFX не подтянулся / неверный module pathMaven Reload, javafx:run или VM options
IllegalStateException: Not on FX application threadUI меняют из фонового потокаPlatform.runLater(() -> …)
Интерфейс "зависает"Долгая работа в setOnActionTask / фоновый поток + runLater
JavaFX runtime components are missingЗапуск только через classpath без --module-pathVM options или mvn javafx:run
javafx:run не стартуетНеверный mainClass в плагинеУказать класс с public static void main
Код компилируется, Run в IDEA падаетВременная конфигурация без VM optionsСохранить конфигурацию с --module-path после compile

Подробнее про экосистему и контейнеры — JavaFX и GUI.


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

  1. Соберите DemoApp и переключите Main на Application.launch(DemoApp.class, args).
  2. Перепишите форму конвертера на VBox — метка, поле, результат, ряд кнопок.
  3. Добавьте CSS: scene.getStylesheets().add(".../app.css") и класс -fx-font-size.
  4. Сохраните результат в файл через FileChooser.showSaveDialog.
  5. Вынесите разметку в FXML — см. JavaFX и GUI.

Дальше

ЦельМатериал
Справочник APIСправочник по JavaFX и Swing — элементы UI
Swing в JDK без MavenПростые приложения на Java — Swing
Потоки и UIJVM, память и потоки
Теория десктопаАрхитектура десктопных приложений · Особенности разработки десктопных приложений · раздел "Десктопные приложения"

В подборках

Статья входит в тематические подборки и блок "С чего начать?" на главной. Соседние шаги того же маршрута:

Первые шаги (маршрут подборки) — Первая программа на Spring Framework, EF Core — первая программа, Первая программа на JavaBean, ADO.NET / Dapper — первая программа, Первая программа на JavaServer Faces, Первая программа на ASP.NET Core.