Files
Lab8-Prog/docs stuff/JAVAFX_GUIDE.md
T
2026-06-12 17:42:16 +03:00

557 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<UUID?>(null) // Любой объект (nullable)
SimpleListProperty<String>() // ObservableList
```
Каждая Property — это `ObservableValue<T>` + `WritableValue<T>`:
- Можно читать: `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<TaskProperty> = FXCollections.observableArrayList()
val table = TableView<TaskProperty>()
val nameCol = TableColumn<TaskProperty, String>("Name")
nameCol.cellValueFactory = Callback { data ->
SimpleObjectProperty(data.value.name.value) // возвращает ObservableValue
}
val doneCol = TableColumn<TaskProperty, Boolean>("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<TodoItem> = FXCollections.observableArrayList()
override fun start(stage: Stage) {
val table = TableView<TodoItem>()
table.items = items
val col = TableColumn<TodoItem, String>("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. Это декларативнее и избавляет от тонны ручных обновлений.