31 votes

Comment envelopper les lignes dans une cellule de jtable ?

J'essaie d'implémenter un TableRenderer personnalisé tel que décrit dans le document ce tutoriel . J'aimerais que le moteur de rendu mette en forme chaque texte qui est trop long pour la cellule donnée. L'idée est d'utiliser un TextArea comme moteur de rendu, car il supporte le retour à la ligne. Cependant, le code suivant ne se comporte pas comme prévu :

public class LineWrapCellRenderer  extends JTextArea implements TableCellRenderer {

    @Override
    public Component getTableCellRendererComponent(
            JTable table,
            Object value,
            boolean isSelected,
            boolean hasFocus,
            int row,
            int column) {
        this.setText((String)value);
        this.setWrapStyleWord(true);            
        this.setLineWrap(true);         
        return this;
    }

}

J'ai défini ce moteur de rendu avec

table.setDefaultRenderer(String.class, new LineWrapCellRenderer());

Mais les entrées de la cellule restent non enveloppées. Si j'ajoute this.setBackground(Color.YELLOW) au getTableCellRendererComponent() méthode, toutes les cellules sont jaunes comme prévu, mais pas enveloppées.

Des idées ?

UPDATE : Comme Michael Borgwardt l'a indiqué dans les commentaires, le problème n'est pas le retour à la ligne, mais la hauteur des lignes : Les lignes de JTables sont de taille fixe, donc si une cellule devient plus haute (parce que le texte est maintenant multi-lignes), nous devons augmenter la hauteur de la ligne. Mais de combien ? Je vais vérifier si cela vaut la peine de poser une autre question à l'OS. Si non, j'ajouterai cette solution ici.

Mise à jour2 : Le code suivant déterminera la hauteur de la rangée (si elle est placée dans le fichier getTableCellRendererComponent() ) :

int fontHeight = this.getFontMetrics(this.getFont()).getHeight();
int textLength = this.getText().length();
int lines = textLength / this.getColumns() +1;//+1, cause we need at least 1 row.           
int height = fontHeight * lines;            
table.setRowHeight(row, height);

3 votes

Ne modifie pas l'état de la table dans le moteur de rendu - comme dans Jamais de la vie

17 votes

Chère Cléopâtre ! !! S'il te plaît, donne-nous une solution fonctionnelle au lieu de nous dire que tu peux faire mieux.

0 votes

11voto

Michael Borgwardt Points 181658

Le problème est que la hauteur des rangées dans JTable est fixe, donc il ne s'agit pas simplement d'avoir un moteur de rendu qui enveloppe le texte ; je ne sais pas pourquoi il ne le fait pas, mais s'il le faisait, le texte enveloppé serait coupé - ou peut-être est-ce exactement ce que vous voyez. Pour ajuster la hauteur des lignes, vous devez les définir individuellement.

Voici un code pour cela :

int rows = 10;
int cols = 5;
JTable table = new JTable(rows, cols);

// Set the 1st row to 60 pixels high
table.setRowHeight(0, 60);

// Set the height of all rows to 32 pixels high,
// regardless if any heights were assigned to particular rows
table.setRowHeight(32);
// the height of the 1st row is set to 32 pixels high

// Returns the preferred height of a row.
// The result is equal to the tallest cell in the row.
public int getPreferredRowHeight(JTable table, int rowIndex, int margin) {
    // Get the current default height for all rows
    int height = table.getRowHeight();

    // Determine highest cell in the row
    for (int c=0; c<table.getColumnCount(); c++) {
        TableCellRenderer renderer = table.getCellRenderer(rowIndex, c);
        Component comp = table.prepareRenderer(renderer, rowIndex, c);
        int h = comp.getPreferredSize().height + 2*margin;
        height = Math.max(height, h);
    }
    return height;
}

// The height of each row is set to the preferred height of the
// tallest cell in that row.
public void packRows(JTable table, int margin) {
    packRows(table, 0, table.getRowCount(), margin);
}

// For each row >= start and < end, the height of a
// row is set to the preferred height of the tallest cell
// in that row.
public void packRows(JTable table, int start, int end, int margin) {
    for (int r=0; r<table.getRowCount(); r++) {
        // Get the preferred height
        int h = getPreferredRowHeight(table, r, margin);

        // Now set the row height using the preferred height
        if (table.getRowHeight(r) != h) {
            table.setRowHeight(r, h);
        }
    }
}

0 votes

