Fixed size JavaFX component - javafx

Creating new components in JavaFX is still a but muddy to me compared to "Everything is a JPanel" in Swing.
I'm trying to make a fixed size component. I hesitate to call it a control, it's a pane of activity, not a button.
But here's my problem.
The fixed size I want is smaller than the contents of the element.
The grid is, in truth, 200x200. I'm shifting it up and left 25x25, and I'm trying to make the fixed size of 150x150. You can see in my example I've tried assorted ways of forcing it to 150, but in my tests, the size never sticks. Also, to be clear, I would expect the lines to clip at the boundary of the component.
This is, roughly, what I'm shooting for in my contrived case (note this looks bigger than 150x150 because of the retina display on my Mac, which doubles everything):
I've put some in to a FlowPane, and they stack right up, but ignore the 150x150 dimensions.
FlowPane fp = new FlowPane(new TestPane(), new TestPane(), new TestPane());
var scene = new Scene(fp, 640, 480);
stage.setScene(scene);
I tried sticking one in a ScrollPane, and the scroll bars never appear, even after resizing the window.
TestPane pane = new TestPane();
ScrollPane sp = new ScrollPane(pane);
var scene = new Scene(sp, 640, 480);
stage.setScene(scene);
And I struggle to discern whether I should be extending Region or Control in these cases.
I am missing something fundamental.
package pkg;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.shape.Line;
import javafx.scene.transform.Translate;
public class TestPane extends Control {
public TestPane() {
setMinHeight(150);
setMaxHeight(150);
setMinWidth(150);
setMaxWidth(150);
setPrefHeight(150);
setPrefWidth(150);
populate();
}
#Override
protected double computePrefHeight(double width) {
return 150;
}
#Override
protected double computePrefWidth(double height) {
return 150;
}
#Override
protected double computeMaxHeight(double width) {
return 150;
}
#Override
protected double computeMaxWidth(double height) {
return 150;
}
#Override
protected double computeMinHeight(double width) {
return 150;
}
#Override
protected double computeMinWidth(double height) {
return 150;
}
#Override
public boolean isResizable() {
return false;
}
private void populate() {
Translate translate = new Translate();
translate.setX(-25);
translate.setY(-25);
getTransforms().clear();
getTransforms().addAll(translate);
ObservableList<Node> children = getChildren();
for (int i = 0; i < 4; i++) {
Line line = new Line(0, i * 50, 200, i * 50);
children.add(line);
line = new Line(i * 50, 0, i * 50, 200);
children.add(line);
}
}
}
Addenda, to clarify.
I want a fixed sized component. It's a rectangle. I want it X x Y big.
I want to draw things in my box. Lines, circles, text.
I want the things I draw to clip to the boundaries of the component.
I don't want to use Canvas.
More addenda.
What I'm looking for is not much different from what a ScrollPane does, save I don't want any scroll bars, and I don't want the size of the outlying pane to grow or shrink.

