S-JIS[2011-04-06] 変更履歴

Scala Component(Swing)

ScalaSwingのコンポーネントのメモ。

 

概要

ScalaのSwingでラベルやボタン等のコンポーネントの親クラスがscala.swing.Component。
内部でjavax.swing.JComponentを保持している。

abstract class Component extends UIElement {
  override lazy val peer: javax.swing.JComponent = new javax.swing.JComponent with SuperMixin {}
〜
}

ちなみにpeerは親クラスのUIElementで宣言されており、「def peer: java.awt.Component」。
また、例えばComponentの子クラスであるLabelでは以下の様に上書き(オーバーライド)されている。

class Label(text0: String, icon0: Icon, align: Alignment.Value) extends Component {
  override lazy val peer: JLabel = new JLabel(text0, toNullIcon(icon0), align.id) with SuperMixin
〜
}

一番親では「def」で宣言されているのに、子クラスでは「val」でオーバーライドしている。
これは、Scalaではvalで定義した(publicな)変数は、コンパイルされるとセッターメソッド・ゲッターメソッドとして定義される、すなわちメソッド定義(def)と同じ為。
しかも「lazy val」なので、一度しかインスタンス化されない。

また、Scalaでは(Javaと同じく)共変戻り値型が使用できるので、戻り型をjava.awt.Component → javax.swing.JComponent → javax.swing.JLabelとする事が出来る。


独自コンポーネントの作成例

直線を描く独自コンポーネントを作ってみる。
独自コンポーネントではjavax.swingのコンポーネントをラップする訳では無いので、peerはデフォルトのままでいい。

import scala.swing._
object ComponentSample extends SimpleSwingApplication {
  override def top = new MainFrame {
    title = "サンプル"
    contents = new FlowPanel {
      contents += new Label("ラベル1")

      import java.awt.Color
      contents += new LineComponent(64, 48)
      contents += new LineComponent(-64, 48, Color.BLUE)
      contents += new LineComponent(64, -48, Color.GREEN)
      contents += new LineComponent(-64, -48, Color.YELLOW)

      contents += new Label("ラベル2")
    }
  }
}
class LineComponent(nx: Int, ny: Int, c: Color = java.awt.Color.BLACK) extends Component {

  peer.setPreferredSize(new Dimension(nx.abs, ny.abs))

  override protected def paintComponent(g: Graphics2D) {
    g.setColor(c)
    val sx = if (nx < 0) -nx else 0
    val sy = if (ny < 0) -ny else 0
    val ex = if (nx < 0) 0 else nx
    val ey = if (ny < 0) 0 else ny
    g.drawLine(sx, sy, ex, ey)
  }
}

Componentにはpaint系のメソッドがあるので、オーバーライドして独自の描画を行う。
なお、javax.swing.JComponentではpaint系メソッドの引数はGraphicsであり、実体はGraphics2Dなのでキャストして使うのが普通。
だが、Scalaではその部分を影で実行してくれるので、引数がGraphics2Dのpaint系メソッドが使用できる。

呼ばれる順 メソッド名 備考
1 paint 描画の大元。このメソッドをオーバーライドして、super.paintを呼び出さないと、下記の各メソッドは呼ばれない。
2 paintComponent コンポーネントの描画を行う。
3 paintBorder コンポーネントの枠の描画を行う。Borderクラスの指定による枠線とは別に呼ばれる模様。
4 paintChildren 子コンポーネントの描画を行う。

Graphics2Dの描画範囲は、左上の座標が(0,0)で、サイズはコンポーネントの推奨サイズになる模様。
したがって、コンストラクターの中でコンポーネントに対して推奨サイズをセットしている。
(scala.swing.Componentには推奨サイズをセットするメソッドが無いようなので、peerのメソッドを直接呼び出している)
レイアウトマネージャーに従って大きさが変わるコンポーネントにしたければ、(最小サイズや最大サイズをセットして、)
paint系メソッドの中でコンポーネントのsizeを取得してそれを元に描画すればいい。
(sizeはレイアウトマネージャーの中で(そのコンポーネントの大きさとして)セットされるから)


描画範囲より広い範囲に描きたい場合は、クリップ(描画領域)を大きくする。

  override protected def paintBorder(g: Graphics2D) {
    val r = g.getClipBounds
    g.setClip(r.x - 1, r.y - 1, r.width + 2, r.height + 2)

    val sz = size
    g.drawRect(-1, -1, sz.width + 1, sz.height + 1)
  }

コンポーネントの再描画

クリックなどのイベントによって再描画したい場合はrepaintを呼び出す。
(revalidateとかは呼ばなくても描画されるようだ)

import scala.swing._
object ComponentClickSample extends SimpleSwingApplication {
  override def top = new MainFrame {
    title = "サンプル"
    contents = new FlowPanel {
      contents += new Label("ラベル1") with MyBorder
      contents += new Label("ラベル2") with MyBorder
      contents += new Button("ボタン") with MyBorder
   }
  }
}
trait MyBorder {
  self: Component =>

  var borderFlag = false

  // イベント
  listenTo(this.mouse.clicks)
  reactions += {
    case event.MouseClicked(_, _, _, _, _) =>
      borderFlag = !borderFlag
      repaint()
  }

  // 描画
  override protected def paintBorder(g: Graphics2D) {
    if (borderFlag) {
      g.setColor(java.awt.Color.RED)
      val r = new Rectangle(size)
      g.drawRect(r.x, r.y, r.width - 1, r.height - 1)
    }
  }
}

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