Mittwoch, 29. Mai 2013

3D-Rotation mit der Maus

Ich fand es mal interessant zu sehen, wie einfach es ist, einen Würfel mit JavaFX3D zu zeichnen und mit der Maus zu drehen. Die einzige Schwierigkeit dabei war, dass man drauf achten muss, die Würfel direkt in der Mitte zu platzieren, er sonst durch den Rotationspunkt von seiner ursprünglichen Position abweicht. Der Folgende Code realisiert das drehen des Würfels bei gedrückter Maustaste.

public class MouseRotation extends Application{
 
 private DoubleProperty side2Angle = new SimpleDoubleProperty();
    private DoubleProperty side3Angle = new SimpleDoubleProperty();
    private DoubleProperty side4Angle = new SimpleDoubleProperty();
    private DoubleProperty side5Angle = new SimpleDoubleProperty();
    private DoubleProperty side6TranslateZ = new SimpleDoubleProperty();
     private DoubleProperty rootAngleX = new SimpleDoubleProperty();
     private DoubleProperty rootAngleY = new SimpleDoubleProperty();
     
     public static void main(String[] args) {
         launch(args);
     }

     @Override
     public void start(Stage stage) {
         stage.setTitle("Cube");
         stage.setScene(makeScene());
         stage.show();
         animate();
     }

     private Scene makeScene() {
         return SceneBuilder.create()
             .width(500)
             .height(500)
             .root(createRoot())
             .onMouseDragged(new MouseEventHandler())
             .camera(PerspectiveCameraBuilder.create()
                 .build())
             .depthBuffer(true)
             .build();
     }

     private Parent createRoot() {
         final Rectangle side1 = RectangleBuilder.create()
             .x(-100)
             .y(-100)
             .width(200)
             .height(200)
             .fill(Color.RED)
             .build();
         
         final Rotate side2Rotate = RotateBuilder.create()
                 .pivotX(-100)
                 .pivotY(-100)
                 .pivotZ(0)
                 .axis(Rotate.Y_AXIS)
                 .build();
             side2Rotate.angleProperty().bind(side2Angle);

             final Rectangle side2 = RectangleBuilder.create()
                 .x(-100)
                 .y(-100)
                 .width(200)
                 .height(200)
                 .fill(Color.GREEN)
                 .transforms(side2Rotate)
                 .build();

             final Rotate side3Rotate = RotateBuilder.create()
                 .pivotX(100)
                 .pivotY(-100)
                 .pivotZ(0)
                 .axis(new Point3D(-1, 0, 0))
                 .build();
             side3Rotate.angleProperty().bind(side3Angle);

             final Rectangle side3 = RectangleBuilder.create()
                 .x(-100)
                 .y(-100)
                 .width(200)
                 .height(200)
                 .fill(Color.BLUE)
                 .transforms(side3Rotate)
                 .build();

             final Rotate side4Rotate = RotateBuilder.create()
                 .pivotX(100)
                 .pivotY(100)
                 .pivotZ(0)
                 .axis(new Point3D(0, -1, 0))
                 .build();
             side4Rotate.angleProperty().bind(side4Angle);

             final Rectangle side4 = RectangleBuilder.create()
                 .x(-100)
                 .y(-100)
                 .width(200)
                 .height(200)
                 .fill(Color.CYAN)
                 .transforms(side4Rotate)
                 .build();

             final Rotate side5Rotate = RotateBuilder.create()
                 .pivotX(-100)
                 .pivotY(100)
                 .pivotZ(0)
                 .axis(Rotate.X_AXIS)
                 .build();
             side5Rotate.angleProperty().bind(side5Angle);

             final Rectangle side5 = RectangleBuilder.create()
                 .x(-100)
                 .y(-100)
                 .width(200)
                 .height(200)
                 .fill(Color.MAGENTA)
                 .transforms(side5Rotate)
                 .build();

             final Rectangle side6 = RectangleBuilder.create()
                 .x(-100)
                 .y(-100)
                 .width(200)
                 .height(200)
                 .fill(Color.YELLOW)
                 .build();

             side1.translateZProperty().set(100);
             side2.translateZProperty().set(100);
             side3.translateZProperty().set(100);
             side4.translateZProperty().set(100);
             side5.translateZProperty().set(100);
             side6.translateZProperty().bind(side6TranslateZ.add(100));

         
         final Rotate rootRotateX = RotateBuilder.create()
              .pivotX(0)
              .pivotY(0)
              .pivotZ(0)
              .axis(Rotate.X_AXIS)
              .build();
          rootRotateX.angleProperty().bind(rootAngleX);
          
         final Rotate rootRotateY = RotateBuilder.create()
            .pivotX(0)
            .pivotY(0)
            .pivotZ(0)
            .axis(Rotate.Y_AXIS)
            .build();
          rootRotateY.angleProperty().bind(rootAngleY);

         return GroupBuilder.create()
             .children(side1, side6, side2, side3, side4, side5)
             .translateX(250)
             .translateY(250)
             .transforms(rootRotateX, rootRotateY)
             .build();
     }
     
