PseudoClass:来自设置自定义状态时中断的状态的通知

kle*_*tra 1 javafx pseudo-class

下面是一个行为不像我期望的行为的例子 - 想知道行为或我的期望是否错误(很可能是我做了一些愚蠢的错误;)。

它能做什么:

  • 它实现了一个带有自定义伪类的自定义按钮,其方式与 java doc中的代码示例非常相似
  • 它在按钮返回的 observableSet 上注册一个监听器getPseudoClassStates(),记录更改时的活动状态
  • 它有 ui(一个简单的按钮),可以切换自定义按钮的特殊状态

预期行为:通过鼠标/键盘与自定义按钮交互的通知(悬停、聚焦、武装......)

实际行为:如果未设置特殊状态,则与预期相同,设置一次特殊状态后没有通知

问题:

  • 错误或功能?
  • 如何更改设置以始终收到通知?

这个例子:

public class PseudoStateNotification extends Application {
    private SpecialButton specialButton;
    private SetChangeListener sl;

    public static class SpecialButton extends Button {
        private static PseudoClass SPECIAL = PseudoClass.getPseudoClass("special");
        private BooleanProperty special = new SimpleBooleanProperty(this, "special", false) {

            @Override
            protected void invalidated() {
                pseudoClassStateChanged(SPECIAL, get());
            }

        };

        public SpecialButton(String text) {
            super(text);
        }

        public void setSpecial(boolean sp) {
            special.set(sp);
        }

        public boolean isSpecial() {
            return special.get();
        }

        public BooleanProperty specialProperty() {
            return special;
        }
    }

    private Parent createContent() {
        sl = change -> LOG.info("pseudo-changed: " + change.getSet());
        specialButton =  new SpecialButton("custom buttom");
        specialButton.getPseudoClassStates().addListener(sl);
        BorderPane pane = new BorderPane(specialButton);
        Button toggle = new Button("toggle special state");
        toggle.setOnAction(e -> {
            specialButton.setSpecial(!specialButton.isSpecial());
        });
        pane.setBottom(toggle);
        return pane;
    }

    @Override
    public void start(Stage stage) throws Exception {
        stage.setScene(new Scene(createContent(), 200, 200));
        URL uri = getClass().getResource("pseudo.css");
        stage.getScene().getStylesheets().add(uri.toExternalForm());
        stage.setTitle(FXUtils.version());
        stage.show();
    }

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

    @SuppressWarnings("unused")
    private static final Logger LOG = Logger
            .getLogger(PseudoStateNotification.class.getName());

}
Run Code Online (Sandbox Code Playgroud)

样式表pseudo.css:

.button:special {
  -fx-text-fill: red;
}
Run Code Online (Sandbox Code Playgroud)

更新:

正如在Jose's answer 中很好地跟踪的那样,根本原因是ObservaleSet<PseudoClass> Node.getPseudoClassStates()返回一个自由飞行的不可修改的包装器围绕状态,这使得返回的集合可被垃圾收集。在我的书中,这是一个错误- 例如,可比较的 apiTableView.getVisibleLeafColumns()被正确实现并保持对包装器的强引用。hack-around 是在客户端代码中保持对集合的强引用。

Jos*_*eda 5

这不是自定义状态的问题,而是在某些时候被垃圾收集的弱侦听器的常见情况:SetChangeListener一段时间后您将消失。

您可以通过根本不单击切换按钮来验证它。只需将按钮悬停几次,然后单击它。过了一段时间,您将不会收到任何通知。

例如,这是一系列更改:

INFO: pseudo-changed: [focused]
INFO: pseudo-changed: [hover, focused]
INFO: pseudo-changed: [focused]
INFO: pseudo-changed: [hover, focused]
INFO: pseudo-changed: [focused]
INFO: pseudo-changed: [hover, focused]
INFO: pseudo-changed: [hover, pressed, focused]
INFO: pseudo-changed: [hover, pressed, focused, armed]
INFO: pseudo-changed: [hover, focused, armed]
INFO: pseudo-changed: [hover, focused]
INFO: pseudo-changed: [hover, pressed, focused]
INFO: pseudo-changed: [hover, pressed, focused, armed]
INFO: pseudo-changed: [hover, focused, armed]
INFO: pseudo-changed: [hover, focused]
INFO: pseudo-changed: [hover, pressed, focused]
INFO: pseudo-changed: [hover, pressed, focused, armed]
INFO: pseudo-changed: [hover, focused, armed]
INFO: pseudo-changed: [hover, focused]
INFO: pseudo-changed: [focused]
INFO: pseudo-changed: [hover, focused]
Run Code Online (Sandbox Code Playgroud)

在最后一次之后,我不再收到通知。

为了避免这种情况,我们需要做的就是持有一组可观察的伪类的强引用:

private ObservableSet<PseudoClass> states;

private Parent createContent() {
    sl = change -> LOG.info("pseudo-changed: " + change.getSet());
    specialButton =  new SpecialButton("custom buttom");
    states = specialButton.getPseudoClassStates();
    states.addListener(sl);
    ...
}
Run Code Online (Sandbox Code Playgroud)

现在它可以处理来自按钮或切换按钮的任何状态更改。

INFO: pseudo-changed: [hover, focused]
INFO: pseudo-changed: [focused]
INFO: pseudo-changed: []
INFO: pseudo-changed: [special]
INFO: pseudo-changed: [hover, special]
INFO: pseudo-changed: [hover, pressed, special]
INFO: pseudo-changed: [hover, pressed, focused, special]
...
INFO: pseudo-changed: [focused, special]
INFO: pseudo-changed: [special]
INFO: pseudo-changed: []
INFO: pseudo-changed: [hover]
...
Run Code Online (Sandbox Code Playgroud)

编辑

虽然finalize已被弃用,但它仍可用于检查侦听器是否在第一种情况下被 gc'ed:

specialButton.getPseudoClassStates().addListener(new SetChangeListener<PseudoClass>() {
        @Override
        public void onChanged(Change<? extends PseudoClass> change) {
            LOG.info("pseudo-changed: " + change.getSet());
        }

        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            LOG.info("SetChangeListener finalized by gc");
        }
    });
Run Code Online (Sandbox Code Playgroud)

所以在上面的测试中,你会得到:

...
INFO: pseudo-changed: [hover, focused]
INFO: pseudo-changed: [focused]
INFO: pseudo-changed: [hover, focused]
INFO: SetChangeListener finalized by gc
Run Code Online (Sandbox Code Playgroud)

之后不再有通知,如前所述。

但是,如果您持有强引用,即使您显式调用System.gc(),侦听器也不会被 gc'ed。

这也是值得一试的FXCollection::UnmodifiableObservableSet实施中,在一个WeakSetChangeListener使用,同时保持对监听器本身的引用:

private SetChangeListener<E> listener;
private void initListener() {
        if (listener == null) {
            listener = c -> {
                callObservers(new SetAdapterChange<E>(UnmodifiableObservableSet.this, c));
            };
            this.backingSet.addListener(new WeakSetChangeListener<E>(listener));
        }
    }
Run Code Online (Sandbox Code Playgroud)

这意味着引用private SetChangeListener<E> listener;并不是真正需要的。