Um Sistema Java Completo - Criando Aplicações Gráficas com o NetBeans



Comments



Description

Um Sistema Java Completo - Criando Aplicações Gráficas com o NetBeansParte 1: Prototipação da interface com o usuário Aprenda a criar aplicações com aparência profissional, desenvolvendo um exemplo completo no NetBeans, começando com menus, janelas e componentes principais O NetBeans é hoje o melhor IDE livre para iniciantes em Java, sem no entanto dever nada em recursos voltados para o profissional mais experiente. A versão 4.0 foi apresentada na edição anterior, com foco no novo modelo de projetos, que oferece flexibilidade para os projetos mais complexos. Nesta série de artigos, o enfoque será demonstrar como construir aplicações gráficas com a biblioteca padrão de componentes visuais do J2SE, o Swing, usando os recursos do NetBeans. Para desenvolvedores acostumados ao desenvolvimento com ferramentas RAD como Delphi ou Visual Basic, haverá certo esforço de adaptação à filosofia de trabalho diferente do Java visual. Isso inclui a forma de desenhar formulários usando gerenciadores de layout; o tratamento de eventos baseado em listeners; e a forma de utilização de componentes mais sofisticados (como tabelas e árvores), que exigem customização baseada na extensão de classes ou implementação de interfaces. Nesta parte apresentamos o desenvolvimento da interface gráfica de uma aplicação de "Lista de Tarefas", ilustrando boas práticas do desenvolvimento Swing e mostrando como o NetBeans pode auxiliar na implementação dessas práticas. Se você está começando com o desenvolvimento Swing, não deixe de consultar os quadros “Conceitos essenciais do Swing” e "Gerenciadores de Layout e o NetBeans". E leitores atentos ao lançamento recente do NetBeans 4.1 podem consultar o quadro “NetBeans 3.6, 4.0 e 4.1” para informações sobre as diferenças entre as versões. No contexto deste artigo, as versões 4.1 e 4.0 são praticamente idênticas, e pouco mudou desde a versão 3.6 no que se refere ao desenvolvimento visual. A aplicação de exemplo A Lista de Tarefas ou “todo list” é um componente comum de aplicações de produtividade pessoal, que permite cadastrar tarefas com prioridades e datas de conclusão associadas. A interface gráfica inclui uma janela principal, que lista as tarefas ordenadas por prioridade ou por data de conclusão, e um diálogo para edição ou inclusão de uma tarefa. A janela principal é típica de aplicações desktop, contendo uma barras de menus e de ferramentas, e uma área central para a visualização das tarefas; essa janela deve também ser capaz de adaptar-se à resolução de vídeo do sistema ou ao tamanho determinado pelo usuário. Já o diálogo é um típico formulário de entrada de dados, com caixas de edição, checkboxes e outros controles posicionados em uma ordem natural de preenchimento, com os usuais botões Ok e Cancelar na parte inferior. O termo "formulário" é utilizado de forma genérica para referenciar janelas, diálogos e painéis, ou seja, qualquer classe que possa ser editada visualmente pela adição de componentes visuais. Outras janelas da aplicação, como uma caixa “Sobre”, e um diálogo para exibição de alertas sobre tarefas próximas de suas datas de conclusão serão construídas nas próximas partes desta série. Antes de iniciar a construção de uma interface visual, é sempre bom desenhar um esboço contendo os principais componentes e sua disposição nas janelas. Papel e caneta ou um programa de desenho são geralmente melhores para isso do que o seu IDE favorito, pois permitem que as idéias fluam sem serem “viciadas” pela estrutura e componentes padrões do IDE. A idéia aqui é fazer brainstorming sobre como deve ser a interface com o usuário, e não obter uma definição precisa da aparência de cada formulário. Também há o benefício de se poder focar nos componentes realmente essenciais para o usuário, antes de 1 entrar em detalhes de implementação, como ícones ou atalhos. Veja na Figura 1 um esboço das duas janelas aplicação de exemplo deste artigo. Design: por favor, ajustem as cores para deixar o fundo azulado o mais branco possível: Arquitetura da aplicação É um padrão em desenvolvimento orientado a objetos utilizar a arquitetura MVC como base de uma aplicação interativa. Dessa forma, o código da nossa aplicação será organizado em classes de modelo, visão e controlador, utilizando para tal uma estrutura de pacotes. Neste artigo será realizada apenas a primeira etapa do desenvolvimento da aplicação, que é a prototipação da interface com o usuário, utilizando os recursos de desenho de interfaces Swing do NetBeans. Vamos limitar o código ao mínimo que possibilite a navegação e exibição de informações; assim poderemos validar a usabilidade e a adequação da interface às necessidades da aplicação. Nas próximas edições, além de novos formulários, vamos criar as classes de negócios, contendo a lógica de validação e persistência. Criação do projeto Para a aplicação de exemplo, vamos usar o modelo Java Application do NetBeans (selecione File|New Project, escolha a categoria General e depois Java Application). Forneça “Todo” como nome do projeto e aceite os padrões, criando assim a classe todo.Main. Deixaremos esta classe vazia; mas adiante ela será modificada para instanciar a janela principal da aplicação. Crie também os pacotes todo.modelo, todo.visao e todo.controle. Em uma aplicação de produção, o prefixo para os nomes dos pacotes da aplicação deveria incluir o nome DNS da empresa, por exemplo, "br.com.javamagazine.todo.modelo", de modo a evitar conflitos com bibliotecas e componentes de terceiros. A janela principal Para criar a janela principal, usamos o modelo JFrame form: selecione File|New File, depois Java GUI Forms e JFrame Form (veja a Figura 2). Utilize como nome "ListaTarefas" e modifique o nome do pacote para "todo.visao". Será aberto o editor visual de classes do NetBeans (veja a Figura 3). Nos lados esquerdo e direito são exibidas várias visões (ou visualizações) relacionadas com o editor visual do IDE. À direita temos a paleta (Pallete), onde podem ser encontrados os componentes visuais do Swing e AWT. Abaixo da paleta, a área de propriedades (Properties) apresenta as propriedades do objeto selecionado, permitindo sua customização. À esquerda, logo abaixo da área que exibe as visões de projeto, arquivos e ambiente de execução (Projects, File e Runtime) temos o inspetor (Inspector), que apresenta a estrutura hierárquica de objetos visuais. O inspetor é muito útil quando um componente está sobreposto por outro, ou quando não está visível na área de desenho por algum outro motivo. O componente selecionado na área de desenho é sempre selecionado no inspetor, e vice-versa; e as propriedades sempre refletem o componente selecionado. É possível ainda selecionar vários componentes na área de desenho (ou no inspetor) e modificar propriedades em todos eles ao mesmo tempo na janela de propriedades. Há algumas diferenças no posicionamento e configurações padrões dessas partes do IDE entre as versões 4.1 e 4.0 do NetBeans, entretanto é possível arrastar qualquer visão para outra posição na janela principal do IDE, além de customizar a aparência da paleta. Pessoalmente prefiro uma mistura dos padrões das duas versões, configurando a paleta para uma aparência mais compacta (como na versão 4.0) e colocando o inspetor no lado esquerdo (como no NetBeans 4.1), de modo a deixar mais espaço para a janela de propriedades. Para reposicionar o inspetor, basta arrastá-lo pela sua barra de título; para deixar a paleta mais compacta, clique nela com o botão direito e escolha Hide component names. 2 Outra dica de customização do NetBeans é colocar a visão de saída (output) no modo de "auto-esconder", para que ela não reduza o espaço disponível para o inspetor e a paleta; ela fica como apenas um botão na parte inferior da janela do IDE. Note que a Figura 3 já apresenta a visão de saída nesta configuração. Basta clicar no ícone da janela de saída, quando ela for ativada pela próxima compilação ou execução do projeto. O nosso esboço indica a presença de uma barra de menus e uma de ferramentas na parte de cima da janela, e com uma tabela ocupando toda a sua área interna. Iniciamos pela adição da barra de menus: clique no ícone do JMenuBar ( ) na paleta e clique em qualquer parte da área de desenho. Por enquanto, deixe a barra de menus como está; mais adiante iremos inserir os demais menus. Agora selecione o componente JToolbar ( ) e clique logo abaixo da barra de menus. O resultado é a colocação do JToolbar na posição "norte" do BorderLayout (veja mais sobre este e outros gerenciadores no quadro "Gerenciadores de layout e o NetBeans"). Observe que a barra aparece bem estreita, por não conter ainda nenhum componente. Selecione o JButton ( ) e clique em seguida no JToolbar (na área de desenho). Ele irá automaticamente se ajustar ao tamanho do botão. Repita o procedimento algumas vezes, para inserir os botões de adicionar, editar, excluir e marcar tarefas. Utilize símbolos como “+” e "*" para o texto dos componentes (altere a propriedade text), enquanto não criamos os ícones apropriados para cada um. Selecione o JToogleButton (próximo ao JButton na paleta, com ícone igual) e acrescente dois botões desse tipo ao JToolbar. Eles correspondem às opções de ordenação das tarefas por prioridade ou por data de conclusão, e para exibir ou não as tarefas já concluídas. Por fim, acrescente mais um JButton para a operação de visualização de alertas. Continue utilizando símbolos como “!” no texto dos botões, como substitutos provisórios para os ícones que ainda não foram acrescentados. Para obter o agrupamento e separação visual entre os grupos de botões de uma JToolbar não irá funcionar o uso de um JSeparator, como faremos mais adiante para os menus. O problema é que o JSeparator sempre se expande para ocupar toda a largura do container. Utilize em seu lugar um JLabel contendo como texto apenas um espaço em branco. Para criar uma barra de status, insira um JLabel ( ) na parte inferior da janela principal, de modo que ele seja colocado na posição "sul" do BorderLayout. Finalmente, insira um JScrollPane ( ) na parte central do JFrame, e dentro dele insira um JTable ( ). Sempre que o conteúdo de um componente puder ser maior do que a sua área ocupada na janela, o componente deve ser colocado dentro de um JScrollPane; componentes Swing precisam do JScrollPane para exibir barras de rolagem. A janela principal da aplicação já começa a se parecer com o nosso esboço inicial. A Figura 4 ilustra como está a janela neste momento. Antes de prosseguir, recomendo escolher nomes (propriedade name) significativos para cada componente, ou pelo menos para aqueles cuja função é mais do que decorativa. Isso facilita a identificação dos componentes no inspetor e também será útil posteriormente, quando forem codificados os eventos. A Figura 5 apresenta o inspetor com os nomes dos componentes alterados. Formatando a tabela Durante a prototipação de uma interface gráfica, seja ela desktop ou web, é importante desde o início inserir dados que sejam o mais próximo possível da realidade. Caso contrário, não será possível decidir, por exemplo, se o tamanho e a legibilidade dos componentes são adequados ou se a disposição na tela é intuitiva. Dessa forma, não é recomendável utilizar “textos falsos” (como “Nonono” ou "Xxxxx"), pois eles não refletem a informação que será vista pelo usuário final. 3 No caso da janela principal, isso significa não apenas configurar as colunas e os títulos da tabela, mas também inserir algumas linhas de dados. Selecione o JTable (no inspetor ou na área de desenho), depois selecione sua propriedade model e clique no botão de reticências ( ) ao seu lado. Isso exibe o "customizador" da propriedade, que é um mecanismo padrão do Java para que um componente visual ofereça suporte personalizado à configuração de suas propriedades, independentemente do IDE utilizado. Alguns IDEs podem optar por oferecer seus próprios customizadores como alternativa (ou em adição) aos fornecidos pelos próprios componentes. O customizador padrão do NetBeans para uma propriedade do tipo TableModel (mostrado na Figura 6) apresenta duas abas. Na primeira, são definidas as colunas da tabela, incluindo o nome e o tipo. Na segunda, é possível inserir um conjunto de dados iniciais para a tabela. Forneça estes dados conforme a figura. Se você está habituado a IDEs como Delphi ou Visual Basic, poderá achar a configuração de tabelas do NetBeans um tanto restrita. Mas veremos na próxima edição que a maioria dessas configurações é na verdade feita por código Java, em classes criadas como parte da aplicação. Veremos ainda que as tabelas do Swing fornecem flexibilidade bastante superior à oferecida pelos seus correspondentes em ambientes visuais não-Java. Editando menus A edição de menus e itens de menus no NetBeans não é feita na área de desenho, mas sim no inspetor. Apenas o primeiro nível na barra de menus pode ser configurado e visualizado na própria área de desenho. Para adicionar novos itens ou submenus, deve ser utilizado o menu de contexto dos componentes, em vez da paleta. Clique com o botão direito no JMenuBar e selecione a opção Add JMenu; depois repita mais duas vezes a operação, de modo a terminar com quatro menus; Altere a propriedade text dos menus para "Arquivo", "Editar", "Opções" e "Ajuda". Até aqui as alterações podem ser observadas na área de desenho do editor visual do NetBeans. Em seguida clique com o botão direito no JMenu "Arquivo" recém-criado e escolha Add>JMenuItem O item adicionado não é exibido na área de desenho, mas pode ser visto no inspetor; selecione esse item e mude seu text para “Nova lista de tarefas”. Repita a operação para criar o item “Abrir lista de tarefas”, depois adicione um JSeparator e mais um JMenuItem, para a opção “Sair”. Ao selecionar um item de menu ou botão na IDE para alterar suas propriedades, é comum dar-se um duplo clique acidentalmente. Isso faz o NetBeans sair do editor visual e mudar para o editor de código, permitindo editar o código associado ao evento de ação do botão ou item. Se isso acontecer, observe no topo do editor de código dois botões: Source e Design. Um clique em Design retorna ao editor visual; e a qualquer momento pode-se clicar em Source para ver o código Java correspondente ao formulário. Para visualizar a aparência dos itens recém-adicionados ao nosso menu Arquivo, clique no ícone com um pequeno olho ( ) na barra de ferramentas da área de desenho. Isso irá executar imediatamente um "clone" do JFrame, contendo os mesmos componentes, mas sem o código de tratamento de eventos (que ainda não acrescentamos de qualquer modo). Assim é possível interagir com os menus e visualizar seus itens e submenus. A Figura 7 mostra um dos menus nessa pré-visualização. Repita o processo para inserir os demais itens, de acordo com a estrutura apresentada na Figura 8. Observe na figura o uso de componentes JMenuItem, JSeparator, JCheckboxMenuItem e JRadioButtonMenuItem. Você pode mudar a ordem com que os elementos aparecem em cada menu, arrastando os componentes para outra posição dentro do inspetor. Ícones e barras de ferramentas 4 Um item de menu ou botão de barra de ferramentas contendo apenas texto é considerado “pobre” para os padrões atuais de interfaces gráficas: espera-se que ao menos os itens mais importantes tenham ícones associados, além de teclas de atalho. Todas essas opções podem ser configuradas pelas propriedades do componente, mas os iniciantes em Swing costumam ter dificuldades especiais com os ícones. A primeira dificuldade vem de que a maioria dos IDEs Java (entre eles o NetBeans) não incluem um conjunto padrão de ícones prontos para uso. A Sun fornece um conjunto de ícones especialmente adaptados ao look-and-feel Metal padrão do Swing (até o J2SE 1.4) em java.sun.com/developer/techDocs/hi/repository. Eles podem ser baixados todos em um pacote jar ou um a um diretamente da página, e podem ser redistribuídos livremente com sua aplicação. Preferi, no entanto, copiar os ícones do Workbench do Eclipse, que são mais modernos em vários aspectos e também podem ser redistribuídos livremente. Os ícones estão todos na pasta src/icones nos fontes para download deste artigo (embora apenas uma pequena deles sejam utilizados pela aplicação que estamos construindo). A segunda dificuldade é na forma de referenciar os ícones, de forma independente do diretório de instalação no computador do usuário final. Caso contrário você vai precisar fornecer também um programa de instalação, assim como lidar com questões como letras de drives (que só existem no Windows) e separadores de diretórios. A solução recomendada é colocar os ícones junto às classes da aplicação, de modo que possam ser localizados por uma busca pelo classpath, da mesma forma que são localizados os arquivos .class. pela JVM. Arquivos de dados encontrados desta maneira são chamados de recursos da aplicação. Você pode então copiar a pasta src/icones do pacote de download para o mesmo local em seu projeto no NetBeans. Por estarem na mesma pasta do código Java da aplicação, os recursos (no caso os ícones) serão automaticamente copiados para a pasta build/classes durante a compilação do projeto. Assim estarão junto aos bytecodes das classes da aplicação, como desejamos. O próximo passo é configurar os ícones nos itens de menu. Para cada item, selecione sua propriedade icon e clique no botão do seu customizador ( ). No diálogo exibido (Figura 9) selecione a opção Classpath e clique em Select File. Escolha o ícone adequado e observe como a caixa de texto correspondente, em vez de registrar o caminho completo para o arquivo do ícone no sistema de arquivos, registra apenas o caminho relativo ao classpath da aplicação, por exemplo /icones/delete_obj.gif. Caso o NetBeans não exiba os ícones recém-copiados no customizador da propriedade icon, entre nas propriedades do projeto (clique com o botão direito sobre o ícone do projeto, na visão de projeto) e adicione uma pasta qualquer ao seu classpath de compilação (item Libraries, aba Build nas propriedades do projeto). Ao se confirmar a alternação nas propriedades, o IDE irá re-escanear o classpath e assim notar a presença dos ícones. Depois não deixe de voltar às propriedades do projeto e remover esta pasta adicional. Quando o projeto for empacotado para distribuição (com Build|Build Main Project) os arquivos de ícones serão copiados para o pacote jar executável, juntamente com os bytecodes da aplicação. A aplicação poderá assim ser instalada em qualquer pasta no computador do usuário final. Da mesma forma que foi feito para os menus, podemos agora configurar os ícones para os botões da barra de ferramentas. A Figura 10 apresenta a barra customizada com os ícones. Tome o cuidado de escolher para os botões os mesmos ícones dos itens de menu equivalentes. Aceleradores e atalhos Aplicações visuais bem-escritas definem mnemônicos para todos os seus itens de menus, e também aceleradores para os itens utilizados com mais freqüência pelo usuário. 5 Um mnemônico permite navegar pelos menus utilizando apenas o teclado, o que torna a operação da aplicação mais ágil para usuários freqüentes e mais confortável em dispositivos como quiosques1[1]. Mnemônicos também são um dos principais itens de acessibilidade2[2] de uma aplicação. Um mnemônico é em geral a primeira letra do item do menu, conjugada com a tecla Alt. Além de itens de menus, outros componentes como botões e labels podem ter mnemônicos, permitindo a navegação pelo teclado em um formulário de entrada de dados. Mais adiante veremos como fazer isso, quando detalharmos a construção do diálogo de edição de tarefas. Já um acelerador permite a execução direta de alguma operação importante, sem a necessidade de navegar até ela nos menus. Um exemplo é o popular Ctrl+X para o comando Editar|Recortar. Mnemônicos e aceleradores são definidos em propriedades dos itens de menus e demais controles; então é fácil configurá-los no NetBeans. A Figura 11 apresenta essa configuração para o item Editar|Adicionar tarefa da aplicação de exemplo. E a Figura 12 mostra os menus da aplicação completos, já com ícones, mnemônicos e aceleradores definidos. Observe que as letras correspondentes aos mnemônicos são exibidas sublinhadas em cada item de menu, enquanto que os aceleradores definidos são exibidos ao lado do texto do item correspondente. Já que falamos de facilidade de uso, aproveitamos para um comentário sobre as barras de ferramentas. Conceitualmente, elas seriam “atalhos de mouse”, então se uma operação é importante o suficiente para estar na barra de ferramentas, ela também deve ter um atalho de teclado associado. Além disso, todo elemento numa barra de ferramentas deve corresponder a algum item de menu, ou então a alguma opção num diálogo. Em outras palavras, atalhos e barras de ferramentas nunca devem ser a única forma de se realizar uma tarefa. Voltando à aplicação: para que as barras e menus fiquem completos, eles necessitam apenas da configuração de tooltips, que podem ser configurados alterando a propriedade toolTipText de cada componente. A janela de edição de tarefas Para criar a segunda janela da aplicação, clique com o botão direito no pacote "todo.visao" no inspetor e selecione New>File/Folder no menu de contexto; depois escolha o modelo JDialog Form na categoria Java GUI Forms. Chame a classe de "EditaTarefa". Altere o gerenciador de layout para o valor nulo ("Null Layout"). Assim será possível posicionar os componentes de forma livre dentro do diálogo. Depois mude para um GridBagLayout, deixando que o NetBeans gere um conjunto inicial de GridBagConstraints3[3] para cada componente, que será depois ajustado manualmente. O quadro “Entenda os GridBagConstraints” descreve o significado de cada uma das propriedades de layout que teremos que ajustar. A Figura 13 apresenta o JDialog com componentes posicionados no gerenciador de layout nulo. Depois da mudança para o GridBagLayout vai parecer que a janela não mudou, mas ao abrir o customizador do gerenciador de layout (clicando com o botão direito no JDialog e selecionando Customize Layout), veremos que o conjunto de constraints gerados pela conversão é menos que ótimo (Figura 14). Há várias colunas e linhas adicionais e os espaçamentos desiguais em cada célula prejudicam o alinhamento dos componentes. Apesar disso, ainda não encontrei um desenvolvedor que, depois de algum tempo com o NetBeans, prefira utilizar o GridBagLayout desde o início, porque será mais trabalhoso configurar manualmente todas as propriedades a cada componente adicionado. 1[1] Como os quiosques que informam sobre localizações de lojas em shopping centers ou caixas eletrônicos de bancos 2[2] Uma aplicação é considerada acessível se foi construída levando em consideração a facilidade de uso por pessoas com deficiências visuais ou motoras 3[3] As propriedades que vemos agrupadas na categoria Layout em um componente são na verdade as propriedades do objeto de constraints do gerenciador de layout, cuja classe é específica para cada gerenciador. 6 O posicionamento dos JLabels e caixas de texto (incluindo os dois JSpinner), não oferece dificuldades. Lembre-se também de colocar o JTextArea dentro de um JScrollPane. Os botões de Salvar, Cancelar e Remover deverão ser inseridos todos dentro de um painel; é este painel que será posicionado dentro do diálogo (mais detalhes adiante). Observe ainda os dois JSeparator, colocados antes e depois da área de texto de observações, e o JLabel a parte superior do diálogo, que será utilizado como uma área ara exibição de mensagens de erros. Daqui em diante entramos em mais detalhes sobre como configurar o diálogo para chegar à aparência final, conforme a Figura 15. Todos os ajustes serão feitos dentro do customizador do GridBagLayout, o qual ao fim do processo estará como na Figura 16. Inicie os ajustes zerando a margem interna (Internal Padding) e colocando o valor 1 na altura (Grid Height ) de todos os componentes. Posicionamento e espaçamento no formulário Foi inserido um espaçamento de cinco pixels entre cada componente e entre os componentes e as bordas do diálogo, para que não pareçam “grudados”. Observe as áreas em amarelo na Figura 16; elas indicam onde foram adicionados espaçamentos. Aproveite o fato de que é possível selecionar múltiplos componentes utilizando a tecla Ctrl + mouse, para configurar espaçamentos uniformes. Os labels de “Descrição”, “Prioridade” e “Data de conclusão”, assim como o checkbox “Gerar alerta” foram ancorados (modificando Anchor) à direita (posição "leste"); os controles correspondentes (um JTextField, dois JSpinner e um JFormattedTextField) foram alinhados à esquerda (posição "oeste"). Ambos os grupos receberam espaçamento à esquerda e abaixo, exceto pela primeira linha (Descrição), que ganhou também espaçamento acima (caso contrário, ela ficaria colada ao label de mensagem). Para JScrollPane e os dois separadores, foi acrescentado espaçamento à esquerda, à direita e abaixo, e eles foram configurados para ocuparem três células de largura (alterando Grid Width). A caixa de texto de descrição ocupa duas células, assim como a de data de conclusão. As caixas de prioridade e de dias de antecedência para o alerta ocupam apenas uma célula de largura. O campo de observações Queremos que o diálogo de edição de atividades seja redimensionável, e que o campo de observações ocupe todo o espaço remanescente. Assim, o campo deve ser configurado para se expandir tanto na horizontal quanto na vertical (mudando a propriedade Fill para Both) e receber peso 1 em ambos os sentidos (modifique Weight X e Weight Y). Observe que estas constraints se aplicam ao JScrollPane que contém o JTextArea. Já os dois separadores devem ser configurados para se expandirem apenas na horizontal (alterando o valor de Fill para Horizontal), mas sem nenhum peso. Configurando os botões Um JButton normalmente assume a dimensão mínima que permita exibir seu texto, gerando telas deselegantes, onde o botão de "Ok" é muito menor do que o de "Cancelar", por exemplo. A aparência fica melhor quando botões relacionados têm as mesmas dimensões. A maneira mais fácil de fazer isso é inserindo os botões dentro do seu próprio painel, e configurando o layout deste painel para um GridLayout. O painel é então posicionado na parte inferior do diálogo, posicionado na parte de baixo do GridBagLayout, com duas células de largura. À esquerda do painel de botões está o checkbox que registra se a tarefa foi ou não completada. A âncora do painel é colocada na posição "leste", de modo que o conjunto de botões fique alinhado à direita. Label de mensagens 7 Para o label de mensagens, posicionado no topo do diálogo, queremos um visual diferente do padrão. Ele deve ficar claramente diferenciado no formulário de edição, dada sua função de exibir mensagens informativas. Obtemos o efeito de “faixa” mudando a cor de fundo do label, e deixado seus espaçamentos (Insets) zerados, de modo que ele fique colado aos cantos do diálogo; entretanto, não queremos que o texto fique colado. A solução é definir uma borda: alteramos a propriedade border do label para EmptyBorder e configuramos esta borda com espaçamentos de 5 pixels em cada direção. Quanto às cores de frente e de fundo, foram escolhidos um tom suave de amarelo e um tom mais forte de azul-claro, para se obter um bom contraste. A mudança na cor de fundo só será visível se for modificada a propriedade opaque do componente, pois o padrão é que um JLabel tenha o fundo transparente, incorporando a cor de fundo do seu container. Já em relação à fonte, foi mantido o padrão do Swing (Dialog); apenas retiramos o negrito da fonte padrão. Na maioria das vezes deve-se evitar a customização de fontes e cores em formulários de entrada de dados, pois isso pode tornar a aplicação deselegante ou ilegível caso o usuário opte por um look-and-feel customizado, ou por um tema de cores diferente para o lookand-feel padrão. Caso seja necessário mudar as cores, mesmo que para um tom fixo, tenha sempre o cuidado de fixar ambas as cores de frente e de fundo do componente. Mnemônicos no formulário Para garantir a agilidade na digitação e a acessibilidade do formulário, devem ser definidos mnemônicos de teclado para os campos de texto e outros componentes, de forma similar ao que foi feito para os menus. O procedimento envolve, primeiro, associar cada JLabel ao seu componente de entrada de dados, por meio da propriedade labelFor. Em seguida, é configurado o campo displayedMnemonic para a tecla desejada, que será sublinhada no texto do label. Os "botões" (JButton, JToogleButton, JCheckBox e JRadioButton) são configurados pelas suas propriedades nmemonic específicas. Use a Figura 15 como referência para definir as teclas de mnemônico para cada componente. Testando o protótipo Até este ponto, as janelas do protótipo foram testadas apenas pela pré-visualização do editor visual do NetBeans, que nem sempre é fiel ao comportamento das classes Java. Vamos então inserir o mínimo de código para que as duas telas possam ser iniciadas como parte de uma aplicação, e verificar se o resultado funciona corretamente. Localize a classe todo.Main na visão de projeto e dê um clique duplo para abri-la no editor de código. Edite o código conforme a Listagem 1: dessa forma, a aplicação iniciará instanciando uma janela ListaTarefas e tornando-a visível. Em seguida, use o mesmo procedimento para abrir a classe ListaTarefas (caso ela tenha sido fechada). Ela será aberta no editor visual, em vez de no editor de código. Dê um clique duplo no primeiro botão da barra de ferramentas. Será então mostrado o editor de código, com o cursor posicionado no método que trata o evento actionPerformed do botão. (Os trechos em azul-claro são gerados pelo próprio editor visual e não poderão ser modificados no editor de código.) Complete o código para o evento conforme a Listagem 2. O objetivo é apenas instanciar o diálogo de edição de tarefas e exibi-lo de forma modal4[4]. Você já poderá executar a aplicação com Run|Run Main Project e verificar o comportamento do exemplo; ou então selecionar Build|Build Main Project para gerar o pacote jar executável para a aplicação, e iniciar a aplicação na linha de comando da maneira usual (onde estamos supondo que o diretório Todo/dist contem o jar da aplicação): $ java -jar Todo/dist/Todo.jar 4[4] Um diálogo exibido de forma modal impede que se interaja com sua janela "mãe", até que seja fechado pelo usuário. 8 É importante que uma aplicação Swing seja testada desde o início com look-andfeels alternativos, para garantir que ela é realmente uma aplicação multiplataforma. Por exemplo, para utilizar o look-and-feel GTK do Linux, a linha de comando seria: $ java -Dswing.defaultlaf=com.sun.java.swing.plaf.gtk.GTKLookAndFeel Todo/dist/Todo.jar feel. Conclusões Este artigo demonstrou como usar as facilidades oferecidas pelo NetBeans para desenvolver aplicações Java com aparência profissional, baseadas nos componentes visuais do Swing. Como se viu, é importante atentar para detalhes, naturalmente, seguir as boas práticas da plataforma Java. Artigos futuros irão demonstrar como utilizar as janelas que foram prototipadas nesta aplicação em uma aplicação orientada a objetos, considerando acesso a bancos de dados e uso eficiente de threads. Veremos também como integrar e usar componentes de terceiros no NetBeans. Links netbeans.org Site oficial do NetBeans netbeans.org/kb/articles/form_getstart40.html GUI Building in NetBeans IDE 4.0 java.sun.com/docs/books/tutorial/uiswing Trilha sobre Swing no Java Tutorial da Sun cld.blog-city.com/read/1149708.htm Swing Pointer, uma coleção de recursos para o desenvolvedor Swing -jar A Figura 17 apresenta o aspecto das duas janelas da aplicação com este look-and- Alguns conceitos de Swing Para os que estão começando com a programação visual em Java, são apresentados aqui alguns conceitos do Swing, que são usados ou citados ao longo do texto. Componentes, em um sentido amplo, são objetos visuais (ex.: JCheckBox, JButton, JSeparator), ou objeto não-visuais (como GridBagLayout) que podem interagir com objetos visuais por meio dos padrões JavaBeans. Um container é qualquer objeto que possa conter outros objetos visuais. Todo container tem um gerenciador de layout que organiza o posicionamento e dimensionamento dos componentes dentro do container. Exemplos: JPanel e JDialog. Todo componente possui um tamanho mínimo, que é a menor dimensão (altura, largura) capaz de exibir todo o seu conteúdo (texto, ícone ou ambos). Em alguns (poucos) casos, este tamanho é derivado de outras propriedades do componente; por exemplo, em um JTextArea podem ser especificadas colunas e linhas da sua área de texto visível. NetBeans 3.6, 4.0 e 4.1 O NetBeans é um dos projetos mais antigos de IDEs livre com recursos visuais, mas não era inicialmente com uma interface com o usuário bem-projetada. Era sim um exemplo de como a plataforma Java e a tecnologia de componentes JavaBeans poderia viabilizar a construção de aplicações desktop complexas. Como mostrados em outros artigos desta coluna, na versão 3.6 iniciou-se um ciclo de mudanças profundas no NetBeans, de modo a modernizá-lo e deixá-lo mais coerente com modernas práticas de desenvolvimento de software em Java. A primeira mudança, no 3.6, foi a criação de uma nova interface com o usuário, baseada em visualizações que podem ser ancoradas em qualquer parte da tela, organizadas em abas, e abandonando o antigo modelo MDI (como o usado no Gerenciador de Programas do antigo Windows 3.0 e em partes do Microsoft Office). 9 A versão 4.0 inaugurou um novo modelo de projetos, baseado no Ant, que permite customizar com facilidade o processo de construção e empacotamento de aplicações. Basicamente, o desenvolvedor pode acrescentar novas etapas ao processo, por exemplo, geração de código via XDoclet ou pré-compilação de páginas JSP com o Jasper do Tomcat – tudo sem necessitar de instalar um plug-in especialmente construído para o NetBeans. Na recém-lançada versão 4.1 a novidade foram recursos voltados para o desenvolvimento J2EE, em especial suporte a EJBs e a web services, não havendo grandes mudanças na interface com o usuário ou no suporte ao desenvolvimento Swing, exceto pela presença de novos templates e assistentes. Da versão 3.6 até a versão 4.1 há poucas mudanças no editor visual, a maioria delas apenas estéticas. A única mudança realmente importante foi o acréscimo, na versão 4.0, de um customizador para o GridBagLayout, utilizado neste artigo para configurar o diálogo de edição de propriedades. Gerenciadores de layout e o NetBeans A maior dificuldade do iniciante em Swing é lidar com os gerenciadores de layout predefinidos. Isso é sentido especialmente por desenvolvedores habituados a ambientes RAD para Windows. O motivo é que nestes ambientes se costuma posicionar os componentes de modo fixo (em pixels) nos formulários, enquanto que no Swing o posicionamento é determinado por um gerenciador de layout. Por isso foi preparado este quadro, que relaciona os usos mais comuns dos principais gerenciadores de layout do J2SE (e um específico do NetBeans), além das facilidades oferecidas pelo IDE para a customização visual de componentes com esses gerenciadores. FlowLayout O gerenciador de layout de “fluxo” apenas posiciona os componentes em fila, um após o outro, cada qual com suas dimensões mínimas. O FlowLayout imita o fluxo de texto em uma folha de papel e, como um parágrafo em um processador de textos, também pode alinhar os componentes à direita, à esquerda, ou centralizados dentro do container. O uso mais comum deste layout é para preencher uma linha com a maior quantidade possível de componentes, por exemplo em barras de ferramentas ou de status. No NetBeans: Novos componentes são sempre adicionados ao final do fluxo, mas eles podem ser reposicionados pelo mouse. Um quadrado pontilhado indica a nova posição do componente durante esta operação. BorderLayout O BorderLayout posiciona os compomentes nas “bordas” do container, deixando a maior parte da sua área disponível para o componente inserido no centro. Cada borda é identificada por um ponto cardeal (NORTH, SOUTH, EAST, WEST). Apenas um componente será visível em cada borda, expandido na altura ou largura para ocupar toda a borda do container, porém assumindo o valor mínimo na outra dimensão. Note que esta disposição reflete o padrão na maioria das aplicações desktop, como processadores de texto ou programas de desenho: uma barra de ferramentas ao norte, uma barra de status ao sul, opcionalmente outras barras de ferramentas ao leste e oeste, e uma área de edição ao centro. No NetBeans: O componente é posicionado na borda da área de desenho que for clicada quando é feita sua adição ao container, e permite que um componente seja arrastado para outra posição (outra borda ou para o centro) desde que esta posição esteja vazia. Caso um componente seja arrastado para uma posição já ocupada, o BorderLayout irá se perder (e consequentemente também o NetBeans), e a correção terá que ser feita na visão de propriedades e/ou no inspetor. GridLayout 10 O GridLayout organiza os componentes em uma "grade" ou tabela com tamanho (linhas e colunas) pré-fixadas no momento da sua criação. Todas as células possuem o mesmo tamanho, e são expandidas para ocupar a área total disponível no container. Caso haja menos componentes do que células, o espaço das células vazias é distribuído igualmente entre os componentes; mas podem ficar células vazias nas últimas colunas da última linha. O GridLayout é adequado quando se deseja que um grupo de componentes (como um grupo de botões) tenha dimensões uniformes, como na caixa de ferramentas de um programa de desenho ou o par de botões “Ok” e “Cancela” de um diálogo. No NetBeans: Novos componentes são sempre acrescentados na próxima célula vazia da grade, mas podem ser arrastados com o mouse para uma posição (célula) diferente. GridBagLayout Com nome estranho ("saco de grades"), o GridBagLayout é o mais poderoso e mais flexível dos gerenciadores fornecidos com o J2SE. Ele imita em linhas gerais o funcionamento de uma tabela HTML, em que um componente pode ocupar várias células, ou seja, se estender por várias colunas e linhas. Os componentes podem ser expandidos para ocupar toda a área das suas células, ou serem alinhados em qualquer posição do conjunto de células. Linhas e colunas assumem as dimensões do maior componente, mas é necessário que tenham todas o mesmo tamanho. E algumas células podem ser configuradas para ocuparem toda a área disponível no container. Podemos afirmar com segurança que qualquer disposição de componentes pode ser configurada em um GridBagLayout. Por outro lado, a quantidade de constraints (propriedades e restrições de layout) possível para cada componente também deu a este gerenciador a fama de ser difícil de programar. No NetBeans: O editor visual do NetBeans não tinha suporte ao GridBagLayout antes da versão 4.0, o que obrigava o desenvolvedor a configurar cada uma das dezenas de constraints, manualmente para cada componente. Mas a nova versão fornece um customizador para o GridBagLayout, numa janela externa ao editor visual. Nela é representada a grade definida para o layout, assim como a área ocupada por cada componente. Botões de setas permitem ajustar facilmente cada propriedade, e os componentes podem ser arrastados entre as células da grade. Observe o código de cores no customizador: azul significa que a célula é ocupada por um componente; cinza indica uma célula vazia; vermelho indica uma linha ou coluna sem componentes; e amarelo mostra espaçamento (insets) interno à célula. O customizador do GridBagLayout é realmente fácil de usar, e em pouco tempo você estará acostumado com ele. Pena que, pelo customizador, não seja possível modificar propriedades além dos constraints de cada componente, nem adicionar novos componentes. Então será necessário entrar e sair do customizador várias vezes durante o desenho de um formulário. Null Layout Na verdade o Null Layout não é um gerenciador de layout, mas sim a ausência de qualquer gerenciador (equivale a definir a propriedade layout do container com o valor null). O resultado é que no IDE componentes podem ser posicionados à vontade na área de desenho; eles permanecerão na posição onde foram colocados e com as dimensões indicadas pelo desenvolvedor. Criar telas com o "layout nulo" às vezes chamado de usar o "posicionamento absoluto". No NetBeans: O NetBeans pode parecer “pobre” no seu suporte ao layout nulo, pois não fornece as ferramentas de alinhar e centralizar componentes, ou para ajustar as dimensões de grupos de componentes. Por outro lado, o uso desse layout não é recomendado, por ser incompatível com o uso de look-and-feels customizados e ir contra à 11 filosofia de portabilidade do Java. Então faz sentido que o NetBeans desestimule o uso desta opção ao não fornecer facilidades específicas. Absolute Layout O AbsoluteLayout não é um gerenciador padrão do Java, mas uma adição do NetBeans; seu uso requer que o pacote modules/ext/AbsoluteLayout.jar seja copiado para a estação do usuário e configurado no classpath. Ele funciona como um Null Layout, com a diferença de que as dimensões do container são calculadas corretamente, permitindo o uso do método pack() (com o Null Layout, o resultado são dimensões [0, 0]). Ele apresenta todas as desvantagens do Null Layout, portanto também não é recomendado para uso geral. GridBagConstraints As propriedades que determinam a posição e dimensões de um componente dentro de um GridBagLayout são reunidas em um objeto chamado GridBagConstraints. Cada componente adicionado a um container cujo gerenciador de layout seja um GridBagLayout possui seu próprio objeto GridBagConstraints. Neste quadro fornecemos uma breve explicação do significado de cada uma dessas propriedades. Note que identificamos as propriedades da classe GridBagConstraints do modo como aparecem no customizador do GridBagLayout. Estes nomes não são os mesmos que serão encontrados na documentação javadoc da classe, onde é seguido um estilo mais “telegráfico”, na sintaxe de Java. Por exemplo, a propriedade "Grid Width" do customizador é na verdade gridWidth; e "Internal Padding X" é ipadx. Grid X e Grid Y – indicam a posição do componente dentro da tabela ou grade utilizada pelo GridBagLayout para posicionar os componentes. Grid Width e Grid Height – determinam a quantidade de células da tabela que serão ocupadas pelo componente, respectivamente na largura e altura. Fill – indica se o componente irá ocupar seu tamanho mínimo, deixando vazio o restante das células ocupadas; ou se ele será expandido para ocupar todo o espaço alocado para a célula. A expansão pode ser apenas na vertical (valor Vertical), apenas na horizontal (Horizontal), ou em ambos os sentidos (Both). Internal Padding X e Internal Padding Y – indicam espaço acrescentado ao próprio componente, aumentando o seu tamanho mínimo, em vez de acrescentado à célula que o contém. Anchor – indica o alinhamento do componente em relação à suas células, caso ele não preencha toda a área alocada a elas. Seus valores possíveis são baseados nos pontos cardeais, como North ou SouthEast. Weight X e Weight Y – Valores maiores do que zero indicam que a célula será expandida para além do seu tamanho mínimo, ocupando o espaço na largura ou altura que sobrar no container. Estas propriedades costumam ser utilizadas apenas quando o container pode ser redimensionado pelo usuário. Insets – indicam espaçamentos a serem inseridos nas quatro bordas da célula, afastando o componente destas bordas. Dois componentes em células adjacentes e com espaçamentos zerados serão exibidos "grudados" um no outro. Dicas do editor visual Aqui estão reunidas algumas dicas que podem tornar mais produtivo o uso do editor visual do NetBeans para a construção de interfaces Swing. A maioria explora o uso do botão direito do mouse na área de desenho ou no inspetor como alternativa à visão de propriedades. Quais propriedades foram alteradas? 12 A visão de propriedades coloca em negrito os nomes das propriedades que estão com valores diferentes do padrão, de modo que fica fácil verificar o que foi customizado em um componente, por exemplo, quando se deseja deixar um outro componente com a mesma aparência. Para retornar propriedades modificadas aos seus valores padrão, abra o customizador da propriedade e clique no botão Reset to Defaults. Alterando o texto de um componente Componentes que exibem textos (como JLabel, JButton e JMenuItem) oferecem em seu menu de contexto a opção Edit Text para edição rápida do texto do componente diretamente na área de desenho. Alterando o layout de um container O gerenciador de layout de um container também pode ser modificado diretamente pelo menu de contexto. Renomeando um componente É importante dar a todos os componentes nomes intuitivos, porque esses nomes são utilizados nas propriedades do container (por exemplo, JFrame, JDialog ou JPanel) que os contém. O menu de contexto do componente fornece a opção de renomear o componente, mas por algum bug ela nem sempre funciona se for utilizada na área de desenho do editor. Por outro lado, ela sempre funciona se utilizada na visão do inspetor. Novas linhas ou colunas em um GridBagLayout No customizador do GridBagLayout, pode-se criar novas linhas e colunas simplesmente arrastando-se o componente para além da parte inferior da grade, ou para 13 além da fronteira direita da grade. No processo pode-se criar também várias linhas e colunas vazias, que poderão depois ser ocupadas por novos componentes. Caso você queira inserir novos componentes no início (ou no meio) da grade, inicie arrastando os componentes mais à direita (ou mais embaixo) para criar novas linhas; e depois arraste os demais componentes para as células criadas, até que fiquem células vazias nas posições desejadas. Figura 1. Esboço da aplicação Todo Figura 2. Criação da janela ListaTarefas 14 Figura 3. Editor visual do NetBeans 4.0 (esquerda) e do 4.1 (direira). Notem a posição diferente do inspetor e a configuração do palete de componentes. Figura 4. Protótipo parcial da janela principal (ListaTarefas); observe o agrupamento dos botões (4, 2, 1) na barra de ferramentas Figura 5. Hierarquia de objetos da janela principal 15 Figura 6. Abas do customizador para o TableModel do JTable na janela principal Figura 7. Menu Arquivo na pré-visualização do editor visual Figura 8. Estrutura completa de menus da janela principal, visualizada no inspetor 16 Figura 9. Escolhendo um ícone para um item de menu ou toolbar Figura 10. Barra de ferramentas do protótipo depois de configurada com os ícones Figura 11. Configurando mnemônicos e aceleradores no NetBeans (propriedades correspondentes destacadas em amarelo) Figura 12. Os menus da aplicação completamente configurados Figura 13. Diálogo de edição de tarefas, “rascunhado” com o Null Layout 17 Figura 14. Customizador de layout do diálogo de edição de tarefas, após a conversão para um GridBagLayout Figura 15. Forma final do diálogo de edição de tarefas Figura 16. Customizador do GridBagLayout do diálogo de edição de tarefas 18 Figura 17. Aplicação Todo com o look-and-feel GTK do Linux Listagem 1. Modificações na classe principal da aplicação para iniciar a janela da listagem de tarefas package todo; import javax.swing.*; import todo.visao.*; public class Main { public Main() {} public static void main(String[] args) { JFrame w = new ListaTarefas(); w.pack(); w.setVisible(true); } } Listagem 2. Código vinculado ao evento actionPerformed no primeiro botão da barra de ferramentas, para exibir o formulário de edição de tarefas import javax.swing.*; // ... private void botaoAdicionarActionPerformed(java.awt.event.ActionEvent evt) { JDialog d = new EditaTarefa(this, true); d.pack(); d.setVisible(true); } 19 Parte 2: JTable, MVC Aplicado e Tratamento de Eventos Saiba como customizar componentes JTable, organizar o tratamento de eventos, estruturar uma aplicação visual para facilitar extensões e manutenções. Nesta edição, damos continuidade à construção da aplicação iniciada na edição anterior, apresentando conceitos fundamentais de desenvolvimento Java e recursos da série 4.x do IDE livre NetBeans. A primeira parte foi focada na programação visual e em como o NetBeans pode ser utilizado para prototipar uma interface com usuário baseada no Swing, além de mostrar características dos principais gerenciadores de layout. Nesta segunda parte, passamos ao editor de código. Vamos customizar o visual da tabela que exibe as tarefas, para indicar com cores diferentes tarefas completadas, atrasadas ou em estado de alerta. Também iremos tratar dos eventos gerados pelos componentes da interface, evoluir a arquitetura e saber pelos componentes da interface, evoluir a arquitetura e saber como evitar que o tratamento de eventos transforme seu código orientado e objetos em “código espaguete”. Arquitetura MVC em aplicações Gráficas Durante a prototipação da interface gráfica, buscamos ficar o máximo possível dentro do editor visual do NetBeans. O objetivo era apenas criar uma “casca” visual para a aplicação que pudesse ser avaliada e discutida com os usuários. Agora vamos começar a colocar lógica por trás dessa casca, permitindo avaliar e testar a real funcionalidade da aplicação. É comum, no desenvolvimento de aplicações visuais, acabar gerando código desorganizado, onde uma modificação em qualquer parte gera efeitos colaterais nos locais mais inesperados; ou onde a simples adição de uma informação extra exige uma cascata de mudanças em várias partes da aplicação. Para evitar isso, vamos adotar uma arquitetura muito popular em aplicações interativas, a arquitetura MVC (Movel-View-Controller, Modelo-Visão-Controlador). A MVC foi usada ou descrita em vários artigos na Java Magazine (em maior parte no contexto de aplicações web). Nela, cada classe tem um papel bem definido: tratar da exibição das informações (Visão); responder a ações do usuário (Controlador); ou cuidar da consistência e persistência dos dados (Modelo). Classes com papéis diferentes são praticamente independentes entre si, e assim é possível modificá-las sem medo de gerar “efeitos colaterais”. O uso da arquitetura MVC também torna fácil identificar onde fazer cada mudança. Para tornar o uso da arquitetura bem explícito, iremos organizar as classes Java em três pacotes: todo.visao, todo.controle e todo.modelo. No pacote visao estão classes gráficas (como janelas ou componentes personalizados), ou classes necessárias para o seu funcionamento. Essas classes não tomam decisões a respeito de como uma operação deve ser realizada – este é o papel das classes do pacote controle, as quais efetivamente respondem aos eventos do usuário (como cliques num item de menu) e decidem qual operação realizar. Já as classes do pacote modelo representam os dados da aplicação, e contêm a inteligência necessária para realizar ações sobre esses dados. Figura 1. Dependências entre componentes no modelo MVC 20 A Figura 1 ilustra o relacionamento entre os pacotes, usando um diagrama UML (o desenho de pasta representa um pacote e agrupa várias classes)1. Observe que o Controlador fica no “meio do caminho” entre a Visão e o Modelo2. Essa separação traz um benefício adicional: ela permite que uma mesma Visão seja reutilizada com vários Modelos contendo as mesmas informações – mas obtendo essas informações de fontes diferentes (ex.: um Modelo baseado em banco de dados e outro acessando arquivos XML). Pelo seu lado, o Modelo fica independente da interface com o usuário (que faz parte da Visão): as classes do Modelo podem, por exemplo, ser utilizadas depois, numa aplicação web ou num MIDlet J2ME. Inteligência em objetos No início da Orientação a Objetos, era comum defender-se a idéia de que um objeto deveria conter toda a “inteligência”relacionada a ele. Mas com o passar do tempo verificouse que essa estratégia poderia levar a objetos “gordos”, concentrando mutas funcionalidades, e com manutenção difícil e baixo desempenho. Hoje, no entanto, se considera uma alternativa válida criar também objetos “burros”, que apenas trafegam informações de um “objeto inteligente” para outro, deixando-os mais independentes entre si. No nosso caso, os “objetos burros” serão Value Objects (VOs)( que são JavaBeans, com atributos e seus métodos get/set, e possivelmente alguma funcionalidade localizada). E os objetos inteligentes são os objetos de Visão, Modelo e Controle. A única classe VO de que necessitamos agrupa todas as informações de uma tarefa, então este será o seu nome. Na nossa aplicação, teremos classes de Visão que sabem representar graficamente uma tarefa, e classes de Modelo que sabem como recuperar e salvar tarefas do bando de dados (ou de um collection etc.). Para simplificar, a classe Tarefa ( e outros VOs que surgirem) podem ser deixados no mesmo pacote das classes de Modelo. Afinal, são as operações implementadas no Modelo que determinam quais informações deverão estar nos VOs. Arquitetura da aplicação Nossas classes de Visão – ListaTarefas (um JFrame) e EditaTarefa (um JDialog) – foram prototipadas no artigo anterior. Agora estamos preocupados em definir a interface externa destas classes, isto é, o que irão expor para o Controlador. Precisamos definir os eventos, que representam ações realizadas pelo usuário, e os métodos para manipular informações encapsuladas em VOs. Teremos apenas uma classe no Controlador, chamada ConsultaEditaTarefas. Em aplicações mais complexas, poderá haver várias classes controladoras. Uma estratégia para começar é criar um controlador para cada conjunto de operações consultar-editar-apagar da aplicação. Nosso exemplo também terá apenas uma classe no Modelo, chamada GerenciadorTarefas. Esta classe é um DAO (Data Access Object) e será responsável por ler e atualizar registros no banco de dados. Aplicações mais complexas terão classes de Modelo que encapsulam processos de negócios em vez de apenas entidades de informação. E irão para o Controlador apenas estas classes de mais alto nível, reservando os DAOs para uso interno. Na Figura 2 temos um modelo UML contendo as classes que iremos desenvolver. Note como todas as classes no diagrama têm dependências em relação ao VO Tarefa. (Mas como este não tem inteligência significativa, ele é com freqüência omitido em diagramas de classes, e na avaliação de dependências entre classes da aplicação). Começando a construção 21 Nossa idéia é prosseguir construindo a aplicação “de cima para baixo”. Iniciamos pela interface com o usuário (Visão) e vamos descendo até chegar à logica de banco de dados (Modelo). Em cada etapa será feito o mínimo de trabalho necessário para que seja possível testar uma nova funcionalidade. Assim, nossos objetos de modelo serão por algum tempo versões temporárias, mantendo os dados em memória sem acessar o bando de dados. A classe Main (não representada na Figura 2) permanece como sendo a classe principal da aplicação, e do projeto no NetBeans. Seu papel é instanciar e conectar as classes de Visão, Modelo e Controle. A Listagem 1 apresenta a classe Tarefa e a Listagem 2, a classe Main. Note que Tarefa fornece alguns métodos utilitários relacionados com a manipulação das datas de conclusão e prazo de alerta. Parece ir contra a recomendação de que um VO não deve ter inteligência, mas estes métodos não dependem de nada externo ao próprio objeto, então este é o seu lugar. A Listagem 3 apresenta a classe GerenciadorTarefas, que implementa um único método: listaTarefas(). Neste ponto, o método constrói um java.util.List contendo objetos Tarefas pré-fabricados para que seja possível testar o código da Visão e do Controlador. Você pode gerar os métodos get/set automaticamente no NetBeans. Depois de definir os atributos da classe Tarefa, clique com o botão direito no editor de código (ou na visão de projeto do NetBeans) e escolha Refactor|Encapsulate Fields. A Listagem 4 mostra a primeira versão da classe controladora ConsultaEditaTarefas. Tudo o que ela faz no momento é passar para a Visão (ListaTarefas) a lista de objetos Tarefa retornada pelo Modelo (GerenciadorTarefas). Note que as classes de Visão devem ter métodos para receber as tarefas, e é responsabilidade da Visão repassar os dados das tarefas para componentes visuais internos, como caixas de texto e componente JTable. Figura 2. Principais classes da aplicação Lista de Tarefas Modelo da aplicação e models do Swing Na maioria dos toolkits gráficos, os objetos visuais de tabela e de lista manipulam apenas arrays de strings; para preenchê-los, os dados devem ser convertidos em strings e inseridos linha a linha (ou célula a célula). Esta abordagem tem dois problemas: aumento do consumo de memória, pois os dados são duplicados na tabela; e maior tempo gasto para inserir um conjunto extenso de dados. No Swing, entretanto, os dados para exibição num componente de tabela (JTable) devem se passados num classe que implementa a interface TableModel. Esta classe fica responsável por fornecer para a tabela os dados das células visíveis, e em informar quando os dados forem modificados. Com isso, não são acrescentadas ou alteradas linhas na tabela em si; essas operações são realizadas no TableModel associado à tabela. O Swing fornece a classe DefaultTableModel, que armazena dados em vetores de vetores (java.util.Vector), emulando a abordagem dos outros toolkits. Isso permite começar rapidamente, mas o recomendado mesmo é criar o seu próprio TableModel, que acessa diretamente os VOs. Dessa forma não há duplicação de dados e se obtêm ganhos de 22 performance, pois a tabela pede ao seu modelo apenas os dados que irá efetivamente utilizar. Todos os componentes do Swing seguem a abordagem descrita: em vez de o próprio componente armazenar as informações que exibe, essa responsabilidade e delegada para um objeto model do Swing. (O uso do nome “model” nas classes e interfaces utilizadas pelos componentes do Swing não é coincidência. Para uma discussão mais aprofundada sobre o assunto, consulte o quadro “Arquitetura MVC e o Swing”.) Listagem 1. VO Tarefa package todo.modelo; import java.util.Date; import Java.util.Calendar; public class Tarefa { private int id; private String descricao; private int prioridade; private Date dataConclusao; private boolean gerarAlerta; private int diasAlerta; private String observacoes; private boolean concluida; private Calendar getDataHojeNormalizada() Calendar hoje - Calendar.getlnstanceC); hoje.set(Calendar.HOUR_OF_DAY. 0); hoje.set(Calendar.MINUTE. 0); hoje.set(Calendar.SECOND. 0); hoje.set(Calendar.MILLISECOND, 0); return hoje; } public boolean isAtrasada() { Date conclusao = getDataConclusao(); if (conclusao == null) return false; else { return conclusao.compareTo( getDataHojeNormalizada().getTime()) < 0; } } public boolean isAlertaAtivo() { Date conclusao = getDataConclusao(); if (!isGerarAletra() || conclusao == null) return false; else { Calendar diaConclusao = Calendar.getlnstance(); diaConclusao.setTime(getDataConclusao()); int dias = getDataHojeNormalizada().get(Calendar.DAY_OF_YEAR) - diaConclusao.get(Calendar.DAY_OF_YEAR); return dias <= getDiasAlerta(); } } public Tarefa() { setConcluida(false); setGerarAlerta(false); } 23 // ... métodos get/set } Listagem 2. Classe principal da aplicação package todo; import import import import javax.swing.*; todo.visao.ListaTarefas; todo.controle.ConsultaEditaTarefas; todo.modelo.GerenciadorTarefas; public class Main { public Main() {} public static void main(String[] args) { ListaTarefas visao = new ListaTarefas(); GerenciadorTarefas modelo = new GerenciadorTarefas(); ConsultaEditaTarefas controle = new ConsultaEditaTarefas( visao, modelo); visao.pack() ; visao.setVisible(true); } } Listagem 3. Classe “fake” de modelo, GerenciadorTarefa package todo.modelo; import java.util.*; import java.text.*; public class GerenciadorTarefas { List tarefas = new ArrayList(); public GerenciadorTarefas() { DateFormat df = DateFormat.getDatelnstance( DateFormat.SHORT); Tarefa umaTarefa = new Tarefa(); umaTarefa.setDescricao(“Entregar coluna para a Java Magazine”); try { umaTarefa.setDataConclusao(df.parse(“05/06/2005”)); } catch(Exception e) { // não faz nada } umaTarefa.setPrioridade(1); umaTarefa.setObservacoes(“Testar exemplo no NB 4.0 e 4.1”); umaTarefa.setGerarAlerta(true); umaTarefa.setDiasAlerta(15); umaTarefa.setConcluida(false); tarefas.add(umaTarefa); //… cria e adiciona mais instâncias de Tarefa … } public List listaTarefas() { 24 return tarefas; } private boolean stringVazia(String str) { return str == null || str.trim().length() == 0; } private void validaTarefa(Tarefa tarefa) | throws ValidacaoException { if (stringVazia(tarefa.getDescricao())) throw new ValidacaoException( “Deve ser fornecida uma descrição para a tarefa”); } public void adicionaTarefa(Tarefa tarefa) throws ValidacaoException validaTarefa(tarefa); tarefas.add(tarefa); } public void editaTarefa(Tarefa tarefa) throws ValidacaoException ( validaTarefa(tarefa); } public void removeTarefa(Tarefa tarefa) tarefas.remove(tarefa); } } Listagem 4. Classe controladora ConsultaEditaTarefas package todo.controle; import todo.visao.*; import todo.modelo.*; public class ConsultaEditaTarefas { private ListaTarefas visao; private GerenciadorTarefas modelo; public ConsultaEditaTarefas(ListaTarefas visao, GerenciadorTarefas modelo) { this.visao = visao; this.modelo = modelo; listaTarefas(); } public void listaTarefas() { visao.setListaTarefas(modelo.listaTarefas)); } } As classes model do Swing devem ser tratadas como sendo internas à Visão, permanecendo invisíveis ao restante da aplicação. A Listagem 5 apresenta a classe TarefasTableModel, que permite a exibição dos dados de uma List em um JTable. A Listagem 6 mostra as modificações feitas na classe ListaTarefas para utilizar este TableModel. (O método getValoresTarefa() adicionado à nossa classe model será necessário depois para customizar a formatação da tabela.) 25 Sempre que criar no NetBeans classes que estendem outras classes ou implementar interfaces você pode usar o comando Tool|Override Methods para gerar a declaração dos métodos a serem redefinidos ou implementados. Chegamos ao ponto em que é possível executar a aplicação e testar o seu funcionamento. O conjunto de classes criadas até o momento é ilustrado na Figura 3, que é uma captura da visão de projetos do NetBeans. O resultado esperado é o apresentado na Figura 4, onde o JTable exibe os dados dos VOs pré-fabricados retornados pelo GerenciadorTarefas. Figura 3. Classes da aplicação após a inserção de um TableModel customizado Listagem 5. TableModel para exibição de um List package todo.visao; import import import import java.util.*; java.text.*; javax.swing.table.*; todo.modelo.*; public class TarefasTableModel extends AbstractTableModel { private List tarefas; private DateFormat df = DateFormat.getDatelnstance( DateFormat.SHORT); public TarefasTableModel(List tarefas) { this.tarefas = tarefas; public Object getValueAt(int rowlndex. int columnIndex) { Tarefa umaTarefa = tarefas.get(rowIndex); switch(columnIndex) { case 0: return umaTarefa.getPrioridade(); case 1: return umaTarefa.getDescricao(); case 2: return umaTarefa.isGerarAlerta(): case 3: return umaTarefa.getDataConclusao(); } return null; } public int getRowCount() { return tarefas.size(); } public int getColumnCount() { return 4; } 26 public Tarefa getValoresTarefa(int rowIndex) { return tarefas.get(rowIndex); } } Listagem 6. Modificações em ListaTarefas para utilizar a TableModel package todo.visao; // ... imports public class ListaTarefas extends javax.swing.JFrame { public void setListaTarefas(List tarefas) { this.tarefas.setModel(new TarefasTableModel(tarefas)); } // … código gerado pelo editor visual do NetBeans } Customizando as colunas da tabela Visualmente, a janela mostra na Figura 4 foi um retrocesso em relação ao protótipo construído no artigo anterior, pois não apresenta mais os títulos das colunas. Isso aconteceu porque, o usar um TableModel customizado, toda a configuração feita no editor visual do NetBeans sobre um DefaultTableModel passa a ser ignorada durante a execução da aplicação. Algumas configurações de colunas poderiam ser inseridas no próprio TableModel; entretanto, outras, como a largura das colunas, exigem a criação de um ColumnModel, que agrega por sua vez uma coleção de objetos TableColumn (cada TableColumn indica o título da coluna, sua largura, se ela pode ser redimensionada etc.). Optamos então por colocar todas as configurações no ColumnModel, em vez de dividi-la entre as duas classes. Queremos que nosso ColumnModel customizado, ao qual chamaremos de TarefasCollumnModel (Listagem 7) exiba as colunas com títulos “Prioridade”, “Alerta?” e “Data de Conclusão”, com larguras pré-fixadas e grandes o suficiente para exibição do seu conteúdo. Queremos também que toda a largura restante na tabela seja ocupada pela coluna de descrição. O segredo é configurar as larguras das colunas de acordo com a fonte de caracteres configurada para tabela (a fonte default vai depender do look-and-feel padrão do Swing em seu sistema operacional). Infelizmente um ColumnModel não tem acesso ao seu JTable, de modo que foi necessário passar o objeto FontMetrics da fonte no construtor para o ColumnModel. Usamos a largura dos caracteres “0” e “M” como referência para a largura dos campos, e tivemos o cuidado de garantir que as colunas coubessem seus títulos. A Listagem 8 apresenta as modificações necessárias na classe ListaTarefas para utilizar o novo ColumnModel; a Figura 5 mostra o resultado da execução da aplicação modificada. 27 Ainda falta ajustar a formatação individual de cada célula, o que irá exigir a criação de uma terceira classe auxiliar para o componente JTable. Figura 4. Aplicação utilizando o novo TableModel Figura 5. Visual com colunas customizadas Listagem 7. Customização das colunas da tabela package todo.visao; import java.awt.*; import javax.swing.table.*; public class TarefasColumnModel extends DefaultTableColumnModel { private TableColumn criaColuna(int columnIndex, int largura, FontMetrics fm, boolean resizeable, String titulo) { int larguraTitulo = fm.stringWidth(titulo +” “); if (largura < larguraTitulo) largura = larguraTitulo; TableColumn col = new TableColumn(columnIndex); col.setCellRenderer(null); col.setHeaderRenderer (null) ; col.setHeaderValue(titulo); col.setPreferredWidth(largura); if (!resizeable) { col.setMaxWidth(largura); col.setMinWidth(largura); } col.setResizable(resizeable); return col; } public TarefasColumnModel(FontMetrics fm) { int digito = fm.stringWidth(“0”); int letra - fm.stringWidth(“M”); addColumn(criaColuna(0, 3 * digito, fm, false, “Prioridade”)); addColumn(criaColuna(1, 20 * letra, fm, true, “Descrição”)); addColumn(criaColuna(2, 3 * letra, fm, false, “Alarme?”)); addColumn(criaColuna(3, 10 * digito, fm, false, “Conclusão”)); 28 } } Listagem 8. Vinculando o TarefasColumnModel ao JTable em ListaTarefas package todo.visao; // ... imports public class ListaTarefas extends javax.swing.JFrame { public void setListaTarefas(List tarefas) { this.tarefas.setModel(new TarefasTableModel(tarefas)); public ListaTarefas() { initComponents(); tarefas.setAutoCreateColumnsFromModel(false); FontMetrics fm = tarefas.getFontMetrics(tarefas,getFont()); tarefas.setColumnModel(new TarefasColumnModel(fm)); } // ... código gerado pelo editor visual do NetBeans } Renderizando células O JTable é um componente genérico para exibição de dados numa grade, que pode ser adaptado a duas situações distintas: quando os dados são uniformes dentro de cada coluna (típico de aplicações de banco de dados). quando cada célula numa mesma coluna pode ser um tipo diferente (como numa planilha eletrônica). Nossa aplicação se enquadra na primeira situação. Em ambas as situações, a formatação dos valores individuais será feita por um TableCellRenderer. Classes que implementam esta interface podem ser vinculadas a uma coluna da tabela por meio do ColumnModel (primeira situação) ou a um tipo específico de dados (segunda situação)3. Optamos por definir um único renderizador de célula para as quatro, o TarefasCellRender (Listagem 9). Nosso renderizador é simples, pois estende o DefaultTableCellRenderer fornecido pelo Swing. Para utilizá-lo, basta mudar, em TarefasColumnModel, a linha: col.setCellRenderer(null); para: col.setCellRenderer(new TarefasCellRenderer()); Quando a célula for desenhada, será acionado o TableCellRenderer configurado para o tipo ou para a coluna. Esse renderizador deve retornar um JComponent que será responsável pela formatação do valor, a nossa implementação, assim como a padrão do Swing, utiliza um único JLabel para o desenho de todas as células. Desta forma um JTable com milhares de células não irá criar o mesmo números de objetos, sendo eficiente em consumo de memória e CPU. Nosso objetivo é configurar a aparência das linhas como um todo e não apenas de células individuais. Por isso é necessário primeiro referenciar o JTable que chama o renderizado; depois acessamos o TableModel e obtemos a Tarefa correspondente à linha da célula sendo desenhada. O método corTarefa() utiliza os métodos isAtrasada(), isAlertaAtivo(),e isConcluida() de Tarefa para selecionar uma cor de fundo diferente para cada linha. O código de cores utilizado é o seguinte: vermelho:tarefa atrasada, isto é, que já passou da sua data de conclusão 29 amarelo: tarefa em alerta, isto é, que foi configurada para gerar alertas alguns dias antes da sua data de conclusão azul: tarefa concluída branco: tarefa que não está em nenhum dos outros estados. Caso a célula esteja selecionada, é utilizada a cor cinza escura para o fundo da célula e o código de cores é utilizado para a cor de texto. Listagem 9. Renderizador para as células de uma Tarefa package todo.visao; // ... imports private class TarefasCellRenderer extends DefaultYableCellRenderer{ public TarefasCellRenderer() { super();} private Color corTarefa(Tarefa tarefa) { if (tarefa.isConcluida()) return Color.CYAN; else { Date conclusao = tarefa.getDataConclusao(); Date hoje = new Date(); if (conclusao = null) return Color.WHITE; else if (tarefa.isAtrasada()) return Color. PINK; else if (tarefa.isAlertaAtivo()) return Color.YELLOW; else return Color.WHITE; } } private Object formata(Object value) { if (value instanceof Date) { Date data = (Date)value; DateFormat df = DateFormat.getDateInstance(); return df.format(data); } else if (value instanceof Boolean) return (Boolean)value ? “S” : “N”; else return value; } public Component getTableCellRendererComponent( javax.swing.JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { value = formata(value); JLabel label = (Jlabel)super.getTableCellRendererComponent( table, value, isSelected, hasFocus, row, column); if (column ! = 1) label .setHorizontalAlignment(JLabel .CENTER); Tarefa tarefa = ((TarefasTableModel)table.getModel()) .getValoresTarefa(row); if (isSelected) { 30 label.setForeground(corTarefa(tarefa)); label.setBackground(Color.GRAY); } else { label.setForeground(Color.BLACK); label.setBackground(corTarefa(tarefa)); } return label; } } É recomendado que toda customização de cores num componente Swing mude ao mesmo tempo a cor de texto e a de fundo, em ambos os estados (normal e selecionado). Caso contrário, poderá haver uma combinação ilegível, devido às cores definidas pelo tema dos sistema operacional, ou do look-and-feel escolhido pelo usuário. Figura 6. Aparência com TableCellRenderer customizado A mesma classe TarefasCellRenderer é utilizada para renderizar todas as colunas. Por isso foi criado o método formata(), que utiliza os valores “S” e “N” para colunas booleanas e um DateFormat para colunas do tipo Date. O resultado final pode ser visto na Figura 6. A Figura 7 apresenta a situação do projeto com as duas classes adicionadas, o modelo de colunas e o renderizador de células. Agora a listagem de tarefas está em sua forma final, podemos dar interatividade à aplicação. Eventos internos e externos O próximo passo será permitir que sejam adicionadas e editadas tarefas – mas sem ainda salvá-las num banco de dados. Para implementar esta funcionalidade, é importante entender que nem todos os eventos devem ser tratados da mesma forma na aplicação. Figura 7. Classes da aplicação ao fim da customização do JTable 31 Podemos classificar os eventos da interface com o usuário em duas categorias: Eventos internos – São consumidos pelas classes de Visão e não é necessário repassá-los ao Controlador. Eventos internos afetam apenas a aparência e o estado das classes de Visão; eles não levam à execução de métodos do Modelo. Por exemplo, ao selecionar uma tarefa na lista, devem ser habilitados os botões de editar, remover e marcar como concluída. Quando não houver tarefa selecionada, esses botões devem ser desabilitado, tudo sem passar pelo Controlador ou Modelo. Eventos externos – Correspondem às operações realizadas pelo usuário na aplicação, devendo, portanto, ser repassados às classes de Controlador. Estas, por sua vez, respondem chamando métodos das classes de Modelo e depois comandando a atualização dos dados exibidos na Visão. Por exemplo, a adição de uma nova tarefa envolve a chamada ao método na classe do Modelo que realiza esta operação, seguindo pela entrega de uma novas lista de tarefas para a classe de Visão selecionada. Para implementar os eventos internos, podemos utilizar os recursos do editor visual do NetBeans. No IDE, um clique duplo em qualquer componente cria um método para o tratamento do seu evento padrão (geralmente o evento “Action”) e abre o editor de código para que possa programar o corpo do método. Já os eventos externos devem ser encaminhados ao Controlador. Eventos internos: seleção de linhas na tabela Vamos iniciar pelos eventos internos, que são mais simples por envolverem código em apenas uma classe. Vamos controlar o estado dos botões e menus da classe de Visão ListaTarefas. A operação de edição será habilitada apenas quando houver uma única tarefa selecionada; já a marcação e a remoção podem atuar sobre múltiplas tarefas selecionadas, portanto serão habilitadas mesmo havendo uma seleção múltipla. Já os botões de adicionar tarefa, de exibir tarefas concluídas, e os de ordenação podem estar sempre habilitados. Não será possível criar o código para o tratamento dos eventos de seleção através do editor visual do NetBeans. O motivo é que os eventos não são gerados pelo componente JTable, mas pelo objeto ListSelectionModel vinculado a ele – e este objeto não é acessível pelo inspetor no editor visual. Então temos que escrever o código do listener para o evento de seleção, incluindo a lógica que habilita ou desabilita os botões apropriados, e não esquecendo de alternar também o estado dos itens de menus correspondentes. A Listagem 10 apresenta as mudanças necessárias na classe ListaTarefas. É fácil identificar outros eventos internos das duas classes de Visão. Por exemplo, em EditaTarefa, a marcação do checkbox “GerarAlerta” deve habilitar o campo de texto “diasAlerta”. Ou então, o botão “Cancela”, que simplesmente fecha (dispose()) o JDialog, sem gerar nenhum outro evento para o controlador. Deixamos a codificação destes eventos simples como exercício para o leitor. Neste ponto, compile e execute a aplicação e verifique o seu funcionamento antes de avançar. Listagem 10. Modificações em ListaTarefas para habilitar e desabilitar os botões e menus conforme a seleção na tabela package todo.visao; // ... imports public class ListaTarefas extends javax.swing.JFrame { private void habilitaEdicao(boolean habilitado) { botaoEditar.setEnabled(habilitado); menuEditar.setEnabled(habilitado); } 32 // habilitaMarcacao() e habilitaRemocao() // seguem o mesmo modelo de habilitaEdicao() private ListSelectionListener selectionListener = new ListSelectionListener() { public void valueChanged(ListSelectionEvent e) { habilitaEdicao(tarefas.getSelectedRowCount() ==1); habilitaMarcacao(tarefas.getSelectedRowCount() >= 1); habilitaRemocao(tarefas.getSelectedRowCount() >= 1); } }; public void setListaTarefas(List tarefas) { this.tarefas.setModel(new TarefasTableModel(tarefas)); } public ListaTarefas() { initComponents(); tarefas.setAutoCreateColumnsFromModel(false); FontMetrics fm = tarefas.getFontMetrics(tarefas.getFont()); tarefas.setColumnModel(new TarefasColumnModel(fm)); tarefas.getSelectionModel().addListSelectionListener( selectionListener); habilitaEdicao(false); habilitaRemocao(false); } // ... código gerado pelo editor visual do NetBeans } Eventos externos: adicionando e editando tarefas Para não poluir as classes de Visão com dezenas de métodos no estilo “addEditarTarefaListener()”, “addAdicionarTarefaListener” etc. decidimos expor em cada classe de Visão apenas o evento ActionEvent à sua classe controladora. A classe ActionEvent inclui a propriedade actionCommand, que espelha a propriedade de mesmo nome do componentes que gerou o evento. Dessa forma, o controlador pode saber exatamente quando a operação foi requisitada pelo usuário, sem, ter que definir listeners adicionais. A Listagem 11 apresenta a classe ConsultaEditaTarefas modificada para tratar apenas dos eventos de adicionar e editar tarefa. A Listagem 12 mostra as modificações nas classes ListaTarefa para gerar o evento (lembre-se de configurar os actionCommands no editor visual). A Listagem 13 apresenta as modificações no diálogo EditaTerefas, de modo que os botões de Salvar e de Remover também gerem eventos ActionEvent para o controlador. Como o código de gerenciamento dos listeners e de geração de evento é o mesmo para ambas as classes, ele foi movido para uma classe utilitária ActionSupport, apresentada na Listagem 14. Observe que é o Controlador que sabe se o diálogo foi instanciado para adição de uma nova tarefa, ou para a edição de uma nova tarefa existente. Mas o diálogo necessita receber esta informação para desabilitar o botão de remover em caso de uma tarefa nova. Foram omitidas as listas das classes ModeloException e ValidacaoException, ambas no pacote todo.modelo. Estas classes simplesmente sinalizam erros ocorridos internamente nas classes de modelo; representam erros de “alto nível” da aplicação, em vez de erros de “baixo nível” como IOException ou NumberFormatException. Note que 33 as áreas de mensagens das duas classes de Visão são utilizadas para exibir a mensagem de erro fornecida pelas exceções vindas do Modelo. Listagem 11. Classe controladora modificada para tratar da adição e edição de tarefas package todo.controle; //... imports public class ConsultaEditaTarefas implements ActionListener { // ...atributos omitidos public ConsultaEditaTarefas(ListaTarefas visao. GerenciadorTarefas modelo) { this.visao = visao; this.modelo = modelo; visao.addActionListener(this); listaTarefas(); } public void listaTarefas() { visao.setListaTarefas(modelo.listaTarefas()); public void actionPerformed(java.awt.event.ActionEvent e) { try { if (e.getActionCommand().equals(“novaTarefa”)) { editaTarefa(true); } else if (e.getActionCommand().equals(“editarTarefa”)) { editaTarefa(false); } else if (e.getActionCommand().equals(“salvarTarefa”)) { salvaTarefa(); } else visao.setMensagem(“Não implementado: [" + e.getActionCommand() + “J”, true); } catch (Exception ex) { visao.setMensagem(ex.getMessage(), true); } } private void editaTarefa(boolean novaTarefa) { editaTarefaDialog = new EditaTarefa(visao.true); editaTarefaDialog.setNovaTarefa(novaTarefa); if (novaTarefa) editaTarefaDialog.setTarefa(new Tarefa( )); else editaTarefaDialog.setTarefa(visao. getTarefaSelecionada()); editaTarefaDialog.addActionListener(this); editaTarefaDialog.setVisible(true); } 34 private void salvaTarefa() { Tarefa tarefa = editaTarefaDialog.getTarefa(); try { if (editaTarefaDialog.isNovaTarefa()) modelo.adicionaTarefa(tarefa); else modelo.editaTarefa(tarefa); editaTarefaDialog.dispose(); editaTarefaDialog = null; listaTarefas(); } catch (ModeloException e) { editaTarefaDialog.setMensagem(e.getMessage(), true); } } } Listagem 12. Modificações em ListaTarefa para gerar o evento Action para o Controlador package todo.visao; // ... imports public class ListaTarefas extends javax.swing.JFrame { private ActionSupport actionSupport = new ActionSupport(this); public void addActionListener(ActionListener listener) { actionSupport.addActionListener(listener); } public void removeActionListener(ActionListener listener) { actionSupport.removeActionListener(listener); } public void setMensagem(String msg, boolean isErro) { status.setText(msg); if (isErro) status.setForeground(Color.RED); else status.setForeground(Color.DARK_GRAY); } public void setListaTarefas(List tarefas) { this.tarefas.setModel(new TarefasTableModel(tarefas)); } public ListaTarefas() { initComponents() ; tarefas.setAutoCreateColumnsFromModel(false); FontMetrics fm = tarefas.getFontMetrics( tarefas. getFont()); tarefas.setColumnModel(new TarefasColumnModel(fm)); menuAdicionar.addActionListener(actionSupport); menuEditar.addActionListener(actionSupport); botaoAdicionar.addActionListener( actionSupport) ; botaoEditar.addActionListener( actionSupport) ; 35 } // ... Código gerado pelo NetBeans } O mesmo controlador responde aos eventos, tanto do JFrame ListaTarefas quanto do JDialog EditaTarefa. Isto faz sentido porque o JDialog é apenas parte de um processo iniciado pelo JFrame, e não uma classe de Visão independente. Assim também podemos manter o diálogo aberto em caso de erros de validação dos dados digitados pelo usuário. No final das contas, temos no código do controlador um grande switch, onde é feita a decisão de qual método da classe de Modelo chamar baseado no valor do actionCommand do evento. Numa aplicação maior, esse código poderia ser parametrizado, como sugere o quadro “Frameworks MVC para aplicações gráficas”. Mais uma vez estamos em condições de rodar a aplicação e testar a funcionalidade recém-acrescentada. A forma final do projeto está na Figura 8. Estamos quase com a aplicação pronta, exceto pelo fato de que as edições são perdidas quando ela é encerrada. A solução para essa e outras questões será o assunto da próxima parte. Figura 8. Classes da aplicação após o tratamento dos eventos de adicionar e editar tarefas. Listagem 13. Modificações em EditaTarefa para gerar o evento Action para o controlador package todo.visao; // ... imports public class EditaTarefa extends javax.swing.JDialog{ private Tarefa tarefa; private boolean novaTarefa; public void setNovaTarefa(boolean novaTarefa) { this.novaTarefa - novaTarefa; remover.setEnabled(!novaTarefa); if (isNovaTarefa()) setMensagem(“Forneça os dados para a nova tarefa”. false); else setMensagem(“Alterando os dados da tarefa”. false); } public boolean isNovaTarefa() { return novaTarefa; 36 public void setMensagem(String msg. boolean isErro) { // ... igual ao correspondente em ListaTarefa } public void setTarefa(Tarefa tarefa) { this.tarefa = tarefa; descricao.setText(tarefa.getDescricao()); prioridade.setValue(tarefa.getPrioridade()); // ... demais atributos omitidos } public Tarefa getTarefa() { tarefa.setDescricao(descricao.getText()); tarefa.setPrioridade(((Number)prioridade.getValue()), intValue()); // ... demais atributos omitidos return tarefa; } // actionSupport(), addActionListener e // removeActionListener() iguais aos de ListaTarefas ... public EditaTarefa(java.awn.Frame parent, boolean modal) { super(parent, modal); initComponents(); salvar.addActionListener(actionSupport); remover.addActionListener(actionSupport); } // … código gerado pelo editor visual do NetBeans } Listagem 14. Classe utilitária ActionSupport package todo.visao; // .. imports public class ActionSupport implements ActionListener { private Window window; public ActionSupport(Window window) { this.window = window; } private List listeners = new ArrayList< ActionListener>(); public void addActionListener(ActionListener listener) { listeners.add(listener); } public void removeActionListener(ActionListener listener) { listeners.remove (listeners) ; } public void fireActionEvent(ActionEvent e) { Iterator it = listeners.iterator(); while (it.hasNext()) { 37 ActionListener listener = it.next(); listener.actionPerformed(new ActionEvent(window, ActionEvent.ACTION_PERFORMED, e.getActionCommand())); } } public void actionPerformed(ActionEvent e) { fireActionEvent(e); } } Conclusões Neste artigo vimos como configurar um componente JTable do Swing, que possui flexibilidade de customização sem igual em outros toolkits gráficos. Vimos também que a plena exploração das capacidades desse componente exige a criação de novas classes em vez de simplesmente definir propriedades no editor visual. E mostramos como tratar eventos gerados pelos componentes gráficos, sem cair no “código espaguete” - mantendo a separação de responsabilidades proposta pela arquitetura MVC. O próximo artigo da série irá demostrar como filtrar o conteúdo da lista de tarefas e como armazenar as tarefas num banco de dados, além de apresentar um componente para a seleção visual de datas. ________________________________________________________ [1] – Os diagramas UML neste artigo foram criados com a aplicação Java. [2] – Nesta artigo estamos usando Visão, Modelo e Controle praticamente como sinônimos para os pacotes de mesmo nome; em aplicações mais complexas, esses pacotes seriam quebrados em vários.livre ArgoUML (argouml.org). [3] – Neste caso, que não é ilustrado pela aplicação de exemplo, o vínculo é configurado por meio de um atributo do próprio JTable, um Map, onde as chaves são objetos Class (como String.class, Data.class) e os valores são os objetos TableCellRenderer criados pelo desenvolvedor. Arquitetura MVC e Swing O Swing foi arquitetado de acordo com o modelo MVC. Cada classe ou componente visual que manipulamos (por exemplo um JButton) é na verdade a agregação de um Controlador, que obtém dados de um Modelo, com uma Visão que é fornecida pelo lookand-feel atual. Esta visão é chamada de “UI Delegate”, indicando que é ela que realmente desenha o componente no vídeo. A Figura Q1 ilustra o uso da arquitetura MVC no Swing. (Note que neste diagrama não estão representados nomes de classes ou interfaces reais, mas apenas uma descrição da arquitetura que é aplicada individualmente a cada tipo de componente.) Nos casos mais simples, o programador não precisa fornecer a classe model para um componente Swing, pois o próprio componente instancia sua classe model padrão (ex.: DefaultTableModel) e oferece métodos utilitários para armazenar valores nessa instância. Assim o programador fica com a impressão de estar lidando com uma única classe virtual, e essa impressão é reforçada num editor visual como o do NetBeans. Alguns tipos de models do Swing são compartilhados entre vários tipos diferentes componentes. Por exemplo, uma barra de rolagem e um spinner podem utilizar o mesmo modelo (que indica os limites superior e inferior, e os incrementos para o valor armazenado). É possível também que um componente esteja associado ao mesmo tempo a vários models diferentes, por exemplo um que representa os dados propriamente ditos e outro que indica os elementos selecionados pelo usuário. É o que acontece com o JTable, que tem um TableModel um ListSelectionModel e um ColumnModel. 38 Um erro comum, entretanto, é acreditar que as classes model do Swing devem ser usadas como classes de Modelo de aplicação MVC. Não é o caso, pois estamos lidando com níveis de abstração diferentes (a aplicação como um todo e o toolkit gráfico específico). Caso as classes de Modelo da aplicação sejam construídas segundo os models do Swing elas ficarão dependentes da Visão, ou seja, haverá uma independência grande e indesejada do Modelo com a Visão. Figura Q1. Visão geral da arquitetura MVC, conforme utilizada pelos componentes do Swing Frameworks MVC para aplicações gráficas O leitor com experiencia em frameworks MVC para aplicações web, como o Struts, irá logo imaginar que o grande switch no Controlador do nosso exemplo poderia ser substituído por um arquivo de configuração. Esta é uma das idéias básicas em frameworks par aplicações gráficas, como o NetBeans Platform (netbeans.org/products/platform). (O NetBeans Platform é uma separação do código do Controlador MVC do NetBeans do restante do IDE, de modo que ele possa ser utilizado em outros tipos de aplicações.) Um exemplo popular de aplicação construída desta forma é o MC4J (mc4j.sf.net), um console para gerenciamento de aplicações que suportam o padrão JMX. Vale a pena conhecer e estudar o código dessa ferramenta open source. Infelizmente, frameworks MVC para aplicações gráficas ainda não atingiram o mesmo nível de popularidade que têm em aplicações web. Talvez isso seja por conta da facilidade em construir aplicações simples num editor visual, sem se preocupar com a estruturação do código, o que pode fazer com que considerem pequeno o ganho em produtividade se fosse adotado um framework. Mas isso parece estar finalmente mudando, com o crescimento de projetos relacionados, do Eclipse.org e da comunidade JavaDesktop no portal java.net. Links http://java.sun.com/products/jfc/tsc/articles/architecture Documento que descreve a arwquitetura do Swing e sua aplicação do padrãoMVC. http://netbeans.org Site oficial do Netbeans 39 Parte 3: Banco de Dados e Preferências dos Usuários Nesta parte concluímos a aplicação, implementando suporte a preferências e acesso a banco de dados, e fazendo ajustes finais Neste artigo completamos aplicação de Lista de Tarefas (“Todo”), que iniciamos nesta coluna na Edição 25 – um exemplo completo do desenvolvimento de uma aplicação gráfica baseada no Swing, utilizando os recursos do IDE livre NetBeans. O primeiro artigo desta série demonstrou como usar os recursos do editor visual do NetBeans para prototipar a interface como o usuário. O segundo mostrou como customizar a exibição de dados em JTable do Swing e como organizar o tratamento de eventos na aplicação, de acordo com a arquitetura Model-View-Controller. Este último artigo mostra como filtrar as informações exibidas na tabela e como ler e gravar as informações em um banco de dados relacional. Para manter a aplicação simples de instalar e distribuir, será utilizado o banco de dados HSQLDB (apresentado na Edição 7). Dessa forma, a aplicação final será formada por apenas dois pacotes jar: o Todo.jar gerado pelo NetBeans, contendo as classes da aplicação; e o hsqldb.jar do HSQLDB. O quadro “Configurando o projeto no NetBeans” descreve como configurar o IDE para uso do HSQLDB durante o desenvolvimento da aplicação. E o leitor que preferir utilizar um banco de dados diferente pode consultar o quadro “Experimentando com outros bancos”. Arquitetura da aplicação No artigo anterior, chegamos a uma aplicação bastante funcional, que permitia edição e consulta de tarefas armazenadas em memoria, mas sem ainda persistir os dados em disco. Utilizamos a arquitetura MVC, pela qual as classes da aplicação devem atuar apenas num dos seguintes papéis: gerenciamento dos dados (Modelo), visualização dos dados (Visão), ou resposta às ações do usuário (Controle). Criamos um pacote para cada parte: o diagrama de classes UML da Figura 1 representa as principais classes desses pacotes. Figura 1. Principais classes da aplicação de exemplo, desenvolvidas nas duas partes desta série. As classes em azul-claro são neste artigo, e a classe GerenciadorTarefas é praticamente reescrita. No pacote todo.modelo, temos a classe Tarefa, construída com um VO (Value Object, um “repositório de dados” com pouca ou nenhuma inteligência própria). Há também a classe GerenciadorTarefas, cuja versão inicial tinha o objetivo de fazer uma “simulação” da persistência mantendo os dados em memória, pois o foco estava nas classes de visão e controle. Nesta edição vamos expandir a classe GerenciadorTarefas para conter o código de persistência num banco de dados. Também será criada uma nova classe de modelo, 40 chamada Parametros, contendo os dados de configuração da aplicação – por exemplo, o diretório onde são salvos os arquivos do bando de dados. O pacote todo.visao contém duas janelas que foram prototipadas na primeira parte, ListaTarefas (um JFrame) e EditaTarefas (um JDialog), além de várias classes auxiliares. A Figura 2 reapresenta as duas janelas, para que o leitor que não tenha idéia da aparência e funcionalidade da aplicação. O pacote todo.controle contém uma única classe, ConsultaEditaTarefas, que responde às ações do usuário para criar, editar ou listar tarefas. Essas classe delega as ações em si para a classe de modelo GerenciadorTarefas e comanda a atualização das classes de visão, quando necessário. Neste artigo será criada outra classe controladora, chamada CriaAbreListaTarefas, responsável por criar novos bancos de dados existentes em outras localizações. Ela também receberá as funcionalidades de “miscelânea” da aplicação, por exemplo a exibição da caixa “Sobre”. Figura 2. Janelas da aplicação Lista de Tarefas Os três pacotes contêm ainda uma série de classes auxiliares não descritas aqui, por exemplo, exceções customizadas ou modelos para o JTable do Swing. Elas não afetam a arquitetura da aplicação e na maioria dos casos são utilizadas por classes fora dos respectivos pacotes. A Figura 3 apresenta as classes da aplicação, na visão de projetos do NetBeans. O quadro “Todas as classes do exemplo” apresenta uma breve descrição do papel de cada uma. Figura 3. Todas as classes da aplicação final, na visão de projeto do NetBeans. 41 Acesso ao banco de dados A nova versão da classe GerenciadorTarefas foi construída como um DAO. A idéia é que as demais classes da aplicação não tenham conhecimento do uso de banco de dados relacionais ou de outra tecnologia de armazenamento e recuperação de informações – elas apenas chamam os métodos da classe DAO para ler e gravar objetos. Nosso DAO fornece métodos como listaTarefas() e editaTarefa(), que serão chamados pelo controlador apropriado (no caso, ConsultaEditaTarefas) de acordo com a operação solicitada pelo usuário. A Listagem 1 fornece o código completo dessa classe, que será detalhado a seguir. A nova classe de modelo irá interagir com a classe Parametros para obter as configurações de acesso ao banco de dados, que serão utilizadas em conecta() para criar uma conexão ao HSQLDB. A conexão é mantida como variável de instância (atributo) da classe DAO. É fornecido também o método desconecta(), para que a aplicação possa fechar a conexão quando for encerrada. O próprio HSQLDB cria automaticamente uma base de dados vazio no momento da conexão, caso o arquivo referenciado na URL JDBC não exista. Como padrão, estamos utilizando a base dB/todo (que corresponde ao arquivo dB/todo.script) no diretório pessoal do usuário, que será o $HOME em sistemas Linux ou a pasta correspondente em sistemas Windows. Para simplificar a utilização, o próprio DAO irá criar as tabelas na base de dados, caso elas não existam. Isso é feito pelos métodos privativos existemTabelas() e criaTabelas(). Visando simplificar a escrita dos métodos de consulta e alteração de dados propriamente ditos, são fornecidos os métodos auxiliares finaliza(), executa(), prepara(), consulta() e altera(), que encapsulam seqüências comuns chamadas JDBC. Todos eles, exceto os dois últimos, poderiam ser movidos para uma superclasse abstrata numa aplicação com vários DAOs. Os métodos consulta() e altera() seriam praticamente iguais, variando apenas quanto aos nomes dos campos, em outras classes DAO de uma aplicação maior. O método finaliza() tem importância especial, pois temos que garantir que os recursos do banco de dados sejam liberados o quanto antes, mesmo que ocorram exceções durante o acesso. Todos os métodos de acesso ou alteração devem conter uma cláusula finally que chama finaliza() (veja por exemplo o método executa()). Os métodos específicos de acesso e alteração de dados – adicionaTarefa(), editatarefa(), marcaComoConcluida(), removeTarefa(), listaTarefas() e listaTarefasComAlarme() – são todos escritos chamando a finaliza() é garantia em caso de erros. Por fim, o DAO fornece o método validaTarefa(), que é chamado pelo controlador antes de se inserir ou modificar uma tarefa. Isso permite que a operação seja abortada, e o usuário informado de que houve erros na digitação dos atributos da tarefa. Observe que os métodos da classe de todo.modelo, nunca retornam exceções de baixo nível ao controlador, por exemplo SQLException ou IOException. São retornadas subclasses de ModeloException, como a BancoDeDadosException. Estas classes, por sua vez, encapsulam a exceção de baixo nível como “causa” (acessível pelo método getCause() de Exception), de modo que a informação esteja disponível durante a depuração da aplicação. Isso foi feito para que o controlador fique completamente isolado de qualquer conhecimento relativo ao banco de dados ou outro mecanismo de persistência. O mesmo estilo de programação poderia ser utilizado caso fosse empregada outra forma de persistência, por exemplo arquivos XML. 42 Listagem 1. Classe de modelo GerenciadorTarefas package todo.modelo; import import import import import java.io.*; java.sql .*; java.text.*; java.util.List; java.util.ArrayList; public class GerenciadorTarefas { private Parametros params; private Connection con; private Statement stmt; private ResultSet rs; public GerenciadorTarefas(Parametros params) throws BancoDeDadosException { this.params = params; conecta(); } public void reconecta(String database) throws BancoDeDadosException { desconecta(); params.setDatabase(database); conecta(); } private void conecta() throws BancoDeDadosException { try { Class.forName(params.getJdbcDriver()); con = DriverManager.getConnection(params.getJdbcUrl(). "sa", ""); if (!existemTabelasC) criaTabelas() ; } catch (BancoDeDadosException e) { throw new BancoDeDadosException( "Não foi possível criar as tabelas no banco de dados", e.getCause()); } catch (ClassNotFoundException e) { throw new BancoDeDadosException( "Não foi possível carregar o driver do banco de da dos" , e); } catch (SQLException e) { throw new BancoDeDadosException( Não foi possível conectar ao banco de dados" , e); } } private boolean existemTabelas() { try { String sql = "SELECT COUNT(*) FROM todo"; stmt = con.createStatement(); rs = stmt.executeQuery(sql); 43 return true; } catch (SQLException e) { return false; } finally { finaliza(); } } private void criaTabelas() throws BancoDeDadosException { executa ("CREATE TABLE todo (" + "id IDENTITY. " + "descricao VARCHAR(100), " + "prioridade INTEGER, " + "concluida BOOLEAN, " + "dataConclusao DATE, " + "alerta BOOLEAN, " + "diasAlerta INTEGER, " + "observacoes VARCHAR(250) " + ")"); } public void desconecta() try { if (con != null) con.close(); con = null ; } catch (SOLException e) {/* ignora a exceção */} } private void finaliza() { try { if (rs != null) rs.close() ; rs = null; if (stmt != null) stmt.close(); stmt = null; } catch (SOLException e) {/* ignora a exceção*/} } private void executa(String sql) throws BancoDeDadosException{ try { stmt = con.createStatement(); stmt.executeUpdate(sql ); } catch (SQLException e) { throw new BancoDeDadosException( "Não foi possível alterar o banco de dados", e); } finally (finaliza();} } private PreparedStatement prepara(String sql) throws SQLException } try { 44 PreparedStatement pst = con.prepareStatement(sql); stmt = pst; return pst; } finally {finaliza();} } private List<Tarefa> consulta(String where, String orderBy) throws BancoDeDadosException { List<Tarefa> resultado = new ArrayList<Tarefa>(); try { String sql = "SELECT id, descricao, prioridade, concluida, " + "dataConclusao, alerta, diasAlerta, observacoes FROM todo “; if (where != null) sql += "WHERE" + where + " "; if (orderBy != null) sql += "ORDER BY " + orderBy; stmt = con.createStatement(); rs = stmt. executeOuery(sql); while (rs.next()) { Tarefa tarefa = new Tarefa(); tarefa.setId(rs.getInt(1)); tarefa.setDescricao(rs.getString(2)); tarefa.setPrioridade(rs.getInt(3)); tarefa.setConcluida(rs.getBoolean(4)); tarefa.setDataConclusao(rs.getDate(5)); tarefa.setGerarAletraCrs.getBoolean(6)); tarefa.setDiasAlerta(rs.getInt(7)); tarefa.setObservacoes(rs.getString(8)); resultado.add(tarefa); } } catch (SOLException e) { throw new BancoDeDadosException{ "Não foi possível consultar o banco de dados", e); } finally {finaliza(); } return resultado; } private void altera(String sql, Tarefa tarefa) throws BancoDeDadosException { try { PreparedStatement pst = con.prepareStatement(sql); stmt = pst; pst.setString(1, tarefa.getDescricao()); pst.setlnt(2, tarefa.getPrioridade()); pst.setBoolean(3, tarefa.isConcluida()); if (tarefa.getDataConclusao() == null) { pst. setDate(4, null); } else { pst.setDate(4, new Date(tarefa.getDataConclusao().getTime())); } 45 pst.setBoolean(5, tarefa.isGerarAletra()); pst.setlnt(6, tarefa.getDiasAlerta()); pst.setString(7, tarefa.getObservacoes()); pst.executeUpdate(); } catch (SQLException e) { throw new BancoDeDadosException( "Não foi possível alterar o banco de dados", e); } finally { finaliza(); } } public List<Tarefa> listaTarefas(boolean prioridadeOuData) throws BancoDeDadosException { return consultaCnull, prioridadeOuData ? "prioridade, dataConclusao, descricao" "dataConclusao, prioridade, descricao"); } public List<Tarefa> listaTarefasComAlarme() throws ModeloException { return consulta("alerta = true AND" + "datediff( 'dd' ,curtime().dataConclusao)<=diasAlerta". "dataConclusao, prioridade. descricao"); } public void adicionaTarefa(Tarefa tarefa) throws ValidacaoException, BancoDeDadosException { validaTarefa(tarefa); String sql = "INSERT INTO todo (" + "descricao, prioridade, concluida, dataConclusao, alerta," + "diasAlerta, observacoes) VALUES (?, ?, ?, ?, ?, ? n"; altera(sql, tarefa); } public void editaTarefa(Tarefa tarefa) throws ValidacaoException, BancoDeDadosException { String sql - "UPDATE todo SET" + "descricao - ?, prioridade = ?, concluida = ?, dataConclusao - ?, " + "alerta = ?, diasAlerta = ?, observacoes = ? " + "WHERE id - " + tarefa.getld(); altera(sql, tarefa); } public void marcaComoConcluida(int id, boolean concluida) throws BancoDeDadosException { executa (“UPDATE todo SET concluida = “ + concluida + “ “ + “Where id = “ + id); } public void removeTarefa(int id) throws BancoDeDadosException{ executa("DELETE FROM todo WHERE id = " + id); } private boolean stringVazia(String str) { return str == null l l str.trim().length() == 0; } private void validaTarefa(Tarefa tarefa) 46 throws ValidacaoException { if (stringVazia(tarefa.getDescricao())) throw new ValidacaoException( "Deve ser fornecida uma descri,ao para a tarefa"); } } Banco de dados e a arquitetura MVC Programadores acostumados a seguir uma lógica seqüencial nos métodos de tratamento de eventos de ambientes RAD, podem ficar um pouco perdidos com a divisão de responsabilidades imposta pela arquitetura MVC. Para tornar as coisas mais claras, a Figura 4 apresenta um diagrama de seqüência UML para a operação de “adicionar tarefa” da aplicação. O processo é disparado pelo usuário que clica no botão da barra de ferramentas (ou no item de menu correspondente) da visão ListaTarefas. Isso gera um evento, que é capturado pelo controlador ConsultaEditaTarefas. O controlador então exibe o dialogo de edição, que é outra visão (EditaTarefas), e o controle retorna para o usuário. Figura 4. Diagramas de seqüência para a criação de uma nova tarefa. Depois de digitar os dados, o usuário clica no botão Salvar, que provoca o envio de um novo evento, também capturado pelo controlador. Em resposta a este segundo evento, o controlador chama o método apropriado da classe DAO, que pode ser adicionaTarefa() ou editaTarefa(). Se houver alguma exceção, o controlador exibe a mensagem de erros na área reservada para isso no próprio diálogo de edição. Se não houver nenhuma exceção, ele fecha o diálogo e atualiza a visão. Caso o usuário cancele o diálogo de edição, não será gerado nenhum evento para o controlador, e o diálogo será simplesmente descartado. Algumas operações, como remover tarefas e marcar tarefas como concluídas atuam sobre uma seleção contendo múltiplas tarefas. Neste caso, o controlador simplesmente chama o método removerTarefa() ou maracrComoConcluida() do modelo num loop for. Para mais detalhes, veja a Listagem 2. Nela está o código do controlador ConsultaEditaTarefas, que tem poucas alterações com relação à versão do artigo anterior. 47 Listagem 2. Classe controladora ConsultaEditaTarefas package todo.controle; import import import import import import import java.util.*; java.text.DateFormat; java.awt.Cursor; java.awt.event.*; javax.swing.*; todo.visao.*; todo.modelo.*; public class ConsultaEditaTarefas implements ActionListener { private ListaTarefas visao; private ConsultaEditaTarefas controle; private GerenciadorTarefas modelo; public ConsultaEditaTarefas(ListaTarefas visao, GerenciadorTarefas modelo) { this.visao = visao; this.modelo = modelo; visao.addActionListener(this); try { listaTarefas(); } catch (BancoDeDadosException e) { visao.setMensagem(e.getMessage(), true); } } public void listaTarefas() throws BancoDeDadosException { visao.setListaTarefas(modelo.listaTarefas(true)); } public void actionPerformed(java.awt.event.ActionEvent e) { visao.setCursor(new Cursor(Cursor.WAIT_CURSOR)); try { if (false) ; // faz nada else if (e.getActionCommand().equals("novaTarefa")) { editaTarefa(true); } else if (e.getActionCommand().equals("editarTarefa")) { editaTarefa(false); } else if (e.getActionCommand().equals("salvarTarefa")) { salvaTarefa() ; } else if (e.getActionCommand().equals("marcarTarefas")) { marcaTarefas() ; } else if (e.getActionCommand().equals("removerTarefas")) { removeTarefas(); } else if (e.getActionCommand().equals("verAlertas")) { exibeAlertas() ; } } 48 catch (Exception ex) { visao.setMensagem(ex.getMessage(), true); } visao.setCursor(null); } private void editaTarefa(boolean novaTarefa) { editaTarefaDialog = new EditaTarefa(visao, true); editaTarefaDialog.setNovaTarefa(novaTarefa); if (novaTarefa) editaTarefaDialog.setTarefa(new Tarefa()); else editaTarefaDialog.setTarefa(visao.getTarefaSelecionada()); editaTarefaDialog.addActionListener(this); editaTarefaDialog.setVisible(true); } private void salvaTarefa() { Tarefa tarefa = editaTarefaDialog.getTarefa(); try { if (editaTarefaDialog.isNovaTarefa()) modelo.adicionaTarefa(tarefa); else modelo.editaTarefa(tarefa); editaTarefaDialog.dispose(); editaTarefaDialog = null; listaTarefas (); } catch (ModeloException e) { editaTarefaDialog.setMensagem(e.getMessage(), true); } } private void marcaTarefas() { Tarefa[] tarefas = visao.getTarefasSelecionadas(); try { for (Tarefa tarefa : tarefas) { modelo.marcaComoConcluida( tarefa.getld(),!tarefa.isConcluida()); } listaTarefas(); } catch (ModeloException e) { editaTarefaDialog.setMensagem(e.getMessage(), true); } } private void removeTarefas() { Tarefa[] tarefas = visao.getTarefasSelecionadas(); int removidas = 0; try { for (Tarefa tarefa : tarefas) { int resposta = JOptionPane.showConfirmDialog(visao, "Tem certeza de que deseja remover" + " a tarefa\n[" + tarefa.getDescricao() + "J ?", "Remover Tarefas", JOptionPane.YES_NO_OPTION); if (resposta == JOptionPane.YES_OPTION) { modelo.removeTarefa(tarefa.getld()); removidas++; 49 } } if (removidas > 0) listaTarefas(); } catch (ModeloException e) { editaTarefaDialog.setMensagem(e.getMessage(), true); } } public void exibeAlertas() { try { List<Tarefa> tarefas = modelo.listaTarefasComAlarme(); for(Tarefa tarefa : tarefas) { DateFormat df = DateFormat.getDatelnstance(); JOptionPane.showMessageDialog(visao, "A seguinte tarefa está a menos de " + tarefa.getDiasAlerta() + " dia(s) da sua data de conclusão:\n" + df.format(tarefa.getDataConclusao()) + "\n" + "[" + tarefa.getDescricao() + "J", "Alerta", JOptionPane.INFORMATION_MESSAGE); } if (tarefas.size() == 0) { visao.setMensagem( "Não há nenhuma tarefa em alerta no momento.",false); } } catch (ModeloException e) { editaTarefaDialog.setMensagem(e.getMessage(), true); } } } Filtragem de informações na tabela Agora que nossa aplicação mantém a lista de tarefas entre uma execução e outra, a utilização se torna muito mais interessante, mas o leitor poderá sentir a falta de recursos para filtrar as tarefas. O protótipo criado na primeira parte prevê um único filtro para a lista de tarefas, que é exibir (ou não) as tarefas marcadas como concluídas, e que caso sejam exibidas serão apresentadas em azul-claro. Além deste filtro, foi prevista a ordenação das tarefas por prioridade ou por data de conclusão, ambas em ordem crescente. Há duas estratégias básicas para fazer a filtragem e a ordenação: pela própria visão, sem afetar o controlador ou o modelo; ou pelo modelo, de modo a aproveitar os recursos do banco de dados. Filtrar na visão simplifica o modelo (com o custo de deixar a visão mais complexa), mas faz sentido conceitualmente, pois estamos falando apenas de como as informações são apresentadas para o usuário, e não de regras de validação ou consistência de dados. Já filtrar no modelo, pode ser necessário caso o volume de dados seja grande, ou os critérios de filtragem, muito complexos. Também pode ser mais fácil de programar, pois o SQL permite escrever código de consulta e alteração de dados de forma mais sucinta do que em Java. No nosso caso da Lista de Tarefas, espera-se que nunca haja um volume de dados muito grande; por isso a lista é mantida inteiramente em memória (dentro da classe visao.TarefasTableModel). Nossas operações de filtragem e ordenação são facilmente escritas em Java, utilizando a API padrão de coleções, e o usuário tem retorno quase instantâneo – coisa difícil de se obter com um banco de dados. 50 Adotamos então a estratégia de fazer a filtragem na visão. Desse modo os eventos do menu Opções são capturados e tratados pela própria classe de visão ListaTarefas, em vez de serem repassados para o controlador. O código que chama as operações de ordenação e filtragem é simples e poderia ter sido escrito diretamente no método de tratamento de eventos gerado pelo NetBeans (com um clique duplo sobre os botões e itens de menus) – mas como temos a mesma operação sendo realizada por menus e botões, é mais conveniente criar métodos auxiliares. Assim garantimos que o status do menu e da barra de ferramentas esteja consistente e evitamos repetição de código, o que é sempre uma boa idéia, por menor que seja o código duplicado. Criamos os métodos ordenaPorPrioridade() e mostraConcluidas(), chamados pelos métodos de tratamento de eventos botaoOrdenarActionPerformed(), botaoMostrarActionPerformed(), menuMostrarActionPerformed(), menuOrdenarPrioridadesActionPerformed() e menuOrdenarDatasActionPerformed(), que são gerados pelo NetBeans para os eventos dos itens de menu. A ordenação e filtragem em si é realizada pela classe TarefasTableModel, que já era a responsável por fornecer e formatar os dados para o JTable contendo as tarefas listadas. As modificações em TarefasTableModel em relação ao artigo anterior são indicadas em negrito na Listagem 3. Listagem 3. Modificações na classe TarefasTableModel package todo.visao; import import import import java.util.*; java.text.*; javax.swing.table.*; todo.modelo.*; public class TarefasTableModel extends AbstractTableModel { private List<Tarefa> tarefas; private List<Tarefa> tarefasFiltradas; private boolean mostrarConcluidas = true; private boolean ordenarPorPrioridade = true; private DateFormat df = DateFormat.getDatelnstance( DateFormat.SHORT); public TarefasTableModel (List<Tarefa> tarefas) { this.tarefas = tarefas; filtraTarefas() ; } public Object getValueAt(int rowlndex, int columnlndex) { Tarefa umaTarefa = tarefasFiltradas.get(rowlndex); switch(columnlndex) { case 0: return umaTarefa.getPrioridade(); case 1: return umaTarefa.getDescricao(); case 2: return umaTarefa.isGerarAletra(); case 3: return umaTarefa.getDataConclusao(); } return null; } public int getRowCount() { return tarefasFiltradas.size(); } public int getColumnCount() { 51 return 4; } public Tarefa getValoresTarefa(int rowlndex) { if (rowlndex > tarefasFiltradas.size() ) return null; else return tarefasFiltradas.get(rowlndex); } public boolean isMostrarConcluidas() { return mostarConcluidas; } public void setMostrarConcluidas(boolean mostrarConcluidas) { this.mostrarConcluidas = mostrarConcluidas; filtraTarefas(); } public boolean isOrdenarPorPrioridade() { return ordenarPorPrioridade; } public void setOrdenarPorPrioridade( boolean ordenarPorPrioridade) { this.ordenarPorPrioridade = ordenarPorPrioridade; filtrarTarefas(); } private void filtraTarefas() { tarefasFiltradas = new ArrayList<Tarefa>(); for (Tarefa tarefa : tarefas) { if (!isMostrarConcluidas() && tarefa.isConcluida()) continue; tarefasFiltradas.add(tarefa); } if (! isOrdenarPorPrioridade()) Collections.sort(tarefasFiltradas, new Comparator<Tarefa)() { public int compare(Tarefa t1, Tarefa t2) { if (t1.getDataConclusao().equals(t2.getDataConclusao())) { if (t1.getPrioridade() == t2.getPrioridade()) return t1.getDescricao().compareTo(t2.getDescricao()); else return t1.getPrioridade()>t2.getPrioridade() 1:-1; } else return t1.getDataConclusao().compareTo( t2.getDataConclusao()); } }); fireTableDataChanged(); } } 52 Criando e abrindo listas Estamos com quase toda a funcionalidade da nossa aplicação pronta. Faltam apenas dois itens do menu Arquivo: um cria uma nova lista de tarefas armazenada em outro arquivo que não o ${user.home}/dB/todo; outro abre listas de tarefas armazenadas em outros arquivos. Este é um caso claro de preferências dos usuários. Uma aplicação mais complexa certamente iria armazenar outras preferências (deixamos como exercício armazenar as seleções do menu Opções). Como as preferências também devem ser informações persistentes, criamos uma classe de modelo para lidar com elas. Daí a classe Parametros já citada. Ela fornece métodos get e set para preferências individuais e é construída utilizando a Preferences API (para mais sobre essa API, veja o artigo de Paloma Sol na Edição 10). A classe Parametros é bastante simples, como vemos na Listagem 4. Observe que a API de preferências exige que a aplicação indique os valores padrão para cada preferência, pois apenas valores diferentes serão efetivamente armazenados. Para isso são definidas várias constantes no início da classe. Um caso especial é o valor para a preferência chamada database, gerado dinamicamente a partir do diretório pessoal do usuário. Chegamos ao ponto em que é necessário acrescentar um segundo controlador à aplicação, a classe CriaAbreListaTareafas. Sua função é lidar justamente com as opções do menu Arquivo, visto que elas não têm relação direta com o ciclo criar-editar-removerconsultar para as tarefas. Visando simplificar a aplicação, este controlador também lida com outro item de menu, ainda não coberto, o Ajuda/Sobre. Não há necessidade de criar classes de visao auxiliares para estas operações, pois as APIs JOptionPane e JFileChooser são sufientes para esta aplicação. Seria possível definir visualmente no NetBeans as caixas “Sobre” e “Abrir arquivo”, mas no nosso caso é desnecessário, pois apenas configuramos propriedades como o título dos diálogos padrão, sem acrescentar novos componentes. Acaba portanto sendo bem mais simples chamar diretamente as APIs. A Figura 5 apresenta a aparência dos diálogos “Sobre” e “Abrir Lista de Tarefas”. O diálogo “Criar Lista de Tarefas” é similar a este último. Figura 5. Caixa Sobre e diálogos de Abrir e Nova Lista de Tarefas. O código do novo controlador é mostrado na Listagem 5. Para abrir e salvar tarefas foi usado o componente JFileChooser do Swing. Este componente utiliza um objeto FileFilter para determinar quais arquivos devem ser exibidos; por isso declaramos e instanciamos uma classe aninhada, armazenando o objeto no atributo hsqlDatabases da classe CriaAbreListaTarefas: private FileFilter hsqlDatabases = newFileFilter(){...} Esse objeto da classe aninhada fornece o filtro de arquivos a ser passado para os diálogos de abrir e salvar listas de tarefas, garantindo que sejam exibidos apenas pastas e arquivos cujo nome contenham a extensão .script do HSQLDB. 53 Listagem 4. Classe de modelo Parametros package todo.modelo; import java.io.File; import java.util.prefs.*; public class Parametros { private static final String jdbcDriver = "org.hsqldb.jdbcDriver”; private stafic final String defaultUrl = "jdbc:hsqldb:"; private static final String defaultDatabase = "db/todo"; private Preferences prefs = Preferences.userNodeForPackage(Parametros.class); public String getDatabase() { //return prefs.get ("database", System.getProperty( //"user.dir") return prefs.get("database", System.getProperty("user.home”) + File.separator + defaultDatabase); } public void setDatabase(String database) { prefs.put("database", database); } public String getJdbcUrl() { return defaultUrl + getDatabase(); } public String getJdbcDriver() { return jdbcDriver; } } Listagem 5. Classe de controladora CriaAbreListaTarefas package todo.controle; import import import import import import java.io.File; java.awt.Cursor; java.awt.event.*; java.io.IOException; javax.swing.*; javax.swing.filechooser.*; import todo.modelo.*; import todo.visao.ListaTarefas; public class CriaAbreListaTarefas implements ActionListener { private ListaTarefas visao; private GerenciadorTarefas modele; private Parametros params; public CriaAbreListaTarefasCListaTarefas visao, GerenciadorTarefas modelo, Parametros params) { this.visao = visao; this.modelo = modelo; 54 this.params = params; visao.addActionListener(this); visao.addWindowListener(fechaBanco); } private WindowListener fechaBanco = new WindowAdapter() { public void windowClosed(WindowEvent e) { modelo.desconecta(); } public void windowClosing(WindowEvent e) { modelo.desconecta(); } }; public void actionPerformed(java.awt.event.ActionEvent e) { visao.setCursor(new Cursor(Cursor.WAIT_CURSOR)); try { if (false) ; // faz nada else if (e.getActionCommand().equals("criarListaTarefas")) { novaListaTarefas(); } else if (e.getActionCommand().equals("abrirListaTarefas")) { abreListaTarefas(); } else if (e.getActionCommand().equals("sobre")) sobre() ; } } catch (Exception ex) { visao.setMensagem(ex.getMessage(). true); } visao.setCursor(null); } private FileFilter hsqlDatabases = new FileFilter() { public String getDescription() { return "Listas de Tarefas - hsqldb (*.script)"; } public boolean accept(File f) { if (f.isDirectory()) return true; else if (f.getName().endsWith(".script")) return true; else return false; } }; private String criaOuAbreBanco( File database) throws BancoDeDadosException { String arq = database.getAbsolutePath(); if (arq.startsWith("file:")) arq = arq.substring(5); if (arq.endsWith(".script")) arq = arq.substring(O, arq.length() - 7); modelo.reconecta(arq); visao.setListaTarefas(modelo.listaTarefas(true)); 55 return arq; } public void abreListaTarefas(String bd) throws BancoDeDadosException { try { String arq = criaOuAbreBanco(new File(bd)); visao.setMensagem("Aberta lista de tarefas; "+arq.false); } catch (BancoDeDadosException e) { visao.setMensagem( "Nao foi possivel abrir a Lista de Tarefas", true); } } private void novaListaTarefas() { JFileChooser dlg = new JFileChooser(); dlg.setDialogTitle("Nova Lista de Tarefas"); dlg.setFileFilter(hsqlDatabases); File dir = new File(params.getDatabase()).getparentFile(); dlg.setCurrentDirectory(dir); if (dlg.showSaveDialog(visao)==JFileChooser.APPROVE_OPTION){ try { String arq = criaOuAbreBanco(dlg.getSelectedFile()); visao.setMensagem("Criada lista de tarefas; " + arq, false); } catch (BancoDeDadosException e) visao.setMensagemC "Não foi possível criar a Lista de Tarefas". true); } } } private void abreListaTarefas() { JFileChooser dig = new JFileChooser(); dlg.setDialogTitle("Abrir Lista de Tarefas"); dlg.setFileFilter(hsqlDatabases); File dir = new File(params.getDatabase()).getParentFile(); dlg.setCurrentDirectory(dir); if (dlg.showOpenDialog(visao)==JFileChooser.APPROVE_OPTION){ try { String arq = criaOuAbreBanco(dlg.getSelectedFile()); visao.setMensagem("Aberta lista de tarefas: " + arq, false); } catch (BancoDeDadosException e) { visao.setMensagem( "Nao foi possivel abrir a Lista de Tarefas", true); } } } private void sobre() { JOptionPane.showMessageDialog(visao, "Todo - Lista de Tarefas\nVersao 1.0\n\n" + "Revista Java Magazine 27", "Sobre Todo", 56 JOptionPane.INFORMATION_MESSAGE); } } Alertas e linha de comando Finalmente chegamos à última funcionalidade a ser implementada: a exibição de alertas para tarefas próximas das datas de conclusão. Esta operação é representada pelo ícone da barra de ferramentas, e também é executada no início da aplicação para chamar a atenção do usuário. A relação de tarefas com alertas ativos é gerada por listaTarefasComAlertas() do DAO GerenciadorTarefas, e é exibida por exibeAlertas() do controlador ConsultaEditaTarefas. Novamente por simplicidade, optamos por utilizar um JOptionPane do Swing, em vez de criar outra classe de visão. Um exemplo desta caixa é apresentado na Figura 6. Figura 6. Alerta avisando sobre uma tarefa próxima da sua data de conclusão A Listagem 5 exibe a classe todo.Main alterada para prever a funcionalidade dos alertas. Ela instancia as classes de Modelo, Controlador e Visão e os conecta por meio dos seus construtores. Em seguida esta classe comanda a verificação dos alertas, utilizando o método já apresentado no controlador ConsultaEditaTarefas. A classe Main também verifica se algum argumento foi fornecido na linha de comando. Sendo fornecidos vários argumentos, a aplicação exibe uma mensagem de erro no console. Caso tenha sido fornecido um único argumento, ele é considerado como o nome do arquivo contendo uma lista de tarefas e é passado para o controlador CriaAbreListaTarefas, que comanda a abertura do arquivo. Não sendo fornecido nenhum argumento de linha de comando, é utilizado o bando de dados padrão do usuário, de acordo com suas preferências. Vale destacar a importância do suporte à linha de comando em uma aplicação gráfica. Na nossa aplicação, por exemplo, isso permite que seja criado um atalho/lançador na área de trabalho do usuário para abrir diretamente uma lista de tarefas especifica; ou que uma lista de tarefas seja aberta com um clique duplo, no gerenciador de arquivos do seu SO. Empacotando para os usuários Invocado o comando Build/Clean and Build Main Project no NetBeans, podemos gerar o pacote dist/Todo.jar, um jar executável para distribuição. Entretanto, como adicionamos o suporte a banco de dados, este pacote não será mais suficiente para a execução da aplicação. Uma arquivo .zip hsqldb.jar). ficarão nem estratégia de distribuição muito utilizada nesse caso é fornecer um único contendo o pacote jar executável e os jars das bibliotecas utilizadas (ou seja, o Com isso, ao descompactar o .zip de distribuição da aplicação, todos os jars mesmo diretório. Não será necessário criar um executável nativo, ou um script (.bat, .cmd, .sh etc.) para configurar o classpath da aplicação; basta incluir dentro do jar executável uma referência aos pacotes de bibliotecas necessários. 57 Isso é feito modificando-se o arquivo de manifesto do pacote jar. Para configurar o manifesto, entre na visão de arquivos do NetBeans e localize, na raiz do projeto, o arquivo MANIFEST.MF (veja a Figura 7). Figura 7. Arquivo de manifesto do jar executável na visão de arquivos do projeto do NetBeans Abra este arquivo, que será exibido no editor de texto simples do NetBeans, e acrescente a linha: Class-path:hsqldb.jar Então reconstrua (menu Build) o pacote da aplicação, e copie tanto esse pacote quanto o hsqldb.jar para uma pasta qualquer do sistema. Em seguida, execute a aplicação no prompt de comandos: java-jar/caminho/para/aplicacao/Todo.jar Veja como agora a aplicação é iniciada corretamente. O mesmo comando pode ser utilizado num atalho/lançador na área de trabalho do usuário. Conclusões Encerramos aqui nossa série sobre desenvolvimento Java/Swing, com o NetBeans. Outras pequenas modificações feitas na versão final da aplicação, disponível para download, são comentadas no quadro “Ajustes diversos”, onde também são fornecidas algumas dicas, por exemplo como detectar um clique duplo. Se você está com a sensação de que falamos muito das APIs do Java e pouco do NetBeans em si, na verdade esta é uma das grandes vantagens do Java sobre outras plataformas: o desenvolvedor não fica dependente do IDE utilizado. Exceto pela prototipação visual dos formulários, todo o projeto poderia ser modificado, recompilado e testado em outros IDEs, como o Eclipse. Ou mesmo sem nenhum IDE, apenas usando as ferramentas de linha de comando do JSDK e um editor de textos. Por outro lado, demonstramos nesta série como o NetBeans oferece recursos suficientes para que o desenvolvedor considere o seu uso em substituição a outros IDEs Java no mercado. Não apenas as facilidades visuais, mas também o suporte ao Ant, a estrutura de projetos, recursos de refatoração de código, depurador visual e auto-completar código, além da interface flexível colocam o NetBeans em pé de igualdade com os melhores produtos. LINKS: netbeans.org Site oficial do NetBeans hsqldb.sf.net Site oficial do banco de dados HSQLDB javamagazine.com.br/downloads/jm27/jm27-apcompleta-p3.zip 58 Fernando Lozano: ([email protected],www.lozano.eti.br) é consultor independente, atuando há mais de dez anos em projetos de integração de redes, desenvolvimento de sistemas e tuning de banco de dados. É também conselheiro do Linux Professional Institute do Brasil e autor do livro “Java em GNU/Linux”, além de líder da comunidade Linux no portal Java.net. Configurando o projeto no NetBeans O bando de dados HSQLDB pode funcionar embutido num aplicação Java, sem criar novos threads nem abrir portas TCP. É uma excelente alternativa para aplicações de automação de escritório, tanto que o HSQLDB foi incorporado à versão 2.0 do OpenOffice em seu novo componente de banco de dados. Neste artigo, foi utilizada a versão 1.8.0.1 do HSQLDB, que pode ser obtida livremente de hsqldb.sf.net. Para instalá-la, descompacte o .zip e localize o arquivo lib/hsqldb.jar. Este jar é tudo o que precisamos para a aplicação, já que a aplicação irá criar automaticamente o banco de dados e as tabelas necessárias. De volta ao NetBeans, clique com o botão direito no aplicação, na visão de projetos; selecione a opção Properties e depois o elemento Libraries em Categories. Em seguida clique em Add JAR/Folder e escolha o arquivo hsqldb.jar, para acrescentá-lo ao classpath do projeto no NetBeans (o resultado é apresentado na Figura Q1). Depois clique em Ok para fechar o diálogo de propriedades. Caso deseje inspecionar o banco de dados da aplicação, você pode utilizar o navegador JDBC incluso no NetBeans. (Para mais informações sobre esse recurso, consulte o artigo “O Novo NetBeans”, na Edição 24.) Figura Q1. Adicionando a biblioteca do HSQLDB ao classpath da aplicação Experimentando com outros bancos As características da aplicação Lista de Tarefas tornam o HSQLDB sensivelmente mais adequado do que um banco de dados cliente/servidor, como o MySQL, Oracle, DB2 etc. Entretanto, a arquitetura de aplicação proposta não tem nenhuma dependência com o HSQLDB em particular. Caso o leito deseje experimentar com outros bancos de dados, será necessário modificar as configurações relativas à conexão JDBC (classe todo.modelo.Paramentros) e talvez, dependendo do banco, modificar o comando SQL CREATE TABLE em todo.modelo.GerenciadorTarefas e incluir comandos adicionais (ou mudar a sintaxe), para a criação do banco de dados vazio. Se for adotado um banco cliente/servidor, não fará sentido fornecer as funcionalidades de “Abrir lista de tarefas” e “Criar lista de tarefas”, que pressupõem hum banco de dados baseado em arquivos locais. Em vez disso, poderá ser fornecido um diálogo simples para edição dos parâmetros de conexão. Mas essas facilidades permanecerão 59 válidas para outros bancos de dados baseados em arquivos, como o Derby ou McKoi: será necessário apenas mudar os filtros de arquivos para extensões apropriadas). Todas as classes do exemplo todo.Main Fornece o método main() para iniciar a aplicação. todo.controle.ConsultaEditaTarefas Para edição e consulta a tarefas, é o principal controlador da aplicação. todo.controle.CriaAbreListaTarefas Para criar e abrir um arquivo (do HSQLDB) de uma lista de tarefas, e coordenar outras operações gerais. todo.modelo.BandoDeDadosException Indica um erro qualquer de banco de dados. todo.modelo.GerenciadorTarefas É a classe de modelo principal da aplicação, seguindo o pattern DAO (Data Access Objetc); tem a funcionalidade de recuperar e armazenar tabelas no banco de dados. todo.modelo.ModeloException Superclasse para todas as classes ES exceção do pacote todo.modelo. todo.modelo.Parametros Classe de modelo para parâmetros da aplicação, recuperados via a API Preferences do J2SE. todo.modelo.Tarefa Classe que segue o pattern VO (Value Object), encapsulando todos os atributos de uma tarefa. todo.modelo.ValidacaoException Indica um erro de validação na digitação dos dados de uma tarefa. todo.visao.ActionSupport Superclasse abstrata das classes de visão, fornece métodos utilitários para a geração de eventos actionPerformed para classes de controle, seguindo o modelo de eventos do Swing/AWT. todo.visao.EditaTarefa Diálogo (JDialog) para a entrada dos dados de uma tarefa, seja na sua adição ou edição. todo.visao.ListaTarefas Janela principal (JFrame) da aplicação, exibe uma lista de tarefas em um componente JTable. todo.visao.TarefasCellRenderer Customiza um JTable para exibir as informações de uma tarefa em cores diferentes, de acordo com o status de concluída ou com alerta. todo.visao.TarefasColumnModel Customiza um JTable para exibir quatro colunas (Prioridade, Descrição, Alarme e Data de Conclusão). As colunas têm tamanho fixo, com exceção de Descrição, que ocupa toda a largura restante. todo.visao.TarefasTableModel 60 Customiza um JTable para exibir as informações a partir de uma lista de objetos Tarefa (List<Tarefa>). Evita conversões desnecessárias dos atributos para um vetor de strings, economizando memória. Ajustes diversos Em relação à aplicação prototipada na primeira parte e tornada interativa na segunda, foram feitos uma série de ajustes. É normal que, à medida que uma aplicação chegue próxima a sua conclusão, sejam identificados algumas pequenas questões relativas à interface com o usuário, habilitação de componentes, ou posicionamento de janelas. Ajustes desse tipo estão espalhados pela aplicação e não são apresentados em listagens para poupar espaço. Mas os fontes completos da aplicação final – e das versões intermediarias – estão disponíveis para download no site da Java Magazine. Segue uma breve relação dos justes realizados: • • As duas visões ListaTarefas e EditaTarefas foram centralizadas, uma em relação à tela e a outra em relação à sua janela mãe, por meio do método setLocationRelativeTo(), chamado no construtor. O método actionPerformed() dos controladores chama setCursor() para ativar o cursor de “ampulhetas” (ou o cursor padrão de espera na sua plataforma) e restaurar o cursor padrão depois de tratar um evento (ou em caso de exceções). Assim operações potencialmente demoradas, como conectar ao banco de dados ou listar vários registros, terão feedback visual para o usuário. O JTable e JTextField para edição da quantidade de dias para emissão de alertas na edição de uma tarefa não estavam sendo habilitados/desabilitados junto com o JCheckbox correspondente. Problema resolvido. O controlador CriaAbreListaTarefas foi registrado com um WindowListener da classe de visão ListaTarefas. Desse modo, ele garante o fechamento do banco de dados ao final da aplicação. Foi capturado o evento MouseClick do JTable, em ListaTarefas. Se o atributo clickCount do evento. • • • Onde está o RAD? O leitor habituado a um ambiente RAD (rapid Application Development) como o VB ou Delphi pode estar um pouco frustrado por não termos utilizado recursos visuais para o código de banco de dados. O fato é que a biblioteca padrão de classes do J2SE não inclui componentes no estilo DataControl, DBGrid etc., que aumentam inicialmente a produtividade, mas estimulam a programação de “código espaguete” misturando lógica de acesso a dados, visualização de dados e regras de negocio dentro da mesma classe de formulário (janela), e acabam prejudicando a manutenção da aplicação. Embora existam componentes sililares no mercado, que podem ser acrescentados ao NetBeans (ou outros IDEs) a prática recomendada em aplicações Java é ou construir classes DAO para acesso a dados via JDBC; ou então utilizar um framework de mapeamento objeto-relacional com o Hibernate. Em ambos os casos, é mantida a separação de responsabilidades entre as várias classes da aplicação, e o código é escrito num estilo completamente orientado a objetos em vez de parcialmente procedural. Para sermos justos, temos que lembrar que nada impede que as mesmas práticas de projetos sejam empregadas no Delphi e outros ambientes RAD orientados a objetos, mas infelizmente a cultura predominante neles é de não explorar classes e objetos na arquitetura da aplicação. 61
Copyright © 2024 DOKUMEN.SITE Inc.