You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

能否用常规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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 09:36:06