Essa semana pintou a dúvida de como fazer um console em java. Na verdade a necessidade era criar um painel onde pudesse diferenciar a formatação do texto (especialmente a cor) dependendo da fonte de envio, por exemplo, para os logs vindos do banco de dados imprimiria em azul, os logs do modem, em verde, e assim por diante.
Antes de pensar em construir um console o primeiro componente que tentei utilizar foi o JTextArea. Como havia comentado, meu objetivo era diferenciar visualmente os logs vindos de diferentes partes da aplicação, basicamente modificando a cor de exibição do texto. Pesquisando um pouco descobri que o JTextArea lhe permite modificar os atributos do texto porém de modo uniforme, ou seja, para todo o componente. Essa descoberta inviabilizou o uso deste componente e me fez pensar em como seria a implementação do que eu estava precisando, certamente não era o primeiro e nem o último a precisar disso.
Parti então para a implementação de um console onde pudesse na adição de novas mensagens especificar o formato desejado de exibição do texto. Os componentes para quem precisa manter diferentes formatos de texto/conteúdo são o JEditorPane e o JTextPane. Pra minha surpresa, nenhum dos meus livros cobria estes dois componentes, apenas deixava uma referência a um livro específico sobre componentes swing e uma mensagem dizendo que se tratava de componentes complexos :-(. De fato os componentes JEditorPane e JTextPane são bastante complexos, no entanto, para o uso no qual precisava, não foi necessário estuda-los completamente.
Já que a idéia era construir um console, pensei em um atributo que gostaria de encontrar em qualquer proposta deste genero, um setup de quantidade máxima de dados mantidos no console. Uma vez ultrapassado este valor, o próprio console remove o excedente, como uma FIFO, mantendo um certo controle sobre a quantidade de memória utilizada pelo componente. Os demais atributos seriam a possibilidade de setup rápido das mensagem enviadas, facilitando a vida de quem utiliza o console.
O código abaixo, embora extenso, detalha completamente a implementação do console proposto. A primeira parte descreve a implementação da classe JavaMilkConsole e a segunda parte demonstra o uso deste componente (imagem exibida inicialmente). O código está bastante comentado afim de facilitar a compreensão de todos (qualquer nível).
package javamilk; import java.awt.Color; import java.awt.Font; import java.io.IOException; import java.io.PrintWriter; import java.io.Writer; import javax.swing.JTextPane; import javax.swing.SwingUtilities; import javax.swing.text.BadLocationException; import javax.swing.text.Style; import javax.swing.text.StyleConstants; import javax.swing.text.StyledDocument; /** * Essa classe implementa um console de visualização extendendo o * componente JTextPane * @author giuliano */ public class JavaMilkConsole extends JTextPane{ //Constantes private static final Color TEXTCOLOR = Color.BLACK; private static final Color TEXTBGCOLOR = Color.WHITE; private static final int TEXTFONTSIZE = 12; private static final String TEXTFONTFAMILY = Font.SANS_SERIF; private static final Boolean TEXTBOLD = false; private static final int MAXROW = 100; //Variáveis que mantém o setup atual do estilo private Color textColor = TEXTCOLOR; private Color textBgColor = TEXTBGCOLOR; private int textFontSize = TEXTFONTSIZE; private String textFontFamily = TEXTFONTFAMILY; private Boolean textBold = TEXTBOLD; //Strings que definem os estilos que compoe este console private static final String PADRAO = "padrao"; //Conteúdo exibido no console private StyledDocument conteudo; //Estilos que criaremos no console private Style padrao; //Estilo padrão - único para este console //A classe console oferece um modo de escrita semelhante ao da classe //System. Por essa razão o objeto "out" foi mantido público. //Exempo de uso será. objJavaMilkConsole.out.println("minha msg"); public PrintWriter out; //Buffer do console int[] bufRows; //Buffer que guarda a quantidade de caracteres por linha int actNumRows; //Numero atual de rows no console int p2Fill; //Aponta para a posição a ser preenchida int p2Remove; //Aponta para a posição a ser removida /** * Construtor padrão da classe JavaMilkConsole * @param maxRow Tamanho do buffer de linhas do console. Deve ser maior ou * igual a 1, caso contrário o tamanho padrão será assumido. */ public JavaMilkConsole(int maxRow){ //Já que a idéia é ser um console de visualização vamos inibir a edição setEditable(false); //JavaMilkConsole herda tudo de JTextPane e consequentemente de JEditorPane //Essa duas classes possuem como modelo de dados a classe StyledDocument //Vamos puxar uma referência a este modelo pra que possamos adicionar //os estilos que queremos mostrar no console conteudo = this.getStyledDocument(); //O modelo de dados StyledDocument trabalha em hierarquia, dessa forma //podemos criar um estilo padrão e descender os demais. A influencia //disso é que uma vez configurado um parâmetro no estilo pai os demais //herdam essa característica. //Como a idéia deste console é permitir que o usuário modifique a //qualquer momento a configuração de estilo (cor, fundo, fonte, etc) não //faz sentido oferecer mais um estilo (pelo menos não neste momento). padrao = conteudo.addStyle(PADRAO, null); //estilo padrão é o root //Agora que temos um estilo definido, vamos fazer a configuração default //dele. Daqui a pouco iremos implementar os métodos que permitirão //modificar isso em tempo de execução. //Pra configurar cada estilo, vamos usar a classe StyleContants //Repare que o primeiro argumento é sempre o nome do estilo, seguido //da configura referente ao método. StyleConstants.setFontFamily(padrao,TEXTFONTFAMILY); StyleConstants.setFontSize(padrao, TEXTFONTSIZE); StyleConstants.setForeground(padrao, TEXTCOLOR); StyleConstants.setBackground(padrao, TEXTBGCOLOR); StyleConstants.setBold(padrao, TEXTBOLD); //Como dito anteriormente, oferecemos um mecanismo de escrita no console //semelhante ao da classe "System". //Pra que isso seja possível, precisamos utilizar interna que direcione //o fluxo de dados para dentro do nosso console. Veja a implementação //da classe ConsoleWriter. out = new PrintWriter(new ConsoleWriter()); //Inicializando buffer if(maxRow<1) maxRow=MAXROW; bufRows = new int[maxRow+1]; actNumRows = 0; p2Fill = 0; p2Remove = 0; } /** * Construtor sobrecarregado sem a opção de especificar tamanho do buffer. * Neste caso o tamanho é o default configurado na classe. */ public JavaMilkConsole(){ this(MAXROW); } /** * Método publico que permite modificar a cor de exibição do texto * @param cor cor do texto que desejamos */ public void setFontForeground(Color cor){ textColor = cor; StyleConstants.setForeground(padrao, textColor); } /** * Método público que permite modificar a cor de fundo do texto * @param cor cor de fundo que desejamos */ public void setFontBackground(Color cor){ textBgColor = cor; StyleConstants.setBackground(padrao, textBgColor); } /** * Método público que permite modificar o tamanho da fonte * @param tamanho Tamanho de exibição da fonte */ public void setFontSize(int tamanho){ textFontSize = tamanho; StyleConstants.setFontSize(padrao, textFontSize); } /** * Método público que pemite modifica a familia a qual a fonte pertence * @param familia Familia da fonte */ public void setFontFamily(String familia){ textFontFamily = familia; StyleConstants.setFontFamily(padrao, textFontFamily); } /** * Método público que permite tornar o texto bold (negrito) * @param bold */ public void setFontBold(Boolean negrito){ textBold = negrito; StyleConstants.setBold(padrao, textBold); } /** * Apaga todo o conteúdo exibido atualmente no console. * Vale lembrar que a implementação deste método não é thread safety * (pretendo mostrar isso em outro post). */ public void clearContents(){ try { //remove todo o conteúdo do console conteudo.remove(0, conteudo.getLength()); //Zera as todo buffer resetBuffer(); } catch (BadLocationException ex) { ex.printStackTrace(); } } /** * Adiciona uma mensagem no console. Lembre-se que este método não adiciona * por padrão a quebra de linha, logo, caso queira esse efeito, adicione o * caracter especial \n em sua mensagem. * @param msg Mensagem a ser adicionada no console */ public void appendMessage(String msg){ insertMessage(msg); } /** * Adiciona uma mensagem no console, na cor especificada. * Lembre-se que este método não adiciona por padrão a quebra de linha, * logo, caso queira esse efeito, adicione o caracter especial \n em sua * mensagem. * @param msg Mensagem a ser adicionada no console * @param corTexto Cor do texto */ public void appendMessage(String msg, Color corTexto){ //Alteramos a cor do texto do estilo padrão StyleConstants.setForeground(padrao, corTexto); //Adicionamos a mensagem no console - com o estilo modificado insertMessage(msg); //Voltamos o estilo ao setup anterior, já que esta chamada não //deve modificar permanentemente o estilo. As demais chamadas seguiram //o estilo definido no console (por padrão ou através de métodos //específicos). StyleConstants.setForeground(padrao, textColor); } /** * Adiciona uma mensagem no console, na cor especificada (texto e fundo). * Lembre-se que este método não adiciona por padrão a quebra de linha, * logo, caso queira esse efeito, adicione o caracter especial \n em sua * mensagem. * @param msg Mensagem a ser adicionada no console * @param corTexto Cor do texto * @param corFundoTexto Cor de fundo do texto */ public void appendMessage(String msg, Color corTexto, Color corFundoTexto){ //Alteramos a cor do texto do estilo padrão StyleConstants.setForeground(padrao, corTexto); //Alteramos a cor de fundo StyleConstants.setBackground(padrao, corFundoTexto); //Adicionamos a mensagem no console - com o estilo modificado insertMessage(msg); //Voltamos o estilo ao setup anterior, já que esta chamada não //deve modificar permanentemente o estilo. As demais chamadas seguiram //o estilo definido no console (por padrão ou através de métodos //específicos). StyleConstants.setForeground(padrao, textColor); StyleConstants.setBackground(padrao, textBgColor); } /** * Adiciona uma mensagem no console, na cor especificada (texto e fundo) e * em negrito. * Lembre-se que este método não adiciona por padrão a quebra de linha, * logo, caso queira esse efeito, adicione o caracter especial \n em sua * mensagem. * @param msg Mensagem a ser adicionada no console * @param corTexto Cor do texto * @param corFundoTexto Cor de fundo do texto * @param negrito se o texto deve ser impresso em negrito */ public void appendMessage(String msg, Color corTexto, Color corFundoTexto, Boolean negrito){ //Alteramos a cor do texto do estilo padrão StyleConstants.setForeground(padrao, corTexto); //Alteramos a cor de fundo StyleConstants.setBackground(padrao, corFundoTexto); //Alteramos o estilo StyleConstants.setBold(padrao, negrito); //Adicionamos a mensagem no console - com o estilo modificado insertMessage(msg); //Voltamos o estilo ao setup anterior, já que esta chamada não //deve modificar permanentemente o estilo. As demais chamadas seguiram //o estilo definido no console (por padrão ou através de métodos //específicos). StyleConstants.setForeground(padrao, textColor); StyleConstants.setBackground(padrao, textBgColor); StyleConstants.setBold(padrao, textBold); } /** * Adiciona uma mensagem no console em negrito. * Lembre-se que este método não adiciona por padrão a quebra de linha, * logo, caso queira esse efeito, adicione o caracter especial \n em sua * mensagem. * @param msg Mensagem a ser adicionada no console * @param negrito se o texto deve ser impresso em negrito */ public void appendMessage(String msg, Boolean negrito){ //Alteramos o estilo StyleConstants.setBold(padrao, negrito); //Adicionamos a mensagem no console - com o estilo modificado insertMessage(msg); //Voltamos o estilo ao setup anterior, já que esta chamada não //deve modificar permanentemente o estilo. As demais chamadas seguiram //o estilo definido no console (por padrão ou através de métodos //específicos). StyleConstants.setBold(padrao, textBold); } //Métodos internos /** * Método interno para adicionar conteúdo no console. * Este método é thread safety. Por alguma razão se não for implementado * deste modo (thread safety) o scroll automático não ocorre quando a * inserção do texto é feito dentro de uma thread (veja o exemplo de uso * deste console). Vou investigar melhor isso e coloco em um post futuro. * @param msg Mensagem a ser adicionada no console. */ private void insertMessage(final String msg){ Runnable threadSafetyOp = new Runnable(){ public void run() { try { //Adicionamos a nova mensagem no conteúdo do console //Repare que a msg é adicionada no termino do conteúdo atual //e o estilo utilizado é o padrao definido pro console conteudo.insertString(conteudo.getLength(), msg, padrao); //Chama o método para atualizar o controlador de linhas do console //Remove (se necessário) as linhas excedentes updateConsole(msg); } catch (BadLocationException ex) { ex.printStackTrace(); } } }; if(SwingUtilities.isEventDispatchThread()){ threadSafetyOp.run(); } else{ SwingUtilities.invokeLater(threadSafetyOp); } } /** * Ultima mensagem inserida no buffer * @param msg */ private void updateConsole(String msg){ //Varre toda a mensagem for(int c=0;c= bufRows.length){ //Significa que precisamos remover conteúdo do console try { //Remove linha apontada conteudo.remove(0, bufRows[p2Remove]); //Zera a posição no buffer bufRows[p2Remove]=0; //move ponteiro para a proxima linha if(p2Remove >= bufRows.length-1){ //Se for a ponta do buffer, retorne para o inicio p2Remove=0; } else{ //Caso contrário, incremente-o p2Remove++; } } catch (BadLocationException ex) { ex.printStackTrace(); } } else{ actNumRows++; } //Atualize o buffer bufRows[p2Fill] = bufRows[p2Fill] + 1; if(p2Fill >= bufRows.length-1){ //Se for a ponta do buffer, retorne para o inicio p2Fill=0; } else{ //Caso contrário, incremente-o p2Fill++; } } else{ //atualizo a quantidade de caracteres da linha atual bufRows[p2Fill] = bufRows[p2Fill] + 1; } } } /** * Inicializa conteúdo buffer e coloca os ponteiros em posição de reset. */ private void resetBuffer(){ actNumRows = 0; p2Fill = 0; p2Remove = 0; //Reinicializa valor do buffer for(int i=0; i < bufRows.length; i++){ bufRows[i]=0; } } //Classes internas //Com a implementação da classe abstrata Writer poderemos direcionar o //fluxo de dados para dentro do nosso console private class ConsoleWriter extends Writer{ @Override public void write(char[] cbuf, int off, int len) throws IOException { //Adiciona a nova mensagem no console insertMessage(new String(cbuf,off,len)); } @Override public void flush() throws IOException { //Não faremos nada } @Override public void close() throws IOException { //Não faremos nada } } }
Agora o código exemplo de uso do componente JavaMilkConsole.
package javamilk; import java.awt.BorderLayout; import java.awt.Color; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JColorChooser; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTextField; import javax.swing.border.EmptyBorder; /** * Classe simples pra demonstrar o uso do componente JavaMilkConsole. * @author giuliano */ public class JavaMilkDemo { public JavaMilkDemo(){ //Criamos os alguns objetos de controle JButton printMsg = new JButton("Imprimir Mensagem"); JButton clearConsole = new JButton("Limpar Console"); JButton threadMsg = new JButton("Thread de Mensagens"); final JTextField textMsg = new JTextField(); //Painel que irá abrigar os controles - botões e caixa de texto JPanel painelControle = new JPanel(); //Borda do painel de controle painelControle.setBorder(new EmptyBorder(5,0,5,0)); //Layout do painel de controle - horizontal painelControle.setLayout(new BoxLayout(painelControle,BoxLayout.X_AXIS)); //Adicionamos os componentes de controle que criamos - a ordem //de inserção é importante painelControle.add(textMsg); //Criar um componente invisivel pra espaçar um componente de outro painelControle.add(Box.createHorizontalStrut(5)); //repetimos os 2 passos anteriores até adicionar todos os componentes painelControle.add(printMsg); painelControle.add(Box.createHorizontalStrut(5)); painelControle.add(clearConsole); painelControle.add(Box.createHorizontalStrut(5)); painelControle.add(threadMsg); //Segund painel de componentes - para customizar a mensagem final JButton fgColorText = new JButton("Cor do texto"); fgColorText.setBackground(Color.BLUE); final JButton bgColorText = new JButton("Cor de fundo do texto"); bgColorText.setBackground(Color.WHITE); final JCheckBox boldText = new JCheckBox("Negrito"); //Painel para abrigar os controles acima final JPanel painelSetup = new JPanel(); painelSetup.setBorder(new EmptyBorder(5,0,5,0)); painelSetup.setLayout(new BoxLayout(painelSetup,BoxLayout.X_AXIS)); //Adicionando os componentes painelSetup.add(boldText); painelSetup.add(Box.createHorizontalStrut(5)); painelSetup.add(fgColorText); painelSetup.add(Box.createHorizontalStrut(5)); painelSetup.add(bgColorText); // JPanel painelControleSetup = new JPanel(); painelControleSetup.setBorder(new EmptyBorder(5,0,5,0)); painelControleSetup.setLayout(new BorderLayout()); painelControleSetup.add(painelControle,BorderLayout.NORTH); painelControleSetup.add(painelSetup,BorderLayout.SOUTH); //Agora vamos criar o um objeto console - repare que passei o argumento //50 no construtor, o que significa que teremos no máximo 50 linhas no //console. final JavaMilkConsole console = new JavaMilkConsole(50); //Se for o caso de fazermos algum setup no console, este pode //ser feito aqui (cor do texto, fundo, tamanho do buffer, etc) //console.setBackground(Color.GREEN); //console.setFontBackground(Color.GREEN); //Vamos criar um painel pra abrigar o console + painel de controles JPanel painelPrincipal = new JPanel(); //definir a borda do painel painelPrincipal.setBorder(new EmptyBorder(5,5,5,5)); //definir um gerenciador de layout painelPrincipal.setLayout(new BorderLayout()); //Finalmente adicionar os componentes: console + painel de controles painelPrincipal.add(new JScrollPane(console), BorderLayout.CENTER); //painelPrincipal.add(painelControle, BorderLayout.SOUTH); painelPrincipal.add(painelControleSetup,BorderLayout.SOUTH); //Até aqui temos nossos paineis criados - falta adiciona-los em uma //janela (JFrame) e criar a lógica de controle //Criando a janela JFrame janela = new JFrame(); janela.setTitle("JavaMilk Demo - como usar o JavaMilkConsole"); janela.setContentPane(painelPrincipal); janela.getRootPane().setDefaultButton(printMsg); janela.setSize(740, 480); janela.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); janela.setVisible(true); //Criando a lógica de controle //Quando clicarmos no botão printMsg printMsg.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent e) { //Imprima mensagem de texto, usando o setup de cor texto, //cor de fundo, bold e font console.appendMessage(textMsg.getText() + "\n", fgColorText.getBackground(),bgColorText.getBackground(), boldText.isSelected()); //console.appendMessage(textMsg.getText() + "\n",true); } }); //Quando clicarmos no botão clearConsole clearConsole.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { //Apaga todo conteúdo do console console.clearContents(); } }); //Quando clicarmos no botão threadMsg threadMsg.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent e) { //Criar uma thread pra ficar imprimindo no console //usando o modo stream new Thread(){ @Override public void run(){ for(int i=0; i<100;i++){ try { console.out.println("Mensagem " + i + " vinda da thread " + this.getName()); //Coloque a thread em sleep por um tempo Thread.sleep(500); } catch (InterruptedException ex) { ex.printStackTrace(); } } } }.start(); } }); //Quando clicamos no botão fgColorText fgColorText.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent e) { Color fgColor = JColorChooser.showDialog(painelSetup, "", fgColorText.getBackground()); if(fgColor != null){ fgColorText.setBackground(fgColor); } } }); //Quando clicarmos no botão bgColorText bgColorText.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent e) { Color bgColor = JColorChooser.showDialog(painelSetup, "", bgColorText.getBackground()); if(bgColor != null){ bgColorText.setBackground(bgColor); } } }); } public static void main(String[] args){ java.awt.EventQueue.invokeLater(new Runnable() { public void run() { new JavaMilkDemo(); } }); } }
Confesso que com a inserção do código o post ficou gigantesco. Embora pareça assustador mais da metado do contéudo são comentário que deixei no código afim de facilitar quem se aventurar a entende-lo. A construção deste componente trouxe a necessidade de aprender componentes novos e complexos e rever diversos conceitos da linguagem. Avalio como positivo, afinal o que vale é a jornada. Até a próxima.
Referências
1 - Livro Core Swing: Advanced Programming
2 - Tutorial sobre componentes de texto - Text Swing
3 - Exemplo de implementação de um console simples usando o JTextArea
4 - Tutorial sobre como utilizar o componente JColorChooser
5 - Ótimo exemplo de implementação de um console usando JTextPane
Esse trecho do exemplo que você deixou está incompleto, você tem o restante dele ainda?
ResponderExcluirprivate void updateConsole(String msg){
//Varre toda a mensagem
for(int c=0;c= bufRows.length){
//Significa que precisamos remover conteúdo do console
try
{
//Remove linha apontada
conteudo.remove(0, bufRows[p2Remove]);
//Zera a posição no buffer
bufRows[p2Remove]=0;
//move ponteiro para a proxima linha
if(p2Remove >= bufRows.length-1){
//Se for a ponta do buffer, retorne para o inicio
p2Remove=0;
}
else{
//Caso contrário, incremente-o
p2Remove++;
}
}
catch (BadLocationException ex)
{
ex.printStackTrace();
}
}
else{
actNumRows++;
}
//Atualize o buffer
bufRows[p2Fill] = bufRows[p2Fill] + 1;
if(p2Fill >= bufRows.length-1){
//Se for a ponta do buffer, retorne para o inicio
p2Fill=0;
}
else{
//Caso contrário, incremente-o
p2Fill++;
}
}
else{
//atualizo a quantidade de caracteres da linha atual
bufRows[p2Fill] = bufRows[p2Fill] + 1;
}
}
}
Oi Alex, tudo bem?
ResponderExcluirO código parece completo ... me mande seu email e eu lhe envio o exemplo que tenho comigo.
Um abraço.
Giuliano
Olá, é Exatamente o Problema que estou tendo, atualmente contrui uma especie de interpretador Portugol-Java(para Universidade a qual estudo) para controle de dispositivos eletronicos via porta Serial, tentei como vc a utilização do JTextArea, mas agora estou tentando via JEditorPane, contudo estou tendo problemas na alteração das cores das palavras reservadas, tentei seguir seu código mas como o Alex citou o updateConsole parece ter alguma inconsistencia, teria como enviar ao meu e-mail seu exemplo? Obrigado! magno_ritzmann@yahoo.com.br
ResponderExcluirEste comentário foi removido pelo autor.
ResponderExcluir