14.8 为净资产应用增加GUI

我们很急切想把这个净资产的应用向朋友炫耀,但是我们知道,GUI会使其更具吸引力。Scala提供了scala.swing程序库,它让用Scala编写Swing变得很容易。我们先了解一些基础知识,然后立即把GUI加到应用上。

scala.swing程序库有个单例对象,叫做SimpleGUIApplication。这个对象已经有了main()方法。它需要你实现top()方法,返回一个所有耳熟能详的Frame。这样,实现一个Swing应用就是简单地继承SimpleGUIApplication,实现top()方法而已。事件如何处理呢?我们可以很有品味地处理它们——抛弃滥俗的监听器方法,用地道的模式匹配处理事件。下面这个例子会有助于我们正确地理解这一切。代码如下:

UsingScala/SampleGUI.scala

  1. import scala.swing._
  2. import event._
  3. object SampleGUI extends SimpleGUIApplication {
  4. def top = new MainFrame {
  5. title = "A Sample Scala Swing GUI"
  6. val label = new Label { text = "------------"}
  7. val button = new Button { text = "Click me" }
  8. contents = new FlowPanel {
  9. contents += label
  10. contents += button
  11. }
  12. listenTo(button)
  13. reactions += {
  14. case ButtonClicked(button) =>
  15. label.text = "You clicked!"
  16. }
  17. }
  18. }

去吧!用scalac SampleGUI.scala编译上面的代码,用scala SampleGUI运行它。如图14-1所示左边是弹出的初始化窗口。右边是点击了按钮的效果。

14.8 为净资产应用增加GUI - 图1

图14-1 示例代码的运行效果

上例从SimpleGUIApplication继承出单例对象SampleGUI,提供了top()方法的实现。这个方法创建了一个实例,它属于一个从MainFrame继承而来的匿名类。MainFrameScala swing程序库的一部分,负责关闭框架,退出应用,还充当着主应用的窗口。因此,不同于Swing JFrame,使用MainFrame消除了处理defaultCloseOperation属性关闭窗口的烦恼。

然后,设置title属性,创建LabelButton的实例。MainFramecontents属性表示主窗口持有的内容,它只能包含一个组件,在这个例子里持有的是FlowPanel的实例。之后,将创建出的标签和按钮添加(使用附加方法+=())到FlowPanelcontents属性。可以想象,FlowPanel对应于AWT/Swingjava.awt.Flowlayout,它会水平地排布其组件,一个接一个。

最后需要处理事件,在这个例子里就是按钮的事件。先调用listenTo()方法,将button注册为事件源;也就是让主窗口监听按钮事件。然后,为reactions属性提供一个偏函数,注册事件处理器。在处理器里,用恰当的case类匹配感兴趣的事件。在这个例子里,就是点击按钮的事件,用ButtonClicked这个case类进行匹配。

现在,让我们把注意力转回到为净资产应用添加GUI上来。有个复杂的地方应该注意一下。创建多个查询价格的actor时,要记住只从主UI所在的线程里更新GUI组件。这是因为Swing的UI组件并不是线程安全的。下面我们开始着手写代码。

完成这个例子时,GUI的样子如图14-2所示。

14.8 为净资产应用增加GUI - 图2

图14-2 例子完成时的GUI

这个表显示了用户持有的每支股票的代码、股份、价格和总价。最终,会在底部看到净资产的值,在顶部看到上次价格更新时间。Update按钮会启动从Web获取数据的动作。

我们写的一部分代码会处理GUI组件。其余的代码会用之前写的StockPriceFinder,向Yahoo服务发送请求,接收应答。一旦得到应答,就会计算每支股票的价格和净资产——这就是我们的业务逻辑。我确定,你愿意把业务逻辑和操作GUI组件的代码分离,保持代码的内聚性。先来看看单例对象NetAssetStockPriceHelper,它处理业务逻辑,扮演着GUI和StockPriceFinder之间的联系人:

UsingScala/NetAssetStockPriceHelper.scala

  1. import scala.actors._
  2. import Actor._
  3. object NetAssetStockPriceHelper {
  4. val symbolsAndUnits = StockPriceFinder.getTickersAndUnits
  5. def getInitialTableValues : Array[Array[Any]] = {
  6. val emptyArrayOfArrayOfAny = new Array[Array[Any]](0,0)
  7. (emptyArrayOfArrayOfAny /: symbolsAndUnits) { (data, element) =>
  8. val (symbol, units) = element
  9. data ++ Array(List(symbol, units, "?", "?").toArray)
  10. }
  11. }
  12. def fetchPrice(updater: Actor) = actor {
  13. val caller = self
  14. symbolsAndUnits.keys.foreach { symbol =>
  15. actor { caller ! (symbol, StockPriceFinder.getLatestClosingPrice(symbol)) }
  16. }
  17. val netWorth = (0.0 /: (1 to symbolsAndUnits.size)) { (worth, index) =>
  18. receiveWithin(10000) {
  19. case (symbol : String, latestClosingPrice: Double) =>
  20. val units = symbolsAndUnits(symbol)
  21. val value = units * latestClosingPrice
  22. updater ! (symbol, units, latestClosingPrice, value)
  23. worth + value
  24. }
  25. }
  26. updater ! netWorth
  27. }
  28. }

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

  1. import scala.swing._
  2. import event._
  3. import scala.actors._
  4. import Actor._
  5. import java.awt.Color
  6. object NetAssetAppGUI extends SimpleGUIApplication {
  7. def top = mainFrame

这里创建了一个叫NetAssetAPPGUI单例对象,继承自SimpleGUIApplication。定义了必需的top()方法。返回一个值mainFrame,稍后定义。我们看看如何创建一个MainFrame实例:

UsingScala/NetAssetAppGUI.scala

  1. val mainFrame = new MainFrame {
  2. title = "Net Asset"
  3. val dateLabel = new Label { text = "Last updated: ----- " }
  4. val valuesTable = new Table(
  5. NetAssetStockPriceHelper.getInitialTableValues,
  6. Array("Ticker", "Units", "Price", "Value")) {
  7. showGrid = true
  8. gridColor = Color.BLACK
  9. }
  10. val updateButton = new Button { text = "Update" }
  11. val netAssetLabel = new Label { text = "Net Asset: ????" }

这里设置了预期的title值,创建了四个所需的组件:两个标签,一个表格和一个按钮。创建标签和按钮很容易。这里关注一下表格。我们创建了一个scala.swing. Table实例,给它的构造函数传了两个实参。第一个实参是表格的初始数据,从NetAssetStockPriceHelpergetInitialTableValues()方法里获得。第二个实参由列头的名字组成。

记住,不能在主窗口放置多个组件。因此,把这些组件放到BoxPanel里,同时,把BoxPanel放到主框架的contents里,如下所示:

UsingScala/NetAssetAppGUI.scala

  1. contents = new BoxPanel(Orientation.Vertical) {
  2. contents += dateLabel
  3. contents += valuesTable
  4. contents += new ScrollPane(valuesTable)
  5. contents += new FlowPanel {
  6. contents += updateButton
  7. contents += netAssetLabel
  8. }
  9. }

BoxPanel会把传给它的组件堆起来(因为方向是垂直的)。把dateLabel放在顶部,之后跟着表格,底部放着持有另一个标签和按钮的FlowPanel

我们几乎完工了。只差处理事件和更新UI:

UsingScala/NetAssetAppGUI.scala

  1. listenTo(updateButton)
  2. reactions += {
  3. case ButtonClicked(button) =>
  4. button.enabled = false
  5. NetAssetStockPriceHelper fetchPrice uiUpdater
  6. }

在上面的代码里,订阅了按钮的事件,添加了一个处理器。在处理器里,先禁用Update按钮,然后发出请求,让NetAssetStockPriceHelper获取价格,计算价值。我们给NetAssetStockPriceHelper提供了一个uiUpdater,这是一个稍后会创建的actor。记住,fetchPrice()会返回一个actor,请求会在一个单独的线程里处理,上面的调用是非阻塞的。与此同时,NetAssetStockPriceHelper会并发地请求股票价格,计算价值。第一个价格一到,就会向uiUpdater发送消息。所以,最好快点创建一个uiUpdater,然后就能开始更新UI了。

UsingScala/NetAssetAppGUI.scala

  1. val uiUpdater = new Actor {
  2. def act = {
  3. loop {
  4. react {
  5. case (symbol : String, units : Int, price : Double, value :
  6. Double) =>updateTable(symbol, units, price, value)
  7. case netAsset =>
  8. netAssetLabel.text = "Net Asset: " + netAsset
  9. dateLabel.text = "Last updated: " + new java.util.Date()
  10. updateButton.enabled = true
  11. }
  12. }
  13. }
  14. override protected def scheduler() = new SingleThreadedScheduler
  15. }
  16. uiUpdater.start()

uiUpdater指向Actor的一个匿名实例。一旦调用start(),它就会在主事件分发线程里运行,因为我们改写了scheduler()方法,返回SingleThreadedScheduler的实例。在act()方法里,接收NetAssetStockPriceHelper发送的消息,恰当地更新UI组件。最后缺失的一段就是updateTable()方法了,它会用接收的数据更新表格。下面就是这个方法,到括号结尾,我们就完成了要开发的代码:

UsingScala/NetAssetAppGUI.scala

  1. def updateTable(symbol: String, units : Int, price : Double, value :
  2. Double) {
  3. for(i <- 0 until valuesTable.rowCount) {
  4. if (valuesTable(i, 0) == symbol) {
  5. valuesTable(i, 2) = price
  6. valuesTable(i, 3) = value
  7. }
  8. }
  9. }
  10. }
  11. }

