JTable のセルの値を更新するテーブルモデルの実装方法

以前の記事「JTable でデータとビューをつなぐテーブルモデル」では、 AbstractTableModel から派生してテーブルモデルを実装することで、全てのセルが編集不可になるところまで確認しました。

しかし、グリッドコンポーネントでデータがいつも編集不可で良いはずはありません。

もちろん、ビューである JTable からの入力を受け付け、それをテーブルモデルが管理する実際のデータに反映する方法はあります。

ここでは、「カスタムのセルレンダラーを作りセルの値によって背景色を変更する方法」で作成したプログラムを少しだけ直して、 次のようにデータが編集可能状態になることをみてみましょう。

セルのデータ編集を許可するためのテーブルモデル ~ isCellEditable と setValueAt の実装

JTable が特定のセルのデータを編集可とするか編集不可とするか決めるときにも、やはり、 テーブルモデルに問い合わせを行います。

TableModel インターフェイスの isCellEditable メソッドを、 セルの行インデックス、列インデックスを渡して呼び出し、それが true を返せば編集可とし、false を返せば編集不可とします。

そして、JTable から値が入力されると後には、setValueAt メソッドが呼ばれますので、 ここで実際のデータを更新できます。

ユーザーが JTable からデータを入力したからといって、必ずしもデータを更新する必要はありません。 不正な値であれば受け付けなければ良いだけです。入力をどう扱うのかもテーブルモデル次第です。

データを更新したら JTable にイベントで通知

もしデータを更新したらそれに応じて、 JTable も適切に更新する必要があります。 JTable にデータが更新されたことを知らせるために、データの更新イベントで通知します。 JTable はデータモデルのイベントイベントリスナーを設定しているので、イベントの通知を受け取ります。

AbstractTableModel ではイベント通知のためのメソッドを実装しています。

セルに関連付けされたデータが更新されたら fireTableCellUpdated メソッドで、 イベントリスナーに通知します。

以上、ここまでを実装すると次のようになります。

package com.keicode.java.testapp;

import javax.swing.table.AbstractTableModel;

@SuppressWarnings("serial")
class MyTableModel3 extends AbstractTableModel {

  Object[][] data = {
      {"X", "100", true},
      {"X", "200", false},
      {"X", "300", false},
      {"Y", "400", false},
      {"Y", "500", false},
      {"Z", "600", false},
      {"Z", "700", false}};
  
  String[] columns = {"Column 0", "Column 1", "Column 2"};
  
  @Override
  public boolean isCellEditable(int rowIndex, int columnIndex) {
    return true;
  }

  @Override
  public void setValueAt(Object val, int rowIndex, int columnIndex) {
    data[rowIndex][columnIndex] = val;
    fireTableCellUpdated(rowIndex, columnIndex);
  }

  @Override
  public Class<?> getColumnClass(int columnIndex) {
    return data[0][columnIndex].getClass();
  }

  @Override
  public String getColumnName(int column) {
    return columns[column];
  }

  @Override
  public int getRowCount() {
    return data.length;
  }

  @Override
  public int getColumnCount() {
    return columns.length;
  }

  @Override
  public Object getValueAt(int rowIndex, int columnIndex) {
    return data[rowIndex][columnIndex];
  }
    
}

これを使う JTable 側はこちら。クラス名は変えましたが内容は以前と同じです。 TableTest16.java としました。

package com.keicode.java.testapp;

import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.table.TableColumn;

@SuppressWarnings("serial")
public class TableTest16 extends JFrame {
  public TableTest16() {
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    setSize(350, 150);

    JTable table = new JTable();
    MyTableModel3 tableModel = new MyTableModel3();
    table.setModel(tableModel);
    
    MyTableCellRenderer tableCellRenderer = new MyTableCellRenderer();
    TableColumn col = table.getColumnModel().getColumn(0);
    col.setCellRenderer(tableCellRenderer);
    
    JScrollPane scrollPane = new JScrollPane(table);
    add(scrollPane);
  }

  public static void main(String[] args) {
    SwingUtilities.invokeLater(new Runnable() {
      @Override
      public void run() {
        TableTest16 app = new TableTest16();
        app.setVisible(true);
      }
    });
  }
}

MyTableCellRenderer も以前と同じです。

package com.keicode.java.testapp;

import java.awt.Color;
import java.awt.Component;
import javax.swing.JLabel;
import javax.swing.JTable;
import javax.swing.table.DefaultTableCellRenderer;

@SuppressWarnings("serial")
public class MyTableCellRenderer extends DefaultTableCellRenderer {
  
  @Override
  public Component getTableCellRendererComponent(
    JTable table, 
    Object value, 
    boolean isSelected, 
    boolean hasFocus,
    int row, 
    int column) {

    Component renderer = super.getTableCellRendererComponent(
        table, 
        value, 
        isSelected, 
        hasFocus, 
        row, 
        column);

    if(value == null) {
      return renderer;
    }
    
    setHorizontalAlignment(JLabel.CENTER);
    
    switch(value.toString().toUpperCase()) {
    case "X": 
      setBackground(Color.RED);
      break;
    case "Y":
      setBackground(Color.BLUE);
      break;
    default:
      setBackground(Color.WHITE);
    }
    
    return renderer;
  }
}

さて、テーブルモデル MyTableModel3 についてですが、ここでは isCellEditable は常に true を返しているので、全てのセルが編集可能です。

JTable からの入力データは setValueAt メソッドを通して、テーブルモデルクラスに渡され、 ここではそのまま data 変数の配列にセットしています。

値を更新しているので、fireTableCellUpdated を呼び出して、データの更新イベントをリスナーに通知します。

ちなみに、ここでは画面から入力して Enter キーを押したタイミングで、セルが編集モードから通常の表示モードに切り替わるので、 そのタイミングでデータを読み直すのでイベント通知をしなくても(たまたま)セルが更新されます。

しかしもし、入力したセルと更新したセルが異なるような場合には、JTable は特定のセルの内容を更新しなければならないことを知りえないので、 実際のデータと画面表示が同期しない状態になってしまいます。

したがって、イベントによって適切にリスナーに通知することが重要になります。

以上、JTable でユーザーの入力を受け付けるには、テーブルモデルで isCellEditable で true を返して編集可能であることを示し、 さらに setValueAt でデータをテーブルモデルに取り込む。さらに、データを更新したらイベントで関連のあるオブジェクトに通知する、という仕組みについて説明しました。

ここまでお読みいただき、誠にありがとうございます。SNS 等でこの記事をシェアしていただけますと、大変励みになります。どうぞよろしくお願いします。

© 2024 Java 入門