Cela semble être un problème. Après avoir fixé la hauteur de ligne à une valeur supérieure, le retour à la ligne apparaît. Problème maintenant : Comment obtenir la nouvelle hauteur parfaite pour.

0 votes

Regardez l'article dont j'ai donné le lien, il contient un exemple de code que je pourrais facilement adapter pour obtenir une solution parfaitement fonctionnelle.

9 votes

Le lien que vous suggérez ne fonctionne plus... peut-être un argument en faveur de l'affichage du code ici ?

6voto

Alessandro Rossi Points 1204

Bonjour, j'ai eu votre même problème mais la solution que j'ai mise en place s'inspire de l'exemple disponible dans le Tutoriel Java pour dessiner du texte multiligne et dessine le texte sur la cellule en utilisant les API de texte.

http://java.sun.com/docs/books/tutorial/2d/text/drawmulstring.html

import java.awt.Component;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.font.FontRenderContext;
import java.awt.font.LineBreakMeasurer;
import java.awt.font.TextLayout;
import java.text.AttributedCharacterIterator;
import java.text.AttributedString;
import java.text.BreakIterator;

import javax.swing.JTable;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellRenderer;

public class MultilineTableCell 
    implements TableCellRenderer {
    class CellArea extends DefaultTableCellRenderer {
        /**
         * 
         */
        private static final long serialVersionUID = 1L;
        private String text;
        protected int rowIndex;
        protected int columnIndex;
        protected JTable table;
        protected Font font;
        private int paragraphStart,paragraphEnd;
        private LineBreakMeasurer lineMeasurer;

        public CellArea(String s, JTable tab, int row, int column,boolean isSelected) {
            text = s;
            rowIndex = row;
            columnIndex = column;
            table = tab;
            font = table.getFont();
            if (isSelected) {
                setForeground(table.getSelectionForeground());
                setBackground(table.getSelectionBackground());
            }
        }
        public void paintComponent(Graphics gr) {
            super.paintComponent(gr);
            if ( text != null && !text.isEmpty() ) {
                Graphics2D g = (Graphics2D) gr;
                if (lineMeasurer == null) {
                    AttributedCharacterIterator paragraph = new AttributedString(text).getIterator();
                    paragraphStart = paragraph.getBeginIndex();
                    paragraphEnd = paragraph.getEndIndex();
                    FontRenderContext frc = g.getFontRenderContext();
                    lineMeasurer = new LineBreakMeasurer(paragraph,BreakIterator.getWordInstance(), frc);
                }
                float breakWidth = (float)table.getColumnModel().getColumn(columnIndex).getWidth();
                float drawPosY = 0;
                // Set position to the index of the first character in the paragraph.
                lineMeasurer.setPosition(paragraphStart);
                // Get lines until the entire paragraph has been displayed.
                while (lineMeasurer.getPosition() < paragraphEnd) {
                    // Retrieve next layout. A cleverer program would also cache
                    // these layouts until the component is re-sized.
                    TextLayout layout = lineMeasurer.nextLayout(breakWidth);
                    // Compute pen x position. If the paragraph is right-to-left we
                    // will align the TextLayouts to the right edge of the panel.
                    // Note: this won't occur for the English text in this sample.
                    // Note: drawPosX is always where the LEFT of the text is placed.
                    float drawPosX = layout.isLeftToRight()
                        ? 0 : breakWidth - layout.getAdvance();
                    // Move y-coordinate by the ascent of the layout.
                    drawPosY += layout.getAscent();
                    // Draw the TextLayout at (drawPosX, drawPosY).
                    layout.draw(g, drawPosX, drawPosY);
                    // Move y-coordinate in preparation for next layout.
                    drawPosY += layout.getDescent() + layout.getLeading();
                }
                table.setRowHeight(rowIndex,(int) drawPosY);
            }
        }
    }
    public Component getTableCellRendererComponent(
            JTable table, Object value,boolean isSelected, boolean hasFocus, int row,int column
        )
    {
        CellArea area = new CellArea(value.toString(),table,row,column,isSelected);
        return area;
    }   
}

Il redimensionne également la hauteur des lignes mais il ne le fait bien que lorsque ce moteur de rendu est utilisé pour une seule colonne.

Et voici la façon dont je l'ai invoqué pour rendre ma table.

final int wordWrapColumnIndex = ...;
myTable = new JTable() {    
    public TableCellRenderer getCellRenderer(int row, int column) {
        if (column == wordWrapColumnIndex ) {
            return wordWrapRenderer;
        }
        else {
            return super.getCellRenderer(row, column);
        }
    }
};