     private void animate() {
         TimelineBuilder.create()
             .keyFrames(
                 new KeyFrame(
                     Duration.seconds(0),
                     new KeyValue(side2Angle, 0),
                     new KeyValue(side3Angle, 0),
                     new KeyValue(side4Angle, 0),
                     new KeyValue(side5Angle, 0),
                     new KeyValue(side6TranslateZ, 0)
                 ),
                 new KeyFrame(
                     Duration.seconds(1),
                     new KeyValue(side2Angle, 0),
                     new KeyValue(side3Angle, 0),
                     new KeyValue(side4Angle, 0),
                     new KeyValue(side5Angle, 90),
                     new KeyValue(side6TranslateZ, 0)
                 ),
                 new KeyFrame(
                     Duration.seconds(2),
                     new KeyValue(side2Angle, 0),
                     new KeyValue(side3Angle, 0),
                     new KeyValue(side4Angle, 90),
                     new KeyValue(side5Angle, 90),
                     new KeyValue(side6TranslateZ, 0)
                 ),
                 new KeyFrame(
                     Duration.seconds(3),
                     new KeyValue(side2Angle, 0),
                     new KeyValue(side3Angle, 90),
                     new KeyValue(side4Angle, 90),
                     new KeyValue(side5Angle, 90),
                     new KeyValue(side6TranslateZ, 0)
                 ),
                 new KeyFrame(
                     Duration.seconds(4),
                     new KeyValue(side2Angle, 90),
                     new KeyValue(side3Angle, 90),
                     new KeyValue(side4Angle, 90),
                     new KeyValue(side5Angle, 90),
                     new KeyValue(side6TranslateZ, 0)
                 ),
                 new KeyFrame(
                     Duration.seconds(5),
                     new KeyValue(side2Angle, 90),
                     new KeyValue(side3Angle, 90),
                     new KeyValue(side4Angle, 90),
                     new KeyValue(side5Angle, 90),
                     new KeyValue(side6TranslateZ, -200)
                 )
             )
             .build().play();
     }

     public class MouseEventHandler implements EventHandler<MouseEvent>{

   @Override
   public void handle(MouseEvent mouseEvent) {
    double mouseX = mouseEvent.getSceneX();
    double mouseY = mouseEvent.getSceneY();
    
    rootAngleX.set(mouseY);
    rootAngleY.set(mouseX);
   }
      
     }

}
Durch das Ändern, der Methode "onMouseDragged" in "makeScene" kann man sicherlich das Eingabegerät noch variieren - z.B. könnte man hier mit Touchevents arbeiten, oder die ganze Sache über die Tastatur steuern.

Die Animation kann man natürlich weglassen. Die Werte der Seiten, die die Position angeben müssen dann mit den entsprechenden Werten aus der Animation aufsummiert werden. Sonst liegen alle Seiten übereinander. 

Nodes in Tabellen (Combobox, Textfield, ToggleButton)

Ich hatte vor einigen Tagen Probleme mit dem Einfügen von TextFields und Comboboxen in einer Tabelle. Man findet dazu einiges im Netz, was jedoch schnell unübersichtlich wird.

Hier ein kleines einfaches Beispiel, wie es ganz einfach realisiert werden kann:

Zuerst wird Datenmodell für die Tabelle entworfen:


public class Datamodel {
 
 private SimpleObjectProperty<ComboBox<String>> combobox;
 
 private SimpleObjectProperty<TextField> textfield;
 
 private SimpleObjectProperty<ToggleButton> toggleButton;
 
 public Datamodel(){
  combobox = new SimpleObjectProperty<ComboBox<String>>();
  textfield = new SimpleObjectProperty<TextField>();
  toggleButton = new SimpleObjectProperty<ToggleButton>();
 }

 public ComboBox<String&gt getCombobox() {
  return combobox.get();
 }

