S-JIS[2011-04-12/2011-11-10] 変更履歴

Scala Table(Swing)

ScalaSwingのテーブルのメモ。


サンプル

Tableの例。

import scala.swing._
object TableSample extends SimpleSwingApplication {
  override def top = new MainFrame {
    title = "テーブルサンプル"

    contents = new ScrollPane(new Table {
      override lazy val model = super.model.asInstanceOf[javax.swing.table.DefaultTableModel]

      autoResizeMode = Table.AutoResizeMode.Off //列の幅を自動変更しない

      // 列(カラム)の初期化
      model.addColumn("列1")
      model.addColumn("列2")
      model.addColumn("列3")

      // 行(データ)の追加
      model.addRow(Array[AnyRef]("foo", "bar", "zzz"))
      model.addRow(Array[AnyRef]("ほげ", "ふが", "はげ"))
    })

    size = new Dimension(256, 212)
  }
}

Tableのコンストラクターは何種類かあるが、行と列の初期値を与えるコンストラクターはmodelがDefaultTableModelではないので、可変のテーブルを作るには不便そう。

行追加時にAnyRef(Object)の配列を渡しているが、内部でさらにjava.util.Vectorに変換されるので、
何らかのクラスから行データを作るなら、そのクラスから(配列でなく)直接java.util.Vectorに変換する方がいいだろう。


セル毎・行毎のツールチップ

セル毎にツールチップの内容を変える例。

ateraiさん(てんぷらメモ)のJTableのTooltipsを行ごとに変更によると、ツールチップを変更するにはJTableのメソッドをオーバーライドする必要がある。
しかしScalaのTableはJTableをpeerとして保持しているものの、独自拡張した無名クラスになっている。したがってそれを継承したクラスを作ることが出来ない。(ソースをコピペすることは出来るが、今後のScalaのバージョンアップで内容が変わりうる事を考えれば、やりたくない)
そんな中で、レンダラーを変える方法だとTableの中でレンダラー取得用メソッドが用意されているので、オーバーライドすることが出来る。

    contents = new ScrollPane(new Table {
〜
      override protected def rendererComponent(isSelected: Boolean, focused: Boolean, row: Int, column: Int): Component = {
        val r = super.rendererComponent(isSelected, focused, row, column)
        val d = model.getValueAt(row, column)
        r.tooltip = d.toString
        r
      }
    })

表示されるセルの行位置・列位置が引数で渡されるので、それを元にデータを取得すれば、データ内容をツールチップとして表示することが出来る。
ここで列位置を無視して値を取得するようにすれば、行毎にツールチップを出せる状態になる。


セルの選択方法

デフォルトでは、セルをクリックすると行全体が選択される。[2011-11-10]
JavaのJTableではsetSelectionMode()で選択方法を変更したが、ScalaのTableではTable.selectionのIntervalModeで変更する。

行の選択方法の指定
IntervalMode(Scala) ListSelectionModel(Java) 内容
Single SINGLE_SELECTION 1行だけ選択可能。
SingleInterval SINGLE_INTERVAL_SELECTION 間をとばさずに範囲選択可能。(Shiftキー)
MultiInterval MULTIPLE_INTERVAL_SELECTION 複数行を選択可能。(Shiftキー+Ctrlキー)

個別のセルを選択できるようにする為にはElementModeを指定する。

選択範囲の指定
ElementMode(Scala) 内容
Row 行を選択する。
Column 列を選択する。
Cell セルを選択する。
None 選択しない。
class MyTable extends Table {
  selection.intervalMode = Table.IntervalMode.MultiInterval
  selection.elementMode = Table.ElementMode.Cell
}

セルの編集

デフォルトでは、セルをダブルクリックしたりF2キーを押したり、あるいは文字を入力すると、どのセルでも編集できる。[2011-04-13]

編集できる列を固定する方法

JavaのJTableではgetCellEditor()メソッドがエディターを返すようになっており、ここでnullを返すと編集できない。
ScalaのTableではgetCellEditorからeditorメソッドが呼ばれるようになっているので、これをオーバーライドする。

    contents = new ScrollPane(new Table {
〜
      // 3列目(列番号が2)のときだけ編集できるようにする例
      override protected def editor(row: Int, column: Int) = {
        if (viewToModelColumn(column) == 2) super.editor(row, column) else null
      }
    })

