能否用常规JTable实现保龄球计分卡?如何拆分单元格为两个文本框?
用JTable实现保龄球计分卡:完全可行!附单元格拆分方案
当然可以用常规JTable实现保龄球计分卡,而且完全不需要依赖GridBagLayout——核心在于利用JTable的自定义单元格渲染器(TableCellRenderer)和编辑器(TableCellEditor)来实现“拆分单元格”的效果。下面我一步步给你拆解实现思路和代码示例:
一、核心思路:自定义单元格组件
JTable的每个单元格本质上可以渲染任意Swing组件,所以我们可以创建一个包含两个文本框的JPanel,把它作为单元格的渲染/编辑组件,以此实现“单元格拆分”的效果。同时用一个自定义数据类来存储每个单元格的两个分数值。
1. 定义存储单元格数据的类
首先创建一个简单的类来保存每一轮(frame)的两次投球分数:
public class FrameScore { private Integer firstRoll; private Integer secondRoll; // 构造器、getter、setter public FrameScore(Integer first, Integer second) { this.firstRoll = first; this.secondRoll = second; } public Integer getFirstRoll() { return firstRoll; } public void setFirstRoll(Integer firstRoll) { this.firstRoll = firstRoll; } public Integer getSecondRoll() { return secondRoll; } public void setSecondRoll(Integer secondRoll) { this.secondRoll = secondRoll; } }
2. 自定义单元格渲染器
这个渲染器负责把FrameScore的数据显示到包含两个文本框的面板中,同时可以根据保龄球规则处理特殊显示(比如全中显示X,补中显示/):
import javax.swing.*; import javax.swing.table.TableCellRenderer; import java.awt.*; public class FrameCellRenderer extends JPanel implements TableCellRenderer { private JTextField tf1; private JTextField tf2; public FrameCellRenderer() { setLayout(new FlowLayout(FlowLayout.CENTER, 2, 0)); tf1 = new JTextField(1); tf2 = new JTextField(1); // 渲染状态下文本框不可编辑 tf1.setEditable(false); tf2.setEditable(false); add(tf1); add(tf2); } @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { // 适配选中状态的样式 if (isSelected) { setBackground(table.getSelectionBackground()); tf1.setBackground(table.getSelectionBackground()); tf2.setBackground(table.getSelectionBackground()); } else { setBackground(table.getBackground()); tf1.setBackground(table.getBackground()); tf2.setBackground(table.getBackground()); } if (value instanceof FrameScore) { FrameScore score = (FrameScore) value; Integer first = score.getFirstRoll(); Integer second = score.getSecondRoll(); // 处理全中(第一次投球10分) if (first != null && first == 10) { tf1.setText("X"); tf2.setText(""); } // 处理补中(两次投球总和10分) else if (first != null && second != null && first + second == 10) { tf1.setText(first.toString()); tf2.setText("/"); } // 普通情况 else { tf1.setText(first != null ? first.toString() : ""); tf2.setText(second != null ? second.toString() : ""); } } else { tf1.setText(""); tf2.setText(""); } return this; } }
3. 自定义单元格编辑器
编辑器允许用户在两个文本框中输入分数,完成后把数据存回FrameScore对象:
import javax.swing.*; import javax.swing.table.TableCellEditor; import javax.swing.text.AttributeSet; import javax.swing.text.BadLocationException; import javax.swing.text.PlainDocument; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; public class FrameCellEditor extends AbstractCellEditor implements TableCellEditor { private JPanel panel; private JTextField tf1; private JTextField tf2; private FrameScore currentScore; public FrameCellEditor() { panel = new JPanel(new FlowLayout(FlowLayout.CENTER, 2, 0)); tf1 = new JTextField(1); tf2 = new JTextField(1); // 添加输入验证:只允许输入数字 tf1.setDocument(new PlainDocument() { @Override public void insertString(int offs, String str, AttributeSet a) throws BadLocationException { if (str.matches("[0-9]")) { super.insertString(offs, str, a); } } }); tf2.setDocument(tf1.getDocument()); // 回车触发编辑完成 tf2.addActionListener(e -> stopCellEditing()); panel.add(tf1); panel.add(tf2); } @Override public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { currentScore = (FrameScore) value; // 初始化文本框内容 tf1.setText(currentScore.getFirstRoll() != null ? currentScore.getFirstRoll().toString() : ""); tf2.setText(currentScore.getSecondRoll() != null ? currentScore.getSecondRoll().toString() : ""); return panel; } @Override public Object getCellEditorValue() { // 获取输入值并转换为Integer Integer first = tf1.getText().isEmpty() ? null : Integer.parseInt(tf1.getText()); Integer second = tf2.getText().isEmpty() ? null : Integer.parseInt(tf2.getText()); // 规则验证:全中时第二次投球不允许输入 if (first != null && first == 10) { second = null; } currentScore.setFirstRoll(first); currentScore.setSecondRoll(second); return currentScore; } }
二、组装JTable并使用自定义组件
最后把这些组件整合到JTable中:
import javax.swing.*; import javax.swing.table.DefaultTableModel; public class BowlingScoreCard extends JFrame { public BowlingScoreCard() { setTitle("保龄球计分卡"); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setSize(600, 200); // 创建表格模型:10列对应10轮,1行对应玩家分数 DefaultTableModel model = new DefaultTableModel(1, 10) { @Override public Class<?> getColumnClass(int columnIndex) { return FrameScore.class; } }; // 初始化每一列的FrameScore对象 for (int i = 0; i < 10; i++) { model.setValueAt(new FrameScore(null, null), 0, i); } JTable table = new JTable(model); // 设置行高和列宽 table.setRowHeight(40); for (int i = 0; i < 10; i++) { table.getColumnModel().getColumn(i).setPreferredWidth(50); } // 绑定自定义渲染器和编辑器 table.setDefaultRenderer(FrameScore.class, new FrameCellRenderer()); table.setDefaultEditor(FrameScore.class, new FrameCellEditor()); add(new JScrollPane(table)); setVisible(true); } public static void main(String[] args) { SwingUtilities.invokeLater(BowlingScoreCard::new); } }
三、关于GridBagLayout的替代
你完全不需要用GridBagLayout——上面的示例中我们只用了FlowLayout来排列两个文本框,简单又高效。如果需要更精细的布局控制,也可以用BoxLayout或者直接手动设置组件边界,完全能满足需求。
这样一套实现下来,就用纯JTable完成了保龄球计分卡的功能,单元格也被拆分成了两个可编辑的文本框,完全符合你的要求!
内容的提问来源于stack exchange,提问作者bennispfr
相关产品推荐
相关产品推荐

