JavaFX: как переместить «стрелку вниз» в TitledPane, чтобы она была справа - PullRequest
1 голос
/ 10 марта 2019

Надеюсь, у всех все хорошо.

Я пытаюсь переместить стрелку раскрывающегося списка на титульной панели, чтобы расположить ее справа, а не слева, как это сделано по умолчанию. Я использую JavaFX 8, и многие из найденных ресурсов не работают.

Я обнаружил, что могу переместить стрелку на определенную величину, например, на 20 пикселей, показанные ниже

.accordion .title > .arrow-button .arrow
{
    -fx-translate-x: 20;
}

Но я хочу что-то отзывчивое. Есть ли какой-нибудь способ, которым я могу получить ширину названной панели, а затем вычесть несколько пикселей так, чтобы при изменении размера стрелка выглядела справа? Есть ли лучший способ сделать это? Я добавил элемент, используя SceneBuilder2, если это имеет значение.

Большое спасибо за ваше время.

Редактировать: следующее было добавлено для уточнения

enter image description here enter image description here

Прежде всего, я хочу, чтобы стрелка была выровнена вправо, как показано ниже

enter image description here enter image description here

Вместо «вправо» от стрелки. Я действительно ценю всю помощь.

Ответы [ 2 ]

2 голосов
/ 10 марта 2019

К сожалению, нет общедоступного API для перемещения стрелки вправо от TitledPane. Это не означает, что это невозможно, однако нам просто нужно динамически перевести стрелку, используя привязки. Чтобы остальная часть заголовка выглядела правильно, нам также нужно будет перевести текст и графику, если они есть, влево. Самый простой способ сделать все это - создать подкласс TitledPaneSkin и получить доступ к внутренним областям «области заголовка».

Вот пример реализации. Это позволяет вам позиционировать стрелку слева или справа с помощью CSS. Он также реагирует на изменение размеров, выравнивание и графические изменения.

package com.example;

import static javafx.css.StyleConverter.getEnumConverter;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.Property;
import javafx.css.CssMetaData;
import javafx.css.SimpleStyleableObjectProperty;
import javafx.css.StyleableObjectProperty;
import javafx.css.StyleableProperty;
import javafx.scene.Node;
import javafx.scene.control.Skin;
import javafx.scene.control.TitledPane;
import javafx.scene.control.skin.TitledPaneSkin;
import javafx.scene.layout.Region;
import javafx.scene.text.Text;

public class CustomTitledPaneSkin extends TitledPaneSkin {

    public enum ArrowSide {
        LEFT, RIGHT
    }

    /* ********************************************************
     *                                                        *
     * Properties                                             *
     *                                                        *
     **********************************************************/

    private final StyleableObjectProperty<ArrowSide> arrowSide
            = new SimpleStyleableObjectProperty<>(StyleableProperties.ARROW_SIDE, this, "arrowSide", ArrowSide.LEFT) {
        @Override protected void invalidated() {
            adjustTitleLayout();
        }
    };
    public final void setArrowSide(ArrowSide arrowSide) { this.arrowSide.set(arrowSide); }
    public final ArrowSide getArrowSide() { return arrowSide.get(); }
    public final ObjectProperty<ArrowSide> arrowSideProperty() { return arrowSide; }

    /* ********************************************************
     *                                                        *
     * Instance Fields                                        *
     *                                                        *
     **********************************************************/

    private final Region title;
    private final Region arrow;
    private final Text text;

    private DoubleBinding arrowTranslateBinding;
    private DoubleBinding textGraphicTranslateBinding;
    private Node graphic;

    /* ********************************************************
     *                                                        *
     * Constructors                                           *
     *                                                        *
     **********************************************************/

    public CustomTitledPaneSkin(TitledPane control) {
        super(control);
        title = (Region) Objects.requireNonNull(control.lookup(".title"));
        arrow = (Region) Objects.requireNonNull(title.lookup(".arrow-button"));
        text = (Text) Objects.requireNonNull(title.lookup(".text"));

        registerChangeListener(control.graphicProperty(), ov -> adjustTitleLayout());
    }

    /* ********************************************************
     *                                                        *
     * Skin Stuff                                             *
     *                                                        *
     **********************************************************/

    private void adjustTitleLayout() {
        clearBindings();
        if (getArrowSide() != ArrowSide.RIGHT) {
            // if arrow is on the left we don't need to translate anything
            return;
        }

        arrowTranslateBinding = Bindings.createDoubleBinding(() -> {
            double rightInset = title.getPadding().getRight();
            return title.getWidth() - arrow.getLayoutX() - arrow.getWidth() - rightInset;
        }, title.paddingProperty(), title.widthProperty(), arrow.widthProperty(), arrow.layoutXProperty());
        arrow.translateXProperty().bind(arrowTranslateBinding);

        textGraphicTranslateBinding = Bindings.createDoubleBinding(() -> {
            switch (getSkinnable().getAlignment()) {
                case TOP_CENTER:
                case CENTER:
                case BOTTOM_CENTER:
                case BASELINE_CENTER:
                    return 0.0;
                default:
                    return -(arrow.getWidth());
            }
        }, getSkinnable().alignmentProperty(), arrow.widthProperty());
        text.translateXProperty().bind(textGraphicTranslateBinding);

        graphic = getSkinnable().getGraphic();
        if (graphic != null) {
            graphic.translateXProperty().bind(textGraphicTranslateBinding);
        }
    }

