如何遍历整个场景图层次结构?

axi*_*sty 1 javafx javafx-2

我想编写一个方法,将整个场景图(包括节点的名称和与节点关联的css属性)打印到控制台.这将使得能够查看场景图的层次结构以及与每个元素相关联的所有css属性.我尝试这样做(请参阅下面的scala代码),但我的尝试失败了.

我正在测试它的场景图包含我按照本教程制作的自定义控制器.它不打印自定义控制器中包含的任何嵌套控件.自定义控制器在舞台上显示正常(并且它正常运行)所以我知道所有必需的控件都是场景图的一部分,但由于某种原因,示例代码不会递归到自定义控制器中.它会打印自定义控制器的名称,但不会打印控制器内的嵌套元素.

object JavaFXUtils {

  def printNodeHierarchy(node: Node): Unit = {
    val builder = new StringBuilder()
    traverse(0, node, builder)
    println(builder)
  }

  private def traverse(depth: Int, node: Node, builder: StringBuilder) {
    val tab = getTab(depth)
    builder.append(tab)
    startTag(node, builder)
    middleTag(depth, node, builder)
    node match {
      case parent: Parent => builder.append(tab)
      case _ =>
    }
    endTag(node, builder)
  }

  private def getTab(depth: Int): String = {
    val builder = new StringBuilder("\n")
    for(i <- 0 until depth) {
      builder.append("   ")
    }
    builder.toString()
  }

  private def startTag(node: Node, builder: StringBuilder): Unit = {
    def styles: String = {
      val styleClasses = node.getStyleClass
      if(styleClasses.isEmpty) {
        ""
      } else {
        val b = new StringBuilder(" styleClass=\"")
        for(i <- 0 until styleClasses.size()) {
          if(i > 0) {
            b.append(" ")
          }
          b.append(".").append(styleClasses.get(i))
        }
        b.append("\"")
        b.toString()
      }
    }

    def id: String = {
      val nodeId = node.getId
      if(nodeId == null || nodeId.isEmpty) {
        ""
      } else {
        val b = new StringBuilder(" id=\"").append(nodeId).append("\"")
        b.toString()
      }
    }

    builder.append(s"<${node.getClass.getSimpleName}$id$styles>")
  }

  private def middleTag(depth: Int, node: Node, builder: StringBuilder): Unit = {
    node match {
      case parent: Parent =>
        val children: ObservableList[Node] = parent.getChildrenUnmodifiable
        for (i <- 0 until children.size()) {
          traverse(depth + 1, children.get(i), builder)
        }
      case _ =>
    }
  }

  private def endTag(node: Node, builder: StringBuilder) {
    builder.append(s"</${node.getClass.getSimpleName}>")
  }

}
Run Code Online (Sandbox Code Playgroud)

打印整个场景图的内容的正确方法是什么?接受的答案可以用Java或Scala编写.

更新

经过进一步审查,我注意到它对某些自定义控件有效,但不是全部.我有3个自定义控件.我将展示其中的两个.自定义控件是ClientLogo和MenuViewController.先前列出的遍历代码正确显示ClientLogo的子代,但不显示MenuViewController的子代.(也许这是因为MenuViewController是TitledPane的子类?)

client_logo.fxml:

<?import javafx.scene.image.ImageView?>
<fx:root type="javafx.scene.layout.StackPane" xmlns:fx="http://javafx.com/fxml" id="clientLogo">
  <ImageView fx:id="logo"/>
</fx:root>
Run Code Online (Sandbox Code Playgroud)

ClientLogo.scala

class ClientLogo extends StackPane {

  @FXML @BeanProperty var logo: ImageView = _

  val logoFxml: URL = classOf[ClientLogo].getResource("/fxml/client_logo.fxml")
  val loader: FXMLLoader = new FXMLLoader(logoFxml)
  loader.setRoot(this)
  loader.setController(this)
  loader.load()
  logo.setImage(Config.clientLogo)
  logo.setPreserveRatio(false)

  var logoWidth: Double = .0
  def getLogoWidth = logoWidth
  def setLogoWidth(logoWidth: Double) {
    this.logoWidth = logoWidth
    logo.setFitWidth(logoWidth)
  }

  var logoHeight: Double = .0
  def getLogoHeight = logoHeight
  def setLogoHeight(logoHeight: Double) {
    this.logoHeight = logoHeight
    logo.setFitHeight(logoHeight)
  }
}
Run Code Online (Sandbox Code Playgroud)

ClientLogo的用法:

<ClientLogo MigPane.cc="id clientLogo, pos (50px) (-25px)"/>
Run Code Online (Sandbox Code Playgroud)

menu.fxml:

<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.shape.Line?>
<?import javafx.scene.control.Label?>
<fx:root type="javafx.scene.control.TitledPane" xmlns:fx="http://javafx.com/fxml" id="menu" collapsible="true" expanded="false">
  <GridPane vgap="0" hgap="0">
    <children>
      <Line  fx:id="line" styleClass="menu-line" startX="0" startY="1" endX="150" endY="1"               GridPane.rowIndex="0" GridPane.columnIndex="0"/>
      <Label fx:id="btnSettings"  id="btn-settings"   styleClass="btn-menu" text="%text.change.password" GridPane.rowIndex="1" GridPane.columnIndex="0"/>
      <Label fx:id="btnAdmin"     id="btn-admin"      styleClass="btn-menu" text="%text.admin"           GridPane.rowIndex="2" GridPane.columnIndex="0"/>
      <Label fx:id="btnQuickTips" id="btn-quick-tips" styleClass="btn-menu" text="%text.quick.tips"      GridPane.rowIndex="3" GridPane.columnIndex="0"/>
      <Label fx:id="btnLogout"    id="btn-logout"     styleClass="btn-menu" text="%text.logout"          GridPane.rowIndex="4" GridPane.columnIndex="0"/>
    </children>
  </GridPane>