0 votes

J'aimerais utiliser ce code dans mon programme. Si je le peux, sous quelle licence dois-je inclure ce code ?

0 votes

La majeure partie de ce code provient d'un tutoriel du domaine public qui ne devrait pas empêcher toute politique de licence, je suppose.

0 votes

-1 pour changer l'état de la table dans ... la méthode paint ?? C'est encore pire que de le faire dans la méthode getXXRendererComp qui est déjà une interdiction absolue ... et d'autres interdictions : créer un nouveau composant à chaque appel, ne pas implémenter les indications de taille ... devraient avoir plus qu'un seul vote de temps en temps ;-)

2voto

willcodejavaforfood Points 20365

Vous pourriez utiliser un JLabel comme moteur de rendu et insérer le texte dans une balise HTML et ajouter simplement <br> le cas échéant

Comment utiliser le HTML dans les composants Swing

0voto

Greg Points 1

Rédigez les en-têtes en HTML. Voici un exemple que j'ai. Le seul problème que j'ai rencontré est que j'ai du mal à les faire défiler dans la fenêtre de l'en-tête. JPanel si j'ajuste la hauteur des en-têtes.

    myTable.getColumnModel().getColumn(1).setPreferredWidth(75);
    myTable.getColumnModel().getColumn(1).setHeaderValue("<html><b>Day Of<br>Week</b></html>");

0voto

mike rodent Points 482

La seule façon d'implémenter ceci correctement en utilisant Swing (je ne sais pas pour JavaFX : les mêmes principes peuvent éventuellement s'appliquer) est de comprendre et d'utiliser setBounds sur le composant du moteur de rendu.

Je suis arrivé à cette conclusion par tâtonnement, plutôt qu'en examinant le code source. Mais il est clair que cette méthode est responsable de la mise en page du texte (quelle que soit la police) et du calcul puis de l'implémentation du word-wrapping).

import java.awt.*;
import java.io.*;
import java.util.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;

public class MultiWrapColDemo {
  public static void main(String[] args) throws FileNotFoundException {
    EventQueue.invokeLater(new ShowIt());
  }
}

class ShowIt implements Runnable {
  @Override
  public void run() {
    JTable table = new JTable();
    table.getColumnModel().addColumnModelListener( new WrapColListener( table ) );
    table.setDefaultRenderer( Object.class, new JTPRenderer() );

    // examples:
//    table.setIntercellSpacing( new Dimension( 40, 20 ));
//    table.setIntercellSpacing( new Dimension( 4, 2 ));

    Vector<Vector<String>> dataVector = new Vector<Vector<String>>();
    String lorem1 = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore";
    String lorem2 = "et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum";

    for (int i = 0; i < 12; i++) {
      Vector<String> row = null;
      if (i % 4 == 0) {
        row = new Vector<String>(Arrays.asList(new String[] { "iggle", lorem1, "poggle", "poke" }));
      } else if (i % 4 == 1) {
        row = new Vector<String>(Arrays.asList(new String[] { lorem2, "piggle", "poggle", lorem1 }));
      } else if (i % 4 == 2) {
        row = new Vector<String>(Arrays.asList(new String[] { lorem1, "piggle", lorem2, "poke" }));
      } else
        row = new Vector<String>(Arrays.asList(new String[] { "iggle", lorem2, "poggle", lorem2 }));
      dataVector.add(row);
    }
    Vector<String> columnIdentifiers = new Vector<String>(Arrays.asList(new String[] { "iggle", "piggle", "poggle",
        "poke" }));
    table.getTableHeader().setFont(table.getTableHeader().getFont().deriveFont(20f).deriveFont(Font.BOLD));
    ((DefaultTableModel) table.getModel()).setDataVector(dataVector, columnIdentifiers);
    JFrame frame = new JFrame("MultiWrapColTable");
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    JScrollPane jsp = new JScrollPane(table);
    frame.getContentPane().add(jsp);
    frame.pack();
    frame.setBounds(50, 50, 800, 500);
    frame.setVisible(true);
  }
}

// if the renderer on a column (or the whole table) is not a JTextComponent calculating its preferredSize will not do 
// any wrapping ... but it won't do any harm....
class JTPRenderer extends JTextPane implements TableCellRenderer {
  @Override
  public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus,
      int row, int column) {
    setText(value.toString());
    return this;
  }
}

class WrapColListener implements TableColumnModelListener {

  JTable m_table;

  WrapColListener( JTable table ){
    m_table = table;
  }

