docs added
This commit is contained in:
@@ -0,0 +1,556 @@
|
||||
# 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. Это декларативнее и избавляет от тонны ручных обновлений.
|
||||
Reference in New Issue
Block a user