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

ScalaFx children hierarchy and casting / instance reference

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

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

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

我可以直接访问 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) 

这里的示例演示了 "safer" 对节点层次结构中元素的访问(节点层次结构的创建在相当烦人的代码结构中,因此不包括在内):

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 ie的完全限定参考。 javafx.scene.shape.Rectangle,我想改用 scala 引用,但出现错误,迫使我采用基于 javafx 的引用。没什么大不了的,因为我可以使用 javafx 参考,但我不知道是否有基于 scalafx 的选项?

很高兴得到建设性的反馈。

如果我没理解错的话,您似乎想导航 sub-scene 的节点(属于 higher-level UI element construct) 以更改其中某些节点的外观。我说得对吗?

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

首先,我将从表面上看您的问题:您需要浏览场景以识别 Rectangle 实例并更改其样式。 (我注意到你的 safe 版本也改变了第二个 VBox 的样式,但为了简单起见,我将忽略它。)这是一个合理的课程如果您几乎无法控制每个元素的 UI 的结构,则可以采取行动。 (如果你直接控制这个结构,还有更好的机制,我稍后会讲到。)

此时,可能值得扩展 ScalaFXJavaFX 之间的关系。前者只不过是后者的一组 wrappers,使库具有 Scala 风格。一般来说,它是这样工作的: ScalaFX 版本的 UI class 需要相应的 JavaFX class 实例作为参数;然后它对其应用类似于 Scala 的操作。为了简化事情,在 ScalaFXJavaFX 实例之间存在 隐式转换,所以它(主要是) 似乎是靠魔法起作用的。但是,要启用后一个功能,您 必须 添加以下 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] 和 [=] 更 惯用 25=](两者都不受欢迎)。

例如,假设您有一个 Node,并且您想看看它是否真的是一个 Rectangle(因为后者是前者的 sub-class)。按照您示例的样式,您可以编写如下内容:

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 的真实类型时才匹配,然后它将 n 转换为 r 作为 Rectangle.

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

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

那么让我们继续您的 安全 方法。显然,它会比 direct 方法慢得多且效率低得多,但它仍然依赖于场景的结构,即使它对 child层级的每一层都增加了ren。如果我们更改层次结构,它可能会停止工作。

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

(顺便说一句,在这种特殊情况下,隐式转换存在一些问题,这使得纯 ScalaFX 解决方案有点麻烦,所以我将直接匹配JavaFX 版本的 classes 相反,重命名以避免与等效的 ScalaFX 类型产生歧义。隐式 c调用此函数时 nversions 将正常工作。)

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 higher-order 函数 递归地将每个 child 依次传递给 setRectStyle 方法。 asScalaimport scala.collection.JavaConverters._ 语句提供。
  3. 因为函数是递归的,但是递归调用不在tail位置(函数的最后一条语句),所以不是tail-recursive。这意味着如果你将一个 巨大的 场景传递给函数,你可能会得到一个 WhosebugException。任何合理大小的场景都应该没问题。 (但是,作为练习,您可能想要编写一个 tail-recursive 版本,以便该函数 堆栈安全 。)
  4. 随着场景变大,此代码将变得更慢且效率更低。 UI 代码中可能不是您最关心的问题,但 难闻的气味 都一样。

因此,正如我们所见,必须浏览一个场景具有挑战性、效率低下并且可能容易出错。有没有更好的办法?你打赌!

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

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

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;"
}

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

但还有更多!

关于 ScalaFX/JavaFX 的真正重要特性之一是它具有 可观察的属性 可用于让场景自行管理。您会发现 UI 节点上的大多数字段属于某种类型 "Property"。这允许您做的是 绑定 一个 属性 到字段,这样当您更改 属性 时,您会相应地更改场景。当与 事件处理程序 结合使用时,场景会自行处理所有事情。

在这里,我对后者进行了修改class。现在,它有一个处理程序可以检测何时选择和取消选择场景,并通过更改定义 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
      }
    }
  }
}

告诉我你过得怎么样...