  void refresh_row_heights() {
    int n_rows = m_table.getRowCount();
    int n_cols = m_table.getColumnCount();
    int intercell_width = m_table.getIntercellSpacing().width;
    int intercell_height = m_table.getIntercellSpacing().height;
    TableColumnModel col_model = m_table.getColumnModel();
    // these null checks are due to concurrency considerations... much can change between the col margin change
    // event and the call to refresh_row_heights (although not in this SSCCE...)
    if( col_model == null ) return;
    // go through ALL rows, calculating row heights
    for (int row = 0; row < n_rows; row++) {
      int pref_row_height = 1;
      // calculate row heights from cell, setting width constraint by means of setBounds...
      for (int col = 0; col < n_cols; col++) {
        Object value = m_table.getValueAt(row, col);
        TableCellRenderer renderer = m_table.getCellRenderer(row, col);
        if( renderer == null ) return;
        Component comp = renderer.getTableCellRendererComponent( m_table, value, false, false,
            row, col);
        if( comp == null ) return;
        int col_width = col_model.getColumn(col).getWidth();
        // constrain width of component
        comp.setBounds(new Rectangle(0, 0, col_width - intercell_width, Integer.MAX_VALUE ));
        // getPreferredSize then returns "true" height as a function of attributes (e.g. font) and word-wrapping
        int pref_cell_height = comp.getPreferredSize().height  + intercell_height;
        if (pref_cell_height > pref_row_height) {
          pref_row_height = pref_cell_height;
        }
      }
      if (pref_row_height != m_table.getRowHeight(row)) {
        m_table.setRowHeight(row, pref_row_height);
      }
    }
  }

  @Override
  public void columnAdded(TableColumnModelEvent e) {
    refresh_row_heights();

  }

  @Override
  public void columnRemoved(TableColumnModelEvent e) {
    // probably no need to call refresh_row_heights

  }

  @Override
  public void columnMoved(TableColumnModelEvent e) {
    // probably no need to call refresh_row_heights

  }

  @Override
  public void columnMarginChanged(ChangeEvent e) {
    refresh_row_heights();
  }

  @Override
  public void columnSelectionChanged(ListSelectionEvent e) {
    // probably no need to call refresh_row_heights

  }

}

Ce qui précède fonctionne bien dans ce SSCCE... mais dans le monde réel, avec des polices plus complexes, plus de texte et des tableaux plus grands, vous commencez à rencontrer des problèmes. Je propose donc ci-dessous une nouvelle version de la classe Listener ainsi qu'une nouvelle version du moteur de rendu (juste pour introduire l'utilisation d'une police complexe...). Substituez-les dans le SSCCE ci-dessus si vous êtes intéressés...

/*
 * This class reflects the fact that 1) when you drag a column boundary using the mouse a very large number of
 * ChangeEvents are generated and 2) with more complex fonts, more text and larger tables ("real world") the amount
 * of computation in calculating the row heights becomes significant and leads to an unresponsive GUI, or worse.
 * This "first" strategy to address this involves setting a pause between the detection of a change event and the
 * refreshing of the rows.  Naturally this involves a Timer, the run() method of which is not the EDT, so it
 * must then submit to EventQueue.invokeLater...
 * The larger the table, the more text involved, and the more complex the fonts... the more ingenuity will have to 
 * be used in coping with the potentially vast amount of computation involved in getting the ideal row heights.  This
 * is in the nature of the beast.  Ideas might involve: 
 * 1) adjusting the row heights immediately only for rows which are visible or likely to be visible (Viewport), and 
 * then making successive calls to EventQueue.invokeLater to deal with all the other rows
 * 2) giving cells a "memory" of their heights as a function of the allowed width.  Unfortunately it will not allow
 * the possibility of interpolating intermediate values because the question of whether a line wraps may hinge on a 
 * single pixel difference, although an imperfect solution to this would be err on the side of caution, i.e. pretend
 * that a column is a little thinner than it is to cause wrapping before it is strictly necessary... particularly when 
 * cells are out of view...
 * ... other ideas...(?)  
 */
class FirstRealWorldWrapColListener implements TableColumnModelListener {

  JTable m_table;
  final static long PAUSE_TIME = 50L;
  java.util.Timer m_pause_timer = new java.util.Timer( "pause timer", true );
  TimerTask m_pause_task;

  class PauseTask extends TimerTask{
    @Override
    public void run() {
      EventQueue.invokeLater( new Runnable(){
        @Override
        public void run() {
          refresh_row_heights();
          System.out.println( "=== setting m_pause_task to null..." );
          m_pause_task = null;
        }});
    }
  }

