← Back to list

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…

Pedro Aquino · 2025-11-26 19:12 · 0 claps · 16.0 min read
#java #javafx #maven
Open on Medium ↗

Interface gráfica com JavaFx

Edição: Pedro Aquino

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
  • E-mail

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

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

Estrutura

Dentro do módulo Student crie outro módulo chamado student_model

Criação do módulo 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

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

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

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

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:

Dentro do módulo Student crie outro módulo chamado student_ui

Criação do módulo 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

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/resources

Estrutura do módulo student_ui caminho src/main/java

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

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