К сожалению, нет общедоступного 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.
Этот способ работы не так "прост", как скин, и требует двух вещей:
-
TitledPane
должен использовать значение по умолчанию TitledPaneSkin
.
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.