14.8 为净资产应用增加GUI
我们很急切想把这个净资产的应用向朋友炫耀,但是我们知道,GUI会使其更具吸引力。Scala提供了scala.swing
程序库,它让用Scala编写Swing变得很容易。我们先了解一些基础知识,然后立即把GUI加到应用上。
scala.swing
程序库有个单例对象,叫做SimpleGUIApplication
。这个对象已经有了main()
方法。它需要你实现top()
方法,返回一个所有耳熟能详的Frame
。这样,实现一个Swing应用就是简单地继承SimpleGUIApplication
,实现top()
方法而已。事件如何处理呢?我们可以很有品味地处理它们——抛弃滥俗的监听器方法,用地道的模式匹配处理事件。下面这个例子会有助于我们正确地理解这一切。代码如下:
UsingScala/SampleGUI.scala
import scala.swing._
import event._
object SampleGUI extends SimpleGUIApplication {
def top = new MainFrame {
title = "A Sample Scala Swing GUI"
val label = new Label { text = "------------"}
val button = new Button { text = "Click me" }
contents = new FlowPanel {
contents += label
contents += button
}
listenTo(button)
reactions += {
case ButtonClicked(button) =>
label.text = "You clicked!"
}
}
}
去吧!用scalac SampleGUI.scala
编译上面的代码,用scala SampleGUI
运行它。如图14-1所示左边是弹出的初始化窗口。右边是点击了按钮的效果。
图14-1 示例代码的运行效果
上例从SimpleGUIApplication
继承出单例对象SampleGUI
,提供了top()
方法的实现。这个方法创建了一个实例,它属于一个从MainFrame
继承而来的匿名类。MainFrame
是Scala swing
程序库的一部分,负责关闭框架,退出应用,还充当着主应用的窗口。因此,不同于Swing JFrame
,使用MainFrame
消除了处理defaultCloseOperation
属性关闭窗口的烦恼。
然后,设置title
属性,创建Label
和Button
的实例。MainFrame
的contents
属性表示主窗口持有的内容,它只能包含一个组件,在这个例子里持有的是FlowPanel
的实例。之后,将创建出的标签和按钮添加(使用附加方法+=()
)到FlowPanel
的contents
属性。可以想象,FlowPanel
对应于AWT/Swing
的java.awt.Flowlayout
,它会水平地排布其组件,一个接一个。
最后需要处理事件,在这个例子里就是按钮的事件。先调用listenTo()
方法,将button
注册为事件源;也就是让主窗口监听按钮事件。然后,为reactions
属性提供一个偏函数,注册事件处理器。在处理器里,用恰当的case
类匹配感兴趣的事件。在这个例子里,就是点击按钮的事件,用ButtonClicked
这个case
类进行匹配。
现在,让我们把注意力转回到为净资产应用添加GUI上来。有个复杂的地方应该注意一下。创建多个查询价格的actor时,要记住只从主UI所在的线程里更新GUI组件。这是因为Swing的UI组件并不是线程安全的。下面我们开始着手写代码。
完成这个例子时,GUI的样子如图14-2所示。
图14-2 例子完成时的GUI
这个表显示了用户持有的每支股票的代码、股份、价格和总价。最终,会在底部看到净资产的值,在顶部看到上次价格更新时间。Update
按钮会启动从Web获取数据的动作。
我们写的一部分代码会处理GUI组件。其余的代码会用之前写的StockPriceFinder
,向Yahoo服务发送请求,接收应答。一旦得到应答,就会计算每支股票的价格和净资产——这就是我们的业务逻辑。我确定,你愿意把业务逻辑和操作GUI组件的代码分离,保持代码的内聚性。先来看看单例对象NetAssetStockPriceHelper
,它处理业务逻辑,扮演着GUI和StockPriceFinder
之间的联系人:
UsingScala/NetAssetStockPriceHelper.scala
import scala.actors._
import Actor._
object NetAssetStockPriceHelper {
val symbolsAndUnits = StockPriceFinder.getTickersAndUnits
def getInitialTableValues : Array[Array[Any]] = {
val emptyArrayOfArrayOfAny = new Array[Array[Any]](0,0)
(emptyArrayOfArrayOfAny /: symbolsAndUnits) { (data, element) =>
val (symbol, units) = element
data ++ Array(List(symbol, units, "?", "?").toArray)
}
}
def fetchPrice(updater: Actor) = actor {
val caller = self
symbolsAndUnits.keys.foreach { symbol =>
actor { caller ! (symbol, StockPriceFinder.getLatestClosingPrice(symbol)) }
}
val netWorth = (0.0 /: (1 to symbolsAndUnits.size)) { (worth, index) =>
receiveWithin(10000) {
case (symbol : String, latestClosingPrice: Double) =>
val units = symbolsAndUnits(symbol)
val value = units * latestClosingPrice
updater ! (symbol, units, latestClosingPrice, value)
worth + value
}
}
updater ! netWorth
}
}
getInitialTableValues()
方法返回一个二维数组,用初始值填充表格,包括股票符号和股份。因为最初的价格和值是未知的,方法返回?
填充表格。
fetchPrice()
方法以UI更新的actor为参数,这个方法的返回值也是个actor。参数actor在UI端,负责更新UI线程里的UI组件。首先,它发送并发请求到StockPriceFinder
,获取各个股票的价格。其次,它一收到应答,就会计算股票价格,将其发送给更新UI的actor,这样就可以立即更新UI。而且,它会继续接收其余的股票价格,确定净资产。当接收到所有的价格,它会把净资产发送到更新UI的actor,这样就可以显示这个数量。通读这个方法,我们会注意到,它类似于之前在14.7节,“让净资产应用并发”,见到的代码。主要的差异在于后者打印出结果,fetchPrice()
则把细节发送给更新UI的actor,显示到GUI上。
现在,剩下的唯一任务是编写GUI代码,同NetAssetStockPriceHelper
打交道。先从这个类的定义开始:
UsingScala/NetAssetAppGUI.scala
import scala.swing._
import event._
import scala.actors._
import Actor._
import java.awt.Color
object NetAssetAppGUI extends SimpleGUIApplication {
def top = mainFrame
这里创建了一个叫NetAssetAPPGUI
单例对象,继承自SimpleGUIApplication
。定义了必需的top()
方法。返回一个值mainFrame
,稍后定义。我们看看如何创建一个MainFrame
实例:
UsingScala/NetAssetAppGUI.scala
val mainFrame = new MainFrame {
title = "Net Asset"
val dateLabel = new Label { text = "Last updated: ----- " }
val valuesTable = new Table(
NetAssetStockPriceHelper.getInitialTableValues,
Array("Ticker", "Units", "Price", "Value")) {
showGrid = true
gridColor = Color.BLACK
}
val updateButton = new Button { text = "Update" }
val netAssetLabel = new Label { text = "Net Asset: ????" }
这里设置了预期的title
值,创建了四个所需的组件:两个标签,一个表格和一个按钮。创建标签和按钮很容易。这里关注一下表格。我们创建了一个scala.swing. Table
实例,给它的构造函数传了两个实参。第一个实参是表格的初始数据,从NetAssetStockPriceHelper
的getInitialTableValues()
方法里获得。第二个实参由列头的名字组成。
记住,不能在主窗口放置多个组件。因此,把这些组件放到BoxPanel
里,同时,把BoxPanel
放到主框架的contents
里,如下所示:
UsingScala/NetAssetAppGUI.scala
contents = new BoxPanel(Orientation.Vertical) {
contents += dateLabel
contents += valuesTable
contents += new ScrollPane(valuesTable)
contents += new FlowPanel {
contents += updateButton
contents += netAssetLabel
}
}
BoxPanel
会把传给它的组件堆起来(因为方向是垂直的)。把dateLabel
放在顶部,之后跟着表格,底部放着持有另一个标签和按钮的FlowPanel
。
我们几乎完工了。只差处理事件和更新UI:
UsingScala/NetAssetAppGUI.scala
listenTo(updateButton)
reactions += {
case ButtonClicked(button) =>
button.enabled = false
NetAssetStockPriceHelper fetchPrice uiUpdater
}
在上面的代码里,订阅了按钮的事件,添加了一个处理器。在处理器里,先禁用Update按钮,然后发出请求,让NetAssetStockPriceHelper
获取价格,计算价值。我们给NetAssetStockPriceHelper
提供了一个uiUpdater
,这是一个稍后会创建的actor。记住,fetchPrice()
会返回一个actor,请求会在一个单独的线程里处理,上面的调用是非阻塞的。与此同时,NetAssetStockPriceHelper
会并发地请求股票价格,计算价值。第一个价格一到,就会向uiUpdater
发送消息。所以,最好快点创建一个uiUpdater
,然后就能开始更新UI了。
UsingScala/NetAssetAppGUI.scala
val uiUpdater = new Actor {
def act = {
loop {
react {
case (symbol : String, units : Int, price : Double, value :
Double) =>updateTable(symbol, units, price, value)
case netAsset =>
netAssetLabel.text = "Net Asset: " + netAsset
dateLabel.text = "Last updated: " + new java.util.Date()
updateButton.enabled = true
}
}
}
override protected def scheduler() = new SingleThreadedScheduler
}
uiUpdater.start()
值uiUpdater
指向Actor
的一个匿名实例。一旦调用start()
,它就会在主事件分发线程里运行,因为我们改写了scheduler()
方法,返回SingleThreadedScheduler
的实例。在act()
方法里,接收NetAssetStockPriceHelper
发送的消息,恰当地更新UI组件。最后缺失的一段就是updateTable()
方法了,它会用接收的数据更新表格。下面就是这个方法,到括号结尾,我们就完成了要开发的代码:
UsingScala/NetAssetAppGUI.scala
def updateTable(symbol: String, units : Int, price : Double, value :
Double) {
for(i <- 0 until valuesTable.rowCount) {
if (valuesTable(i, 0) == symbol) {
valuesTable(i, 2) = price
valuesTable(i, 3) = value
}
}
}
}
}
上面的方法只是简单的对表循环,定位感兴趣的股票代码,更新这一行。如果需要改进这一点的话,可以设计出其他方式在表中查找。比如,组装表格之初,把行号存到map
里,然后,在这个map
里查找,快速地定位到行。
现在运行这个应用,我们会注意到,股票价格和价值到达之后就会更新,如图14-3所示:
图14-3 更新股票价格
一旦接收到所有价格,净资产和时间都会更新,如图14-4所示:
图14-4 更新净资产和时间
迄今为止,我们创建的代码只走了正常路径。如果网络链接正常,服务及时响应,一切顺利。但现实往往不尽如人意。我们要在actor里处理异常,将失败传回给uiUpdater
actor,以便在UI上显示消息。为了做到这一点,还需要另外增加一个case
语句,接收异常消息,当然,actor遇到错误情形,需要发送这些消息。
为了方便,我在这里再次列出完整的UI代码——也许,你会惊讶于代码的简洁:
UsingScala/NetAssetAppGUI.scala
import scala.swing._
import event._
import scala.actors._
import Actor._
import java.awt.Color
object NetAssetAppGUI extends SimpleGUIApplication {
def top = mainFrame
val mainFrame = new MainFrame {
title = "Net Asset"
val dateLabel = new Label { text = "Last updated: ----- " }
val valuesTable = new Table(
NetAssetStockPriceHelper.getInitialTableValues,
Array("Ticker", "Units", "Price", "Value")) {
showGrid = true
gridColor = Color.BLACK
}
val updateButton = new Button { text = "Update" }
val netAssetLabel = new Label { text = "Net Asset: ????" }
contents = new BoxPanel(Orientation.Vertical) {
contents += dateLabel
contents += valuesTable
contents += new ScrollPane(valuesTable)
contents += new FlowPanel {
contents += updateButton
contents += netAssetLabel
}
}
listenTo(updateButton)
reactions += {
case ButtonClicked(button) =>
button.enabled = false
NetAssetStockPriceHelper fetchPrice uiUpdater
}
val uiUpdater = new Actor {
def act={
loop {
react {
case (symbol : String, units : Int, price : Double, value :
Double) =>updateTable(symbol, units, price, value)
case netAsset =>
netAssetLabel.text = "Net Asset: " + netAsset
dateLabel.text = "Last updated: " + new java.util.Date()
updateButton.enabled = true
}
}
}
override protected def scheduler() = new SingleThreadedScheduler
}
uiUpdater.start()
def updateTable(symbol: String, units : Int, price : Double,
value : Double) {
for(i <- 0 until valuesTable.rowCount) {
if (valuesTable(i, 0) == symbol) {
valuesTable(i, 2) = price
valuesTable(i, 3) = value
}
}
}
}
}
在本章里,我们亲眼目睹了Scala的简洁和表现力,享受到了模式匹配、XML处理和函数式风格带来的裨益,也见识到了并发API的益处和简单。我们已然准备就绪,将这些益处带入真实世界的项目之中。谢谢您阅读本书!