上面的方法只是简单的对表循环,定位感兴趣的股票代码,更新这一行。如果需要改进这一点的话,可以设计出其他方式在表中查找。比如,组装表格之初,把行号存到map里,然后,在这个map里查找,快速地定位到行。

现在运行这个应用,我们会注意到,股票价格和价值到达之后就会更新,如图14-3所示:

14.8 为净资产应用增加GUI - 图3

图14-3 更新股票价格

一旦接收到所有价格,净资产和时间都会更新,如图14-4所示:

14.8 为净资产应用增加GUI - 图4

图14-4 更新净资产和时间

迄今为止,我们创建的代码只走了正常路径。如果网络链接正常,服务及时响应,一切顺利。但现实往往不尽如人意。我们要在actor里处理异常,将失败传回给uiUpdater actor,以便在UI上显示消息。为了做到这一点,还需要另外增加一个case语句,接收异常消息,当然,actor遇到错误情形,需要发送这些消息。

为了方便,我在这里再次列出完整的UI代码——也许,你会惊讶于代码的简洁:

UsingScala/NetAssetAppGUI.scala

  1. import scala.swing._
  2. import event._
  3. import scala.actors._
  4. import Actor._
  5. import java.awt.Color
  6. object NetAssetAppGUI extends SimpleGUIApplication {
  7. def top = mainFrame
  8. val mainFrame = new MainFrame {
  9. title = "Net Asset"
  10. val dateLabel = new Label { text = "Last updated: ----- " }
  11. val valuesTable = new Table(
  12. NetAssetStockPriceHelper.getInitialTableValues,
  13. Array("Ticker", "Units", "Price", "Value")) {
  14. showGrid = true
  15. gridColor = Color.BLACK
  16. }
  17. val updateButton = new Button { text = "Update" }
  18. val netAssetLabel = new Label { text = "Net Asset: ????" }
  19. contents = new BoxPanel(Orientation.Vertical) {
  20. contents += dateLabel
  21. contents += valuesTable
  22. contents += new ScrollPane(valuesTable)
  23. contents += new FlowPanel {
  24. contents += updateButton
  25. contents += netAssetLabel
  26. }
  27. }
  28. listenTo(updateButton)
  29. reactions += {
  30. case ButtonClicked(button) =>
  31. button.enabled = false
  32. NetAssetStockPriceHelper fetchPrice uiUpdater
  33. }
  34. val uiUpdater = new Actor {
  35. def act={
  36. loop {
  37. react {
  38. case (symbol : String, units : Int, price : Double, value :
  39. Double) =>updateTable(symbol, units, price, value)
  40. case netAsset =>
  41. netAssetLabel.text = "Net Asset: " + netAsset
  42. dateLabel.text = "Last updated: " + new java.util.Date()
  43. updateButton.enabled = true
  44. }
  45. }
  46. }
  47. override protected def scheduler() = new SingleThreadedScheduler
  48. }
  49. uiUpdater.start()
  50. def updateTable(symbol: String, units : Int, price : Double,
  51. value : Double) {
  52. for(i <- 0 until valuesTable.rowCount) {
  53. if (valuesTable(i, 0) == symbol) {
  54. valuesTable(i, 2) = price
  55. valuesTable(i, 3) = value
  56. }
  57. }
  58. }
  59. }
  60. }

在本章里,我们亲眼目睹了Scala的简洁和表现力,享受到了模式匹配、XML处理和函数式风格带来的裨益,也见识到了并发API的益处和简单。我们已然准备就绪,将这些益处带入真实世界的项目之中。谢谢您阅读本书!