Michal Rama
Michal Rama

Reputation: 299

How to center text without using any layout?

import javafx.geometry.VPos;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
import javafx.scene.text.TextBoundsType;
import javafx.stage.Stage;

public class HorizontalTextAlignment extends javafx.application.Application {

    @Override
    public void start(Stage stage) {
        var rectangle = new Rectangle(600, 100, Color.TURQUOISE);
        var text = new Text(rectangle.getWidth() / 2, rectangle.getHeight() / 2, "TEXT");
        text.setBoundsType(TextBoundsType.VISUAL);
        text.setFont(new Font(100));
        text.setTextAlignment(TextAlignment.CENTER);
        text.setTextOrigin(VPos.CENTER);
        stage.setScene(new Scene(new Pane(rectangle, text)));
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

enter image description here

I would need to center the text without using any layout. As you may notice, while vertical alignment works, horizontal doesn't. Of course, this could be solved by a simple calculation.

// You need to change the horizontal position of the text for this to work.
var text = new Text(rectangle.getLayoutX(), rectangle.getHeight() / 2, "TEXT");
...
var rectangleWidth = rectangle.getWidth();
var textWidth = text.getLayoutBounds().getWidth();
text.setLayoutX((rectangleWidth - textWidth) / 2);

enter image description here

The problem is, although it's not easy to see in the picture that it's not exactly in the middle.

This is good to see if you only change the initialization.

var text = new Text(rectangle.getLayoutX(), rectangle.getHeight() / 2, "TEXT");

enter image description here

However, this cannot be solved by simply subtracting the desired value (in this case -2) from the horizontal position, because when I change the text.

enter image description here

It is necessary to subtract 9. And that is the problem. I can never know in advance how much must be deducted from the horizontal position.

How do I solve this, please?

Thank you

Upvotes: 0

Views: 1301

Answers (3)

Oboe
Oboe

Reputation: 2713

If are not forced to use those specific Nodes, I would use Label instead of Text and StackPane instead of Pane:

Rectangle rectangle = new Rectangle(600, 100, Color.TURQUOISE);

Label label = new Label("TEXT");
label.setAlignment(Pos.CENTER);
label.setFont(new Font(100));

StackPane.setAlignment(label, Pos.CENTER);
StackPane.setAlignment(rectangle, Pos.CENTER);

StackPane pane = new StackPane(rectangle, label);

pane.prefWidthProperty().bind(rectangle.widthProperty());
pane.prefHeightProperty().bind(rectangle.heightProperty());

stage.setScene(new Scene(pane));
stage.show();

Update

Given that you cannot use StackPane, here is a solution completely out of the box.

You can create a new Shape subtracting the Text from a Rectangle. Then you will have two Rectangles, one with the text transparent and another one for the background (that will give color to the text).

The benefit of these approach is that you end with two Shapes with the same bounds and will be easier to layout in the Scene.

import javafx.application.Application;
import javafx.geometry.VPos;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Shape;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
import javafx.scene.text.TextBoundsType;
import javafx.stage.Stage;

public class App extends Application {

    @Override
    public void start(Stage stage) {

        Rectangle rectangle = new Rectangle(600, 100);

        Text text = new Text("TEXT");

        text.setTextAlignment(TextAlignment.CENTER);
        text.setBoundsType(TextBoundsType.VISUAL);
        text.setFont(new Font(100));
        text.setTextOrigin(VPos.CENTER);

        text.setX(rectangle.getWidth() / 2 - text.getLayoutBounds().getWidth() / 2);
        text.setY(rectangle.getHeight() / 2);

        Shape shape = Shape.subtract(rectangle, text);

        shape.setFill(Color.TURQUOISE);

        shape.layoutXProperty().bind(rectangle.layoutXProperty());
        shape.layoutYProperty().bind(rectangle.layoutYProperty());
        shape.translateXProperty().bind(rectangle.translateXProperty());
        shape.translateYProperty().bind(rectangle.translateYProperty());

        stage.setScene(new Scene(new Pane(rectangle, shape)));
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Upvotes: 1

VGR
VGR

Reputation: 44338

Instead of a Text object, you can use a Label with its alignment set to center the text. Then you just need to force the Label’s minimum width to match the parent’s width (which is, roughly, mimicking what some layouts do):

@Override
public void start(Stage stage) {
    var rectangle = new Rectangle(600, 100, Color.TURQUOISE);
    var text = new Label("TEXT");
    text.setFont(new Font(100));
    text.setAlignment(Pos.CENTER);
    Pane pane = new Pane(rectangle, text);
    text.minWidthProperty().bind(pane.widthProperty());
    text.minHeightProperty().bind(pane.heightProperty());
    stage.setScene(new Scene(pane));
    stage.show();
}

Upvotes: 1

Slaw
Slaw

Reputation: 45806

This could all be accomplished much more easily by using a StackPane (e.g. as shown in @Oboe's answer). Using the appropriate layout should always be your first choice. However, the rest of my answer shows how to do it manually.


The x and y properties define the origin point of the text node. The x-coordinate determines where the left side of the text starts. However, the y-coordinate can be customized somewhat via the textOrigin property.

                   X (always)
                   |
Y (VPos.TOP)-------╔════════════════════════════════╗
                   ║                                ║
Y (VPos.CENTER)----║          Text Node             ║
                   ║                                ║
Y (VPos.BOTTOM)----╚════════════════════════════════╝

Note: There's also VPos.BASELINE (the default) but I don't know how to visualize that.

As you can see, when you set the x property to rectangle.getWidth() / 2 you are aligning the left side of the text node with the horizontal center of the rectangle. If you want to align the horizontal center of the text node with the horizontal center of the rectangle you have to take the width of the text node into account. Here's an example:

import javafx.application.Application;
import javafx.geometry.VPos;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
import javafx.scene.text.TextBoundsType;
import javafx.stage.Stage;

public class App extends Application {

  @Override
  public void start(Stage stage) {
    var rectangle = new Rectangle(600, 100, Color.TURQUOISE);

    var text = new Text("TEXT");
    text.setBoundsType(TextBoundsType.VISUAL);
    text.setFont(new Font(100));
    text.setTextAlignment(TextAlignment.CENTER);
    text.setTextOrigin(VPos.CENTER);

    // WARNING: Assumes Rectangle is at (0, 0)
    text.setX(rectangle.getWidth() / 2 - text.getLayoutBounds().getWidth() / 2);
    text.setY(rectangle.getHeight() / 2);

    stage.setScene(new Scene(new Pane(rectangle, text)));
    stage.show();
  }
}

Notice that I set the x property after setting everything else (e.g. font, text, etc.). That's necessary for the resulting text's width to be known. Of course, this is not responsive; if the the dimensions of the rectangle or text change you have to manually recompute the new x value. The ideal solution, when we ignore layouts, is to use a bindings. Something like:

// WARNING: Assumes Rectangle is at (0, 0)
text.xProperty()
    .bind(
        Bindings.createDoubleBinding(
            () -> rectangle.getWidth() / 2 - text.getLayoutBounds().getWidth() / 2,
            rectangle.widthProperty(),
            text.layoutBoundsProperty()));

Unfortunately, changing the x property changes the layout bounds of the text and the above leads to a StackOverflowError. The same is true of the text's bounds-in-local (this is a characteristic of shapes). There is a solution though if you don't mind positioning the text via its layoutX and layoutY properties:

// WARNING: Assumes Rectangle is at (0, 0)

// leave text.x = 0 and text.y = 0
text.layoutXProperty()
    .bind(
        Bindings.createDoubleBinding(
            () -> rectangle.getWidth() / 2 - text.getLayoutBounds().getWidth() / 2,
            rectangle.widthProperty(),
            text.layoutBoundsProperty()));
// assumes textOrigin = VPos.CENTER
text.layoutYProperty().bind(rectangle.heightProperty().divide(2));

Note the layoutX and layoutY properties come from Node. They're used by layouts to position their children. But since you're not using a layout which automatically positions its children you can safely set them manually. There's also the translateX and translateY properties. These add onto the layout properties. For example, ignoring other things, the final x-position will be layoutX + translateX (similar for the final y-position); for a Text node the final x-position would be x + layoutX + translateX (again, similar for the final y-position).

You may have noticed that all my examples warn about assuming the rectangle is at the origin of its parent. To fix that issue you simply have to add the rectangles x and y positions to the text's layout position.

Upvotes: 1

Related Questions