|
|
Samuel Mota
Aprenda a usar este pattern destinado a enviar notificações a outros objetos.
Introdução
Este tutorial irá explicar como criar uma GUI (Graphical User Interface) que não congele quando a aplicação estiver em um processo longo e se mantenha respondendo aos eventos do usuário.
Esta não é uma das tarefas mais simples de se fazer pois envolve diretamente o uso de Threads e o desenho do sistema de eventos da plataforma Java, mas é muito facilitada pela existência de mecanismos para implementar o Pattern Observer. Como o objetivo é ser apenas um tutorial caso queira se aprofundar no assunto veja este site ou procure mais sobre o pattern.
Alguns conceitos importantes:
- Thread: é uma linha de execução do programa. A aplicação pode ter uma ou N threads, cada uma dessas linhas é executada independentemente da outra, (quase) em paralelo. O importante é que dentro de uma thread as instruções são processadas sequencialmente, com isso existe uma fila de instruções. É um recurso para permitir a uma mesma aplicação usar de forma mais produtiva o processamento da máquina fazendo várias tarefas ao mesmo tempo.
Exemplo
Para este exemplo utilizaremos uma tela com uma barra de progresso para manter nosso feliz usuário informado sobre o processo.
O processo será simulado, mas poderá ser qualquer atividade que seu programa precise executar em resposta a um evento, não necessariamente um click de botão, neste caso o seguinte código será usado para representar o processo:
1 for(int i = 0;i < classeGUI.TAM_PROCESSO; i++) {
2 // Este é um processo muito muito longo que congelaria nossa tela, oh!
3 }
|
Neste exemplo a cada 10 iterações do loop vamos atualizar a barra de progresso e ao final vamos atualizar a mensagem para o usuário.
O problema
Quando executamos um processo em resposta a um evento a tela congela, ou seja, não responde a nenhum evento enquanto não for terminado o evento anterior e isso inclui redesenhar a tela para informar o usuário do progresso por exemplo.
A tela ficaria assim ao alternar de aplicação no Windows:
O código que gerou este problema é o seguinte:
01 package br.com.guj;
02
03 import java.awt.BorderLayout;
04 import java.awt.event.ActionEvent;
05 import java.awt.event.ActionListener;
06 import javax.swing.JButton;
07 import javax.swing.JFrame;
08 import javax.swing.JLabel;
09 import javax.swing.JProgressBar;
10 import javax.swing.JTextField;
11
12 public class classeGUI extends JFrame {
13 // Nro de iterações para o loop de simulação do processo
14 public static int TAM_PROCESSO = 100000000;
15
16 // Variaveis da interface
17 private JProgressBar barradeprogresso;
18 private JButton botaoOK;
19 private JTextField texto;
20 private JLabel label;
21
22 /**
23 * Construtor padrão, instancia e monta objetos da tela
24 */
25 protected classeGUI() {
26 Container contentPane = getContentPane();
27 contentPane.setLayout(new BorderLayout());
28
29 botaoOK = new JButton("Processar");
30 botaoOK.addActionListener( new ActionListener() {
31 public void actionPerformed(ActionEvent e) {
32 for(int i=0;i < classeGUI.TAM_PROCESSO; i++) {
33 // Este é um processo muito muito longo que congelaria nossa tela, oh!
34 }
35 }
36 });
37
38 texto = new JTextField();
39
40 label = new JLabel("Programa Exemplo");
41 label.setHorizontalAlignment(JTextField.CENTER);
42
43 barradeprogresso = new JProgressBar();
44 contentPane.add(label,BorderLayout.NORTH);
45 contentPane.add(texto,BorderLayout.CENTER);
46 contentPane.add(botaoOK,BorderLayout.WEST);
47 contentPane.add(barradeprogresso,BorderLayout.SOUTH);
48
49 setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
50
51 pack();
52 }
53
54 /**
55 * Instancia e mostra a tela do usuário
56 * @param args Parâmetros da linha de execução
57 */
58 public static void main(String[] args) {
59 new classeGUI().show();
60 }
61 }
|
O código acima é bastante simples e apenas executa o processo no tratamento do evento, vamos deixar para estudar o código correto mais adiante.
A origem desse comportamento está no modelo adotado pelo Java para gerenciar os eventos, ou seja, foi a opção escolhida ao desenhar a arquitetura da plataforma.
Existe uma única thread conhecida como event-dispatching thread onde todos os eventos são colocados e conforme vimos no início deste tutorial todas as instruções de uma thread são executadas em sequência e por isso enquanto nosso processo não terminar nenhum outro evento será processado.
A solução
A solução lógica é separarmos nosso processo da tela e criarmos uma thread só para nosso processo.
Para criar threads podemos utilizar a herança da classe Thread ou implementar a interface Runnable, a explicação sobre threads em java está além do objetivo deste tutorial mas você pode entender melhor o assunto lendo o The Java Tutorial.
Para fazermos essa separação utilizamos também uma interface e uma classe, Observable e Observer respectivamente.
A interface Observer deve ser implementada por todos os objetos que queiram ser notificados de mudanças em outros objetos. Ela define apenas um método a ser implementado: update(Observable o, Object arg) , que será chamado a cada notificação, e é nele que faremos o controle para atualizarmos a tela.
A classe Observable deve ser implementada por todos os objetos que queiram notificar mudanças a outros objetos e ela expõe vários métodos para permitir a notificação dos observadores.
No nosso caso, a tela deve ser notificada e por isso implementa a interface e o processo deverá notificar alterações e por isso herda a classe.
É importante notar que a maioria dos componentes do Swing não são thread safe, ou seja, não fazem o controle de acesso por thread, por isso caso seu programa utilize entradas do usuário na tela certifique-se de carrega-las no seu objeto do processo antes do processamento pois caso haja mudanças nos dados durante o processamento e o processo tentar utiliza-las poderá haver inconsistencia nos parâmetros.
Observable
Nosso processo será observado, por isso a cada mudança significativa vamos notificar os observadores.
Para notificar usamos um dos métodos notify disponíveis, neste caso usamos o notifyObserver(Object) e nele passamos um objeto que terá algum significado para o objeto notificado.
Quando quisermos enviar as notificações setamos nosso objeto como alterado chamando o método setChanged().
Veja o código abaixo:
01 package br.com.guj;
02
03 import java.util.Observable;
04 import java.util.Observer;
05
06 /**
07 * @author smota
08 */
09 public class classeProcesso
10 extends Observable
11 implements Runnable {
12
13 /**
14 * Construtor que recebe um objeto que irá observa-lo
15 * @param observador Objeto que irá acompanhar as mudanças deste objeto
16 */
17 public classeProcesso(Observer observador) {
18 //Adiciona o objeto observador a lista de observadores
19 addObserver(observador);
20 //...outras inicializações
21 }
22
23 /**
24 * Ponto de entrada da Thread.
25 * @see java.lang.Runnable#run()
26 */
27 public void run() {
28 int i;
29 for(i=0; i<= classeGUI.TAM_PROCESSO; i++) {
30 //Notifica o processamento a cada 10 iterações
31 if((i % 10 == 0)) {
32 notifyObservers(new Integer(i));
33 setChanged();
34 }
35 }
36
37 //Notifica fim do processo
38 notifyObservers(new Boolean(true));
39 setChanged();
40 }
41
42 }
|
Simples assim? Sim... a complexidade não está no código mas sim no significado das mensagens para o objeto notificado.
Observer
Como vimos observer será nossa tela, o código abaixo faz o serviço:
001 package br.com.guj;
002
003 import java.awt.BorderLayout;
004 import java.awt.Container;
005 import java.awt.event.ActionEvent;
006 import java.awt.event.ActionListener;
007 import java.util.Observable;
008 import java.util.Observer;
009 import javax.swing.JButton;
010 import javax.swing.JFrame;
011 import javax.swing.JLabel;
012 import javax.swing.JProgressBar;
013 import javax.swing.JTextField;
014
015 /**
016 * @author smota
017 */
018 public class classeGUI
019 extends JFrame
020 implements Observer {
021 //Nro de iterações para o loop de simulação do processo
022 public static int TAM_PROCESSO = 1000000;
023
024 //Variaveis da interface
025 private JProgressBar barradeprogresso;
026 private JButton botaoOK;
027 private JTextField texto;
028 private JLabel label;
029
030 //Variavel de controle da thread do processo
031 private Thread processo;
032
033 /**
034 * Executa o processo da aplicação
035 */
036 private void executaProcesso() {
037 if(processo==null) { //Instancia a thread SE não existir uma
038 processo = new Thread(new classeProcesso(this));
039 processo.start();
040 } else {
041 System.out.println("O processo ainda está em execução");
042 }
043 }
044
045 /**
046 * Construtor padrão, instancia e monta objetos da tela
047 */
048 protected classeGUI() {
049 Container contentPane = getContentPane();
050 contentPane.setLayout(new BorderLayout());
051
052 botaoOK = new JButton("Processar");
053
054 //Listener do botao
055 botaoOK.addActionListener( new ActionListener() {
056 public void actionPerformed(ActionEvent e) {
057 executaProcesso();
058 }
059 });
060
061 texto = new JTextField();
062
063 label = new JLabel("Programa Exemplo");
064 label.setHorizontalAlignment(JTextField.CENTER);
065
066 barradeprogresso = new JProgressBar();
067 barradeprogresso.setMaximum(classeGUI.TAM_PROCESSO);
068
069 contentPane.add(label,BorderLayout.NORTH);
070 contentPane.add(texto,BorderLayout.CENTER);
071 contentPane.add(botaoOK,BorderLayout.WEST);
072 contentPane.add(barradeprogresso,BorderLayout.SOUTH);
073
074 setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
075
076 pack();
077 }
078
079 /**
080 * Atualiza a tela
081 * @see java.util.Observer#update(java.util.Observable, java.lang.Object)
082 * @param o Objeto que sofreu uma atualização
083 * @param arg Argumento passado pelo objeto para seus observadores
084 */
085 public void update(Observable o, Object arg) {
086 if(arg instanceof Integer) {
087 //Seta o valor do progresso
088 barradeprogresso.setValue( ((Integer) arg).intValue());
089 texto.setText( String.valueOf(((Integer) arg).intValue()));
090 } else if(arg instanceof Boolean) {
091 if( ((Boolean) arg).booleanValue() ) {
092 barradeprogresso.setValue(0);
093 label.setText("Processo finalizado!");
094 }
095 }
096 }
097
098 /**
099 * Instancia e mostra a tela do usuário
100 * @param args Parâmetros da linha de execução
101 */
102 public static void main(String[] args) {
103 new classeGUI().show();
104 }
105 }
|
Vamos estudar as partes importantes:
Para executar nosso processo registramos o listener e chamamos o método executaProcesso, o código
1 private void executaProcesso() {
2 if(processo==null) { //Instancia a thread SE não existir uma
3 processo = new Thread(new classeProcesso(this));
4 processo.start();
5 } else {
6 System.out.println("O processo ainda está em execução");
7 }
8 }
|
primeiro verifica se o processo existe, caso exista é porque o botão foi clicado e nosso processo está sendo executado, imprimimos uma mensagem no console. Caso não exista criamos uma thread passando para o construtor a própria tela para que o processo possa nos registrar como observadores.
O método update é invocado a cada vez que o objeto observado é marcado como alterado. O código
01 public void update(Observable o, Object arg) {
02 if(arg instanceof Integer) {
03 //Seta o valor do progresso
04 barradeprogresso.setValue( ((Integer) arg).intValue());
05 texto.setText( String.valueOf(((Integer) arg).intValue()));
06 } else if(arg instanceof Boolean) {
07 if( ((Boolean) arg).booleanValue() ) {
08 barradeprogresso.setValue(0);
09 label.setText("Processo finalizado!");
10 }
11 }
12 }
|
Caso receba um objeto do tipo Integer como arg apenas atualiza a barra de progresso e o texto na caixa de texto, caso receba o Boolean e este seja true significa que o processo acabou e avisamos o usuário.
É neste método que está o segredo, deve-se fazer um planejamento no modo como serão notificadas as diferentes alterações para não impactar na performance (adianto que esse ai não é o ideal) e ainda assim ser significativo para a interface, aqui podemos aplicar outros patterns ou definirmos algo que melhor se encaixe no funcionamento do processo e da interface.
Uma possibilidade é disponibilizarmos no objeto do processo alguns métodos para melhorar a notificação, após a tela ser notificada ela pergunta ao objeto sobre seu status. (só uma idéia:)
Resultado
O resultado deste exemplo é uma tela que mantém o usuário informado sobre o andamento do processo, mas esta abordagem permite tirarmos melhor proveito da interface com o usuário, é claro que exige também um cuidado especial de controle das açoes do usuário enquanto nosso processo roda (no exemplo existe um controle simples, lembra?) !
Conclusão
Esta não é a única solução para o problema, nem mesmo a melhor pois há limitações por se utilizar a herança para o objeto Observable (sim existem técnicas para transpor essa limitação) mas talvez seja a mais simples e mais rápida de se implementar sendo com isso muito útil para interfaces simples baseadas em caixas de diálogos ou outras aplicações.
É possível apartir deste conceito construir modelos mais complexos e completos, caso tenha alguma dúvida pesquise sobre o assunto e poste as dúvidas aqui no GUJ.
Até breve!
|
|
|