※viewToModelColumnを使っておかないと、ドラッグ&ドロップで列を入れ替えたときに別の列が変更可能になってしまう。


更新された値を取得する方法

セルを編集して更新された値を取得する方法。[2011-04-14]

セルが更新されると、TableModelに値がセットされ、更新イベントが発生する。なのでそのイベントを捕捉してやればいい。
ただ、TableのデフォルトコンストラクターでTableModelを初期化した場合は、イベントを捕捉できる状態になっていない。自分でリスナーを登録してやる必要がある。(リスナー自体は用意されている)

    contents = new ScrollPane(new Table {
〜
      model.addTableModelListener(modelListener)
      listenTo(this)
      reactions += {
        case event.TableUpdated(source, range, column) =>
          range.foreach { row =>
            println(model.getValueAt(row, column))
          }
      }
    })

TableModelを自分でセットしている場合は、modelのセッターメソッド内でリスナーが登録される。

    contents = new ScrollPane(new Table {
      super.model = new javax.swing.table.DefaultTableModellistenTo(this)
      reactions += {
        case event.TableUpdated(source, range, column) =>
          range.foreach { row =>
            println(model.getValueAt(row, column))
          }
      }
    })

しかしmodelを自分でセットするなら、素直に新しいクラスを作ってsetValueAtをオーバーライドすればいいような気もする^^;

    contents = new ScrollPane(new Table {
      super.model = new javax.swing.table.DefaultTableModel {
        override def setValueAt(value: AnyRef, row: Int, column: Int) {
          println("old=" + getValueAt(row, column))
          super.setValueAt(value, row, column)
          println("new=" + value)
        }
      }
〜
    })

列挙子選択エディター

