ScalaFx子层次结构和强制转换/实例引用



我想知道这是否是ScalaFx的最佳方式:GUI由一堆节点组成,我从SQL-DB中吸取内容。Main Pane是一个由几百个元素组成的FlowPane。每个元素由四个层次结构组成(参见描述层次的数字):

1          2          3              4
VBox -+-> VBox ---> StackPane -+-> ImageView
+-> Label                +-> Rectangle

根据我的经验,我可以访问不同级别的节点及其属性。Ie.I可以通过更改ImageView节点下方的矩形颜色来向用户提供反馈,因为复合元素是通过鼠标单击或ContextMenu选择的。

我可以直接访问Rectangle属性,但很容易出错,因为列表引用children.get(0)直接依赖于子节点的顺序,因为节点位于父节点中。

val lvone = vbnode.children  // VBox (main)
val lvtwo = lvone.get(0)  // VBox
val lvthree = lvtwo.asInstanceOf[javafx.scene.layout.VBox].children.get(0)  // StackPane
val lvfour = lvthree.asInstanceOf[javafx.scene.layout.StackPane].children.get(0)  // Rectangle
if (lvfour.isInstanceOf[javafx.scene.shape.Rectangle]) lvfour.asInstanceOf[javafx.scene.shape.Rectangle].style = "-fx-fill: #a001fc;"
println("FOUR IS:"+lvfour.getClass) 

下面的示例演示了对节点层次结构中元素的"更安全"访问(节点层次结构的创建是在相当烦人的代码结构中进行的,因此不包括在内):

val levelone = vbnode.children   
println("LV1 Node userData:"+vbnode.userData)  // my database reference for the main / container element
println("LV1 Parent children class:"+levelone.get(0).getClass) // class javafx.scene.layout.VBox
for (leveltwo <- levelone) {
println("LV2 Children Class:"+leveltwo.getClass)
println("LV2 Children Class Simple Name:"+leveltwo.getClass.getSimpleName)  // VBox
if (leveltwo.getClass.getSimpleName == "VBox") {
leveltwo.style = "-fx-border-width: 4px;" +
"-fx-border-color: blue yellow blue yellow;"
for (levelthree <- leveltwo.asInstanceOf[javafx.scene.layout.VBox].children) {
println("LV3 children:"+levelthree.getClass.getName)
if (levelthree.getClass.getSimpleName == "StackPane") {
for (levelfour <- levelthree.asInstanceOf[javafx.scene.layout.StackPane].children) {
println("LV4 children:"+levelfour.getClass.getName)
if (levelfour.getClass.getSimpleName == "Rectangle") {
if (levelfour.isInstanceOf[javafx.scene.shape.Rectangle]) println("Rectangle instance confirmed")
println("LV4 Found a Rectangle")
println("original -fx-fill / CSS:"+ levelfour.asInstanceOf[javafx.scene.shape.Rectangle].style)
levelfour.asInstanceOf[javafx.scene.shape.Rectangle].style = "-fx-fill: #a001fc;"
} // end if
} // end for levelfour
} // end if
} // end for levelthree
} // end if
} // end for leveltwo

问题:由于只有基于javafx API的引用是可以接受的(顺便说一下,我使用的是ScalaIDE),是否有更聪明的方法来进行节点类型的类型转换?我使用的选项是:
1-简单/快捷方式:使用leveltwo.getClass.getSimpleName == "VBox"进行评估,这是API丛林中的快捷方式。但是它有效和安全吗
2-可能使用按书风格的混乱方式:

if (levelfour.isInstanceOf[javafx.scene.shape.Rectangle])

另一个问题:现在参考基于javafx的完全限定引用,即javafx.scene.shape.Rectangle,我想使用scala引用,但我遇到了一个错误,它迫使我采用基于javafx的引用。没什么大不了的,因为我可以使用javafx引用,但我想知道是否有基于scalafx的选项?

很乐意得到建设性的反馈。

如果我理解正确,您似乎想要导航子场景(属于更高级别的UI元素构造)的节点,以更改其中一些节点的外观。我有这个权利吗?

你提出了许多不同的问题,都在一个问题中,所以我会尽我所能解决所有问题。因此,这将是一个的答案,所以请耐心等待。顺便说一句,将来,如果你每一期都问一个问题,会有所帮助。;-)

首先,我将从表面上理解您的问题:您需要浏览场景,以便识别Rectangle实例并更改其样式。(我注意到您的safe版本也更改了第二个VBox的样式,但为了简单起见,我将忽略它。)如果您几乎无法控制每个元素的UI的结构,那么这是一个合理的操作方案。(如果你直接控制这个结构,有更好的机制,我稍后会讨论。)

