JavaFX-8: limit number of characters in a textfield - javafx

I'm trying to limit the number of characters which can be inserted in a JavaFX-8 TextField: when the limit is reached endLineReached() method is called.
I would like to set the maximum number of chars basing on size (the visible part on GUI) of the textfield. So the code I'm using is
textfield.textProperty().addListener((ov, oldValue, newValue) -> {
onEndLine(textfield.getText(), textfield.getPrefColumnCount());
});
public void onEndLine(String text, int prefColumnCount) {
if (text.length() > prefColumnCount) {
endLineReached();
}
}
The problem is that textfield.getPrefColumnCount() not work properly. Furthermore I'm not able to set prefColumnCount value programmatically because the textfield width could change every time the program is re-started (not during execution). The textfield is child of a JavaFX HBox.

If you want to limit the length of the text based solely on the visual width of the text field, that's a little tricky as it depends on many factors (the font, the width of the text field, the padding applied to the text field, in the case of a variable-width font the actual text that has been entered, etc).
Note this seems a strange thing to do (imo), because the user will be able to enter relatively long text if it happens to use narrow characters, but relatively short text if it happens to use wide characters (in this example below I can enter 26 "l"s but only 8 "m"s). Really the text the user is allowed to enter should be based on logical considerations (i.e. business rules), not visual ones. But maybe you have some unusual use-case for this.
You can use a text formatter on the text field to veto the addition of text. To check the width, create a new Text object with the new text, set the font to the same font as the text field, and then check its width. You need to account for padding in the text field as well. Note this is a little fragile, as you don't really know the implementation of the layout of the text field, but this appears to work ok, at least with the JDK version I have.
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.scene.control.TextInputControl;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.stage.Stage;
public class TextFieldNoScroll extends Application {
#Override
public void start(Stage primaryStage) {
TextField textField = new TextField();
textField.setTextFormatter(new TextFormatter<String>(change -> {
if (change.isAdded()) {
Insets textFieldInsets = change.getControl().getPadding();
double horizPadding = textFieldInsets.getLeft() + textFieldInsets.getRight() ;
Text newText = new Text(change.getControlNewText());
newText.setFont(((TextInputControl)change.getControl()).getFont());
double newTextWidth = newText.getBoundsInLocal().getWidth();
if (newTextWidth + horizPadding > change.getControl().getWidth()) {
return null ;
}
}
return change ;
}));
VBox root = new VBox(textField);
root.setAlignment(Pos.CENTER);
Scene scene = new Scene(root, 120, 120);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}

Related

JavaFX - pagination: how to hide bottom (control) panel and occupy its area with a page?

In some situations I need to completely hide the page change buttons in the pagination control, but the hidden area may have been occupied by a page, that is, extend it downwards.
In this example of a window with pagination, I'd like to hide the gray area and enlarge the sky-colored area to the hidden area.
How can I do this?
I tried this:
package javafxpagination;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Pagination;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class TestPagination extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Test pagination");
Pagination pagination = new Pagination();
pagination.setPageCount(4);
pagination.setMaxPageIndicatorCount(4);
pagination.setCurrentPageIndex(0);
pagination.setPageFactory((pageIndex) -> {
Pane pagePane = new Pane();
pagePane.setPrefWidth(600);
pagePane.setPrefHeight(400);
pagePane.setStyle("-fx-background-color: #DDF1F8;"); //sky color
Button button = new Button("Hide/show page buttons");
button.setOnAction(new EventHandler<ActionEvent>()
{
#Override
public void handle(ActionEvent event)
{
HBox paginationHBox = (HBox)pagination.lookup(".control-box");
Node paginationControl = pagination.lookup(".pagination-control");
paginationControl.setStyle("-fx-background-color: gray;");
paginationControl.setVisible(!paginationControl.isVisible()); //switch visibility
paginationControl.setManaged(paginationControl.isVisible());
paginationControl.minHeight(0.0);
paginationControl.prefHeight(0.0);
paginationControl.maxHeight(0.0);
}
});
pagePane.getChildren().add(button);
return pagePane;
});
VBox vBox = new VBox(pagination);
Scene scene = new Scene(vBox, 800, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
}
but it did not hide gray area and not stretch the sky-colored page area.
Solution using your VBox layout manager
By adding this line to your application, the Pagination control will expand to fill the available space:
VBox.setVgrow(pagination, Priority.ALWAYS);
That answers the "how to occupy" portion of your question: "JavaFX - pagination: how to hide bottom (control) panel and occupy its area with a page?"
For the hiding, you are already accomplishing that, which is based on the info at:
JavaFX - setVisible hides the element but doesn't rearrange adjacent nodes
with the addition of the lookup,
Node paginationControl = pagination.lookup(".pagination-control")
that you are already doing, plus these lines:
paginationControl.setVisible(!paginationControl.isVisible());
paginationControl.setManaged(paginationControl.isVisible());
The lines to set the pagination control's minHeight, prefHeight, maxHeight to 0, are not needed and I wouldn't recommend keeping them.
Background on Layout Managers
Layouts are managed by layout managers. These layout managers have some default behavior, but sometimes you need to configure them or give them hints to achieve the layout behavior you want.
You have your Pagination control inside a VBox. If you look at the VBox documentation, it says:
If a vbox is resized larger than its preferred height, by default it will keep children to their preferred heights, leaving the extra space unused. If an application wishes to have one or more children be allocated that extra space it may optionally set a vgrow constraint on the child.
So, the default behavior, which is what you are using, is not to expand the managed content to fill additional available space (which is what you are seeing).
Alternate solution - replacing the pagination control
An alternative implementation is, when hiding, replace the Pagination control with the panel to be displayed rather than hiding the Pagination controls.
For example, remove the pagination control from the parent layout container (the vbox) and, in its place, add the panel to be displayed directly into the parent layout container.
To show the pagination again, you would do the opposite.
That is probably the solution that I would prefer for this kind of work.
For such a solution, you need to be mindful that a node can only be the child of a single parent at any one time and take that into account for your implementation. Also, the previous information about layout managers and setting Vgrow constraints on nodes still applies for this solution.

JavaFX change selected tab close button background programmatically

I'm trying to change the close icon background of the selected tab only, the problem is that it works when there is one tab, but when I add more nothing happens until I close the first one, here is what I did :
tabPane.lookup(".tab:selected:top").lookup(".tab-container").lookup(".tab-close-button").setStyle("-fx-background-color: red");
Now the problem is that when I select another tab and try to change it's color nothing happens, until I close the previous changed one.
Quick example:
I have 2 tabs with white close buttons,
I select tab1 : the color changed,
I select tab2 : nothing happens,
I close tab1 then select tab2 : tab2's close button changed.
The selected tab is correct because if I print those 2 lines for each tab:
System.out.println(tabPane.getSelectionModel().getSelectedItem());
System.out.println(tabPane.lookup(".tab:selected:top").lookup(".tab-container").lookup(".tab-close-button"));
here is the result :
javafx.scene.control.Tab#6d81182e <----tab1
TabPaneSkin$TabHeaderSkin$2#7d620380[styleClass=tab-close-button] <----tab1
//now I switch to tab2
javafx.scene.control.Tab#416ca04a <----tab2
TabPaneSkin$TabHeaderSkin$2#7d620380[styleClass=tab-close-button] <----this address should not be the same as the first one.
PS : I can't do it in CSS because the color should change dynamically, the line is executed inside a listener when I switch tab.
Thanks for the help!
CSS lookups are inherently reliant on implementation details of the rendering framework, and as such they can be unreliable and you should really avoid using them whenever possible. In particular, lookups will not work if a CSS pass has not been made on the scene graph (typically this happens during rendering), or if changes to the CSS state have occurred since the last CSS pass. I think what is happening with your code is that the listener is invoked before the CSS state has been updated with the new selection state, causing you to retrieve the wrong Tab from the .tab:selected lookup (though I'm by no means certain this is what's happening).
Note that you already have most of the dynamic behavior you need, because the "selected" pseudoclass will be updated dynamically.
The best approach for functionality like this is to define an external CSS file which covers all your possible styles, and then to programmatically modify CSS state using one of a number of mechanisms. A couple of ways to do this are:
Use "looked-up colors". These act like variable names in CSS.
Use custom pseudoclasses, which you can use as selectors in the CSS file, and whose state you can update programatically.
Here's an example using the first approach. Here we define a looked-up color in the CSS file called "selected-tab-color" which initially sets the close button to be blue.
tabpane.css:
.tab-pane {
selected-tab-color: blue ;
}
.tab:selected > .tab-container > .tab-close-button {
-fx-background-color: selected-tab-color ;
}
In Java code, we can update this simply with a call to tabPane.setStyle("selected-tab-color: red;"), which you can do, e.g., in an event handler (but really anywhere, as long as it's on the JavaFX application thread). In this example, we just list to the selected tab, and make one tab have a red close button, the others blue.
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class TabPaneTest extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
TabPane tabPane = new TabPane();
Tab tab1 = new Tab("Tab 1", new Label("Tab 1"));
Tab tab2 = new Tab("Tab 2", new Label("This tab has\na custom close button"));
Tab tab3 = new Tab("Tab 3", new Label("Tab 3"));
tabPane.getTabs().addAll(tab1, tab2, tab3);
tabPane.getSelectionModel().selectedItemProperty().addListener((obs, oldTab, newTab) -> {
if (newTab == tab2) {
tabPane.setStyle("selected-tab-color: red;");
} else {
tabPane.setStyle("selected-tab-color: blue;");
}
});
BorderPane root = new BorderPane(tabPane);
Scene scene = new Scene(root);
scene.getStylesheets().add(getClass().getResource("tabpane.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.setWidth(250);
primaryStage.setHeight(250);
primaryStage.show();
}
public static void main(String[] args) {
Application.launch(args);
}
}
You can use this technique to turn the effect off entirely, by reverting to the default:
tabPane.setStyle("selected-tab-color: -fx-mark-color;");
however, this relies on knowing the details of the default CSS implementation (i.e. the name of the default color for the tab close button).
A slightly better way to turn the functionality on or off programmatically is to use a custom Pseudoclass. These work like any other CSS pseudoclass, and can have their state modified programmatically.
In this example, the color of the close button is red only if the containing tab pane has the warn-on-close pseudoclass set:
.tab-pane:warn-on-close .tab:selected > .tab-container > .tab-close-button {
-fx-background-color: red ;
}
And in the Java code here, we switch this on when tab 2 is selected, and off when the others are selected. Again, arbitrary logic is possible here.
import javafx.application.Application;
import javafx.css.PseudoClass;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class TabPaneTest extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
TabPane tabPane = new TabPane();
Tab tab1 = new Tab("Tab 1", new Label("Tab 1"));
Tab tab2 = new Tab("Tab 2", new Label("This tab has\na custom close button"));
Tab tab3 = new Tab("Tab 3", new Label("Tab 3"));
tabPane.getTabs().addAll(tab1, tab2, tab3);
PseudoClass warnOnClosePseudoClass = PseudoClass.getPseudoClass("warn-on-close");
tabPane.getSelectionModel().selectedItemProperty().addListener((obs, oldTab, newTab) ->
tabPane.pseudoClassStateChanged(warnOnClosePseudoClass, newTab == tab2));
BorderPane root = new BorderPane(tabPane);
Scene scene = new Scene(root);
scene.getStylesheets().add(getClass().getResource("tabpane.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.setWidth(250);
primaryStage.setHeight(250);
primaryStage.show();
}
public static void main(String[] args) {
Application.launch(args);
}
}

JavafX & CSS - Move Node to Front

As the title basically. I have a node in JavaFX which I want to be displayed in front of all other nodes according to certain CSS rules. I do not want this to change the ordering of the nodes in my VBox which .toFront() appears to do. See this question.
Is this even possible?
EDIT: To clarify. The situation is the following. I have a VBox containing a bunch of tightly packed ImageViews. When I hover over one I want it to grow slightly to give it the feel that the image is being lifted off of the screen. But since the ImageViews are so tightly packed only the top edge grows (visibly). The bottom edge grows but is below the following image and cannot be seen.
EDIT 2: Upon request here is a screenshot of what I am doing.
The different colour gradients are ImageViews and as I hover over one it should grow as the top edge of the top gradient has in this image (look closely at the top right corner next to the X). However as is also visible in this image the bottom edge of this ImageView has become hidden by the next gradient in this VBox and the grow is not visible.
This sounds like the perfect situation for using the viewOrder property of Node added in Java 9. The viewOrder controls how Nodes are drawn in relation to other Nodes of the same Parent without changing the order of the Nodes in the child list. Here's the Javadoc:
Defines the rendering and picking order of this Node within its parent.
This property is used to alter the rendering and picking order of a
node within its parent without reordering the parent's children list.
For example, this can be used as a more efficient way to implement
transparency sorting. To do this, an application can assign the
viewOrder value of each node to the computed distance between that
node and the viewer.
The parent will traverse its children in decreasing viewOrder order.
This means that a child with a lower viewOrder will be in front of a
child with a higher viewOrder. If two children have the same
viewOrder, the parent will traverse them in the order they appear in
the parent's children list.
However, viewOrder does not alter the layout and focus traversal order
of this Node within its parent. A parent always traverses its children
list in order when doing layout or focus traversal.
Here's an example using this property:
import javafx.animation.ScaleTransition;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;
import java.util.ArrayList;
public class Main extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
var box = new HBox(createRectangles(Color.DARKBLUE, Color.FIREBRICK, 25));
box.setAlignment(Pos.CENTER);
box.setPadding(new Insets(50, 20, 50, 20));
primaryStage.setScene(new Scene(box));
primaryStage.show();
}
private Rectangle[] createRectangles(Color start, Color end, int count) {
var list = new ArrayList<Rectangle>(count);
for (double i = 0; i < count; i++) {
var rect = new Rectangle(30, 60, start.interpolate(end, i / count));
var scaleTrans = new ScaleTransition(Duration.millis(250), rect);
scaleTrans.setFromX(1.0);
scaleTrans.setFromY(1.0);
scaleTrans.setToX(1.2);
scaleTrans.setToY(1.2);
rect.setOnMouseEntered(e -> {
scaleTrans.stop(); // <--- doesn't seem necessary*
scaleTrans.setRate(1.0);
rect.setViewOrder(-1.0);
scaleTrans.play();
});
rect.setOnMouseExited(e -> {
scaleTrans.stop(); // <--- doesn't seem necessary*
scaleTrans.setRate(-1.0);
rect.setViewOrder(0.0);
scaleTrans.play();
});
// *the "stop()"'s don't seem to be necessary. When I commented
// them out the animation still worked. In fact, the animation
// actually seems smoother in the situation where you move the
// mouse over and then away quickly (before the zoom-in completes).
list.add(rect);
}
return list.toArray(new Rectangle[0]);
}
}
It uses Rectangles instead of ImageViews but the concept is the same. When the mouse hovers over a Rectangle it sets the view order to be lower than the others and then plays a ScaleTransition to make it bigger. When the mouse exits it resets the view order back to 0 and then reverses the ScaleTransition.
Note: I used the var keyword which was added in Java 10.
And here is a GIF of the example in action:
Edit: Since you brought up CSS I went and checked if the view order could be set from a stylesheet. And it appears it can. Looking at the CSS Reference Guide there is a CSS property defined for Node named -fx-view-order.
Here is one such solution, creating a new stage to show the zoomed in image.
I do not set the proper coordinates in this sample, but this works as a proof of concept.
In a nutshell: capture the onMouseEntered and onMouseExited events and hide or show the new stage.
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.image.ImageView;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
public class Main extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
FlowPane root = new FlowPane();
root.setHgap(5);
root.setVgap(5);
for (int i = 0; i < 25; i++) {
root.getChildren().add(getImagePane());
}
primaryStage.setScene(new Scene(root));
primaryStage.show();
}
private VBox getImagePane() {
VBox pane = new VBox() {{
setAlignment(Pos.CENTER);
setPadding(new Insets(5));
setWidth(50);
setHeight(50);
setStyle("-fx-border-color: black");
}};
ImageView img = new ImageView("sample/imageGallery/cerulean.png") {{
setFitWidth(50);
setFitHeight(50);
setPreserveRatio(true);
Stage stage = zoomedStage(pane, this);
setOnMouseEntered(mouseEvent -> stage.show());
setOnMouseExited(mouseEvent -> stage.hide());
}};
pane.getChildren().add(img);
return pane;
}
private Stage zoomedStage(VBox parent, ImageView img) {
Stage stage = new Stage();
stage.setWidth(110);
stage.setHeight(110);
VBox pane = new VBox() {{
setAlignment(Pos.CENTER);
setPadding(new Insets(5));
setWidth(110);
setHeight(110);
setStyle("-fx-border-color: black");
}};
pane.setPickOnBounds(false);
ImageView zoomedImage = new ImageView(img.getImage());
zoomedImage.setFitHeight(100);
zoomedImage.setFitWidth(100);
pane.getChildren().add(zoomedImage);
stage.setScene(new Scene(pane));
stage.initStyle(StageStyle.UNDECORATED);
return stage;
}
}
From here, it should just be a matter or fixing the stage's coordinates to be centered over the image and then remove the stage's decoration.
Known Issues:
You will also need to handle the issue with the mouse cursor being blocked by the new stage. This will lead to a loop where the mouse is constantly entering and exiting the thumbnail of the image, causing the zoomed in stage to flicker.
From my point of view, you have a VBox with spacing 0 so each ImageView is tightly packed to each other so the glow effect is not well visible in every image. In that case you could just add a margin each time you want to select an ImageView in order to 'help' the glowing effect to appear.
For Java 8 :
Sadly, this can't be happened from a CSS cause the ImageView does not provide any rule for setting margin or padding. So you are more or less (in my opinion) bound to write that behaviour through code.
:
private Node createImageView(String imageLink) {
// Setting the image view
ImageView imageView = new ImageView(imageLink);
// setting the fit width of the image view
imageView.setFitWidth(400);
// Setting the preserve ratio of the image view
imageView.setPreserveRatio(true);
// Instantiating the Glow class
Glow glow = new Glow();
imageView.setOnMouseEntered(e -> {
// setting level of the glow effect
glow.setLevel(0.9);
// Adding a margin on TOP and Bottom side just to make the
// glowing effect visible
VBox.setMargin(imageView, new Insets(2,0,2,0));
});
imageView.setOnMouseExited(e -> {
// remove the glow effect
glow.setLevel(0.0);
// remove the margins
VBox.setMargin(imageView, new Insets(0));
});
// Applying bloom effect to text
imageView.setEffect(glow);
return imageView;
}

