2009-06-08 47 views
31

Estoy tratando de implementar un TableRenderer personalizado como se describe en this tutorial. Me gustaría que el renderizador alinee cada texto que es largo para la celda dada. La idea es utilizar TextArea como renderizador, ya que admite el ajuste de línea. Sin embargo, el siguiente código no se comporta como se esperaba:¿Cómo envolver las líneas en una celda jtable?

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; 
    } 

} 

fijo esto render con

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

Pero las entradas de celda quede sin envolver. Si agrego this.setBackground(Color.YELLOW) al método getTableCellRendererComponent(), todas las celdas son amarillas como se esperaba, pero no están envueltas.

¿Alguna idea?

ACTUALIZACIÓN: Como Michael Borgwardt indicó en los comentarios, el problema no es el ajuste de línea, pero la altura de la fila: JTables filas se fijan tamaño, por lo que si una célula es cada vez mayor (que el texto es ahora multi alineado), tenemos que aumentar la altura de la fila. ¿Pero cuánto? Comprobaré si esto vale otra pregunta SO. Si no, agregaré esta solución aquí.

Update2: El siguiente código determinará la altura de la fila (si se coloca en 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

no cambie el estado de la tabla en el renderizador - como en ** never-ever ** – kleopatra

+15

Querida Cleopatra !!! Por favor, danos una solución de trabajo en lugar de solo decir que puedes hacer esto mejor. –

+0

Consulte este http://stackoverflow.com/questions/33937074/jtable-cell-wrapping/38932843#38932843 – Yougesh

Respuesta

11

el problema es que la altura de las filas en JTable se fija, por lo que no es sólo una cuestión de tener un procesador que se envuelven s; No estoy seguro de por qué no lo hace, pero si lo hiciera, el texto envuelto se recortaría, o tal vez eso es exactamente lo que está viendo. Para ajustar las alturas de fila, debe configurarlas individualmente.

+0

Eso parece ser un problema. Después de establecer la altura de fila a un valor mayor, aparece el ajuste de línea. Problema ahora: cómo obtener la nueva altura perfecta para. – Arvodan

+0

Mira el artículo al que me he vinculado, tiene un código de ejemplo que podría adaptar fácilmente a una solución que funciona perfectamente. –

+9

Enlace que sugiere que no funcione más ... ¿quizás un argumento a favor de mostrar código aquí? –

6

Hola, tuve el mismo problema pero la solución que implementé está inspirada en la muestra disponible del Tutorial de Java para dibujar texto de líneas múltiples y dibuja el texto en la celda usando las API de texto.

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; 
    } 
} 

Se cambia el tamaño fila heigth también, pero lo hace bien sólo cuando se utiliza este procesador para una sola columna.

Y esta es la forma en que solía invocarlo para renderizar mi tabla.

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

Me gustaría utilizar este código en mi programa. Si puedo, ¿bajo qué licencia debería incluir este código? –

+0

La mayor parte de este código proviene de un tutorial de dominio público que no debería impedir ninguna política de licencia, supongo. –

+0

-1 para cambiar el estado de la tabla en ... ¿el método de pintura? Eso es incluso peor que hacerlo en el getXXRendererComp, que ya es un absoluto no-go ... y otros no-goes: crear un nuevo componente en cada llamada, sin implementar sugerencias de tamaño ... debería tener más de un voto único de vez en cuando;) – kleopatra

0

Escriba los encabezados en HTML. Aquí hay un ejemplo de uno que tengo. El único problema que estoy experimentando es que estoy teniendo dificultades para desplazarme en el JPanel si ajusto la altura de los encabezados.

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

la pregunta no es sobre los encabezados ... – kleopatra

0

uso setBounds en el componente de render (véase más adelante)

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 

    } 

} 

Lo anterior funciona bien en este SSCCE ...pero en el mundo real, con fuentes más complejas, más texto y tablas más grandes, comienzas a tener problemas. Por lo tanto, propongo a continuación una nueva versión de la clase Listener junto con una nueva versión del renderizador (solo para introducir el uso de una fuente compleja ...). Sustituirlos en el SSCCE anterior si está interesado ...

