Interface gráfica com JavaFx
Apesar de verbosa e complexa, nosso querido javinha tem o seu mar de rosas, que vai muito além das simples saídas de código no terminal…
Interface gráfica com JavaFx
Edição: Pedro Aquino
Apesar de verbosa e complexa, nosso querido javinha tem o seu mar de rosas, que vai muito além das simples saídas de código no terminal. Assim como em algumas linguagens modernas, é possível criar e personalizar um front-end digno de carregar o codinome da linguagem mais amada por muitos de nós: o JavaFX.
O que é JavaFX?
JavaFX é uma plataforma de desenvolvimento para a criação de aplicações ricas em interfaces gráficas, que se destaca por sua capacidade de oferecer uma experiência de usuário moderna e interativa. Desenvolvido pela Oracle, JavaFX é uma evolução do Java Swing, proporcionando uma série de funcionalidades que facilitam a construção de aplicação desktop e web com interfaces sofisticadas.
Saiba mais sobre o JavaFX em: https://profissaocloud.com.br/glossario/o-que-e-javafx-como-utilizar-e-suas-caracteristicas
Tecnologias necessárias para a reprodução dos passos deste artigo:
- Java (versão recomendada: 21 ou superior)
- JavaFX (para criação da interface gráfica)
- Maven (para gerenciamento de dependências e build do projeto)
- IDE de sua preferência (IntelliJ IDEA, Eclipse ou NetBeans)
- Docker (para criação de um Container com o cliente do banco MySQL)
- MySQL (para realizar as operações CRUD)
- Scene Builder (para facilitar a criação da interface com JavaFX)
- Chat GPT (para auxiliar nas dúvidas que possam surgir)
Há outros artigos de minha autoria que explicam em detalhes o uso e a instalação de ferramentas como Maven e Docker.
O que é JavaFX Scene Builder?
O JavaFX Scene Builder é uma ferramenta de layout visual que permite projetar rapidamente interfaces de usuário para aplicativos JavaFX, sem a necessidade de programação manual. É possível arrastar e soltar componentes da interface (UI) em uma área de trabalho, ajustar suas propriedades, aplicar folhas de estilo e, em segundo plano, o código FXML correspondente ao layout é gerado automaticamente. O resultado é um arquivo FXML que pode ser integrado a um projeto Java, vinculando a interface à lógica do aplicativo.
Saiba mais sobre JavaFX Scene Builder em: https://www.oracle.com/java/technologies/javase/javafxscenebuilder-info.html
Detalhes do Projeto
Iremos desenvolver um simples sistema de cadastro de alunos. O objetivo é criar algo direto e funcional, para vermos na prática como uma interface se integra às operações de bancos de dados relacionais. Iremos adicionar, remover, ler e atualizar dados de alunos em uma tabela, contendo as seguintes informações:
- Nome
- Idade
Estrutura do Projeto
Primeiro Passo: Preparar o Volume do MySQL via Docker para realizar as operações CRUD.
Crie uma pasta com nome Docker e adicione os 2 arquivos a seguir.
Para este passo, utilizarei três arquivos (dois necessários e um opcional), os quais são:
- docker-compose.yml — conterá os detalhes da imagem Docker (necessário)
- init.sql — conterá o script SQL (necessário)
- README.md — conterá instruções de como subir a imagem a partir de uma porta específica (opcional)
A fim de facilitar a reutilização desses arquivos, deixarei um link para a pasta que os contém no GitHub, juntamente com os demais arquivos do projeto.
- docker-compose.yml
services:
student-db:
image: mysql:8.0.33
container_name: student-db
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: java
MYSQL_DATABASE: student_base
ports:
- "3307:3306"
command: >
--character-set-server=utf8mb4
--collation-server=utf8mb4_general_ci
volumes:
- db_data:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
volumes:
db_data:
- init.sql
-- Cria o banco e usa
CREATE DATABASE IF NOT EXISTS student_base
DEFAULT CHARACTER SET utf8mb4
DEFAULT COLLATE utf8mb4_0900_ai_ci;
USE student_base;
-- Cria a tabela student
CREATE TABLE IF NOT EXISTS student (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
idade INT NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
registered_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
-- Insere dados de exemplo
INSERT INTO student (name, idade, email) VALUES
('Pedro Aquino', 22, 'pedro@example.com'),
('Maria Oliveira', 19, 'maria@example.com'),
('Carlos Souza', 25, 'carlos@example.com');
Adicione esses dois arquivos em uma pasta, acesse-a pelo terminal e execute os seguintes comandos:
- Suba o Container:
sudo docker-compose up
- Conecte-se ao MySQL:
sudo docker exec -it student-db mysql -uroot -pjava
- Execute no terminal MySQL:
USE student_base;
SHOW TABLES; -- Deve mostrar a tabela "student"
SELECT * FROM student; -- Deve listar os 3 estudantes inserido
Passos inicias para criar um volume no Docker
Os passos anteriores devem ser seguidos para a criação do volume inicial. Se, por algum imprevisto, o volume já tiver sido criado e você estiver encontrando dificuldades para gerenciá-lo, siga os comandos a seguir.
- Derrubar e Remover volume
sudo docker compose down -v
- Listar volume
sudo docker volume ls
- Remover um volume pelo nome/id
sudo docker volume rm nome_do_volume
- Listar imagens
sudo docker image ls
- Remover imagens pelo nome/id
sudo docker rmi <ID_ou_nome:tag>
- Listar Container que usam um volume específico
sudo docker ps -a --filter volume=<nome_volume>
- Remover volume por nome específico
sudo docker volume rm <nome_volume>
- Remover um Container por id/nome
sudo docker rm -f <nome/id_conteiner>
Análise a stack dos logs de erro, talvez algum comando acima consiga lhe ajudar.
Para acessar a imagem MySQL via porta 3307 que configuramos anteriormente use o comando:
mysql -h 127.0.0.1 -P 3307 -uroot -pjava student_base
Certifique-se que já tenha executado o comando:
sudo docker compose up -d
Diferença entre Imagem, Volume e Container no Docker
Imagem
- É como um “modelo” ou “snapshot” imutável (somente leitura).
- Contém o sistema operacional base + dependências + binários da sua aplicação.
- Exemplo: mysql:8.0, nginx:alpine.
- Você não altera uma imagem em tempo de execução; se precisa mudar algo, cria outra imagem (com Dockerfile ou docker commit).
- Pense numa receita de bolo.
Container
- É uma instância em execução de uma imagem
- A imagem é somente leitura, mas o container adiciona uma camada de escrita temporária.
- Você pode iniciar, parar, remover e recriar containers quantas vezes quiser.
- Se o container for removido, tudo escrito dentro dele some, exceto se esteja em um volume.
- Pense no bolo pronto feito a partir da receita.
Volume
- É o mecanismo de persistência de dados no Docker.
- Armazenado no host (fora da camada efêmera do container).
- Se o container for apagado, os dados continuam no volume.
- Exemplo: bancos de dados (MySQL, PostgreSQL) precisam de volume para não perder as tabelas.
- Pense numa caixa onde você guarda o bolo após pronto — se jogar o bolo fora (container), a caixa com os pedaços (dados) ainda existe.
Exemplo prático
- Imagem: mysql:8.0 (MySQL pronto para usar).
- Container: student-db (instância rodando do MySQL, porta 3307).
- Volume: db_data (onde ficam ibdata1, tabelas, etc.).
O volume é criado automaticamente ao executar o comando (docker compose up -d) pois está especificado no arquivo docker-compose.yml, o sufixo -d serve para não deixar o terminal travado enquanto o volume está sendo criado.
Se você quiser excluir alguma imagem criada precisa eliminar primeiro os container ligados a ela (se houver).
Segundo Passo: Criação dos módulos na IDE a partir do maven, usarei como exemplo o IntelliJ IDEA.
No mesmo diretório que se encontra o arquivo Docker, crie um módulo, usei como o exemplo o nome Student (seguindo a estrutura do projeto supracitado) este módulo irá conter o pom.xml principal da aplicação, não vamos editá-lo por enquanto.
Ao criar um módulo, o Maven gera automaticamente a estrutura de pastas necessárias para a execução do projeto (src/main e src/test). No entanto, no módulo Student, essa estrutura não será necessária e pode ser excluída.
O diretório src/test pode ser excluído de todos os módulos. Como o próprio nome indica, ele é destinado a testes (por exemplo, com JUnit). Como este projeto não utilizará testes, essas pastas podem ser descartadas.
De início adicione a tag abaixo (dependência para conexão do banco MySQL) no pom.xml do módulo Student:
<dependencies>
<!-- MySQL Connector -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
</dependencies>
Estrutura
Dentro do módulo Student crie outro módulo chamado student_model
Criação do módulo student_model
No diretório src/main/java, crie um subdiretório chamado model e, dentro dele, a classe Student.java. Nessa classe, defina atributos privados para id, nome, idade e e-mail. Em seguida, gere um construtor com argumentos e um sem argumentos, logo após, os métodos getters de cada atributo.
Recomendo criar os atributos na mesma ordem e nomes onde as colunas foram definidas na tabela do banco de dados, pois isso facilita o desenvolvimento futuro. A ordem e o nome são especialmente importantes para o construtor.
O diretório resource não é necessário neste caso, sendo sua exclusão opcional. Mais adiante será explicado para que ele é utilizado.
Agora iremos trabalhar com a camada DAO (Data Access Object), uma das camadas dos Design Patterns (Padrões de Projeto). Essa camada é responsável por conectar o projeto ao banco de dados e executar as operações CRUD.
Diferença entre DAL e DAO
A diferença entre DAL (Data Access Layer) e DAO (Data Access Object) reside na sua abrangência. O DAL é uma camada completa de acesso a dados, enquanto o DAO é um componente específico dentro dessa camada, focado em operações com uma única entidade ou tabela.
Como vamos lidar com somente a tabela student, usamos a abreviação DAO.
No módulo Student crie outro módulo chamado student_core
Criação do módulo student_core
No diretório src/main/java, crie um subdiretório chamado core e, dentro dele, a classe DatabaseConnection.java. Nessa classe, defina atributos privados.
Ela funcionará como uma camada de acesso aos dados, utilizando a API JDBC (Java Database Connectivity) para estabelecer a comunicação entre a aplicação Java e o banco de dados. O JDBC fornece os recursos necessários para abrir conexões, enviar comandos SQL e processar os resultados retornados. Assim, essa classe será responsável por mapear as tabelas e executar as instruções SQL, centralizando a lógica de acesso aos dados.
É importante garantir que as informações utilizadas para a conexão sejam as mesmas definidas na criação do volume MySQL. Caso estejam corretas, a conexão será estabelecida com sucesso.
No módulo Student crie outro módulo chamado student_repository
Criação do módulo student_repository
Dentro do diretório src/main/java, crie um subdiretório chamado repository e, dentro dele, a classe StudentRepository.java.
Nessa classe serão implementadas as operações CRUD (Create, Read, Update, Delete) do projeto.
Para a execução das operações, são necessários dois módulos que o projeto já possui: student_core (responsável pela conexão com o banco) e student_model (responsável pelo acesso aos atributos). Essas dependências devem ser declaradas no arquivo pom.xml do módulo student_repository.
Exemplo do pom.xml no módulo student_repository
Import da classe student_repository
package repository;
import model.Student;
import core.DatabaseConnection;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
- CRUD - C (create)
public void insert(Student student) throws SQLException {
String sql = "INSERT INTO student (nome, idade, email) VALUES (?, ?, ?)";
try (Connection l_Context = DatabaseConnection.getConnection();
PreparedStatement l_Parameter = l_Context.prepareStatement(sql)) {
l_Parameter.setString(1, student.getName());
l_Parameter.setInt(2, student.getAge());
l_Parameter.setString(3, student.getEmail());
l_Parameter.executeUpdate();
}
}
- CRUD - R (Read)
public List<Student> findAll() throws SQLException {
List<Student> students = new ArrayList<>();
String sql = "SELECT * FROM students";
try (Connection l_Context = DatabaseConnection.getConnection();
PreparedStatement l_Parameter = l_Context.prepareStatement(sql);
ResultSet l_Result = l_Parameter.executeQuery()){
while (l_Result.next()){
Student student = new Student(
l_Result.getInt("id"),
l_Result.getString("name"),
l_Result.getInt("age"),
l_Result.getString("email"),
l_Result.getTimestamp("registered_at").toLocalDateTime()
);
students.add(student);
}
return students;
}
}
public Student findById(int id) throws SQLException {
String sql = "SELECT * FROM student WHERE id = ?";
try (Connection l_Context = DatabaseConnection.getConnection();
PreparedStatement l_Parameter = l_Context.prepareStatement(sql)) {
l_Parameter.setInt(1, id);
try (ResultSet l_Result = l_Parameter.executeQuery()) {
if (l_Result.next()) {
return new Student(
l_Result.getInt("id"),
l_Result.getString("name"),
l_Result.getInt("age"),
l_Result.getString("email"),
l_Result.getTimestamp("registered_at").toLocalDateTime()
);
}
return null;
}
}
}
- CRUD - U (Update)
public void update(Student student) throws SQLException {
if (student.getStudent_id() <= 0)
throw new IllegalArgumentException("User must have a valid ID");
String sql = "UPDATE students SET nome = ?, idade = ?, email = ? WHERE id = ?";
try (Connection l_Context = DatabaseConnection.getConnection();
PreparedStatement l_Parameter = l_Context.prepareStatement(sql)) {
l_Parameter.setString(1, student.getName());
l_Parameter.setInt(2, student.getAge());
l_Parameter.setString(3, student.getEmail());
l_Parameter.setInt(4, student.getId());
l_Parameter.executeUpdate();
}
}
- CRUD - D (Delete)
public void delete(Student student) throws SQLException {
if (student.getStudent_id() <= 0)
throw new IllegalArgumentException("User must have a valid ID");
String sql = "DELETE FROM students WHERE id = ?";
try (Connection l_Context = DatabaseConnection.getConnection();
PreparedStatement l_Parameter = l_Context.prepareStatement(sql)) {
l_Parameter.setInt(1, student.getId());
l_Parameter.executeUpdate();
}
}
Todos esses métodos devem constar na classe.
No módulo Student crie outro módulo chamado student_service
Criação do módulo student_service
No diretório src/main/java, crie um subdiretório chamado service e, dentro dele, a classe StudentService.java.
Essa classe é responsável por lidar com as regras de negócio. Nos Design Patterns, essa camada é denominada BLL (Business Logic Layer). É nela que se assegura, por exemplo, que apenas nomes válidos sejam persistidos nas colunas, que a idade seja armazenada como um número inteiro positivo e o campo de e-mail contenha o caractere “@” acompanhado de um domínio válido.
Além disso, a BLL centraliza outras regras e restrições da aplicação, garantindo consistência e integridade nos dados antes que sejam enviados à camada de acesso ao banco.
package service;
import model.Student;
import repository.StudentRepository;
import java.sql.SQLException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
public class StudentService {
private final StudentRepository repository = new StudentRepository();
public void addStudent(Student student) throws SQLException {
if (student.getName() == null || student.getName().trim().isEmpty())
throw new IllegalArgumentException("Name cannot be empty!");
if (student.getAge() < 0 || student.getAge() > 150)
throw new IllegalArgumentException("Age must be between 0 and 150!");
if (student.getEmail() == null || !student.getEmail().contains("@"))
throw new IllegalArgumentException("Invalid Email!");
if (student.getRegistered_at() == null)
student.setRegistered_at(LocalDate.from(LocalDateTime.now()).atStartOfDay());
repository.insert(student);
}
public List<Student> listStudents() throws SQLException {
return repository.findAll();
}
public void updateStudent(Student student) throws SQLException {
if (student.getId() <= 0)
throw new IllegalArgumentException("Invalid ID!");
if (student.getName() == null || student.getName().trim().isEmpty())
throw new IllegalArgumentException("Name cannot be empty!");
if (student.getAge() < 0 || student.getAge() > 150)
throw new IllegalArgumentException("Age must be between 0 and 150!");
if (student.getEmail() == null || !student.getEmail().contains("@"))
throw new IllegalArgumentException("Invalid Email!");
repository.update(student);
}
public void deleteStudent(int id) throws SQLException {
repository.delete(id);
}
public Student getStudent(int id) throws SQLException {
return repository.findById(id);
}
}
Chegamos ao principal tópico deste projeto: a criação da interface gráfica utilizando o Scene Builder.
Certifique-se de que o JavaFX e o Scene Builder estão instalados corretamente em sua máquina antes de prosseguir.
Links para Download:
- JavaFX: https://openjfx.io/
- Scene Builder: https://gluonhq.com/products/scene-builder/
Dentro do módulo Student crie outro módulo chamado student_ui
Criação do módulo student_ui
Dentro do diretório src/main/java, crie um subdiretório chamado ui e, dentro dele, a classe MainApp.java.
Neste módulo, utilizaremos, além do diretório src/main/java, o diretório src/main/resources. É neste subdiretório que armazenamos o arquivo FXML da interface gráfica e, quando necessário, outros recursos do projeto, como imagens nos formatos PNG, JPG, JPEG, entre outros. Manter essa organização é fundamental, pois o JavaFX depende do caminho correto desses arquivos para carregá-los durante a execução da aplicação.
Temporariamente, deixaremos a IDE de lado e acessaremos diretamente o Scene Builder para a construção da interface gráfica.
Interface do Scene Builder
Existe uma playlist no YouTube que ensina a manusear o ambiente do Scene Builder, oferecendo um guia prático para iniciantes.
[embed]
Neste projeto, serão criadas quatro telas. Começaremos pela tela principal, que será denominada studentView.
Nessa tela, haverá uma imagem e quatro botões — Register, Update, Delete e Read — cada um direcionando para uma interface específica onde será possível realizar a respectiva operação do CRUD.
Link para a imagem usada na interface: https://www.freepik.com/icon/female-student_12034876#fromView=keyword&page=1&position=23&uuid=b379ccff-005d-44ee-beac-8598c73a3948
Observe que, no canto inferior esquerdo, informamos o caminho da classe que executará a aplicação no campo Controller Class.
Esse caminho corresponde ao nome do package seguido do nome da classe responsável por chamar a interface. No lado direito, definimos um ID e um On Action. O ID permite identificar qual botão o usuário clicou, enquanto o On Action define a ação que será executada após o clique. Cada botão deve possuir um ID e uma ação exclusivos. Na imagem acima, é mostrado o exemplo do botão Register.
Devemos salvar tanto a imagem utilizada na interface quanto o próprio arquivo da interface com a extensão .fxml dentro da pasta resources. É possível criar subpastas dentro de src/main/resources, mas é fundamental verificar o arquivo FXML e garantir que o caminho até a imagem esteja correto.
Arquivo studentView.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.VBox?>
<VBox prefHeight="400.0" prefWidth="640.0" xmlns="http://javafx.com/javafx/24.0.1" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="ui.MainApp">
<AnchorPane maxHeight="-1.0" maxWidth="-1.0" VBox.vgrow="ALWAYS">
<TextField alignment="CENTER" editable="false" layoutX="86.0" layoutY="35.0" prefHeight="28.0"
prefWidth="450.0" text="Welcome"/>
<Button fx:id="btnRegister" layoutX="281.0" layoutY="107.0" onAction="#openWindowRegister" prefHeight="87.0"
prefWidth="146.0" text="Register"/>
<Button fx:id="btnRead" layoutX="452.0" layoutY="107.0" onAction="#openWindowRead" prefHeight="87.0"
prefWidth="146.0" text="Read"/>
<Button fx:id="btnUpdate" layoutX="281.0" layoutY="220.0" onAction="#openWindowUpdate" prefHeight="87.0"
prefWidth="146.0" text="Update"/>
<Button fx:id="btnDelete" layoutX="452.0" layoutY="220.0" onAction="#openWindowDelete" prefHeight="87.0"
prefWidth="146.0" text="Delete"/>
<ImageView fitHeight="200.0" fitWidth="200.0" layoutX="50.0" layoutY="100.0">
<Image url="student.png"/>
</ImageView>
</AnchorPane>
</VBox>
Adicione a dependência do JavaFX no pom.xml do módulo student_ui para que as importações sejam resolvidas corretamente
Estrutura do módulo student_ui caminho src/main/resources
Estrutura do módulo student_ui caminho src/main/java
Antes de prosseguirmos com o desenvolvimento, é necessário adicionar a seção <build> em dois arquivos pom.xml: no módulo student_ui, que será responsável por executar a interface, e no pom.xml principal do módulo Student.
- Student_ui — pom.xml
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.0.0</version>
<configuration>
<mainClass>ui.App</mainClass>
</configuration>
</plugin>
</plugins>
</build>
Essa configuração permite que o comando mvn exec:java saiba exatamente qual classe deve ser executada.
- Student — pom.xml
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
</plugin>
</plugins>
</build>
O maven-jar-plugin é responsável por empacotar o projeto em um arquivo .jar, gerado durante a fase de build.
A ordem dos módulos é importante para a compilação do projeto. Por exemplo, a classe model depende da classe core para funcionar corretamente; portanto, o módulo model não pode ser compilado antes do módulo core.
- pom.xml Principal (completo)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.pedro</groupId>
<artifactId>Student</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>student_core</module>
<module>student_model</module>
<module>student_repository</module>
<module>student_service</module>
<module>student_ui</module>
</modules>
<properties>
<maven.compiler.source>24</maven.compiler.source>
<maven.compiler.target>24</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- MySQL Connector -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
</plugin>
</plugins>
</build>
</project>
- Class App
package ui;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.util.Objects;
public class App extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
Parent root = FXMLLoader.load(Objects.requireNonNull(getClass().getResource("/studentView.fxml")));
primaryStage.setTitle("Student UI");
primaryStage.setScene(new Scene(root));
primaryStage.centerOnScreen();
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
A classe App é responsável por exibir a primeira interface gráfica, denominada studentView. Por padrão, ela deve estender a classe abstrata Application, presente na biblioteca JavaFX. Essa classe define o ciclo de vida de uma aplicação JavaFX e exige a sobrescrita do método start(Stage primaryStage), no qual configuramos a janela principal (stage), definimos a cena (scene) e carregamos o arquivo FXML correspondente.
Como a App apenas inicializa a interface, ela deve ser conectada dinamicamente à classe MainApp, que será responsável por gerenciar as novas janelas conforme os cliques do usuário nos botões da interface principal. Essa conexão ocorre quando o FXMLLoader carrega o arquivo FXML: a partir do atributo fx:controller, ele instancia a classe MainApp em tempo de execução, injeta os elementos anotados com @FXML e associa os métodos definidos em onAction aos eventos da interface. Esse processo é realizado em runtime por reflexão, e não em tempo de compilação.
Antes de manipularmos a classe MainApp, podemos testar se a interface principal já está funcionando. Para isso, utilize os comandos mvn install e mvn exec:java diretamente no terminal. Neste estágio, ainda não será feita a conexão da interface ao volume no Docker.
[embed]
Há duas maneiras de executar o projeto: a primeira é acessando o diretório student_ui e executando o comando mvn exec:java; a segunda é, a partir do diretório student, utilizar o comando mvn -pl student_ui exec:java. A flag -pl (projects list) informa ao Maven que a execução deve ocorrer apenas no módulo especificado — neste caso, o student_ui — sem necessidade de entrar manualmente no diretório.
- Class MainApp
package ui;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Modality;
import javafx.stage.Stage;
import java.util.Objects;
public class MainApp {
private Stage registerStage, readStage, updateStage, deleteStage;
private Stage ensureChild(Stage current, String fxml, String title, ActionEvent e) throws Exception {
if (current == null || !current.isShowing()) {
Parent root = FXMLLoader.load(Objects.requireNonNull(getClass().getResource(fxml)));
Stage owner = (Stage) ((Node) e.getSource()).getScene().getWindow();
Stage child = new Stage();
child.initOwner(owner);
child.initModality(Modality.NONE);
child.setTitle(title);
child.setResizable(false);
child.setScene(new Scene(root));
child.show();
child.setOnHidden(ev -> {
if (child == registerStage) registerStage = null;
if (child == readStage) readStage = null;
if (child == updateStage) updateStage = null;
if (child == deleteStage) deleteStage = null;
});
return child;
} else {
current.toFront();
current.requestFocus();
return current;
}
}
@FXML
private void openWindowRegister(ActionEvent e) {
try { registerStage = ensureChild(registerStage, "/register/register.fxml", "Register", e); }
catch (Exception ex) { ex.printStackTrace(); }
}
@FXML
private void openWindowRead(ActionEvent e) {
try { readStage = ensureChild(readStage, "/read/read.fxml", "Read", e); }
catch (Exception ex) { ex.printStackTrace(); }
}
@FXML
private void openWindowUpdate(ActionEvent e) {
try { updateStage = ensureChild(updateStage, "/update/update.fxml", "Update", e); }
catch (Exception ex) { ex.printStackTrace(); }
}
@FXML
private void openWindowDelete(ActionEvent e) {
try { deleteStage = ensureChild(deleteStage, "/delete/delete.fxml", "Delete", e); }
catch (Exception ex) { ex.printStackTrace(); }
}
}
A classe MainApp atua como controller responsável pela abertura e gerenciamento das janelas secundárias da aplicação (Register, Read, Update e Delete). Para isso, mantém uma referência a cada uma dessas janelas e, por meio do método privado ensureChild, realiza o carregamento do arquivo FXML, cria e configura um novo Stage e garante que apenas uma instância de cada janela permaneça aberta.
Caso a janela já esteja ativa, ela é apenas trazida para frente, evitando duplicações. Além disso, quando uma janela é fechada, sua referência é reinicializada, permitindo nova abertura futura.
Os métodos anotados com @FXML correspondem aos botões da interface principal (studentView) e acionam a abertura da respectiva janela, assegurando a navegação controlada entre as telas da aplicação.
Chegamos à última etapa do nosso desenvolvimento. Será apresentado, de forma breve, o código da classe RegisterController, que está diretamente vinculadas às interfaces criadas em arquivos .fxml (assim como as outras classes se vinculam aos seus respectivos arquivos .fxml).
- Class RegisterController
package ui.register;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import model.Student;
import service.StudentService;
import java.time.LocalDate;
import java.time.LocalDateTime;
public class RegisterController {
@FXML private TextField txtName;
@FXML private Spinner<Integer> spnAge;
@FXML private TextField txtEmail;
@FXML private DatePicker dpRegisteredDate;
@FXML private Label lblStatus;
private final StudentService service = new StudentService();
@FXML
public void initialize() {
spnAge.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(0, 150, 18));
dpRegisteredDate.setValue(LocalDate.now());
}
@FXML
void onSave(ActionEvent event) {
try {
String name = txtName.getText().trim();
int age = spnAge.getValue();
String email = txtEmail.getText().trim();
LocalDate date = dpRegisteredDate.getValue();
LocalDateTime registeredAt = LocalDateTime.now();
Student s = new Student(0, name, age, email, registeredAt);
service.addStudent(s);
lblStatus.setText("Saved!");
clearForm();
} catch (Exception ex) {
lblStatus.setText("Error: " + ex.getMessage());
}
}
private void clearForm() {
txtName.clear();
spnAge.getValueFactory().setValue(18);
txtEmail.clear();
dpRegisteredDate.setValue(LocalDate.now());
}
}
Interface de registro
A classe RegisterController é responsável por tratar os dados inseridos pelo usuário nos campos da interface. Conectada à classe de serviço (Service), onde estão implementadas as regras de negócio (BLL), ela permite validar as informações recebidas antes de armazená-las no banco de dados. Dessa forma, assegura que cada coluna da tabela receba valores consistentes, evitando falhas e possíveis inconsistências na aplicação.
As interfaces foram desenvolvidas com o auxílio de uma ferramenta de inteligência artificial. Para a análise detalhada, todo o código completo do projeto encontra-se disponível no repositório. Em caso de dúvidas ou questionamentos, entre em contato comigo pessoalmente ou pelo e-mail pedro.aquino.ct@gmail.com.
Assista ao vídeo demonstrativo para compreender como conectar o projeto ao Docker e executar as operações de CRUD na prática. Estude com dedicação e lembre-se: o aprendizado contínuo exige ir além do que já se sabe. A evolução só acontece quando nos desafiamos a aprender o novo.
[embed]
Link para o projeto: https://github.com/pedro-vaf/student
Autor: Pedro Aquino — Desenvolvedor Back-End
Referências:
https://profissaocloud.com.br/glossario/o-que-e-javafx-como-utilizar-e-suas-caracteristicas/
메타데이터
- post_id
- 0f862fea18bd
- slug
- interface-gráfica-com-javafx-0f862fea18bd
- url
- https://medium.com/@pedro.vaf/interface-gr%C3%A1fica-com-javafx-0f862fea18bd
- canonical_url
- https://medium.com/@pedro.vaf/interface-gr%C3%A1fica-com-javafx-0f862fea18bd
- author_url
- https://medium.com/@pedro.vaf
- status
- ok
- fetched_at
- 2026-07-14 20:31:00