Дочерняя иерархия ScalaFx и справочник по приведениям / экземплярам - PullRequest
1 голос
/ 07 октября 2019

Я брожу, если это оптимальный способ сделать это с ScalaFx: GUI состоит из нескольких узлов, в которые я высасываю контент из SQL-DB. Главная панель - это FlowPane, заполненная несколькими сотнями элементов. Каждый элемент состоит из четырех уровней иерархии (см. Числа, описывающие уровни):

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

Насколько я знаю, я могу получить доступ к узлам и их атрибутам на разных уровнях. То есть. Я могу дать обратную связь с пользователем, изменив цвет Rectangle под узлом 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

Вопросы: Есть ли более умный способ приведения типов узлов, поскольку допустимы только ссылки на основе API javafx (кстати, я использую ScalaIDE)? Опции, которые я использую:
1 - простой / быстрый путь: оценка с использованием leveltwo.getClass.getSimpleName == "VBox", который является ярлыком из API джунглей. Но насколько он эффективен и безопасен?
2 - способ загромождать, используя, вероятно, стиль книги:

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

Другой вопрос: теперь ссылка на полностью квалифицированную ссылку, основанную на javafx, т.е. javafx.scene.shape.Rectangle, я хотел бы вместо этого использовать ссылку scala, но я получаю ошибку, которая вынуждает меня принять ссылку на основе javafx. Ничего страшного, так как я могу использовать ссылку на javafx, но мне интересно, есть ли опция, основанная на scalafx?

Рад получить конструктивную обратную связь.

1 Ответ

1 голос
/ 11 октября 2019

Если я вас правильно понимаю, вы, похоже, хотите перемещаться по узлам под-сцены (которая относится к элементу конструкции UI более высокого уровня), чтобы изменить внешний вид некоторых изузлы внутри него. Имею ли я это право?

Вы затрагиваете несколько разных вопросов, все в рамках одного вопроса, поэтому я сделаю все возможное, чтобы решить их все. В результате это будет длинный ответ, поэтому, пожалуйста, потерпите меня. Кстати, в будущем будет полезно, если вы зададите один вопрос для каждой проблемы. ; -)

Во-первых, я собираюсь взять вашу проблему за чистую монету: вам нужно просмотреть сцену, чтобы идентифицировать экземпляр Rectangle и изменить его стиль. (Я отмечаю, что ваша безопасная версия также меняет стиль второй VBox, но я буду игнорировать это ради простоты.) Это разумный вариант действий, если у вас малоне контролировать структуру каждого элемента UI . (Если вы напрямую управляете этой структурой, есть гораздо лучшие механизмы, о которых я расскажу позже.)

На этом этапе, возможно, стоит остановиться на связи между ScalaFX и JavaFX . Первый - чуть больше, чем набор оболочек для второго, чтобы придать библиотеке аромат Scala . В общем, это работает так: ScalaFX версия класса UI принимает в качестве аргумента соответствующий экземпляр класса JavaFX ;затем он применяет к ней Scala -подобные операции. Чтобы упростить задачу, существует неявных преобразований между ScalaFX и JavaFX , так что он (в основном), кажется, работает по волшебству. Однако, чтобы включить эту последнюю функцию, вы должны добавить следующие import к каждому из ваших исходных файлов, которые ссылаются на ScalaFX :

import sclafx.Includes._

Например,если JavaFX имеет javafx.Thing (не имеет) с методами доступа setSize и getSize, то версия 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, а затем преобразует n в r как Rectangle.

Кстати, я ожидаю, чтоСравнение типов более эффективно, чем получение имени класса через getClass.getSimpleName и сравнение со строкой, и вероятность ошибки меньше. (Например, если вы неправильно набрали имя класса строки, с которой сравниваете, например, «Vbox» вместо «VBox», то это не приведет к ошибке компиляции, и совпадение всегда будет неудачным.)

Как вы указали, ваш прямой подход к идентификации Rectangle ограничен тем фактом, что он требует очень специфической структуры сцены. Если вы измените способ представления каждого элемента, то вы должны соответствующим образом изменить свой код, иначе вы получите кучу исключений.

Итак, давайте перейдем к вашему безопасному подходу. Понятно, что он будет намного медленнее и менее эффективным, чем подход direct , но он по-прежнему зависит от структуры сцены, даже если он менее чувствителен к порядку, в котором дочерние элементы добавляются при каждомуровень иерархии. Если мы изменим иерархию, она, скорее всего, перестанет работать.

Вот альтернативный подход, который использует иерархию классов библиотеки, чтобы помочь нам. В сцене JavaFX все равно Node. Кроме того, узлы, которые имеют дочерние элементы (такие как VBox и StackPane), также являются подклассами Pane. Мы будем использовать рекурсивную функцию для просмотра элементов ниже указанного начального экземпляра Node: каждый встречающийся Rectangle будет менять свой стиль.

(Кстати, в данном конкретном случае есть некоторые проблемыс неявными преобразованиями, что делает чистое решение ScalaFX немного громоздким, поэтому вместо этого я собираюсь сопоставить непосредственно версии JavaFX классов, переименованных во избежание двусмысленности сэквивалентные ScalaFX типы. При вызове этой функции неявные преобразования будут работать нормально.)

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 используется для преобразования childrenУзел Pane в последовательности Scala , поэтому мы можем использовать функцию foreach высшего порядка для рекурсивной передачи каждого потомка по очереди методу setRectStyle. asScala становится доступным с помощью оператора import scala.collection.JavaConverters._.
  3. Поскольку функция рекурсивная, но рекурсивный вызов не находится в позиции tail (последний оператор функции), не хвостовая рекурсия . Это означает, что если вы передадите огромную сцену функции, вы можете получить 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
      }
    }
  }
}

Дайте мне знать, как вы попали ...

...