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

时间:2014-01-17 00:38:30

标签: javafx javafx-2

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

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

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

}

打印整个场景图的内容的正确方法是什么?接受的答案可以用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>

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)
  }
}

ClientLogo的用法:

<ClientLogo MigPane.cc="id clientLogo, pos (50px) (-25px)"/>

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>

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)
    })
  }
}

MenuViewController的用法:

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

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

<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>

如您所见,ClientLogo自定义控件中的子元素已正确遍历,但缺少menu.fxml中的元素。我希望看到the substructure that is added to a TitledPane和GridPane及其嵌套的子结构在menu.fxml中列出。

2 个答案:

答案 0 :(得分: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);
            }
        }
    }
}

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

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

<强>买者

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

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

  1. stage.show()已被要求进行初始场景。
  2. 修改场景发生脉冲后。
  3. 手动强制布局传递后。
    • 使用synchronous snapshot of the scene会强制布局传递,但我认为可能还有其他一些我不记得的名称允许您在没有快照的情况下强制布局传递。
  4. 在运行转储之前,请检查您是否已完成其中一项操作。

    使用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();
    

答案 1 :(得分:0)

基于@ jewelsea的优秀答案,这是Jython的一个版本。它通过使用一个特殊的案例处理程序来进行更深入的遍历,这些控件对findchildren方法没有很好的响应。它应该直截了当地转换为泛型java,只需使用python代码作为伪代码!

from javafx.scene import Parent
from javafx.scene import control

class SceneGraph(object):
  def parse(self,n,depth=0):
      print "    "*depth,n
      if isinstance(n,control.Tab):
          for i in n.getContent().getChildrenUnmodifiable():
              self.parse(i,depth+1)
      elif isinstance(n,control.TabPane):
          for i in n.getTabs():
              self.parse(i,depth+1)
      elif isinstance(n,Parent):
          for i in n.getChildrenUnmodifiable():
              self.parse(i,depth+1)
      elif isinstance(n,control.Accordion):
          for i in n.getPanes():
              self.parse(i,depth+1)

其他控件可能需要特殊处理程序,并且可以通过仔细检查类heirachy来改进代码。

在这个阶段,我不完全确定如何最好地处理第三方控件包。