我正在使用ScalaFX并试图了解它是如何工作的。作为一种努力(不是我在生产中要做的事情),我想要一种获得窗口标题的方法。
下面是我的Graph.scala文件:
package graphing
import scalafx.application.JFXApp
import scalafx.scene.Scene
import scalafx.scene.paint.Color
class Graph {
val app = new JFXApp {
stage = new JFXApp.PrimaryStage {
title = "First GUI"
scene = new Scene {
fill = Color.Coral
}
}
}
def getTitle() = {
app.stage.getTitle
}
def generateChart(args: Array[String]) = app.main(args)
}下面是我的驱动程序对象,它使用了这个Graph:
package graphing
import graphing.Graph
object Driver extends App {
val graph = new Graph
println(graph.getTitle())
graph.generateChart(args)
}但是,由于行的原因,这不起作用。
println(graph.getTitle()
引发的错误:

有人能解释一下发生了什么事,以及我如何在这里实现我的目标吗?
发布于 2020-05-31 19:28:21
这里的问题涉及JavaFX (因此也是ScalaFX)初始化。
初始化JavaFX是一项复杂的业务。(事实上,我只是最近才知道事情比我原先想象的还要复杂。有关进一步的背景信息,请参阅这个最近的回答 on StackOverflow。幸运的是,您的问题更容易解决。)
ScalaFX极大地简化了JavaFX初始化,但要求将JFXApp特性用作object定义的一部分。
JFXApp包含一个main方法,它必须是应用程序的起点;正是这个方法处理了为您初始化JavaFX的复杂性。
在您的示例中,您的Driver对象扩展了scala.App,因此App的(因此,也是Driver的) main方法成为您自己的应用程序的起点。对于常规命令行接口(CLI)应用程序来说,这很好,但是如果没有大量额外的复杂性,它就不能与ScalaFX/JavaFX应用程序一起使用。
在您的代码中,JFXApp的main方法从不执行,因为它被定义为类成员,它不是Scala object的main方法,因此也不是JVM自动执行的候选方法。您确实从Graph.generateChart()方法手动调用它,但该方法本身直到您试图获得场景的标题之后才被调用,因此作为阶段的NPE还没有被初始化。
如果将graph.generateChart(args)调用放在println(graph.getTitle())语句之前会怎样?这能解决问题吗?遗憾的是,没有。
这就是为什么。
JFXApp还执行了另一项神奇的操作:它在object应用程序线程(JAT)上为其object(以及由该对象扩展的任何其他classes,但不是针对扩展traits)执行构造代码。这一点很重要:只有在JAT上执行的代码才能直接与JavaFX交互(即使通过ScalaFX)。如果尝试对任何其他线程(包括应用程序的主线程)执行JavaFX操作,则会得到异常。
(这种魔力依赖于不受欢迎的Scala特性scala.DelayedInit,它已经从Scala3.0的库(又名Dotty )中删除,因此将来需要一种不同的机制。然而,值得阅读文献资料的这一特性作为进一步的背景。)
因此,当Driver的构造代码调用graph.generateChart(args)时,它会导致JavaFX被初始化,启动JAT,并在其上执行Graph的构造代码。然而,当Driver的构造函数调用println(graph.getTitle())时(该构造函数仍在主线程上执行),存在两个问题:
Graph的构造代码可能已经执行,也可能没有执行,因为它是在另一个线程上执行的。(这个问题被称为竞争条件,因为在试图调用println(graph.getTitle())的主线程与试图初始化graph实例的JAT之间存在竞争。)您可能在某些情况下赢得比赛,但也会经常失败。下面是您的应用程序工作的推荐方法:
package graphing
import scalafx.application.JFXApp
import scalafx.scene.Scene
import scalafx.scene.paint.Color
object GraphDriver
extends JFXApp {
// This executes at program startup, automatically, on the JAT.
stage = new JFXApp.PrimaryStage {
title = "First GUI"
scene = new Scene {
fill = Color.Coral
}
}
// Print the title. Works, because we're executing on the JAT. If we're NOT on the JAT,
// Then getTitle() would need to be called via scalafx.application.Platform.runLater().
println(getTitle())
// Retrieve the title of the stage. Should equal "First GUI".
//
// It's guaranteed that "stage" will be initialized and valid when called.
def getTitle() = stage.title.value
}请注意,我已经将您的Graph类和Driver对象组合成一个对象GraphDriver。虽然我不确定您的应用程序在体系结构上需要什么样的外观,但对于您来说,这应该是一个好的起点。
还请注意,根本不使用scala.App。
调用GraphDriver.getTitle()时要小心:这段代码需要在JAT上执行。执行可能在不同线程上运行的任何代码的标准解决方法是将其按名称传递给scalafx.application.Platform.runLater()。例如:
import scalafx.application.Platform
// ...
Platform.runLater(println(ObjectDriver.getTitle()))
// ...https://stackoverflow.com/questions/62109858
复制相似问题