Java com café: março 2012

Como limitar um JTextField

É muito comum encontrar nos fóruns especializados em Java a seguinte questão: "Como eu faço para limitar o tamanho de um JtextField?". Ocorre um certo desapontamento dos iniciantes na linguagem por não haver um método chamado setMaxLenght(x). Ou ainda com os que trabalham visualmente, procurar a propriedade no componente que limitaria o tamanho máximo de caracteres e não encontrar. Aí programadores que vieram do Delphi por exemplo dizem "O Delphi é muito melhor, blablabla...". Só digo uma coisa: Revejam seus conceitos sobre o que é Java.
A verdade é que o componente JTextField é definido sobre um modelo de documento que por padrão é chamado PlainDocument. Portanto, para para ter o controle sobre o JTextField devemos criar uma sub classe de PlainDocument, sobrescrever alguns métodos dela e trocar o modelo do JTextField para este novo.
Minha intenção não é entrar a fundo neste assunto, mas sim dar uma solução principalmente pra quem está trabalhando visualmente e não quer ficar editanto código a todo momento.
Então vamos lá, primeiramente crie a seguinte classe no seu projeto:

import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.PlainDocument;

public class JTextFieldLimit extends PlainDocument {

    private int limit;
    // optional uppercase conversion
    private boolean toUppercase = false;
    
    private int maximo = 500;

    public JTextFieldLimit(int limit) {
        super();
        if (limit == 0)
            this.limit = maximo;
        else
            this.limit = limit;
    }

    public JTextFieldLimit(int limit, boolean upper) {
        super();
        if (limit == 0)
            this.limit = maximo;
        else
            this.limit = limit;
        toUppercase = upper;
    }

    @Override
    public void insertString(int offset, String str, AttributeSet attr)
            throws BadLocationException {
        if (str == null) {
            return;
        }

        if ((getLength() + str.length()) <= limit) {
            if (toUppercase) {
                str = str.toUpperCase();
            }
            super.insertString(offset, str, attr);
        }
    }
}

Esta classe não é nenhuma novidade e pode ser encontrada em vários sites pela internet (na verdade fiz algumas mudanças nela). O que ela faz é permitir a limitação de caracteres e opcionalmente fazer a conversão para maiúsculas. Mas o problema é que pra cada JTextField criado, temos que chamar o método setDocument passando essa classe e os parâmetros. Então para resolver este problema, crie o seguinte método na classe onde estão seus campos de texto:

    public void preparaForm() {

        // JTextFields limitados e com maiúsculas
        for (Component c : this.getComponents()) {
            if (c instanceof JTextField) {
                ((JTextField) c).setDocument(new JTextFieldLimit(((JTextField) c).getColumns(), true));
            }
        }
        
    }

Este método deve ser chamado sempre antes de exibir o seu formulário. Ele vai verificar cada componente do formulário e caso este seja do tipo JTextField, será configurado para utilizar o modelo de documento apresentado. Mas onde vou definir o limite de caracteres para cada campo? Observe que o primeiro parâmetro passado para o documento é: ((JTextField) c).getColumns(). Visualmente você poderá definir este número na propriedade column do componente. Mas porque essa propriedade? Isto está parecendo uma bela gambiarra... Fique tranquilo, a propriedade column é apenas um número usado para calcular o tamanho preferido para a largura do campo. Se ele for zero, o tamanho preferido será qualquer um dependendo da implementação do objeto. Isto é, você define a largura visualmente utilizando seu gerenciador de layout preferido e pouco importará o valor definido para a propriedade column.
Enfim, trabalhando desta forma você não precisará ficar escrevendo código para configurar o documento e limite de caracteres para cada campo de texto. Isto não é problema se o projeto é simples, mas se estiver desenvolvendo vários formulários, não é muito produtivo.

Observação: O método só funciona caso esteja trabalhando com JInternalFrames e caso não esteja, pode haver alguma diferença no método que retorna os componentes (this.getComponents). Se for um JDialog por exemplo, ficaria assim:  
this.getRootPane().getContentPane().getComponents().

Por hoje é só! Qualquer dúvida deixe nos comentários.

Editado em 17/12/2012:

Devido a alguns JTextFields estarem dentro de JPanels e JTabbedPanes, desenvolvi um método recursivo que poderá ser chamado dentro do método preparaForm citado anteriormente:
 
    private void preparaJTextFields(Object obj) {

        if (obj instanceof JTextField) {
            ((JTextField) obj).setDocument(new JTextFieldLimit(((JTextField) obj).getColumns(), true));
        }
       
        if (obj instanceof JPanel) {
            for (Component c : ((JPanel) obj).getComponents()) {
                preparaJTextFields(c);
            }
        }

        if (obj instanceof JTabbedPane) {
            for (Component c : ((JTabbedPane) obj).getComponents()) {
                preparaJTextFields(c);
            }
        }

    }   

E a chamada pode ser feita assim:
 
    public void preparaForm() {

        //Centraliza o JInternalFrame
        Dimension d = this.getDesktopPane().getSize();
        this.setLocation((d.width - this.getSize().width) / 2, (d.height - this.getSize().height) / 2);

        //JTextFields limitados
        preparaJTextFields(this.getContentPane());
        
    }

