Desenvolvendo uma calculadora I: GUI (desktop app)

Desenvolvendo uma calculadora I: GUI (desktop app)
logo Java logo exp4j

E aí, prontos para mais um desafio?

Neste convido-lhes a criar uma calculadora que realize as quatro operações aritméticas. Nesta devem existir os botões das operações, o botão de igualdade, o botão de ponto para permitir cálculo com números reais, os botões dos dígitos, os botões de parênteses, um botão para limpar a tela, um botão de backspace, além do visor para exibição do resultado.

O design do programa é livre.

Este programa será uma aplicação desktop utilizando a linguagem de programação Java e sua biblioteca gráfica Swing.

Dica: Para conhecer os componentes gráficos do Java e enriquecer a aplicação, consultem a documentação Java nos links Trail: Creating a GUI With Swing (The Java Tutorials) e javax.swing (Javadoc, Java 19).

A resolução:

Esta resolução será dividida em três posts. O primeiro cuidará da GUI (Graphical User Interface), a parte Visual da aplicação. O segundo tratará da parte Lógica, enquanto o último da distribuição da aplicação. Dada a ressalva, sigamos:

Inicialmente confirmem se a plataforma Java necessária para a construção/execução do programa está devidamente instalada/configurada em três simples passos:

Passo 1: Abram seus interpretadores de comandos (terminal (Linux)/prompt de comando (Windows)) e verifiquem se o Java, a JRE (Java Runtime Environment) e a JVM (Java Virtual Machine) estão instaladas digitando:

java -version

Além do Java compiler, digitando:

javac -version

Nota: Todo código digitado em um interpretador de comandos deve ser seguido pelo pressionamento da tecla Enter, para que o comando seja executado.

Se as versões foram exibidas no interpretador de comandos, dirijam-se ao passo 2, senão, façam o download e a instalação da última versão do JDK (Java SE Development Kit), compatível com seus Sistemas Operacionais. Após a instalação, confirmem testando-a novamente com os comandos java e javac.

Passo 2: Verifiquem se as variáveis de ambiente JAVA_HOME, PATH e CLASSPATH estão definidas, executando os seguintes comandos no interpretador de comandos Linux:

echo $JAVA_HOME
echo $PATH
echo $CLASSPATH

ou Windows:

echo %JAVA_HOME%
echo %PATH%
echo %CLASSPATH%

Se as variáveis foram exibidas no interpretador de comandos, seus computadores estão prontos para desenvolver programas em Java e vocês não devem realizar o passo 3. Do contrário, devem definir as variáveis de ambiente, considerando que todos os trechos em negrito nos blocos abaixo sejam substituídos pelos caminhos correspondentes de vossos computadores. Assim sendo, em ambiente Linux, façam:

1. Acessem o diretório profile.d, diretório este, que é carregado junto à inicialização do sistema:

cd /etc/profile.d

2. Criem dentro deste diretório o arquivo para definição das variáveis de ambiente:

sudo nano export_vars.sh

3. No arquivo aberto definam as variáveis redigindo os trechos abaixo. Ao final, salvem o arquivo, realizando a combinação Ctrl+X => S (Sim) => Enter.

export JAVA_HOME=/path/to/JDK-installed-directory
export PATH=$PATH:$JAVA_HOME/bin
export CLASSPATH=.:$JAVA_HOME/lib

Em Windows, façam:

setx -m JAVA_HOME "X:\path\to\JDK-installed-directory"
setx -m PATH "%PATH%;%JAVA_HOME%\bin"
setx -m CLASSPATH ".;%JAVA_HOME%\lib"

Nota: No Windows é necessário abrir o interpretador de comandos no modo "Executar como Administrador" para que as definições sejam realizadas.

Passo 3: Por fim, independente do Sistema Operacional, reiniciem o computador para que as definições sejam processadas. Em seguida, confirmem as definições testando-as novamente com o comando echo. Com isto feito, os passos são concluídos.

Bom, agora, criamos o diretório p3, via interpretador de comandos:

mkdir p3

E neste o arquivo .java:

Em Linux:

touch p3/Calculadora.java

Em Windows:

NUL> p3\Calculadora.java

Ao fim, temos a seguinte estrutura:

p3/
  Calculadora.java

Agora abrimos o arquivo Calculadora.java em um editor de texto como gedit (Linux) ou notepad (Windows), ou ainda outro, caso prefiram e neste criamos uma classe com o mesmo nome do arquivo. Já dentro desta classe criamos o método main, que será o responsável pela execução do programa. Façamos desta forma:

public class Calculadora {
  public static void main(String[] args) {  
  }
}

Agora, dentro do método main, criamos um objeto JFrame, que nada mais é, que a janela do nosso programa. Em seguida definimos as configurações, conforme o código abaixo:

// Cria a janela
JFrame frame = new JFrame();
// Define as dimensões (largura e altura) da janela
frame.setSize(250, 405);
// Define que a janela ao ser fechada deve encerrar a aplicação
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// Define que a janela não será redimensionável
frame.setResizable(false);
// Define o título da janela
frame.setTitle("Calculadora");
// Define que a janela será aberta no centro da tela
frame.setLocationRelativeTo(null);
// Define que a janela será visível
frame.setVisible(true);

Como o JFrame faz parte da biblioteca gráfica Swing, sua importação faz-se necessária. Para isso, incluímos, no topo de nosso arquivo, antes mesmo da declaração da classe a instrução abaixo.

import javax.swing.JFrame;

Com isto feito, salvamos o arquivo e depois testamos. Para tal, dois passos são necessários:

Passo 1: Compilação do arquivo-fonte (.java) em arquivo-intermediário (.class), também chamado de bytecode.

Passo 2: Execução do programa, que neste caso é a Interpretação do arquivo-intermediário por uma JVM.

Realizamos estes abrindo nosso interpretador de comandos e executando os códigos abaixo:

1. Acessa o diretório onde se encontra o arquivo-fonte. Em ambiente Linux:

cd /path/to/java-file-directory

Em ambiente Windows:

cd X:\path\to\java-file-directory

2. Compila

javac Calculadora.java

3. Executa

java Calculadora

Uma tela similar é esperada:

Exibição da janela da aplicação

Figura 1 - Exibição da janela da aplicação.
Fonte: Autor (CC BY-NC-SA 4.0)

O JFrame possui um Container que é onde adicionamos os componentes, como botões, por exemplo. Para usá-lo, precisamos obtê-lo, por meio do método getContentPane. Além disso, precisamos também definir um mecanismo que controle como os componentes serão dispostos no Container. Este mecanismo é conhecido como LayoutManager. A plataforma fornece distintos mecanismos deste, cada qual com suas características. No entanto, utilizaremos a abordagem Absolute Positioning, que nos permite colocar cada componente exatamente onde queremos, por meio de um sistema de coordenadas de tela. Assim sendo, modificamos nosso arquivo Calculadora.java, dentro do método main, incluindo a instrução abaixo:

// Define o não uso de um LayoutManager (AB) para o Container da janela
frame.getContentPane().setLayout(null);

Agora criamos o visor e os botões da calculadora com os componentes JLabel e JButton:

// Cria o visor
JLabel lblVisor = new JLabel();
// Define a dimensão (largura, altura) do visor
lblVisor.setSize(230, 50);
// Cria os botões e define seus respectivos
// textos e dimensões (largura, altura)
JButton btnParEsq = new JButton();
btnParEsq.setText("(");
btnParEsq.setSize(50, 50);
JButton btnParDir = new JButton();
btnParDir.setText(")");
btnParDir.setSize(50, 50);
JButton btnBack = new JButton();
btnBack.setText("←");
btnBack.setSize(50, 50);
JButton btnClear = new JButton();
btnClear.setText("C");
btnClear.setSize(50, 50);
JButton btn7 = new JButton();
btn7.setText("7");
btn7.setSize(50, 50);
JButton btn8 = new JButton();
btn8.setText("8");
btn8.setSize(50, 50);
JButton btn9 = new JButton();
btn9.setText("9");
btn9.setSize(50, 50);
JButton btnDiv = new JButton();
btnDiv.setText("÷");
btnDiv.setSize(50, 50);
JButton btn4 = new JButton();
btn4.setText("4");
btn4.setSize(50, 50);
JButton btn5 = new JButton();
btn5.setText("5");
btn5.setSize(50, 50);
JButton btn6 = new JButton();
btn6.setText("6");
btn6.setSize(50, 50);
JButton btnMult = new JButton();
btnMult.setText("×");
btnMult.setSize(50, 50);
JButton btn1 = new JButton();
btn1.setText("1");
btn1.setSize(50, 50);
JButton btn2 = new JButton();
btn2.setText("2");
btn2.setSize(50, 50);
JButton btn3 = new JButton();
btn3.setText("3");
btn3.setSize(50, 50);
JButton btnSub = new JButton();
btnSub.setText("−");
btnSub.setSize(50, 50);
JButton btn0 = new JButton();
btn0.setText("0");
btn0.setSize(50, 50);
JButton btnPonto = new JButton();
btnPonto.setText(".");
btnPonto.setSize(50, 50);
JButton btnIgual = new JButton();
btnIgual.setText("=");
btnIgual.setSize(50, 50);
JButton btnSoma = new JButton();
btnSoma.setText("+");
btnSoma.setSize(50, 50);

E os adicionamos ao Container da janela:

// Adiciona o visor ao Container da janela
frame.getContentPane().add(lblVisor);
// Adiciona os botões ao Container da janela
frame.getContentPane().add(btnParEsq);
frame.getContentPane().add(btnParDir);
frame.getContentPane().add(btnBack);
frame.getContentPane().add(btnClear);
frame.getContentPane().add(btn7);
frame.getContentPane().add(btn8);
frame.getContentPane().add(btn9);
frame.getContentPane().add(btnDiv);
frame.getContentPane().add(btn4);
frame.getContentPane().add(btn5);
frame.getContentPane().add(btn6);
frame.getContentPane().add(btnMult);
frame.getContentPane().add(btn1);
frame.getContentPane().add(btn2);
frame.getContentPane().add(btn3);
frame.getContentPane().add(btnSub);
frame.getContentPane().add(btn0);
frame.getContentPane().add(btnPonto);
frame.getContentPane().add(btnIgual);
frame.getContentPane().add(btnSoma);

E sem esquecer adicionamos às importações necessárias (em seu devido lugar):

import javax.swing.JButton;
import javax.swing.JLabel;

Por fim, veremos as alterações realizadas, salvando o arquivo editado, recompilando-o e reexecutando-o assim como já realizado anteriormente.

Exibição da janela da aplicação, com visor e botões sem posionamentos definidos.

Figura 2 - Exibição da janela da aplicação, com visor e botões sem posionamentos definidos.
Fonte: Autor (CC BY-NC-SA 4.0)

Opa! Não era exatamente esta tela que esperávamos! Afinal, onde estão o visor e os botões criados? Por que eles não apareceram? Bom, a resposta é simples: falta definirmos os posicionamentos dos componentes, já que optamos pela abordagem AB!

Antes de definí-los, entendamos:

O sistema de coordenadas de tela é similar ao sistema de coordenadas cartesianas. A diferença, é que o ponto (0,0) fica no TOPO-ESQUERDA, ao invés do BASE-ESQUERDA. Para exemplificar vejam a imagem abaixo. Ela ilustra uma grade, que poderia representar a janela do programa, e um quadrado, com suas dimensões (largura e altura), bem como seu posicionamento x,y, que nesse caso é: 20,20. Observem que este refere-se ao canto superior-esquerdo do quadrado.

Representação de um sistema de coordenadas de tela - Exemplo

Figura 3 - Representação de um sistema de coordenadas de tela - Exemplo
Fonte: Autor (CC BY-NC-SA 4.0)

Agora com este entendimento, façamos:

// Define o posicionamento do visor
lblVisor.setLocation(10, 10); 
// Define o posicionamento dos botões
btnParEsq.setLocation(10, 70);    
btnParDir.setLocation(70, 70);
btnBack.setLocation(130, 70);
btnClear.setLocation(190, 70);
btn7.setLocation(10, 130);
btn8.setLocation(70, 130);
btn9.setLocation(130, 130);
btnDiv.setLocation(190, 130);
btn4.setLocation(10, 190);
btn5.setLocation(70, 190);
btn6.setLocation(130, 190);
btnMult.setLocation(190, 190);
btn1.setLocation(10, 250);
btn2.setLocation(70, 250);
btn3.setLocation(130, 250);
btnSub.setLocation(190, 250);
btn0.setLocation(10, 310);
btnPonto.setLocation(70, 310);
btnIgual.setLocation(130, 310);
btnSoma.setLocation(190, 310);

Salvamos, recompilamos e reexecutamos e então obtemos:

Exibição da janela da aplicação, com visor e botões com posionamentos definidos.

Figura 4 - Exibição da janela da aplicação, com visor e botões com posionamentos definidos.
Fonte: Autor (CC BY-NC-SA 4.0)

Bem melhor, não é mesmo? Mas podemos melhorar ainda mais!

Para tal, criamos os objetos abaixo:

// Cria cores no formato sRGB
Color color1 = new Color(255, 255, 255);
Color color2 = new Color(36, 41, 47);
Color color3 = new Color(146, 150, 153);
Color color4 = new Color(82, 183, 227);
Color color5 = new Color(229, 97, 74);
// Cria fonte (nome, estilo, tamanho)
Font font1 = new Font("Helvetica", Font.BOLD, 24);

E agora aplicamos as definições:

// Define o background do Container da janela
frame.getContentPane().setBackground(color2);
// Define visor como um componente opaco
lblVisor.setOpaque(true);
// Define o background do visor
lblVisor.setBackground(color4);
// Define o foreground do visor
lblVisor.setForeground(color1);
// Define a fonte do visor
lblVisor.setFont(font1);

Nota: JLabel é por padrão transparente. Assim para pintá-lo, faz-se necessário definí-lo como um componente opaco.

// Define o background do botão
btnObject.setBackground(colorObject);
// Define o foreground do botão 
btnObject.setForeground(color1);
// Define a fonte do botão
btnObject.setFont(font1);

Neste último, substituímos btnObject por cada um dos botões que criamos. Já em colorObject, substituímos, seguindo:

  • botões [ (, ), 0-9, . ] => color3
  • botões [ ←, C, ÷, ×, −, + ] => color4
  • botão [ = ] => color5

E não esqueçamos das importações:

import java.awt.Color;
import java.awt.Font;

Vejam que Color e Font não são componentes Swing, mas AWT (Abstract Window Toolkit), biblioteca gráfica que precedeu a Swing, mostrando assim, que estas bibliotecas podem ser utilizadas concomitantemente sem nenhum problema.

Bom, agora salvamos, recompilamos e reexecutamos.

GUI estilizada com definições de cor, fonte, plano de fundo, entre outros.

Figura 5 - GUI estilizada com definições de cor, fonte, plano de fundo, entre outros.
Fonte: Autor (CC BY-NC-SA 4.0)