    private void clearBindings() {
        if (arrowTranslateBinding != null) {
            arrow.translateXProperty().unbind();
            arrow.setTranslateX(0);
            arrowTranslateBinding.dispose();
            arrowTranslateBinding = null;
        }
        if (textGraphicTranslateBinding != null) {
            text.translateXProperty().unbind();
            text.setTranslateX(0);
            if (graphic != null) {
                graphic.translateXProperty().unbind();
                graphic.setTranslateX(0);
                graphic = null;
            }
            textGraphicTranslateBinding.dispose();
            textGraphicTranslateBinding = null;
        }
    }

    @Override
    public void dispose() {
        clearBindings();
        unregisterChangeListeners(getSkinnable().graphicProperty());
        super.dispose();
    }

    /* ********************************************************
     *                                                        *
     * Stylesheet Handling                                    *
     *                                                        *
     **********************************************************/

    public static List<CssMetaData<?, ?>> getClassCssMetaData() {
        return StyleableProperties.CSS_META_DATA;
    }

    @Override
    public List<CssMetaData<?, ?>> getCssMetaData() {
        return getClassCssMetaData();
    }

    private static class StyleableProperties {

        private static final CssMetaData<TitledPane, ArrowSide> ARROW_SIDE
                = new CssMetaData<>("-fx-arrow-side", getEnumConverter(ArrowSide.class), ArrowSide.LEFT) {

            @Override
            public boolean isSettable(TitledPane styleable) {
                Property<?> prop = (Property<?>) getStyleableProperty(styleable);
                return prop != null && !prop.isBound();
            }

            @Override
            public StyleableProperty<ArrowSide> getStyleableProperty(TitledPane styleable) {
                Skin<?> skin = styleable.getSkin();
                if (skin instanceof CustomTitledPaneSkin) {
                    return ((CustomTitledPaneSkin) skin).arrowSide;
                }
                return null;
            }

        };

        private static final List<CssMetaData<?, ?>> CSS_META_DATA;

        static {
            List<CssMetaData<?,?>> list = new ArrayList<>(TitledPane.getClassCssMetaData().size() + 1);
            list.addAll(TitledPaneSkin.getClassCssMetaData());
            list.add(ARROW_SIDE);
            CSS_META_DATA = Collections.unmodifiableList(list);
        }

    }

}

Затем вы можете применить этот скин ко всем TitledPane в вашем приложении из CSS, например:

.titled-pane {
    -fx-skin: "com.example.CustomTitledPaneSkin";
    -fx-arrow-side: right;
}

/*
 * The arrow button has some right padding that's added
 * by "modena.css". This simply puts the padding on the
 * left since the arrow is positioned on the right.
 */
.titled-pane > .title > .arrow-button {
    -fx-padding: 0.0em 0.0em 0.0em 0.583em;
}

Или вы можете нацелиться только на определенные TitledPane s, добавив класс стиля и используя указанный класс вместо .titled-pane.

Вышеуказанное работает с JavaFX 11 и, вероятно, с JavaFX 10 и 9. Чтобы заставить его скомпилироваться на JavaFX 8, вам нужно изменить некоторые вещи:

  • Импорт com.sun.javafx.scene.control.skin.TitledPaneSkin вместо.

    • Классы скинов были опубликованы в JavaFX 9.
  • Удалить звонки на registerChangeListener(...) и unregisterChangeListeners(...). Я считаю, что заменить их на следующее - это правильно:

    @Override
    protected void handleControlPropertyChange(String p) {
        super.handleControlPropertyChange(p);
        if ("GRAPHIC".equals(p)) {
            adjustTitleLayout();
        }
    }
    
  • Используйте new SimpleStyleableObjectProperty<ArrowSide>(...) {...} и new CssMetaData<TitledPane, ArrowSide>(...) {...}.

    • Вывод типа был улучшен в более поздних версиях Java.
  • Использование (StyleConverter<?, ArrowSide>) getEnumConverter(ArrowSide.class).

    • В общей подписи getEnumConverter была ошибка, исправленная в более поздней версии. Использование броска работает вокруг проблемы. Вы можете пожелать @SuppressWarnings("unchecked") актерский состав.

Проблема: Даже с учетом вышеуказанных изменений в JavaFX 8 возникает проблема - стрелка переводится только после фокусировки TitledPane. Похоже, это не проблема с приведенным выше кодом, поскольку даже изменение свойства alignment не приводит к обновлению TitledPane до тех пор, пока оно не будет сфокусировано (даже если не используется вышеуказанный скин, а скорее только скин по умолчанию) , Мне не удалось найти решение этой проблемы (при использовании пользовательского скина), но, возможно, вы или кто-то еще можете. Я использовал Java 1.8.0_202 при тестировании на JavaFX 8.