 public void setCombobox(ComboBox<String&gt combobox) {
  this.combobox.set(combobox);
 }

 public TextField getTextfield() {
  return textfield.get();
 }

 public void setTextfield(TextField textfield) {
  this.textfield.set(textfield);
 }

 public ToggleButton getToggleButton() {
  return toggleButton.get();
 }

 public void setToggleButton(ToggleButton togglebutton) {
  this.toggleButton.set(togglebutton);
 }
 
}

Das Datenmodell enthält hier SimpleObjectProperties in denen die jeweiligen Nodes gespeichert werden.
Wichtig hierbei ist, dass die Variablenbezeichnungen in der Bezeichnungen der Getter- und Setter-Methoden auftreten - also diese am besten generieren lassen. Nachdem die Zugriffsmethoden automatisch erzeugt wurden, müssen die noch etwas umgeschrieben werden, damit auch die entsprechenden Nodes gesetzt bzw. zurückgegeben werden und nicht die Properties.

Da das Datenmodell implementiert ist, kann es nun an eine Tabelle gebunden werden. Hierzu wird erst einmal eine Tabelle mit den entsprechenden Spalten erstellt werden:


public class TableExample extends Application{
 
 private TableView<Datamodel> table;
 private TableColumn<Datamodel, ComboBox<String>> columnWithCombobox;
 private TableColumn<Datamodel, TextField> columnWithTextfield;
 private TableColumn<Datamodel, ToggleButton> columnWithTogglebutton;
 private Button bt;

 @Override
 public void start(Stage primeStage) throws Exception {
  Group group = new Group();
  Scene scene = new Scene(group, 500, 430);
  
  BorderPane pane = new BorderPane();
  pane.setCenter(initTable());
  pane.setBottom(initButton());
  group.getChildren().add(pane);
  
  primeStage.setScene(scene);
  primeStage.show();
 }
 
 private Node initTable() {
  table = new TableView<Datamodel>();
  columnWithCombobox = new TableColumn<Datamodel, ComboBox&ltString>>("1");
  columnWithCombobox.setCellValueFactory(new PropertyValueFactory<Datamodel, ComboBox<String>>("combobox"));
  columnWithCombobox.setMinWidth(200);
  columnWithTextfield = new TableColumn<Datamodel, TextField>("2");
  columnWithTextfield.setCellValueFactory(new PropertyValueFactory<Datamodel, TextField>("textfield"));
  columnWithTogglebutton = new TableColumn<Datamodel, ToggleButton>("3");
  columnWithTogglebutton.setCellValueFactory(new PropertyValueFactory<Datamodel, ToggleButton>("toggleButton"));
  columnWithTogglebutton.setMaxWidth(50);
  
  table.getColumns().addAll(columnWithCombobox, columnWithTextfield, columnWithTogglebutton);
  
  return table;
 }

In der Methode "initTable" wird die Tabelle dementsprechend konfiguriert. Wichtig dabei ist das setzten der CellValueFactory. Die dazugehörige PropertyValueFactory wird durch das Datenmodell und dem Typ bestimme, den die Zelle verkörpern soll - also ComboBox, TextField, oder ToggleButton. Weiterhin muss die Variablenbezeichnung aus dem Datenmodel angegeben werden, um mit den Getter- und Setter-Methoden arbeiten zu können.


Um dann eine neue Zeile in die Tabelle einfügen zu können habe ich einen Button zu Hilfe genommen:

private Node initButton() {
  bt = new Button("Zeile hinzufügen");
  
  bt.setOnAction(new EventHandler<ActionEvent>() {
   
   @Override
   public void handle(ActionEvent paramT) {
    addNewLineInTable();
   }

  });
  
  return bt;
 }



 private void addNewLineInTable() {
  ComboBox<String> combobox = new ComboBox<String>();
  combobox.setMinWidth(190);
  combobox.setPromptText("PromtText");
  TextField textfield = new TextField();
  ToggleButton togglebutton = new ToggleButton();
  
  Datamodel datamodel = new Datamodel();
  datamodel.setCombobox(combobox);
  datamodel.setTextfield(textfield);
  datamodel.setToggleButton(togglebutton);
  
  table.getItems().add(datamodel);
 }
Der Button ruft beim Klick "addNewLineInTable" auf. In dieser Methode werden die gewünschten Nodes instantiiert und an den entsprechenden Variablen des Datenmodells gesetzt. Zum Schluss wird das neue Exemplar des Datenmodells als neues Item in der Tabelle hinzugefügt.