</fx:root>
Run Code Online (Sandbox Code Playgroud)

MenuViewController.scala:

class MenuViewController extends TitledPane with ViewController[UserInfo] with LogHelper with Resources {

  private var menuWidth: Double = .0

  @FXML @BeanProperty var line: Line = _
  @FXML @BeanProperty var btnSettings: Label = _
  @FXML @BeanProperty var btnAdmin: Label = _
  @FXML @BeanProperty var btnQuickTips: Label = _
  @FXML @BeanProperty var btnLogout: Label = _

  val menuFxml: URL = classOf[MenuViewController].getResource("/fxml/menu.fxml")
  val loader: FXMLLoader = new FXMLLoader(menuFxml)
  loader.setRoot(this)
  loader.setController(this)
  loader.setResources(resources)
  loader.load()

  var userInfo: UserInfo = _

  @FXML
  private def initialize() {
    def handle(message: Any): Unit = {
      setExpanded(false)
      uiController(message)
    }
    btnSettings.setOnMouseClicked(EventHandlerFactory.mouseEvent(e => handle(SettingsClicked)))
    btnAdmin.setOnMouseClicked(EventHandlerFactory.mouseEvent(e => handle(AdminClicked)))
    btnQuickTips.setOnMouseClicked(EventHandlerFactory.mouseEvent(e => handle(QuickTipsClicked)))
    btnLogout.setOnMouseClicked(EventHandlerFactory.mouseEvent(e => handle(LogoutClicked)))
  }

  override def update(model: UserInfo) {
    userInfo = model
    setText(if (userInfo == null) "Menu" else userInfo.displayName)
  }

  def getMenuWidth: Double = {
    return menuWidth
  }

  def setMenuWidth(menuWidth: Double) {
    this.menuWidth = menuWidth
    val spaceToRemove: Double = (menuWidth / 3)
    line.setEndX(menuWidth - spaceToRemove)
    line.setTranslateX(spaceToRemove / 2)
    Array(btnSettings, btnAdmin, btnQuickTips, btnLogout).foreach(btn => {
      btn.setMinWidth(menuWidth)
      btn.setPrefWidth(menuWidth)
      btn.setMaxWidth(menuWidth)
    })
  }
}
Run Code Online (Sandbox Code Playgroud)

MenuViewController的用法:

<MenuViewController MigPane.cc="id menu, pos (100% - 250px) (29)" menuWidth="200" fx:id="menu"/>
Run Code Online (Sandbox Code Playgroud)

这是将场景图打印到控制台的输出示例:

<BorderPane styleClass=".root">
   <StackPane>
      <MigPane id="home" styleClass=".top-blue-bar-bottom-blue-curve">
         <ClientLogo id="clientLogo">
            <ImageView id="logo"></ImageView>
         </ClientLogo>
         ...
         <MenuViewController id="menu" styleClass=".titled-pane">
         </MenuViewController>
      </MigPane>
   </StackPane>
</BorderPane>
Run Code Online (Sandbox Code Playgroud)

如您所见,ClientLogo自定义控件中的子元素已正确遍历,但缺少menu.fxml中的元素.我希望看到添加到TitledPane的子结构GridPane以及menu.fxml中列出的嵌套子结构.

jew*_*sea 5

推荐的工具

ScenicView是第三方工具,用于在SceneGraph上进行内省.强烈建议您使用ScenicView而不是开发自己的调试工具来转储场景图.

转储实用程序

这是一个非常简单的小例程,用于转储场景图System.out,通过DebugUtil.dump(stage.getScene().getRoot())以下方式调用它:

import javafx.scene.Node;
import javafx.scene.Parent;

public class DebugUtil {
    /** Debugging routine to dump the scene graph. */
    public static void dump(Node n) {
        dump(n, 0);
    }

    private static void dump(Node n, int depth) {
        for (int i = 0; i < depth; i++) System.out.print("  ");
        System.out.println(n);
        if (n instanceof Parent) {
            for (Node c : ((Parent) n).getChildrenUnmodifiable()) {
                dump(c, depth + 1);
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

上面的简单例程通常会转储所有样式类信息,因为toString()节点上的默认值输出了styleclass数据.

另一个增强功能是检查正在打印的节点是否也是Labeled或Text的实例,然后在这些情况下还打印与节点关联的文本字符串.

警告

对于某些控件类型,为控件创建的一些节点由控件的皮肤实例化,该皮肤可以在CSS中定义,并且皮肤有时会懒惰地创建其子节点.

要查看这些节点,您需要做的是确保在场景图上运行css布局传递,方法是在发生以下事件之一后运行转储:

  1. stage.show() 被称为初始场景.
  2. 在修改场景发生脉冲后.
  3. 手动强制布局传递后.
    • 获取场景同步快照将强制布局传递,但我认为可能有一些其他的API,我不记得其名称允许您强制布局传递没有快照.

在运行转储之前,请检查是否已完成其中一项操作.

使用AnimationTimer的示例

// Modify scene.
// . . .
// Start a timer which delays the scene dump call.
AnimationTimer timer = new AnimationTimer() {
    @Override
    public void handle(long now) {
        // Take action on receiving a pulse.
        dump(stage.getScene().getRoot());
        // Stop monitoring pulses.
        // You can stop immediately like this sample. 
        stop();
        // OR you could count a specified number pulses before stopping.
    }
};
timer.start();
Run Code Online (Sandbox Code Playgroud)