22 de dez. de 2016
Livro gratuito sobre JavaFX
E aconteceu! Esse blog virou um pequeno livro de bolso sobre JavaFX! Agradeço a todos que acessam o blog, são 400 acessos diários(em média), com picos de 1000 acessos por dia!
O livro é simples, com o conteúdo do blog, mas somente as partes básicas. Há também um código testado e preparado para ser importado no Eclipse para uso.
Veja a página do livro para mais informações.
Será que há mais livros por vir? Veremos.
27 de nov. de 2016
Um pequeno temporizador (ou cronômetro)
Nessa rápida postagem iremos compartilhar com vocês um pequeno temporizador feito em JavaFX.
A ideia é bem simples, um temporizador configurável que pode ser usado como auxílio aos que gostam da técnica pomodoro. Já quem quer aprender JavaFX, esse é só um código de exemplo de uso da classe Animation.
A ideia é bem simples: você escolhe um tempo e o temporizador começa a diminuir aquele tempo tocando um "tick" até chegar a 0 e então um som de bomba explodindo é tocado.
O código não é complexo, veja abaixo e sem seguida uma pequena explicação:
A ideia é bem simples, um temporizador configurável que pode ser usado como auxílio aos que gostam da técnica pomodoro. Já quem quer aprender JavaFX, esse é só um código de exemplo de uso da classe Animation.
A ideia é bem simples: você escolhe um tempo e o temporizador começa a diminuir aquele tempo tocando um "tick" até chegar a 0 e então um som de bomba explodindo é tocado.
O código não é complexo, veja abaixo e sem seguida uma pequena explicação:
- A aplicação funciona dependendo da classe Animation que é controlada pelo único botão da aplicação. Criamos uma animação sem fim(sem tempo para parar), mas que a sua execução é controlada pelo botão;
- Para que seja executado código a cada um segundo, adicionamos um KeyFrame na animação que é executado a cada 1 segundo e tem por ação o método atualizaValores;
- Quando o botão é clicado, verificamos o estado atual dele (se ele está com texto PARAR ou Começar) e de acordo com esse valor paramos ou iniciamos a animação e também pegamos o atual valor selecionado no slider;
- No método atualizaValores é onde tudo acontece. Nele atualizamos o valor do label, atualizamos o tempo restante e também tocamos o áudio;
- O método atualizaLabelTempo é responsável por pegar o valor em segundos e transformar no formato mm:ss, para que seja mostrado no label;
Essa é a ideia básica dessa app feita em menos de 1 hora. Você pode pegar o código no github e melhorar para adicionar mais funcionalidades! Veja abaixo o código final:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package org.aprendendojavafx.timer; | |
import javafx.animation.Animation; | |
import javafx.animation.Animation.Status; | |
import javafx.animation.KeyFrame; | |
import javafx.animation.Timeline; | |
import javafx.application.Application; | |
import javafx.geometry.Pos; | |
import javafx.scene.Scene; | |
import javafx.scene.control.Button; | |
import javafx.scene.control.Label; | |
import javafx.scene.control.Slider; | |
import javafx.scene.layout.VBox; | |
import javafx.scene.media.Media; | |
import javafx.scene.media.MediaPlayer; | |
import javafx.scene.text.Font; | |
import javafx.scene.text.FontWeight; | |
import javafx.stage.Stage; | |
import javafx.util.Duration; | |
public class Main extends Application { | |
final String SOM_TICK = "/sons/tick.mp3"; | |
final String SOM_GRANADA = "/sons/granada.mp3"; | |
final String COMECAR = "Começar"; | |
final String PARAR = "PARAR"; | |
private Slider sldValor; | |
private Label lblValor; | |
private Button btnAcao; | |
// temporiza o tempo do slider | |
private Timeline temporizador; | |
int tempoFaltante; | |
private MediaPlayer mpTick; | |
private MediaPlayer mpGranada; | |
public static void main(String[] args) { | |
launch(); | |
} | |
@Override | |
public void start(Stage palco) throws Exception { | |
inicializacoes(); | |
VBox vbRaiz = constroiInterface(); | |
// não pode alterar o slider se já estiver tocando | |
sldValor.disableProperty().bind( | |
temporizador.statusProperty().isEqualTo(Status.RUNNING)); | |
ajustaLabel(); | |
// ajustes finais | |
palco.setScene(new Scene(vbRaiz)); | |
palco.setWidth(600); | |
palco.setHeight(400); | |
palco.setTitle("Temporizador (parado)"); | |
palco.show(); | |
// deve existir forma melhor e fazer isso | |
lblValor.textProperty().addListener(i -> { | |
if(btnAcao.getText().equals(COMECAR)) { | |
palco.setTitle("Temporizador (parado)"); | |
} else { | |
palco.setTitle("Temporizador (" + lblValor.getText() + ")"); | |
} | |
}); | |
} | |
private void inicializacoes() { | |
temporizador = new Timeline(); | |
temporizador.setCycleCount(Animation.INDEFINITE); | |
String arquivoTick = Main.class.getResource(SOM_TICK).toString(); | |
String arquivoGranada = Main.class.getResource(SOM_GRANADA).toString(); | |
mpTick = new MediaPlayer(new Media(arquivoTick)); | |
// para evitar flood no play, só para qunado parar de tocar a mídia de vez | |
mpTick.setOnEndOfMedia(() -> { | |
mpTick.stop(); | |
}); | |
mpGranada = new MediaPlayer(new Media(arquivoGranada)); | |
} | |
private VBox constroiInterface() { | |
// inicializando nossos elementos de interface ativos | |
lblValor = new Label(); | |
sldValor = new Slider(0, 30 * 60, (30 * 60) / 2); | |
btnAcao = new Button(COMECAR); | |
// configura as marquinhas do slider | |
sldValor.setMajorTickUnit(100); | |
sldValor.setBlockIncrement(1f); | |
sldValor.setSnapToTicks(true); | |
sldValor.setShowTickMarks(true); | |
// configuraÇão do label | |
lblValor.setFont(Font.font("", FontWeight.BOLD, 90)); | |
// o valor do label muda se mexermos o slider | |
sldValor.valueProperty().addListener(l -> ajustaLabel()); | |
VBox vbRaiz = new VBox(50, lblValor, sldValor, btnAcao); | |
// alinha os elementos ao centro | |
vbRaiz.setAlignment(Pos.CENTER); | |
// registra o listener do botão | |
btnAcao.setOnAction(e -> acao()); | |
return vbRaiz; | |
} | |
private void ajustaLabel() { | |
// só pode ajudar assim se for ajuste manual do slider | |
if(sldValor.isDisable() == false) { | |
int segundos = sldValor.valueProperty().intValue(); | |
atualizaLabelTempo(segundos); | |
} | |
} | |
public void acao() { | |
if (btnAcao.getText().equals(COMECAR)) { | |
btnAcao.setText(PARAR); | |
int segundos = sldValor.valueProperty().intValue(); | |
final KeyFrame frameSegundos = new KeyFrame(Duration.seconds(1), | |
e -> atualizaValores()); | |
temporizador.getKeyFrames().setAll(frameSegundos); | |
temporizador.playFrom(Duration.seconds(segundos)); | |
tempoFaltante = segundos; | |
} else { | |
btnAcao.setText(COMECAR); | |
temporizador.stop(); | |
} | |
} | |
public void atualizaValores() { | |
if(tempoFaltante < 1) { | |
temporizador.stop(); | |
btnAcao.setText(COMECAR); | |
mpGranada.stop(); | |
mpGranada.play(); | |
return; | |
} | |
tempoFaltante--; | |
mpTick.play(); | |
atualizaLabelTempo(tempoFaltante); | |
sldValor.setValue(tempoFaltante); | |
} | |
private void atualizaLabelTempo(int segundos) { | |
String txtMinutos = String.format("%02d", segundos / 60); | |
String txtSegundos = String.format("%02d", segundos % 60); | |
lblValor.setText(txtMinutos + ":" + txtSegundos); | |
} | |
} |
7 de set. de 2016
[Especial] Eleições 2016: Mostrar dados abertos com JavaFX
É amigos, mais um ano de eleições. Nós, profissionais de TI, podemos criar aplicações que exploram dados abertos. Por exemplo, já falamos aqui de uma API para as eleições de 2014:
[Especial] Eleições 2014: Uma aplicação em JavaFX e apresentando uma API Java
Hoje vamos mostrar mais uma pequena aplicação para uma API criada por meu colega Pedro. Essa API permite ver as leis de vereadores da cidade de São José dos Campos, o Têmis.
Essa postagem também ajudara a fixar os conceitos em JavaFX que você deve ter aprendido ao ler esse blog. Caso ainda não seja tão experiente em JavaFX, siga os links que coloquei e leia os artigos, pois praticamente tudo que foi usado na aplicação já foi discutido aqui!
JavaFX é um framework essencialmente desktop. No entanto, você pode criar sua aplicação e migrar ela para um celular, como já falamos aqui. O JavaFX oferece todos os recursos para facilmente criar uma aplicação visualmente atraente e útil: tabelas, efeitos, gráficos e muito mais.
Além do mais, JavaFX é Java, ou seja, temos acesso a infinitas APIs para ajudar a acessar WEB Services, parse de arquivos, I/O, inteligência artificial e muito mais!
O funcionamento é simples, pois Nossa aplicação é simples e consiste de poucas classes. Veja abaixo:
[Especial] Eleições 2014: Uma aplicação em JavaFX e apresentando uma API Java
Hoje vamos mostrar mais uma pequena aplicação para uma API criada por meu colega Pedro. Essa API permite ver as leis de vereadores da cidade de São José dos Campos, o Têmis.
Essa postagem também ajudara a fixar os conceitos em JavaFX que você deve ter aprendido ao ler esse blog. Caso ainda não seja tão experiente em JavaFX, siga os links que coloquei e leia os artigos, pois praticamente tudo que foi usado na aplicação já foi discutido aqui!
Dados abertos e JavaFX
JavaFX é um framework essencialmente desktop. No entanto, você pode criar sua aplicação e migrar ela para um celular, como já falamos aqui. O JavaFX oferece todos os recursos para facilmente criar uma aplicação visualmente atraente e útil: tabelas, efeitos, gráficos e muito mais.
Além do mais, JavaFX é Java, ou seja, temos acesso a infinitas APIs para ajudar a acessar WEB Services, parse de arquivos, I/O, inteligência artificial e muito mais!
TemisFX
Essa é a nossa aplicação:
A pessoa pode escolher o vereador e ao clicar no mesmo, a lista de leis é carregada e podemos selecionar uma lei para ler a mesma. Veja o vídeo:
O funcionamento é simples, pois Nossa aplicação é simples e consiste de poucas classes. Veja abaixo:
- A classe App é quem estende de Application.
- As classes Alderman e Law são representantes da resposta da API do Têmis. Elas têm os mesmos campos do JSON para que possamos transformar a resposta do Web Service REST em objeto Java;
- TemisClientService é uma interface Java que representa as operações com a API. Já a classe TemisClientServiceImpl implementa essa interface e nela é que fazemos a chamado ao Web Service;
- TemisClientUI é onde toda a aplicação é criada. Essa classe estende de BorderPane e nela criamos a lista e os outros paineis da aplicação. A lista dos vereadores é customizada através da AldermanListCell.
Vamos na classe TemisClientUI. Como falamos, ela estende de BorderPane e nela temos as listas e os paines principais da aplicação. Veja o código e em seguida uma explicação sobre os diversos métodos:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package org.fxapps.temis.ui; | |
import java.util.function.Consumer; | |
import java.util.function.Supplier; | |
import org.fxapps.temis.model.Alderman; | |
import org.fxapps.temis.model.Law; | |
import org.fxapps.temis.service.TemisClientService; | |
import org.fxapps.temis.ui.cell.AldermanListCell; | |
import javafx.animation.FadeTransition; | |
import javafx.beans.InvalidationListener; | |
import javafx.beans.property.BooleanProperty; | |
import javafx.beans.value.ChangeListener; | |
import javafx.beans.value.ObservableValue; | |
import javafx.concurrent.Task; | |
import javafx.geometry.Orientation; | |
import javafx.scene.Cursor; | |
import javafx.scene.control.Alert; | |
import javafx.scene.control.Label; | |
import javafx.scene.control.ListView; | |
import javafx.scene.control.SplitPane; | |
import javafx.scene.layout.BorderPane; | |
import javafx.scene.layout.VBox; | |
import javafx.scene.web.WebView; | |
import javafx.util.Duration; | |
public class TemisClientUI extends BorderPane { | |
private final static String BASE_URL = "http://temis-server.herokuapp.com/"; | |
private TemisClientService service = TemisClientService.get(BASE_URL); | |
ListView<Alderman> aldermenList; | |
private ListView<Law> listLaws; | |
private Label lblTitleLaw; | |
private BooleanProperty loadingProperty; | |
public TemisClientUI(BooleanProperty loadingSomething) { | |
super(); | |
this.loadingProperty = loadingSomething; | |
this.disableProperty().bind(loadingSomething); | |
buildAll(); | |
} | |
private void buildAll() { | |
buildLawsList(); | |
buildAldermenList(); | |
setTop(aldermenList); | |
} | |
private void buildLawsList() { | |
lblTitleLaw = new Label(); | |
listLaws = new ListView<>(); | |
listLaws.setPrefWidth(100); | |
WebView lawText = new WebView(); | |
listLaws.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<Law>() { | |
@Override | |
public void changed(ObservableValue<? extends Law> observable, Law oldValue, Law newValue) { | |
if (newValue != null && newValue.getDesc() != null) { | |
lawText.getEngine().loadContent(newValue.getDesc()); | |
} | |
} | |
}); | |
SplitPane paneLaws = new SplitPane(listLaws, lawText); | |
VBox vbCenter = new VBox(paneLaws, lblTitleLaw); | |
setCenter(vbCenter); | |
} | |
private void buildAldermenList() { | |
aldermenList = new ListView<>(); | |
aldermenList.setCursor(Cursor.HAND); | |
aldermenList.setOrientation(Orientation.HORIZONTAL); | |
aldermenList.setCellFactory(param -> new AldermanListCell()); | |
aldermenList.setPrefHeight(220); | |
aldermenList.getSelectionModel().selectedItemProperty().addListener((InvalidationListener) observable -> { | |
Alderman alderman = aldermenList.getSelectionModel().getSelectedItem(); | |
loadLaws(alderman); | |
}); | |
loadAldermen(); | |
} | |
private void loadLaws(Alderman alderman) { | |
doAsyncWork(() -> service.laws(alderman, 0, 10000), laws -> { | |
fadeCenterPane(); | |
listLaws.getItems().setAll(laws); | |
lblTitleLaw.setText("Foram " + laws.size() + " leis votadas ou criados por " + alderman.getName()); | |
listLaws.getSelectionModel().select(0); | |
}); | |
} | |
private void fadeCenterPane(){ | |
FadeTransition ft = new FadeTransition(Duration.seconds(1)); | |
ft.setFromValue(0); | |
ft.setToValue(1); | |
ft.setAutoReverse(true); | |
ft.setNode(getCenter()); | |
ft.play(); | |
} | |
private void loadAldermen() { | |
doAsyncWork(service::aldermen, aldermen -> { | |
loadingProperty.set(false); | |
fadeCenterPane(); | |
aldermenList.getItems().setAll(aldermen); | |
aldermenList.getItems().stream().filter(a -> a.getName().equals("Mesa Diretora")).findFirst().ifPresent(a -> { | |
aldermenList.getSelectionModel().select(a); | |
aldermenList.scrollTo(a); | |
}); | |
}); | |
} | |
public <T extends Object> void doAsyncWork(Supplier<T> action, Consumer<T> success) { | |
Task<T> tarefaCargaPg = new Task<T>() { | |
@Override | |
protected T call() throws Exception { | |
loadingProperty.set(true); | |
return action.get(); | |
} | |
@Override | |
protected void succeeded() { | |
loadingProperty.set(false); | |
success.accept(getValue()); | |
} | |
@Override | |
protected void failed() { | |
loadingProperty.set(false); | |
getException().printStackTrace(); | |
Alert dialog = new Alert(Alert.AlertType.ERROR); | |
dialog.setTitle("Error"); | |
dialog.setHeaderText(null); | |
dialog.setResizable(true); | |
dialog.setContentText("Error acessando Têmis. Verifique se o mesmo está disponível."); | |
dialog.showAndWait(); | |
} | |
}; | |
Thread t = new Thread(tarefaCargaPg); | |
t.setDaemon(true); | |
t.start(); | |
} | |
} |
- Perceba que a aplicação recebe uma boolean property no construtor. A ideia é quando fizermos alguma chamada ao servidor, deixar a tela em modo de espera. Assim manipulamos essa propriedade toda vez que algo está sendo carregado; Por isso usamos uma property, podemos fazer binding dos elementos da app com essa property!
- Em seguida criamos o painel que contém as leis. Para garantir que quando uma lei é selecionada o conteúdo é mostrado, adicionar um listener ao elemento selecionado na lista de leis. Outro ponto interessante é que o texto da lei vem como HTML já. Assim usamos o browser do JavaFX para carregar aquele conteúdo em HTML. O painel das leis e o seu conteúdo fica em um SplitPane, para que o usuário possa dimensionar manualmente os paineis e facilitar a leitura;
- O método fadeCenterPane é interessante pois ele que faz um efeito de transição quando selecionamos um vereador. Simplesmente nele tocarmos uma FadeTransition;
- Já os vereadores ao serem carregados são colocado em uma ListView que tem a orientação horizontal e tem a célula customizada;
- Por fim, notem que a aplicação se comporta de maneira assíncrona. Isso é facilmente feito usando a Task do JavaFX. Como a mesma usa generics, conseguimos facilmente trocar os tipos usados na task, ou seja, usamos o mesmo método para pegar as leis e a lista de vereadores.
Conclusão
Apresentamos a aplicação TêmisFX e falamos sobre apresentação de dados abertos com JavaFX. O código completo está no github.
5 de set. de 2016
Listas e imagens: ListView
Nessa rápida postagem iremos falar sobre imagens dentro das ListView do JavaFX.
A classe javafx.scene.control.ListView permite mostrar listas de algum objeto e também que seja selecionado um dos items. A ListView é um Node. Ou seja, você pode adicionar ela em qualquer um dos painéis que já falamos aqui e assim mostrar a lista na sua aplicação. Veja abaixo um exemplo de ListView:

Como as tabelas, temos um método getItems que permite você acessar os itens e adicionar novos. A lista pode ser vertical, como acima, ou horizontal usando o método lista.setOrientation(Orientation.HORIZONTAL). Um exemplo:

Cada item é adicionado em uma ListCell<TIPO>, que você pode customizar. Uma forma simples de customizar é criando sua própria ListCell (uma classe que estende de ListCell) e sobreescrever o método updateItem. Ao sobreescrever o método, não se esqueça de chamar super.updateItem e seja feliz mexendo nas configurações da célula. Importante dizer que a célula pode ter o texto mudado e também o graphics, ou seja, você pode configurar uma imagem e o texto de acordo com o objeto que você colocou no TIPO.
O TIPO é o seguinte: Uma ListCell é parametrizada, ou seja, você informa o tipo de objeto que ela aceita. Assim você vai ter erros se tentar inserir outros tipos. Ou seja, se a sua lista é do tipo String, você tem erros ao inserir Integer nessa lista. O mesmo vale para a ListCell, você cria uma ListCell para o TIPO que escolher. Por fim, o tipo vale também para tipos customizados da sua aplicação.
Agora que você criou sua célula, como você informa que quer usar ela na sua lista? Bem, a resposta é simplesmente informar uma fábrica de células usando o método setCellFactory. Não se apavore, com Java 8 você simplesmente usa o seguinte: lista.setCellFactory(param -> new MinhaListCell())
O código abaixo esclarece tudo que mencionei:
Falamos brevemente de ListCell. Na verdade o objetivo dessa postagem é preparar terreno para a próxima que fala da aplicação que mostramos nos screenshots - até lá!
A ListView
A classe javafx.scene.control.ListView permite mostrar listas de algum objeto e também que seja selecionado um dos items. A ListView é um Node. Ou seja, você pode adicionar ela em qualquer um dos painéis que já falamos aqui e assim mostrar a lista na sua aplicação. Veja abaixo um exemplo de ListView:

Como as tabelas, temos um método getItems que permite você acessar os itens e adicionar novos. A lista pode ser vertical, como acima, ou horizontal usando o método lista.setOrientation(Orientation.HORIZONTAL). Um exemplo:

Customizando ListView
Cada item é adicionado em uma ListCell<TIPO>, que você pode customizar. Uma forma simples de customizar é criando sua própria ListCell (uma classe que estende de ListCell) e sobreescrever o método updateItem. Ao sobreescrever o método, não se esqueça de chamar super.updateItem e seja feliz mexendo nas configurações da célula. Importante dizer que a célula pode ter o texto mudado e também o graphics, ou seja, você pode configurar uma imagem e o texto de acordo com o objeto que você colocou no TIPO.
O TIPO é o seguinte: Uma ListCell é parametrizada, ou seja, você informa o tipo de objeto que ela aceita. Assim você vai ter erros se tentar inserir outros tipos. Ou seja, se a sua lista é do tipo String, você tem erros ao inserir Integer nessa lista. O mesmo vale para a ListCell, você cria uma ListCell para o TIPO que escolher. Por fim, o tipo vale também para tipos customizados da sua aplicação.
Agora que você criou sua célula, como você informa que quer usar ela na sua lista? Bem, a resposta é simplesmente informar uma fábrica de células usando o método setCellFactory. Não se apavore, com Java 8 você simplesmente usa o seguinte: lista.setCellFactory(param -> new MinhaListCell())
Um exemplo com imagem
O código abaixo esclarece tudo que mencionei:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// aqui a gente cria a nossa lista e seta o tipo de célua | |
ListView<Alderman> aldermenList = new ListView<>(); | |
aldermenList.setOrientation(Orientation.HORIZONTAL); | |
aldermenList.setCellFactory(param -> new AldermanListCell()); | |
aldermenList.setPrefHeight(220); | |
aldermenList.getItems().setAll(service.aldermen()); | |
// essa classe mostra nossa implementação de ListCel | |
import org.fxapps.temis.model.Alderman; | |
import org.fxapps.temis.ui.cache.ImageCache; | |
import javafx.geometry.Pos; | |
import javafx.scene.control.ContentDisplay; | |
import javafx.scene.control.ListCell; | |
import javafx.scene.image.Image; | |
import javafx.scene.image.ImageView; | |
public class AldermanListCell extends ListCell<Alderman> { | |
@Override | |
protected void updateItem(Alderman item, boolean empty) { | |
super.updateItem(item, empty); | |
Image image = ImageCache.noPhoto(); | |
if (item == null) { | |
setGraphic(photo(image)); | |
setText(""); | |
return; | |
} | |
if (item.getPhoto() != null) { | |
image = ImageCache.saveImageIfAbsent(item.getEmail(), item.getPhoto()); | |
} | |
setGraphic(photo(image)); | |
setText(item.getName()); | |
setGraphicTextGap(10); | |
setAlignment(Pos.CENTER); | |
setContentDisplay(ContentDisplay.TOP); | |
setPrefWidth(200); | |
} | |
static final ImageView photo(Image image) { | |
ImageView imageView = new ImageView(image); | |
imageView.setFitWidth(120); | |
imageView.setFitHeight(150); | |
imageView.setStyle("-fx-effect: dropshadow(three-pass-box, rgba(0,0,255,0.8), 30, 0, 0, 0);"); | |
return imageView; | |
} | |
} |
Conclusão
Falamos brevemente de ListCell. Na verdade o objetivo dessa postagem é preparar terreno para a próxima que fala da aplicação que mostramos nos screenshots - até lá!
20 de ago. de 2016
App de Sorteio usando JavaFX
Nessa postagem rápida, vamos mostrar uma aplicação útil, feita em JavaFX, que usamos nos eventos dos nossos JUG.
O código original foi feito em algumas horas, depois foi melhorado para ser integrado a um Web Service REST de outra aplicação, mas o uso básico é possível para sortear números sem sequer acessar o WS.
Veja o sorteio de números:
Após alguns sorteios você pode ver o que falta sortear e o que já foi sorteado:
Bem, era isso, uma postagem rápida mesmo! Espero que a aplicação seja util a alguém. O código está no github do JUG Vale.
O código original foi feito em algumas horas, depois foi melhorado para ser integrado a um Web Service REST de outra aplicação, mas o uso básico é possível para sortear números sem sequer acessar o WS.
Veja o sorteio de números:
Após alguns sorteios você pode ver o que falta sortear e o que já foi sorteado:
6 de mar. de 2016
Completar automaticamente ComboBox
Baseado nessa postagem do GUJ, eu criei um simples exemplo de auto complete em ComboBox. O que ele basicamente faz é:
- Quando o usuário digita com o ComboBox selecionado, ele trabalha com uma String temporária que armazena o texto;
- A cada tecla digitada, o conteúdo do combobox é mostrado e atualizado;
- Se backspace é digitado, atualizamos o filtro;
- A cada tecla digitada, mostramos os itens do combo box, quando o combo box é oculto, o filtro é limpo e a tooltip ocultada.
O resultado é mais ou menos o seguinte:
O código da classe e mais uma aplicação de exemplo está abaixo. Coloquei também no github, me mande PR para melhorar e há MUITAS melhorias a serem feitas, como suporte a espaço e acentos.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.util.stream.Stream; | |
import javafx.collections.FXCollections; | |
import javafx.collections.ObservableList; | |
import javafx.event.Event; | |
import javafx.scene.control.ComboBox; | |
import javafx.scene.control.Tooltip; | |
import javafx.scene.input.KeyCode; | |
import javafx.scene.input.KeyEvent; | |
import javafx.stage.Window; | |
/** | |
* | |
* Uses a combobox tooltip as the suggestion for auto complete and updates the | |
* combo box itens accordingly <br /> | |
* It does not work with space, space and escape cause the combobox to hide and | |
* clean the filter ... Send me a PR if you want it to work with all characters | |
* -> It should be a custom controller - I KNOW! | |
* | |
* @author wsiqueir | |
* | |
* @param <T> | |
*/ | |
public class ComboBoxAutoComplete<T> { | |
private ComboBox<T> cmb; | |
String filter = ""; | |
private ObservableList<T> originalItems; | |
public ComboBoxAutoComplete(ComboBox<T> cmb) { | |
this.cmb = cmb; | |
originalItems = FXCollections.observableArrayList(cmb.getItems()); | |
cmb.setTooltip(new Tooltip()); | |
cmb.setOnKeyPressed(this::handleOnKeyPressed); | |
cmb.setOnHidden(this::handleOnHiding); | |
} | |
public void handleOnKeyPressed(KeyEvent e) { | |
ObservableList<T> filteredList = FXCollections.observableArrayList(); | |
KeyCode code = e.getCode(); | |
if (code.isLetterKey()) { | |
filter += e.getText(); | |
} | |
if (code == KeyCode.BACK_SPACE && filter.length() > 0) { | |
filter = filter.substring(0, filter.length() - 1); | |
cmb.getItems().setAll(originalItems); | |
} | |
if (code == KeyCode.ESCAPE) { | |
filter = ""; | |
} | |
if (filter.length() == 0) { | |
filteredList = originalItems; | |
cmb.getTooltip().hide(); | |
} else { | |
Stream<T> itens = cmb.getItems().stream(); | |
String txtUsr = filter.toString().toLowerCase(); | |
itens.filter(el -> el.toString().toLowerCase().contains(txtUsr)).forEach(filteredList::add); | |
cmb.getTooltip().setText(txtUsr); | |
Window stage = cmb.getScene().getWindow(); | |
double posX = stage.getX() + cmb.getBoundsInParent().getMinX(); | |
double posY = stage.getY() + cmb.getBoundsInParent().getMinY(); | |
cmb.getTooltip().show(stage, posX, posY); | |
cmb.show(); | |
} | |
cmb.getItems().setAll(filteredList); | |
} | |
public void handleOnHiding(Event e) { | |
filter = ""; | |
cmb.getTooltip().hide(); | |
T s = cmb.getSelectionModel().getSelectedItem(); | |
cmb.getItems().setAll(originalItems); | |
cmb.getSelectionModel().select(s); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import javafx.application.Application; | |
import javafx.scene.Scene; | |
import javafx.scene.control.ComboBox; | |
import javafx.scene.control.Tooltip; | |
import javafx.scene.layout.StackPane; | |
import javafx.stage.Stage; | |
public class ComboBoxAutoCompleteTest extends Application { | |
private static final String[] LISTA = { "Abacate", "Abacaxi", "Ameixa", "Amora", "Araticum", "Atemoia", "Avocado", | |
"Banana prata", "Caju", "Cana descascada", "Caqui", "Caqui Fuyu", "Carambola", "Cereja", "Coco verde", | |
"Figo", "Figo da Índia", "Framboesa", "Goiaba", "Graviola", "Jabuticaba", "Jambo", "Jambo rosa", "Jambolão", | |
"Kino (Kiwano)", "Kiwi", "Laranja Bahia", "Laranja para suco", "Laranja seleta", "Laranja serra d’água", | |
"Laranjinha kinkan", "Lichia", "Lima da pérsia", "Limão galego", "Limão Taiti", "Maçã argentina", | |
"Maçã Fuji", "Maçã gala", "Maçã verde", "Mamão formosa", "Mamão Havaí", "Manga espada", "Manga Haden", | |
"Manga Palmer", "Manga Tommy", "Manga Ubá", "Mangostim", "Maracujá doce", "Maracujá para suco", "Melancia", | |
"Melancia sem semente", "Melão", "Melão Net", "Melão Orange", "Melão pele de sapo", "Melão redinha", | |
"Mexerica carioca", "Mexerica Murcote", "Mexerica Ponkan", "Mirtilo", "Morango", "Nectarina", | |
"Nêspera ou ameixa amarela", "Noni", "Pera asiática", "Pera portuguesa", "Pêssego", "Physalis", "Pinha", | |
"Pitaia", "Romã", "Tamarilo", "Tamarindo", "Uva red globe", "Uva rosada", "Uva Rubi", "Uva sem semente", | |
"Abobora moranga", "Abobrinha italiana", "Abobrinha menina", "Alho", "Alho descascado", | |
"Batata baroa ou cenoura amarela", "Batata bolinha", "Batata doce", "Batata inglesa", "Batata yacon", | |
"Berinjela", "Beterraba", "Cebola bolinha", "Cebola comum", "Cebola roxa", "Cenoura", "Cenoura baby", | |
"Couve flor", "Ervilha", "Fava", "Gengibre", "Inhame", "Jiló", "Massa de alho", "Maxixe", "Milho", | |
"Pimenta biquinho fresca", "Pimenta de bode fresca", "Pimentão amarelo", "Pimentão verde", | |
"Pimentão vermelho", "Quiabo", "Repolho", "Repolho roxo", "Tomate cereja", "Tomate salada", | |
"Tomate sem acidez", "Tomate uva", "Vagem", "Agrião", "Alcachofra", "Alface", "Alface americana", | |
"Almeirão", "Brócolis", "Broto de alfafa", "Broto de bambu", "Broto de feijão", "Cebolinha", "Coentro", | |
"Couve", "Espinafre", "Hortelã", "Mostarda", "Rúcula", "Salsa", "Ovos brancos", "Ovos de codorna", | |
"Ovos vermelhos" }; | |
public static void main(String[] args) { | |
launch(); | |
} | |
@Override | |
public void start(Stage stage) throws Exception { | |
ComboBox<String> cmb = new ComboBox<>(); | |
cmb.setTooltip(new Tooltip()); | |
cmb.getItems().addAll(LISTA); | |
stage.setScene(new Scene(new StackPane(cmb))); | |
stage.show(); | |
stage.setTitle("Filtrando um ComboBox"); | |
stage.setWidth(300); | |
stage.setHeight(300); | |
new ComboBoxAutoComplete<String>(cmb); | |
} | |
} |
28 de fev. de 2016
Criando controles customizados com JavaFX
Essa é uma tradução do artigo criado por Hendrik. Veja mais no blog GuiGarage
Por Hendrik Ebbers
Uma coisa que eu frequentemente faço com Java Swing é a customização de componentes e a criação de novos tipos de componentes. Um exemplo é o JGrid. Desde que JavaFX foi lançado, eu quis portar o JGrid. Depois de muitos experimentos e protótipos ruins, eu acho que encontrei a forma correta de fazer isso. As apresentações de Gerrit Grunwald e Jonathan Giles no JavaOne me ajudaram muito a fazer isso. As gravações dessas apresentações estão disponívels online (link e link - NOTA: links quebrados), então eu recomendo quem estiver interessado nesse tópico a investir algum tempo e assistir elas.
Como componente de interface no JavaFX é composto por um control(controle), uma skin("casca") e um behaviour(comportamento). Em um caso ideal, há também a parte do CSS.

A melhor forma de começar é criar uma classe que herda de javafx.scene.control.Control. Essa classe é basicamente compara ao JComponent. O Control deve ter as propriedades do componente e atuar como a classe principal dele porque instâncias dessa classe serão depois criadas automaticamente no códido da aplicação e serão adicionadas para a árvore da interface com o usuário(UI):
MyCustomControl myControl = new MyCustomControl();
panel.getChildren().add(myControl);
Quando programando componentes Swing na forma correta você coloca tudo que depende da visualização ou a interação com o usuário em uma classe de UI (veja LabelUI por exemplo). JavaFX vai além e disponibiliza uma classe de skin para toda a visualização e o layout relacionado com o código e a classe behaviour para todas as interações com o usuário.

Para fazer isso em JavaFX você deve entender como as classes se relacionam. Aqui está um pequeno diagrama que mostra as relações entre essas classes:

public class MyCustomControlBehavior extends BehaviorBase {
public MyCustomControlSkin(MyCustomControl control) {
super(control, new MyCustomControlBehavior(control));
}
}
Alguns de vocês podem ficar confusos quando ver o pacote da classe BehavioyBase. No momento é uma API privada e normalmente você não deveria usar essas classes no seu código, mas os caras da Oracle sabem desse problema e irão providencias a classe BehaviorBase como uma API com o JavaFX 8. Então uma boa prática é usar essa classe privada agora e refatorar assim que o JavaFX for lançado(*).
NOTA: O javaFX 8 foi lançado já!
Por Hendrik Ebbers
Uma coisa que eu frequentemente faço com Java Swing é a customização de componentes e a criação de novos tipos de componentes. Um exemplo é o JGrid. Desde que JavaFX foi lançado, eu quis portar o JGrid. Depois de muitos experimentos e protótipos ruins, eu acho que encontrei a forma correta de fazer isso. As apresentações de Gerrit Grunwald e Jonathan Giles no JavaOne me ajudaram muito a fazer isso. As gravações dessas apresentações estão disponívels online (link e link - NOTA: links quebrados), então eu recomendo quem estiver interessado nesse tópico a investir algum tempo e assistir elas.
Primeiros passos
Como componente de interface no JavaFX é composto por um control(controle), uma skin("casca") e um behaviour(comportamento). Em um caso ideal, há também a parte do CSS.

A melhor forma de começar é criar uma classe que herda de javafx.scene.control.Control. Essa classe é basicamente compara ao JComponent. O Control deve ter as propriedades do componente e atuar como a classe principal dele porque instâncias dessa classe serão depois criadas automaticamente no códido da aplicação e serão adicionadas para a árvore da interface com o usuário(UI):
MyCustomControl myControl = new MyCustomControl();
panel.getChildren().add(myControl);
Quando programando componentes Swing na forma correta você coloca tudo que depende da visualização ou a interação com o usuário em uma classe de UI (veja LabelUI por exemplo). JavaFX vai além e disponibiliza uma classe de skin para toda a visualização e o layout relacionado com o código e a classe behaviour para todas as interações com o usuário.

Para fazer isso em JavaFX você deve entender como as classes se relacionam. Aqui está um pequeno diagrama que mostra as relações entre essas classes:

Criando o comportamento (classe Behaviour)
Se o seu componente é somente para visualização de dados e não há interação, é baste simples criar um comportamento. Assim, você simplesmente precisa criar um classe que herde de com.sun.javafx.scene.control.behavior.BehaviorBase.public class MyCustomControlBehavior extends BehaviorBase {
public MyCustomControlSkin(MyCustomControl control) {
super(control, new MyCustomControlBehavior(control));
}
}
Alguns de vocês podem ficar confusos quando ver o pacote da classe BehavioyBase. No momento é uma API privada e normalmente você não deveria usar essas classes no seu código, mas os caras da Oracle sabem desse problema e irão providencias a classe BehaviorBase como uma API com o JavaFX 8. Então uma boa prática é usar essa classe privada agora e refatorar assim que o JavaFX for lançado(*).
NOTA: O javaFX 8 foi lançado já!
Criando a casca (classe Skin)
Depois que a classe de comportamento é criada, podemos dar uma olhada na skin. Sua classe de skin vai muito provavelmente herdar de com.sun.javafx.scene.control.skin.BaseSkin e criar um novo comportamento para o seu Control. O código normalmente se parece com o seguinte:
public class MyCustomControlSkin extends SkinBase{
public MyCustomControlSkin(MyCustomControl control) {
super(control, new MyCustomControlBehavior(control));
}
} |
Criando o controle
A última classe que precisamos é o controle. Primeiro criamos uma classe vazia:
public class MyCustomControl extends Control {
public MyCustomControl() { }
}
Nesse ponto nós temos uma falha nas dependências das nossas classes. A skin conhece o behavior e o control. Aqui tudo parece correto, no entanto, no código da aplicação você vaim simplesmente criar um novo controle e usar como eu mostrei antes. O problema é que a classe de controle não sabe nada sobre a skin ou o behavior. Esse foi um dos maiores desafios quando eu estava aprendendo JavaFX.
Juntando as coisas
O que inicialmente parece um grande problema, é na verdade parte do poder disponibilizado pelo JavaFX. Com JavaFX é muito fácil criar diferente visualizações (skins) para os controles. Nessa parte você pode customizar a aparência dos componentes usando CSS. Como a skin é a parte principal da aparência, ela deve ser definida no CSS também. Assim, invés criar um objeto skin para o controle manualmente, você simplesmente define a classe skin que deve ser usada no seu controle. A criação e tudo mais é automaticamente feito pelas APIs do JavaFX. Para fazer isso, você deve ligar o seu controle a uma classe CSS.
Primeiramente, crie um novo arquivo no seu projeto. Eu acredito que a melhor prática é usar o mesmo pacote que o controle está e não um arquivo CSS criado sob src/main/resource:

Dentro do seu CSS você deve especificar um novo seletor para o seu componente e adicionar a skin como uma propriedade. Isso deveria parecer como o seguinte exemplo:
.custom-control {
-fx-skin: "com.guigarage.customcontrol.MyCustomControlSkin";
}
Assim que você criar o CSS, você tem que definir o mesmo no seu controle. Portanto, você deve configurar o caminho para o arquivo css e o seletor dos seus componentes:
public class MyCustomControl extends Control {
public MyCustomControl() {
getStyleClass().add("custom-control");
}
protected String getUserAgentStylesheet() {
return MyCustomControl.class.getResource("customcontrol.css").toExternalForm();
}
}
Depois que todas essas coisas foram feitas corretamente, JavaFX vai criar uma instância de skin para o seu controle. Você não precisa se preocupar com instanciação ou o mecanismo de depdendëncias. Nesse ponto eu gostaria de agradecer Jonathan Giles, que dedicou um tempo para codificar a integração do css para o gridfx comigo e explicou todo o mecanismo e benefícios.
Acesso à Skin e Behavior
Normalmente não há a necessidade de acessar a Skin e o Behavior através do controle, mas se você precisar disso, você pode acessá-los da seguinte forma:

Como controler.getSkin() rece um javafx.scene.control.Skin e não a classe SkinBase você deve fazer um cast se precisar do behaviour:
((SkinBase)getSkin()).getBehavior();
Uma solução para os que odeiam CSS
Para alguns de vocës esse mecanimos parece um pouco exagerado. Talvez vocë simplesmente precise de um controle na sua aplicação e você não planeja usar uma skin com CSS e fazer tudo isso. Para esse caso de uso, há um ótimo workaround na API do JavaFX. Você pode ignorar toda a parte do CSS e configurar a classe skin no seu controle usando código:
public class MyCustomControl extends Control {
public MyCustomControl() {
setSkinClassName(MyCustomControlSkin.class.getName());
}
}
O benefício dessa forma de trabalho é que refatorar pacotes e nome de classes não vai quebrar o seu código e você não precisa de um arquivo CSS extra. Por outro lado, há uma grande desvantagem. Você não poderia usar skins definidas por CSS em qualquer extensão do seu controle. Acho que toda API pública, como o GridFX, deveria usar a forma com CSS. Em alguns casos de uso internos, a forma hardcoded(essa que falamos agora) é muito mais rápida.
Conclusão
Agora nós criamos um controle, uma skin e um behavior que está funcionando bem e pode ser adicionado na árvore UI da sua aplicação. Mas, como acontece com o Swing, simplesmente herdar de JComponent não levará você a ver nada na tela. Então o próximo passo é adicionar estilo e organizar o layout do seu componente. Irei falar disso no meu próximo artigo.
Se você quiser dar uma olhada em algum componente existente veja jgridfx ou JFXtras. No jgridfx os seguintes arquivos batem com esse artigo:
- com.guigarage.fx.grid.GridView (control)
- com.guigarage.fx.grid.skin.GridViewSkin (skin)
- com.guigarage.fx.grid.behavior.GridViewBehavior (behavior)
- /src/main/resources/com/guigarage/fx/grid/gridview.css (css)
17 de fev. de 2016
Um CRUD com JavaFX usando banco de dados
Devido ao sucesso da postagem anterior Um CRUD com JavaFX, resolvi estender a mesma para falar sobre o acesso a um banco de dados MySQL utilizando a mesma estrutura do artigo anterior.
Acessando banco de dados com Java
Esse é talvez um dos assuntos mais buscados por quem está aprendendo Java. Há muitas referências a isso na internet, inclusive uma série de artigos que o criado desse blog escreveu há mais de 5 anos atrás para o JavaFree, veja: Acessando dados com Java: 1º parte - Simples Dao - Acessando Dados com Java: Parte 2 - Prevendo problemas - Acessando Dados com Java: Parte 3 - Hibernate Annotations
Preparando o banco
O primeiro passo nosso é garantir que você tenha o MySQL instalado(que depende do sistema operacional sendo utilizado, Linux ou outros) e uma vez feito isso, entre no MySQL e então seguir alguns passos. Para esse artigo foi criado o seguinte:
- Usuário contas_app com senha aprendajavafx;
CREATE USER 'contas_app'@'localhost' IDENTIFIED BY 'aprendajavafx';
- Banco de dados contas crud-contas;
CREATE DATABASE `crud-contas` DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci
- Garanta os privilégios do usuário contas_app para o banco crud-contas
mysql> use crud-contas
Database changed
mysql> GRANT ALL PRIVILEGES ON * . * TO 'contas_app'@'localhost';
- Tabela Contas com os campos que precisamos
create table contas(
id int auto_increment,
concessionaria varchar(30) not null,
descricao varchar(1000) not null,
data_vencimento date not null,
primary key(id)
);
Notem que o ID é auto_increment, ou seja, o MySQL vai dar o ID para nós. Se inserirmos alguns valores de teste, podemos notar que o ID já foi preenchido:
Criando uma classe para acesso ao banco de dados
Conforme falamos no artigo anterior, para trocar onde os dados serão armazenados nós iremos criar uma implementação de ContasService e trocar o tipo retornado no método getNewInstance.
Para falarmos com o nosso banco de dados MySQL, precisamos ter o Driver JDBC no classpath da aplicação, pois é ele quem possibilita a comunicação de uma aplicação Java com o banco. Por isso, baixe o driver JDBC do MySQL e coloque no classpath da aplicação.
A implementação do banco de dados simplesmente envia comandos SQL para o MySQL invés de abrir e manipular arquivos, assim temos que:
* Salvar significa realizar um INSERT com os dados da conta passada;
* Buscar é trazer dados usando SELECT e transformar em uma lista de contas;
* Apagar é realizar um DELETE;
* Por fim atualizar é a realização de um UPDATE.
Claro que tudo pode ser facilitado usando um framework que faz o mapeamento de objetos para o modelo relacional de banco de dados, como o Hibernate, mas por hoje vamos ficar no básico! Finalmente, veja como ficou nossa classe ContasDBService (lembre-se que todo o código pode ser encontrado no github):
A implementação do banco de dados simplesmente envia comandos SQL para o MySQL invés de abrir e manipular arquivos, assim temos que:
* Salvar significa realizar um INSERT com os dados da conta passada;
* Buscar é trazer dados usando SELECT e transformar em uma lista de contas;
* Apagar é realizar um DELETE;
* Por fim atualizar é a realização de um UPDATE.
Claro que tudo pode ser facilitado usando um framework que faz o mapeamento de objetos para o modelo relacional de banco de dados, como o Hibernate, mas por hoje vamos ficar no básico! Finalmente, veja como ficou nossa classe ContasDBService (lembre-se que todo o código pode ser encontrado no github):
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package org.aprendendojavafx.crud.service.impl; | |
import java.sql.Connection; | |
import java.sql.DriverManager; | |
import java.sql.PreparedStatement; | |
import java.sql.ResultSet; | |
import java.sql.SQLException; | |
import java.text.ParseException; | |
import java.text.SimpleDateFormat; | |
import java.util.ArrayList; | |
import java.util.Date; | |
import java.util.List; | |
import org.aprendendojavafx.crud.model.Conta; | |
import org.aprendendojavafx.crud.service.ContasService; | |
/** | |
* Faz as operações de CRUD usando banco de dados MySQL. Para rodar essa classe | |
* vocë precisa <br /> | |
* - Certificar-se que o driver do MySQL está no classpath da aplicação - Criar | |
* o banco de dados e atualizar o código abaixo de acordo com o seu banco - | |
* Criar a tabela contas. | |
* | |
* Se houver qualquer erro, o programa irá sair, assim, cheque os logs para ver o erro. | |
* | |
* @author wsiqueir | |
* | |
*/ | |
public class ContasDBService implements ContasService { | |
// dados para acesso ao banco, atualize de acordo com o seu banco de dados | |
final String USUARIO = "contas_app"; | |
final String SENHA = "aprendajavafx"; | |
final String URL_BANCO = "jdbc:mysql://localhost:3306/crud-contas"; | |
// constantes de acesso | |
final String CLASSE_DRIVER = "com.mysql.jdbc.Driver"; | |
// comandos | |
final String INSERIR = "INSERT INTO contas(concessionaria, descricao, data_vencimento) VALUES(?, ?, STR_TO_DATE(?, '%d/%m/%Y'))"; | |
final String ATUALIZAR = "UPDATE contas SET concessionaria=?, descricao=?, data_vencimento = STR_TO_DATE(?, '%d/%m/%Y') WHERE id = ?"; | |
final String BUSCAR = "SELECT id, concessionaria, descricao, DATE_FORMAT(data_vencimento, %d/%m/%Y') FROM contas WHERE ID = ?"; | |
final String BUSCAR_TODOS = "SELECT id, concessionaria, descricao, DATE_FORMAT(data_vencimento, '%d/%m/%Y') FROM contas"; | |
final String APAGAR = "DELETE FROM contas WHERE id = ?"; | |
// tratamento de data | |
final String FORMATO_DATA = "dd/MM/yyyy"; | |
final SimpleDateFormat FORMATADOR = new SimpleDateFormat(FORMATO_DATA); | |
@Override | |
public void salvar(Conta conta) { | |
try { | |
Connection con = conexao(); | |
PreparedStatement salvar = con.prepareStatement(INSERIR); | |
String dateStr = FORMATADOR.format(conta.getDataVencimento()); | |
salvar.setString(1, conta.getConcessionaria()); | |
salvar.setString(2, conta.getDescricao()); | |
salvar.setString(3, dateStr); | |
salvar.executeUpdate(); | |
salvar.close(); | |
con.close(); | |
} catch (Exception e) { | |
e.printStackTrace(); | |
System.err.println("ERROR SALVANDO CONTA"); | |
System.exit(0); | |
} | |
} | |
@Override | |
public List<Conta> buscarTodas() { | |
List<Conta> contas = new ArrayList<>(); | |
try { | |
Connection con = conexao(); | |
PreparedStatement buscarTodos = con.prepareStatement(BUSCAR_TODOS); | |
ResultSet resultadoBusca = buscarTodos.executeQuery(); | |
while (resultadoBusca.next()) { | |
Conta conta = extraiConta(resultadoBusca); | |
contas.add(conta); | |
} | |
buscarTodos.close(); | |
con.close(); | |
} catch (Exception e) { | |
e.printStackTrace(); | |
System.err.println("ERROR BUSCANDO TODAS AS CONTAS."); | |
System.exit(0); | |
} | |
return contas; | |
} | |
@Override | |
public Conta buscaPorId(int id) { | |
Conta conta = null; | |
try { | |
Connection con = conexao(); | |
PreparedStatement buscar = con.prepareStatement(BUSCAR); | |
buscar.setInt(1, id); | |
ResultSet resultadoBusca = buscar.executeQuery(); | |
resultadoBusca.next(); | |
conta = extraiConta(resultadoBusca); | |
buscar.close(); | |
con.close(); | |
} catch (Exception e) { | |
e.printStackTrace(); | |
System.err.println("ERROR BUSCANDO CONTA COM ID " + id); | |
System.exit(0); | |
} | |
return conta; | |
} | |
@Override | |
public void apagar(int id) { | |
try { | |
Connection con = conexao(); | |
PreparedStatement apagar = con.prepareStatement(APAGAR); | |
apagar.setInt(1, id); | |
apagar.executeUpdate(); | |
apagar.close(); | |
con.close(); | |
} catch (Exception e) { | |
e.printStackTrace(); | |
System.err.println("ERROR APAGANDO CONTA COM ID " + id); | |
System.exit(0); | |
} | |
} | |
@Override | |
public void atualizar(Conta conta) { | |
try { | |
Connection con = conexao(); | |
PreparedStatement atualizar = con.prepareStatement(ATUALIZAR); | |
String dateStr = FORMATADOR.format(conta.getDataVencimento()); | |
atualizar.setString(1, conta.getConcessionaria()); | |
atualizar.setString(2, conta.getDescricao()); | |
atualizar.setString(3, dateStr); | |
atualizar.setInt(4, conta.getId()); | |
atualizar.executeUpdate(); | |
atualizar.close(); | |
con.close(); | |
} catch (Exception e) { | |
e.printStackTrace(); | |
System.err.println("ERROR ATUALIZANDO CONTA COM ID " + conta.getId()); | |
System.exit(0); | |
} | |
} | |
// abre uma nova conexão com o banco de dados. Se algum erro for lançado | |
// aqui, verifique o erro com atenção e se o banco está rodando | |
private Connection conexao() { | |
try { | |
Class.forName(CLASSE_DRIVER); | |
return DriverManager.getConnection(URL_BANCO, USUARIO, SENHA); | |
} catch (Exception e) { | |
e.printStackTrace(); | |
if(e instanceof ClassNotFoundException) { | |
System.err.println("VERIFIQUE SE O DRIVER DO BANCO DE DADOS ESTÁ NO CLASSPATH"); | |
} else { | |
System.err.println("VERIFIQUE SE O BANCO ESTÁ RODANDO E SE OS DADOS DE CONEXÃO ESTÃO CORRETOS"); | |
} | |
System.exit(0); | |
// o sistema deverá sair antes de chegar aqui... | |
return null; | |
} | |
} | |
// extrain o objeto Conta do result set | |
private Conta extraiConta(ResultSet resultadoBusca) throws SQLException, ParseException { | |
Conta conta = new Conta(); | |
conta.setId(resultadoBusca.getInt(1)); | |
conta.setConcessionaria(resultadoBusca.getString(2)); | |
conta.setDescricao(resultadoBusca.getString(3)); | |
Date dataVencimento = FORMATADOR.parse(resultadoBusca.getString(4)); | |
conta.setDataVencimento(dataVencimento); | |
return conta; | |
} | |
} |
15 de fev. de 2016
Um CRUD com JavaFX
Finalmente!!! Uma das postagens mais esperadas desse blog é o CRUD!
VEJA A PARTE 2 COM BANCO DE DADOS
Todo programador já fez(ou vai fazer) um CRUD na vida e é, no geral, a forma que aprendemos conceitos básicos de uma tecnologia. Nesse artigo vamos mostrar um simples CRUD em JavaFX para explorar algumas características da tecnologia.
CRUD é uma sigla que vem das operações básicas que podemos fazer um algo armazenado em uma fonte de dados:
Create: Criar uma nova instância de algo na fonte dos dados;
Retrieve: Trazer os dados já armazenados;
Update: Atualizar dados já armazenados;
Delete: Apagar.
A fonte de dados pode ser um banco de dados(MySQL, MariaDB, Postgres, etc), um arquivo(arquivos em diversos formatos, como CSV), um Web Service (que após chamado, salva os dados em algum lugar) ou até mesmo a própria memória dinâmica do computador(sendo que os dados se perdem quando a aplicação é fechada). Aqui vamos usar um arquivo CSV, uma forma simples de armazenar objetos em um arquivo de texto.
O motivo de não usarmos um banco de dados tradicional é que o foco do artigo é mostrar JavaFX. Um banco de dados comum implica em termos que falar de SQL, conexão com o banco, select, etc, isso vai fugir do foco.
Nossa aplicação simples vai ser um CRUD de Contas. Sim, bills, contas! Pagamos todo mês contas de luz, água, gás, faturas, etc, etc, argh. Para representar a conta, criamos um objeto Java chamado Conta com três atributos: id (gerado automaticamente para controle interno), concessionária, descrição e data de vencimento. É isso, simples, não? Em breve mostramos o código dessa classe, mas agoras que conhecemos os campos, veja a telinha que modelamos usando o SceneBuilder que gerou um FXML (se não sabe o que é isso, veja esse artigo sobre FXML)

Começamos com uma tabela com três colunas representando os campos, após a tabela temos os campos de texto para entrada do nome, descrição e um campo para entrada de data do tipo DatePicker, que infelizmente ainda não abordamos aqui por ter sido adicionado apenas no JavaFX 8, e por fim os botões de ações.
A lógica da aplicação é a seguinte:
VEJA A PARTE 2 COM BANCO DE DADOS
Todo programador já fez(ou vai fazer) um CRUD na vida e é, no geral, a forma que aprendemos conceitos básicos de uma tecnologia. Nesse artigo vamos mostrar um simples CRUD em JavaFX para explorar algumas características da tecnologia.
Definição de CRUD
CRUD é uma sigla que vem das operações básicas que podemos fazer um algo armazenado em uma fonte de dados:
Create: Criar uma nova instância de algo na fonte dos dados;
Retrieve: Trazer os dados já armazenados;
Update: Atualizar dados já armazenados;
Delete: Apagar.
Onde armazenar os dados?
A fonte de dados pode ser um banco de dados(MySQL, MariaDB, Postgres, etc), um arquivo(arquivos em diversos formatos, como CSV), um Web Service (que após chamado, salva os dados em algum lugar) ou até mesmo a própria memória dinâmica do computador(sendo que os dados se perdem quando a aplicação é fechada). Aqui vamos usar um arquivo CSV, uma forma simples de armazenar objetos em um arquivo de texto.
O motivo de não usarmos um banco de dados tradicional é que o foco do artigo é mostrar JavaFX. Um banco de dados comum implica em termos que falar de SQL, conexão com o banco, select, etc, isso vai fugir do foco.
Nosso CRUD
Nossa aplicação simples vai ser um CRUD de Contas. Sim, bills, contas! Pagamos todo mês contas de luz, água, gás, faturas, etc, etc, argh. Para representar a conta, criamos um objeto Java chamado Conta com três atributos: id (gerado automaticamente para controle interno), concessionária, descrição e data de vencimento. É isso, simples, não? Em breve mostramos o código dessa classe, mas agoras que conhecemos os campos, veja a telinha que modelamos usando o SceneBuilder que gerou um FXML (se não sabe o que é isso, veja esse artigo sobre FXML)
Começamos com uma tabela com três colunas representando os campos, após a tabela temos os campos de texto para entrada do nome, descrição e um campo para entrada de data do tipo DatePicker, que infelizmente ainda não abordamos aqui por ter sido adicionado apenas no JavaFX 8, e por fim os botões de ações.
A lógica da aplicação é a seguinte:
- A tabela tem ID tblContas e três colunas: clConc, clDesc e clVenc. Elas são populadas com os dados de um objeto do tipo Conta;
- Os campos de texto e o campo de data tem um (txtConc, txtDesc, dpVenc) serão injetados no controller para que possamos saber o valor que o usuário entrou;
- Cada um dos botões ação:
- salvar: salva o objeto de acordo com a informação entrada pelo usuário. Não está habilitado quando um campo está selecionado na tabela;
- atualizar: Só está habilitado quando selecionamos uma linha da coluna e permite atualizar os dados dessa linha (os campos de entrada de dados vão ser atualizados com o valor selecionado para serem modificados pelo usuário);
- apagar: apaga a linha selecionada;
- limpar: limpa o campo selecionado atualmente.
As operações e os elementos da tela ficam na classe ContasController. Código que veremos já já.
Fazendo as operações com o banco de dados
As operações em sí com o banco ficam na interface ContasService. Ela contém os métodos salvar, que recebe uma instância de conta a ser salva, atualizar, que recebe a conta já salva para ser atualizada, apagar, que apaga uma conta e buscarTodos, que retorna todas as contas selecionadas. É nessa classe que fazemos as operações.
Todo o código poderia ficar dentro do controller, MAS ISSO É COISA FEIA, temos que definir os métodos em uma interface e, vejam que interessante, usando a capacidade do Java 8 de definir métodos padrões, criamos um método getInstance para retornar a implementação que queremos dessa interface. Assim, criamos a clase ContasCSVService, que é uma implementação da interface, e retornamos uma nova instância nesse método! Podemos, obviamente, criar uma interface, por exemplo ContasBDService que faria a mesma coisa, mas que invés de usar um arquivo CSV, se comunica com um banco de dados, a mesma ideia poderia ser aplicada para um arquivo XLS, como ContasXLSService, e por aí vai. O código do controller, que os métodos da interface, não iria sofrer nenhuma modificação, pois só precisaríamos trocar a intância de ContasService retornada no método getNewInstance! (claro que com CDI e outras tecnologias, sequer criar manualmente precisaríamos). Veja a nossa interface
VAMOS AO CÓDIGO!
Agora, depois desse mooonnnntteee de papo, vamos ao código. Claro que se você leu acima com atenção, vai ser muito simples de entender tudo, mas mesmo assim o código está comentado na medida do possível.
Não vou colocar o todo código na postagem, pois já está muito extensa, mas veja o código da classe Conta, da interface ContasService e da classe ContasController. O restante está no github!
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package org.aprendendojavafx.crud; | |
import java.util.Date; | |
/** | |
* | |
* Nossa classe de modelo do objeto que "sofrerá" as operações de CRUD | |
* @author wsiqueir | |
* | |
*/ | |
public class Conta { | |
private int id; | |
private String concessionaria; | |
private String descricao; | |
private Date dataVencimento; | |
// gets e sets omitidos.. | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package org.aprendendojavafx.crud; | |
import java.net.URL; | |
import java.time.LocalDate; | |
import java.time.LocalDateTime; | |
import java.time.ZoneId; | |
import java.util.Date; | |
import java.util.ResourceBundle; | |
import javafx.beans.binding.BooleanBinding; | |
import javafx.fxml.FXML; | |
import javafx.fxml.Initializable; | |
import javafx.scene.control.Button; | |
import javafx.scene.control.DatePicker; | |
import javafx.scene.control.TableColumn; | |
import javafx.scene.control.TableView; | |
import javafx.scene.control.TextField; | |
import javafx.scene.control.cell.PropertyValueFactory; | |
/** | |
* | |
* O controller da aplicação, onde a mágica acontece | |
* @author wsiqueir | |
* | |
*/ | |
public class ContasController implements Initializable { | |
@FXML | |
private TableView<Conta> tblContas; | |
@FXML | |
private TableColumn<Conta, String> clConsc; | |
@FXML | |
private TableColumn<Conta, String> clDesc; | |
@FXML | |
private TableColumn<Conta, Date> clVenc; | |
@FXML | |
private TextField txtConsc; | |
@FXML | |
private TextField txtDesc; | |
@FXML | |
private DatePicker dpVencimento; | |
@FXML | |
private Button btnSalvar; | |
@FXML | |
private Button btnAtualizar; | |
@FXML | |
private Button btnApagar; | |
@FXML | |
private Button btnLimpart; | |
private ContasService service; | |
// Esse método é chamado ao inicializar a aplicação, igual um construtor. Ele vem da interface Initializable | |
@Override | |
public void initialize(URL location, ResourceBundle resources) { | |
service = ContasService.getNewInstance(); | |
configuraColunas(); | |
configuraBindings(); | |
atualizaDadosTabela(); | |
} | |
// métodos públicos chamados quando o botão é clicado | |
public void salvar() { | |
Conta c = new Conta(); | |
pegaValores(c); | |
service.salvar(c); | |
atualizaDadosTabela(); | |
} | |
public void atualizar() { | |
Conta c = tblContas.getSelectionModel().getSelectedItem(); | |
pegaValores(c); | |
service.atualizar(c); | |
atualizaDadosTabela(); | |
} | |
public void apagar() { | |
Conta c = tblContas.getSelectionModel().getSelectedItem(); | |
service.apagar(c.getId()); | |
atualizaDadosTabela(); | |
} | |
public void limpar() { | |
tblContas.getSelectionModel().select(null); | |
txtConsc.setText(""); | |
txtDesc.setText(""); | |
dpVencimento.setValue(null); | |
} | |
// métodos privados do controller | |
// pega os valores entrados pelo usuário e adiciona no objeto conta | |
private void pegaValores(Conta c) { | |
c.setConcessionaria(txtConsc.getText()); | |
c.setDescricao(txtDesc.getText()); | |
c.setDataVencimento(dataSelecionada()); | |
} | |
// método utilitário para pega a data que foi selecionada (que usa o tipo novo do java 8 LocalDateTime) | |
private Date dataSelecionada() { | |
LocalDateTime time = dpVencimento.getValue().atStartOfDay(); | |
return Date.from(time.atZone(ZoneId.systemDefault()).toInstant()); | |
} | |
// chamado quando acontece alguma operação de atualização dos dados | |
private void atualizaDadosTabela() { | |
tblContas.getItems().setAll(service.buscarTodas()); | |
limpar(); | |
} | |
// configura as colunas para mostrar as propriedades da classe Conta | |
private void configuraColunas() { | |
clConsc.setCellValueFactory(new PropertyValueFactory<>("concessionaria")); | |
clDesc.setCellValueFactory(new PropertyValueFactory<>("descricao")); | |
clVenc.setCellValueFactory(new PropertyValueFactory<>("dataVencimento")); | |
} | |
// configura a lógica da tela | |
private void configuraBindings() { | |
// esse binding só e false quando os campos da tela estão preenchidos | |
BooleanBinding camposPreenchidos = txtConsc.textProperty().isEmpty().or(txtDesc.textProperty().isEmpty()) | |
.or(dpVencimento.valueProperty().isNull()); | |
// indica se há algo selecionado na tabela | |
BooleanBinding algoSelecionado = tblContas.getSelectionModel().selectedItemProperty().isNull(); | |
// alguns botões só são habilitados se algo foi selecionado na tabela | |
btnApagar.disableProperty().bind(algoSelecionado); | |
btnAtualizar.disableProperty().bind(algoSelecionado); | |
btnLimpart.disableProperty().bind(algoSelecionado); | |
// o botão salvar só é habilitado se as informações foram preenchidas e não tem nada na tela | |
btnSalvar.disableProperty().bind(algoSelecionado.not().or(camposPreenchidos)); | |
// quando algo é selecionado na tabela, preenchemos os campos de entrada com os valores para o | |
// usuário editar | |
tblContas.getSelectionModel().selectedItemProperty().addListener((b, o, n) -> { | |
if (n != null) { | |
LocalDate data = null; | |
data = n.getDataVencimento().toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); | |
txtConsc.setText(n.getConcessionaria()); | |
txtDesc.setText(n.getDescricao()); | |
dpVencimento.setValue(data); | |
} | |
}); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package org.aprendendojavafx.crud; | |
import java.io.IOException; | |
import java.nio.file.Files; | |
import java.nio.file.Path; | |
import java.nio.file.Paths; | |
import java.text.ParseException; | |
import java.text.SimpleDateFormat; | |
import java.util.Date; | |
import java.util.List; | |
import java.util.stream.Collectors; | |
/** | |
* | |
* Uma implementação do ContasService para lidar com arquivo CSV | |
* @author wsiqueir | |
* | |
*/ | |
public class ContasCSVService implements ContasService { | |
// divisor de colunas no arquivo | |
private static final String SEPARADOR = ";"; | |
// o caminho para o arquivo deve ser selecionado aqui | |
private static final Path ARQUIVO_SAIDA = Paths.get("./dados.csv"); | |
// os dados do arquivo | |
private List<Conta> contas; | |
// formato de data usado no arquivo | |
final SimpleDateFormat formatoData = new SimpleDateFormat("dd/MM/yyyy"); | |
public ContasCSVService() { | |
carregaDados(); | |
} | |
@Override | |
public void salvar(Conta conta) { | |
conta.setId(ultimoId() + 1); | |
contas.add(conta); | |
salvaDados(); | |
} | |
@Override | |
public void atualizar(Conta conta) { | |
Conta contaAntiga = buscaPorId(conta.getId()); | |
contaAntiga.setConcessionaria(conta.getConcessionaria()); | |
contaAntiga.setDataVencimento(conta.getDataVencimento()); | |
contaAntiga.setDescricao(conta.getDescricao()); | |
salvaDados(); | |
} | |
@Override | |
public List<Conta> buscarTodas() { | |
return contas; | |
} | |
@Override | |
public void apagar(int id) { | |
Conta conta = buscaPorId(id); | |
contas.remove(conta); | |
salvaDados(); | |
} | |
public Conta buscaPorId(int id) { | |
return contas.stream().filter(c -> c.getId() == id).findFirst() | |
.orElseThrow(() -> new Error("Conta não encontrada")); | |
} | |
// salva a lista de dados no arquivo, gerando um novo CSV e escrevendo o arquivo completamente | |
private void salvaDados() { | |
StringBuffer sb = new StringBuffer(); | |
for (Conta c : contas) { | |
String linha = criaLinha(c); | |
sb.append(linha); | |
sb.append(System.getProperty("line.separator")); | |
} | |
try { | |
Files.delete(ARQUIVO_SAIDA); | |
Files.write(ARQUIVO_SAIDA, sb.toString().getBytes()); | |
} catch (IOException e) { | |
e.printStackTrace(); | |
System.exit(0); | |
} | |
} | |
// o ID mais alto é retornado aqui para continuarmos contando os IDs | |
private int ultimoId() { | |
return contas.stream().mapToInt(Conta::getId).max().orElse(0); | |
} | |
// carrega os dados do arquivo para a lista contas | |
private void carregaDados() { | |
try { | |
if(!Files.exists(ARQUIVO_SAIDA)) { | |
Files.createFile(ARQUIVO_SAIDA); | |
} | |
contas = Files.lines(ARQUIVO_SAIDA).map(this::leLinha).collect(Collectors.toList()); | |
} catch (IOException e) { | |
e.printStackTrace(); | |
System.exit(0); | |
} | |
} | |
// transforma uma linha do CSV para o tipo Conta | |
private Conta leLinha(String linha) { | |
String colunas[] = linha.split(SEPARADOR); | |
int id = Integer.parseInt(colunas[0]); | |
Date dataVencimento = null; | |
try { | |
dataVencimento = formatoData.parse(colunas[3]); | |
} catch (ParseException e) { | |
e.printStackTrace(); | |
System.exit(0); | |
} | |
Conta conta = new Conta(); | |
conta.setId(id); | |
conta.setConcessionaria(colunas[1]); | |
conta.setDescricao(colunas[2]); | |
conta.setDataVencimento(dataVencimento); | |
return conta; | |
} | |
// transforma um objeto conta em um arquivo CSV | |
private String criaLinha(Conta c) { | |
String dataStr = formatoData.format(c.getDataVencimento()); | |
String idStr = String.valueOf(c.getId()); | |
String linha = String.join(SEPARADOR, idStr, c.getConcessionaria(), c.getDescricao(), | |
dataStr); | |
return linha; | |
} | |
} |
Assinar:
Postagens (Atom)