JavaFX Node wird nicht korrekt angezeigt

Lodoss

Aktives Mitglied
Guten abend.

Ich unternehme gerade meine ersten Schritte in JavaFX und bin auf ein Problem gestoßen dessen Ursache ich nicht herrausbekomme...

Was ich tun möchte:
Ich möchte eine Art von Desktoppane bauen ähnlich der JDesktoppane in Swing (in FX gibt es sowas ja scheinbar nicht als Standard-Componente)

Dazu habe ich eine Klasse FXDesktopPane erzeugt, die später die Fenster-verwaltung übernehmen soll, Dazu eine Klasse FXWindow die analog zum JInternalFrame funktionieren soll

Das FXWindow - Fenster ist im prinzip so aufgebaut, dasa es einen Bereich für den Fenster-Inhalt (contentpane) hat, und einen Bereich für den Fenstertitel und Fenster-Buttons X / Minimize / Maximize (windowTitlePane). Die Contentpane nimmt immer den gesamten Bereich des Fensters abzüglich der windowTitlePane ein.

Um das ganze zu testen habe ich mir zwei demo-fenster gemacht.

Problem ist, dass er mir den Hintergrund der windowTitlePane für das eine Fenster (Login) anzeigt, für das andere (Something) aber nicht. Wo liegt der Fehler?

FXDesktopPane code:
Java:
package javaFXDemo;

import javafx.scene.layout.Pane;

public class FXDesktopPane extends Pane
{
   public FXDesktopPane()
   {
     super();
     this.setStyle("-fx-background: #cccccc;");
   }

   public void addWindow(FXWindow win)
   {
     this.getChildren().addAll(win);
   }

   @Override
   public boolean isResizable()
   {
     return true;
   }
}

FXWindow code:
Java:
public class FXWindow extends Region
{
  double orgSceneX, orgSceneY;
  double orgTranslateX, orgTranslateY;

  private Pane contentPane;
  private Pane windowTitlePane;
   private Label windowTitle;

   public FXWindow()
   {
     super();
     contentPane = new Pane();
  
  EventHandler<MouseEvent> circleOnMousePressedEventHandler = new EventHandler<MouseEvent>()
  {
  @Override
  public void handle(MouseEvent t)
  {
  orgSceneX = t.getSceneX();
  orgSceneY = t.getSceneY();
  orgTranslateX = ((FXWindow)t.getSource()).getTranslateX();
  orgTranslateY = ((FXWindow)t.getSource()).getTranslateY();
  }
  };
  this.addEventHandler(MouseEvent.MOUSE_PRESSED, circleOnMousePressedEventHandler);

     this.setOnMouseDragged(new EventHandler<MouseEvent>()
     {
    public void handle(MouseEvent event)
    {
    double offsetX = event.getSceneX() - orgSceneX;
    double offsetY = event.getSceneY() - orgSceneY;
    double newTranslateX = orgTranslateX + offsetX;
    double newTranslateY = orgTranslateY + offsetY;

    ((FXWindow)event.getSource()).setTranslateX(newTranslateX);
    ((FXWindow)event.getSource()).setTranslateY(newTranslateY);
    }
    });

     windowTitle = new Label("");

     windowTitlePane = new HBox(10, windowTitle);
     windowTitlePane.setPadding(new Insets(0, 0, 0, 3));

     this.setStyle("-fx-background-color: #dddddd; -fx-border-color: #555555; -fx-border-style: solid; -fx-border-width: 3;");
     windowTitlePane.setStyle("-fx-background-color: #ffffff;");
     this.getChildren().addAll(windowTitlePane);

   }

   public void setTitle(String title)
   {
     windowTitle.setText(title);
   }

   public String getTitle()
   {
     return windowTitle.getText();
   }

   @Override
   public boolean isResizable()
   {
     return true;
   }

   public Pane getContentPane()
   {
     return contentPane;
   }

   protected void setContentPane(Pane pane)
   {
     contentPane = pane;
     getChildren().removeAll(contentPane);
     getChildren().addAll(contentPane);
   }

   @Override
   protected void layoutChildren()
   {
     super.layoutChildren();
     double titleHeight = calcPrefHeightTitle();

     windowTitlePane.resize(this.getWidth(), titleHeight);

     contentPane.setTranslateY(titleHeight);
     // to avoid display of components out of the viewable space of the contentpane
     contentPane.setClip(new Rectangle(this.getWidth(), this.getHeight()-titleHeight));
     // resize the content pane to match the window pane size
     contentPane.resize(this.getWidth(), this.getHeight()-titleHeight);
   }

   private double calcPrefHeightTitle()
   {
     double h = 0;
     for (Node n : windowTitlePane.getChildren())
     {
       System.out.println("child height: " + n.getLayoutBounds().getHeight() + " " + n.getClass());
       double h2 = n.getLayoutBounds().getHeight();
       if (h<h2) h=h2;
     }
     return h;
   }

Demo Fenster 1 (LoginFenster)
Java:
public class LoginWindow extends FXWindow
{
   public LoginWindow()
   {
     super();

     Label userNameLb = new Label("Benutzer:");
     Label passwordLb = new Label("Kennwort:");

     TextField userNameTxt = new TextField();
     PasswordField passwordTxt = new PasswordField();

     Button loginBtn = new Button("Anmelden");
     loginBtn.setId("loginbtn");
     loginBtn.setOnAction(new EventHandler<ActionEvent>()
     {
       @Override
       public void handle(ActionEvent event)
       {
       }
     });

     GridPane gp = new GridPane();

     gp.setHgap(10);
     gp.setVgap(10);
     gp.setPadding(new Insets(25, 25, 25, 25));

     gp.add(userNameLb, 0, 0);
     gp.add(userNameTxt, 1, 0);
     GridPane.setHgrow(userNameTxt, Priority.ALWAYS);

     gp.add(passwordLb, 0, 1);
     gp.add(passwordTxt, 1, 1);
     GridPane.setHgrow(passwordTxt, Priority.ALWAYS);

     gp.add(loginBtn, 1, 2);
     GridPane.setHalignment(loginBtn, HPos.RIGHT);

     this.setContentPane(gp);

     this.setPrefWidth(350);
     this.setPrefHeight(170);

     this.setTitle("Login");
   }
}

Demo Fenster 2: Something
Java:
package javaFXDemo;

import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;

public class SomethingWin extends FXWindow
{
   public SomethingWin()
   {
     super();

     GridPane gp = new GridPane();

     gp.setHgap(10);
     gp.setVgap(10);
     gp.setPadding(new Insets(25, 25, 25, 25));

     gp.add(new Label("Something"), 0, 0);

     this.setContentPane(gp);

     this.setPrefWidth(300);
     this.setPrefHeight(300);

     this.setTitle("Something");
   }
}

und die Main:
Java:
package javaFXDemo;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class FXDesktopDemo extends Application
{

   public static void main(String[] args)
   {
     launch(args);
   }

   @Override
   public void start(Stage primaryStage) throws Exception
   {
     FXDesktopPane d = new FXDesktopPane();
     Scene sc = new Scene(d, 500, 500);

     primaryStage.setScene(sc);
     primaryStage.show();

     d.addWindow(new SomethingWin());
     d.addWindow(new LoginWindow());



   }

}
 
Zuletzt bearbeitet:
Nachtrag:
Der Fehler scheint im FXWindow zu liegen. Die Methode "calcPrefHeightTitle" gibt im Login-Fenster die korrekte Höhe zurück, im SomethingWin aber immer 0 (das Label scheint auch schon 0 Höhe zu haben)...

Die Frage ist, warum?
 
Ich versuche es nachher mal, aber eine Bemerkung vorweg: WARUM extendest du die ganzen Klassen? In JavaFX wird i.d.R. die GUI eher als Baumstruktur aus Layouts mit Child-Nodes aufgebaut. Idealerweise über FXML, muss aber nicht notwendigerweise so sein.
Diese Art ist für mich reichlich "oldskool" und schwerer zu lesen (aber wie gesagt: ich probier es später mal).
 
Ich versuche es nachher mal, aber eine Bemerkung vorweg: WARUM extendest du die ganzen Klassen? In JavaFX wird i.d.R. die GUI eher als Baumstruktur aus Layouts mit Child-Nodes aufgebaut. Idealerweise über FXML, muss aber nicht notwendigerweise so sein.
Diese Art ist für mich reichlich "oldskool" und schwerer zu lesen (aber wie gesagt: ich probier es später mal).

a) Da ich die Funktionalität jedes Fensters Kapseln möchte sind diese so oder so jeweils eine Klasse, da kann ich dann auch gleich von Fenster ableiten statt den Init-Code von aussen zu Basteln.
b) FXML hab ich auf der Liste der Dinge die ich mir anschauen möchte, aber soweit bin ich in JAVAFX noch nicht, erstmal krabbeln lernen vor dem gehen 🙂
c) oldskool.... damals war alles besser^^

aber schonmal Danke für das Feedback, ich warte gespannt
 
Aha... Ich hab das mit dem JInternalFrame überlesen, daher war mir nicht bewusst, wo du mit deinem FXWindow hin möchtest. Mea Culpa dafür. Ich würde die konkreten Window-Instanzen (Login, Something) inhaltlich dennoch weiterhin mit FXMLs machen - dass das aussenrum eher so gemacht werden muss, wie du es implementiert hast, ist etwas anderes.

Im Moment habe ich die Ursache noch nicht gefunden.
Ich hab noch in der Ausgabe den Namen des Fensters hinzugefügt und bemerkt, dass die calcPrefHeightTitle für Something nur ein einziges Mal aufgerufen wird, am Anfang. Danach nie wieder.
Auch für Login ist es Anfangs 0 und wird erst aktualisiert, wenn das gesamte Fenster überhaupt bewegt wird (am Anfag ist auch dieses Grau).

Mit diesem Code zusätzlich im FXDesktopPane klappt es dann mit dem Something-Fenster, wie mit dem Login-Fenster. Das Problem, das initial kein weisser Titel da ist, bleibt persistent.
Java:
@Override
    protected void layoutChildren() {
        super.layoutChildren();
        this.getChildren().stream().forEach(n -> {
            if (n instanceof FXWindow)
                ((FXWindow) n).layoutChildren();
        });
    }
