问题
I hope everyone is doing well.
I'm trying to move the drop down arrow in a TitledPane to be laid out on the right, instead of the left like it is by default. I'm using JavaFX 8, and many of the resources I've found don't seem to work.
I have found that I am able to move the arrow a specific amount, like 20 pixels shown below
.accordion .title > .arrow-button .arrow
{
-fx-translate-x: 20;
}
But I want something responsive. Is there some way that I can get the width of the titled pane, and then subtract some pixels so that so that the arrow appears to be laid out on the right when resizing? Is there a better way to it? I added the element using SceneBuilder2 if that matters.
Thanks so much for your time.
Edit: The following was added for clarification
Primarily, I want the arrow to be right justified, like below
Instead of just "to the right" of the arrow. I really appreciate all the assistance.
回答1:
Unfortunately, there's no public API for moving the arrow to the right side of the TitledPane
. This doesn't mean this can't be accomplished, however, we just have to translate the arrow dynamically, using bindings. In order for the rest of the title area to look correct we'll also have to translate the text, and graphic if present, to the left. The easiest way to do all this is by subclassing TitledPaneSkin
and accessing the internals of the "title region".
Here's an example implementation. It lets you position the arrow on the left or right side via CSS. It's also responsive to resizing as well as alignment and graphic changes.
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);
}
}
}
You can then apply this skin to all TitledPane
s in your application from CSS, like so:
.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;
}
Or you could target only certain TitledPane
s by adding a style class and using said class instead of .titled-pane
.
The above works with JavaFX 11 and likely JavaFX 10 and 9 as well. To get it to compile on JavaFX 8 you need to change some things:
Import
com.sun.javafx.scene.control.skin.TitledPaneSkin
instead.- The skin classes were made public in JavaFX 9.
Remove the calls to
registerChangeListener(...)
andunregisterChangeListeners(...)
. I believe replacing them with the following is correct:@Override protected void handleControlPropertyChange(String p) { super.handleControlPropertyChange(p); if ("GRAPHIC".equals(p)) { adjustTitleLayout(); } }
Use
new SimpleStyleableObjectProperty<ArrowSide>(...) {...}
andnew CssMetaData<TitledPane, ArrowSide>(...) {...}
.- Type inference was improved in later versions of Java.
Use
(StyleConverter<?, ArrowSide>) getEnumConverter(ArrowSide.class)
.- There was a bug in the generic signature of
getEnumConverter
that was fixed in a later version. Using the cast works around the problem. You may wish to@SuppressWarnings("unchecked")
the cast.
- There was a bug in the generic signature of
Issue: Even with the above changes there's a problem in JavaFX 8—the arrow is only translated once the TitledPane
is focused. This doesn't appear to be a problem with the above code as even changing the alignment
property does not cause the TitledPane
to update until it has focus (even when not using the above skin, but rather just the default skin). I've been unable to find a workaround to this problem (while using the custom skin) but maybe you or someone else can. I was using Java 1.8.0_202 when testing for JavaFX 8.
If you don't want to use a custom skin, or you're on JavaFX 8 (this will cause the arrow to be translated without needing to focus the TitledPane
first), you can extract the necessary code, with some modifications, into a utility method:
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);
}
}
Note: While this puts the arrow on the right without having to focus the TitledPane
first, the TitledPane
still suffers from the issue noted above. For instance, changing the alignment
property doesn't update the TitledPane
until it's focused. I'm guessing this is simply a bug in JavaFX 8.
This way of doing things is not as "easy" as the skin approach and requires two things:
- The
TitledPane
must be using the defaultTitledPaneSkin
. The
TitledPane
must have been displayed in aWindow
(window was showing) before calling the utility method.- Due to the lazy nature of JavaFX controls, the skin and the associated nodes will not have been created until the control has been displayed in a window. Calling the utility method before the control was displayed will result in a
NullPointerException
being thrown since thelookup
calls will returnnull
. If using FXML, note that the
initialize
method is called during a call toFXMLLoader.load
(any of the overloads). This means, under normal circumstances, it's not possible for the created nodes to be part of aScene
yet, let alone a showingWindow
. You must wait for theTitledPane
to be displayed first, then call the utility method.Waiting for the
TitledPane
to be displayed can be achieved by listening to theNode.scene
property, theScene.window
property, and theWindow.showing
property (or you could listen forWindowEvent.WINDOW_SHOWN
events). However, if you immediately put the loaded nodes into a showingWindow
, then you can forgo observing the properties; call the utility method inside aPlatform.runLater
call from insideinitialize
.
- Due to the lazy nature of JavaFX controls, the skin and the associated nodes will not have been created until the control has been displayed in a window. Calling the utility method before the control was displayed will result in a
When using the skin approach, the whole wait-for-showing-window hassle is avoided.
Usual Warning: This answer relies on the internal structure of TitledPane
which may change in a future release. Be cautious when changing JavaFX versions. I only (somewhat) tested this on JavaFX 8u202 and JavaFX 11.0.2.
回答2:
This isn’t exactly the same, visually, but you can hide the arrow button and create a graphic that acts like an arrow button. TitledPane extends Labeled, so you have control over the placement of the graphic relative to the text, via the contentDisplay property.
First, hide the arrow button in the stylesheet:
.accordion .title > .arrow-button
{
visibility: hidden;
}
In the code, you can create a Label to act as a fake button and set it as the TitledPane’s graphic. The entire title line is sensitive to the mouse, so an interactive control (like a Button) is not needed.
Label collapseButton = new Label();
collapseButton.textProperty().bind(
Bindings.when(titledPane.expandedProperty())
.then("\u25bc").otherwise("\u25b6"));
titledPane.setGraphic(collapseButton);
titledPane.setContentDisplay(ContentDisplay.RIGHT);
来源:https://stackoverflow.com/questions/55082933/javafx-how-to-move-drop-down-arrow-in-titledpane-to-be-on-right