Que mudança hein! Mas podemos evoluir ainda mais! Primeiro vemos que em torno do componente que recebe o foco, no caso, o botão (, há um retângulo azul claro. Para eliminá-lo façamos:

// Define que o botão não será estilizado, caso receba foco. 
// Este estilo é correspondente ao L&F (Look and Feel) utilizado.
btnObject.setFocusPainted(false);

Atentemos também ao que foi exibido em alguns botões, isto é, ... Mas, o que exatamente é isto? Bom, isso é uma indicação de que o conteúdo presente excede o tamanho do componente, e este, então, é truncado. Uma solução seria diminuir o tamanho da fonte utilizada, mas outra solução também é bem-vista, a saber:

// Define que o botão não terá borda
btnObject.setBorder(null);

Alteramos também o Cursor do mouse para HAND a fim de identificar que o componente que utilizá-lo representa uma área clicável. Para isso, é feito:

// Cria um cursor
Cursor cursor1 = new Cursor(Cursor.HAND_CURSOR);
// Define um cursor para o botão
btnObject.setCursor(cursor1);

Bom, redijimos as instruções para cada um dos botões.

E adicionamos a importação:

import java.awt.Cursor;

Ao fim, salvamos, recompilamos, reexecutamos e obtemos:

ButtonUI identificada na GUI, por meio do pressionamento de um botão.

Figura 6 - ButtonUI identificado na GUI, por meio do pressionamento de um botão.
Fonte: Autor (CC BY-NC-SA 4.0)

Observem agora, que o botão de subtração (pressionado) aparece com uma cor diferente dos demais, isto ocorre, por conta, do ButtonUI padrão do botão, que, neste caso é o MetalButtonUI. Para manter a GUI padronizada, criamos uma BasicButtonUI e apliquemos-a aos botões:

// Cria uma BasicButtonUI
BasicButtonUI bbui = new BasicButtonUI();
// Defina a ButtonUI do botão
btnObject.setUI(bbui);

Nota: Adicione a instrução btnObject.setUI(bbui); antes de btnObject.setFocusPainted(false); btnObject.setBorder(null); e btnObject.setCursor(cursor1);

Seguindo adicionando a respectiva importação:

import javax.swing.plaf.basic.BasicButtonUI;

Bom, salvamos, recompilamos e reexecutamos.

Agora os botões apareceram devidamente, mas percebemos que a janela é aberta e só após alguns segundos os botões são exibidos. Isto ocorre, pois definimos que a visualização da janela fosse chamada antes da construção dos botões. Para evitar esse tipo de coisa, devemos mover (e sempre manter) a instrução frame.setVisible(true) como a última instrução de um bloco de construção de componentes.

A instrução frame.setLocationRelativeTo(null); também pode ser afetada, já que faz o cálculo da centralidade, baseada nos componentes existentes na janela. Assim, também mantenhamos-a ao fim.

Bom, com estas novas informações, faz-se necessário uma reorganização do nosso código para que nada fique fora de seu devido lugar. Devemos também melhorar nosso código no sentido de não repetí-lo tantas vezes como feito até agora, porque embora, seja didático, é bem pouco produtivo. Para isso, sigamos, observando o componente JButton.

Vejam que para este, há 10 definições para cada um dos 20 objetos, o que gera, nada mais que 200 instruções/linha. Muita coisa, não é mesmo?

Para solucionar isso, a criação de um método que fique responsável por estas definições é ideal, mas para isso, observamos antes a assinatura dos métodos que utilizamos até agora:

// methodName(parameterType1 parameterName1, ...):returnType
setText(String text):void
setSize(int width, int height):void
setLocation(int x, int y):void
setBackground(Color col1):void
setForeground(Color col2):void
setFont(Font fon):void
setUI(ButtonUI bui):void
setFocusPainted(boolean val):void
setBorder(Border bor):void
setCursor(Cursor cur):void

Por meio dos parameterType e returnType, temos a ciência de quais importações são necessárias para a nossa aplicação. Entre as apresentadas, apenas não temos a classe ButtonUI e a interface Border, portanto, adicionamos:

import javax.swing.border.Border;
import javax.swing.plaf.ButtonUI;

Nota: Como boa prática de programação, mantenhamos as importações ordenadas (alfabeticamente).

Agora, fora do método main, mas ainda dentro da classe, criamos um método com o nome createGUI do seguinte modo:

public void createGUI() {
}

Este método será responsável pela declaração e instanciação dos componentes.

Depois criamos um segundo método, que será responsável pelas definições dos componentes JButton. Façamos desse modo:

public void setJButtonSettings(JButton btn, String text, int width, int height, int x, int y, Color col1, Color col2, Font fon, ButtonUI bui, boolean val, Border bor, Cursor cur) {
    btn.setText(text);
    btn.setSize(width, height);
    btn.setLocation(x, y);
    btn.setBackground(col1);
    btn.setForeground(col2);
    btn.setFont(fon);
    btn.setUI(bui);
    btn.setFocusPainted(val);
    btn.setBorder(bor);
    btn.setCursor(cur);
}

Com o raciocínio similar ao do JButton criamos os métodos para as definições dos componentes JLabel e JFrame. Façamos-os:

public void setJLabelSettings(JLabel lbl, int width, int height, int x, int y, boolean val, Color col1, Color col2, Font fon) {
    lbl.setSize(width, height);
    lbl.setLocation(x, y);
    lbl.setOpaque(val);
    lbl.setBackground(col1);
    lbl.setForeground(col2);
    lbl.setFont(fon);
}
public void setJFrameSettings(JFrame frame, int width, int height, int val1, boolean val2, String title, LayoutManager lm, Color col, Component[] components, Component c, boolean val3) {
    frame.setSize(width, height);
    frame.setDefaultCloseOperation(val1);
    frame.setResizable(val2);
    frame.setTitle(title);
    frame.getContentPane().setLayout(lm);
    frame.getContentPane().setBackground(col);
    for (Component component : components) {
        frame.getContentPane().add(component);
    }
    frame.setLocationRelativeTo(c);
    frame.setVisible(val3);
}

Neste último método criado, observem atentamente às seguintes observações:

  • Há na passagem de parâmetros um objeto LayoutManager, um Component e um Component[ ] (Array de Component), o que implica à adição de novas importações, a saber:
import java.awt.Component;
import java.awt.LayoutManager;
  • O uso do LayoutManager advêm da necessidade do método setLayout cuja assinatura é:
setLayout(LayoutManager lm):void
  • De modo similar, o método setLocationRelativeTo, requer um Component:
setLocationRelativeTo(Component c):void
  • Já dentro do método há o uso de um forEach, a fim de evitar a repetição (manual) da instrução frame.getContentPane().add(component);. Vejam também que o objeto component, contempla, tanto JLabel e JButton, já que ambas estendem a classe Component, conforme esquema abaixo:
// Class Hierarchy

// JButton
java.lang.Object
  java.awt.Component
    java.awt.Container
      javax.swing.JComponent
        javax.swing.AbstractButton
          javax.swing.JButton

// JLabel
java.lang.Object
  java.awt.Component
    java.awt.Container
      javax.swing.JComponent
        javax.swing.JLabel
  • Com o uso de um Array podemos adicionar e ler todos os elementos em apenas uma instrução.

Bom, agora migramos as instruções abaixo do método main para o createGUI:

Color color1 = new Color(255, 255, 255);
Color color2 = new Color(36, 41, 47);
Color color3 = new Color(146, 150, 153);
Color color4 = new Color(82, 183, 227);
Color color5 = new Color(229, 97, 74);
Font font1 = new Font("Helvetica", Font.BOLD, 24);
Cursor cursor1 = new Cursor(Cursor.HAND_CURSOR);
BasicButtonUI bbui = new BasicButtonUI();

No mesmo método incluímos seguidamente:

JLabel lblVisor = new JLabel();
setJLabelSettings(lblVisor, 230, 50, 10, 10, true, color4, color1, font1);

Nota: A linguagem Java é extremamente rigorosa com a passagem de PARÂMETROS, no que se refere ao TIPO, a QUANTIDADE e a ORDEM/SEQUÊNCIA. Assim, se qualquer um destes não for respeitado, o programa não funcionará.

JButton btnParEsq = new JButton();
setJButtonSettings(btnParEsq, "(", 50, 50, 10, 70, color3, color1, font1, bbui, false, null, cursor1);
JButton btnParDir = new JButton();
setJButtonSettings(btnParDir, ")", 50, 50, 70, 70, color3, color1, font1, bbui, false, null, cursor1);
JButton btnBack = new JButton();
setJButtonSettings(btnBack, "←", 50, 50, 130, 70, color4, color1, font1, bbui, false, null, cursor1);
JButton btnClear = new JButton();
setJButtonSettings(btnClear, "C", 50, 50, 190, 70, color4, color1, font1, bbui, false, null, cursor1);
JButton btn7 = new JButton();
setJButtonSettings(btn7, "7", 50, 50, 10, 130, color3, color1, font1, bbui, false, null, cursor1);
JButton btn8 = new JButton();
setJButtonSettings(btn8, "8", 50, 50, 70, 130, color3, color1, font1, bbui, false, null, cursor1);
JButton btn9 = new JButton();
setJButtonSettings(btn9, "9", 50, 50, 130, 130, color3, color1, font1, bbui, false, null, cursor1);
JButton btnDiv = new JButton();
setJButtonSettings(btnDiv, "÷", 50, 50, 190, 130, color4, color1, font1, bbui, false, null, cursor1);
JButton btn4 = new JButton();
setJButtonSettings(btn4, "4", 50, 50, 10, 190, color3, color1, font1, bbui, false, null, cursor1);
JButton btn5 = new JButton();
setJButtonSettings(btn5, "5", 50, 50, 70, 190, color3, color1, font1, bbui, false, null, cursor1);
JButton btn6 = new JButton();
setJButtonSettings(btn6, "6", 50, 50, 130, 190, color3, color1, font1, bbui, false, null, cursor1);
JButton btnMult = new JButton();
setJButtonSettings(btnMult, "×", 50, 50, 190, 190, color4, color1, font1, bbui, false, null, cursor1);
JButton btn1 = new JButton();
setJButtonSettings(btn1, "1", 50, 50, 10, 250, color3, color1, font1, bbui, false, null, cursor1);
JButton btn2 = new JButton();
setJButtonSettings(btn2, "2", 50, 50, 70, 250, color3, color1, font1, bbui, false, null, cursor1);
JButton btn3 = new JButton();
setJButtonSettings(btn3, "3", 50, 50, 130, 250, color3, color1, font1, bbui, false, null, cursor1);
JButton btnSub = new JButton();
setJButtonSettings(btnSub, "−", 50, 50, 190, 250, color4, color1, font1, bbui, false, null, cursor1);
JButton btn0 = new JButton();
setJButtonSettings(btn0, "0", 50, 50, 10, 310, color3, color1, font1, bbui, false, null, cursor1);
JButton btnPonto = new JButton();
setJButtonSettings(btnPonto, ".", 50, 50, 70, 310, color3, color1, font1, bbui, false, null, cursor1);
JButton btnIgual = new JButton();
setJButtonSettings(btnIgual, "=", 50, 50, 130, 310, color5, color1, font1, bbui, false, null, cursor1);
JButton btnSoma = new JButton();
setJButtonSettings(btnSoma, "+", 50, 50, 190, 310, color4, color1, font1, bbui, false, null, cursor1);
JFrame frame = new JFrame();
Component[] co = {
    lblVisor, btnParEsq, btnParDir, btnBack, btnClear, btn7, btn8, btn9, btnDiv, btn4, btn5, btn6, btnMult, btn1, btn2, btn3, btnSub, btn0, btnPonto, btnIgual, btnSoma
};
setJFrameSettings(frame, 250, 405, JFrame.EXIT_ON_CLOSE, false, "Calculadora", null, color2, co, null, true);

Agora removemos todo código ainda disponível no método main, e em seguida neste adicionamos:

// Cria uma instância (objeto) da classe Calculadora
Calculadora calc = new Calculadora();
// Chame o método createGUI
calc.createGUI();

Concluindo isto, nosso arquivo deverá estar seccionado conforme o esboço abaixo. Caso não, devemos reorganizá-lo.

IMPORTAÇÃO: Color (AWT)
IMPORTAÇÃO: Component (AWT)
IMPORTAÇÃO: Cursor (AWT)
IMPORTAÇÃO: Font (AWT)
IMPORTAÇÃO: LayoutManager (AWT)
IMPORTAÇÃO: JButton (Swing)
IMPORTAÇÃO: JFrame (Swing)
IMPORTAÇÃO: JLabel (Swing)
IMPORTAÇÃO: Border (Swing)
IMPORTAÇÃO: ButtonUI (Swing)
IMPORTAÇÃO: BasicButtonUI (Swing)

CLASSE: Calculadora

    MÉTODO: main    
    MÉTODO: createGUI    
    MÉTODO: setJLabelSettings    
    MÉTODO: setJButtonSettings    
    MÉTODO: setJFrameSettings

Agora, salvamos o arquivo, recompilamos e reexecutamos.

GUI com código otimizado

Figura 7 - GUI com código otimizado.
Fonte: Autor (CC BY-NC-SA 4.0)

Uall... que evolução no código hein! Quantas coisas aprendidas! Criamos métodos para tratar específicas finalidades, deixando nosso código muito mais modularizado e menos repetitivo.

Dando continuidade à evolução de nossa GUI, agora propomos a troca das cores dos backgrounds por variantes mais escuras, isto quando o cursor passar por cima dos botões, ou mesmo quando os botões forem pressionados. Prontos para mais este desafio?

Primeiro criamos três novas cores, adicionando-as seguidamente às outras em nosso código:

Color color6 = new Color(116, 120, 124);
Color color7 = new Color(34, 159, 213);
Color color8 = new Color(212, 57, 30);

Agora uma pausa para uma breve explicação:

Quando falamos de executar uma ação, quando o cursor passa por cima dos botões, ou mesmo quando o botão é pressionado estamos falando de Eventos (Event) que são disparados por um agente externo, neste caso, o mouse. Todo programa em seu ciclo de vida normal não aguarda um evento, portanto, para que isso aconteça é necessário implementar um Ouvinte de Eventos (Listener), que ficará responsável por ouvir e delegar o evento recebido ao seu respectivo Manipulador (Handler).

Para isto implementamos um Listener, compatível com o agente, que em nosso caso é a Interface MouseListener, substituindo a instrução public class Calculadora { por:

public class Calculadora implements MouseListener {

E adicionando a respectiva importação:

import java.awt.event.MouseListener;

Como MouseListener é uma Interface ela já nos fornece os métodos para manipulação de cada um dos eventos, e nos obriga à adicioná-los em nosso código. Para tanto, adicionamos-os, dentro da nossa classe, após o último método criado, conforme o bloco abaixo:

@Override
public void mousePressed(MouseEvent e) {
    // Disparado quando o botão do mouse é
    // pressionado sobre um componente.
}

@Override
public void mouseReleased(MouseEvent e) {
    // Disparado quando o botão do mouse é 
    // liberado sobre um componente.
}

@Override
public void mouseEntered(MouseEvent e) {
    // Disparado quando o mouse entra 
    // na área (passa por cima) de um componente.
}

@Override
public void mouseExited(MouseEvent e) {
    // Disparado quando o mouse sai da área de um componente.
}

@Override
public void mouseClicked(MouseEvent e) {
    // Disparado quando o botão do mouse é 
    // clicado (pressionado e liberado) de um componente.
}

Adicionamos também a importação da classe MouseEvent:

import java.awt.event.MouseEvent;

Para fins de identificação dos componentes (botões), nomeamos de forma exclusiva, utilizando, por exemplo, o nome dos próprios objetos. Também associamos os componentes ao Listener. Para tal, façamos:

1. Acrescentamos String name e MouseListener ml como os últimos parâmetros passado ao método setJButtonSettings. Algo como:

setJButtonSettings(paramType paramName,..., ..., String name, MouseListener ml){}

2. Agora dentro desse método acrescentamos (ao fim) a instrução:

// Define um nome para o botão
btn.setName(name);
// Adiciona um MouseListener ao botão
btn.addMouseListener(ml);

3. Por fim, incluímos em todas as chamadas do método setJButtonSettings em seu método createGUI os parâmetros, conforme:

setJButtonSettings(param,..., ..., "btnObject", this);

Atentemos em adicionar uma vírgula antes do btnObject para separá-lo do parâmetro anterior. Atentemos também nas aspas duplas, que não podem ser omitidas, pois representam um parâmetro do tipo String.

Agora declaramos os atributos globais dentro de nossa classe:

Component comp;
String compName;

E incluímos dentro de nosso método mouseEntered a instrução:

// Obtêm e armazena o componente que acionou o evento
comp = e.getComponent();
// Obtêm e armazena o nome do componente que acionou o evento
compName = e.getComponent().getName();
// Identifica o componente conforme seu nome, 
// e então, modifica sua cor de background.
switch (compName) {
    case "btnParEsq":
    case "btnParDir":
    case "btn7":
    case "btn8":
    case "btn9":
    case "btn4":
    case "btn5":
    case "btn6":
    case "btn1":
    case "btn2":
    case "btn3":
    case "btn0":
    case "btnPonto":
        comp.setBackground(color6);
        break;
    case "btnBack":
    case "btnClear":
    case "btnDiv":
    case "btnMult":
    case "btnSub":
    case "btnSoma":
        comp.setBackground(color7);
    break;
    case "btnIgual":
        comp.setBackground(color8);
    break;
}

Copiamos a mesma instrução e incluímos dentro do método mouseExited, substituindo apenas os objetos color6, color7 e color8, por color3, color4 e color5, respectivamente.

Se tudo correr bem, ao passar o mouse sobre os botões, teremos cores escuras. Já ao tirar, teremos as cores iniciais. Assim sendo, salvamos, recompilamos e reexecutamos.

O interpretador de comandos reportou mensagens de erro, não é mesmo? Mensagens do tipo: error: cannot find symbol para cada um dos objetos Color. Mas porque isto aconteceu, considerando que estes objetos já existem? Isso acontece, por conta do escopo local destes objetos, o que os torna visíveis apenas ao método que os circunda, a saber, o createGUI. Para torná-los visíveis à todos os métodos, devemos migrá-los para fora do método, tornando-os globais. Façamos do seguinte modo:

// FORA DO MÉTODO: DECLARE-OS
Color color3;
Color color4;
Color color5;
Color color6;
Color color7;
Color color8;
// DENTRO DO MÉTODO: INSTANCIE-OS
color3 = new Color(146, 150, 153);
color4 = new Color(82, 183, 227);
color5 = new Color(229, 97, 74);
color6 = new Color(116, 120, 124);
color7 = new Color(34, 159, 213);
color8 = new Color(212, 57, 30);

Com a alteração feita, salvamos, recompilamos e reexecutamos.

Consideremos também que o método mousePressed recebe a mesma instrução, que o método mouseEntered. Equivalentemente, isto ocorre entre mouseExited e mouseReleased. Para tal, temos cenários de duplicidade de código, para evitá-los, criemos os métodos:

public void setDarkColorMode(MouseEvent e) {
}
public void setNormalColorMode(MouseEvent e) {
}

Agora, migramos a instrução que há no método mouseEntered para o método setDarkColorMode. Façamos o mesmo de mouseExited para setNormalColorMode.

Ao fim, chamamos os novos métodos em cada um dos manipuladores:

@Override
public void mousePressed(MouseEvent e) {
    setDarkColorMode(e);
}

@Override
public void mouseReleased(MouseEvent e) {
    setNormalColorMode(e);
}

@Override
public void mouseEntered(MouseEvent e) {
    setDarkColorMode(e);        
}

@Override
public void mouseExited(MouseEvent e) {
    setNormalColorMode(e);        
}

Agora tratamos do click, afinal queremos ver os valores serem exibidos no visor! Para isso:

1. Declaramos e inicializamos o atributo global:

String expression = "";

2. Tornamos também global o objeto lblVisor, do mesmo modo que foi feito aos objetos Color.

3. Já no método mouseClicked incluímos:

compName = e.getComponent().getName();
switch (compName) {
    case "btnParEsq":    
        // expression armazena seu valor atual concatenado ao caractere 
        expression = expression.concat("(");
        break;
    case "btnParDir":
        expression = expression.concat(")");
        break;
    case "btnBack":
        // criar método
        break;
    case "btnClear":
        // criar método
        break;
    case "btn7":
        expression = expression.concat("7");
        break;
    case "btn8":
        expression = expression.concat("8");
        break;
    case "btn9":
        expression = expression.concat("9");
        break;
    case "btn4":
        expression = expression.concat("4");
        break;
    case "btn5":
        expression = expression.concat("5");
        break;
    case "btn6":
        expression = expression.concat("6");
        break;
    case "btn1":
        expression = expression.concat("1");
        break;
    case "btn2":
        expression = expression.concat("2");
        break;
    case "btn3":
        expression = expression.concat("3");
        break;
    case "btn0":
        expression = expression.concat("0");
        break;
    case "btnPonto":
        expression = expression.concat(".");
        break;
    case "btnDiv":
        expression = expression.concat("÷");
        break;
    case "btnMult":
        expression = expression.concat("×");
        break;
    case "btnSub":
        expression = expression.concat("−");
        break;
    case "btnSoma":
        expression = expression.concat("+");
        break;
    case "btnIgual":
        // criar método
        break;
}
// Define o texto do visor com o valor de expression
lblVisor.setText(expression);

Com isto feito, salvamos, recompilamos, reexecutamos e testamos os botões, por exemplo, com a expressão 3+2×5:

Expressão (alinhada à esquerda) é impressa no visor da GUI

Figura 8 - Expressão (alinhada à esquerda) é impressa no visor da GUI.
Fonte: Autor (CC BY-NC-SA 4.0)

Considerando o resultado, dois ajustes necessários nos apresentam, a saber:

  • Manter o texto na direita;
  • Deixar um espaço entre o texto e a extremidade do visor;

Para tal, façamos:

1. Criamos uma borda, utilizando a classe EmptyBorder dentro de seu método createGUI:

// Cria uma borda vazia (e transparente) 
// (SUPERIOR, ESQUERDA, INFERIOR, DIREITA)
Border emptyBorder = new EmptyBorder(10, 10, 10, 10);

2. Na seção de importações, incluímos:

import javax.swing.border.EmptyBorder;

3. Já em nosso método setJLabelSettings, incluímos mais dois parâmetros, do seguinte modo:

setJButtonSettings(paramType paramName,..., ..., int align, Border bor){}

4. Dentro deste mesmo método incluímos as instruções:

// Define o alinhamento do texto do visor
lbl.setHorizontalAlignment(align);
// Define uma borda ao visor
lbl.setBorder(bor);

5. Por fim, incluímos na chamada do método setJLabelSettings em nosso método createGUI os parâmetros, conforme:

setJLabelSettings(param,..., ..., JLabel.RIGHT, emptyBorder);

Com isto feito, salvamos, recompilamos e reexecutamos. O resultado esperado é:

Expressão (alinhada à direita) é impressa no visor da GUI.

Figura 9 - Expressão (alinhada à direita) é impressa no visor da GUI.
Fonte: Autor (CC BY-NC-SA 4.0)

Bem melhor, não é mesmo? Agora, testemos uma expressão maior, por exemplo, 3+2×5−3+2×5−3+2×5:

Expressão impressa no visor da GUI é truncada.

Figura 10 - Expressão impressa no visor da GUI é truncada.
Fonte: Autor (CC BY-NC-SA 4.0)

Hum.... ter a expressão truncada não parece o comportamento ideal em uma calculadora. Assim sendo, devemos pensar em alternativas, tais como:

  • Diminuir o texto toda vez que o conteúdo exceder o tamanho do componente
  • Quebrar o texto, gerando linhas e rolagem horizontal
  • Permitir rolagem vertical (e automática)

Considerando a aplicação desenvolvida, a terceira alternativa é a mais razoável. No entanto, o JLabel não tem esse comportamento por padrão. Para isto, devemos usar um componente específico, anexando à ele, o JLabel. Este novo componente é o JScrollPane. Assim façamos:

1. Criamos em nossa classe o método setJScrollPaneSettings:

public void setJScrollPaneSettings(JScrollPane scroll, int width, int height, int x, int y, Color col1, Border bor1, Component c, Border bor2, Color col2) {
    // Define as dimensões do scroll
    scroll.setSize(width, height);
    // Define o posicionamento do scroll
    scroll.setLocation(x, y);
    // Define o background do scroll
    scroll.setBackground(col1);
    // Define uma borda ao scroll
    scroll.setBorder(bor1);
    // Anexa um componente ao viewport do scroll
    scroll.setViewportView(c);
    // Define uma borda ao viewport do scroll
    scroll.setViewportBorder(bor2);
    // Define o background do viewport do scroll
    scroll.getViewport().setBackground(col2);        
}

2. Na seção de importações, incluímos:

import javax.swing.JScrollPane;

3. Depois declaramos o componente globalmente:

JScrollPane scroll;

3. Já no método createGUI após a chamada do método setJLabelSettings incluímos:

scroll = new JScrollPane();
setJScrollPaneSettings(scroll, 230, 50, 10, 10, color4, null, lblVisor, emptyBorder, color4);

4. Ainda no mesmo método substituímos o primeiro elemento do Component[] co, isto é, lblVisor, por scroll. Assim na execução do método seguinte, a saber, o setJFrameSettings, o scroll será adicionado ao Container da janela, ao invés do JLabel, que agora está contido na viewport do JScrollPane.

5. Como o JScrollPane já possui definido dimensão, posicionamento e borda, não se faz necessário repetí-los no componente que nele está contido. Assim sendo, removemos:

  • Do método setJLabelSettings os parâmetros int width, int height, int x, int y e Border bor, bem como suas respectivas instruções;
  • Do método createGUI, os mesmos parâmetros na chamada do método setJFrameSettings.

Salvamos, recompilamos, reexecutamos e testamos a expressão anterior.

Uso de rolagem no visor da GUI

Figura 11 - Uso de rolagem no visor da GUI.
Fonte: Autor (CC BY-NC-SA 4.0)

Vendo o resultado, identificamos mais dois desafios à superar, isto é:

  • Estilizar as barras de rolagem, no sentido de se aproximar da GUI proposta até agora;
  • Fazer com que a barra de rolagem mantenha-se sempre à direita, a cada novo clique, mantendo assim, a respectiva visualização.

Para o primeiro desafio, incluímos o parâmetro val à nosso método setJScrollPaneSettings, conforme:

setJScrollPaneSettings(paramType paramName,..., ..., int val){}

Dentro deste mesmo método incluímos a instrução:

// Define quando a barra de rolagem horizontal deve ser exibida
scroll.setHorizontalScrollBarPolicy(val);

Já no método createGUI, incluímos o parâmetro na chamada do método setJScrollPaneSettings, conforme:

setJScrollPaneSettings(param,..., ..., JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);

Já para o segundo desafio, primeiro criamos, globalmente, um Point:

Point point;

Instanciamos-o no createGUI.

point = new Point();

Na seção de importações, incluímos:

import java.awt.Point;

Por fim, criamos na classe um novo método, o moveScroll:

public void moveScroll() {
    // Define as coordenadas de um ponto (x,y)
    // x = largura do conteúdo contido na viewport do scroll + largura (aproximada) do caractere - largura do scroll 
    // y = 10 (posição do scroll)
    point.setLocation(scroll.getViewport().getViewSize().width + 50 - scroll.getWidth(), 10);
    // Define a área visível (executa a rolagem) para a posição defina pelo ponto
    scroll.getViewport().setViewPosition(point);
}

Chamamos o método moveScroll no método mouseClicked após a instrução lblVisor.setText(expression);, isto é, após o (último) caractere ser impresso no visor.

moveScroll();

Salvamos, recompilamos e reexecutamos.

Expressão impressa no visor com rolagem fixa (à direita)

Figura 12 - Expressão impressa no visor com rolagem fixa (à direita).
Fonte: Autor (CC BY-NC-SA 4.0)

Com isto feito, finalizamos a parte I desta resolução, já podendo iniciar a parte II desta incrível série! Se gostaram, por favor, deixem seus COMENTÁRIOS, nos SIGAM e principalmente COMPARTILHEM para que assim, seja possível alcançar mais pessoas, que como vocês, curtem Programação, com conteúdos PRÁTICOS e GRATUITOS!

Para ver e/ou baixar o código-fonte desta resolução, ou mesmo melhorá-lo, visitem o repositório no GitHub.

Até a próxima!

Cite este material

FERNANDES, Fábio. Desenvolvendo uma calculadora I: GUI (desktop app). aprendaCodar, 09 de maio de 2022. Disponível em: <https://aprendacodar.blogspot.com/2022/05/desenvolva-uma-calculadora-desktop-app.html>. Acesso em:

Fábio Fernandes

Graduado em Ciência da Computação e Especialista em Análise de Dados com BI e Big Data. Instrutor, Desenvolvedor e Produtor de Conteúdo. Apaixonado por Tecnologia e pelo compartilhamento de conhecimentos.

2 Comentários

  1. Show de bola! Muito bem preparado, muito bem explicado! Parabés professor! Estou ansioso para fazer.

    ResponderExcluir
    Respostas
    1. Que legal Ícaro! Fico feliz em poder contribuir e lhe agregar em conhecimentos! Bora!

      Excluir
Postar um comentário
Postagem Anterior Próxima Postagem