Der Grund ist wahrscheinlich, dass dein
Java:
d.addWindow(new SomethingWin());
d.addWindow(new LoginWindow());
gemacht wird, bevor die Stage tatsächlich schon sichtbar ist (auch wenn du schon #show auf der primaryStage gemacht hat, ist diese wahrscheinlich erst etwas später da (daher auch die Höhe von 0).

Noch einen Hinweis: In der Bibliothek "jfxtras" gibt es so etwas schon:
Webseite:
http://jfxtras.org/
JavaDoc
http://jfxtras.org/doc/8.0/jfxtras-window/index.html
Code allgemein:
https://github.com/JFXtras/jfxtras
Code des "Internal Frames":
https://github.com/JFXtras/jfxtras/...java/jfxtras/scene/control/window/Window.java
 
Für ein Beispiel mit FXML kann ich dir später mal das Something-Fenster auf FXML umbauen (also wahrscheinlich gibt es dann ein generisches Fenster, dem man die FXML übergibt. Der Rest (Controller und so weiter wird dann dort separat gehandhabt).
 
FXML-Beispiel:

FXMLWindow mit generischen Parameter "T" für den Controller.
Java:
import java.io.IOException;
import java.net.URL;

import javafx.fxml.FXMLLoader;
import javafx.scene.layout.Pane;

public class FXMLWindow<T> extends FXWindow {
  
    private T controller;
  
    public T getController() {
        return this.controller;
    }
  
    public FXMLWindow(URL fxmlLocation, String title, double prefWidth, double prefHeight) throws IOException {
        super();
      
        FXMLLoader loader = new FXMLLoader(fxmlLocation);
        Pane pane = loader.load();
        controller = loader.getController();
      
        this.setContentPane(pane);
      
        this.setPrefWidth(300);
        this.setPrefHeight(300);
      
        this.setTitle(title);
    }
  
}

Beispeil FXML:
HTML:
<?xml version="1.0" encoding="UTF-8"?>
<!--
    Do not edit this file it is generated by e(fx)clipse from ../src/oldskool/Test.fxgraph
-->

<?import java.lang.*?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.VBox?>

<BorderPane xmlns:fx="http://javafx.com/fxml" fx:id="rootBorderPane" fx:controller="oldskool.TestController">

    <padding>
        <Insets left="10" top="10" right="10" bottom="10"/>
    </padding>
    <center>
        <VBox fx:id="centerVBox" spacing="5">
            <Label fx:id="title" text="Title"/>
            <TextField fx:id="text" promptText="write something..."/>
        </VBox>
    </center>
</BorderPane>

Und der TestController (sihe im FXML -> fx:controller="oldskool.TestController"):
Java:
package oldskool;

import java.net.URL;
import java.util.ResourceBundle;

import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;

public class TestController {
  
    @FXML
    private BorderPane rootBorderPane;
    @FXML
    private VBox centerVBox;
    @FXML
    private Label title;
    @FXML
    private TextField text;
  
    @FXML
    private URL location;
    @FXML
    private ResourceBundle resourceBundle;
  
    @FXML
    protected void initialize() {
        text.setText("bla");
    }
  
    public void doSomeTest() {
        text.setText("blub");
    }
}

Das Ganze noch im Main verlinkt:
Java:
        d.addWindow(new SomethingWin());
        d.addWindow(new LoginWindow());
      
        FXMLWindow<TestController> testFxmlWindow = new FXMLWindow<TestController>(getClass().getResource("Test.fxml"), "Test", 250, 400);
        d.addWindow(testFxmlWindow);
        testFxmlWindow.getController().doSomeTest();

Was passiert hier? Das FXMLWindow lädt das deklarative UI anhand der FXML-Datei. Dort ist der Controller verlinkt, der durch den FXMLLoader im FXMLWindow ebenfalls geladen wird.
Der FXMLLoader hält das Pane (muss immer nach von Node abgeleitet gecastet werden, glaub ich) und den Controller eh immer als generischen Typen vor, man kann den Controller also quasi casten wonach man will. Wenn man es parametriert wird das Ganze etwas "eleganter", finde ich.
Was macht der Controller? Er setzt im #initialize automatisch immer den Text "bla", da ich aber dann noch seine Methode #doSomeTest aufrufe, wird dieser quasi sofort noch einmal mit blub überschrieben (kommentier die Zeile aus, dann siehst du was ich meine).

In etwa klar soweit? Bei Fragen immer fragen...
 
Yay, ich habe eine praktikable Lösung gefunden.

Anscheinend ist das Label nur bedingt in der Lage seine pref-Height anhand seines Inhalts zu ermitteln.

Label durch Text ersetzt und whola, es klappt wie gedacht....


Zu deinen Ausführungen:
Erstmal vielen Dank das du dir so viel Zeit genommen hast.

Ich habe das Verhalten, dass sich das eine Fenster 4 mal die höhe berechnet, das zweite aber nur einmal genauer Untersucht: Für jede fokusierbare Komponente, die auf dem Fenster hinzugefügt wird, wird ein layout-durchlauf gemacht (ich hab noch keinen schimmer warum)

zum Thema FXML: ist wie gesagt auf der Liste, das konzept finde ich generell gut.

zum Lösungsansatz:
Java:
@Override
    protected void layoutChildren() {
        super.layoutChildren();
        this.getChildren().stream().forEach(n -> {
            if (n instanceof FXWindow)
                ((FXWindow) n).layoutChildren();
        });
    }

das wirkt merkwürdig auf mich, denn eigentlich sollte das doch über den Node-Tree sowieso passieren, und wenn nein, wüsste ich schon gerne wieso 🙂

Zum Thema jfxtras:
ich versuche dependencies zu vermeiden wo es möglich ist (oldschool ich weiss^^) und einzusetzen wo es sinnvoll ist. in diesem fall sehe ich die Notwendigkeit noch nicht

Zum Thema FXMLWindow:
Schau ich mir auf jeden fall genauer an sobald ich mich in FXML reingelesen habe, momentan ist da noch zu viel ??? für mich drin. Ich komme drauf zurück.

PS: https://github.com/JFXtras/jfxtras/...java/jfxtras/scene/control/window/Window.java der Link ist Gold wert, danke!
 
Zuletzt bearbeitet von einem Moderator:

Neue Themen


Zurück
Oben