列挙型(Enumeration)をコンボボックスで選択できるようにしてみよう。[2011-04-15]
(参考: JTableの日付エディター

import javax.swing._

class EnumCellEditor extends DefaultCellEditor(new JComboBox) {
  import java.awt._

  protected var evalue: Enumeration#Value = _

  override def getComponent() = super.getComponent.asInstanceOf[JComboBox]

  override def getTableCellEditorComponent(table: JTable, value: AnyRef, isSelected: Boolean, row: Int, column: Int): Component = {
    evalue = null

    val combo = getComponent
    combo.removeAllItems()

    // 列挙型クラスの取得
    val e = value.getClass.getMethod("scala$Enumeration$$outerEnum").invoke(value).asInstanceOf[Enumeration]
    e.values.foreach { combo.addItem(_) }
    combo.setSelectedItem(value)

    super.getTableCellEditorComponent(table, value, isSelected, row, column)
  }

  override def stopCellEditing(): Boolean = {
    evalue = super.getCellEditorValue.asInstanceOf[Enumeration#Value]
    super.stopCellEditing()
  }

  override def getCellEditorValue() = evalue
}
    contents = new ScrollPane(new Table {
〜
      peer.setDefaultEditor(classOf[Enumeration#Value], new EnumCellEditor());
〜
    })

列挙型クラスは、リフレクションを使って値(列挙子)から取得してみた。
コンストラクター辺りで列挙型クラス(オブジェクト)を渡すようにすれば、もっと確実だろうけど…。


ダブルクリックで列幅を自動調節

ヘッダーの列と列の間にマウスカーソルがある(列幅変更の矢印マークになっている)ときにダブルクリックして、データの最大幅に自動的に合わせる方法。[2011-04-14]

以前、JavaのJTableHeaderを拡張してダブルクリック時に列幅を自動調節できるテーブルヘッダーを作ったので、それをそのまま使うか、Scalaにコンバートすればいいんだけど。

    contents = new ScrollPane(new Table {
      peer.setTableHeader(new SampleHeader(peer.getTableHeader().getColumnModel))
〜
    })

しかしせっかくScalaを使っているのだから、トレイトにしてみる。

import scala.swing._

trait ColumnWidthToFitDataTable {
  selfTable: Table =>

  import java.awt._
  import java.awt.event._
  import javax.swing.SwingUtilities
  import javax.swing.table.JTableHeader

  protected val mouseListener = new MouseAdapter {
    override def mouseClicked(e: MouseEvent) {
      if (SwingUtilities.isLeftMouseButton(e) && e.getClickCount % 2 == 0) { //左ダブルクリック
        val header = selfTable.peer.getTableHeader
        if (header.getCursor.getType == Cursor.E_RESIZE_CURSOR) { // 矢印カーソル
          val vc = header.columnAtPoint(new Point(e.getX - 3, e.getY))
          if (vc >= 0) {
            columnWidthToFitData(vc)
            e.consume()
          }
        }
      }
    }
  }

  // 列幅をデータの最大幅に変更する
  def columnWidthToFitData(vc: Int) {
    val table = selfTable.peer

    val vrows = table.getRowCount // 表示行数
    if (vrows > 0) {
      val w = (0 until vrows).map { i =>
        val r     = table.getCellRenderer(i, vc) // レンダラー
        val value = table.getValueAt(i, vc) // データ
        r.getTableCellRendererComponent(table, value, false, false, i, vc).getPreferredSize.width // データ毎の幅
      }.max

      table.getColumnModel.getColumn(vc).setPreferredWidth(w + 1);
    }
  }

  // 初期状態としてマウスリスナーをセット
  selfTable.peer.getTableHeader.addMouseListener(mouseListener)

  // テーブルヘッダーが差し替えられた時にマウスリスナーを移し替える
  selfTable.peer.addPropertyChangeListener(new java.beans.PropertyChangeListener {
    def propertyChange(e: java.beans.PropertyChangeEvent) {
      if ((e.getSource eq selfTable.peer) && e.getPropertyName == "tableHeader") {
        Option(e.getOldValue.asInstanceOf[JTableHeader]).foreach { _.removeMouseListener(mouseListener) }
        Option(e.getNewValue.asInstanceOf[JTableHeader]).foreach { _.addMouseListener(mouseListener) }
      }
    }
  })
}
    contents = new ScrollPane(new Table with ColumnWidthToFitDataTable {
〜
    })

行ソート

JavaのSwingではsetAutoCreateRowSorter(true)によって行ソート(一番上の行ヘッダーの項目をクリックするとその項目でソートされる)が出来る。[2011-10-17]

Scalaにはこのメソッドに対するラッパーメソッドが無いので、peerに対して呼び出す。(peer.setAutoCreateRowSorter(true))
また、ソートする際には各項目のクラスが必要になるので、クラスも返すようにする。(TableModel#getColumnClass()
が、ScalaのSwingにはバグがあって、これだけではソートが実行されないので対処する必要がある。(apply()をオーバーライドし、viewToModelRow()を呼ぶようにする)

Scala(2.9.1)のSwingのTableのソートのバグとは、Tableクラスは値を取得する際にapplyメソッドを呼ぶようになっているが、このapplyメソッドは行インデックスに対する考慮がされていない事。
(参考: Bug in scala.swing.Table?

class Table extends Component with Scrollable.Wrapper {
  override lazy val peer: JTable = new JTable with Table.JTableMixin with SuperMixin {
    〜
    override def getValueAt(r: Int, c: Int) = Table.this.apply(r,c).asInstanceOf[AnyRef]
  }
〜
  def apply(row: Int, column: Int): Any = model.getValueAt(row, viewToModelColumn(column))

  def viewToModelColumn(idx: Int) = peer.convertColumnIndexToModel(idx)
  def modelToViewColumn(idx: Int) = peer.convertColumnIndexToView(idx)
〜
}
class RowSortTable extends Table {
  import javax.swing.table._

  class RowSortTableModel extends DefaultTableModel {
    private var columnClass = Array[Class[_]]()

    override def addColumn(name: Object) { addColumn(name, classOf[Object]) }
    def addColumn(name: Object, c: Class[_]) {
      columnClass :+= c
      super.addColumn(name)
    }

    override def getColumnClass(index: Int) = columnClass(index)
  }

  override def apply(row: Int, column: Int): Any = model.getValueAt(viewToModelRow(row), viewToModelColumn(column))
  def viewToModelRow(idx: Int) = peer.convertRowIndexToModel(idx)
  def modelToViewRow(idx: Int) = peer.convertRowIndexToView(idx)

  super.model = new RowSortTableModel
  override def model = super.model.asInstanceOf[RowSortTableModel]

  peer.setAutoCreateRowSorter(true)
}

使用例:

class MyTable extends RowSortTable {

  model.addColumn("name", classOf[String])
  model.addColumn("size", classOf[java.lang.Integer])
}

Scala Swingへ戻る / Scala目次へ戻る / 技術メモへ戻る
メールの送信先:ひしだま