  FirstRealWorldWrapColListener( JTable table ){
    m_table = table;
  }

  void queue_refresh(){
    if( m_pause_task != null ){
      return;
    }
    System.out.println( "=== scheduling..." );
    m_pause_task = new PauseTask();
    m_pause_timer.schedule( m_pause_task, PAUSE_TIME ); 

  }

  void refresh_row_heights() {

    int n_rows = m_table.getRowCount();
    int n_cols = m_table.getColumnCount();
    int intercell_width = m_table.getIntercellSpacing().width;
    int intercell_height = m_table.getIntercellSpacing().height;
    TableColumnModel col_model = m_table.getColumnModel();
    // these null checks are due to concurrency considerations... much can change between the col margin change
    // event and the call to refresh_row_heights (although not in this SSCCE...)
    if( col_model == null ) return;
    // go through ALL rows, calculating row heights
    for (int row = 0; row < n_rows; row++) {
      int pref_row_height = 1;
      // calculate row heights from cell, setting width constraint by means of setBounds...
      for (int col = 0; col < n_cols; col++) {
        Object value = m_table.getValueAt(row, col);
        TableCellRenderer renderer = m_table.getCellRenderer(row, col);
        if( renderer == null ) return;
        Component comp = renderer.getTableCellRendererComponent( m_table, value, false, false,
            row, col);
        if( comp == null ) return;
        int col_width = col_model.getColumn(col).getWidth();
        // constrain width of component
        comp.setBounds(new Rectangle(0, 0, col_width - intercell_width, Integer.MAX_VALUE ));
        // getPreferredSize then returns "true" height as a function of attributes (e.g. font) and word-wrapping
        int pref_cell_height = comp.getPreferredSize().height  + intercell_height;
        if (pref_cell_height > pref_row_height) {
          pref_row_height = pref_cell_height;
        }
      }
      if (pref_row_height != m_table.getRowHeight(row)) {
        m_table.setRowHeight(row, pref_row_height);
      }
    }
  }

  @Override
  public void columnAdded(TableColumnModelEvent e) {
//    refresh_row_heights();
    queue_refresh();

  }

  @Override
  public void columnRemoved(TableColumnModelEvent e) {
    // probably no need to call refresh_row_heights

  }

  @Override
  public void columnMoved(TableColumnModelEvent e) {
    // probably no need to call refresh_row_heights

  }

  @Override
  public void columnMarginChanged(ChangeEvent e) {
//    refresh_row_heights();
    queue_refresh();
  }

  @Override
  public void columnSelectionChanged(ListSelectionEvent e) {
    // probably no need to call refresh_row_heights

  }

}

// if the renderer on a column (or the whole table) is not a JTextComponent calculating its preferredSize will not do 
// any wrapping ... but it won't do any harm....
class JTPRenderer extends JTextPane implements TableCellRenderer {
  Font m_default_font, m_big_font, m_default_alternate_font, m_big_alternate_font;
  HashMap<AttributedCharacterIterator.Attribute, Object>  m_red_serif_attr_map;
  //
  JTPRenderer() {
    m_default_font = getFont();
    m_big_font = m_default_font.deriveFont(m_default_font.getSize() * 1.5f);
    m_red_serif_attr_map = new HashMap<AttributedCharacterIterator.Attribute, Object >();
    m_red_serif_attr_map.put( TextAttribute.FAMILY, Font.SERIF );
    m_red_serif_attr_map.put( TextAttribute.FOREGROUND, Color.RED );
    m_red_serif_attr_map.put( TextAttribute.WIDTH, TextAttribute.WIDTH_EXTENDED );
    m_default_alternate_font = m_default_font.deriveFont( m_red_serif_attr_map );
    m_big_alternate_font = m_big_font.deriveFont( m_red_serif_attr_map );
    // simpler alternate font:
//    m_default_alternate_font = m_default_font.deriveFont( Font.BOLD | Font.ITALIC );
//    m_big_alternate_font = m_big_font.deriveFont( Font.BOLD | Font.ITALIC );
  }

  @Override
  public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus,
      int row, int column) {
    int rc = row + column;
    if( rc % 4 == 2 )
      setFont( rc % 5 == 1 ?  m_big_alternate_font : m_default_alternate_font );
    else 
      setFont( rc % 5 == 1 ?  m_big_font : m_default_font );
    setText(value.toString());
    return this;
  }

}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X