How to access the Scrollbars of a ScrollPane - java

I'm trying to get some information about the ScrollBar components that are by standard included in a ScrollPane. Especially i'm interested in reading the height of the horizontal Scrollbar. How can i reference it?

I think you can use the lookupAll() method of the Node class for find the scroll bars.
http://docs.oracle.com/javafx/2/api/javafx/scene/Node.html#lookupAll(java.lang.String)
For example:
package com.test;
import java.util.Set;
import javafx.application.Application;
import javafx.geometry.Orientation;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.ScrollBar;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.ScrollPaneBuilder;
import javafx.scene.text.Text;
import javafx.scene.text.TextBuilder;
import javafx.stage.Stage;
public class JavaFxScrollPaneTest extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
String longString = "The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog.";
Text longText = TextBuilder.create().text(longString).build();
ScrollPane scrollPane = ScrollPaneBuilder.create().content(longText).build();
primaryStage.setScene(new Scene(scrollPane, 400, 100));
primaryStage.show();
Set<Node> nodes = scrollPane.lookupAll(".scroll-bar");
for (final Node node : nodes) {
if (node instanceof ScrollBar) {
ScrollBar sb = (ScrollBar) node;
if (sb.getOrientation() == Orientation.HORIZONTAL) {
System.out.println("horizontal scrollbar visible = " + sb.isVisible());
System.out.println("width = " + sb.getWidth());
System.out.println("height = " + sb.getHeight());
}
}
}
}
}

Since the mentioned methods did not work for everybody (including me), I investigated it a bit more and found the source of the problem.
In general, both methods work, but only as soon as the ScrollPane's skin property has been set. In my case, skin was still null after loading my view using FXMLLoader.
By delaying the call in case the skin property has not been initialized (using a one-shot listener) solves the problem.
Working boiler-plate code:
ScrollPane scrollPane;
// ...
if (scrollPane.getSkin() == null) {
// Skin is not yet attached, wait until skin is attached to access the scroll bars
ChangeListener<Skin<?>> skinChangeListener = new ChangeListener<Skin<?>>() {
#Override
public void changed(ObservableValue<? extends Skin<?>> observable, Skin<?> oldValue, Skin<?> newValue) {
scrollPane.skinProperty().removeListener(this);
accessScrollBar(scrollPane);
}
};
scrollPane.skinProperty().addListener(skinChangeListener);
} else {
// Skin is already attached, just access the scroll bars
accessScrollBar(scrollPane);
}
private void accessScrollBar(ScrollPane scrollPane) {
for (Node node : scrollPane.lookupAll(".scroll-bar")) {
if (node instanceof ScrollBar) {
ScrollBar scrollBar = (ScrollBar) node;
if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
// Do something with the horizontal scroll bar
// Example 1: Print scrollbar height
// System.out.println(scrollBar.heightProperty().get());
// Example 2: Listen to visibility changes
// scrollBar.visibleProperty().addListener((observable, oldValue, newValue) -> {
// if(newValue) {
// // Do something when scrollbar gets visible
// } else {
// // Do something when scrollbar gets hidden
// }
// });
}
if (scrollBar.getOrientation() == Orientation.VERTICAL) {
// Do something with the vertical scroll bar
}
}
}
}

This not is the best pratice, but works,
private boolean determineVerticalSBVisible(final ScrollPane scrollPane) {
try {
final ScrollPaneSkin skin = (ScrollPaneSkin) scrollPane.getSkin();
final Field field = skin.getClass().getDeclaredField("vsb");
field.setAccessible(true);
final ScrollBar scrollBar = (ScrollBar) field.get(skin);
field.setAccessible(false);
return scrollBar.isVisible();
} catch (final Exception e) {
e.printStackTrace();
}
return false;
}
Use "hsb" for Horizontal ScrollBar.
Best Regards,
Henrique Guedes.

Related

Items replacing but not asked

