It was really hard to do, but I did it. :D
The following code is complete, compileable, testable and executable. It includes, among the necessary explanations, the complete code of all classes (even the imports
). Therefore, other people will be able to test and reuse it.
Your problem is with hour-type fields. However, to make sure my solution is good, I have solved the problem with time, date, and CPF fields, as well as leaving open the way to implement with any other required formats. The fields with these formats JTable
have the following characteristics in this implementation:
Are data with masks. ##:##
is used for hours, ##/##/####
for dates and ###.###.###-##
for Cpfs. It never lets the mask be overwritten or erased.
Due to the use of the mask, it does not let invalid characters be typed.
To JTable
does not let you leave the editing field if its content is neither valid nor totally blank. In the case of the hours, he will check if the time is between 00 and 23 and if the minutes are between 00 and 59. On the dates, he will check whether the months are 30 or 31 days and will check whether the year is leap or not on 29 February. In the CPF field, the calculation of the verifier digits shall be made.
After you finish typing one of these fields with a valid value (and only a valid value), it automatically jumps to the next field without you having to click or press TAB.
First, let’s start with our classes to represent simple data. In this case, times, dates and Cpfs.
Filing cabinet Hora.java
:
import java.util.stream.Stream;
/**
* @author Victor Stafusa
*/
public final class Hora {
private final int horas;
private final int minutos;
public static Hora parse(String formatado) {
try {
int[] parts = Stream.of(formatado.split(":")).mapToInt(Integer::parseInt).toArray();
if (parts.length != 2) return null;
return new Hora(parts[0], parts[1]);
} catch (IllegalArgumentException e) {
return null;
}
}
public Hora(int horas, int minutos) {
if (horas < 0 || horas > 23 || minutos < 0 || minutos > 59) throw new IllegalArgumentException();
this.horas = horas;
this.minutos = minutos;
}
public int getHoras() {
return horas;
}
public int getMinutos() {
return minutos;
}
// Não estou usando o toString() para você poder ver que não preciso confiar na existência de um toString() em sua classe.
public String converte() {
return String.format("%02d:%02d", horas, minutos);
}
}
Filing cabinet Data.java
:
import java.util.Arrays;
import java.util.stream.Stream;
/**
* @author Victor Stafusa
*/
public final class Data {
private final int dia;
private final int mes;
private final int ano;
public static Data parse(String formatado) {
try {
int[] parts = Stream.of(formatado.split("/")).mapToInt(Integer::parseInt).toArray();
if (parts.length != 3) return null;
return new Data(parts[0], parts[1], parts[2]);
} catch (IllegalArgumentException e) {
return null;
}
}
public Data(int dia, int mes, int ano) {
if (dia <= 0 || dia > 31 || mes <= 0 || mes > 12 || ano < 1583 || ano > 9999) throw new IllegalArgumentException();
if (dia == 31 && Arrays.asList(2, 4, 6, 9, 11).contains(mes)) throw new IllegalArgumentException();
if (dia == 30 && mes == 2) throw new IllegalArgumentException();
if (dia == 29 && mes == 2 && (ano % 4 != 0 || Arrays.asList(100, 200, 300).contains(ano % 400))) throw new IllegalArgumentException();
this.dia = dia;
this.mes = mes;
this.ano = ano;
}
public int getDia() {
return dia;
}
public int getMes() {
return mes;
}
public int getAno() {
return ano;
}
// Não estou usando o toString() para você poder ver que não preciso confiar na existência de um toString() em sua classe.
public String converte() {
return String.format("%02d/%02d/%04d", dia, mes, ano);
}
}
Filing cabinet Cpf.java
:
import java.util.regex.Pattern;
/**
* @author Victor Stafusa
*/
public final class Cpf {
private static final Pattern REGEX = Pattern.compile("^\\d{3}\\.\\d{3}\\.\\d{3}\\-\\d{2}$");
private final String digitos;
public static Cpf parse(String formatado) {
try {
return new Cpf(formatado);
} catch (IllegalArgumentException e) {
return null;
}
}
public Cpf(String digitos) {
if (!REGEX.matcher(digitos).matches()) throw new IllegalArgumentException();
String d2 = digitos.replaceAll("\\.|\\-", "");
int[] numeros = new int[11];
for (int i = 0; i < 11; i++) {
numeros[i] = d2.charAt(i) - '0';
}
int a = 0;
int b = 0;
for (int i = 0; i < 9; i++) {
a += (i + 1) * numeros[i];
b += (11 - i) * numeros[i];
}
a = a % 11 % 10;
b += 2 * a;
b = b * 10 % 11 % 10;
if (a != numeros[9] || b != numeros[10]) throw new IllegalArgumentException();
this.digitos = digitos;
}
public String getDigitos() {
return digitos;
}
}
These three classes up there are relatively simple Beans. However, note that each of them has a static method parse(String)
which returns an instance of the class if a String
the parameter is valid or null
if it is not, which will be useful further on in our CellEditor
.
The logic that decides whether or not the data is valid is in the constructors, so that invalid instances cannot be created. However, this logic could be somewhere else if it were necessary, because what matters to the CellEditor
(that will be explained below) is that the static method returns an instance only when it is valid, returning null
otherwise. Also note that these classes are immutable, to avoid that valid instances may become invalid in the future, or even change from one valid state to another, but at an inopportune time.
To the JTable
, each row is represented by an instance of any class. In this example, I have each row with five columns, called campo1
, hora
, campo2
, data
and cpf
. The columns campo1
and campo2
are just normal columns with standard behavior, while the other three are our special columns. These fields for each row are stored in the class MeuElemento
, nothing more is than a very simple bean with getters and setters with nothing special.
Here is the file MeuElemento.java
:
/**
* @author Victor Stafusa
*/
public class MeuElemento {
private String campo1;
private String campo2;
private Hora hora;
private Data data;
private Cpf cpf;
public MeuElemento() {
}
public String getCampo1() {
return campo1;
}
public void setCampo1(String campo1) {
this.campo1 = campo1;
}
public String getCampo2() {
return campo2;
}
public void setCampo2(String campo2) {
this.campo2 = campo2;
}
public Hora getHora() {
return hora;
}
public void setHora(Hora hora) {
this.hora = hora;
}
public Data getData() {
return data;
}
public void setData(Data data) {
this.data = data;
}
public Cpf getCpf() {
return cpf;
}
public void setCpf(Cpf cpf) {
this.cpf = cpf;
}
}
Having then the class MeuElemento
that represents each of the lines, we can then create a list of elements and with it build our TableModel
. Here is the file MeuTableModel.java
:
import java.util.List;
import javax.swing.table.AbstractTableModel;
/**
* @author Victor Stafusa
*/
public class MeuTableModel extends AbstractTableModel {
private static final long serialVersionUID = 1L;
private final List<MeuElemento> elementos;
public MeuTableModel(List<MeuElemento> elementos) {
this.elementos = elementos;
}
@Override
public int getRowCount() {
return elementos.size();
}
@Override
public int getColumnCount() {
return 5;
}
@Override
public String getColumnName(int columnIndex) {
return new String[]{"Nome 1", "Horas", "Nome 2", "Data", "CPF"}[columnIndex];
}
@Override
public Class<?> getColumnClass(int columnIndex) {
return new Class<?>[]{String.class, Hora.class, String.class, Data.class, Cpf.class}[columnIndex];
}
@Override
public boolean isCellEditable(int rowIndex, int columnIndex) {
return true;
}
@Override
public Object getValueAt(int rowIndex, int columnIndex) {
MeuElemento elemento = elementos.get(rowIndex);
switch (columnIndex) {
case 0:
return elemento.getCampo1();
case 1:
return elemento.getHora();
case 2:
return elemento.getCampo2();
case 3:
return elemento.getData();
case 4:
return elemento.getCpf();
default:
throw new IllegalArgumentException();
}
}
@Override
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
MeuElemento elemento = elementos.get(rowIndex);
switch (columnIndex) {
case 0:
elemento.setCampo1((String) aValue);
break;
case 1:
elemento.setHora((Hora) aValue);
break;
case 2:
elemento.setCampo2((String) aValue);
break;
case 3:
elemento.setData((Data) aValue);
break;
case 4:
elemento.setCpf((Cpf) aValue);
break;
default:
throw new IllegalArgumentException();
}
fireTableCellUpdated(rowIndex, columnIndex);
}
}
Having the TableModel
, our next step is to CellRenderer
. Therefore, the file follows MeuCellRenderer.java
:
import java.awt.Color;
import java.awt.Component;
import javax.swing.JTable;
import javax.swing.table.DefaultTableCellRenderer;
/**
* @author Victor Stafusa
*/
public class MeuCellRenderer extends DefaultTableCellRenderer {
private static final long serialVersionUID = 1L;
private final ModelCartao mCard;
public MeuCellRenderer(ModelCartao mCard) {
this.mCard = mCard;
}
@Override
protected void setValue(Object value) {
setText(value == null ? ""
: value instanceof Hora ? ((Hora) value).converte()
: value instanceof Data ? ((Data) value).converte()
: value instanceof Cpf ? ((Cpf) value).getDigitos()
: value.toString());
}
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
Color c = mCard.isCor(row) ? Color.GREEN : Color.WHITE;
setBackground(c);
return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
}
}
The method setValue(Object)
is responsible for completing the value to be displayed according to the object referring to the cell that will be obtained from the TableModel
. Because of this, this method ends up being responsible for converting the value (whatever class it is) to the representation in String
that will show you on the screen.
Note that I have kept the same logic of yours to choose whether the cell is white or green. As I do not know what its class ModelCartao
does and does not even the criteria it uses to decide the color of the cell, I decided to put a minimalist implementation of it so that you then replace it with the implementation you want. This is my file ModelCartao.java
:
public class ModelCartao {
public boolean isCor(int row) {
return row % 2 == 0; // Use o critério que você achar melhor.
}
}
Having done all this, now comes the most difficult and laborious part. We come to our CellEditor
. Here is the file MeuCellEditor.java
:
import java.awt.AWTException;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.Robot;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.text.ParseException;
import java.util.function.Predicate;
import javax.swing.AbstractCellEditor;
import javax.swing.JFormattedTextField;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.table.TableCellEditor;
import javax.swing.text.JTextComponent;
import javax.swing.text.MaskFormatter;
/**
* @author Victor Stafusa
*/
public class MeuCellEditor extends AbstractCellEditor implements TableCellEditor {
private static final long serialVersionUID = 1L;
private JTextComponent component;
private int colunaEditando;
private int linhaEditando;
public MeuCellEditor() {
this.colunaEditando = -1;
this.linhaEditando = -1;
}
private boolean celulaHora() {
return colunaEditando == 1;
}
private boolean celulaData() {
return colunaEditando == 3;
}
private boolean celulaCpf() {
return colunaEditando == 4;
}
@Override
public boolean stopCellEditing() {
boolean vazio = component.getText().replaceAll(":|_| |\\/|\\-|\\.", "").isEmpty();
if (!vazio && getCellEditorValue() == null) return false;
return super.stopCellEditing();
}
@Override
public Object getCellEditorValue() {
return celulaHora() ? Hora.parse(component.getText())
: celulaData() ? Data.parse(component.getText())
: celulaCpf() ? Cpf.parse(component.getText())
: component.getText();
}
@Override
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
this.colunaEditando = column;
this.linhaEditando = row;
if (celulaHora()) {
component = campoHora();
if (value instanceof Hora) component.setText(((Hora) value).converte());
} else if (celulaData()) {
component = campoData();
if (value instanceof Data) component.setText(((Data) value).converte());
} else if (celulaCpf()) {
component = campoCpf();
if (value instanceof Cpf) component.setText(((Cpf) value).getDigitos());
} else {
component = new JTextField();
if (value != null) component.setText(String.valueOf(value));
}
EventQueue.invokeLater(() -> component.requestFocusInWindow());
return component;
}
private static Robot ROBOT;
private synchronized static Robot obterRobot() {
if (ROBOT == null) {
try {
ROBOT = new Robot();
} catch (AWTException e) {
throw new UnsupportedOperationException(e);
}
}
return ROBOT;
}
private static JFormattedTextField campoMascara(String mascara, int posicaoFinal, Predicate<String> teste) {
Robot robot = obterRobot();
MaskFormatter formatter;
try {
formatter = new MaskFormatter(mascara);
} catch (ParseException e) {
throw new AssertionError(e);
}
formatter.setValidCharacters("0123456789");
formatter.setPlaceholderCharacter('_');
JFormattedTextField jftf = new JFormattedTextField(formatter);
jftf.addKeyListener(new KeyListener() {
@Override
public void keyTyped(KeyEvent e) {
if (jftf.getCaretPosition() == posicaoFinal && teste.test(jftf.getText().replace("_", "") + String.valueOf(e.getKeyChar()))) {
robot.keyPress(KeyEvent.VK_TAB);
robot.keyRelease(KeyEvent.VK_TAB);
}
}
@Override
public void keyPressed(KeyEvent e) {
}
@Override
public void keyReleased(KeyEvent e) {
}
});
return jftf;
}
private static JFormattedTextField campoHora() {
return campoMascara("##:##", 4, x -> Hora.parse(x) != null);
}
private static JFormattedTextField campoData() {
return campoMascara("##/##/####", 9, x -> Data.parse(x) != null);
}
private static JFormattedTextField campoCpf() {
return campoMascara("###.###.###-##", 13, x -> Cpf.parse(x) != null);
}
}
This archive demands greater explanations:
First, the CellEditor
keeps the fields colunaEditando
and linhaEditando
to find out which cell JTable
which is being edited in order to find the most appropriate component type for editing. Such component is stored in the field component
when it becomes available (initially null
).
The methods celulaHora()
, celulaData()
and celulaCpf()
to identify the cell type of the component being edited. The logic of these methods is simple, and consists only of checking a fixed column number, but for some JTable
Complicated things you’re going to do in the real world, surely there will be cases where it won’t be that simple.
The method stopCellEditing()
is fundamental. It is the method responsible for holding the issue in the cell (when returning false
) if the contents given in the entry are not filled correctly without being blank. The replaceAll(":|_| |\\/|\\-|\\.", "")
is the part that eliminates the special characters of the mask, where ":|_| |\\/|\\-|\\."
that is to say "two dots or underline or white space or bar or dash or dot". The idea is that the stopCellEditing()
just come back true
if the field is either properly filled or empty, returning false
otherwise (if partially, incomplete and/or invalid). Therefore, the user cannot leave this field filled incorrectly or partially, forcing a filling that is valid or leaves the field empty.
The method getCellEditorValue()
is responsible for obtaining the object (not necessarily a String
) represented by the editing cell. In our case, in addition to String
, he can return Hora
, Data
or Cpf
.
The method getTableCellEditorComponent(JTable, Object, boolean, int, int)
is responsible for creating the CellEditor
. It also records which cell is being edited. It is important to note that this method does not reuse components from one cell to another, discarding them when a change of the chosen cell occurs (and this is on purpose). Otherwise, it is possible to get some bugs from making the values of a cell start to be incorrectly copied into other cells when the component is reused.
The EventQueue.invokeLater(() -> component.requestFocusInWindow())
is an important workaround, as sometimes a field starts receiving typing without being focused, which would cause problems to jump alone to the next field afterwards without needing the TAB. With that gambiarra, humm, ops I mean... special technique the component gains focus alone when it starts receiving typing.
The method campoMascara(String, int, Predicate<String>)
is responsible for creating the editing component. It creates a JFormattedTextField
according to the given mask in the parameter. It is in this method that it is possible to see the technique I used to make it jump to the subsequent field alone when typing is finished. I used a master-power-super-gambeta-plus with the java.awt.Robot
which keeps sending to the operating system a pressing of the TAB key from inside a KeyListener
to jump field whenever he realizes that it has been filled properly. The parameter posicaoFinal
serves so that he can know in which position of the text he should jump from the field and the teste
serves to enable him to assess whether the field has been filled in correctly or not. It is important to note that the TAB will only be triggered when the field is Caret in the last position and this will make it valid (and this is what is tested), because this criterion has to agree with the situation where the stopCellEditing()
returns true
with the field completed.
Finally, the static methods campoHora()
, campoData()
and campoCpf()
are responsible for creating the JFormattedTextField
necessary, specifying for each different case, the mask, the position in which the automatic TAB can be triggered and the test to verify whether or not the fill was correct.
Finally, to complete, our main class to run and test our program. Here’s the file TesteJTable.java
:
import java.awt.EventQueue;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JTable;
/**
* @author Victor Stafusa
*/
public class TesteJTable {
public static void main(String[] args) {
EventQueue.invokeLater(TesteJTable::rodar);
}
private static void rodar() {
JFrame jf = new JFrame("Teste");
jf.setBounds(20, 20, 500, 200);
jf.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
List<MeuElemento> elementos = new ArrayList<>(4);
elementos.add(new MeuElemento());
elementos.add(new MeuElemento());
elementos.add(new MeuElemento());
elementos.add(new MeuElemento());
MeuTableModel tm = new MeuTableModel(elementos);
JTable jt = new JTable(tm);
jt.setDefaultEditor(Object.class, new MeuCellEditor());
jt.setDefaultRenderer(Object.class, new MeuCellRenderer(new ModelCartao()));
jf.add(jt);
jf.setVisible(true);
}
}
mas esse método está ultrapassado
- what it means?– user28595
I’ll edit, for better understanding!
– Thomas Braz Pinto
Dude, a hint, there’s a lot of unnecessary text there, just focus on the problem faced. I even understood your problem, but the long text ended up confusing.
– user28595
From what I understand you have a table with columns of editable hours, and you want it to be applied masks in the cells and, when filled, you want the focus to be passed to next. Correct?
– user28595
accurate, but I need dynamics to do this (because Jtable is going to have some features that are not the case at the moment), I wanted to explain as detailed as possible precisely to better understand, sorry for the excess, if I can help, any questions just talk!
– Thomas Braz Pinto
I did something similar, but this change of focus I think is not interesting not huh. You use your own Tablemodel? If yes, everything is easier.
– user28595
Yes, I have my own Model, using the Abstract method! I’ll make some images here to better explain, then I’ll post the functionality I want ok?
– Thomas Braz Pinto
Add, if you have, your Cellrenderer and cellEditor code in addition to your tablemodel.
– user28595
Dude, we can go to chat, because it’s complicated this system, it’s going to be something gigantic here in the topic
– Thomas Braz Pinto
Let’s go continue this discussion in chat.
– Thomas Braz Pinto
Could it be by pressing TAB? It must be automatic when filling the whole field?
– user28595
It can’t be by pressing tab, you have to transfer at the end of the typing, because as I said, imagine that you have 10 beats per day, for 5 years, you need agility not to stay so long doing this (there are days that the guys here have about 20 processes from 5 months to 20 years here), so I need it to be at the time that the user finished typing all the transfer, the tab is already standard, you can use it automatically, but it will "reduce the efficiency" of what I need
– Thomas Braz Pinto
Well, I do not see all this difficulty in pressing tab to each field, is a less complex solution that facilitates the chance of an answer that meets and I have already found in the ready soen.
– user28595
If you find anything, put it here.
– user28595