18 KiB
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→ButtonQLabel→LabelQLineEdit→TextFieldQComboBox→ComboBoxQVBoxLayout→VBoxQHBoxLayout→HBoxQGridLayout→GridPaneQTableWidget→TableViewQGraphicsView→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. Это декларативнее и избавляет от тонны ручных обновлений.