2011年1月2日日曜日

Scala Swingでデザインパターン[Simple Factory]

ScalaのGUIプログラムの実行方法がわかったところで、今度はもうちょっと複雑なプログラムを組んでみようかな~ということで、昔買ったデザインパターンをSwing or awt なGUIのサンプルを用いて解説している本のサンプルを実装してみました。
こちらのプログラムは画面上部のテキストボックスに名前を入力したら、名前を姓(Last Name)と名(First Name)に分割してそれぞれのテキストボックスに表示するという簡単なプログラムです。
1. 名前を半角空白区切りにすると前が名で後ろが姓
2. 名前をカンマ区切りにすると前が姓で後ろが名

というような分割ルールになっています。

結構簡単なプログラムのはずなんですが、思いのほかはまってしまいました。
はまりポイントは下記の通り
  1. GroupLayout用Panelが用意されていない。
  2. ラベル(Label)の中央寄せの仕方がわからない。
  3. BoxLayoutを利用すると何故かテキストボックスのはばがやたらひろがる。
  4. 複数のボタンにそれぞれアクションをつけたい場合ってどうするの?
  1. GroupLayout用Panelが用意されていない。
    これは別に大した問題でもなかったのですが・・・。
    scala.swing パッケージないでは、あらかじめLayoutManagerを登録したPanelを利用するようになっています。別にJPanelとGroupLayoutクラスをそのまま利用しても特に問題はなかったのですが、今回はscala.swingパッケージを利用することにこだわっていたので、仕方なくBoxPanel、GridPanel、FlowPanelを利用して作成するようにしました。
    この部分のソースはこちら
    /**
         * コンポーネントの配置
         */
        contents = new BoxPanel(Orientation.Vertical) {
          preferredSize = new Dimension(300, 200)
          
          // 名前入力欄とそのラベルを追加
          contents += topLabel
          contents += topText
    
          // 隙間を作るために空のパネルを貼り付ける
          contents += new Panel {
            maximumSize = new Dimension(50,20)
            preferredSize = maximumSize
          }
    
          // 分割した姓名を表示するためのテキストボックスを追加
          contents += new GridPanel(2,1) {
            maximumSize   = new Dimension(300, 60)
            preferredSize = maximumSize
            vGap = 2
            // 名のラベルとテキストボックス
            contents += new FlowPanel {
              contents += new Label("First Name:")
              contents += firstNameText
            }
            // 姓のラベルとテキストボックス
            contents += new FlowPanel {
              contents += new Label("Last Name:")
              contents += lastNameText
            }
          }
          
          // ボタンを配置
          contents += new FlowPanel {
            contents += computeButton
            contents += clearButton
            contents += closeButton
          }
        }
    
  2. ラベル(Label)の中央寄せの仕方がわからない。
    これはScala関係ないような気もしますが・・・。
    swing.scalaのLabelクラスには
    • horizontalAlignment
    • horizontalTextPosition
    • xAlignment
    • xLayoutAlignment
    と4つも水平方向のレイアウトを定義しそうな名前のメソッドがあって、どれを使っていいのかわからなかった。
    正解はxLayoutAlignmentでした。
    サンプルソースはこんな感じです
    // トップラベル
        val topLabel = new Label ("Enter name:") {
          xLayoutAlignment = Component.CENTER_ALIGNMENT
        }
    
  3. BoxLayoutを利用すると何故かテキストボックスのはばがやたらひろがる。
    BoxLayoutを利用するとテキストボックスが勝手にmaximumSizeになってまうようです。
    なので、BoxLayout上でテキストボックスを配置する場合は
    val topText = new TextField (21) {
          maximumSize = preferredSize
        }
    
    というようにmaximumSizeをpreferredSizeで上書きする必要がありました。
  4. 複数のボタンにそれぞれアクションをつけたい場合ってどうするの?
    Scalaではボタンをクリックしたときはscala.swing.event.ButtonClickedケースクラスに押したボタンのクラスが渡される形でイベントが実行される。
    なので
    reactions += {
          case ButtonClicked(b) => b.text match {
            case "Close"   => System.exit(0)
            case "Clear"   => clearFields
            case "Compute" => {
              val namer = nFactory.getNamer(topText.text)
              firstNameText.text = namer.first
              lastNameText.text = namer.last
            }
          }
        }
    
    というようにmatch式でボタン名などを基準に処理を分けることができました。