I'm working on a little game. So I create the game interface (countains the gun, the canvas which I use as gamespace and an interface for the user to control is gun). I place the different elements in the window and there is my problem. When I execute my code, all is well placed but once I use one of the buttons (buttons in both codes) or the slider (second code), the slider and the fire button replace themselves. And I don't understand why because I never asked this rellocation in my code. Also, when the items rellocate, I can't use any other items excepted the fire button and the slider.
Here are screenshots of what I have before using a button (first screenshot) (it's also how I want the interface to be) and the second screenshot shows the rellocation I have.
How it looks when I use nothing.
How it looks when I use a button or the slider.
Main.java :
package application;
import bureaux.Bureau;
import canons.Canon;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Button;
import javafx.scene.control.ToolBar;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.stage.Stage;
public class Main extends Application{
private StackPane root, rootBureau;
private Scene scene;
private Stage stage;
private Text joueur;
private Button menu, musique, ajoutJoueur;
private FlowPane rootJeu;
private Bureau bureauJoueur;
private ToolBar toolBar;
private Canon canonJoueur;
#Override
public void start(Stage primaryStage) throws IOException {
getRoot();
getScene();
stage = primaryStage;
creerInterface();
stage.setTitle("Mad Java Guns");
stage.setResizable(false);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
public void creerInterface(String mode) { // creating the gamespace and the objects that the user need to play
getToolBar().getItems().addAll(getMusique(), getAjoutJoueur());
getRootBureau().getChildren().add(getBureauJoueur());
getRootJeu().getChildren().add(getCanonJoueur());
getRoot().getChildren().addAll(getToolBar(), getJoueur(), getRootBureau(), getRootJeu());
}
// Getters
public StackPane getRoot() {
if(root == null) {
root = new StackPane();
}
return root;
}
public Scene getScene() {
if(scene == null) {
scene = new Scene(root,1000,800);
}
return scene;
}
public Text getJoueur() { // gamespace
if(joueur == null) {
joueur = new Text("Espace de jeu");
joueur.setFont(Font.font("Arial", 20));
joueur.setTranslateY(120);
}
return joueur;
}
public ToolBar getToolBar() {
if(toolBar == null) {
toolBar = new ToolBar();
toolBar.setTranslateY(122);
toolBar.setTranslateX(3);
toolBar.setStyle("-fx-background-color: transparent");
}
return toolBar;
}
public Button getMusique() { // button too change the music in game
if (musique == null) {
musique = new Button("Musique");
musique.setOnMouseClicked(e -> {
System.out.println("musique"); // not coded yet
});
musique.setFocusTraversable(false);
}
return musique;
}
public Button getAjoutJoueur() { // add players in the game
if(ajoutJoueur == null) {
ajoutJoueur = new Button("Ajouter un joueur");
ajoutJoueur.setOnMouseClicked(e -> {
System.out.println("ajoutJoueur"); //not coded yet
});
ajoutJoueur.setFocusTraversable(false);
}
return ajoutJoueur;
}
public StackPane getRootBureau() { // pane where the user's interface will be placed
if(rootBureau == null) {
rootBureau = new StackPane();
rootBureau.setStyle("-fx-background-color: lightgrey");
rootBureau.setMaxSize(990, 250);
rootBureau.setTranslateY(270);
}
return rootBureau;
}
public Bureau getBureauJoueur() { // user's interface
if(bureauJoueur == null) {
bureauJoueur = new Bureau("Billy", getCanonJoueur());
}
return bureauJoueur;
}
}
Class Bureau.java :
package bureaux;
import canons.Canon;
import javafx.geometry.Orientation;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.Slider;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
public class Bureau extends Parent {
private Slider sliderCanon;
private HBox boxPrincipale;
private VBox boxControlesCanon;
private Button feu;
public Bureau(String nom, Canon canon) {
getBoxControlesCanon().getChildren().addAll(getSliderCanon(), getFeu());
getBoxPrincipale().getChildren().add(getBoxControlesCanon());
this.setTranslateX(-480); // placing the boxes
this.setTranslateY(-95);
this.getChildren().add(getBoxPrincipale());
}
//Getteurs
public HBox getBoxPrincipale() {
if(boxPrincipale == null) { // return a HBox which countains the VBox (next function)
// and other elements which aren't created yet.
boxPrincipale = new HBox();
}
return boxPrincipale;
}
public VBox getBoxControlesCanon() { // return a VBox which countains the controls of the gun
//(gun not showed in the code, doesn't concern the problem)
if(boxControlesCanon == null) {
boxControlesCanon = new VBox();
boxControlesCanon.setSpacing(20);
}
return boxControlesCanon;
}
public Slider getSliderCanon() { //slider to orient the gun (gun not showed in the code, doesn't concern the problem)
if(sliderCanon == null) {
sliderCanon = new Slider(0, 360, 0);
sliderCanon.setOrientation(Orientation.VERTICAL);
sliderCanon.valueProperty().addListener(e -> {
System.out.println(sliderCanon.getValue());
});
sliderCanon.setShowTickMarks(true);
sliderCanon.setShowTickLabels(true);
sliderCanon.setMajorTickUnit(90f);
}
return sliderCanon;
}
public Button getFeu() { // fire button
if(feu == null) {
feu = new Button("Feu");
feu.setOnMouseClicked(e -> {
System.out.println("Feu");
});
feu.setFocusTraversable(false);
}
return feu;
}
}
Please ask for more informations if necessary. Thanks for your help.
EDIT : sorry for the unpoliteness on top of this text, I used to edit it and add "Hello" but it just don't want to show it :/
You are using Bureau extends Parent. You will have to be more specific and used the nodes that will produce the outcome you need.
Try something like Bureau extends HBox. Then
getBoxControlesCanon().getChildren().add(new VBox(getSliderCanon(), getFeu()));
To get the left alignment, you may need to do something like
getBoxControlesCanon().getChildren().addAll(new VBox(getSliderCanon(), getFeu()), someOtherNode);
HBox.setHGrow(someOtherNode, Priority.ALWAYS);
If you look at Parent compared to VBox, you will see that Parent does not describe how children nodes will be laid out. A lot of nodes that are a subclass of Parent do describe how their children nodes will be laid out.

Synchronize scrollbars of two JavaFx WebViews

I'm using two WebViews to display two versions of HTML formatted text for comparison. The two display the same amount of text (same number of lines and corresponding lines have always the same length).
When the displayed text exceeds the size of the node, the WebView gets scroll bars. Of course I want these scroll bars to scroll synchronously so that always the corresponding text is displayed.
In order to supply a minimal, complete and verifiable example, I trimmed the code down to this:
import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.ScrollBar;
import javafx.scene.layout.GridPane;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
public class SynchronizedWebViewsTest extends Application {
protected class DifferencePanel extends GridPane {
private WebView actualPane;
private WebView expectedPane;
public DifferencePanel() {
setPadding(new Insets(20, 20, 20, 20));
actualPane = new WebView();
expectedPane = new WebView();
setResultPanes();
addRow(0, actualPane, expectedPane);
}
public void setHtml(WebView webView) {
Platform.runLater(() -> {
webView.getEngine().loadContent(createHtml());
});
}
public void synchronizeScrolls() {
final ScrollBar actualScrollBarV = (ScrollBar)actualPane.lookup(".scroll-bar:vertical");
final ScrollBar expectedScrollBarV = (ScrollBar)expectedPane.lookup(".scroll-bar:vertical");
actualScrollBarV.valueProperty().bindBidirectional(expectedScrollBarV.valueProperty());
final ScrollBar actualScrollBarH = (ScrollBar)actualPane.lookup(".scroll-bar:horizontal");
final ScrollBar expectedScrollBarH = (ScrollBar)expectedPane.lookup(".scroll-bar:horizontal");
actualScrollBarH.valueProperty().bindBidirectional(expectedScrollBarH.valueProperty());
}
private String createHtml() {
StringBuilder sb = new StringBuilder(1000000);
for (int i = 0; i < 100; i++) {
sb.append(String.format("<nobr>%03d %2$s%2$s%2$s%2$s%2$s%2$s%2$s%2$s</nobr><br/>\n",
Integer.valueOf(i), "Lorem ipsum dolor sit amet "));
}
return sb.toString();
}
private void setResultPanes() {
setHtml(actualPane);
setHtml(expectedPane);
}
} // ---------------------------- end of DifferencePanel ----------------------------
public static void main(String[] args){
launch(args);
}
#Override
public void start(Stage dummy) throws Exception {
Stage stage = new Stage();
stage.setTitle(this.getClass().getSimpleName());
DifferencePanel differencePanel = new DifferencePanel();
Scene scene = new Scene(differencePanel);
stage.setScene(scene);
differencePanel.synchronizeScrolls();
stage.showAndWait();
}
}
I tried using adding a listener:
actualScrollBarV.onScrollFinishedProperty().addListener(event -> {
System.out.println(event);
});
But the listener is never invoked.
I'm using Java version 1.8.0_92, but with version 9.0.4 I get the same result.
Can anybody tell me, what I'm missing here?
I would post a comment, but sadly I did not have enough reputation.
Did you tried the following solution? Create listeners on value changed event, instead of binding. Synchronizing two scroll bars JavaFX
I could not get the ScrollBar approach working. It turned out that the listeners were actually invoked (breakpoints in lambdas are not always working?). Setting the scroll bar value of the other WebView did not get it inclined change the scroll bar or the view port. :-(
There is something strange going on with events in WebView; that might be because there is a native library involved...
However, the approach using the event handler of WebView works. The event handler of each WebView simply mirrors all events to the other WebView, using a synchronizing field Boolean scrolling to avoid recursion.
import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.Event;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.GridPane;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
public class SynchronizedWebViewsTest extends Application {
protected class DifferencePanel extends GridPane {
private Boolean scrolling = Boolean.FALSE;
private WebView actualPane;
private WebView expectedPane;
public DifferencePanel() {
setPadding(new Insets(20, 20, 20, 20));
actualPane = new WebView();
expectedPane = new WebView();
setResultPanes();
addRow(0, actualPane, expectedPane);
}
public void setHtml(WebView webView) {
Platform.runLater(() -> {
webView.getEngine().loadContent(createHtml());
});
}
public void synchronizeScrolls() {
wireViews(actualPane, expectedPane);
wireViews(expectedPane, actualPane);
}
private void wireViews(WebView webView, WebView otherWebView) {
webView.addEventHandler(Event.ANY, event -> {
if (!scrolling.booleanValue()) {
synchronized (scrolling) {
scrolling = Boolean.TRUE;
if (event instanceof MouseEvent) {
MouseEvent mouseEvent = (MouseEvent) event;
Point2D origin = webView.localToScreen(0, 0);
Point2D otherOrigin = otherWebView.localToScreen(0, 0);
double offsetX = otherOrigin.getX() - origin.getX();
double offsetY = otherOrigin.getY() - origin.getY();
double x = mouseEvent.getX();
double y = mouseEvent.getY();
double screenX = mouseEvent.getScreenX() + offsetX;
double screenY = mouseEvent.getScreenY() + offsetY;
MouseButton button = mouseEvent.getButton();
int clickCount = mouseEvent.getClickCount();
boolean shiftDown = mouseEvent.isShiftDown();
boolean controlDown = mouseEvent.isControlDown();
boolean altDown = mouseEvent.isAltDown();
boolean metaDown = mouseEvent.isMetaDown();
boolean primaryButtonDown = mouseEvent.isPrimaryButtonDown();
boolean middleButtonDown = mouseEvent.isMiddleButtonDown();
boolean secondaryButtonDown = mouseEvent.isSecondaryButtonDown();
boolean synthesized = mouseEvent.isSynthesized();
boolean popupTrigger = mouseEvent.isPopupTrigger();
boolean stillSincePress = mouseEvent.isStillSincePress();
MouseEvent otherMouseEvent =
new MouseEvent(otherWebView, otherWebView, mouseEvent.getEventType(), x, y, screenX,
screenY, button, clickCount, shiftDown, controlDown, altDown, metaDown,
primaryButtonDown, middleButtonDown, secondaryButtonDown, synthesized,
popupTrigger, stillSincePress, null);
otherWebView.fireEvent(otherMouseEvent);
}
else {
otherWebView.fireEvent(event.copyFor(otherWebView, otherWebView));
}
scrolling = Boolean.FALSE;
}
}
});
}
private String createHtml() {
StringBuilder sb = new StringBuilder(1000000);
for (int i = 0; i < 100; i++) {
sb.append(String.format("<nobr>%03d %2$s%2$s%2$s%2$s%2$s%2$s%2$s%2$s</nobr><br/>\n",
Integer.valueOf(i), "Lorem ipsum dolor sit amet "));
}
return sb.toString();
}
private void setResultPanes() {
setHtml(actualPane);
setHtml(expectedPane);
}
}
public static void main(String[] args){
launch(args);
}
#Override
public void start(Stage dummy) throws Exception {
Stage stage = new Stage();
stage.setTitle(this.getClass().getSimpleName());
DifferencePanel differencePanel = new DifferencePanel();
Scene scene = new Scene(differencePanel);
stage.setScene(scene);
differencePanel.synchronizeScrolls();
stage.showAndWait();
}
}
This works for all input methods I'm interested in:
Keyboard: PageUp, PageDown, all 4 arrow keys, "space bar" (same as PageDown) and shift-"space bar" (same as PageUp), Home and End
Mouse wheel: RollDown and RollUp as well as shift-RollUp (scroll left) and shift-RollDown (scroll right)
Using the mouse to click or to drag the scroll bar.
Using the mouse to select text outside of the current view port.
Mirroring the mouse events has the added benefit that text gets selected in both WebViews.

How to retrieve the final Slider value when snapToTicks==true?

I have the following JavaFX scene (note the setting of snapToTicks):
package com.example.javafx;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Slider;
import javafx.stage.Stage;
public class SliderExample extends Application {
public static void main(String[] args) { launch(args); }
#Override
public void start(Stage primaryStage) {
Slider slider = new Slider(0.25, 2.0, 1.0);
slider.setShowTickLabels(true);
slider.setShowTickMarks(true);
slider.setMajorTickUnit(0.25);
slider.setMinorTickCount(0);
slider.setSnapToTicks(true); // !!!!!!!!!!
Scene scene = new Scene(slider, 800, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
}
which renders a slider like this:
Since snapToTicks is set to true the slider will finally move to the nearest value once the mouse button is released.
How can that final value be retrieved?
I tried
slider.valueProperty().addListener( n -> {
if (!slider.isValueChanging()) {
System.err.println(n);
}
});
which works well except for the minimum and maximum values - if the mouse is already at a position left to the slider or at a position right to the slider, the listener will not be called at all anymore since the final value has already been set.
I have also tried to use the valueChangingProperty:
slider.valueChangingProperty().addListener( (prop, oldVal, newVal) -> {
// NOT the final value when newVal == false!!!!!!!
System.err.println(prop + "/" + oldVal + "/" + newVal);
});
but the problem is that JavaFX will still change the value to the snapped value after that listener has been called with newVal equal to false (which I would even consider a bug, but probably I missed something). So its not possible to access the final, snapped value in that method.
I finally came up with the below solution, based on the proposal from #ItachiUchiha. Essentially, the solution uses both, a valueProperty and a valueChangingProperty listener, and uses some flags to track the current state. At the end, the perform() method is called exactly once when the slider movement is done and the final value is available. This works when the slider is moved either with the mouse or through the keyboard.
A reusable class implemented as subclass of Slider is available at https://github.com/afester/FranzXaver/blob/master/FranzXaver/src/main/java/afester/javafx/components/SnapSlider.java.
package com.example.javafx;
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.control.Slider;
import javafx.stage.Stage;
public class SliderExample extends Application {
public static void main(String[] args) { launch(args); }
private boolean isFinal = true; // assumption: no dragging - clicked value is the final one.
// variable changes to "false" once dragging starts.
private Double finalValue = null;
#Override
public void start(Stage primaryStage) {
final Slider slider = new Slider(0.25, 2.0, 1.0);
slider.setShowTickLabels(true);
slider.setShowTickMarks(true);
slider.setMajorTickUnit(0.25);
slider.setMinorTickCount(0);
slider.setSnapToTicks(true);
slider.valueProperty().addListener(new ChangeListener<Number>() {
final double minCompare = slider.getMin() + Math.ulp(slider.getMin());
final double maxCompare = slider.getMax() - Math.ulp(slider.getMax());
#Override
public void changed(ObservableValue<? extends Number> observable,
Number oldValue, Number newValue) {
if (isFinal) { // either dragging of knob has stopped or
// no dragging was done at all (direct click or
// keyboard navigation)
perform((Double) newValue);
finalValue = null;
} else { // dragging in progress
double val = (double) newValue;
if (val > maxCompare || val < minCompare) {
isFinal = true; // current value will be treated as final value
// once the valueChangingProperty goes to false
finalValue = (Double) newValue; // remember current value
} else {
isFinal = false; // no final value anymore - slider
finalValue = null; // has been dragged to a position within
// minimum and maximum
}
}
}
});
slider.valueChangingProperty().addListener(new ChangeListener<Boolean>() {
#Override
public void changed(ObservableValue<? extends Boolean> observable,
Boolean oldValue, Boolean newValue) {
if (newValue == true) { // dragging of knob started.
isFinal = false; // captured values are not the final ones.
} else { // dragging of knob stopped.
if (isFinal) { // captured value is already the final one
// since it is either the minimum or the maximum value
perform(finalValue);
finalValue = null;
} else {
isFinal = true; // next captured value will be the final one
}
}
}
});
Scene scene = new Scene(slider, 800, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private void perform(double value) {
System.err.printf("FINAL: %s\n", value);
}
}

JavaFX 2 TextArea that changes height depending on its contents

In JavaFX 2.2, is there any way to make TextArea (with setWrapText(true) and constant maxWidth) change its height depending on contents?
The desired behaviour: while user is typing something inside the TextArea it resizes when another line is needed and decreases when the line is needed no more.
Or is there a better JavaFX control that could be used in this situation?
You can bind the prefHeight of the text area to the height of the text it contains. This is a bit of a hack, because you need a lookup to get the text contained in the text area, but it seems to work. You need to ensure that you lookup the text node after CSS has been applied. (Typically this means after it has appeared on the screen...)
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.TextArea;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class ResizingTextArea extends Application {
#Override
public void start(Stage primaryStage) {
TextArea textArea = new TextArea();
textArea.setWrapText(true);
textArea.sceneProperty().addListener(new ChangeListener<Scene>() {
#Override
public void changed(ObservableValue<? extends Scene> obs, Scene oldScene, Scene newScene) {
if (newScene != null) {
textArea.applyCSS();
Node text = textArea.lookup(".text");
textArea.prefHeightProperty().bind(Bindings.createDoubleBinding(new Callable<Double>() {
#Override
public Double call() {
return 2+text.getBoundsInLocal().getHeight();
}
}), text.boundsInLocalProperty()));
}
}
});
VBox root = new VBox(textArea);
Scene scene = new Scene(root, 400, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Two things to add to James_D's answer (because I lack the rep to comment):
1) For big fonts like size 36+, the text area size was wrong at first but corrected itself when I clicked inside the text area. You can call textArea.layout() after applying CSS, but the text area still does not resize immediately after the window is maximized. To get around this, call textArea.requestLayout() asynchronously in a Change Listener after any change to the Text object's local bounds. See below.
2) The text area was still a few pixels short and the scroll bar still visible. If you replace the 2 with textArea.getFont().getSize() in the binding, the height fits perfectly to the text, no matter whether the font size is tiny or huge.
class CustomTextArea extends TextArea {
CustomTextArea() {
setWrapText(true);
setFont(Font.font("Arial Black", 72));
sceneProperty().addListener((observableNewScene, oldScene, newScene) -> {
if (newScene != null) {
applyCss();
Node text = lookup(".text");
// 2)
prefHeightProperty().bind(Bindings.createDoubleBinding(() -> {
return getFont().getSize() + text.getBoundsInLocal().getHeight();
}, text.boundsInLocalProperty()));
// 1)
text.boundsInLocalProperty().addListener((observableBoundsAfter, boundsBefore, boundsAfter) -> {
Platform.runLater(() -> requestLayout());
});
}
});
}
}
(The above compiles for Java 8. For Java 7, replace the listener lambdas with Change Listeners according to the JavaFX API, and replace the empty ()-> lambdas with Runnable.)

Auto-sizing a Shape Assigned as a Graphic to a Cell in JavaFx

At work I created a TableView that needs to have specific cells flash from one color to the other simultaneously. This is relatively easy using Rectangles, FillTransitions, and a ParallelTransition as shown in the toy example below. After assigning the rectangle to the FillTransition, I set the TableCell's graphic to the rectangle. Then I just had to add/remove the FillTransition from the ParallelTransition depending on whether or not the cell should be blinking.
An area where I had a lot of difficulty, however, was in figuring out a way to scale the rectangle to the size of the TableCell containing it as a graphic. The problem I had was that the TableCell would always resize itself to have empty space between its boundaries and the boundaries of the rectangle.
I had to solve this in a very tedious and round-about way: I had to call setFixedCellSize to fix the table's cell height to whatever my rectangle's height was, reposition the rectangle up and to the left by trial and error through calling its setTranslateX/Y, and set the minwidth and minheight of the column to slightly less than whatever my rectangle's width and height was set to. It solved the problem, but I would've hoped for something a little less tedious and annoying.
I would have assumed this could be avoided by doing one or more of the following with the cell:
Calling setScaleShape(true)
Calling setContentDisplay(ContentDisplay.GRAPHIC_ONLY)
Setting the cell's CSS style to include "-fx-scale-shape: true"
Sadly, none of these had any noticeable effect...
My question is a three-parter:
Is there a better way to size a Shape assigned as a graphic for a Cell to fill the boundaries of the Cell?
Why would none of the three methods above have any effect in my case and what is their actual intended purpose? Do they only apply for a shape assigned with setShape() as opposed to setGraphic()?
Are there any legitimate reasons why JavaFx wouldn't support setting the preferred width or height of Nodes other than those that subclass Region? Autosizing seems like something that should be universal to all Nodes in the hierarchy, and it seems intuitive that any Parent node should be able to dictate the size of its children when necessary.
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import javafx.animation.Animation;
import javafx.animation.FillTransition;
import javafx.animation.ParallelTransition;
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderStroke;
import javafx.scene.layout.BorderStrokeStyle;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class FlashingPriorityTable extends Application {
public static void main(String args[]) {
FlashingPriorityTable.launch();
}
#Override
public void start(Stage primaryStage) throws Exception {
// periodically add prioritized items to an observable list
final ObservableList<PItem> itemList = FXCollections.observableArrayList();
class ItemAdder {
private int state, count = 0; private final int states = 3;
public synchronized void addItem() {
state = count++ % states;
PItem item;
if(state == 0)
item = new PItem(Priority.LOW, count, "bob saget");
else if(state == 1)
item = new PItem(Priority.MEDIUM, count, "use the force");
else
item = new PItem(Priority.HIGH, count, "one of us is in deep trouble");
itemList.add(item);
}
};
final ItemAdder itemAdder = new ItemAdder();
Executors.newScheduledThreadPool(1).scheduleAtFixedRate(
() -> itemAdder.addItem(),
0, // initial delay
1, // period
TimeUnit.SECONDS); // time unit
// set up a table view bound to the observable list
final TableColumn<PItem, Priority> priCol = new TableColumn<>("Priority");
priCol.setCellValueFactory(new PropertyValueFactory<PItem, Priority>("priority"));
priCol.setCellFactory((col) -> new PriorityCell()); // create a blinking cell
priCol.setMinWidth(50);
priCol.setMaxWidth(50);
final TableColumn<PItem, Integer> indexCol = new TableColumn<>("Index");
indexCol.setCellValueFactory(new PropertyValueFactory<PItem, Integer>("index"));
indexCol.setCellFactory((col) -> makeBorderedTextCell());
final TableColumn<PItem, String> descriptionCol = new TableColumn<>("Description");
descriptionCol.setCellValueFactory(new PropertyValueFactory<PItem, String>("description"));
descriptionCol.setCellFactory((col) -> makeBorderedTextCell());
descriptionCol.setMinWidth(300);
final TableView<PItem> table = new TableView<>(itemList);
table.getColumns().setAll(priCol, indexCol, descriptionCol);
table.setFixedCellSize(25);
// display the table view
final Scene scene = new Scene(table);
primaryStage.setScene(scene);
primaryStage.show();
}
// render a simple cell text and border
private <T> TableCell<PItem, T> makeBorderedTextCell() {
return new TableCell<PItem, T>() {
#Override protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if(item == null || empty) {
setText(null);
} else {
setBorder(new Border(new BorderStroke(Color.GREEN, BorderStrokeStyle.SOLID, null, null)));
setText(item.toString());
}
}
};
}
/* for cells labeled as high priority, render an animation that blinks (also include a border) */
public static class PriorityCell extends TableCell<PItem, Priority> {
private static final ParallelTransition pt = new ParallelTransition();
private final Rectangle rect = new Rectangle(49.5, 24);
private final FillTransition animation = new FillTransition(Duration.millis(100), rect);
public PriorityCell() {
rect.setTranslateX(-2.75);
rect.setTranslateY(-2.7);
animation.setCycleCount(Animation.INDEFINITE); animation.setAutoReverse(true); }
#Override
protected void updateItem(Priority priority, boolean empty) {
super.updateItem(priority, empty);
if(priority == null || empty) {
setGraphic(null);
return;
}
setGraphic(rect);
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
setBorder(new Border(new BorderStroke(Color.GREEN, BorderStrokeStyle.SOLID, null, null)));
if(priority == Priority.HIGH) {
if(!pt.getChildren().contains(animation)) {
animation.setFromValue(Color.BLACK);
animation.setToValue(priority.getColor());
animation.setShape(rect);
pt.getChildren().add(animation);
pt.stop(); pt.play();
}
} else {
if(pt.getChildren().contains(animation)) {
pt.getChildren().remove(animation);
pt.stop(); pt.play();
}
rect.setFill(priority.getColor());
}
}
}
/* an item that has a priority assigned to it */
public static class PItem {
private ObjectProperty<Priority> priority = new SimpleObjectProperty<>();
private IntegerProperty index = new SimpleIntegerProperty();
private StringProperty description = new SimpleStringProperty();
public PItem(Priority priority, Integer index, String description) {
setPriority(priority); setIndex(index); setDescription(description);
}
public void setPriority(Priority priority_) { priority.set(priority_); }
public Priority getPriority() { return priority.get(); }
public void setIndex(int index_) { index.set(index_); }
public Integer getIndex() { return index.get(); }
public void setDescription(String description_) { description.set(description_); }
public String getDescription() { return description.get(); }
}
/* a priority */
public enum Priority {
HIGH(Color.RED), MEDIUM(Color.ORANGE), LOW(Color.BLUE);
private final Color color;
private Priority(Color color) { this.color = color; }
public Color getColor() { return color; }
}
}
Regarding:
the TableCell would always resize itself to have empty space between its boundaries and the boundaries of the rectangle.
This is because the cell has by default 2 px of padding, according to modena.css:
.table-cell {
-fx-padding: 0.166667em; /* 2px, plus border adds 1px */
-fx-cell-size: 2.0em; /* 24 */
}
One easy way to get rid of this empty space is just override it:
#Override
protected void updateItem(Priority priority, boolean empty) {
super.updateItem(priority, empty);
...
setGraphic(rect);
setStyle("-fx-padding: 0;");
...
}
The next problem you also mention is autosizing. According to JavaDoc, for Node.isResizable():
If this method returns true, then the parent will resize the node (ideally within its size range) by calling node.resize(width,height) during the layout pass. All Regions, Controls, and WebView are resizable classes which depend on their parents resizing them during layout once all sizing and CSS styling information has been applied.
If this method returns false, then the parent cannot resize it during layout (resize() is a no-op) and it should return its layoutBounds for minimum, preferred, and maximum sizes. Group, Text, and all Shapes are not resizable and hence depend on the application to establish their sizing by setting appropriate properties (e.g. width/height for Rectangle, text on Text, and so on). Non-resizable nodes may still be relocated during layout.
Clearly, a Rectangle is not resizable, but this doesn't mean you can't resize it: if the layout doesn't do it for you, you'll need to take care of it.
So one easy solution may be binding the dimensions of the rectangle to those of the cell (minus 2 pixels for the cell borders):
private final Rectangle rect = new Rectangle();
#Override
protected void updateItem(Priority priority, boolean empty) {
super.updateItem(priority, empty);
if(priority == null || empty) {
setGraphic(null);
return;
}
setGraphic(rect);
setStyle("-fx-padding: 0;");
rect.widthProperty().bind(widthProperty().subtract(2));
rect.heightProperty().bind(heightProperty().subtract(2));
...
}
Note that you won't need to translate the rectangle, and it won't be necessary to fix the size of the cell nor the width of the column, unless you want to give it a fixed size.
Note also that setShape() is intended to change the cell shape, that by default is already a rectangle.
This may answer your first two questions. For the third one, sometimes you wish the nodes were always resizable... but if that were the case we will have the opposite problem, trying to keep them constrained...

Categories

Resources