/* 
* 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; 
    } 

} 
0

Como se señaló anteriormente la altura de la fila debe ser calculada, pero la solución actual podría mejorarse. De hecho, no estaba funcionando para mí. jtxt.getColumns() volvía a cero y se dividía por cero. Aquí hay un código que creo que es más limpio:

// set the width on the jTextArea causing a calc of preferred height 
jtxt.setSize(table.getWidth(), Short.MAX_VALUE); 
int prefH = jtxt.getPreferredSize().height; 
table.setRowHeight(row, prefH); 
3

Además de esta pregunta que me gustaría compartir con ustedes una solución para editor de varias líneas de células. Es un poco hacky (almacena referencias a la fila editada), pero hace el trabajo.

import javax.swing.*; 
import javax.swing.table.TableCellEditor; 
import java.awt.*; 
import java.awt.event.ComponentAdapter; 
import java.awt.event.ComponentEvent; 
import java.awt.event.KeyAdapter; 
import java.awt.event.KeyEvent; 

class MultilineTableCellEditor extends AbstractCellEditor implements TableCellEditor { 

    JComponent component = new JTextArea(); 
    JTable table; 
    int lastRowIndex; 

    public MultilineTableCellEditor() { 
     JTextArea textArea = ((JTextArea) component); 
     textArea.setLineWrap(true); 
     textArea.setWrapStyleWord(true); 
     textArea.addComponentListener(new ComponentAdapter() { 
      @Override 
      public void componentResized(ComponentEvent e) { 
       super.componentResized(e); 
       table.setRowHeight(lastRowIndex, (int) (textArea.getPreferredSize().getHeight())); 
      } 
     }); 
     textArea.addKeyListener(new KeyAdapter() { 
      @Override 
      public void keyTyped(KeyEvent e) { 
       super.keyTyped(e); 
       table.setRowHeight(lastRowIndex, (int) (textArea.getPreferredSize().getHeight())); 
      } 
     }); 
    } 

    public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, 
               int rowIndex, int vColIndex) { 
     this.table = table; 
     lastRowIndex = rowIndex; 

     ((JTextArea) component).setText((String) value); 
     component.setFont(table.getFont()); 

     return component; 
    } 

    public Object getCellEditorValue() { 
     return ((JTextArea) component).getText(); 
    } 
} 

usados ​​como tan:

JTable table = new JTable(tableModel) { 
     // Cell renderer by Alessandro Rossi (posted as solution to this question) 
     MultilineTableCell renderer = new MultilineTableCell(); 
     MultilineTableCellEditor editor = new MultilineTableCellEditor(); 

     @Override 
     public TableCellRenderer getCellRenderer(int row, int column) { 
      if (column == multilineColumn) { 
       return renderer; 
      } 
      return super.getCellRenderer(row, column); 
     } 

     @Override 
     public TableCellEditor getCellEditor(int row, int column) { 
      if (column == multilineColumn) { 
       return editor; 
      } 
      return super.getCellEditor(row, column); 
     } 
    }; 
+0

+1 Hace semanas que estoy buscando un contenedor de líneas para un editor, finalmente encontré esto y funcionó perfectamente para mí. ¡Gracias! – ppw

1

me encontré en este mismo problema, y ​​que tenía que modificar un poco el código que está escrito aquí, así que unir mi propia versión:

import java.awt.Component; 
import javax.swing.JTable; 
import javax.swing.JTextArea; 
import javax.swing.table.TableCellRenderer; 

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); 

    int fontHeight = this.getFontMetrics(this.getFont()).getHeight(); 
    int textLength = this.getText().length(); 
    int lines = textLength/this.getColumnWidth(); 
    if (lines == 0) { 
     lines = 1; 
    } 

    int height = fontHeight * lines; 
    table.setRowHeight(row, height); 

    return this; 
} 

} 
Cuestiones relacionadas