Ohhnews

分类导航

$ cd ..
foojay原文

在JavaFX中构建可复用的GlassPaneBase控件

#javafx#玻璃拟态#ui控件#control-skin#桌面应用

玻璃拟态(Glassmorphism)仍然是现代桌面应用中最受青睐的视觉风格之一。柔和的半透明、微妙的层次感与干净的边框,营造出一种不透明面板很难达到的精致感。

JavaFX 并未内置玻璃表面。要做出具有专业质感的玻璃表面,仅有半透明背景和模糊效果远远不够。它需要清晰的公共 API、结构良好的 Skin,以及承载真实内容的能力。

本文将展示如何构建一个可复用的 GlassPaneBase 控件,它可作为卡片、侧边面板、对话框以及任何需要呈现磨砂玻璃效果的表面的基础。Vitra UI 库也采用了同样的方法。

控件 API

这是该控件的基本结构,因为完整实现需要大量代码。所有视觉职责都放在 Skin 中。

$ java
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 结构

可视树由多个分层区域组成:

$ java
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 准备就绪后,可以执行以下代码:

$ java
    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 次浏览

JavaFX 高性能渲染技巧

JavaFX性能研究