Um dos recursos mais interessantes do Java, em relação a componentes do pacote Swing, é o Jtable, por tratar-se de uma estrutura que fornece um poder de manipulação inimaginável. Mas neste artigo não trataremos sobre Jtable e sim sobre o que o faz tão poderoso: O TableModel.
O TableModel nada mais é do que uma interface, ou seja, como um “contrato” para que possamos implementar as suas regras. Vejamos a sua estrutura no código da Listagem 1.
Listagem 1. TableModel
package javax.swing.table;
import javax.swing.*;
import javax.swing.event.*;
public interface TableModel
{
public int getRowCount();
public int getColumnCount();
public String getColumnName(int columnIndex);
public Class<?> getColumnClass(int columnIndex);
public boolean isCellEditable(int rowIndex, int columnIndex);
public Object getValueAt(int rowIndex, int columnIndex);
public void setValueAt(Object aValue, int rowIndex, int columnIndex);
public void addTableModelListener(TableModelListener l);
public void removeTableModelListener(TableModelListener l);
}
Na Listagem 1 podemos perceber que a estrutura do TableModel é relativamente simples, porém muito poderosa, e é a partir desta interface que o Jtable consegue realizar manipulações independentemente do tipo de dado, então vamos entender a função de cada um dos métodos acima, mas lembre-se que eles são apenas assinaturas de métodos e ainda não possuem nenhuma lógica implementada, por isso trata-se apenas de uma Interface.
- getRowCount(): Deverá ser implementada a lógica que retorne a quantidade de linhas do modelo. Atenção: Chamamos de Modelo por tratar-se especificamente da implementação do TableModel e não do Jtable, isso significa que ao realizar um getRowCount() estaremos usando a lógica da implementação do TableModel e não do Jtable.
- getColumnCount(): Deverá ser implementada a lógica que retorne a quantidade de colunas presentes no Model. Nas seções a frente, quando começarmos a criar nossa implementação do TableModel, você verá que utilizaremos este método retornando a quantidade de propriedades do nosso Bean. Ex: Bean Usuario possuirá três colunas: login, senha, nome.
- getColumnName(): Deverá implementar a lógica para retornar o nome da coluna em determinado índice.
- getColumnClass(): Deverá implementar a lógica para retornar a classe da coluna através do seu índice, ou seja, se a coluna Senha for do tipo String, o retorno será String.class assim saberemos qual tipo da coluna em questão e poderemos realizar outros tratamentos com a mesma.
- IsCellEditable(): Deverá implementar a lógica que irá checar se a célula atual, dados os argumentos de posição de linha e coluna, é editável. O método que veremos logo a seguir, setValueAt(), só terá efeito caso a célula for editável.
- getValueAt(): Deverá implementar a lógica que irá retornar o valor em determinada célula, dado os argumentos de posição de linha e coluna.
- setValueAt(): Deverá implementar a lógica que irá configurar um valor em determinada célula. É importante salientar que quando tratamos de “célula” não quer dizer que você precisa trabalhar com o Jtable especificamente, em nosso caso que implementaremos mais a frente você verá que usaremos este método para configurar valores na propriedade do nosso bean. Ex: usuario.setNome('Devmedia').
- AddTableModelListener() e removeTableModelListener(): Para que possamos entender o funcionamento destes métodos nós precisamos entender primeiro o uso do TableModelListener.
Entendendo o TableModelListener e o TableModelEvent
O TableModelListener é importante para entendermos os dois últimos métodos da interface TableModel. Vamos conhece-lo através do código da Listagem 2.
Listagem 2. TableModelListener
public interface TableModelListener extends java.util.EventListener
{
public void tableChanged(TableModelEvent e);
}
O TableModelListener, assim como o TableModel, também é uma interface e nossa função é implementar o método tableChanged() com a lógica necessária. Qualquer alteração que ocorra no nosso TableModel será “monitorado” pelo nosso Listener, muito útil quando desejamos saber o que o usuário alterou sem que ele precise ficar clicando em botões a todo instante.
Perceba na Listagem 2 que o argumento passado para o método tableChanged() é um TableModelEvent que diz respeito as alterações que foram realizadas. Observe a Listagem 3.
Listagem 3. TableModelEvent
package javax.swing.event;
import java.util.EventObject;
import javax.swing.table.*;
public class TableModelEvent extends java.util.EventObject
{
public static final int INSERT = 1;
public static final int UPDATE = 0;
public static final int DELETE = -1;
public static final int HEADER_ROW = -1;
public static final int ALL_COLUMNS = -1;
protected int type;
protected int firstRow;
protected int lastRow;
protected int column;
public TableModelEvent(TableModel source) {
this(source, 0, Integer.MAX_VALUE, ALL_COLUMNS, UPDATE);
}
public TableModelEvent(TableModel source, int row) {
this(source, row, row, ALL_COLUMNS, UPDATE);
}
public TableModelEvent(TableModel source, int firstRow, int lastRow) {
this(source, firstRow, lastRow, ALL_COLUMNS, UPDATE);
}
public TableModelEvent(TableModel source, int firstRow, int lastRow, int column) {
this(source, firstRow, lastRow, column, UPDATE);
}
public TableModelEvent(TableModel source, int firstRow, int lastRow, int column, int type) {
super(source);
this.firstRow = firstRow;
this.lastRow = lastRow;
this.column = column;
this.type = type;
}
public int getFirstRow() { return firstRow; };
public int getLastRow() { return lastRow; };
public int getColumn() { return column; };
public int getType() { return type; }
}
Não implementaremos o TableModelEvent, apenas usaremos ele, mas precisamos entender como utilizá-lo e porque estamos utilizando os seus métodos.
Logo no início temos três variáveis: INSERT, UPDATE e DELETE. Essas variáveis irão especificar que tipo de alteração está sendo feita: uma inserção, uma Atualização ou uma deleção? Esse retorno é dado através do método getType() que retorna a variável type que consequentemente deve estar armazenando um dos tipos acima que citamos. Ou seja, quando um TableEvent é criado um tipo é especificado para ele e conseguimos identificar este tipo através do método getType().
Os atributos HEADER_ROW e ALL_COLUMNS servem como auxilio para passagem de parâmetro no construtor do TableModelEvent, ou seja, se desejarmos criar um Evento onde uma linha inteira foi alterada (todas as colunas) usamos o ALL_COLUMNS.
Ainda no TableModelEvent, temos três métodos que não podemos deixar passar despercebidos, são eles:
- getFirstRow(): Retorna o índice da linha que foi alterada. Aqui entra o uso do HEADER_ROW, pois se a alteração foi feita na linha onde ficam as colunas (META-DATA) o retorno deste método será o HEADER_ROW = -1. Podemos usar isso para checar se a alteração foi feita nas colunas ou nos dados propriamente ditos, por isso o uso do HEADER_ROW é importante neste ponto.
- getLastRow(): Retorna o índice da última linha que foi alterada.
- getColumn(): Retorna o índice da coluna que foi alterada. Aqui também entra o uso do ALL_COLUMNS, pois se todas as colunas sofreram alterações então o retorno será o valor da variável ALL_COLUMNS.
- getType(): Como explicamos logo acima este método retorna INSERT, UPDATE ou DELETE, para sabermos que ação foi feita neste evento.
Temos agora o conhecimento de como funciona o TableModelEvent e já podemos implementar um Listener simples apenas para estudo, como ostra a Listagem 4.
Listagem 4. Implementando um TableModelListener simples
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
public class DevmediaTableModelListener implements TableModelListener {
@Override
public void tableChanged(TableModelEvent e) {
int primeiraLinha = e.getFirstRow();
int ultimaLinha = e.getLastRow();
int coluna = e.getColumn();
String acao = "";
if (e.getType() == TableModelEvent.INSERT){
acao = "inseriu";
}else if (e.getType() == TableModelEvent.UPDATE){
acao = "alterou";
}else{
acao = "deletou";
}
if (primeiraLinha == TableModelEvent.HEADER_ROW){
System.out.println("Você "+acao+" a META-DATA da tabela");
}else{
System.out.println("Você "+acao+" da linha "+primeiraLinha+" até "+ultimaLinha);
}
if (coluna == TableModelEvent.ALL_COLUMNS){
System.out.println("Você "+acao+" todas as colunas");
}else{
System.out.println("Você "+acao+" a coluna "+coluna);
}
}
}
Nosso exemplo da Listagem 4 apenas demonstra tudo que explicamos sobre o TableModelListener e o TableModelEvent, veja que estamos apenas mostrando o que está sendo feito mas seu uso vai muito além disso.
Voltando ao TableModel
Até aqui já sabemos como funciona o TableModel, TableModelListener e o TableModelEvent, vimos com detalhes o uso de cada um, porém ainda ficamos com pendência em dois métodos: addTableModelListener() e removeTableModelListener(). Agora que temos um conhecimento mais aprofundado no Listener fica fácil perceber que os métodos mencionados acima servem para adicionar e remover um Listener do TableModel, respectivamente. É como se estivessemos “anexando” nosso DevmediaTableModelListener, mostrado na Listagem 4, ao nosso TableModel.
Antes de iniciarmos a implementação do nosso TableModel ainda temos mais alguns métodos de suma importância para aprender, são eles:
- fireTableChanged(): Você pode estar se perguntando porque não mostramos estes métodos na interface TableModel, e a resposta é porque eles estão presentes em uma implementação chamada AbstractTableModel e não na interface TableModel. Esta é uma classe abstrata que implementa alguns métodos e adiciona algumas novas funcionalidades tornando o nosso TableModel ainda mais poderoso. O fireTableChanged() é um método responsável por notificar todos os listeners “anexados” ao nosso TableModel que alguma alteração ocorreu. Lembre-se logo no ínicio que explicamos que nosso TableModel poderia ter um listener associado a ele, e o fireTableChanged() é responsável por disparar mudanças que ocorram na tabela.
Os outros métodos com prefixo “fireTable...” são todos métodos provenientes do fireTableChanged(), ou seja, eles apenas passam argumentos distintos para o fireTableChanged(). Vejamos:
- FireTableDataChanged(): Notifica os listeners que todas as células foram alteradas.
- FireTableStructureChanged(): Notifica os listeners que a estrutura da tabela mudou, ou seja, o número de colunas, o tipos das colunas e o nome delas.
- FireTableRowsInserted(): Notifica os listeners que a quantidade de linhas aumentou, ou seja, dados foram adicionados.
- FireTableRowsUpdates(): Notifica os listeners que as linhas foram atualizadas.
- FireTableRowsDeleted(): Notifica os listeners que as linhas foram deletadas.
- FireTableCellUpdated(): Notifica os listeners que o valor de uma célula foi alterada.
Perceba que os métodos “fire...” todos fazem chamada aos listeners, ou seja, é nos listeners que devem implementar as alterações que serão realizadas no Jtable.
Por exemplo, Você adiciona um novo Usuario ao TableModel, e faz chamada ao método fireTableRowsInserted() que por sua vez chama um listener que irá realizar a alteração no Jtable.
Antes precisaremos de um Bean para manipular então criamos o Bean Usuario, como mostra a Listagem 5.
Listagem 5. Bean Usuario
package br.com.crudusuario.bean;
public class Usuario {
private String login;
private String senha;
private String nome;
public String getLogin() {
return login;
}
public void setLogin(String login) {
this.login = login;
}
public String getSenha() {
return senha;
}
public void setSenha(String senha) {
this.senha = senha;
}
public String getNome() {
return nome;
}
public void setNome(String nome) {
this.nome = nome;
}
}
Agora podemos construir nosso DevmediaTableModel, pois já temos o conhecimento necessário para tal. Observe a Listagem 6.
Listagem 6. DevmediaTableModel
package br.com.crudusuario.bean;
import java.util.ArrayList;
import java.util.List;
import javax.swing.table.AbstractTableModel;
public class DevmediaTableModel extends AbstractTableModel {
private List<Usuario> usuarios;
private String[] colunas = new String[]{
"Login","Nome", "Senha"};
/** Creates a new instance of DevmediaTableModel */
public DevmediaTableModel(List<Usuario> usuarios) {
this.usuarios = usuarios;
}
public DevmediaTableModel(){
this.usuarios = new ArrayList<Usuario>();
}
public int getRowCount() {
return usuarios.size();
}
public int getColumnCount() {
return colunas.length;
}
@Override
public String getColumnName(int columnIndex){
return colunas[columnIndex];
}
@Override
public Class<?> getColumnClass(int columnIndex) {
return String.class;
}
public void setValueAt(Usuario aValue, int rowIndex) {
Usuario usuario = usuarios.get(rowIndex);
usuario.setLogin(aValue.getLogin());
usuario.setNome(aValue.getNome());
usuario.setSenha(aValue.getSenha());
fireTableCellUpdated(rowIndex, 0);
fireTableCellUpdated(rowIndex, 1);
fireTableCellUpdated(rowIndex, 2);
}
@Override
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
Usuario usuario = usuarios.get(rowIndex);
switch (columnIndex) {
case 0:
usuario.setLogin(aValue.toString());
case 1:
usuario.setNome(aValue.toString());
case 2:
usuario.setSenha(aValue.toString());
default:
System.err.println("Índice da coluna inválido");
}
fireTableCellUpdated(rowIndex, columnIndex);
}
public Object getValueAt(int rowIndex, int columnIndex) {
Usuario usuarioSelecionado = usuarios.get(rowIndex);
String valueObject = null;
switch(columnIndex){
case 0: valueObject = usuarioSelecionado.getLogin(); break;
case 1: valueObject = usuarioSelecionado.getNome(); break;
case 2 : valueObject = usuarioSelecionado.getSenha(); break;
default: System.err.println("Índice inválido para propriedade do bean Usuario.class");
}
return valueObject;
}
@Override
public boolean isCellEditable(int rowIndex, int columnIndex) {
return false;
}
public Usuario getUsuario(int indiceLinha) {
return usuarios.get(indiceLinha);
}
public void addUsuario(Usuario u) {
usuarios.add(u);
int ultimoIndice = getRowCount() - 1;
fireTableRowsInserted(ultimoIndice, ultimoIndice);
}
public void removeUsuario(int indiceLinha) {
usuarios.remove(indiceLinha);
fireTableRowsDeleted(indiceLinha, indiceLinha);
}
public void addListaDeUsuarios(List<Usuario> novosUsuarios) {
int tamanhoAntigo = getRowCount();
usuarios.addAll(novosUsuarios);
fireTableRowsInserted(tamanhoAntigo, getRowCount() - 1);
}
public void limpar() {
usuarios.clear();
fireTableDataChanged();
}
public boolean isEmpty() {
return usuarios.isEmpty();
}
}
Baseado em todos os conceitos aprendidos acima, fica como exercício para você, caro leitor, estudar o uso da Listagem 5. Mas você pode estar se perguntando: mas e o nosso listener? Como irá funcionar se não criamos um listener? Como serão adicionadas as linhas e colunas sem um listener para ser disparado no “fireTableChanged()”?
O Jtable por si só já implementa o TableModelListener, ou seja, além de ser um componente ele também é um listener, e isso faz com que não precisemos criar um listener e controlar as alterações do Jtable. Veja como podemos fazer com o código da Listagem 7.
Listagem 7. Adicionando nosso Model no Jtable
DevmediaTableModel model = new DevmediaTableModel();
JTable jTableUsuarios = new Jtable(model);
Usuario u = new Usuario();
u.setLogin("ronaldo");
u.setNome("Ronaldo Lanhellas");
u.setSenha("123");
model.addUsuario(u);
Acima nós criamos nosso model, depois criamos um Jtable associando o nosso model ao Jtable. Daqui em diante é só trabalhar com os métodos que criamos no nosso DevmediaTableModel. Se você depurar o método “addUsuario()” verá que na chamada ao fireTableRowsInserted() ele acha um listener que possui o tableChanged() e esse listener está dentro da própria classe Jtable, pois como dissemos anteriormente além de ser um componente ele também é um listener.
Obviamente que se houverem ações especificas que o listener do Jtable não implementa, você poderá criar seu próprio listener e “anexar” ao seu TableModel através do método addTableModelListener(), pois agora você já tem conhecimento suficiente para criar um listener.
Neste artigo vimos com detalhes vários conceitos importantes para só então conseguirmos de fato construir nosso próprio TableModel. Aprendemos primeiro sobre a interface TableModel, logo em seguida vimos como funciona o TableModelListener e juntamente com este assunto já abordamos o TableModelEvent, sendo todas esses três assuntos de extrema importância para entender o funcionamento do nosso DevmediaTableModel.
Ainda há uma discussão que não foi abordada neste artigo, que é o uso do DefaultTableModel, um TableModel padrão utilizado para quem não quer fazer o seu próprio. Muitos condenam o seu uso e dizem que a qualquer custo você deve criar o seu próprio e deixar o Default de lado, pois ele pode trazer lentidão a sua aplicação e outros problemas, como alto acoplamento de código. De fato o melhor é sempre criar o seu próprio TableModel por tratar-se de uma classe mais específica para o que você precisa e mais “consistente” do ponto de vista funcional, mas vale a pena conferir o que o DefaultTableModel faz e estudar um pouco mais sobre a relevância do mesmo.