Если вы не хотите использовать пользовательский скин или используете JavaFX 8 (это приведет к переводу стрелки без необходимости сначала фокусировать TitledPane), вы можете извлечь необходимый код с помощью некоторые модификации, в служебный метод:

public static void putArrowOnRight(TitledPane pane) {
    Region title = (Region) pane.lookup(".title");
    Region arrow = (Region) title.lookup(".arrow-button");
    Text text = (Text) title.lookup(".text");

    arrow.translateXProperty().bind(Bindings.createDoubleBinding(() -> {
        double rightInset = title.getPadding().getRight();
        return title.getWidth() - arrow.getLayoutX() - arrow.getWidth() - rightInset;
    }, title.paddingProperty(), title.widthProperty(), arrow.widthProperty(), arrow.layoutXProperty()));
    arrow.setStyle("-fx-padding: 0.0em 0.0em 0.0em 0.583em;");

    DoubleBinding textGraphicBinding = Bindings.createDoubleBinding(() -> {
        switch (pane.getAlignment()) {
            case TOP_CENTER:
            case CENTER:
            case BOTTOM_CENTER:
            case BASELINE_CENTER:
                return 0.0;
            default:
                return -(arrow.getWidth());
        }
    }, arrow.widthProperty(), pane.alignmentProperty());
    text.translateXProperty().bind(textGraphicBinding);

    pane.graphicProperty().addListener((observable, oldGraphic, newGraphic) -> {
        if (oldGraphic != null) {
            oldGraphic.translateXProperty().unbind();
            oldGraphic.setTranslateX(0);
        }
        if (newGraphic != null) {
            newGraphic.translateXProperty().bind(textGraphicBinding);
        }
    });
    if (pane.getGraphic() != null) {
        pane.getGraphic().translateXProperty().bind(textGraphicBinding);
    }
}

Примечание. Хотя при этом стрелка направляется вправо, без необходимости сначала фокусировать TitledPane, TitledPane все еще страдает от проблемы, отмеченной выше. Например, изменение свойства alignment не обновляет TitledPane, пока оно не будет сфокусировано. Я предполагаю, что это просто ошибка в JavaFX 8.

Этот способ работы не так "прост", как скин, и требует двух вещей:

  1. TitledPane должен использовать значение по умолчанию TitledPaneSkin.
  2. TitledPane должен отображаться в Window (окно было с ) до вызова метода утилиты.

    • Из-за ленивой природы элементов управления JavaFX оболочка и связанные с ней узлы не будут созданы, пока элемент управления не будет отображен в окне. Вызов служебного метода до отображения элемента управления приведет к выбрасыванию NullPointerException, поскольку вызовы lookup вернут null.
    • При использовании FXML обратите внимание, что метод initialize вызывается во время вызова FXMLLoader.load (любая из перегрузок). Это означает, что в нормальных условиях невозможно, чтобы созданные узлы были частью Scene, не говоря уже о показе Window. Вы должны подождать, пока сначала отобразится TitledPane, затем , а затем вызвать служебный метод.

      Ожидание отображения TitledPane может быть достигнуто путем прослушивания свойства Node.scene, свойства Scene.window и свойства Window.showing (или вы можете прослушивать события WindowEvent.WINDOW_SHOWN).Однако, , если вы немедленно поместите загруженные узлы в показ Window, тогда вы можете отказаться от наблюдения за свойствами;вызов служебного метода внутри Platform.runLater вызова изнутри initialize.

При использовании подхода скина вся проблема ожидания окна с отображением


Обычное предупреждение: Этот ответ опирается на внутреннюю структуру TitledPane, которая может измениться в будущем выпуске.Будьте осторожны при изменении версий JavaFX.Я только (несколько) протестировал это на JavaFX 8u202 и JavaFX 11.0.2.

2 голосов
/ 10 марта 2019

Это не совсем то же самое, визуально, но вы можете скрыть кнопку со стрелкой и создать рисунок, который действует как кнопка со стрелкой.TitledPane расширяется надписью, поэтому вы можете управлять размещением графики относительно текста через свойство contentDisplay .

Сначала спрячьте кнопку со стрелкой в ​​таблице стилей:

.accordion .title > .arrow-button
{
    visibility: hidden;
}

В коде вы можете создать метку, которая будет действовать в качестве фальшивой кнопки, и установить ее как графику TitledPane.Вся строка заголовка чувствительна к мыши, поэтому интерактивный элемент управления (например, кнопка) не требуется.

Label collapseButton = new Label();
collapseButton.textProperty().bind(
    Bindings.when(titledPane.expandedProperty())
        .then("\u25bc").otherwise("\u25b6"));

titledPane.setGraphic(collapseButton);
titledPane.setContentDisplay(ContentDisplay.RIGHT);
...