JavaFX中的绘图变换独立布局边界 [英] Drawing transform independent layout bounds in JavaFX

查看:71
本文介绍了JavaFX中的绘图变换独立布局边界的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

对于一个简单的矢量绘图应用程序,我正在寻求实现选择框",即节点的layoutBounds的图形表示.

For a simple vector drawing app I am looking to implement a "Selection Box", that is a graphic representation of the layoutBounds of a Node.

示例:

感谢 jewelsea 和他的

Thanks to jewelsea and his BoundsExample, I have a good understanding now on how to get the data for the box. The part I am struggling with is actually drawing the box on the scene, in a way that correctly respects the transformations on the nodes.

在这种情况下,正确地表示边界逻辑大小将随节点缩放,但选择框的笔触保持恒定.这意味着选择框会随着其相应的节点缩放,但是笔触保持未缩放状态.

Correctly in this case means the bounds logical size get scaled with a node, but the stroke of the selection box stays constant. Meaning a selection box scales with its corresponding node, but the stroke stays unscaled.

我可以想到两种实现这种选择框的通用策略.

I can think of two general strategies to implement such a selection box.

  1. 作为我的自定义节点的属性 选择框可能是我的自定义节点的内部详细信息,其可见性绑定到节点选择状态.在那种情况下,如果可能的话,我需要找到一种方法让节点忽略父转换.

  1. As a property of my custom node The selection box could be an internal details of my custom node with its visibility bound to the nodes selected state. In that case I would need to find a way to have nodes IGNORE the parents transformations, if that is possible.

在透明窗格的缩放节点顶部绘制选择框 在那种情况下,在将节点的变换应用于其边界之后,我会将选择框绑定到缩放后的节点的布局边界.这似乎在JFX中不会发生(即使对于'boundsInParent'也是如此),因为您可以在示例通过对〜122行中的'group'应用一些缩放.

Drawing selection boxes on top of the scaled nodes on a transparent pane In that case I would bind the selection boxes to the layout bounds of the scaled nodes, after applying the transformations of a node to its bounds. This does not seem to happen in JFX (even for 'boundsInParent') as you can quickly test in the example by applying some scaling to 'group' in line ~122.

修改后的缩放比例示例:

Modified example with scaling:

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


由于我是JFX的新手,所以我想寻求建议.希望您发现这个问题有趣:)


Since I am fairly new to JFX I wanted to ask for advice. Hope you find this problem interesting :)

最诚挚的问候,奥利弗.

Best regards, Oliver.

推荐答案

事实证明,SceneBuilder本身是我所知道的最大的可免费使用的JavaFX项目,该问题已经解决.

As it turns out, the SceneBuilder itself is the largest freely available JavaFX project that I know of, that has that excact problem already solved.

通过研究SceneBuilder的com.oracle.javafx.scenebuilder.kit.editor源代码让我放心的是,我提议的第二个策略是要走的路.

By studying the com.oracle.javafx.scenebuilder.kit.editor package of the SceneBuilder source code I was reassured, that my second proposed startegy is the way to go.

这篇关于JavaFX中的绘图变换独立布局边界的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