Swing——JTable添加行号显示,实现行号、标题(表头)与表格互动功能(类似于Excel)

之前写过一遍关于JTable添加行号显示功能的文章。在后期使用中发现交互体验很不友好,出现很多问题:点击行号没有选中相应的行、点击标题没有选择全列(或多选列)、删除行后行号没有相对应的删除或有残影现象、多选单元格的功能没有了等等。于是~最后的最后,我对添加行号显示这一实现进行了重写。
P.S…由于网上找不到相关问题的资料,最后自己花了很长时间去摸索解决的。今天除了分享之外,同时记录下解决的思路。

问题

  • 点击行号没有选中相应的行
  • 点击标题没有选择全列(或多选列)
  • 多选单元格的功能没有了
  • 删除行后行号没有相对应的删除或有残影现象

思路

总得来说就是行号与表格在互动过程中出现的问题。在最初通过表格的容器 jScrollPane.setRowHeaderView(Component view) 添加行号组件的时候就要让行号和表格建立起很好的紧密互动联系。上面问题的最后就是——怎么建立这个互动联系。

探索与解决

首先创建一个继承JTable的类,亦是最终添加的行号组件。

public class RowHeader extends JTable {

    /** 与行号产生联系的JTable */
    private final JTable refTable;
    
    /** 为JTable添加RowHeader,
     * @param refTable 与行号产生联系的JTable
     * @param width 行号的宽度
     */
    public RowHeaderTable(JTable refTable, int width) {
        super(new DefaultTableModel() {
            
            // 与JTable的行数同步
            private int rowCount = refTable.getRowCount();

            @Override
            public void setRowCount(int rowCount) {
                this.rowCount = rowCount;
            }

            @Override
            public boolean isCellEditable(int row, int column) {
                return false;
            }

            @Override
            public int getRowCount() {
                return rowCount;
            }

            @Override
            public int getColumnCount() {
                return 1;
            }

            @Override
            public Object getValueAt(int row, int column) {
                return row+1;   // 行号的显示值从1开始
            }

        });
        this.refTable = refTable;
        setAutoResizeMode(JTable.AUTO_RESIZE_OFF);//不允许调整列宽
        getColumnModel().getColumn(0).setPreferredWidth(width);
        setDefaultRenderer(Object.class, new RowHeaderRenderer(this.refTable, this));//设置渲染器
        setPreferredScrollableViewportSize(new Dimension(width, 0));
    }
}

上面代码是对行号组件进行一些基础配置,核心在 setDefaultRenderer 中的RowHeaderRenderer(this.refTable, this) ——这个自建类将为行号组件与表格提供互动支持。(先分析,最后给出完整代码)

// RowHeaderRenderer 的继承关系,关键是后面的两个接口
class RowHeaderRenderer extends JLabel implements TableCellRenderer, ListSelectionListener

表格组件存在三处点击事件——单元格、行号、标题,三者应该相互影响。在RowHeaderRenderer类的实现中,针对这三个事件进行分析与实现。

点击单元格事件 —— 对表格组件的维持行选择状态的模型添加相关的事件处理。

ListSelectionModel lsm = reftable.getSelectionModel();
// 点击单元格事件。实现当在reftable中选择行时,RowHeader会发生颜色变化
lsm.addListSelectionListener(new ListSelectionListener() {

    private boolean flag = true;   // 记录有效值的开关
    private int rowIndex;       // 记录有效的行值
    private int colIndex;       // 记录有效的列值

    @Override
    public void valueChanged(ListSelectionEvent e) {
    	// 一次点击事件会多次触发这个事件。随之出现了一种情况————
    	// 点击行号后再点击单元格,会出现“无响应”。
    	// 通过调取各数值后发现只有第一次调用valueChanged时,各属性值才是真实值
        if (flag) {     // 只在第一次运行
            flag = false;
            colIndex = reftable.getSelectedColumnCount();
            rowIndex = lsm.getLeadSelectionIndex();
        }
        // getValueIsAdjusting()返回此事件是否是仍然在更改的多个不同事件之一。
        if (!e.getValueIsAdjusting()) {     // 一次点击触发多次。最后一次getValueIsAdjusting()为false
            flag = true;
            // 判断点击事件是否源于标题
            if (header) {
                header = false;
            } else {	// 点击单元格
                if (colIndex > 1 || reftable.getSelectedRowCount() > 1) {
                    // 多选单元格时,不执行任何操作
                    return;
                }
                if (rowIndex > -1) {	// 选中对应行号
                    tableShow.setRowSelectionInterval(rowIndex, rowIndex);
                }
            }
        }
    }
});

点击行号事件 —— 在RowHeaderRenderer类中覆写getTableCellRendererComponent方法

