LL脳がscalaの勉強を始めたよ その114


Scalaコップ本の最終章の33章をやっていきますよー、33章ではこれまでの内容の総まとめとしてSCellsという名前のスプレッドシートアプリケーションを構築していきますよ(`・ω・´)

ビジュアルフレームワーク

まずはスプレッドシートのユーザインターフェースを検討しつつ、基本的なビジュアルフレームワークを用意していきますよ(`・ω・´)

スプレッドシートは次のような使用を持つものと考えますデス

  • スクロールできる表
  • 0から99までの行を持つ
  • AからZまでの列を持つ

スプレッドシートのUIとしては前章でも扱ったSwingのScrollPaneとして、次のように定義してやりますよ

// 実装するアプリのパッケージ名を定義しますよ
package org.test.scells
// Swingパッケージをインポートします
import swing._
// swing.ScrollPaneを拡張してスプレッドシートテーブルを構築します
class Spreadsheet(val height:Int, val width:Int) extends ScrollPane {
  // ScrollPaneのtableサブコンポーネントの定義を行います
  val table = new Table(height, width){
    // 行の高さを設定
    rowHeight = 25
    // 表の自動サイズ変更をOff
    autoResizeMode = Table.AutoResizeMode.Off
    // グリッド線を描画
    showGrid = true
    // グリッド線の色をグレイに設定
    gridColor = new java.awt.Color(150, 150, 150)
  } 
  // ScrollPaneのrowHeaderサブコンポーネントの定義を
  // ListViewを利用して行います
  val rowHeader = new ListView((0 until height) map (_.toString)){
    // セル幅を30ポイントに固定
    fixedCellWidth = 30
    // セルの高さをtableのrowHeightと同じ25ポイントに設定
    fixedCellHeight = table.rowHeight
  }
  // スプレッドシート全体をtable rowHeaderを利用して定義
  viewportView = table
  rowHeaderView = rowHeader
}

スプレッドシートのレイアウトコードを定義したので、SpreadsheetコンポーネントGUIアプリケーションとして動作させるメインプログラムを定義してやります

// パッケージを定義します
package org.test.scells
// swingパッケージをインポートします
import swing._
// 前章と同様にSimpleGUIApplicationを拡張して
// GUIアプリケーションを実装します
object Main extends SimpleGUIApplication {
  // mainから実行されるtopメソッドを定義して
  // メインフレームを定義してやります
  def top = new MainFrame {
    // メインフレームのタイトルを設定します
    title = "ScalaSheet"
    // コンテンツとしてSpreadsheetコンポーネントを設定します
    contents = new Spreadsheet(100, 26)
  }
}

今回もコンパイルして実行するだけでとりあえずGUIが立ち上がりますデス。実行手順はおさらいがてらにこんな感じですね(`・ω・´)

// 各パッケージごとにコンパイルします
$ scalac Main.scala Spreadsheet.scala
// 実行します
$ scala org.test.scells.Main

データ入力と表示の分離

上記コードによってそれっぽいスプレッドシートを作ることはできたのですが、セルに入力した文字がそのまま表示されるだけで、数式による演算なんかの機能は全くないという悲しい状態です(´・ω・`)

とりあえず数式入力等が行えるようにデータ入力と表示を切り離してやります。入力データと表示形式を分離させるには、デフォルトでは入力されたデータをそのまま表示するという動作を行うTableクラスのrenderComponentメソッドをオーバーライドする必要がありますデス

また、renderComponentメソッドで入力データと表示内容を分離するためには各セルの内容を内部配列として定義する必要がありますデス。ただし、内部データとGUIコンポーネントは分離するのがオススメなのでModelという別クラスで次のように定義してやるデス(`・ω・´)

package org.test.scells
// セル情報格納用のModelクラスを定義してやります
class Model(val height:Int, val width:Int){
  // ケースクラスとして各セルを定義します
  case class Cell(row:Int, column:Int)
  val cells = new Array[Array[Cell]](height, width)
  // 表の全セル分の要素をデータとして作成します
  for (i <- 0 until height; j <- 0 until width)
    cells(i)(j) = new Cell(i, j)
}

それでは前出のSpreadsheetクラスについてrenderComponentメソッドをオーバーライドしてやりますデス

package org.test.scells
import swing._
class Spreadsheet(val height:Int, val width:Int) extends ScrollPane {
  
  // セル内のデータを格納するモデル情報を呼び出し
  val cellModel = new Model(height, width)
  import cellModel._
    
  val table = new Table(height, width){
    rowHeight = 25
    autoResizeMode = Table.AutoResizeMode.Off
    showGrid = true
    gridColor = new java.awt.Color(150, 150, 150)
    // renderComponentメソッドをオーバーライドします
    override def rendererComponent(isSelected:Boolean,
        hasFocus:Boolean, row:Int, column:Int):Component =
      // セルが選択状態(編集中)の時は入力データをTextFieldとして提供 
      if(hasFocus) new TextField(userData(row, column))
      // ソレ以外の場合は表示用データをLabelとして表示
      else
        // 表示用データとしては自分の位置座標(cells(row)(column)の値)
        // を右揃えの形式で表示する
        new Label(cells(row)(column).toString){
          xAlignment = Alignment.Right
        } 
    // TextFieldに出力するデータをフォーマットする処理です
    def userData(row:Intnt, column:Int):String = {
      val v = this(row, column)
      // nullの場合は空白を、それ以外の場合は文字列として出力します
      if(v == null) "" else v.toString
    }     
  } 
  val rowHeader = new ListView((0 until height) map (_.toString)){
    fixedCellWidth = 30
    fixedCellHeight = table.rowHeight
  }
  viewportView = table
  rowHeaderView = rowHeader
}

それでは動作を確認してやりますよー

// コンパイルして
$ scalac *.scala
//実行します
$ scala org.test.scells.Main

スクリーンショットは省略しますが想定どうり(コップ本p.588風)のGUIアプリが立ち上がりました(`・ω・´)

いじょうー

時間切れのため以上ですー。次回は数式対応からやっていきますよー