TLDR:
Subclass Region,
make isResizable() return true to respect pref, min, and max sizes,
explicitly set a clip to avoid painting outside the local bounds.
Most of the documentation for this is in the package documentation for javafx.scene.layout
First, note the distinction between resizable and non-resizable nodes. Resizable nodes (for which isResizable() returns true) are resized by their parent during layout, and the parent will make a best-effort to respect their preferred, minimum, and maximum sizes.
Non-resizable nodes are not resized by their parent. If isResizable() returns false, then resize() is a no-op and the preferred, minimum, and maximum sizes are effectively ignored. Their sizes are computed internally and reported to the parent via its visual bounds. Ultimately, all JavaFX nodes have a peer node in the underlying graphical system, and AFAIK the only way a non-resizable node can determine its size is by directly setting the size of the peer. (I'm happy to be corrected on this.)
So unless you want to get your hands really dirty with custom peer nodes (and I don't even know if the API has mechanisms for this), I think the preferred way to create a "fixed size node" is by creating a resizable node with preferred, minimum, and maximum sizes all set to the same value. This is likely by design: as noted in a comment to your question, fixed-size nodes in layout-driven UI toolkits are generally discouraged, other than very low-level components (Text, Shape, etc).
Transformations applied to resizable nodes are generally applied after layout (i.e. they don't affect the layout bounds). Therefore using a translation to manage the internal positioning of the child nodes is not a good approach; it will have effects on the layout of the custom node in the parent which you probably don't intend.
As you note, you are not really defining a control here; it has no behavior or skin. Thus subclassing Control is not really the rigth approach. The most appropriate hook in the API is to subclass Region. Override the layoutChildren() method to position the child nodes (for Shapes and Text nodes, set their coordinates, for resizable children call resizeRelocate(...)).
Finally, to prevent the node spilling out of its intended bounds (150x150 in your example), either ensure no child nodes are positioned outside those bounds, or explicitly set the clip.
Here's a refactoring of your example:
import javafx.scene.layout.Region;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
public class TestPane extends Region {
private Line[] verticalLines ;
private Line[] horizontalLines ;
private static final int WIDTH = 150 ;
private static final int HEIGHT = 150 ;
private static final int LINE_GAP = 50 ;
public TestPane() {
populate();
}
#Override
protected double computePrefHeight(double width) {
return HEIGHT;
}
#Override
protected double computePrefWidth(double height) {
return HEIGHT;
}
#Override
protected double computeMaxHeight(double width) {
return HEIGHT;
}
#Override
protected double computeMaxWidth(double height) {
return WIDTH;
}
#Override
protected double computeMinHeight(double width) {
return WIDTH;
}
#Override
protected double computeMinWidth(double height) {
return WIDTH;
}
#Override
public boolean isResizable() {
return true;
}
#Override
public void layoutChildren() {
double w = getWidth();
double h = getHeight() ;
double actualWidth = verticalLines.length * LINE_GAP ;
double actualHeight = horizontalLines.length * LINE_GAP ;
double hOffset = (actualWidth - w) / 2 ;
double vOffset = (actualHeight - h) / 2 ;
for (int i = 0 ; i < verticalLines.length ; i++) {
double x = i * LINE_GAP - hOffset;
verticalLines[i].setStartX(x);
verticalLines[i].setEndX(x);
verticalLines[i].setStartY(0);
verticalLines[i].setEndY(h);
}
for (int i = 0 ; i < horizontalLines.length ; i++) {
double y = i * LINE_GAP - vOffset;
horizontalLines[i].setStartY(y);
horizontalLines[i].setEndY(y);
horizontalLines[i].setStartX(0);
horizontalLines[i].setEndX(w);
}
setClip(new Rectangle(0, 0, w, h));
}
private void populate() {
verticalLines = new Line[4] ;
horizontalLines = new Line[4] ;
for (int i = 0 ; i <verticalLines.length ; i++) {
verticalLines[i] = new Line();
getChildren().add(verticalLines[i]);
}
for (int i = 0 ; i <horizontalLines.length ; i++) {
horizontalLines[i] = new Line();
getChildren().add(horizontalLines[i]);
}
}
}
A more sophisticated example might have, for example, LINE_GAP as a property. When that property changes you would call requestLayout() to mark the component as "dirty", so its layoutChildren() method would be called again on the next frame rendered.
Here's a quick test case:
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;
public class App extends Application {
#Override
public void start(Stage stage) {
FlowPane root = new FlowPane();
root.setAlignment(Pos.TOP_LEFT);
root.setPadding(new Insets(10));
root.setHgap(5);
root.setVgap(5);
for (int i = 0; i < 6 ; i++) {
root.getChildren().add(new TestPane());
}
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
Which results in:
This plays nicely with the layout pane; resizing the window gives

Related

java.lang.IllegalArgumentException: Problem and Hitbox

I have two problems one this that, if i want to show score with the circle object:
layoutV.getChildren().addAll(virus, score);
I get the following error:
Exception in thread "JavaFX Application Thread" java.lang.IllegalArgumentException: Children: duplicate children added: parent = Pane#6661fc86[styleClass=root].
As far as I understand it is because the Task wants to show multiple scores. So should I use another scene or layout to show score?
My other problem is the hitbox of the object, right know everytime i click the score goes up. I looked up the mouse event getTarget but it does not seem like I can make it so that my object is the only target to use the mouse event on.
public class Main extends Application {
private Stage window;
private Pane layoutV;
private Scene scene;
private Circle virus;
private int score;
private Label scores;
#Override
public void start(Stage primaryStage) {
window = primaryStage;
window.setTitle("Enemy TEST");
this.score = 0;
scores = new Label("Score "+ score);
layoutV = new Pane();
scene = new Scene(layoutV, 600, 600);
window.setScene(scene);
window.show();
Thread th = new Thread(task);
th.setDaemon(true);
th.start();
}
Task task = new Task<Void>() {
#Override
protected Void call() throws Exception {
while (true) {
Platform.runLater(new Runnable() {
#Override
public void run() {
drawCircles();
}
});
Thread.sleep(1000);
}
}
};
public void drawCircles() {
double x = (double)(Math.random() * ((550 - 50) + 1)) + 50;
double y = (double)(Math.random() * ((550 - 50) + 1)) + 50;
double r = (double)(Math.random() * ((30 - 10) + 1)) + 10;
virus = new Circle(x, y, r, Color.VIOLET);
layoutV.setOnMouseClicked(e -> {
if (e.getButton() == MouseButton.PRIMARY) {
layoutV.getChildren().remove(e.getTarget());
this.score++;
System.out.println("score: "+ this.score);
}
});
layoutV.getChildren().addAll(virus);
scene.setRoot(layoutV);
window.setScene(scene);
}
public static void main(String[] args) {
launch(args);
}
}
You have lots of issues, not just the ones from your question:
Although it will work as you coded
it, I don't advise spawning a thread to draw your circles, instead
see:
JavaFX periodic background task
You don't need to set the root in the scene and the scene in the
window every time you draw a new circle.
Nor do you need to set the
mouse handler on the layout every time you draw a circle.
Rather than setting a mouse handler on the layout, you are better off setting a mouse handler on the circles themselves (which you can do before you add them to the scene).
score is an int, not a node you can only add nodes to the scene
graph.
See the documentation for the scene package:
A node may occur at most once anywhere in the scene graph. Specifically, a node must appear no more than once in the children list of a Parent or as the clip of a Node. See the Node class for more details on these restrictions.
How you are adding the node more than once is not clear to me, because you are probably doing it in code different than the Main class you provided.
To add a circle with a score on top, use a StackPane with the score in a label, but make the label mouse transparent, so that it does not register any clicks:
Label scoreLabel = new Label(score + "");
scoreLabel.setMouseTransparent(true);
StackPane balloon = new StackPane(circle, scoreLabel);
layoutV.getChildren.add(balloon);
Add the click handler on the balloon.
And additional issues I don't detail here but are solved in the demo code provided.
To fix all your errors, I would write some code like below. Perhaps you can review it and compare it with your code to help understand one way to create this game.
The example code might not be exactly the functionality you are looking for (that is not really its purpose), but it should be enough to keep you on the right track for implementing your application.
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.text.*;
import javafx.stage.Stage;
import javafx.util.Duration;
import java.util.concurrent.ThreadLocalRandom;
public class Inoculation extends Application {
public static final int W = 600;
public static final int H = 600;
private final IntegerProperty score = new SimpleIntegerProperty(0);
private final Pane playingField = new Pane();
#Override
public void start(Stage stage) {
StackPane overlay = createOverlay();
Pane layout = new StackPane(playingField, overlay);
stage.setScene(new Scene(layout, W, H));
stage.show();
Infection infection = new Infection(playingField, score);
infection.begin();
}
private StackPane createOverlay() {
Label totalScoreLabel = new Label();
totalScoreLabel.textProperty().bind(
Bindings.concat(
"Score ", score.asString()
)
);
StackPane overlay = new StackPane(totalScoreLabel);
StackPane.setAlignment(totalScoreLabel, Pos.TOP_LEFT);
overlay.setMouseTransparent(true);
return overlay;
}
public static void main(String[] args) {
launch(args);
}
}
class Infection {
private static final Duration SPAWN_PERIOD = Duration.seconds(1);
private static final int NUM_SPAWNS = 10;
private final Timeline virusGenerator;
public Infection(Pane playingField, IntegerProperty score) {
virusGenerator = new Timeline(
new KeyFrame(
SPAWN_PERIOD,
event -> spawnVirus(
playingField,
score
)
)
);
virusGenerator.setCycleCount(NUM_SPAWNS);
}
public void begin() {
virusGenerator.play();
}
private void spawnVirus(Pane playingField, IntegerProperty score) {
Virus virus = new Virus();
virus.setOnMouseClicked(
event -> {
score.set(score.get() + virus.getVirusScore());
playingField.getChildren().remove(virus);
}
);
playingField.getChildren().add(virus);
}
}
class Virus extends StackPane {
private static final int MAX_SCORE = 3;
private static final int RADIUS_INCREMENT = 10;
private final int virusScore = nextRandInt(MAX_SCORE) + 1;
public Virus() {
double r = (MAX_SCORE + 1 - virusScore) * RADIUS_INCREMENT;
Circle circle = new Circle(
r,
Color.VIOLET
);
Text virusScoreText = new Text("" + virusScore);
virusScoreText.setBoundsType(TextBoundsType.VISUAL);
virusScoreText.setMouseTransparent(true);
getChildren().setAll(
circle,
virusScoreText
);
setLayoutX(nextRandInt((int) (Inoculation.W - circle.getRadius() * 2)));
setLayoutY(nextRandInt((int) (Inoculation.H - circle.getRadius() * 2)));
setPickOnBounds(false);
}
public int getVirusScore() {
return virusScore;
}
// next random int between 0 (inclusive) and bound (exclusive)
private int nextRandInt(int bound) {
return ThreadLocalRandom.current().nextInt(bound);
}
}
Some additional notes on this implementation that might be useful to know:
The total score is placed in an overlayPane so that it is not obscured by elements added to the playingField (which contains the virus spawns).
The overlayPane is made mouseTransparent, so that it won't intercept any mouse events, and the clicks will fall through to the items in the playing field.
The app currently generates viruses within a fixed field size, regardless of whether you resize the window. That is just the way it is designed and coded, you could code it otherwise if wished. It would be more work to do so.
The Bindings class is used to create a string expression binding which concatenates the static string "Score " with an integer property representing the score. This allows the string representing the score to be bound to the score label text in the overlay so that it automatically updates whenever the score is changed.
The virus generation uses a timeline and is based on the concepts from:
JavaFX periodic background task
The application class is kept deliberately simple to handle mostly just the core application lifecycle, and the actual functionality of the application is abstracted to an Infection class which handles the spawning of the virus and a Virus class that generates a new virus.
This technique is used to center a score for each individual virus on the virus:
how to put a text into a circle object to display it from circle's center?
The virus itself is laid out in a StackPane. The pane has pick on bounds set to false. To remove the virus infection, you must click on the circle which represents the virus and not just anywhere in the square for the stack pane.
Because circle coordinates are in local co-ordinates and the circle is in a parent stack pane representing the virus, the circle itself does not need x and y values set, instead layout x and y values are set on the enclosing pane to allow positioning of the pane representing the entire virus.
The following technique is used to generate random integers in acceptable ranges using ThreadLocalRandom:
How do I generate random integers within a specific range in Java?

JavaFX - Get Actual Computed-Size of HBOX

I have a simple HBOX control which uses USE_COMPUTED_SIZE in Pref-Height, hence the size is all calculated and adjusted by the controls inside, which are a couple VBOX.
The issue comes when I try to add a new Pane as a children to the HBOX and draw a vertical line from top to bottom of the HBOX, so I write my line:
int startX = 5;
int startY = 0;
int endX = 5;
Line line = new Line(startX,startY,endX,hbox.getHeight());
Here, I need the hbox.getHeight(), but surprise: it is =-1, because it is using USE_COMPUTED_SIZE. So, how can I get the real (computed) value of hbox.getHeight()?
Not tested, but I think you can do something like:
public class HBoxWithLine extends HBox {
// Example of configurable property:
private final DoubleProperty lineOffset = new SimpleDoubleProperty(5);
public DoubleProperty lineOffsetProperty() {
return lineOffset ;
}
public final double getLineOffset() {
return lineOffsetProperty().get();
}
public final void setLineOffset(double lineOffset) {
lineOffsetProperty().set(lineOffset);
}
private final Line line = new Line();
public HBoxWithLine() {
getChildren().add(line);
// request layout when offset is invalidated:
lineOffset.addListener(obs -> requestLayout());
}
#Override
protected void layoutChildren() {
line.setStartX(getLineOffset());
line.setEndX(getLineOffset());
line.setStartY(0);
line.setEndY(getHeight());
super.layoutChildren();
}
}
Now you can just create a HBoxWithLine and add (additional) child nodes to it, set it's pref width and height to either fixed values, or Region.USE_COMPUTED_SIZE, etc., and it should just work.

How to create fog in JavaFX 3D?

I am developping a game, in JavaFX 3D, in which the player is running down a long road. There are items and obstacles coming his way. The point is: now the objects just appear out of nowhere if they enter my clipping distance. Therefore I want to create some kind of fog (or anything else that "hides" the initialization of the objects)
The problem is that I cannot find any example in which this is done. I am looking for an example piece of code/link to source/any other advice.
Having said it's too broad a question, and there must be many, many ways to do this, here's one possible implementation. This is 2D but could easily be adapted for a 3D app (I think). The idea is just to let a few light gray circles drift around on a white background, and apply an enormous blur to the whole thing.
You could then let your objects appear with this as a background, and fade them in from gray to their real color (or some such). The colors and velocities of the circles, and the blur radius, probably need some tuning...
import java.util.Random;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.effect.GaussianBlur;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class FogExample extends Application {
private static final int WIDTH = 600 ;
private static final int HEIGHT = 600 ;
#Override
public void start(Stage primaryStage) {
Fog fog = new Fog(WIDTH, HEIGHT);
Scene scene = new Scene(new StackPane(fog.getView()), WIDTH, HEIGHT);
primaryStage.setScene(scene);
primaryStage.show();
}
public static class Fog {
private final int width ;
private final int height ;
private final Pane fog ;
private final Random RNG = new Random();
public Fog(int width, int height) {
this.width = width ;
this.height = height ;
this.fog = new Pane();
Rectangle rect = new Rectangle(0, 0, width, height);
rect.setFill(Color.rgb(0xe0, 0xe0, 0xe0));
fog.getChildren().add(rect);
for (int i = 0; i < 50; i++) {
fog.getChildren().add(createFogElement());
}
fog.setEffect(new GaussianBlur((width + height) / 2.5));
}
private Circle createFogElement() {
Circle circle = new Circle(RNG.nextInt(width - 50) + 25, RNG.nextInt(height - 50) + 25, 15 + RNG.nextInt(50));
int shade = 0xcf + RNG.nextInt(0x20);
circle.setFill(Color.rgb(shade, shade, shade));
AnimationTimer anim = new AnimationTimer() {
double xVel = RNG.nextDouble()*40 - 20 ;
double yVel = RNG.nextDouble()*40 - 20 ;
long lastUpdate = 0 ;
#Override
public void handle(long now) {
if (lastUpdate > 0) {
double elapsedSeconds = (now - lastUpdate) / 1_000_000_000.0 ;
double x = circle.getCenterX() ;
double y = circle.getCenterY() ;
if ( x + elapsedSeconds * xVel > width || x + elapsedSeconds * xVel < 0) {
xVel = - xVel ;
}
if ( y + elapsedSeconds * yVel > height || y + elapsedSeconds * yVel < 0) {
yVel = - yVel ;
}
circle.setCenterX(x + elapsedSeconds*xVel);
circle.setCenterY(y + elapsedSeconds * yVel);
}
lastUpdate = now ;
}
};
anim.start();
return circle ;
}
public Node getView() {
return fog ;
}
}
public static void main(String[] args) {
launch(args);
}
}
Typical 3D systems use a Particle system to do this which involves a combination of transparency, alpha fluctuations, and billboard textures. Believe it or not Fog, smoke and flames... particle effects... are not actually 3D at all but 2D images that have been positioned, sized, and colored to appear in 3D. They are then animated rapidly enough so that the human viewer can't make out the nuances of the 2D images.
The problem with JavaFX 3D is that it does not support transparency "yet".(it is unofficially available). Also there is no particle type object that will let you overlay shapes and textures as James_D suggests without manually managing the positioning with respect to the camera.
However there is hope... the F(X)yz project provides a BillBoard class which will allow you to place an image perpendicular to the camera. You can rapidly update this image using a Timer and some Random.next() type Circle creations for the fog using the approach James_D suggested. An example on using the BillBoard in a manner like this is in BillBoardBehaviourTest and CameraViewTest. Performance is demonstrated in CameraViewTest.
What I would do if I were you is setup a large frustrum wide BillBoard object at the back -Z position of your frustrum (where the objects enter the scene) and then setup a Timer/Random circle generator in a pane off screen. Then using the approach in the CameraViewTest, snapshot the off screen pane (which has the circles) and then set that image to the BillBoard. You will have to have the SnapShot call be on some timer itself to achieve an effect of animation. The CameraViewTest demonstrates how to do that. The effect to the user will be a bunch of moving blurry tiny circles.
Fog on the cheap!

How to get JavaFX TreeView to behave consistently upon node expansion?

I have a JavaFX TreeView with an invisible root and a handful of 'folder' TreeItems that have many 'file' TreeItems as children. The 'folder' TreeItems typically fit inside the TreeView without there being any scrollbars.
invisible-root/
folder/
folder/
folder/
file
file
file
...
file
Sometimes, when I expand a 'folder' TreeItem, the scrollbars appear but the scroll position remains the same. (This is what I want!) However, sometimes, expanding a TreeItem causes the scrollbars appear and the TableView scrolls to the last child of the expanded TreeItem!
This is very unexpected and surprising, especially since I have difficulty predicting which of the two behaviors I will see: (1) stay put, or (2) scroll to last item. Personally, I think behavior (1) is less surprising and preferable.
Any thoughts on how to deal with this?
I see this behavior on Java8u31.
The problem is in VirtualFlow. In layoutChildren() there is this section:
if (lastCellCount != cellCount) {
// The cell count has changed. We want to keep the viewport
// stable if possible. If position was 0 or 1, we want to keep
// the position in the same place. If the new cell count is >=
// the currentIndex, then we will adjust the position to be 1.
// Otherwise, our goal is to leave the index of the cell at the
// top consistent, with the same translation etc.
if (position == 0 || position == 1) {
// Update the item count
// setItemCount(cellCount);
} else if (currentIndex >= cellCount) {
setPosition(1.0f);
// setItemCount(cellCount);
} else if (firstCell != null) {
double firstCellOffset = getCellPosition(firstCell);
int firstCellIndex = getCellIndex(firstCell);
// setItemCount(cellCount);
adjustPositionToIndex(firstCellIndex);
double viewportTopToCellTop = -computeOffsetForCell(firstCellIndex);
adjustByPixelAmount(viewportTopToCellTop - firstCellOffset);
}
The problem arises if position is 1.0 (== scrolled to bottom), because in that case there is no recalculation. A workaround would be to override the TreeViewSkin to provide your own VirtualFlow and fix the behavior there.
The code below is meant to illustrate the problem, it's not a real solution, just a starting point if you really want to fix it:
import com.sun.javafx.scene.control.skin.TreeViewSkin;
import com.sun.javafx.scene.control.skin.VirtualFlow;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.IndexedCell;
import javafx.scene.control.Skin;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class TreeViewScrollBehaviour extends Application {
#Override
public void start(Stage primaryStage) {
TreeView treeView = new TreeView() {
#Override
protected Skin createDefaultSkin() {
return new TTreeViewSkin(this); //To change body of generated methods, choose Tools | Templates.
}
};
TreeItem<String> treeItem = new TreeItem<String>("Root");
for (int i = 0; i < 20; i++) {
TreeItem<String> treeItem1 = new TreeItem<>("second layer " + i);
treeItem.getChildren().add(treeItem1);
for (int j = 0; j < 20; j++) {
treeItem1.getChildren().add(new TreeItem<>("Third Layer " + j));
}
}
treeView.setRoot(treeItem);
StackPane root = new StackPane();
root.getChildren().addAll(treeView);
Scene scene = new Scene(root, 300, 250);
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
class TTreeViewSkin<T extends IndexedCell> extends TreeViewSkin<T> {
public TTreeViewSkin(TreeView treeView) {
super(treeView);
}
#Override
protected VirtualFlow createVirtualFlow() {
return new TVirtualFlow<T>(); //To change body of generated methods, choose Tools | Templates.
}
}
class TVirtualFlow<T extends IndexedCell> extends VirtualFlow<T> {
#Override
public double getPosition() {
double position = super.getPosition();
if (position == 1.0d) {
return 0.99999999999;
}
return super.getPosition(); //To change body of generated methods, choose Tools | Templates.
}
#Override
public void setPosition(double newPosition) {
if (newPosition == 1.0d) {
newPosition = 0.99999999999;
}
super.setPosition(newPosition); //To change body of generated methods, choose Tools | Templates.
}
}
}

Java FX SplitPane

I have two Canvas instances in my SplitPane. When I drag the divider bar, one Canvas grows. When I drag the divider back, the Canvas doesn't really shrink. It holds the same maximum dimension as it was ever given by the resize. So I only see a part of that Canvas, however much space the SplitPane can show. Similar behavior occurs for the other Canvas. I need them to shrink back to fit into their respective sections when the divider is dragged, not just have them clipped.
The Canvases are wrapped in a one-element GridPane subclass before being added to the SplitPane. This wrapper class resizes the Canvas whenever the wrapper resizes.
The problem is that the SplitPane is giving the wrong sizes: whatever the maximum size ever was for that Canvas. Why would the SplitPane think this is the right thing to do?
package experimental;
import javafx.application.Application;
import javafx.geometry.Orientation;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.control.SplitPane;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;
public class SplitPaneProblem extends Application {
static public void main(String[] args) {launch(args);}
public SplitPaneProblem() { }
#Override
public void start(Stage primaryStage) throws Exception {
SplitPane splitPane = new SplitPane();
splitPane.setOrientation(Orientation.VERTICAL);
splitPane.getItems().add(new TestPane("0",new Canvas()));
splitPane.getItems().add(new TestPane("1",new Canvas()));
primaryStage.setScene(new Scene(splitPane));
primaryStage.setHeight(400);
primaryStage.setWidth(400);
primaryStage.show();
}
public class TestPane extends AnchorPane {
String name;
Canvas canvas;
public TestPane(String name,Canvas canvas) {
super();
this.canvas = canvas;
getChildren().add(canvas);
//1 canvas.setManaged(false);
this.name = name;
}
#Override
public void resize(double width,double height) {
System.out.println(name+" input "+width+' '+height);
super.resize(width, height);
System.out.println(name+" panel "+getWidth()+' '+getHeight());
//0 canvas.setWidth (width);
//0 canvas.setHeight(height);
System.out.println(name+" canvas "+canvas.getWidth()+' '+canvas.getHeight());
}
}
}
Canvas doesn't resize by itself. Run the code as shown, and drag the divider up and down. Read the console output. The size of the Canvas instances never changes from (0,0).
Uncomment the lines beginning with //0 and run it again. Now the Canvas instances are forcibly resized to the size of their containing panes. Drag the divider up and down. Note that the Canvas instances grow vertically, but they never shrink, even though the code tells them to. Imagine we were drawing an X across the corners of the Canvases. These X drawings would grow with the divider. But when you drag the divider the other direction, the image is clipped, but it never shrinks.
Uncomment the line beginning with //1 and run it again. Now it works.
I don't know about you, but I think this is bizarre.
Based on this post from Jasper Potts, you can create a resizable canvas, and use it in your split pane.
class ResizableCanvas extends Pane {
private final int id;
private final Canvas canvas = new Canvas();
public ResizableCanvas(int id) {
this.id=id;
getChildren().add(canvas);
}
#Override
protected void layoutChildren() {
final int top = (int)snappedTopInset();
final int right = (int)snappedRightInset();
final int bottom = (int)snappedBottomInset();
final int left = (int)snappedLeftInset();
final int w = (int)getWidth() - left - right;
final int h = (int)getHeight() - top - bottom;
canvas.setLayoutX(left);
canvas.setLayoutY(top);
if (w != canvas.getWidth() || h != canvas.getHeight()) {
canvas.setWidth(w);
canvas.setHeight(h);
GraphicsContext gc = canvas.getGraphicsContext2D();
gc.clearRect(0, 0, w, h);
if(id==1){
gc.setFill(Color.BLUE);
gc.fillOval(5, 5, w-10, h-10);
} else {
gc.setFill(Color.RED);
gc.fillRect(10, 10, w-20, h-20);
}
}
}
}
#Override
public void start(Stage stage) throws Exception {
ResizableCanvas canvas1 = new ResizableCanvas(1);
ResizableCanvas canvas2 = new ResizableCanvas(2);
SplitPane split = new SplitPane();
split.getItems().addAll(canvas1,canvas2);
stage.setScene(new Scene(split, 600, 400));
stage.show();
}
Note that everytime the scene is resized or the split content divider is moved, both canvases will be redrawn, but always with the exact size of their respective panes. As he also suggests, you may consider snapshot your canvas contents, and use an image to improve performance.

Resources