// 点击行号触发。行号状态每改变一次就调用一次
@Override
public Component getTableCellRendererComponent(JTable table, Object obj,
        boolean isSelected, boolean hasFocus, int row, int col) {
    ((DefaultTableModel) table.getModel()).setRowCount(reftable.getRowCount());
    if (isSelected) {
    	// 不能与 isSelected 并用,容易导致显示异常
        if (table.getSelectedRow() < reftable.getRowCount()) {
        	// 选中对应行号的表格行
            reftable.setRowSelectionInterval(row, row);
            reftable.setColumnSelectionInterval(0, reftable.getColumnCount()-1);
            setForeground(Color.WHITE);
            setBackground(tableSeleBg);
        }
    } else {
        setForeground(Color.BLACK);
        if (row % 2 == 0) {
            setBackground(Color.WHITE);
        } else {
	        // 背景色为标题的背景色
            setBackground(reftable.getTableHeader().getBackground());
        }
    }
    setText(String.valueOf(row + 1));
    return this;
}

点击标题事件 —— 对表格组件的TableHeader添加相关的事件处理。

// 点击标头选中列
reftable.getTableHeader().addMouseListener(new MouseAdapter() {
    @Override
    public void mouseClicked(MouseEvent e) {
        if (col.isEmpty()) {    // 清除选择的列
            header = true;
            tableShow.clearSelection();
            reftable.clearSelection();  // 调用该方法会立即触发SelectionListener,需提前设置header
        }
        int p = reftable.columnAtPoint(e.getPoint());
        header = true;
        // 选择多个列
        if (col.contains(p)) {
            col.removeIf((t) -> {
                return t == p;
            });
            reftable.removeColumnSelectionInterval(p, p);
        } else {
            col.add(p);
            reftable.addColumnSelectionInterval(p, p);
            reftable.addRowSelectionInterval(0, reftable.getRowCount()-1);
        }

    }
});

到此~单元格、行号、标题三者的点击联动就实现了。同时提到的问题也基本得到解决。
但是当对表格进行删除行操作时,问题出现了 —— 删除行后行号没有相对应的删除或有残影现象,如果处于选择状态下的话,删除某行后~行号仍然处于选择状态。

解决关键是为表格添加删除行的事件,通知行号组件更新。

// 表格删除行后更新行号
reftable.getModel().addTableModelListener(new TableModelListener() {
    @Override
    public void tableChanged(TableModelEvent e) {
        if (e.getType() == javax.swing.event.TableModelEvent.DELETE) {
            ((DefaultTableModel)tableShow.getModel()).setRowCount(reftable.getRowCount());
            tableShow.getSelectionModel().clearSelection();
            tableShow.revalidate(); // 更新行号组件。消除残影
        }
    }
});

到这里~开始提到的问题都在这四个方法中得到了解决。而最终得到的行号组件跟表格间有了很友好紧密的联动。

源码

最后 —— 附上完整的代码:

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import javax.swing.JLabel;
import javax.swing.JTable;
import javax.swing.ListSelectionModel;
import javax.swing.SwingConstants;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellRenderer;
import sun.swing.table.DefaultTableCellHeaderRenderer;

/** 用于显示行号的RowHeader组件
 * @author Bson Hoang
 */
public class RowHeader extends JTable {

    private final JTable refTable;
    
    public RowHeader(JTable refTable, int width) {
        super(new DefaultTableModel() {
            
            private int rowCount = refTable.getRowCount();

            @Override
            public void setRowCount(int rowCount) {
                this.rowCount = rowCount;
            }

            @Override
            public boolean isCellEditable(int row, int column) {
                return false;
            }

            @Override
            public int getRowCount() {
                return rowCount;
            }

            @Override
            public int getColumnCount() {
                return 1;
            }

            @Override
            public Object getValueAt(int row, int column) {
                return row+1;
            }

        });
        this.refTable = refTable;
        setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
        getColumnModel().getColumn(0).setPreferredWidth(width);
        setDefaultRenderer(Object.class, new RowHeaderRenderer(this.refTable, this));
        setPreferredScrollableViewportSize(new Dimension(width, 0));
    }
    
}

class RowHeaderRenderer extends JLabel implements TableCellRenderer, ListSelectionListener {

    private boolean header = false;
    private final ArrayList<Integer> col = new ArrayList<>();
    private final JTable reftable;
    private final JTable tableShow;
    private Color tableSeleBg;

    public RowHeaderRenderer(JTable reftable, JTable tableShow) {
        this.reftable = reftable;
        this.tableShow = tableShow;
        setOpaque(true);
        setBorder(javax.swing.UIManager.getBorder("TableHeader.cellBorder"));
        setHorizontalAlignment(CENTER);
        initConfig();
    }
    
