在JavaFX中构建可复用的GlassPaneBase控件
玻璃拟态(Glassmorphism)仍然是现代桌面应用中最受青睐的视觉风格之一。柔和的半透明、微妙的层次感与干净的边框,营造出一种不透明面板很难达到的精致感。
JavaFX 并未内置玻璃表面。要做出具有专业质感的玻璃表面,仅有半透明背景和模糊效果远远不够。它需要清晰的公共 API、结构良好的 Skin,以及承载真实内容的能力。
本文将展示如何构建一个可复用的 GlassPaneBase 控件,它可作为卡片、侧边面板、对话框以及任何需要呈现磨砂玻璃效果的表面的基础。Vitra UI 库也采用了同样的方法。
控件 API
这是该控件的基本结构,因为完整实现需要大量代码。所有视觉职责都放在 Skin 中。
public class GlassPaneBase extends Control {
// Regular variables
private final ObservableList<Node> glassChildren = FXCollections.observableArrayList();
private final ObjectProperty<Node> backdropSource = new SimpleObjectProperty<>(this, "backdropSource");
private final BooleanProperty liveBackdrop = new SimpleBooleanProperty(this, "liveBackdrop", false);
private final ReadOnlyIntegerWrapper backdropRevision = new ReadOnlyIntegerWrapper(this, "backdropRevision", 0);
@Override
protected Skin<?> createDefaultSkin() {
return new GlassPaneBaseSkin(this);
}
//More code goes here.
}
这种分离能让应用代码保持整洁,也便于后续扩展该控件。
Skin 结构
可视树由多个分层区域组成:
public class GlassPaneBaseSkin extends SkinBase<GlassPaneBase> {
private static final double LIVE_FRAME_NANOS = 1_000_000_000.0 / 30.0;
//The root of the glass pane.
protected final Pane layerStack = new Pane();
//Flat frosted color.
protected final Rectangle blurSource = new Rectangle();
//A tint for the blurred image.
protected final Rectangle tintLayer = new Rectangle();
//The borders of the glass pane.
protected final Rectangle borderLayer = new Rectangle();
//The blurred picture of what is behind the pane.
protected final ImageView backdropView = new ImageView();
protected final Pane contentLayer = new Pane();
//Rounded clips of the picture and of the content. They are applied to the layers, not to the control, so the
//drop shadow of the layer stack is not cut off.
private final Rectangle backdropClip = new Rectangle();
//The clip is on this holder, not on the blurred picture itself: that way the blur never softens the rounded edge.
private final Pane backdropHolder = new Pane();
private final Rectangle contentClip = new Rectangle();
private final AnimationTimer liveTimer;
private long lastLiveFrame;
private boolean refreshPending;
private final ListChangeListener<Object> glassChildrenListener =
change -> contentLayer.getChildren().setAll(getSkinnable().getGlassChildren());
private double pad;
public GlassPaneBaseSkin(GlassPaneBase control) {
super(control);
blurSource.setFill(Color.web("#B8C6DB"));
borderLayer.setFill(Color.TRANSPARENT);
borderLayer.setStrokeWidth(1.0);
borderLayer.setStroke(Color.WHITE.deriveColor(0, 1, 1, control.getBorderOpacity()));
backdropView.setPreserveRatio(false);
backdropView.setVisible(false);
backdropHolder.getChildren().add(backdropView);
backdropHolder.setClip(backdropClip);
contentLayer.setClip(contentClip);
layerStack.getChildren().addAll(backdropHolder, blurSource, tintLayer, borderLayer, contentLayer);
layerStack.getStyleClass().add("glass-pane-layers");
DropShadow shadow = new DropShadow();
shadow.setRadius(20);
shadow.setColor(Color.web("#000000", 0.18));
shadow.setOffsetY(6);
layerStack.setEffect(shadow);
getChildren().add(layerStack);
//Children added before the skin exists (the skin is created lazily) must be shown too.
contentLayer.getChildren().setAll(control.getGlassChildren());
control.getGlassChildren().addListener(glassChildrenListener);
//Bind visual state to the control's styleable properties.
registerChangeListener(control.blurRadiusProperty(), o -> {
applyBlurEffect();
scheduleRefresh();
});
registerChangeListener(control.tintColorProperty(), o -> updateTint());
registerChangeListener(control.tintOpacityProperty(), o -> updateTint());
registerChangeListener(control.borderOpacityProperty(), o -> updateBorder());
registerChangeListener(control.glassCornerRadiusProperty(), o -> {
updateCorners();
updateClips();
});
//Anything that changes what is behind the pane, or where the pane is, needs a new picture.
registerChangeListener(control.layoutXProperty(), o -> scheduleRefresh());
registerChangeListener(control.layoutYProperty(), o -> scheduleRefresh());
registerChangeListener(control.widthProperty(), o -> scheduleRefresh());
registerChangeListener(control.heightProperty(), o -> scheduleRefresh());
registerChangeListener(control.parentProperty(), o -> scheduleRefresh());
registerChangeListener(control.backdropSourceProperty(), o -> scheduleRefresh());
registerChangeListener(control.backdropRevisionProperty(), o -> scheduleRefresh());
registerChangeListener(control.sceneProperty(), o -> {
updateLiveTimer();
scheduleRefresh();
});
registerChangeListener(control.liveBackdropProperty(), o -> updateLiveTimer());
liveTimer = new AnimationTimer() {
@Override
public void handle(long now) {
if (now - lastLiveFrame >= LIVE_FRAME_NANOS) {
lastLiveFrame = now;
refreshBackdrop();
}
}
};
applyBlurEffect();
updateTint();
updateBorder();
updateCorners();
updateLiveTimer();
scheduleRefresh();
}
private void scheduleRefresh() {
if (refreshPending) {
return;
}
refreshPending = true;
Platform.runLater(() -> {
refreshPending = false;
refreshBackdrop();
});
}
private void refreshBackdrop() {
GlassPaneBase control = getSkinnable();
if (control == null) {
return;
}
Node source = control.getBackdropSource() != null ? control.getBackdropSource() : control.getParent();
double w = layerStack.getWidth();
double h = layerStack.getHeight();
if (source == null || control.getScene() == null || source.getScene() != control.getScene() || w <= 0 || h <= 0) {
showFallback();
return;
}
double radius = Math.max(0, Math.min(255, control.getBlurRadius()));
pad = Math.ceil(radius * 2);
//Where the pane is, expressed in the coordinates of the node that is going to be photographed.
Bounds area = source.sceneToLocal(control.localToScene(control.getLayoutBounds()));
if (area == null || area.getWidth() <= 0 || area.getHeight() <= 0) {
showFallback();
return;
}
SnapshotParameters params = new SnapshotParameters();
params.setFill(Color.TRANSPARENT);
params.setViewport(new Rectangle2D(area.getMinX() - pad, area.getMinY() - pad,
area.getWidth() + 2 * pad, area.getHeight() + 2 * pad));
//Hide this pane while the picture is taken, so it does not appear inside its own backdrop.
boolean wasVisible = layerStack.isVisible();
layerStack.setVisible(false);
WritableImage picture;
try {
picture = source.snapshot(params, null);
} finally {
layerStack.setVisible(wasVisible);
}
backdropView.setImage(picture);
backdropView.setVisible(true);
blurSource.setVisible(false);
updateClips();
}
//More code goes here.
}
用法示例
当 GlassPane 准备就绪后,可以执行以下代码:
public void glass() {
Stage primaryStage = new Stage();
StackPane backdropContent = new StackPane();
String image="Image.jpg";
BackgroundImage backgroundImage=new BackgroundImage(new Image(image),BackgroundRepeat.SPACE,
BackgroundRepeat.SPACE,
BackgroundPosition.DEFAULT,new BackgroundSize(100, 100, true, true, false, true));
Background background=new Background(backgroundImage);
backdropContent.setBackground(background);
Label label = new Label("JAVA FX\nGLASS");
label.setStyle("-fx-text-fill: purple; -fx-font-size: 40px; -fx-font-weight: bold; -fx-text-alignment: center;");
GlassPaneBase glassPaneBase = new GlassPaneBase();
glassPaneBase.addChild(label);
glassPaneBase.setMaxSize(400,300);
backdropContent.getChildren().add(glassPaneBase);
Scene scene = new Scene(backdropContent, 1000, 700);
primaryStage.setScene(scene);
primaryStage.setTitle("CustomControls.Glassmorphism Pro Interface");
primaryStage.show();
}
结语
GlassPaneBase 是 Vitra 中包含的若干面板和控件之一。Vitra 是一个 UI 库,面向需要开发现代 JavaFX 应用、又不想从头重建每一个高级控件的开发者。
你可以在 skinning-javafx-applications.netlify.app 浏览完整的控件集合。## 加入讨论
JavaFX [LOADING...]
[LOADING...]Libán Bande González2026年10月28日 265 次浏览
宣布 Vitra——一个面向 JavaFX 的现代 UI 库
JavaFX库桌面
JavaFX [LOADING...]
[LOADING...]Libán Bande González2026年10月6日 232 次浏览
在 JavaFX 中构建代码编辑器控件
JavaFXJava
JavaFX [LOADING...]
[LOADING...]Libán Bande González2026年10月1日 369 次浏览
宣布《JavaFX 应用程序皮肤化》
JavaFXJava
JavaFX [LOADING...]
[LOADING...]Almas Baimagambetov2021年1月18日 25,036 次浏览