Warning
This section is not ready yet. Working on it!
This example shows a simple GUI where there is no inheritance. Clicking the button increments the number in the text field. If the user has changed it so that it is not an integer, it is set back to 0.
Those familiar with Java will notice a lot of similarities. This is because the scala.swing library is a wrapper around Java’s Swing library. The ideas are similar, but the way in which you interact with them has been changed to match the Scala style.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | import swing._
val frame = new MainFrame
val field = new TextField("0")
val button = Button("Increment") {
try {
field.text = (field.text.toInt+1).toString
} catch {
case ex:
NumberFormatException => field.text = "0"
}
}
val bp = new BorderPanel
import BorderPanel.Position._
bp.layout += field -> North
bp.layout += button -> Center
frame.contents = bp
frame.centerOnScreen
frame.open
|
Scala supports simplified exception handling through its scala.util.Try wrapper type. This is an important Scala idiom for representing a computation that either succeeds with a result value or fails with an exception.
For example, say you want to validate and convert a text field in your UI from string to integer. You could write this simple conversion function to do so:
scala> def toInteger(s: String) = scala.util.Try(s.toInt)
toInteger: (s: String)scala.util.Try[Int]
res0: scala.util.Try[Int] = Failure(java.lang.NumberFormatException: For input string: "blah")
scala> toInteger("35")
res1: scala.util.Try[Int] = Success(35)
Then you can use getOrElse to process the enclosed value and, if the Try value represents failure, return the given default value (as you see above when we tried to validate the string “blah”).
scala> toInteger("35").getOrElse(-1)
res2: Int = 35
scala> toInteger("blah").getOrElse(-1)
res3: Int = -1
It’s clear that being able to validate input efficiently is something that excites us. It certainly makes UI development more reliable and resilient to failures. (We’ve had more than our share of fun chasing down validation bugs in web and mobile app development. Most of the time it is caused by unnecessarily complex validation logic.)
You can see how this plays out in a slightly reworked version of the code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | import swing._
import scala.util.Try
val frame = new MainFrame
val field = new TextField("0")
val button = Button("Increment") {
val attempt = Try(field.text.toInt)
field.text = (attempt.getOrElse(-1)+1).toString
}
val bp = new BorderPanel
import BorderPanel.Position._
bp.layout += field -> North
bp.layout += button -> Center
frame.contents = bp
frame.centerOnScreen
frame.open
|
This example shows how you can override the paint method to make a custom drawing. It also shows interactions with the mouse.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | import swing._
import event._
import java.awt.{Color,Shape}
import java.awt.geom._
var dots = List.empty[Shape]
val panel = new Panel {
override def paint(g:Graphics2D) {
g.setPaint(Color.white)
g.fillRect(0,0,size.width,size.height)
g.setPaint(Color.black)
for(s <- dots) g.fill(s)
}
listenTo(mouse.clicks,mouse.moves)
reactions += {
case mc: MouseClicked =>
dots ::= new Ellipse2D.Double(mc.point.x-2,mc.point.y-2,5,5)
repaint
case mc: MouseDragged =>
dots ::= new Ellipse2D.Double(mc.point.x-2,mc.point.y-2,5,5)
repaint
}
}
val frame = new MainFrame {
contents = panel
size = new Dimension(600,600)
centerOnScreen
}
frame.open
|
Here’s what the output looks like when you drag the mouse quasi-randomly on the blank canvas that first comes appears. (Your output may vary!)
This is a large GUI example. There are two lists with a text fields and some buttons. The first list is populated by the text field and the buttons move things between lists or remove them from the second list.
The populating from the text field demonstrates how you listen to GUI elements and react to them. The behavior of the lists shows how collection methods can play a role in GUIs.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | import swing._
import event._
val list1 = new ListView[String]()
val list2 = new ListView[String]()
val buttons = new FlowPanel {
contents += Button("<-") {
list1.listData ++= list2.selection.items
list2.listData = list2.listData.diff(list2.selection.items)
}
contents += Button("->") {
list2.listData ++= list1.selection.items
list1.listData = list1.listData.diff(list1.selection.items)
}
contents += Button("Remove") {
list2.listData = list2.listData.diff(list2.selection.items)
}
}
val field = new TextField() {
listenTo(this)
reactions += {
case ed:
EditDone =>
list1.listData :
+= text
text = ""
}
}
val frame = new MainFrame {
contents = new BorderPanel {
import BorderPanel.Position._
layout += field -> North
layout += new ScrollPane(list1) -> West
layout += new ScrollPane(list2) -> East
layout += buttons -> Center
}
size = new Dimension(600,500)
centerOnScreen
}
frame.open
|
This program is a little implementation of asteroids. It shows keyboard events and the use of case classes to group data together.
We start by importing varius dependencies. This shows how you can take advantage of existing Java libraries.
Case classes are used to maintain information about key elements of the game, notably the asteroids and bullets. Although you see the word class here, we’re primarily using class to aggregate the data (think about C struct but even nicer). These are used to maintain two typesafe lists of asteroids and bullets, respectively, with type List[Asteroid] and List[Bullet].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | import swing._
import event._
import java.awt.{Color,Shape}
import java.awt.geom._
import javax.swing.Timer
case class Asteroid(x:Double,y:Double,vx:Double,vy:Double,size:Double)
case class Bullet(x:Double,y:Double,vx:Double,vy:Double,age:Int)
val windowSize = 600
val shipSize = 6
var asteroids = List.fill(5){
val theta = math.random*math.Pi*2
Asteroid(windowSize/2+math.cos(theta)*windowSize/4,
windowSize/2+math.sin(theta)*windowSize/4,
math.random-0.5,math.random-0.5,50)
}
var bullets = List[Bullet]()
var shipX = windowSize/2.0
var shipY = windowSize/2.0
var heading = 0.0
var shipVx = 0.0
var shipVy = 0.0
var leftDown = false
var rightDown = false
|
The wrap() method does what you might be thinking it does. It takes an x or y coordinate (even though you only see x in the parameter name) of a given asteroid and ensures it falls within the bounds of the window. Note that this references an external “global” value windowSize. Because this value is immutable, there is no risk of a side effect. (This could be called the revival of the const from C/C++ and Pascal but in a more modern formulation.)
1 2 3 4 5 6 | def wrap(x:Double):Double = {
var nx = x
while(nx < 0) nx += windowSize
while(nx > windowSize) nx -= windowSize
nx
}
|
The definition of the panel is where the actual drawing (and redrawing) of the game takes place. It also shows how to clearly separate the drawing from the reactions to events of interest (keyboard and mouse). Notably, we can handle these events without having to use classes. This allows us to stay focused on design principles instead of the vagaries of event objects and interfaces (even though these details are still present, being able to match the event’s type allows us to avoid premature complexity from a student perspective.)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | val panel = new Panel {
override def paint(g:Graphics2D) {
g.setPaint(Color.black)
g.fillRect(0,0,size.width,size.height)
g.setPaint(Color.lightGray)
for(a <- asteroids) g.fill(new Ellipse2D.Double(a.x-a.size/2,a.y-a.size/2,a.size,a.size))
g.setPaint(Color.red)
for(b <- bullets) g.fill(new Rectangle2D.Double(b.x,b.y,2,2))
g.setPaint(Color.blue)
g.fill(new Ellipse2D.Double(shipX-shipSize,shipY-shipSize,shipSize*2,shipSize*2))
g.setPaint(Color.green)
g.draw(new Ellipse2D.Double(shipX-shipSize,shipY-shipSize,shipSize*2,shipSize*2))
g.fill(new Ellipse2D.Double(shipX+(shipSize+2)*math.cos(heading)-2,shipY+(shipSize+2)*math.sin(heading)-2,4,4))
}
listenTo(keys,mouse.clicks)
reactions += {
case kp:KeyPressed =>
if(kp.key == Key.Left) leftDown = true
else if(kp.key == Key.Right) rightDown = true
else if(kp.key == Key.Up) {
shipVx += math.cos(heading)*0.2
shipVy += math.sin(heading)*0.2
} else if(kp.key == Key.Down) {
shipVx -= math.cos(heading)*0.2
shipVy -= math.sin(heading)*0.2
} else if(kp.key == Key.Space) {
bullets ::= Bullet(shipX+(shipSize+2)*math.cos(heading),shipY+(shipSize+2)*math.sin(heading),shipVx+3*math.cos(heading),shipVy+3*math.sin(heading),0)
}
case kp:KeyReleased =>
if(kp.key == Key.Left) leftDown = false
else if(kp.key == Key.Right) rightDown = false
case me:MouseEntered => requestFocus
}
preferredSize = new Dimension(windowSize,windowSize)
}
|
A timer is particularly useful in game design, where you want to have self-updating without user interaction. In the case of this game, whether or not the user is doing anything, asteroids continue moving, subject to their velocities. Same for bullets. There is also logic to determine collisions and whether the ship is destroyed (which ends the game).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | val timer:Timer = new Timer(10,Swing.ActionListener(e => {
if(leftDown) heading -= math.Pi/40
if(rightDown) heading += math.Pi/40
asteroids = asteroids.map(a => {
a.copy(x = wrap(a.x+a.vx), y = wrap(a.y+a.vy))
})
shipX = wrap(shipX+shipVx)
shipY = wrap(shipY+shipVy)
var hit = List[Asteroid]()
bullets = bullets.map(b => {
b.copy(x = wrap(b.x+b.vx), y = wrap(b.y+b.vy), age = b.age+1)
}).filter(b => {
b.age<100 && asteroids.forall(a => {
val dx = b.x-a.x
val dy = b.y-a.y
val dsqr = dx*dx+dy*dy
val isHit = dsqr < a.size*a.size/4
if(isHit) hit ::= a
!isHit
})
})
asteroids = asteroids.flatMap(a => {
if(hit.contains(a)) {
if(a.size <=10) List()
else List.fill(4)(Asteroid(a.x+(math.random-0.5)*a.size,a.y+(math.random-0.5)*a.size,a.vx+math.random-0.5,a.vy+math.random-0.5,a.size/2))
} else List(a)
})
if(asteroids.exists(a => {
val dx = shipX-a.x
val dy = shipY-a.y
val dsqr = dx*dx+dy*dy
dsqr < (shipSize+a.size/2)*(shipSize+a.size/2)
})) timer.stop()
panel.repaint
}))
|
Setup of the game is fairly concise. Create the main (Swing) frame and set the desired properties. In particular, we embed the created panel (a method that is familiar to anyone who has taught Java based UIs using AWT or Swing) and disallow frame resizing. We also center the frame on the screen (if supported). Then we display the frame and start the timer.
While there are a few details about Swing to know here, much of this code is common to all Swing application development, so it is eminently teachable–and you can always point students to the basic documentation for Java to learn the details.
1 2 3 4 5 6 7 8 9 | val frame = new MainFrame {
contents = panel
resizable = false
centerOnScreen
}
frame.open
panel.requestFocus
timer.start
|
Here’s a screenshot of the game: