skip to Main Content

I have a node which looks like this:

enter image description here

Using only css, I want the label to overlay its parent border color, so the portion of the border color under the label gets invisible.

Css code I used to make this border:

-fx-border-color: black;
-fx-border-width: 3;
-fx-border-radius: 8;

Additional info:

1 -> It’s possible to achieve the same effect by placing -background-color: white to the label, although in my case it’s not a possibility.

2 -> For performance/arch reasons, it’s not possible to achieve this effect through styled BorderPane or TitledPane nodes as suggested in the commends. Hence the importance of only using CSS or at least the less java code as possible.

2

Answers


  1. This is just a code dump of stuff you can choose to study or ignore. I may or may not provide additional explanation or answer further questions about it.

    import javafx.scene.Group;
    import javafx.scene.paint.Color;
    import javafx.scene.shape.LineTo;
    import javafx.scene.shape.MoveTo;
    import javafx.scene.shape.Path;
    import javafx.scene.shape.Rectangle;
    import javafx.scene.text.Font;
    import javafx.scene.text.FontWeight;
    import javafx.scene.text.Text;
    
    public class CustomLabel extends Group {
        public static final double
                LABEL_WIDTH = 200,
                LABEL_HEIGHT = 60;
    
        private static final double
                FONT_SIZE = 15,
                TEXT_BASELINE_LEFT_X = 15,
                TEXT_BASELINE_LEFT_Y = 15,
                BORDER_INSETS = 9,
                BORDER_WIDTH = 2;
    
        private static final Font font = Font.font(
                "monospace",
                FontWeight.BOLD,
                FONT_SIZE
        );
    
        public CustomLabel(String labelText) {
            Text text = createText(
                    labelText
            );
            final double textWidth = text.getLayoutBounds().getWidth();
    
            Path border = createBorder(
                    textWidth
            );
    
            Rectangle background = new Rectangle(
                    LABEL_WIDTH,
                    LABEL_HEIGHT
            );
            background.setFill(Color.TRANSPARENT);
    
            getChildren().addAll(
                    background,
                    border,
                    text
            );
        }
    
        private static Path createBorder(double textWidth) {
            Path border = new Path(
                    new MoveTo(BORDER_INSETS, BORDER_INSETS),
                    new LineTo(TEXT_BASELINE_LEFT_X, BORDER_INSETS),
                    new MoveTo(TEXT_BASELINE_LEFT_X + textWidth, BORDER_INSETS),
                    new LineTo(LABEL_WIDTH - BORDER_INSETS, BORDER_INSETS),
                    new LineTo(LABEL_WIDTH - BORDER_INSETS, LABEL_HEIGHT - BORDER_INSETS),
                    new LineTo(BORDER_INSETS, LABEL_HEIGHT - BORDER_INSETS),
                    new LineTo(BORDER_INSETS, BORDER_INSETS)
            );
            border.setStrokeWidth(BORDER_WIDTH);
            return border;
        }
    
        private static Text createText(String labelText) {
            Text text = new Text(
                    TEXT_BASELINE_LEFT_X,
                    TEXT_BASELINE_LEFT_Y,
                    " " + labelText + " "
            );
    
            text.setFont(font);
            return text;
        }
    }
    

    import javafx.application.Application;
    import javafx.scene.Scene;
    import javafx.stage.Stage;
    
    public class SingleLabelApp extends Application {
        @Override
        public void start(Stage stage) {
            stage.setScene(new Scene(new CustomLabel("hello, world")));
            stage.show();
        }
    
        public static void main(String[] args) {
            launch();
        }
    }
    

    import javafx.animation.Animation;
    import javafx.animation.KeyFrame;
    import javafx.animation.KeyValue;
    import javafx.animation.Timeline;
    import javafx.application.Application;
    import javafx.beans.property.DoubleProperty;
    import javafx.beans.property.SimpleDoubleProperty;
    import javafx.scene.Scene;
    import javafx.scene.layout.Background;
    import javafx.scene.layout.Pane;
    import javafx.scene.paint.Color;
    import javafx.scene.paint.CycleMethod;
    import javafx.scene.paint.RadialGradient;
    import javafx.scene.paint.Stop;
    import javafx.stage.Stage;
    import javafx.util.Duration;
    
    public class SingleLabelAppWithAnimatedGradient extends Application {
        private static final double GRADIENT_RADIUS = 40;
        private static final Duration ANIMATION_DURATION = Duration.seconds(10);
    
        private DoubleProperty offset = new SimpleDoubleProperty(-GRADIENT_RADIUS);
    
        @Override
        public void start(Stage stage) {
            Pane pane = new Pane(
                    new CustomLabel("hello, world")
            );
    
            animateBackground(pane);
    
            stage.setScene(new Scene(pane));
            stage.show();
        }
    
        private void animateBackground(Pane pane) {
            offset.addListener(o -> refreshBackground(pane));
    
            Timeline gradientAnimator = new Timeline(
                    new KeyFrame(
                            Duration.seconds(0),
                            new KeyValue(
                                    offset,
                                    - GRADIENT_RADIUS
                            )
                    ),
                    new KeyFrame(
                            ANIMATION_DURATION,
                            new KeyValue(
                                    offset,
                                    GRADIENT_RADIUS + CustomLabel.LABEL_WIDTH
                            )
                    )
            );
            gradientAnimator.setCycleCount(Animation.INDEFINITE);
    
            gradientAnimator.play();
        }
    
        private void refreshBackground(Pane pane) {
            pane.setBackground(
                    Background.fill(
                            createGradient(
                                    offset.get()
                            )
                    )
            );
        }
    
        private static RadialGradient createGradient(double offset) {
            RadialGradient gradient = new RadialGradient(
                    30,
                    .2,
                    offset,
                    GRADIENT_RADIUS / 2,
                    GRADIENT_RADIUS,
                    false,
                    CycleMethod.NO_CYCLE,
                    new Stop(0, Color.SKYBLUE),
                    new Stop(GRADIENT_RADIUS, Color.PINK)
            );
    
            return gradient;
        }
    
        public static void main(String[] args) {
            launch();
        }
    }
    

    import javafx.application.Application;
    import javafx.scene.Group;
    import javafx.scene.Scene;
    import javafx.stage.Stage;
    
    public class ManyLabelApp extends Application {
        private static final int NUM_LABELS = 1_000;
    
        @Override
        public void start(Stage stage) {
            Group lotsaLabels = new Group();
    
            for (int i = 0; i < NUM_LABELS; i++) {
                CustomLabel customLabel = new CustomLabel(
                        "Item %06d".formatted(i)
                );
                lotsaLabels.getChildren().add(customLabel);
    
                customLabel.setLayoutY(i * CustomLabel.LABEL_HEIGHT);
            }
    
            stage.setScene(new Scene(lotsaLabels));
            stage.show();
        }
    
        public static void main(String[] args) {
            launch();
        }
    }
    

    import javafx.application.Application;
    import javafx.collections.FXCollections;
    import javafx.scene.Scene;
    import javafx.scene.control.ListCell;
    import javafx.scene.control.ListView;
    import javafx.stage.Stage;
    
    import java.util.ArrayList;
    import java.util.List;
    
    public class LabelListAppWithCreatedNodes extends Application {
        private static final int NUM_LABELS = 1_000;
    
        @Override
        public void start(Stage stage) {
            List<String> labelStrings = new ArrayList<>(NUM_LABELS);
            for (int i = 0; i < NUM_LABELS; i++) {
                labelStrings.add("Item %06d".formatted(i));
            }
    
            ListView<String> labelListView = new ListView<>(
                    FXCollections.observableList(labelStrings)
            );
            labelListView.setCellFactory(param -> new ListCell<>() {
                @Override
                protected void updateItem(String item, boolean empty) {
                    super.updateItem(item, empty);
    
                    if (item == null || empty) {
                        setGraphic(null);
                        return;
                    }
    
                    setGraphic(new CustomLabel(item)); // <- not good.
                }
            });
    
            stage.setScene(new Scene(labelListView));
            stage.show();
        }
    
        public static void main(String[] args) {
            launch();
        }
    }
    

    import javafx.scene.Group;
    import javafx.scene.paint.Color;
    import javafx.scene.shape.LineTo;
    import javafx.scene.shape.MoveTo;
    import javafx.scene.shape.Path;
    import javafx.scene.shape.Rectangle;
    import javafx.scene.text.Font;
    import javafx.scene.text.FontWeight;
    import javafx.scene.text.Text;
    
    public class ChangeableCustomLabel extends Group {
        public static final double
                LABEL_WIDTH = 200,
                LABEL_HEIGHT = 60;
    
        private static final double
                FONT_SIZE = 15,
                TEXT_BASELINE_LEFT_X = 15,
                TEXT_BASELINE_LEFT_Y = 15,
                BORDER_INSETS = 9,
                BORDER_WIDTH = 2;
    
        private static final Font font = Font.font(
                "monospace",
                FontWeight.BOLD,
                FONT_SIZE
        );
    
        private final Text text = new Text();
        private final Path border = new Path();
        private final MoveTo textSkipper = new MoveTo();
    
        private static int numCreated = 0;
    
        public ChangeableCustomLabel(String labelText) {
            initText(
                    labelText
            );
            final double textWidth = calculateTextWidth();
    
            initBorder(
                    textWidth
            );
    
            Rectangle background = new Rectangle(
                    LABEL_WIDTH,
                    LABEL_HEIGHT
            );
            background.setFill(Color.TRANSPARENT);
    
            getChildren().addAll(
                    background,
                    border,
                    text
            );
    
            System.out.println("Num custom labels created: " + ++numCreated);
        }
    
        public void setLabelText(String labelText) {
            text.setText(" " + labelText + " ");
            textSkipper.setX(TEXT_BASELINE_LEFT_X + calculateTextWidth());
        }
    
        private void initBorder(double textWidth) {
            textSkipper.setX(TEXT_BASELINE_LEFT_X + textWidth);
            textSkipper.setY(BORDER_INSETS);
    
            border.getElements().addAll(
                    new MoveTo(BORDER_INSETS, BORDER_INSETS),
                    new LineTo(TEXT_BASELINE_LEFT_X, BORDER_INSETS),
                    textSkipper,
                    new LineTo(LABEL_WIDTH - BORDER_INSETS, BORDER_INSETS),
                    new LineTo(LABEL_WIDTH - BORDER_INSETS, LABEL_HEIGHT - BORDER_INSETS),
                    new LineTo(BORDER_INSETS, LABEL_HEIGHT - BORDER_INSETS),
                    new LineTo(BORDER_INSETS, BORDER_INSETS)
            );
            border.setStrokeWidth(BORDER_WIDTH);
        }
    
        private void initText(String labelText) {
            text.setX(TEXT_BASELINE_LEFT_X);
            text.setY(TEXT_BASELINE_LEFT_Y);
            text.setText(labelText);
            text.setFont(font);
        }
    
        private double calculateTextWidth() {
            return text.getLayoutBounds().getWidth();
        }
    }
    

    import javafx.application.Application;
    import javafx.collections.FXCollections;
    import javafx.scene.Scene;
    import javafx.scene.control.ListCell;
    import javafx.scene.control.ListView;
    import javafx.stage.Stage;
    
    import java.util.ArrayList;
    import java.util.List;
    
    public class LabelListAppWithCachedNodes extends Application {
        private static final int NUM_LABELS = 100_000;
    
        @Override
        public void start(Stage stage) {
            List<String> labelStrings = new ArrayList<>(NUM_LABELS);
            for (int i = 0; i < NUM_LABELS; i++) {
                labelStrings.add("Item %06d".formatted(i));
            }
    
            ListView<String> labelListView = new ListView<>(
                    FXCollections.observableList(labelStrings)
            );
            labelListView.setCellFactory(param -> new ListCell<>() {
                final ChangeableCustomLabel changeableCustomLabel = new ChangeableCustomLabel(
                        ""
                );
    
                @Override
                protected void updateItem(String item, boolean empty) {
                    super.updateItem(item, empty);
    
                    if (item == null || empty) {
                        setGraphic(null);
                        return;
                    }
    
                    changeableCustomLabel.setLabelText(item);
                    setGraphic(changeableCustomLabel);
                }
            });
    
            stage.setScene(new Scene(labelListView));
            stage.show();
        }
    
        public static void main(String[] args) {
            launch();
        }
    }
    
    Login or Signup to reply.
  2. +1 for @jewelsea approach.
    More or less I do also have a similar approach to build the border dynamically using Path (including corner radius). I also have another approach by using inverse clipping technique.

    I included both the approaches in the below demo. You can choose or ignore. But my main intension to provide a direction if someone is interested with this.

    enter image description here

    import javafx.application.Application;
    import javafx.geometry.Insets;
    import javafx.geometry.Pos;
    import javafx.scene.Node;
    import javafx.scene.Scene;
    import javafx.scene.control.Label;
    import javafx.scene.layout.Pane;
    import javafx.scene.layout.StackPane;
    import javafx.scene.layout.VBox;
    import javafx.scene.shape.*;
    import javafx.stage.Stage;
    
    import java.util.ArrayList;
    import java.util.List;
    
    public class TitledBorderDemo extends Application {
        String sampleText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
    
        @Override
        public void start(final Stage stage) throws Exception {
            VBox root = new VBox(20);
            root.setStyle("-fx-background-color:linear-gradient(to bottom, pink, yellow);");
            root.setAlignment(Pos.CENTER);
            root.setPadding(new Insets(15));
    
            buildWithPath(root);
            buildWithClip(root);
            buildWithPath(root);
            buildWithClip(root);
    
            Scene scene = new Scene(root, 600, 600);
            scene.getStylesheets().add(getClass().getResource("titledborder.css").toExternalForm());
            stage.setScene(scene);
            stage.setTitle("Titled Border Demo");
            stage.show();
        }
    
        private void buildWithPath(VBox root) {
            Label content = new Label();
            content.setWrapText(true);
            content.setText(sampleText);
    
            TitledBorderWithPath pane = new TitledBorderWithPath();
            pane.setTitle("With Path Approach");
            pane.setContent(content);
            root.getChildren().add(pane);
        }
    
        private void buildWithClip(VBox root) {
            Label content = new Label();
            content.setWrapText(true);
            content.setText(sampleText);
    
            TitledBorderWithClip pane = new TitledBorderWithClip();
            pane.setTitle("With Clip Approach");
            pane.setContent(content);
            root.getChildren().add(pane);
        }
    
        /**
         * Approach by using Path.
         */
        class TitledBorderWithPath extends StackPane {
            private final Path border = new Path();
            private final StackPane container = new StackPane();
            private final Label titleLabel = new Label();
    
            // Can configure this as a CSS styleable property.
            private final double borderRadius = 8;
    
            public TitledBorderWithPath() {
                getStyleClass().add("titled-border-path");
    
                border.getStyleClass().add("border");
                border.setManaged(false);
    
                titleLabel.getStyleClass().add("title-label");
                container.getStyleClass().add("container");
                getChildren().addAll(border, container);
                // Position the title label on the top border
                titleLabel.translateYProperty().bind(titleLabel.heightProperty().divide(2).add(titleLabel.layoutYProperty()).multiply(-1));
    
                titleLabel.needsLayoutProperty().addListener((obs, old, needsLayout) -> {
                    if (!needsLayout) {
                        drawBorder();
                    }
                });
                container.widthProperty().addListener(p -> drawBorder());
                container.heightProperty().addListener(p -> drawBorder());
            }
    
            private void drawBorder() {
                List<PathElement> e = new ArrayList<>();
                border.getElements().clear();
    
                double w = container.getWidth();
                double h = container.getHeight();
                double r = borderRadius;
                double x = titleLabel.getLayoutX() + titleLabel.getTranslateX();
    
    
                e.add(new MoveTo(x, 0));
                e.add(new LineTo(r, 0));
                e.add(arc(0, r));
                e.add(new LineTo(0, h - r));
                e.add(arc(r, h));
                e.add(new LineTo(w - r, h));
                e.add(arc(w, h - r));
                e.add(new LineTo(w, r));
                e.add(arc(w - r, 0));
                e.add(new LineTo(x + titleLabel.getWidth(), 0));
    
                border.getElements().addAll(e);
            }
    
            private ArcTo arc(double x, double y) {
                return new ArcTo(borderRadius, borderRadius, 0, x, y, false, false);
            }
    
            public void setTitle(final String title) {
                this.titleLabel.setText(title);
            }
    
            public void setContent(Node node) {
                container.getChildren().clear();
                container.getChildren().addAll(titleLabel, node);
            }
        }
    
        /**
         * Approach by using clipping.
         */
        class TitledBorderWithClip extends StackPane {
            private Label titleLabel;
    
            final Rectangle clip = new Rectangle();
            final Rectangle inverse = new Rectangle();
    
            final Pane border = new Pane();
            final StackPane container = new StackPane();
    
            public TitledBorderWithClip() {
                getStyleClass().add("titled-border-clip");
                titleLabel = new Label();
                titleLabel.getStyleClass().add("title-label");
    
                container.getChildren().add(titleLabel);
                container.getStyleClass().add("container");
    
                border.getStyleClass().add("border");
    
                getChildren().addAll(border, container);
    
    
                border.widthProperty().addListener(p -> setInverseClip(border, clip));
                border.heightProperty().addListener(p -> setInverseClip(border, clip));
    
                // Position the title label on the top border
                titleLabel.translateYProperty().bind(titleLabel.heightProperty().divide(2).add(titleLabel.layoutYProperty()).multiply(-1));
                titleLabel.needsLayoutProperty().addListener((obs, old, needsLayout) -> {
                    if (!needsLayout) {
                        setInverseClip(border, clip);
                    }
                });
            }
    
            public void setTitle(final String title) {
                this.titleLabel.setText(title);
            }
    
            public void setContent(Node node) {
                container.getChildren().clear();
                container.getChildren().addAll(titleLabel, node);
            }
    
            private void setInverseClip(final Pane node, final Rectangle clip) {
                clip.setWidth(titleLabel.getWidth());
                clip.setHeight(titleLabel.getHeight());
                clip.setX(titleLabel.getLayoutX() + titleLabel.getTranslateX());
                clip.setY(titleLabel.getLayoutY() + titleLabel.getTranslateY());
    
                inverse.setWidth(node.getWidth());
                inverse.setHeight(node.getHeight());
                node.setClip(Shape.subtract(inverse, clip));
            }
        }
    }
    

    titledborder.css

    .title-label{
        -fx-padding: 0px 5px 0px 5px;
        -fx-font-weight: bold;
        -fx-font-size: 14px;
        -fx-translate-x: 5px; /* To adjust the title placement horizontally */
    }
    
    /* With Clip Approach */
    .titled-border-clip {
        -fx-alignment: TOP_LEFT;
    }
    
    .titled-border-clip > .container{
        -fx-padding:10px 5px 5px 5px;
        -fx-alignment: TOP_LEFT;
    }
    
    .titled-border-clip > .border{
        -fx-border-color:red;
        -fx-border-width: 2px;
        -fx-border-radius: 5px;
    }
    
    /* With Path Approach */
    .titled-border-path {
        -fx-alignment: TOP_LEFT;
    }
    
    .titled-border-path > .container{
        -fx-padding:10px 5px 5px 5px;
        -fx-alignment: TOP_LEFT;
    }
    
    .titled-border-path > .border{
        -fx-stroke:red;
        -fx-stroke-width:2px;
    }
    

    For Simple layouts

    Ok, the above approaches may be an overkill for the layouts which doesn’t have a dynamic background (like gradients, images.. etc). For simple layouts, as @Slaw mentioned in the first comment, setting -fx-background-color: inherit; on both the parent and label should do the trick.

    enter image description here

    class SimpleTitledBorder extends StackPane {
            private final Label titleLabel = new Label();
    
            public SimpleTitledBorder() {
                getStyleClass().add("titled-border-simple");
                titleLabel.getStyleClass().add("title-label");
                getChildren().addAll(titleLabel);
                // Position the title label on the top border
                titleLabel.translateYProperty().bind(titleLabel.heightProperty().divide(2).add(titleLabel.layoutYProperty()).multiply(-1));
            }
            public void setTitle(final String title) {
                this.titleLabel.setText(title);
            }
    
            public void setContent(Node node) {
                getChildren().clear();
                getChildren().addAll(titleLabel, node);
            }
        }
    

    CSS Code:

    /* With Simple Approach */
    .title-label{
        -fx-padding: 0px 5px 0px 5px;
        -fx-font-weight: bold;
        -fx-font-size: 14px;
        -fx-translate-x: 5px; /* To adjust the title placement horizontally */
    }
    .titled-border-simple{
        -fx-background-color:inherit;
        -fx-border-color:red;
        -fx-border-width: 2px;
        -fx-border-radius: 5px;
        -fx-padding:10px 5px 5px 5px;
        -fx-alignment: TOP_LEFT;
    }
    
    .titled-border-simple > .title-label{
        -fx-background-color:inherit;
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search