@jewelsea 方法+1。
或多或少我也有类似的方法来使用路径(包括角半径)动态构建边界。我还有其他两种方法,一种使用inverse clipping
技术和其他使用border segments
技术。
我在下面的演示中包含了所有三种方法。您可以选择或忽略。但我的主要目的是如果有人对此感兴趣的话提供一个方向。
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.*;
import javafx.scene.shape.*;
import javafx.stage.Stage;
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);
buildWithStyle(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);
}
private void buildWithStyle(VBox root) {
Label content = new Label();
content.setWrapText(true);
content.setText(sampleText);
TitledBorderWithSegment pane = new TitledBorderWithSegment();
pane.setTitle("With Segment Approach");
pane.setContent(content);
root.getChildren().add(pane);
}
/**
* Approach by using border segment.
*/
class TitledBorderWithSegment extends StackPane {
private final Label titleLabel = new Label();
public TitledBorderWithSegment() {
getStyleClass().add("titled-border-segment");
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));
titleLabel.needsLayoutProperty().addListener((obs, old, needsLayout) -> {
if (!needsLayout) {
buildStyle();
}
});
widthProperty().addListener(p -> buildStyle());
heightProperty().addListener(p -> buildStyle());
}
private void buildStyle() {
double buffer = 8;
double r = getBorder().getStrokes().get(0).getRadii().getTopLeftHorizontalRadius();
double arcLength = Math.round(Math.toRadians(90) * r);
double w = getWidth();
double h = getHeight();
double titleX = titleLabel.getLayoutX() + titleLabel.getTranslateX();
double titleLength = titleLabel.getWidth();
double t = (h - (2 * r)) +
arcLength +
(w - (2 * r)) +
arcLength +
(h - (2 * r)) +
arcLength +
(w - titleX - titleLength - r) - buffer;
setStyle("-fx-border-style: segments(" + t + ", " + titleLength + ") line-cap round;");
}
public void setTitle(final String title) {
this.titleLabel.setText(title);
}
public void setContent(Node node) {
getChildren().clear();
getChildren().addAll(titleLabel, node);
}
}
/**
* 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");
getChildren().addAll(border, container);
border.getStyleClass().add("border");
border.setManaged(false);
titleLabel.getStyleClass().add("title-label");
// 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.getStyleClass().add("container");
container.widthProperty().addListener(p -> drawBorder());
container.heightProperty().addListener(p -> drawBorder());
}
private void drawBorder() {
double w = container.getWidth();
double h = container.getHeight();
double r = borderRadius;
double x = titleLabel.getLayoutX() + titleLabel.getTranslateX();
border.getElements().clear();
border.getElements().addAll(new MoveTo(x, 0),
new LineTo(r, 0),
arc(0, r), // Top left
new LineTo(0, h - r),
arc(r, h), // Bottom left
new LineTo(w - r, h),
arc(w, h - r), // Bottom right
new LineTo(w, r),
arc(w - r, 0), // Top right
new LineTo(x + titleLabel.getWidth(), 0));
}
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));
}
}
}
标题边框.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;
}
/* With Simple Approach */
.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;
-fx-border-color:inherit;
-fx-border-width:inherit;
-fx-border-radius:inherit;
}
/* With Segment Approach */
.titled-border-segment{
-fx-border-color:red;
-fx-border-width: 2px;
-fx-border-radius: 10px;
-fx-padding:10px 5px 5px 5px;
-fx-alignment: TOP_LEFT;
}
对于简单布局
好的,上述方法对于布局来说可能有点大材小用了doesn't有动态背景(如渐变、图像等)。对于简单的布局,正如@Slaw 在第一条评论中提到的,设置-fx-background-color: inherit;
在父级和标签上都应该可以解决问题。
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代码:
/* 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;
}
并继承标签的边框属性,为布局提供了另一种外观:)
.titled-border-simple > .title-label{
-fx-background-color:inherit;
-fx-border-color:inherit;
-fx-border-width:inherit;
-fx-border-radius:inherit;
}