在这一点上,可能值得扩展ScalaFXJavaFX装器,使库具有Scala的风格。通常,它是这样工作的:UI类的ScalaFX版本将相应的JavaFXclass实例作为参数;然后,它对其应用类似于Scala的操作。为了简化操作,在ScaraFXJavaFX实例之间有隐式转换,因此它(大部分)看起来像是魔术一样工作。但是,要启用后一项功能,必须将以下import添加到引用ScalaFX的每个源文件中:

import sclafx.Includes._

例如,如果JavaFX有一个javafx.Thing(它没有),带有setSizegetSize访问器方法,那么ScalaFX

package scalafx
import javafx.{Thing => JThing} // Rename to avoid confusion with ScalaFX Thing.
// ScalaFX wrapper for a Thing.
class Thing(val delegate: JThing) {
// Axilliary default constructor. Let's assume a JThing also has a default
// constructor.
//
// Creates a JavaFX Thing when we don't have one available.
def this() = this(new JThing)
// Scala-style size getter method.
def size: Int = delegate.getSize
// Scala-style size setter method. Allows, say, "size = 5" in your code.
def size_=(newSize: Int): Unit = delegate.setSize(newSize)
// Etc.
}
// Companion with implicit conversions. (The real implementation is slightly
// different.)
object Thing {
// Convert a JavaFX Thing instance to a ScalaFX Thing instance.
implicit def jfxThing2sfx(jThing: JThing): Thing = new Thing(jThing)
// Convert a ScalaFX Thing instance to a JavaFX Thing instance.
implicit def sfxThing2jfx(thing: Thing): JThing = thing.delegate
}

因此,老实说,做了很多工作,但收效甚微(尽管ScalaFX确实简化了属性绑定和应用程序初始化)。尽管如此,我还是希望你能跟随我来到这里。然而,这允许您编写如下代码:

import javafx.scene.shape.{Rectangle => JRectangle} // Avoid ambiguity
import scalafx.Includes._
import scalafx.scene.shape.Rectangle
// ...
val jfxRect: JRectangle = new JRectangle()
val sfxRect: Rectangle = jfxRect // Implicit conversion to ScalaFX rect.
val jfxRect2: JRectangle = sfxRect // Implicit conversion to JavaFX rect.
// ...

接下来,我们将讨论类型检查和铸造。在Scala中,使用模式匹配而不是isInstanceOf[A]asInstanceOf[A](两者都不受欢迎)更符合惯用用法。

例如,假设你有一个Node,你想看看它是否真的是Rectangle(因为后者是前者的一个子类)。按照你的例子的风格,你可能会写以下内容:

def changeStyleIfRectangle(n: Node): Unit = {
if(n.isInstanceOf[Rectangle]) {
val r = n.asInstanceOf[Rectangle]
r.style = "-fx-fill: #a001fc;"
}
else println("DEBUG: It wasn't a rectangle.")
}

同一代码的更惯用的Scala版本如下:

def changeStyleIfRectangle(n: Node): Unit = n match {
case r: Rectangle => r.style = "-fx-fill: #a001fc;"
case _ => println("DEBUG: It wasn't a rectangle.")
}

这可能看起来有点挑剔,但它往往会导致更简单、更干净的代码,我希望您会看到。特别要注意的是,case r: Rectangle只有在n是真实类型的情况下才匹配,然后它将nr强制转换为Rectangle

顺便说一句,我认为比较类型比通过getClass.getSimpleName获得类的名称并与字符串进行比较更有效,而且出错的可能性更小。(例如,如果您将要比较的字符串的类名键入错误,例如"Vbox"而不是"Vbox",则这不会导致编译器错误,并且匹配总是失败。)

正如您所指出的,识别Rectangle直接方法受到限制,因为它需要非常特定的场景结构。如果您更改了每个元素的表示方式,那么您必须相应地更改代码,否则会出现一堆异常。

因此,让我们继续使用安全方法。显然,它将比直接方法慢得多,效率也低得多,但它仍然依赖于场景的结构,即使它对在每个层次级别添加子对象的顺序不太敏感。如果我们改变层次结构,它很可能会停止工作。

这里有一种使用库的类层次结构来帮助我们的替代方法。在JavaFX场景中,一切都是Node。此外,具有子节点(例如VBoxStackPane)也是Pane的子类。我们将使用递归函数来浏览指定的起始Node实例下面的元素:它遇到的每个Rectangle都会更改其样式。

(顺便说一句,在这种特殊的情况下,隐式转换存在一些问题,这使得纯ScalaFX解决方案有点麻烦,所以我将直接在类的JavaFXcalaFX类型产生任何歧义。调用此函数时,隐式转化会正常工作。)