How to draw a line that intersects a text node without seeing the line inside the node

I would like to draw a vertical line, and that vertical line may intersect a text object. When I do so without any modification, the line goes right through the word, making the word hard to read. I want to make it so that the line disappears when it reaches the text, and continues immediately after the text node. I've tried using toBack() with a css background for the text to make a square around it, but it seems like text nodes have transparent backgrounds, so the line is still visible behind the letters. Is there another way of doing this so that the text is not intersected by the line? Note that the text may or may not be there all the time, and it can be at any coordinate due to the nature of my program, so I can't just draw two lines (One before the text and one after).
Using a Label instead of a Text will make things easier. Label is (ultimately) a subclass of Region, so it has properties such as a background color which can be styled, allowing it to have an opaque background. Using the CSS rule
-fx-background-color: -fx-background ;
on a label will give it the default background color. By contrast, Text is a subclass of Shape and only has properties such as fill and stroke, which color the interior and exterior of the glyphs that make up the text, respectively.
So you can do
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Line;
import javafx.scene.text.Text;
import javafx.stage.Stage;
public class TextVsLabel extends Application {
#Override
public void start(Stage primaryStage) {
StackPane labelPane = new StackPane();
labelPane.setMinSize(250, 250);
Label label = new Label("A Label");
label.setStyle("-fx-background-color: -fx-background;");
labelPane.getChildren().add(label);
addLineToPane(labelPane);
StackPane textPane = new StackPane();
textPane.setMinSize(250, 250);
textPane.getChildren().add(new Text("A Text"));
addLineToPane(textPane);
HBox root = new HBox(5, labelPane, textPane);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
private void addLineToPane(Pane pane) {
Line line = new Line();
line.startXProperty().bind(pane.widthProperty().divide(2));
line.endXProperty().bind(line.startXProperty());
line.setStartY(0);
line.endYProperty().bind(pane.heightProperty());
// add line to beginning of pane's parent list, so it appears
// behind everything else
pane.getChildren().add(0, line);
}
public static void main(String[] args) {
launch(args);
}
}
which results in
Note that if you need more space around the text in the label, you can add padding to the label: -fx-padding: 1em;, etc.

How to center/wrap/truncate Text to fit within Rectangle in JavaFX 2.1?

I need to dynamically create Rectangles over Pane in JavaFX 2.1. Next I need to center/wrap/truncate Text over the Rectangle. The text has to fit within the rectangle. I am able to center and wrap the text with the following code, however, if the text length is too long, it will appear out of the rectangle. I want to create the behavior like Label within StackPane, essentially if the Rectangle grows, the Text will grow with it but always remain in the center of the Rectangle and if the Text cannot fit within the Rectangle, it will be truncated accordingly.
Rectangle r;
Text t;
...
//center and wrap text within rectangle
t.wrappingWidthProperty().bind(rect.widthProperty().multiply(0.9);
t.xProperty().bind(rect.xProperty().add(rect.widthProperty().subtract(t.boundsInLocalProperty().getValue().getWidth().divide(2)));
t.yProperty().bind(rect.yProperty().add(rect.heightProperty().divide(2)));
t.setTextAlignment(TextAlignment.CENTER);
t.setTextOrigin(VPos.CENTER);
What properties can I use to achieve that or is there a better way of doing this?
Here is a sample alternate implementation.
It uses a subclass of Group with a layoutChildren implementation rather than the binding api.
import javafx.application.Application;
import javafx.beans.property.StringProperty;
import javafx.geometry.VPos;
import javafx.scene.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.*;
import javafx.stage.Stage;
public class TextInRectangle extends Application {
public static void main(String[] args) throws Exception { launch(args); }
public void start(final Stage stage) throws Exception {
TextBox text = new TextBox("All roads lead to Rome", 100, 100);
text.setLayoutX(30);
text.setLayoutY(20);
final Scene scene = new Scene(text, 160, 140, Color.CORNSILK);
stage.setScene(scene);
stage.show();
}
class TextBox extends Group {
private Text text;
private Rectangle rectangle;
private Rectangle clip;
public StringProperty textProperty() { return text.textProperty(); }
TextBox(String string, double width, double height) {
this.text = new Text(string);
text.setTextAlignment(TextAlignment.CENTER);
text.setFill(Color.FORESTGREEN);
text.setTextOrigin(VPos.CENTER);
text.setFont(Font.font("Comic Sans MS", 25));
text.setFontSmoothingType(FontSmoothingType.LCD);
this.rectangle = new Rectangle(width, height);
rectangle.setFill(Color.BLACK);
this.clip = new Rectangle(width, height);
text.setClip(clip);
this.getChildren().addAll(rectangle, text);
}
#Override protected void layoutChildren() {
final double w = rectangle.getWidth();
final double h = rectangle.getHeight();
clip.setWidth(w);
clip.setHeight(h);
clip.setLayoutX(0);
clip.setLayoutY(-h/2);
text.setWrappingWidth(w * 0.9);
text.setLayoutX(w / 2 - text.getLayoutBounds().getWidth() / 2);
text.setLayoutY(h / 2);
}
}
}
Sample output of the sample app:
A couple of notes:
It is usually best to use a Label rather than trying to recreate part of a Label's functionality.
The layout in layoutChildren approach is a similar to that used by the JavaFX team in implementing the JavaFX control library. There are probably reasons they use layoutChildren rather than binding for layout, but I am not aware of what all of those reasons are.
I find for simple layouts, using the pre-built controls and layout managers from the JavaFX library is best (for instance the above control could have been implemented using just a Label or Text in StackPane). Where I can't quite get the layout I need from the built-in layouts, then I will supplement their usage with bindings, which I find also very easy to work with. I don't run into the need to use layoutChildren that much. It's probably just a question of scaling up to lay out complex node groups - most likely doing the calculations in the layoutChildren method performs better and may be easier to work with and debug when applied to complex node groups.
Rather than truncating Text by calculating a text size and eliding extra characters from a String as a Label does, the code instead calls setClip on the text node to visually clip it to the rectangle's size. If instead you would like to truncate Text more like a Label, then you could look at the code for the JavaFX utility class which does the computation of clipped text.
The sample code in the question does not compile because it is missing a bracket on the wrappingWidthProperty expression and it uses getValue and getWidth methods inside a bind expression, which is not possible - instead it needs to use a listener on the boundsInLocalProperty.
Also, created a small sample app demoing adding text placed in a Label with a rectangular background to a Pane with precise control over the x,y position of the labeled rectangle via binding.

Resources