Pergunta

Para um simples desenho vetorial aplicativo eu estou olhando para implementar uma "Caixa de Selecção", que é uma representação gráfica da layoutBounds de um Nó.

Exemplo:

example

Graças a jewelsea e sua BoundsExample, Eu tenho uma boa compreensão sobre como obter os dados para a caixa.A parte que eu estou lutando com é, na verdade, o desenho da caixa em cena, de uma maneira que corretamente respeita as transformações em nós.

Corretamente, neste caso, significa que os limites de tamanho lógico obter dimensionado com um nó, mas o traço da caixa de seleção permanece constante.O significado de uma caixa de seleção de escalas com o seu nó correspondente, mas o traço permanece fora de escala.

Eu posso pensar em duas estratégias gerais para implementar uma caixa de seleção.

  1. Como uma propriedade de meu costume nó A caixa de seleção poderia ser um detalhes internos do meu nó personalizados com a sua visibilidade vinculado para os nós selecionados no estado.No caso eu precisaria encontrar uma forma para que nós IGNORAR os pais transformações, se é que isso é possível.

  2. Desenho de caixas de seleção no topo da escala de nós em um painel transparente No caso eu gostaria de vincular as caixas de seleção para os limites do layout da escala de nós, depois de aplicar as transformações de um nó para seus limites.Isso não parece acontecer na JFX (mesmo para os "boundsInParent') como você pode testar rapidamente no exemplo aplicando-se alguma escala para o 'grupo' na linha de ~122.

Modificado exemplo de dimensionamento:

package application;

import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.RadioButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.effect.DropShadow;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Shape;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.shape.StrokeType;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.stage.WindowEvent;

/** Demo for understanding JavaFX Layout Bounds */
public class BoundsPlayground extends Application
{
  final ObservableList<Shape>     shapes             = FXCollections.observableArrayList();
  final ObservableList<ShapePair> intersections      = FXCollections.observableArrayList();
  ObjectProperty<BoundsType>      selectedBoundsType = new SimpleObjectProperty<BoundsType>(
                                                         BoundsType.LAYOUT_BOUNDS );

  public static void main( final String[] args )
  {
    launch( args );
  }
  @Override
  public void start( final Stage stage )
  {
    stage.setTitle( "Bounds Playground" );
    // define some objects to manipulate on the scene.
    final Circle greenCircle = new Circle( 100, 100, 50, Color.FORESTGREEN );
    greenCircle.setId( "Green Circle" );
    final Circle redCircle = new Circle( 300, 200, 50, Color.FIREBRICK );
    redCircle.setId( "Red Circle" );

    final Line line = new Line( 25, 300, 375, 200 );
    line.setId( "Line" );
    line.setStrokeLineCap( StrokeLineCap.ROUND );
    line.setStroke( Color.MIDNIGHTBLUE );
    line.setStrokeWidth( 5 );

    final Anchor anchor1 = new Anchor( "Anchor 1", line.startXProperty(), line.startYProperty() );
    final Anchor anchor2 = new Anchor( "Anchor 2", line.endXProperty(), line.endYProperty() );

    final Group group = new Group( greenCircle, redCircle, line, anchor1, anchor2 );

    // monitor intersections of shapes in the scene.
    for ( final Node node : group.getChildrenUnmodifiable() )
    {
      if ( node instanceof Shape )
      {
        shapes.add( (Shape) node );
      }
    }
    testIntersections();

    // enable dragging for the scene objects.
    final Circle[] circles = { greenCircle, redCircle, anchor1, anchor2 };
    for ( final Circle circle : circles )
    {
      enableDrag( circle );
      circle.centerXProperty().addListener( new ChangeListener<Number>()
      {
        @Override
        public void changed( final ObservableValue<? extends Number> observableValue, final Number oldValue,
                             final Number newValue )
        {
          testIntersections();
        }
      } );
      circle.centerYProperty().addListener( new ChangeListener<Number>()
      {
        @Override
        public void changed( final ObservableValue<? extends Number> observableValue, final Number oldValue,
                             final Number newValue )
        {
          testIntersections();
        }
      } );
    }

    // define an overlay to show the layout bounds of the scene's shapes.
    final Group layoutBoundsOverlay = new Group();
    layoutBoundsOverlay.setMouseTransparent( true );
    for ( final Shape shape : shapes )
    {
      if ( !(shape instanceof Anchor) )
      {
        layoutBoundsOverlay.getChildren().add( new BoundsDisplay( shape ) );
      }
    }
    // layout the scene.
    final StackPane background = new StackPane();
    background.setStyle( "-fx-background-color: cornsilk;" );
    final Scene scene = new Scene( new Group( background, group, layoutBoundsOverlay ), 600, 500 );

    group.setScaleX( 5 );
    group.setScaleY( 5 );


    background.prefHeightProperty().bind( scene.heightProperty() );
    background.prefWidthProperty().bind( scene.widthProperty() );
    stage.setScene( scene );
    stage.show();

    createUtilityWindow( stage, layoutBoundsOverlay, new Shape[]{ greenCircle, redCircle } );
  }

  // update the list of intersections.
  private void testIntersections()
  {
    intersections.clear();
    // for each shape test it's intersection with all other shapes.
    for ( final Shape src : shapes )
    {
      for ( final Shape dest : shapes )
      {
        final ShapePair pair = new ShapePair( src, dest );
        if ( !(pair.a instanceof Anchor) && !(pair.b instanceof Anchor) && !intersections.contains( pair )
            && pair.intersects( selectedBoundsType.get() ) )
        {
          intersections.add( pair );
        }
      }
    }
  }

  // make a node movable by dragging it around with the mouse.
  private void enableDrag( final Circle circle )
  {
    final Delta dragDelta = new Delta();
    circle.setOnMousePressed( new EventHandler<MouseEvent>()
    {
      @Override
      public void handle( final MouseEvent mouseEvent )
      {
        // record a delta distance for the drag and drop operation.
        dragDelta.x = circle.getCenterX() - mouseEvent.getX();
        dragDelta.y = circle.getCenterY() - mouseEvent.getY();
        circle.getScene().setCursor( Cursor.MOVE );
      }
    } );
    circle.setOnMouseReleased( new EventHandler<MouseEvent>()
    {
      @Override
      public void handle( final MouseEvent mouseEvent )
      {
        circle.getScene().setCursor( Cursor.HAND );
      }
    } );
    circle.setOnMouseDragged( new EventHandler<MouseEvent>()
    {
      @Override
      public void handle( final MouseEvent mouseEvent )
      {
        circle.setCenterX( mouseEvent.getX() + dragDelta.x );
        circle.setCenterY( mouseEvent.getY() + dragDelta.y );
      }
    } );
    circle.setOnMouseEntered( new EventHandler<MouseEvent>()
    {
      @Override
      public void handle( final MouseEvent mouseEvent )
      {
        if ( !mouseEvent.isPrimaryButtonDown() )
        {
          circle.getScene().setCursor( Cursor.HAND );
        }
      }
    } );
    circle.setOnMouseExited( new EventHandler<MouseEvent>()
    {
      @Override
      public void handle( final MouseEvent mouseEvent )
      {
        if ( !mouseEvent.isPrimaryButtonDown() )
        {
          circle.getScene().setCursor( Cursor.DEFAULT );
        }
      }
    } );
  }

  // a helper enumeration of the various types of bounds we can work with.
  enum BoundsType
  {
    LAYOUT_BOUNDS, BOUNDS_IN_LOCAL, BOUNDS_IN_PARENT
  }

  // a translucent overlay display rectangle to show the bounds of a Shape.
  class BoundsDisplay extends Rectangle
  {
    // the shape to which the bounds display has been type.
    final Shape                    monitoredShape;
    private ChangeListener<Bounds> boundsChangeListener;

    BoundsDisplay( final Shape shape )
    {
      setFill( Color.LIGHTGRAY.deriveColor( 1, 1, 1, 0.35 ) );
      setStroke( Color.LIGHTGRAY.deriveColor( 1, 1, 1, 0.5 ) );
      setStrokeType( StrokeType.INSIDE );
      setStrokeWidth( 3 );
      monitoredShape = shape;
      monitorBounds( BoundsType.LAYOUT_BOUNDS );
    }

    // set the type of the shape's bounds to monitor for the bounds display.
    void monitorBounds( final BoundsType boundsType )
    {
      // remove the shape's previous boundsType.
      if ( boundsChangeListener != null )
      {
        final ReadOnlyObjectProperty<Bounds> oldBounds;
        switch ( selectedBoundsType.get() )
        {
          case LAYOUT_BOUNDS:
            oldBounds = monitoredShape.layoutBoundsProperty();
            break;
          case BOUNDS_IN_LOCAL:
            oldBounds = monitoredShape.boundsInLocalProperty();
            break;
          case BOUNDS_IN_PARENT:
            oldBounds = monitoredShape.boundsInParentProperty();
            break;
          default :
            oldBounds = null;
        }
        if ( oldBounds != null )
        {
          oldBounds.removeListener( boundsChangeListener );
        }
      }

      // determine the shape's bounds for the given boundsType.
      final ReadOnlyObjectProperty<Bounds> bounds;
      switch ( boundsType )
      {
        case LAYOUT_BOUNDS:
          bounds = monitoredShape.layoutBoundsProperty();
          break;
        case BOUNDS_IN_LOCAL:
          bounds = monitoredShape.boundsInLocalProperty();
          break;
        case BOUNDS_IN_PARENT:
          bounds = monitoredShape.boundsInParentProperty();
          break;
        default :
          bounds = null;
      }

      // set the visual bounds display based upon the new bounds and keep it in sync.
      if ( bounds != null )
      {
        updateBoundsDisplay( bounds.get() );

        // keep the visual bounds display based upon the new bounds and keep it in sync.
        boundsChangeListener = new ChangeListener<Bounds>()
        {
          @Override
          public void changed( final ObservableValue<? extends Bounds> observableValue,
                               final Bounds oldBounds, final Bounds newBounds )
          {
            updateBoundsDisplay( newBounds );
          }
        };
        bounds.addListener( boundsChangeListener );
      }
    }

    // update this bounds display to match a new set of bounds.
    private void updateBoundsDisplay( final Bounds newBounds )
    {
      setX( newBounds.getMinX() );
      setY( newBounds.getMinY() );
      setWidth( newBounds.getWidth() );
      setHeight( newBounds.getHeight() );
    }
  }
  // an anchor displayed around a point.
  class Anchor extends Circle
  {
    Anchor( final String id, final DoubleProperty x, final DoubleProperty y )
    {
      super( x.get(), y.get(), 10 );
      setId( id );
      setFill( Color.GOLD.deriveColor( 1, 1, 1, 0.5 ) );
      setStroke( Color.GOLD );
      setStrokeWidth( 2 );
      setStrokeType( StrokeType.OUTSIDE );
      x.bind( centerXProperty() );
      y.bind( centerYProperty() );
    }
  }
  // records relative x and y co-ordinates.
  class Delta
  {
    double x, y;
  }
  // records a pair of (possibly) intersecting shapes.
  class ShapePair
  {
    private final Shape a, b;
    public ShapePair( final Shape src, final Shape dest )
    {
      a = src;
      b = dest;
    }

    public boolean intersects( final BoundsType boundsType )
    {
      if ( a == b )
      {
        return false;
      }
      a.intersects( b.getBoundsInLocal() );
      switch ( boundsType )
      {
        case LAYOUT_BOUNDS:
          return a.getLayoutBounds().intersects( b.getLayoutBounds() );
        case BOUNDS_IN_LOCAL:
          return a.getBoundsInLocal().intersects( b.getBoundsInLocal() );
        case BOUNDS_IN_PARENT:
          return a.getBoundsInParent().intersects( b.getBoundsInParent() );
        default :
          return false;
      }
    }

    @Override
    public String toString()
    {
      return a.getId() + " : " + b.getId();
    }

    @Override
    public boolean equals( final Object other )
    {
      final ShapePair o = (ShapePair) other;
      return o != null && (a == o.a && b == o.b || a == o.b && b == o.a);
    }

    @Override
    public int hashCode()
    {
      int result = a != null ? a.hashCode() : 0;
      result = 31 * result + (b != null ? b.hashCode() : 0);
      return result;
    }
  }

  // define a utility stage for reporting intersections.
  private void createUtilityWindow( final Stage stage, final Group boundsOverlay,
                                    final Shape[] transformableShapes )
  {
    final Stage reportingStage = new Stage();
    reportingStage.setTitle( "Control Panel" );
    reportingStage.initStyle( StageStyle.UTILITY );
    reportingStage.setX( stage.getX() + stage.getWidth() );
    reportingStage.setY( stage.getY() );

    // define content for the intersections utility panel.
    final ListView<ShapePair> intersectionView = new ListView<ShapePair>( intersections );
    final Label instructions = new Label( "Click on any circle in the scene to the left to drag it around." );
    instructions.setMinSize( Control.USE_PREF_SIZE, Control.USE_PREF_SIZE );
    instructions.setStyle( "-fx-font-weight: bold; -fx-text-fill: darkgreen;" );

    final Label intersectionInstructions =
        new Label( "Any intersecting bounds in the scene will be reported below." );
    instructions.setMinSize( Control.USE_PREF_SIZE, Control.USE_PREF_SIZE );

    // add the ability to set a translate value for the circles.
    final CheckBox translateNodes = new CheckBox( "Translate circles" );
    translateNodes.selectedProperty().addListener( new ChangeListener<Boolean>()
    {
      @Override
      public void changed( final ObservableValue<? extends Boolean> observableValue, final Boolean oldValue,
                           final Boolean doTranslate )
      {
        if ( doTranslate )
        {
          for ( final Shape shape : transformableShapes )
          {
            shape.setTranslateY( 100 );
            testIntersections();
          }
        }
        else
        {
          for ( final Shape shape : transformableShapes )
          {
            shape.setTranslateY( 0 );
            testIntersections();
          }
        }
      }
    } );
    translateNodes.selectedProperty().set( false );

    // add the ability to add an effect to the circles.
    final Label modifyInstructions = new Label( "Modify visual display aspects." );
    modifyInstructions.setStyle( "-fx-font-weight: bold;" );
    modifyInstructions.setMinSize( Control.USE_PREF_SIZE, Control.USE_PREF_SIZE );
    final CheckBox effectNodes = new CheckBox( "Add an effect to circles" );
    effectNodes.selectedProperty().addListener( new ChangeListener<Boolean>()
    {
      @Override
      public void changed( final ObservableValue<? extends Boolean> observableValue, final Boolean oldValue,
                           final Boolean doTranslate )
      {
        if ( doTranslate )
        {
          for ( final Shape shape : transformableShapes )
          {
            shape.setEffect( new DropShadow() );
            testIntersections();
          }
        }
        else
        {
          for ( final Shape shape : transformableShapes )
          {
            shape.setEffect( null );
            testIntersections();
          }
        }
      }
    } );
    effectNodes.selectedProperty().set( true );

    // add the ability to add a stroke to the circles.
    final CheckBox strokeNodes = new CheckBox( "Add outside strokes to circles" );
    strokeNodes.selectedProperty().addListener( new ChangeListener<Boolean>()
    {
      @Override
      public void changed( final ObservableValue<? extends Boolean> observableValue, final Boolean oldValue,
                           final Boolean doTranslate )
      {
        if ( doTranslate )
        {
          for ( final Shape shape : transformableShapes )
          {
            shape.setStroke( Color.LIGHTSEAGREEN );
            shape.setStrokeWidth( 10 );
            testIntersections();
          }
        }
        else
        {
          for ( final Shape shape : transformableShapes )
          {
            shape.setStrokeWidth( 0 );
            testIntersections();
          }
        }
      }
    } );
    strokeNodes.selectedProperty().set( true );
    // add the ability to show or hide the layout bounds overlay.
    final Label showBoundsInstructions = new Label( "The gray squares represent layout bounds." );
    showBoundsInstructions.setStyle( "-fx-font-weight: bold;" );
    showBoundsInstructions.setMinSize( Control.USE_PREF_SIZE, Control.USE_PREF_SIZE );
    final CheckBox showBounds = new CheckBox( "Show Bounds" );
    boundsOverlay.visibleProperty().bind( showBounds.selectedProperty() );
    showBounds.selectedProperty().set( true );

    // create a container for the display control checkboxes.
    final VBox displayChecks = new VBox( 10 );
    displayChecks.getChildren().addAll( modifyInstructions, translateNodes, effectNodes, strokeNodes,
        showBoundsInstructions, showBounds );

    // create a toggle group for the bounds type to use.
    final ToggleGroup boundsToggleGroup = new ToggleGroup();
    final RadioButton useLayoutBounds = new RadioButton( "Use Layout Bounds" );
    final RadioButton useBoundsInLocal = new RadioButton( "Use Bounds in Local" );
    final RadioButton useBoundsInParent = new RadioButton( "Use Bounds in Parent" );
    useLayoutBounds.setToggleGroup( boundsToggleGroup );
    useBoundsInLocal.setToggleGroup( boundsToggleGroup );
    useBoundsInParent.setToggleGroup( boundsToggleGroup );
    final VBox boundsToggles = new VBox( 10 );
    boundsToggles.getChildren().addAll( useLayoutBounds, useBoundsInLocal, useBoundsInParent );

    // change the layout bounds display depending on which bounds type has been selected.
    useLayoutBounds.selectedProperty().addListener( new ChangeListener<Boolean>()
    {
      @Override
      public void changed( final ObservableValue<? extends Boolean> observableValue, final Boolean aBoolean,
                           final Boolean isSelected )
      {
        if ( isSelected )
        {
          for ( final Node overlay : boundsOverlay.getChildren() )
          {
            ((BoundsDisplay) overlay).monitorBounds( BoundsType.LAYOUT_BOUNDS );
          }
          selectedBoundsType.set( BoundsType.LAYOUT_BOUNDS );
          testIntersections();
        }
      }
    } );
    useBoundsInLocal.selectedProperty().addListener( new ChangeListener<Boolean>()
    {
      @Override
      public void changed( final ObservableValue<? extends Boolean> observableValue, final Boolean aBoolean,
                           final Boolean isSelected )
      {
        if ( isSelected )
        {
          for ( final Node overlay : boundsOverlay.getChildren() )
          {
            ((BoundsDisplay) overlay).monitorBounds( BoundsType.BOUNDS_IN_LOCAL );
          }
          selectedBoundsType.set( BoundsType.BOUNDS_IN_LOCAL );
          testIntersections();
        }
      }
    } );
    useBoundsInParent.selectedProperty().addListener( new ChangeListener<Boolean>()
    {
      @Override
      public void changed( final ObservableValue<? extends Boolean> observableValue, final Boolean aBoolean,
                           final Boolean isSelected )
      {
        if ( isSelected )
        {
          for ( final Node overlay : boundsOverlay.getChildren() )
          {
            ((BoundsDisplay) overlay).monitorBounds( BoundsType.BOUNDS_IN_PARENT );
          }
          selectedBoundsType.set( BoundsType.BOUNDS_IN_PARENT );
          testIntersections();
        }
      }
    } );
    useLayoutBounds.selectedProperty().set( true );

    final WebView boundsExplanation = new WebView();
    boundsExplanation
        .getEngine()
        .loadContent(
            "<html><body bgcolor='darkseagreen' fgcolor='lightgrey' style='font-size:12px'><dl>"
                + "<dt><b>Layout Bounds</b></dt><dd>The boundary of the shape.</dd><br/>"
                + "<dt><b>Bounds in Local</b></dt><dd>The boundary of the shape and effect.</dd><br/>"
                + "<dt><b>Bounds in Parent</b></dt><dd>The boundary of the shape, effect and transforms.<br/>The co-ordinates of what you see.</dd>"
                + "</dl></body></html>" );
    boundsExplanation.setPrefWidth( 100 );
    boundsExplanation.setMinHeight( 130 );
    boundsExplanation.setMaxHeight( 130 );
    boundsExplanation.setStyle( "-fx-background-color: transparent" );

    // layout the utility pane.
    final VBox utilityLayout = new VBox( 10 );
    utilityLayout
        .setStyle( "-fx-padding:10; -fx-background-color: linear-gradient(to bottom, lightblue, derive(lightblue, 20%));" );
    utilityLayout.getChildren().addAll( instructions, intersectionInstructions, intersectionView,
        displayChecks, boundsToggles, boundsExplanation );
    utilityLayout.setPrefHeight( 530 );
    reportingStage.setScene( new Scene( utilityLayout ) );
    reportingStage.show();

    // ensure the utility window closes when the main app window closes.
    stage.setOnCloseRequest( new EventHandler<WindowEvent>()
    {
      @Override
      public void handle( final WindowEvent windowEvent )
      {
        reportingStage.close();
      }
    } );
  }
}

Desde que eu sou bastante novo para a JFX eu queria pedir conselhos.Espero que você encontrar esse problema interessante :)

Melhores cumprimentos, Oliver.

Foi útil?

Solução

Como se vê, o SceneBuilder si mesmo é a maior livremente disponível JavaFX projeto que eu conheço, que tem que cada problema já resolvido.

Ao estudar a com.oracle.javafx.scenebuilder.kit.editor pacote de SceneBuilder código-fonte Fiquei descansada, que a minha segunda proposta de startegy é o caminho a percorrer.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top