exercício javaFx
Tópicos: máquina de estados, interface gráfica, threads,
o diagrama da máquina de estados:
o projeto:
o código:
package jfxsomajogo; import javafx.application.Application; import javafx.scene.Scene; import javafx.stage.Stage; import javafx.stage.WindowEvent; import jfxsomajogo.modelo.SomaJogoObs; import jfxsomajogo.ui.SomaUI; public class JFXSomaJogo extends Application { public static void main(String[] args) { launch(args);; } @Override public void start(Stage primaryStage){ primaryStage.setTitle("Acereta nos números"); SomaJogoObs a = new SomaJogoObs(); SomaUI ui = new SomaUI(a); Scene scene = new Scene(ui, 300,250); primaryStage.setScene(scene); primaryStage.show(); //que serve para fechar a janela primaryStage.setOnHidden((WindowEvent event) -> { a.paraRelogio(); }); } }
package jfxsomajogo.fsm; import jfxsomajogo.integracao.EstadoID; import jfxsomajogo.modelo.SomaDados; public class AguardaNomeJogador extends SomaFSMAdapter { public AguardaNomeJogador(SomaDados dados) { super(dados); } //transição @Override public ISomaFSM defineJogador(String nome) { dados.defineNomeJogador(nome); if (dados.jogadorValido()) { dados.reset(); //iniciação do jogo return new AguardaResposta(dados); //novo estado }else{ return this; } } //cada um retorna o seu próprio identicador //para evitar o uso do instanceof e o get class @Override public EstadoID obtemEstadoID() { return EstadoID.AGUARDA_NOME; } }
package jfxsomajogo.fsm; import jfxsomajogo.integracao.EstadoID; import jfxsomajogo.modelo.SomaDados; public class AguardaReinicio extends SomaFSMAdapter { public AguardaReinicio(SomaDados dados) { super(dados); } //este estado é basicamente, um "press any key to conitnue" @Override public ISomaFSM recomeca() { return new AguardaNomeJogador(dados); } @Override public EstadoID obtemEstadoID() { return EstadoID.AGUARDA_REINICIO; } }
package jfxsomajogo.fsm; import jfxsomajogo.integracao.EstadoID; import jfxsomajogo.modelo.SomaDados; public class AguardaResposta extends SomaFSMAdapter{ AguardaResposta(SomaDados dados) { super(dados); } @Override public ISomaFSM tentaResposta(int n) { dados.tentaResposta(n); if(dados.excedeuErros()){ return new AguardaReinicio(dados); //muda de estado }else{ return this; //fica no mesmo estado //return new AguardaResposta(dados); //alternativa, mostramos evolução do estado } } @Override public ISomaFSM relogioAvanca() { dados.diminuiCountDown(); if(dados.excedeuErros()){ return new AguardaReinicio(dados); }else{ return this; } } @Override public EstadoID obtemEstadoID(){ return EstadoID.AGUARDA_RESPOSTA; //para evitar o instance of } }
package jfxsomajogo.fsm; import java.io.Serializable; import jfxsomajogo.integracao.EstadoID; public interface ISomaFSM extends Serializable{ //vai servir para definir um estado, transições: default ISomaFSM defineJogador(String nome){return this;} //implementação default, pode ser no Adpater default ISomaFSM tentaResposta(int n){return this;} default ISomaFSM recomeca(){return this;} default ISomaFSM relogioAvanca(){return this;} EstadoID obtemEstadoID(); //retorna um ID //default, permite a implementação default mas devia ser no adaptar e não aqui. //mudar no final para ver como fica }
package jfxsomajogo.fsm; import jfxsomajogo.modelo.SomaDados; public abstract class SomaFSMAdapter implements ISomaFSM{ //guarda a referencia para o modelo de dados protected final SomaDados dados; //protected para os outros estados poderem aceder à informação protected SomaFSMAdapter(SomaDados dados){ this.dados = dados; } //public AdividnhaDados obtemDados(){return dados;} //usa-se protected para isto //implementações de default de cada um dos métodos de }
package jfxsomajogo.integracao; //isto é um package que vai ser usado pelo modelo de dados e pela interface import jfxsomajogo.fsm.AguardaReinicio; public enum EstadoID { //os ID dos estados AGUARDA_NOME, AGUARDA_RESPOSTA, AGUARDA_REINICIO } //este package agrupa as enumerações e constantes relacioandos com //integração entre vários módulos da aplicação //assim, a UD nao tem que improtar coisas dos packages de dados ou FSM
package jfxsomajogo.integracao; //isto é um package que vai ser usado pelo modelo de dados e pela interface public enum MsgCode { START, RIGHT, WRONG, TIMEOUT }
package jfxsomajogo.integracao; //isto é um package que vai ser usado pelo modelo de dados e pela interface public enum PropsID { //podiam ser usadas constantes //"" -> alteração gráfica PROP_ESTADO("prop_estado"), PROP_DESAFIO("prop_desafio"), PROP_RELOGIO("prop_relogio"); String valor; private PropsID(String v) { this.valor = v; } @Override public String toString() { return valor; } }
package jfxsomajogo.modelo; import java.io.Serializable; import java.util.Random; import jfxsomajogo.integracao.MsgCode; public class SomaDados implements Serializable { private static final int COUNTDOWN_TIMEOUT = 10; private static final int MAX_ERROS = 3; private static final int MIN_VALOR = 1; private static final int MAX_VALOR = 100; private String jogador = "X"; private MsgCode msg; //ver o estado actual private int pontuacao, erros; private int numA, numB; private int countdown; Random rand; SomaDados() { rand = new Random(); jogador = "X"; msg = MsgCode.START; pontuacao = erros = numA = numB = -1; countdown = -1; } public final void reset() { pontuacao = 0; erros = 0; novoDesafio(); msg = MsgCode.START; } public void defineNomeJogador(String nome) { jogador = nome; } public void novoDesafio() { numA = rand.nextInt(MAX_VALOR - MIN_VALOR + 1) + MIN_VALOR; numB = rand.nextInt(MAX_VALOR - MIN_VALOR + 1) + MIN_VALOR; resetCountDown(); } public void diminuiCountDown() { --countdown; if (countdown <= 0) { msg = MsgCode.TIMEOUT; erros++; novoDesafio(); } } public void tentaResposta(int resp) { if (resp == (numA + numB)) { pontuacao++; msg = MsgCode.RIGHT; novoDesafio(); } else { erros++; msg = MsgCode.WRONG; } } //para ver a evolução dos estados public boolean jogadorValido() { //se indicar o nome, pode ir para jogar return jogador != null && jogador.length() > 0; } public boolean excedeuErros() { return erros >= MAX_ERROS; } //obtem info para o ecrã public String obtemNomeJogador() { return jogador; } public int obtemNumA() { return numA; } public int obtemNumB() { return numB; } public int obtemPontuacao() { return pontuacao; } public int obtemErros() { return erros; } public int obtemCountDown() { return countdown; } public MsgCode obtemMsg() { return msg; } private void resetCountDown() { countdown = COUNTDOWN_TIMEOUT; } }
package jfxsomajogo.modelo; import java.io.Serializable; import java.util.HashSet; import java.util.Set; import jfxsomajogo.fsm.AguardaNomeJogador; import jfxsomajogo.fsm.ISomaFSM; import jfxsomajogo.integracao.EstadoID; import jfxsomajogo.integracao.MsgCode; import jfxsomajogo.integracao.PropsID; //esta é a classe "contexto" da máquina de estados //tem a máquina de estados + dados //máquina de estados public class SomaJogo implements Serializable { private final SomaDados dados = new SomaDados(); //criar o modelo de dados private ISomaFSM estado = null; //define o estado incial public SomaJogo() { estado = new AguardaNomeJogador(dados); } //defineEstado recebe o próximoe stado private Set<PropsID> defineEstado(ISomaFSM prox, PropsID... eventosBase) { //isto vai ajudar no observable, vai avisar a vista Set<PropsID> eventos = new HashSet<>(Set.of(eventosBase)); ISomaFSM anterior = estado; estado = prox; if (anterior != prox) { eventos.add(PropsID.PROP_ESTADO); } return eventos; } //operação do sistema ("dispositivo") contexto da FSM //-> reencaminha para FSM //os métodos que permitem a transição de estados //ponte entre o estado, o modelo de estados e a vista public Set<PropsID> defineJogador(String nome) { //ao inves de retornar o estado pode ser um return de boolean //retornar Set<PropsID>, um conjunto de situações de jogo que ajuda na vista do jogo //para a parte gráfica return defineEstado(estado.defineJogador(nome)); } public Set<PropsID> tentaResposta(int n) { return defineEstado(estado.tentaResposta(n), PropsID.PROP_DESAFIO); //estas duas significa que podem acontecer dois eventos, e disparar o fire de uma das duas } public Set<PropsID> recomeca() { return defineEstado(estado.recomeca()); } public Set<PropsID> relogioAvanca() { return defineEstado(estado.relogioAvanca(), PropsID.PROP_RELOGIO, PropsID.PROP_DESAFIO); } public EstadoID obtemEstadoID() { return estado.obtemEstadoID(); } //obtenção de dados para uso nas UI //informação relevanta para mostrar ao utilizador //-> reencaminha para dados //isto é o mesmo que os gets apenas traudizdo para obtem //para serem mostrados no UI do utilziador public String obtemNomeJogador() { return dados.obtemNomeJogador(); } public int obtemNumA() { return dados.obtemNumA(); } public int obtemNumB() { return dados.obtemNumB(); } public int obtemPontuacao() { return dados.obtemPontuacao(); } public int obtemErros() { return dados.obtemErros(); } public MsgCode obtemMsg() { return dados.obtemMsg(); } public int obtemCountDown() { return dados.obtemCountDown(); } //não devemos retornar o modelo SomaDados todo //porque se o fizermos vamos dar hipotese a quem faz //a vista de alterar os dados }
package jfxsomajogo.modelo; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.Set; import java.util.Timer; import java.util.TimerTask; import java.util.logging.Level; import java.util.logging.Logger; import jfxsomajogo.integracao.EstadoID; import jfxsomajogo.integracao.MsgCode; import jfxsomajogo.integracao.PropsID; //missao principal - adicionar o suporte para ligacao a vistas // = property change support //mantem a ligacao separada do contexto da FGSM e dados //permite save/load a FSM+dados sem dependencia (referencia) a vistas //adiciona save/load neste nivel e age sobre o modelo (contexto da FSM) public class SomaJogoObs { //os propertir change support SomaJogo jogo = null; //a referencia para o jogo, maquina de estados private PropertyChangeSupport props = null; private final Timer relogio; private final Temporizador segundos; public SomaJogoObs() { jogo = new SomaJogo(); props = new PropertyChangeSupport(jogo); relogio = new Timer(); segundos = new Temporizador(); relogio.schedule(segundos, 0, 1000); } //registar o interesse das vistas, quando uma determinada propriedade for alterada // public void registaPropertyChangeListener(PropsID prop, PropertyChangeListener listener) { props.addPropertyChangeListener(prop.toString(), listener); } private void disparaEventos(Set<PropsID> eventos) { for (var e : eventos) { props.firePropertyChange(e.toString(), null, null); } //alternaiva ao for //eventos.forEach((e) -> { //props.firePropertyChange(e.toString(), null, null); //}); } public void paraRelogio() { segundos.cancel(); relogio.purge(); //eliminar todas as as timer text relogio.cancel(); } // inicio: métodos que fazem a ponte entre as vistas e a maquina de estados public void relogioAvanca() { disparaEventos(jogo.relogioAvanca()); } // operação do sistema ("dispotivo") - reencaminha para jogo depois p/ FSM public void defineJogador(String nome) { disparaEventos(jogo.defineJogador(nome)); } public void tentaResposta(int n) { disparaEventos(jogo.tentaResposta(n)); //alternaiva ao udo do disparaEventos //jogo.tentaResposta(n); //props.firePropertyChange(PropsID.PROP_DESAFIO, null. null); //props.firePropertyChange(PropsID.PROP_ESTADO, null. null); } public void recomeca() { disparaEventos(jogo.recomeca()); } public EstadoID obtemEstadoID() { return jogo.obtemEstadoID(); } //fim: métodos que fazem a ponte entre as vistas e a maquina de estados //obenção de dados para uso das UI - reencaminha p/jogo (depois para dados) public String obtemNomeJogador() { return jogo.obtemNomeJogador(); } public int obtemNumA() { return jogo.obtemNumA(); } public int obtemNumB() { return jogo.obtemNumB(); } public int obtemPontuacao() { return jogo.obtemPontuacao(); } public int obtemErros() { return jogo.obtemErros(); } public MsgCode obtemMSG() { return jogo.obtemMsg(); } public int obtemCountDown() { return jogo.obtemCountDown(); } //load/save public void saveGame(String filename) { ObjectOutputStream out; try { out = new ObjectOutputStream(new FileOutputStream(filename)); out.writeObject(jogo); out.close(); } catch (IOException e) { Logger.getLogger(SomaJogoObs.class.getName()).log(Level.SEVERE, null, e); } } public void loadGame(String filename) { ObjectInputStream in; try { in = new ObjectInputStream(new FileInputStream(filename)); jogo = (SomaJogo) in.readObject(); in.close(); props.firePropertyChange(PropsID.PROP_ESTADO.toString(), null, null); } catch (IOException | ClassNotFoundException e) { Logger.getLogger(SomaJogoObs.class.getName()).log(Level.SEVERE, null, e); } } //time taks class Temporizador extends TimerTask { @Override public void run() { relogioAvanca(); } } }
package jfxsomajogo.ui; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.File; import java.util.HashMap; import java.util.Map; import javafx.application.Platform; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.geometry.Insets; import javafx.scene.control.Alert; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.Menu; import javafx.scene.control.MenuBar; import javafx.scene.control.MenuItem; import javafx.scene.control.TextField; import javafx.scene.input.KeyCode; import javafx.scene.layout.BorderPane; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.stage.FileChooser; import javafx.stage.Stage; import jfxsomajogo.integracao.EstadoID; import jfxsomajogo.integracao.MsgCode; import jfxsomajogo.integracao.PropsID; import jfxsomajogo.modelo.SomaJogoObs; //esta é uma classe genérica, que é um adpater para PropertyChangeListener //PropertyChangeListenerJFXAdapter faz com que seja implementado o PropertyChangeListener //vai implementar o propertyChange, que é o método obrigatório pelo inteface método PropertyChangeListener //e sempre que ele for chamado, e antes de correr o código pretendido ele faz um // Platform.runLater(() do método onChange(evt); // e quanto ele for corrido ele já foi corrido no contexto do .runLater(() //com o objecto qd é criado é abstracto //por exemplo propsID.PROP_ESTADO, new PropertyChangeListenerJFXAdapter() //sou obrigado a implementar o onChange(), com todo o código necessário //para as alterações que eu necessito //e assim o onChange() já foi executado na thread do javaFX //evito assim que estoire, e que acontece quanto tentamos executar código a partir de uma thread //difernte da que está responsavel por fazer a gestão dos componentes gráficos //isto por causa da utilização do timer, porque o timer está numa thread à parte abstract class PropertyChangeListenerJFXAdapter implements PropertyChangeListener{ @Override public final void propertyChange(PropertyChangeEvent evt){ Platform.runLater(()->{ onChange(evt); }); } abstract public void onChange(PropertyChangeEvent evt); } public class SomaUI extends BorderPane { SomaJogoObs adivObs; public SomaUI(SomaJogoObs a) { adivObs = a; FileChooser fileChooser = new FileChooser(); //esta funcao esta extensa. passar código para funcções auxiliares //implica passar as refs locais dos componentes para a classe //cria componentes //menu MenuBar menuBar = new MenuBar(); Menu fileMenu = new Menu("file"); MenuItem fileNew = new MenuItem("new"); MenuItem fileSave = new MenuItem("save"); MenuItem fileLoad = new MenuItem("load"); MenuItem fileExit = new MenuItem("exit"); fileMenu.getItems().addAll(fileNew, fileSave, fileLoad, fileExit); Menu aboutMenu = new Menu("about"); MenuItem aboutInfo = new MenuItem("info"); aboutMenu.getItems().addAll(aboutInfo); menuBar.getMenus().addAll(fileMenu, aboutMenu); setTop(menuBar); //eventos para os menus //file->new fileNew.setOnAction((ActionEvent e) -> { adivObs.recomeca(); }); //file-> save fileSave.setOnAction((ActionEvent e) -> { File f = fileChooser.showSaveDialog(getScene().getWindow()); if (f != null) { adivObs.saveGame(f.getAbsolutePath()); } }); //file -> load fileLoad.setOnAction((ActionEvent e) -> { File f = fileChooser.showSaveDialog(getScene().getWindow()); if (f != null) { adivObs.loadGame(f.getAbsolutePath()); } }); //file -> exit fileExit.setOnAction((ActionEvent e) -> { encerra(); ((Stage) getScene().getWindow()).close(); //Platform.exit(); //funciona apenas em windows }); //about aboutInfo.setOnAction((ActionEvent e) -> { Alert alert = new Alert(Alert.AlertType.INFORMATION); alert.setHeaderText("acerca disto"); alert.getDialogPane().setContentText("programa de javafx"); alert.showAndWait(); }); StackPane areaUtil = new StackPane(); areaUtil.setPadding(new Insets(10)); UINome uinome = new UINome(); //é um pane UIJoga uijoga = new UIJoga(); //é um pane UIFim uifim = new UIFim(); uijoga.setVisible(false); uifim.setVisible(false); areaUtil.getChildren().addAll(uinome, uijoga, uifim); setCenter(areaUtil); } public void encerra() { adivObs.paraRelogio(); } class UINome extends VBox { private final Label lNome = new Label("escreve nome"); private final TextField tfNome = new TextField(); private final Button btnDefineNome = new Button("ok"); public UINome() { super(10); //espaçamento entre items adivObs.registaPropertyChangeListener(PropsID.PROP_ESTADO, new PropertyChangeListenerJFXAdapter() { //registaPropertyChangeListener para sermos avisados de evolução de estados @Override public void onChange(PropertyChangeEvent evt) { setVisible(adivObs.obtemEstadoID() == EstadoID.AGUARDA_NOME); } }); btnDefineNome.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent t) { adivObs.defineJogador(tfNome.getText()); } }); tfNome.setOnKeyPressed(e -> { if (e.getCode() == KeyCode.ENTER) { adivObs.defineJogador(tfNome.getText()); } }); getChildren().addAll(lNome, tfNome, btnDefineNome); } } class UIJoga extends VBox { private final Label lMsg = new Label(); private final Label lDesafio = new Label(); private final TextField tfResposta = new TextField(); private final Button btnEnviaResposta = new Button("ok"); private final Label lRelogio = new Label(); Map<MsgCode, String> mensagens = new HashMap<>(); public UIJoga() { //1,39 super(10); //espaçamento entre items mensagens.put(MsgCode.START, "vamos começar"); mensagens.put(MsgCode.RIGHT, "certo. outro desafio"); mensagens.put(MsgCode.WRONG, "errado. tenta outra vez"); mensagens.put(MsgCode.TIMEOUT, "tempo excedido. outro desafio"); btnEnviaResposta.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent t) { tentaResposta(); } }); tfResposta.setOnKeyPressed(e -> { if (e.getCode() == KeyCode.ENTER) { tentaResposta(); } }); adivObs.registaPropertyChangeListener(PropsID.PROP_ESTADO, new PropertyChangeListenerJFXAdapter() { //registaPropertyChangeListener para sermos avisados de evolução de estados @Override public void onChange(PropertyChangeEvent evt) { if (adivObs.obtemEstadoID() == EstadoID.AGUARDA_RESPOSTA) { setVisible(true); defineMensagens(); //atualziar todas as labels com o texto em causa intiTfResposta(); //por o texto a vazio (escrita do texto), quando existe mudança de estado } else { setVisible(false); } } }); adivObs.registaPropertyChangeListener(PropsID.PROP_DESAFIO, new PropertyChangeListenerJFXAdapter() { //registaPropertyChangeListener para sermos avisados de evolução de estados @Override public void onChange(PropertyChangeEvent evt) { defineMensagens(); } }); adivObs.registaPropertyChangeListener(PropsID.PROP_RELOGIO, new PropertyChangeListenerJFXAdapter() { //registaPropertyChangeListener para sermos avisados de evolução de estados @Override public void onChange(PropertyChangeEvent evt) { //para processamento futuro caso necessário } }); getChildren().addAll(lMsg, lDesafio, tfResposta, btnEnviaResposta, lRelogio); defineMensagens(); } private void defineMensagens() { lMsg.setText(mensagens.getOrDefault(adivObs.obtemMSG(), "nao compreendo..")); //adivObs.obtemMSG() com ajuda de um switch case para obter as mensagens (string) acerca do estado //em que se encontra //a alternativa foi a opção de se usar um Map<MsgCode, String> mensagens lDesafio.setText(adivObs.obtemNomeJogador() + " ,quando é " + adivObs.obtemNumA() + " + " + adivObs.obtemNumB() + "?" ); lRelogio.setText("tempo restante: " + adivObs.obtemCountDown()); } private void intiTfResposta() { //serve para apagar a resposta que é escrita //e só acontece quando existe mudança de estado tfResposta.setText(""); tfResposta.requestFocus(); } private void tentaResposta() { try { int val = Integer.parseInt(tfResposta.getText()); adivObs.tentaResposta(val); intiTfResposta(); } catch (NumberFormatException e) { } } } class UIFim extends VBox { private final Label lResultado = new Label(); private final Button btnRecomecar = new Button("Novo jogo"); public UIFim() { super(5); adivObs.registaPropertyChangeListener(PropsID.PROP_ESTADO, new PropertyChangeListenerJFXAdapter() { @Override public void onChange(PropertyChangeEvent evt) { if (adivObs.obtemEstadoID() == EstadoID.AGUARDA_REINICIO) { setVisible(true); lResultado.setText(adivObs.obtemNomeJogador() + ", o teu resultado:\n" + "pontuação: " + adivObs.obtemPontuacao() + "\n" + "Erros:" + +adivObs.obtemErros() + "(obviamente)\n\n" + "ok para jogar outra vez\n\n" ); //formatação com \n em ambiente grafico - tem tudo a ver } else { setVisible(false); } } }); btnRecomecar.setOnAction((ActionEvent e) -> { adivObs.recomeca(); } ); getChildren().addAll(lResultado, btnRecomecar); } } }
Tags : javafx, Programação avançada
0 thoughts on “exercício javaFx”