    private void initConfig() {
        tableSeleBg = reftable.getSelectionBackground();
        ((DefaultTableCellHeaderRenderer)reftable.getTableHeader().getDefaultRenderer()).setHorizontalAlignment(SwingConstants.CENTER);
        DefaultTableCellRenderer dtcr = new DefaultTableCellRenderer();
        dtcr.setHorizontalAlignment(SwingConstants.CENTER);
        reftable.setDefaultRenderer(Object.class, dtcr);
        reftable.getTableHeader().addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                if (col.isEmpty()) {
                    header = true;
                    tableShow.clearSelection();
                    reftable.clearSelection();
                }
                int p = reftable.columnAtPoint(e.getPoint());
                header = true;
                if (col.contains(p)) {
                    col.removeIf((t) -> {
                        return t == p;
                    });
                    reftable.removeColumnSelectionInterval(p, p);
                } else {
                    col.add(p);
                    reftable.addColumnSelectionInterval(p, p);
                    reftable.addRowSelectionInterval(0, reftable.getRowCount()-1);
                }
            }
        });
        reftable.getTableHeader().addMouseMotionListener(new java.awt.event.MouseAdapter() {
            @Override
            public void mouseMoved(java.awt.event.MouseEvent e) {
                int c = reftable.getColumnModel().getColumnIndexAtX(e.getX());
                if (c > -1) {
                    String s = reftable.getColumnName(c);
                    reftable.getTableHeader().setToolTipText(s == null || s.isEmpty() ? null : s);
                }
            }
        });
        reftable.addMouseMotionListener(new java.awt.event.MouseAdapter() {
            @Override
            public void mouseMoved(java.awt.event.MouseEvent e) {
                int r = reftable.rowAtPoint(e.getPoint());
                int c = reftable.columnAtPoint(e.getPoint());
                if (r > -1 && c > -1) {
                    Object o = reftable.getValueAt(r, c);
                    reftable.setToolTipText(o == null || o.toString().isEmpty() ? null : o.toString());
                }
            }
        });
        reftable.getModel().addTableModelListener(new TableModelListener() {
            @Override
            public void tableChanged(TableModelEvent e) {
                if (e.getType() == javax.swing.event.TableModelEvent.DELETE || 
                    e.getType() == javax.swing.event.TableModelEvent.INSERT) {
                    ((DefaultTableModel)tableShow.getModel()).setRowCount(reftable.getRowCount());
                    tableShow.getSelectionModel().clearSelection();
                    tableShow.revalidate();
                }
            }
        });
        ListSelectionModel lsm = reftable.getSelectionModel();
        lsm.addListSelectionListener(new ListSelectionListener() {
            
            private boolean flag = true;
            private int rowIndex;
            private int colIndex;
            
            @Override
            public void valueChanged(ListSelectionEvent e) {
                if (flag) {
                    flag = false;
                    colIndex = reftable.getSelectedColumnCount();
                    rowIndex = lsm.getLeadSelectionIndex();
                }
                if (!e.getValueIsAdjusting()) {
                    flag = true;
                    if (header) {
                        header = false;
                    } else {
                        if (colIndex > 1 || reftable.getSelectedRowCount() > 1) {
                            return;
                        }
                        if (!col.isEmpty()) {
                            col.clear();
                        }
                        if (rowIndex > -1) {
                            tableShow.setRowSelectionInterval(rowIndex, rowIndex);
                        }
                    }
                }
            }
        });
    }
    
    @Override
    public Component getTableCellRendererComponent(JTable table, Object obj,
            boolean isSelected, boolean hasFocus, int row, int col) {
        ((DefaultTableModel) table.getModel()).setRowCount(reftable.getRowCount());
        if (isSelected) {
            if (table.getSelectedRow() < reftable.getRowCount()) {
                reftable.setRowSelectionInterval(row, row);
                reftable.setColumnSelectionInterval(0, reftable.getColumnCount()-1);
                setForeground(Color.WHITE);
                setBackground(tableSeleBg);
            }
        } else {
            setForeground(Color.BLACK);
            if (row % 2 == 0) {
                setBackground(Color.WHITE);
            } else {
                setBackground(reftable.getTableHeader().getBackground());
            }
        }
        setText(String.valueOf(row + 1));
        return this;
    }

    @Override
    public void valueChanged(ListSelectionEvent e) {
        tableShow.revalidate();
    }    
}

你会发现该类被高度封装。为了便于大家使用,对外只暴露了一个构造函数,所以使用起来非常简单方便:

// 首先将上面的源码保存下来
// 你的JTable及其容器
javax.swing.JTable jt = new javax.swing.JTable();
javax.swing.JScrollPane jsp = new javax.swing.JScrollPane();
jsp.setViewportView(jt);
// 添加宽度为30的行号组件
jsp.setRowHeaderView(new RowHeader(jt, 30));

功能&效果

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

  1. 显示行号(实现点击行号,选中相应行)
  2. 点击单元格,相应行号被选中
  3. 单双行不同颜色显示(浏览效果更佳)
  4. 单击表头选中列,支持任意多选
  5. 添加或删除行后同步更新行号
  6. 表头悬浮提示、单元格悬浮提示
  7. 表头内容、单元格内容居中显示

P.S…最近发现一篇为JTable实现合并单元格的文章,有兴趣的看官可以去了解一下。

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值