# JavaFX — полное введение (с параллелями на PyQt6) > У вас есть опыт PyQt6 — я буду постоянно проводить параллели, чтобы было проще. --- ## 1. Что такое JavaFX? **JavaFX** — это фреймворк для desktop-приложений на Java/Kotlin. Аналог Qt (PyQt6) для мира JVM. | PyQt6 | JavaFX | |---|---| | QApplication | Application | | QWidget | Node / Parent / Region | | QMainWindow | Stage + Scene | | QVBoxLayout / QHBoxLayout | VBox / HBox | | QPushButton | Button | | QTableWidget | TableView | | QGraphicsScene / QGraphicsView | Canvas / Group | | signals & slots | Event handlers + Properties + Bindings | | QTimer | AnimationTimer / Timeline | | Qt Designer (UI файлы) | Scene Builder (FXML файлы) | | QLocale / QTranslator | ResourceBundle / кастомный AppLocale | JavaFX пришёл на смену Swing (аналог PyQt5 → PyQt6). Он: - Использует GPU-ускоренный рендеринг (через Prism). - Имеет встроенную поддержку CSS-стилизации. - Поддерживает FXML (XML-разметка UI, аналог .ui файлов Qt Designer). - Работает через **Property** + **Binding** — встроенная реактивность. --- ## 2. Структура JavaFX-приложения ### 2.1. Точка входа **PyQt6:** ```python app = QApplication(sys.argv) window = QMainWindow() window.show() sys.exit(app.exec()) ``` **JavaFX:** ```kotlin class App : Application() { override fun start(stage: Stage) { stage.title = "Hello" stage.scene = Scene(Button("Click me"), 300.0, 200.0) stage.show() } } fun main() { Application.launch(App::class.java) } ``` `Application.launch()` запускает **JavaFX Application Thread** (FxAT) — аналог Qt Event Loop. **Весь UI-код обязан выполняться в этом потоке.** Если вы обновляете UI из другого потока — используйте `Platform.runLater { ... }` (аналог `QMetaObject.invokeMethod` с `Qt.QueuedConnection`). ### 2.2. Stage и Scene **Stage** — это окно. Аналог `QMainWindow` / `QDialog`. ```kotlin val stage = Stage() stage.title = "My App" stage.isResizable = true stage.minWidth = 400.0 stage.minHeight = 300.0 stage.show() ``` **Scene** — это содержимое окна. Аналог `centralWidget` / `setWidget()`. ```kotlin stage.scene = Scene(rootNode, 800.0, 600.0) ``` У одного Stage может быть только одна Scene (но можно менять динамически). ### 2.3. Node — всё, что можно нарисовать **Node** — базовый класс для любого визуального элемента. Аналог `QWidget`. ``` Node ├── Parent (может содержать дочерние Node) │ ├── Region │ │ ├── Control │ │ │ ├── Button, Label, TextField, ComboBox, TableView... │ │ │ └── ... │ │ └── Pane │ │ ├── VBox, HBox, BorderPane, GridPane, StackPane... │ │ └── FlowPane, AnchorPane, TilePane... │ └── WebView ├── Canvas (рисование через GraphicsContext) ├── Shape (Circle, Rectangle, Text...) └── ImageView ``` Параллель с PyQt6: - `QPushButton` → `Button` - `QLabel` → `Label` - `QLineEdit` → `TextField` - `QComboBox` → `ComboBox` - `QVBoxLayout` → `VBox` - `QHBoxLayout` → `HBox` - `QGridLayout` → `GridPane` - `QTableWidget` → `TableView` - `QGraphicsView` → `Canvas` --- ## 3. Layout-менеджеры (панели) В PyQt6 вы добавляете виджеты в layout: ```python layout = QVBoxLayout() layout.addWidget(button) layout.addWidget(label) widget.setLayout(layout) ``` В JavaFX layout'ы — это тоже Node (наследники Pane): ```kotlin val root = VBox(8.0) // spacing = 8 root.children.addAll(button, label) val hbox = HBox(10.0) // горизонтальный ряд, spacing = 10 val grid = GridPane() grid.add(label, 0, 0) // column=0, row=0 grid.add(field, 1, 0) // column=1, row=0 grid.addRow(1, label2, field2) // row=1 grid.hgap = 8.0 grid.vgap = 8.0 val border = BorderPane() border.top = toolbar // север border.center = splitPane // центр (растягивается) border.bottom = statusBar // юг ``` **BorderPane** — самый удобный layout для главного окна: top/center/bottom/left/right. Аналог `QMainWindow` с setMenuBar, setCentralWidget, addToolBar. --- ## 4. Properties и Binding — реактивность Это **ключевое отличие JavaFX от большинства UI-фреймворков**. В PyQt6 вы обновляете UI так: ```python button.setText("New text") label.setText(str(counter)) ``` В JavaFX можно сделать то же самое: ```kotlin button.text = "New text" label.text = counter.toString() ``` Но можно и **привязать** одно свойство к другому: ```kotlin val name = SimpleStringProperty("John") val label = Label() label.textProperty().bind(name) // label автоматически обновляется при name.set(...) name.set("Jane") // label.text теперь "Jane" — без явного setText! ``` ### 4.1. Виды Property ```kotlin SimpleStringProperty("text") // String? SimpleIntegerProperty(42) // Int SimpleBooleanProperty(true) // Boolean SimpleDoubleProperty(3.14) // Double SimpleObjectProperty(null) // Любой объект (nullable) SimpleListProperty() // ObservableList ``` Каждая Property — это `ObservableValue` + `WritableValue`: - Можно читать: `property.value` / `property.get()` - Можно писать: `property.value = x` / `property.set(x)` - Можно слушать: `property.addListener { _, old, new -> ... }` - Можно привязать: `a.bind(b)` — `a` всегда равно `b` - Можно привязать двусторонне: `a.bindBidirectional(b)` ### 4.2. Практический пример **PyQt6** — подписка на изменение текста: ```python line_edit.textChanged.connect(lambda text: label.setText(text.upper())) ``` **JavaFX**: ```kotlin lineEdit.textProperty().addListener { _, _, newValue -> label.text = newValue.uppercase() } ``` Или через bind: ```kotlin label.textProperty().bind( lineEdit.textProperty().map { it.uppercase() } ) ``` ### 4.3. Почему это важно? - **TableView** обновляется автоматически, если вы используете ObservableList и CellValueFactory с Property. - **Canvas** может подписаться на `Session.collection` и перерисовываться при изменениях. - **i18n**: при смене языка все textProperty, привязанные через `bind`, обновляются одной строкой `AppLocale.currentLocale.set(newLocale)`. --- ## 5. Простое приложение №1: счётчик кликов **Python (PyQt6):** ```python import sys from PyQt6.QtWidgets import * class MainWindow(QMainWindow): def __init__(self): super().__init__() self.counter = 0 button = QPushButton("Click me") button.clicked.connect(self.on_click) self.label = QLabel("0") layout = QVBoxLayout() layout.addWidget(button) layout.addWidget(self.label) widget = QWidget() widget.setLayout(layout) self.setCentralWidget(widget) def on_click(self): self.counter += 1 self.label.setText(str(self.counter)) app = QApplication(sys.argv) w = MainWindow() w.show() app.exec() ``` **JavaFX:** ```kotlin import javafx.application.Application import javafx.scene.Scene import javafx.scene.control.Button import javafx.scene.control.Label import javafx.scene.layout.VBox import javafx.stage.Stage class CounterApp : Application() { override fun start(stage: Stage) { val counter = SimpleIntegerProperty(0) val label = Label() label.textProperty().bind(counter.asString()) // автообновление val button = Button("Click me") button.setOnAction { counter.set(counter.get() + 1) } val root = VBox(8.0, button, label) stage.scene = Scene(root, 300.0, 200.0) stage.title = "Counter" stage.show() } } fun main() { Application.launch(CounterApp::class.java) } ``` Ключевое отличие: label обновляется автоматически через `bind`. Не нужно писать `label.text = ...` в обработчике. --- ## 6. Простое приложение №2: список с удалением **JavaFX TableView — это не QTableWidget.** В PyQt6: ```python table = QTableWidget(0, 3) table.setItem(0, 0, QTableWidgetItem("value")) ``` В JavaFX TableView работает через **CellValueFactory**: ```kotlin data class Task(val name: String, val done: Boolean) class TaskProperty(task: Task) { val name = SimpleStringProperty(task.name) val done = SimpleBooleanProperty(task.done) } val items: ObservableList = FXCollections.observableArrayList() val table = TableView() val nameCol = TableColumn("Name") nameCol.cellValueFactory = Callback { data -> SimpleObjectProperty(data.value.name.value) // возвращает ObservableValue } val doneCol = TableColumn("Done") doneCol.cellValueFactory = Callback { SimpleObjectProperty(data.value.done.value) } table.columns.addAll(nameCol, doneCol) table.items = items items.add(TaskProperty(Task("Buy milk", false))) // строка появляется в таблице val deleteButton = Button("Delete") deleteButton.setOnAction { val selected = table.selectionModel.selectedItem if (selected != null) items.remove(selected) } ``` Так работает и наша таблица HumanBeing: `TableColumn` + `cellValueFactory` извлекает значение из `HumanBeingProperty`. --- ## 7. Canvas — рисование Аналог `QGraphicsScene` + `QGraphicsView`, но без Scene Graph. Canvas — это растровое полотно: ```kotlin class MyCanvas : Canvas() { private val gc = graphicsContext2D fun draw() { gc.clearRect(0.0, 0.0, width, height) gc.fill = Color.web("#1a1a2e") gc.fillRect(0.0, 0.0, width, height) gc.fill = Color.YELLOW gc.fillOval(50.0, 50.0, 100.0, 100.0) // круг gc.stroke = Color.WHITE gc.lineWidth = 2.0 gc.strokeLine(0.0, 0.0, width, height) // линия gc.fill = Color.WHITE gc.font = Font(14.0) gc.fillText("Hello Canvas", 10.0, 20.0) // текст } } ``` Canvas поддерживает: прямоугольники, овалы, линии, текст, изображения, rotate, scale, save/restore (стек трансформаций). Нет встроенных кнопок или обработчиков — всё рисуется вручную. Обработка кликов — через `addEventHandler`: ```kotlin canvas.addEventHandler(MouseEvent.MOUSE_CLICKED) { event -> val x = event.x val y = event.y // проверка попадания } ``` --- ## 8. Модальные диалоги Аналог `QDialog.exec()`: ```kotlin val dialog = Stage() dialog.initModality(Modality.WINDOW_MODAL) // блокирует родительское окно dialog.initOwner(parentWindow) val button = Button("OK") button.setOnAction { result = "some value" dialog.close() } dialog.scene = Scene(VBox(button), 300.0, 200.0) dialog.showAndWait() // блокирует выполнение до закрытия return result ``` `showAndWait()` аналог `QDialog.exec()`: - Метод не возвращает управление, пока диалог не закрыт. - После закрытия продолжается выполнение сразу после `showAndWait()`. - Пока диалог открыт, родительское окно не реагирует на ввод. --- ## 9. Простое приложение №3: Todo-лист Полный пример, который использует TableView, Property, Layout, модальный диалог: ```kotlin import javafx.application.Application import javafx.beans.property.SimpleStringProperty import javafx.collections.FXCollections import javafx.collections.ObservableList import javafx.geometry.Insets import javafx.scene.Scene import javafx.scene.control.* import javafx.scene.layout.VBox import javafx.stage.Stage class TodoApp : Application() { private val items: ObservableList = FXCollections.observableArrayList() override fun start(stage: Stage) { val table = TableView() table.items = items val col = TableColumn("Task") col.cellValueFactory = javafx.util.Callback { SimpleObjectProperty(it.value.task.value) // ObservableValue для каждой ячейки } val addBtn = Button("Add") addBtn.setOnAction { showAddDialog() } val deleteBtn = Button("Delete") deleteBtn.setOnAction { val selected = table.selectionModel.selectedItem if (selected != null) items.remove(selected) } val root = VBox(8.0, table, addBtn, deleteBtn) root.padding = Insets(8.0) stage.scene = Scene(root, 400.0, 300.0) stage.show() } private fun showAddDialog() { val dialog = Stage() dialog.initModality(Modality.WINDOW_MODAL) dialog.initOwner(stage.scene.window) val field = TextField() val okBtn = Button("OK").apply { setOnAction { if (field.text.isNotBlank()) { items.add(TodoItem(field.text.trim())) dialog.close() } } } dialog.scene = Scene(VBox(8.0, field, okBtn).apply { padding = Insets(8.0) }, 280.0, 100.0) dialog.showAndWait() } } class TodoItem(val task: SimpleStringProperty) { constructor(task: String) : this(SimpleStringProperty(task)) } fun main() { Application.launch(TodoApp::class.java) } ``` --- ## 10. Потоки: Platform.runLater **Критическое правило:** любой код, который читает или пишет UI-свойства, должен выполняться в JavaFX Application Thread (FxAT). ```kotlin // НЕПРАВИЛЬНО: вызов из фонового потока thread { Thread.sleep(1000) label.text = "Done" // throws IllegalStateException! } // ПРАВИЛЬНО: через Platform.runLater thread { Thread.sleep(1000) Platform.runLater { label.text = "Done" } } // ПРАВИЛЬНО (Kotlin coroutines): scope.launch { val result = withContext(Dispatchers.IO) { networkCall() } Platform.runLater { label.text = result } } ``` Аналог в PyQt6: `QMetaObject.invokeMethod(lambda: ..., Qt.ConnectionType.QueuedConnection)` или сигналы через跨线程. --- ## 11. Самая частая ошибка новичков ```kotlin val field = TextField() field.text = "initial" // OK — в Application Thread // ... позже: thread { field.text = "bad" } // CRASH — не в Application Thread ``` В PyQt6 это тоже крашит, но PyQt6 иногда «прощает» случайные вызовы из других потоков. JavaFX строже — будет `IllegalStateException: Not on FX application thread`. --- ## 12. Стилизация через CSS JavaFX поддерживает CSS (аналог Qt Stylesheets): ```kotlin label.style = "-fx-text-fill: red; -fx-font-size: 16px;" button.styleClass.add("primary-button") ``` ```css /* styles.css */ .primary-button { -fx-background-color: #0078d4; -fx-text-fill: white; -fx-font-weight: bold; } ``` Подключается через `scene.stylesheets.add("styles.css")`. --- ## 13. Итог: что нужно запомнить | Концепция | Суть | |-----------|------| | **Stage** | Окно (аналог QMainWindow) | | **Scene** | Содержимое окна (аналог centralWidget) | | **Node** | Любой UI-элемент (аналог QWidget) | | **Pane** | Layout-контейнер (аналог QVBoxLayout, QHBoxLayout) | | **Property** | Observable-значение с возможностью bind | | **Binding** | Автосинхронизация свойств (аналог сигналов, но декларативно) | | **ObservableList** | Список, за которым можно наблюдать | | **Platform.runLater** | Единственный способ обновить UI из фонового потока | | **AnimationTimer** | Вызов каждый кадр (60 FPS) для анимации | | **Canvas** | Растровое полотно для рисования (аналог QGraphicsScene без Scene) | | **TableView** | Таблица с колонками, каждая через cellValueFactory | **Коротко:** JavaFX = PyQt6, где всё завязано на Properties. Если ты привык писать `widget.setX(value)` — забудь. Пиши `a.bind(b)` и подписывайся на изменения через listeners. Это декларативнее и избавляет от тонны ручных обновлений.