Capítulo 18: Interfaces Gráficas Desktop Modernas com JavaFX, FXML, MVC e Bindings Reativos
Especialização em Sistemas com Java 21 LTS • Módulo de Banco de Dados e Projetos Integradores
🗺️ Mapa Conceitual do Tópico
flowchart TD
subgraph LIFECYCLE["Ciclo de Vida JavaFX (Application)"]
A["main(): Application.launch()"] --> B["init() (Background Thread)"]
B --> C["start(Stage primary) (JavaFX App Thread)"]
C --> D["stop() (Cleanup na finalização)"]
end
subgraph SCENE_GRAPH["Grafo de Cena (Scene Graph)"]
E["Stage (Janela Nativa do SO)"]
F["Scene (Contêiner de Dimensões e CSS)"]
G["Parent / Root Layout (BorderPane / VBox / GridPane)"]
H["Nós Folha (Button, TextField, TableView, Label)"]
E --> F --> G --> H
end
subgraph MVC_BINDINGS["Padrão MVC & Reatividade"]
I["View: Interface Declarativa FXML + CSS"]
J["Controller: @FXML Injeção e Event Handling"]
K["Model: Observable Properties (StringProperty, ObjectProperty)"]
L["Data Binding Reativo (Unidirecional e Bidirecional)"]
I <--> J <--> K
K --> L
end
C --> SCENE_GRAPH
SCENE_GRAPH --> MVC_BINDINGS
style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
style C fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
style E fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style I fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style L fill:#c8e6c9,stroke:#388e3c,stroke-width:2px
🖥️ 1. A Arquitetura do JavaFX e o Grafo de Cena (Scene Graph)
O JavaFX é a plataforma canônica e moderna para o desenvolvimento de aplicações ricas para desktop (Rich Client Platforms - RCP) na JVM, sucedendo as bibliotecas históricas AWT e Swing.
Diferenciais Arquiteturais Chave:
- Aceleração Gráfica por Hardware (Pipeline Prism): Todo o desenho de componentes visuais é renderizado pela GPU através de DirectX (Windows) ou Metal/OpenGL (Linux/macOS).
- O Grafo de Cena (Scene Graph): Estrutura de dados em árvore hierárquica onde cada elemento gráfico (painéis, botões, formas 3D) é um nó (
javafx.scene.Node). - JavaFX Application Thread: A thread dedicada ao processamento de eventos visuais da UI. Regra de Ouro: Qualquer atualização em componentes gráficos deve ocorrer estritamente nessa thread; operações assíncronas em segundo plano devem despachar atualizações visuais via
Platform.runLater(() -> { ... }).
🎨 2. A Tríade MVC: FXML, Controladores e Estilização CSS
Para garantir desacoplamento absoluto entre engenharia de software e design de interfaces:
1. View Declarativa com FXML
O FXML é uma linguagem declarativa baseada em XML que define a hierarquia de nós da tela. Pode ser editado visualmente via Scene Builder:
<VBox spacing="10" alignment="CENTER" xmlns:fx="http://javafx.com/fxml" fx:controller="com.empresa.ui.PDVController">
<Label text="Sistema de Ponto de Venda" styleClass="titulo-principal"/>
<TextField fx:id="txtCodigoBarras" promptText="Digite o código SKU..."/>
<Button text="Registrar Item" onAction="#onAdicionarItemAction"/>
</VBox>
2. O Controlador (@FXML Controller)
Classe Java responsável por interceptar as interações do usuário. A anotação @FXML permite que a engine de injeção conecte nós do arquivo XML com variáveis da classe sem chamadas manuais como findViewById:
public class PDVController implements Initializable {
@FXML private TextField txtCodigoBarras;
@FXML private TableView<ItemVenda> tabelaItens;
@FXML
private void onAdicionarItemAction() {
// Lógica de manipulação
}
}
3. Estilização com CSS NATIVO
JavaFX permite aplicar folhas de estilo CSS (style.css), utilizando propriedades com prefixo -fx- (-fx-background-color: #2c3e50;, -fx-border-radius: 8px;).
⚡ 3. Propriedades Reativas e Data Binding
Um dos recursos mais poderosos do JavaFX é o sistema de Observables e Properties (SimpleStringProperty, SimpleDoubleProperty, SimpleObjectProperty).
Sincronização Automática com Binding:
Elimina o código repetitivo de Listeners:
// O botão "Finalizar Venda" é desabilitado reativamente enquanto o carrinho estiver vazio
btnFinalizar.disableProperty().bind(carrinho.itensProperty().emptyProperty());
// Conecta o valor de um Slider a um Label de porcentagem bidirecionalmente
lblDesconto.textProperty().bind(sliderDesconto.valueProperty().asString("%.0f %%"));
📊 4. Matriz Comparativa: Plataformas de UI Desktop
| Critério | JavaFX Moderno | Java Swing (Legado) | Electron (Node + Chromium) |
|---|---|---|---|
| Aceleração Gráfica | GPU Nativa (Prism) | Renderização por CPU (Software) | GPU via Chromium |
| Consumo de Memória RAM | Baixo a Médio (~80 a 150 MB) | Mínimo (~40 a 80 MB) | Muito Alto (+400 MB) |
| Design e Temas | CSS Moderno + FXML | Pluggable Look and Feel rígido | HTML5 / CSS3 / Tailwind |
| Bindings Reativos | Nativos (Property API) | Manuais (PropertyChangeListener) |
Reatividade via React/Vue |
| Distribuição Standalone | jlink / jpackage (Instalador .exe nativo) | JAR executável (exige JRE instalada) | Empacotamento Node.js |
💻 5. Código de Demonstração Corporativo (Java 21 LTS)
package com.empresa.ui;
import javafx.application.Application;
import javafx.application.Platform;
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.math.BigDecimal;
/**
* Ponto de Venda (PDV) corporativo reativo desenvolvido em JavaFX 21 puro.
*/
public class SimuladorPdvJavaFx extends Application {
public record ProdutoPdv(String sku, String descricao, double preco) {}
private final ObservableList<ProdutoPdv> itensVenda = FXCollections.observableArrayList();
private final DoubleProperty totalVendaProperty = new SimpleDoubleProperty(0.0);
@Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Ponto de Venda Corporativo (JavaFX 21 LTS)");
// Componentes Visuais
Label lblTitulo = new Label("🛒 Caixa Aberto - Terminal PDV #01");
lblTitulo.setStyle("-fx-font-size: 18px; -fx-font-weight: bold; -fx-text-fill: #1a237e;");
TableView<ProdutoPdv> tabela = new TableView<>(itensVenda);
TableColumn<ProdutoPdv, String> colSku = new TableColumn<>("SKU");
colSku.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().sku()));
TableColumn<ProdutoPdv, String> colDescricao = new TableColumn<>("Descrição do Produto");
colDescricao.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().descricao()));
colDescricao.setPrefWidth(250);
TableColumn<ProdutoPdv, String> colPreco = new TableColumn<>("Preço (R$)");
colPreco.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(String.format("R$ %.2f", data.getValue().preco())));
tabela.getColumns().addAll(colSku, colDescricao, colPreco);
// Formulário de Entrada
TextField txtSku = new TextField();
txtSku.setPromptText("SKU do item");
TextField txtNome = new TextField();
txtNome.setPromptText("Descrição");
TextField txtValor = new TextField();
txtValor.setPromptText("Preço unitário");
Button btnAdicionar = new Button("➕ Incluir Produto");
btnAdicionar.setStyle("-fx-background-color: #2e7d32; -fx-text-fill: white; -fx-font-weight: bold;");
btnAdicionar.setOnAction(e -> {
try {
String sku = txtSku.getText().trim();
String nome = txtNome.getText().trim();
double valor = Double.parseDouble(txtValor.getText().replace(',', '.'));
if (!sku.isEmpty() && !nome.isEmpty()) {
itensVenda.add(new ProdutoPdv(sku, nome, valor));
totalVendaProperty.set(totalVendaProperty.get() + valor);
txtSku.clear();
txtNome.clear();
txtValor.clear();
txtSku.requestFocus();
}
} catch (NumberFormatException ex) {
Alert alert = new Alert(Alert.AlertType.ERROR, "Preço numérico inválido!", ButtonType.OK);
alert.showAndWait();
}
});
// Painel Inferior de Totais com Data Binding
Label lblTotal = new Label();
lblTotal.setStyle("-fx-font-size: 20px; -fx-font-weight: bold; -fx-text-fill: #b71c1c;");
lblTotal.textProperty().bind(totalVendaProperty.asString("TOTAL A PAGAR: R$ %,.2f"));
Button btnFinalizar = new Button("💳 Finalizar Venda");
btnFinalizar.setStyle("-fx-background-color: #1565c0; -fx-text-fill: white; -fx-font-size: 14px;");
btnFinalizar.disableProperty().bind(itensVenda.sizeProperty().isEqualTo(0)); // Desabilita se carrinho vazio
btnFinalizar.setOnAction(e -> {
Alert alert = new Alert(Alert.AlertType.INFORMATION, "Venda liquidada com sucesso no valor de " + lblTotal.getText(), ButtonType.OK);
alert.showAndWait();
itensVenda.clear();
totalVendaProperty.set(0.0);
});
// Montagem do Layout
HBox formBox = new HBox(10, txtSku, txtNome, txtValor, btnAdicionar);
formBox.setAlignment(Pos.CENTER_LEFT);
HBox footerBox = new HBox(20, lblTotal, btnFinalizar);
footerBox.setAlignment(Pos.CENTER_RIGHT);
VBox root = new VBox(15, lblTitulo, formBox, tabela, footerBox);
root.setPadding(new Insets(15));
Scene scene = new Scene(root, 750, 480);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
// Inicialização do runtime gráfico JavaFX
launch(args);
}
}