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

18 KiB
Raw Blame History

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:

app = QApplication(sys.argv)
window = QMainWindow()
window.show()
sys.exit(app.exec())

JavaFX:

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.

val stage = Stage()
stage.title = "My App"
stage.isResizable = true
stage.minWidth = 400.0
stage.minHeight = 300.0
stage.show()

Scene — это содержимое окна. Аналог centralWidget / setWidget().

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:

layout = QVBoxLayout()
layout.addWidget(button)
layout.addWidget(label)
widget.setLayout(layout)

В JavaFX layout'ы — это тоже Node (наследники Pane):

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 так:

button.setText("New text")
label.setText(str(counter))

В JavaFX можно сделать то же самое:

button.text = "New text"
label.text = counter.toString()

Но можно и привязать одно свойство к другому:

val name = SimpleStringProperty("John")
val label = Label()
label.textProperty().bind(name)  // label автоматически обновляется при name.set(...)

name.set("Jane")  // label.text теперь "Jane" — без явного setText!

4.1. Виды Property

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 — подписка на изменение текста:

line_edit.textChanged.connect(lambda text: label.setText(text.upper()))

JavaFX:

lineEdit.textProperty().addListener { _, _, newValue ->
    label.text = newValue.uppercase()
}

Или через bind:

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):

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:

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:

table = QTableWidget(0, 3)
table.setItem(0, 0, QTableWidgetItem("value"))

В JavaFX TableView работает через CellValueFactory:

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 — это растровое полотно:

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:

canvas.addEventHandler(MouseEvent.MOUSE_CLICKED) { event ->
    val x = event.x
    val y = event.y
    // проверка попадания
}

8. Модальные диалоги

Аналог QDialog.exec():

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, модальный диалог:

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).

// НЕПРАВИЛЬНО: вызов из фонового потока
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. Самая частая ошибка новичков

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):

label.style = "-fx-text-fill: red; -fx-font-size: 16px;"

button.styleClass.add("primary-button")
/* 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. Это декларативнее и избавляет от тонны ручных обновлений.