import javafx.scene.{Node => JNode}
import javafx.scene.layout.{Pane => JPane}
import javafx.scene.shape.{Rectangle => JRectangle}
import scala.collection.JavaConverters._
import scalafx.Includes._
// ...
// Change the style of any rectangles at or below starting node.
def setRectStyle(node: JNode): Unit = node match {
// If this node is a Rectangle, then change its style.
case r: JRectangle => r.style = "-fx-fill: #a001fc;"
// If the node is a sub-class of Pane (such as a VBox or a StackPane), then it
// will have children, so apply the function recursively to each child node.
//
// The observable list of children is first converted to a Scala list to simplify
// matters. This requires the JavaConverters import above.
case p: JPane => p.children.asScala.foreach(setRectStyle)
// Otherwise, just ignore this particular node.
case _ =>
}
// ...

关于这个函数的一些快速观察:

  1. 您现在可以使用您喜欢的UI节点的任何层次结构,但是,如果您有多个Rectangle节点,它将更改所有节点的样式。如果这对您不起作用,您可以添加代码来检查每个Rectangle的其他属性,以确定要修改哪个属性
  2. asScala方法用于将Pane节点的children转换为Scala序列,因此我们可以使用foreach高阶函数依次递归地将每个子级传递给setRectStyle方法。CCD_ 34通过CCD_
  3. 因为函数是递归的,但递归调用不在尾部位置(函数的最后一条语句),所以不是尾部递归。这意味着,如果将巨大场景传递给函数,则可能会得到StackOverflowException。你应该对任何合理大小的场景都很满意。(但是,作为练习,您可能需要编写一个尾部递归版本,以便函数是堆栈安全的。)
  4. 场景越大,此代码将变得越慢,效率越低。可能不是UI代码中最关心的问题,但的臭味仍然存在

因此,正如我们所看到的,必须浏览场景是一项挑战,效率低下,而且可能容易出错。有更好的方法吗?你敢打赌!

只有当您可以控制数据元素的场景定义时,以下操作才会起作用。如果你不这样做,你就会陷入基于上述的解决方案。

最简单的解决方案是保留对Rectangle的引用,将其样式作为类的一部分进行更改,然后根据需要直接访问它。例如:

import scalafx.Includes._
import scalafx.scene.control.Label
import scalafx.scene.layout.{StackPane, VBox}
import scalafx.scene.shape.Rectangle
final class Element {
// Key rectangle whose style is updated when the element is selected.
private val rect = new Rectangle {
width = 600
height = 400
}
// Scene representing an element.
val scene = new VBox {
children = List(
new VBox {
children = List(
new StackPane {
children = List(
// Ignore ImageView for now: not too important.
rect // Note: This is the rectangle defined above.
)
}
)
},
new Label {
text = "Some label"
}
)
}
// Call when element selected.
def setRectSelected(): Unit = rect.style = "-fx-fill: #a001fc;"
// Call when element deselected (which I assume you'll require).
def setRectDeselected(): Unit = rect.style = "-fx-fill: #000000;"
}

显然,您可以将数据引用作为参数传递给类,并根据需要使用它来填充场景。无论何时需要更改样式,无论场景结构是什么样子,调用后两个函数中的一个都可以达到所需的外科手术精度。

但还有更多!

ScalaFX/JavaFX真正伟大的功能之一是,它具有可观察的属性,可用于使场景自行管理。您会发现UI节点上的大多数字段都属于某种类型的"属性"。这允许您执行的操作是将属性绑定到字段,以便在更改属性时相应地更改场景。当与事件处理程序结合使用时,场景会自行处理所有事务。

在这里,我修改了后一类。现在,它有一个处理程序,用于检测何时选择和取消选择场景,并通过更改定义Rectangle样式的特性来做出反应。

import scalafx.Includes._
import scalafx.beans.property.StringProperty
import scalafx.scene.control.Label
import scalafx.scene.input.MouseButton
import scalafx.scene.layout.{StackPane, VBox}
import scalafx.scene.shape.Rectangle
final class Element {
// Create a StringProperty that holds the current style for the Rectangle.
// Here we initialize it to be unselected.
private val unselected = "-fx-fill: #000000;"
private val selected = "-fx-fill: #a001fc;"
private val styleProp = new StringProperty(unselected)
// A flag indicating whether this element is selected or not.
// (I'm using a var, but this is heavily frowned upon. A better mechanism might be
// required in practice.)
private var isSelected = false
// Scene representing an element.
val scene = new VBox {
children = List(
new VBox {
children = List(
new StackPane {
children = List(
// Ignore ImageView for now: not too important.
// Key rectangle whose style is bound to the above property.
new Rectangle {
width = 600
height = 400
style <== styleProp // <== means "bind to"
}
)
}
)
},
new Label {
text = "Some label"
}
)
// Add an event handler. Whenever the VBox (or any of its children) are
// selected/unselected, we just change the style property accordingly.
//
// "mev" is a "mouse event".
onMouseClicked = {mev =>
// If this is the primary button, then change the selection status.
if(mev.button == MouseButton.Primary) {
isSelected = !isSelected // Toggle selection setting
styleProp.value = if(isSelected) selected
else unselected
}
}
}
}

让我知道你在上的表现

最新更新