使用 Swing 正确实现这一点的唯一方法(我不了解 JavaFX:可能适用相同的原则)是理解和使用setBounds
渲染器组件。
我是通过反复试验得出这个结论,而不是检查源代码。但是很明显,这种方法负责布局文本(以任何字体)并计算然后实现自动换行)。
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
}
}
以上在这个 SSCCE 中运行良好......但在现实世界中,使用更复杂的字体、更多文本和更大的表格,您开始遇到问题。因此,我在下面提出了一个新版本的 Listener 类以及一个新版本的渲染器(只是为了介绍复杂字体的使用......)。如果有兴趣,将这些代入上述 SSCCE...
/*
* 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;
}
}