E aproveito ainda para mostrar como não permitir acentuação nos JTextFields, basta trocar a linha super.insertString(offset, str, attr); da classe JTextFieldLimit por:
 
//super.insertString(offset, str, attr);
            
// Sem acentos
String unaccented = Normalizer.normalize(str, Normalizer.Form.NFD);
unaccented = unaccented.replaceAll("[^\\p{ASCII}]", "");
super.insertString(offset, unaccented, attr);

Como listar todos itens de um JMenuBar

Hoje vou ensinar como retornar todos os objetos JMenuItems contidos num JMenuBar. Eventualmente você pode querer retornar também os objetos JMenu, então fica livre para fazer sua implementação do método.
O método que vou apresentar pode ser muito útil para fazer o controle de acesso da aplicação, permitindo que o item de menu seja exibido ou não de acordo com as permissões de cada usuário.
A estrutura de menus em Java é basicamente a seguinte:

JMenubar
   |
   |__ JMenu
   |       |
   |       |__JMenuItem
   |  
   |__JMenu
           |
           |__JMenu
                  |
                  |__JMenuItem

Como podemos observar, temos uma barra de menu e dentro dela temos 2 menus. No primeiro menu temos um item e no segundo temos um sub menu e dentro deste outro item. Então para recuperar todos itens, considerando que podem existir itens dentro de sub menus, devemos utilizar a recursividade.
Resumidamente, um método recursivo é um método que faz chamadas a ele mesmo. Certos problemas, como o apresentado, exigem esta abordagem, pois os elementos estão dispostos numa estrutura do tipo árvore que pode ter várias ramificações. Sendo assim, segue abaixo uma possível implementação do método que resolve este problema.

    /**
     * Retorna todos JMenuItems de um JMenuBar.
     *
     * @param obj Uma variável do tipo JMenuBar.
     * @return void
     */
    public void getMenuItems(Object obj) {

        if (obj instanceof JMenuBar) {

            for (Component c : ((JMenuBar) obj).getComponents()) {
                getMenuItems(c);
            }
        }

        if (obj instanceof JMenu) {

            for (Component c : ((JMenu) obj).getMenuComponents()) {
                getMenuItems(c);
            }
        }

        if (obj.getClass() == JMenuItem.class) {
            System.out.println("JMenuItem: " + ((JMenuItem) obj).getName());
        }

    }

Fiquem a vontade para modificar e utilizar. Até a próxima!

Como vincular dados a um JComboBox

Atualmente estou trabalhando num projeto no qual utilizo o framework JPA. Então a cada dia que passa eu acabo aprendendo algo novo sobre esta API e espero com essa experiência poder ajudar os que estão começando agora.
Hoje vou falar um pouco sobre como vincular dados que se encontram em uma tabela do banco a um componente JComboBox. A vinculação é simples de se fazer, mas a exibição da propriedade desejada é um pouco mais complicada como veremos adiante. Vou apresentar um passo a passo que traduzi do site netbeans.org, mas com algumas correções (o exemplo de código do site na parte "To render the combo box propertly" não funciona).
Vamos ao que interessa:

1. Clique com o botão direito no combo box e escolha Bind > elements (Vincular > elements). 

2. Clique em Import Data to Form (Importar dados para o Formulário...). Selecione a conexão e tabela que voce deseja vincular o combo box. Clique em OK. 

3. No combo From the Binding Source (Origem da vinculação), selecione o item que representa o result list da entidade. Por exemplo, se a entidade se chama Cliente.java, o objeto da lista deve ser gerado como clienteList

4. Deixe o valor em Binding Expression (Expressão de vinculação) como null e clique em OK. 

5. Clique com o botão direito no combo box novamente e escolha Bind > selectedItem (Vincular > selectedItem). Vincule a propriedade que será afetada pela selecão do usuário e clique em OK (normalmente o result list escolhido no passo 3). 

Até aqui já temos os dados vinculados ao combo box, porém se executar o programa verá que o combo mostrará dados da entidade selecionada convertida em String (objeto.toString()) e não uma propriedade específica:


Isto acontece porque, segundo o próprio site do NetBeans, a biblioteca Beans Binding não tem uma classe DetailBinding que permite especificar os valores exibidos no JComboBox.
Uma forma de resolver isto é escrever um código personalizado para mostrar a propriedade que queremos: 

1. Selecione o combo box

2. Na aba Propriedades da Janela de Propriedades, selecione a propriedade render

3. Clique no botão ... 

4. No combo na parte superior do editor de propriedades, selecione Custom Code (Código personalizado). 

5. Na área de texto, entre com um código parecido com o que segue abaixo onde Usuario é o nome da sua entidade e getUsuario é a propriedade que retorna o valor a ser vinculado. 

new DefaultListCellRenderer() {

    @Override
    public Component getListCellRendererComponent(
            JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
        super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
        if (value instanceof Usuario) {
            Usuario usuario = (Usuario) value;
            setText(usuario.getUsuario());
        }
        return this;
    }
}
Feito isso, o valor selecionado deverá aparecer no combo:


Uma outra abordagem mais simples para resolver o problema é sobrescrever o método toString da entidade em questão:

    @Override
    public String toString() {
        return getUsuario();
    } 

Basta fazer com que o método retorne o valor que você deseja mostrar no combo box. É isso aí! Espero que tenham gostado.