結局Scalaそのものでわかんなかったのってイベント処理の方法だけだったのか・・・。
う~ん、ちょっとショック。こういうはまりポイントってすぐに解決策見つけちゃう人は見つけちゃうんですよね。
自分は、そうではないのでそういう人がうらやましいです。
最後に全部のソースを下記においておきます。
import scala.swing._
import scala.swing.event._
import javax.swing.border._
import java.awt.Dimension
import java.awt.Component
import java.awt.Color

object SimpleFactory extends SimpleGUIApplication {
  val nFactory = new NameFactory
  def top = new MainFrame {
    title = "Simple Factory Sample"

    /*
     * 使用するコンポーネント
     */
    // トップラベル
    val topLabel = new Label ("Enter name:") {
      xLayoutAlignment = Component.CENTER_ALIGNMENT
    }

    val topText = new TextField (21) {
      maximumSize = preferredSize
    }

    val firstNameText = new TextField(16) {
      maximumSize = preferredSize
    }

    val lastNameText = new TextField(16) {
      maximumSize = preferredSize
    }

    val computeButton = new Button("Compute")
    val clearButton = new Button("Clear")
    val closeButton = new Button("Close")

    /**
     * コンポーネントの配置
     */
    contents = new BoxPanel(Orientation.Vertical) {
      preferredSize = new Dimension(300, 200)
      contents += topLabel
      contents += topText

      // 隙間を作るために空のパネルを貼り付ける
      contents += new Panel {
        maximumSize = new Dimension(50,20)
        preferredSize = maximumSize
      }

      // 分割した姓名を表示するためのテキストボックスを追加
      contents += new GridPanel(2,1) {
        maximumSize   = new Dimension(300, 60)
        preferredSize = maximumSize
        vGap = 2
        // 名のラベルとテキストボックス
        contents += new FlowPanel {
          contents += new Label("First Name:")
          contents += firstNameText
        }
        // 姓のラベルとテキストボックス
        contents += new FlowPanel {
          contents += new Label("Last Name:")
          contents += lastNameText
        }
      }
      
      // ボタンを配置
      contents += new FlowPanel {
        contents += computeButton
        contents += clearButton
        contents += closeButton
      }
    }
    listenTo(computeButton)
    listenTo(clearButton)
    listenTo(closeButton)
    
    reactions += {
      case ButtonClicked(b) => b.text match {
        case "Close"   => System.exit(0)
        case "Clear"   => clearFields
        case "Compute" => {
          val namer = nFactory.getNamer(topText.text)
          firstNameText.text = namer.first
          lastNameText.text = namer.last
        }
      }
    }
    
    // 文字列をクリアする
    def clearFields = {
      firstNameText.text = ""
      lastNameText.text = ""
      topText.text = ""
    }
  }
}

abstract class Namer {
  // 2つのサブクラスによってextendsされる基底クラス
  def last: String;
  def first: String;
}

class LastFirst(s: String) extends Namer {
  private val list = s.split(",").toList
  
  def last   = if (list.length > 0) list.head else s
  def first  = if (list.length > 0) list.tail.mkString("") else ""
}

class FirstFirst(s: String) extends Namer {
  private val list = s.split(" ").toList
  
  def last  = if (list.length > 0) list.tail.mkString("") else ""
  def first = if (list.length > 0) list.head else s
}

class NameFactory {
  // コンマの有無によってどちらのクラスを返すべきかを決定する
  def getNamer(entry:String) : Namer = {
    // コンマの有無によって順序を判定
    if (entry.indexOf(",") > 0) new LastFirst(entry) else new FirstFirst(entry)
  }
}
またしばらくしたら同じ本からいくつかサンプルを紹介したいと思います。
まあするかもしれないし、しないかもしれないですが・・・。 ちなみに、今回の参考文献はこちらです。

Java実例プログラムによるデザインパターン入門講座―Swingプログラムで体得する23のパターン
ジェイムズ・W. クーパー
ピアソンエデュケーション
売り上げランキング: 270022
結城本のサンプルはわかり易いんだけど、実践でどう使うかわかるようなサンプルが欲しい方は
読んでみてはいかがでしょうか?
あとSwingのチュートリアルはやったけど、それ以外に何かもう一冊だけ
写経してみたいな~という方にはお薦めです。
※追伸 >- この投稿が役にたったと思った方は、上か下の広告のクリックをお願いいたします。

0 件のコメント:

コメントを投稿