💻 Exemplos: Capítulo 18: Interfaces Gráficas Desktop Modernas com JavaFX, FXML, MVC e Bindings Reativos
Especialização em Sistemas com Java 21 LTS • Capítulo 18
🎯 Visão Geral dos Exemplos Práticos
Neste laboratório prático, implementaremos exemplos corporativos completos explorando:
- Aplicação Desktop Reativa em JavaFX 21: implementação de interface fluida de checkout de PDV com
TableVieweObservableList. - Sincronização com Data Bindings: cálculos de totais, descontos e habilitação de botões disparados automaticamente pelo grafo de propriedades.
- Despacho Seguro com
Platform.runLater: atualização de métricas da interface a partir de threads concorrentes de segundo plano.
💻 Exemplo 01 — Terminal PDV Completo com JavaFX e Data Binding
package com.empresa.ui;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import java.text.NumberFormat;
import java.util.Locale;
/**
* Terminal de Ponto de Venda comercial com JavaFX puro e reatividade com ObservableProperties.
*/
public class TerminalPdvReativoApp extends Application {
public record ItemCarrinho(String codigo, String descricao, int quantidade, double precoUnitario) {
public double subtotal() { return quantidade * precoUnitario; }
}
private final ObservableList<ItemCarrinho> itensVenda = FXCollections.observableArrayList();
private final DoubleProperty totalBrutoProperty = new SimpleDoubleProperty(0.0);
@Override
public void start(Stage palcoPrincipal) {
palcoPrincipal.setTitle("Terminal de Vendas PDV (JavaFX 21 LTS)");
// 1. Título e Cabeçalho do Caixa
Label lblHeader = new Label("SISTEMA DE PONTO DE VENDA — TERMINAL #04");
lblHeader.setStyle("-fx-font-size: 16px; -fx-font-weight: bold; -fx-text-fill: #0d47a1;");
// 2. Tabela de Itens Reativa (TableView com ObservableList)
TableView<ItemCarrinho> gridItens = new TableView<>(itensVenda);
gridItens.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN);
TableColumn<ItemCarrinho, String> colCod = new TableColumn<>("CÓDIGO");
colCod.setCellValueFactory(f -> new javafx.beans.property.SimpleStringProperty(f.getValue().codigo()));
TableColumn<ItemCarrinho, String> colDesc = new TableColumn<>("PRODUTO");
colDesc.setCellValueFactory(f -> new javafx.beans.property.SimpleStringProperty(f.getValue().descricao()));
TableColumn<ItemCarrinho, String> colQtd = new TableColumn<>("QTD");
colQtd.setCellValueFactory(f -> new javafx.beans.property.SimpleStringProperty(String.valueOf(f.getValue().quantidade())));
TableColumn<ItemCarrinho, String> colPreco = new TableColumn<>("UNITÁRIO");
colPreco.setCellValueFactory(f -> new javafx.beans.property.SimpleStringProperty(formatarBRL(f.getValue().precoUnitario())));
TableColumn<ItemCarrinho, String> colSub = new TableColumn<>("SUBTOTAL");
colSub.setCellValueFactory(f -> new javafx.beans.property.SimpleStringProperty(formatarBRL(f.getValue().subtotal())));
gridItens.getColumns().addAll(colCod, colDesc, colQtd, colPreco, colSub);
// 3. Formulário de Inclusão Rápida de Produtos
TextField txtCod = new TextField();
txtCod.setPromptText("SKU / Código");
TextField txtDesc = new TextField();
txtDesc.setPromptText("Descrição da mercadoria");
TextField txtQtd = new TextField("1");
txtQtd.setPrefWidth(60);
TextField txtPreco = new TextField();
txtPreco.setPromptText("Preço R$");
Button btnAdicionar = new Button("Inserir (Enter)");
btnAdicionar.setStyle("-fx-background-color: #1b5e20; -fx-text-fill: white; -fx-font-weight: bold;");
btnAdicionar.setOnAction(e -> {
try {
String cod = txtCod.getText().trim();
String desc = txtDesc.getText().trim();
int qtd = Integer.parseInt(txtQtd.getText().trim());
double preco = Double.parseDouble(txtPreco.getText().trim().replace(',', '.'));
if (!cod.isEmpty() && !desc.isEmpty() && qtd > 0 && preco > 0) {
ItemCarrinho novo = new ItemCarrinho(cod, desc, qtd, preco);
itensVenda.add(novo);
totalBrutoProperty.set(totalBrutoProperty.get() + novo.subtotal());
txtCod.clear();
txtDesc.clear();
txtQtd.setText("1");
txtPreco.clear();
txtCod.requestFocus();
}
} catch (NumberFormatException ex) {
new Alert(Alert.AlertType.WARNING, "Informe valores numéricos válidos para quantidade e preço!").showAndWait();
}
});
HBox painelEntrada = new HBox(10, txtCod, txtDesc, txtQtd, txtPreco, btnAdicionar);
painelEntrada.setAlignment(Pos.CENTER_LEFT);
// 4. Rodapé Financeiro com Data Bindings
Label lblTotal = new Label();
lblTotal.setStyle("-fx-font-size: 22px; -fx-font-weight: bold; -fx-text-fill: #b71c1c;");
lblTotal.textProperty().bind(Bindings.createStringBinding(
() -> "TOTAL A PAGAR: " + formatarBRL(totalBrutoProperty.get()),
totalBrutoProperty
));
Button btnFecharCaixa = new Button("💳 Finalizar Venda");
btnFecharCaixa.setStyle("-fx-background-color: #0d47a1; -fx-text-fill: white; -fx-font-size: 14px; -fx-font-weight: bold;");
btnFecharCaixa.disableProperty().bind(Bindings.isEmpty(itensVenda));
btnFecharCaixa.setOnAction(e -> {
new Alert(Alert.AlertType.INFORMATION, "Cupom Fiscal emitido com sucesso! " + lblTotal.getText()).showAndWait();
itensVenda.clear();
totalBrutoProperty.set(0.0);
});
HBox painelRodape = new HBox(25, lblTotal, btnFecharCaixa);
painelRodape.setAlignment(Pos.CENTER_RIGHT);
// Estrutura Principal de Layout
VBox layoutRaiz = new VBox(15, lblHeader, painelEntrada, gridItens, painelRodape);
layoutRaiz.setPadding(new Insets(15));
VBox.setVgrow(gridItens, Priority.ALWAYS);
Scene cena = new Scene(layoutRaiz, 820, 520);
palcoPrincipal.setScene(cena);
palcoPrincipal.show();
}
private static String formatarBRL(double valor) {
NumberFormat nf = NumberFormat.getCurrencyInstance(Locale.of("pt", "BR"));
return nf.format(valor);
}
public static void main(String[] args) {
launch(args);
}
}
💻 Exemplo 02 — Monitoramento Assíncrono com Platform.runLater
package com.empresa.ui;
import javafx.application.Platform;
import javafx.beans.property.StringProperty;
/**
* Padrão seguro para atualização de elementos gráficos do JavaFX a partir de threads concorrentes.
*/
public class AtualizadorSeguroUi {
public static void simularColetaTelemetriaAssincrona(StringProperty targetProperty) {
// Dispara uma thread virtual em background para coletar dados de sensores de rede
Thread.ofVirtual().name("telemetria-sensor").start(() -> {
try {
for (int ciclo = 1; ciclo <= 5; ciclo++) {
Thread.sleep(1000); // Coleta a cada 1 segundo
final String leitura = String.format("📡 Telemetria Ciclo #%d: Temperatura 48.%d°C | Carga: %d%%",
ciclo, ciclo * 2, 20 + ciclo * 5);
// IMPORTANTE: Nunca toque na UI diretamente da thread em segundo plano.
// Use Platform.runLater para despachar para a JavaFX Application Thread:
Platform.runLater(() -> {
targetProperty.set(leitura);
System.out.println(" [UI ATUALIZADA]: " + leitura);
});
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
public static void main(String[] args) throws InterruptedException {
System.out.println("================================================================================");
System.out.println(" ATUALIZAÇÃO SEGURA DA JAVAFX APPLICATION THREAD COM RUNLATER ");
System.out.println("================================================================================");
System.out.println("Simulação do despacho de telemetria assíncrona para a thread gráfica...");
System.out.println("A interface gráfica JavaFX garante execução thread-safe via Platform.runLater().");
System.out.println("================================================================================");
}
}
📋 Resultado Esperado no Console
================================================================================
ATUALIZAÇÃO SEGURA DA JAVAFX APPLICATION THREAD COM RUNLATER
================================================================================
Simulação do despacho de telemetria assíncrona para a thread gráfica...
A interface gráfica JavaFX garante execução thread-safe via Platform.runLater().
[UI ATUALIZADA]: 📡 Telemetria Ciclo #1: Temperatura 48.2°C | Carga: 25%
[UI ATUALIZADA]: 📡 Telemetria Ciclo #2: Temperatura 48.4°C | Carga: 30%
[UI ATUALIZADA]: 📡 Telemetria Ciclo #3: Temperatura 48.6°C | Carga: 35%
[UI ATUALIZADA]: 📡 Telemetria Ciclo #4: Temperatura 48.8°C | Carga: 40%
[UI ATUALIZADA]: 📡 Telemetria Ciclo #5: Temperatura 48.10°C | Carga: 45%
================================================================================