我试着在SceneBuilder中使用自定义控件。 我构建的是一个名为ContentSection的控件。为了解决创建自定义控件的问题,我试图复制标题窗格的代码,并对皮肤进行一些更改以符合我对ContentSection的未来要求。
我的控件在我的应用程序中工作正常,但我无法在SceneBuilder中加载它。
这是我的控件代码:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon;
import javafx.beans.DefaultProperty;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.BooleanPropertyBase;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.property.StringPropertyBase;
import javafx.css.CssMetaData;
import javafx.css.PseudoClass;
import javafx.css.StyleConverter;
import javafx.css.Styleable;
import javafx.css.StyleableBooleanProperty;
import javafx.css.StyleableObjectProperty;
import javafx.css.StyleableProperty;
import javafx.geometry.Orientation;
import javafx.scene.AccessibleAction;
import javafx.scene.AccessibleAttribute;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.util.Duration;
@DefaultProperty("content")
public class ContentSection extends Control {
public ContentSection() {
getStyleClass().setAll(DEFAULT_STYLE_CLASS);
// initialize pseudo-class state
pseudoClassStateChanged(PSEUDO_CLASS_EXPANDED, true);
pseudoClassStateChanged(PSEUDO_CLASS_COLLAPSED, false);
}
public ContentSection(final String title, final Node content) {
this();
setTitle(title);
setContent(content);
}
private final StringProperty titleProperty = new StringPropertyBase() {
@Override
protected void invalidated() {
notifyAccessibleAttributeChanged(AccessibleAttribute.TEXT);
}
@Override
public Object getBean() {
return ContentSection.this;
}
@Override
public String getName() {
return "title";
};
};
public final void setTitle(final String value) {
titleProperty().set(value);
}
public final String getTitle() {
return titleProperty == null ? null : titleProperty.get();
}
public final StringProperty titleProperty() {
return titleProperty;
}
private DoubleProperty iconSizeProperty;
public final void setIconSize(final double value) {
iconSizeProperty().set(value);
}
public final double getIconSize() {
return iconSizeProperty == null ? -1 : iconSizeProperty.get();
}
public final DoubleProperty iconSizeProperty() {
if (iconSizeProperty == null) {
iconSizeProperty = new SimpleDoubleProperty(this, "iconSize", -1);
}
return iconSizeProperty;
}
private ObjectProperty<FontAwesomeIcon> iconProperty;
public final void setIcon(final FontAwesomeIcon value) {
iconProperty().set(value);
}
public final FontAwesomeIcon getIcon() {
return iconProperty == null ? null : iconProperty.get();
}
@Override
public String getUserAgentStylesheet() {
return ContentSection.class.getClassLoader().getResource("content-section.css").toExternalForm();
}
public final ObjectProperty<FontAwesomeIcon> iconProperty() {
if (iconProperty == null) {
iconProperty = new SimpleObjectProperty<>(this, "icon");
}
return iconProperty;
}
private ObjectProperty<Node> contentProperty;
public final void setContent(final Node value) {
contentProperty().set(value);
}
public final Node getContent() {
return contentProperty == null ? null : contentProperty.get();
}
public final ObjectProperty<Node> contentProperty() {
if (contentProperty == null) {
contentProperty = new SimpleObjectProperty<Node>(this, "content");
}
return contentProperty;
}
private final BooleanProperty expandedProperty = new BooleanPropertyBase(true) {
@Override
protected void invalidated() {
final boolean active = get();
pseudoClassStateChanged(PSEUDO_CLASS_EXPANDED, active);
pseudoClassStateChanged(PSEUDO_CLASS_COLLAPSED, !active);
notifyAccessibleAttributeChanged(AccessibleAttribute.EXPANDED);
}
@Override
public Object getBean() {
return ContentSection.this;
}
@Override
public String getName() {
return "expanded";
}
};
public final void setExpanded(final boolean value) {
expandedProperty().set(value);
}
public final boolean isExpanded() {
return expandedProperty.get();
}
public final BooleanProperty expandedProperty() {
return expandedProperty;
}
private final BooleanProperty animatedProperty = new StyleableBooleanProperty(true) {
@Override
public Object getBean() {
return ContentSection.this;
}
@Override
public String getName() {
return "animated";
}
@Override
public CssMetaData<ContentSection, Boolean> getCssMetaData() {
return StyleableProperties.ANIMATED;
}
};
public final void setAnimated(final boolean value) {
animatedProperty().set(value);
}
public final boolean isAnimated() {
return animatedProperty.get();
}
public final BooleanProperty animatedProperty() {
return animatedProperty;
}
private final ObjectProperty<Duration> animationDurationProperty = new SimpleObjectProperty<>(this, "animationDuration", Duration.millis(350.0));
public final void setAnimationDuration(final Duration value) {
animationDurationProperty().set(value);
}
public final Duration getAnimationDuration() {
return animationDurationProperty().get();
}
public final ObjectProperty<Duration> animationDurationProperty() {
return animationDurationProperty;
}
private final BooleanProperty collapsibleProperty = new StyleableBooleanProperty(true) {
@Override
public Object getBean() {
return ContentSection.this;
}
@Override
public String getName() {
return "collapsible";
}
@Override
public CssMetaData<ContentSection, Boolean> getCssMetaData() {
return StyleableProperties.COLLAPSIBLE;
}
};
public final void setCollapsible(final boolean value) {
collapsibleProperty().set(value);
}
public final boolean isCollapsible() {
return collapsibleProperty.get();
}
public final BooleanProperty collapsibleProperty() {
return collapsibleProperty;
}
private final ObjectProperty<Number> headerSizeProperty = new StyleableObjectProperty<Number>(44.0) {
@Override
public String getName() {
return "headerSize";
}
@Override
public Object getBean() {
return ContentSection.this;
}
@Override
public CssMetaData<? extends Styleable, Number> getCssMetaData() {
return StyleableProperties.HEADER_SIZE;
}
};
public final void setHeaderSize(final Number value) {
headerSizeProperty().set(value);
}
public final Number getHeaderSize() {
return headerSizeProperty().get();
}
public final ObjectProperty<Number> headerSizeProperty() {
return headerSizeProperty;
}
/** {@inheritDoc} */
@Override
protected Skin<?> createDefaultSkin() {
return new ContentSectionSkin(this);
}
private static final String DEFAULT_STYLE_CLASS = "content-section";
private static final PseudoClass PSEUDO_CLASS_EXPANDED = PseudoClass.getPseudoClass("expanded");
private static final PseudoClass PSEUDO_CLASS_COLLAPSED = PseudoClass.getPseudoClass("collapsed");
private static class StyleableProperties {
private static final CssMetaData<ContentSection, Boolean> COLLAPSIBLE =
new CssMetaData<ContentSection, Boolean>("-fx-collapsible", StyleConverter.getBooleanConverter(), Boolean.TRUE) {
@Override
public boolean isSettable(final ContentSection n) {
return n.collapsibleProperty == null || !n.collapsibleProperty.isBound();
}
@Override
public StyleableProperty<Boolean> getStyleableProperty(final ContentSection n) {
return (StyleableProperty<Boolean>) n.collapsibleProperty();
}
};
private static final CssMetaData<ContentSection, Number> HEADER_SIZE =
new CssMetaData<ContentSection, Number>("-fx-header-size", StyleConverter.getSizeConverter(), 44.0) {
@Override
public boolean isSettable(final ContentSection n) {
return n.collapsibleProperty == null || !n.collapsibleProperty.isBound();
}
@Override
public StyleableProperty<Number> getStyleableProperty(final ContentSection n) {
return (StyleableProperty<Number>) n.headerSizeProperty();
}
};
private static final CssMetaData<ContentSection, Boolean> ANIMATED =
new CssMetaData<ContentSection, Boolean>("-fx-animated", StyleConverter.getBooleanConverter(), Boolean.TRUE) {
@Override
public boolean isSettable(final ContentSection n) {
return n.animatedProperty == null || !n.animatedProperty.isBound();
}
@Override
public StyleableProperty<Boolean> getStyleableProperty(final ContentSection n) {
return (StyleableProperty<Boolean>) n.animatedProperty();
}
};
private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
static {
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<CssMetaData<? extends Styleable, ?>>(Control.getClassCssMetaData());
styleables.add(COLLAPSIBLE);
styleables.add(ANIMATED);
styleables.add(HEADER_SIZE);
STYLEABLES = Collections.unmodifiableList(styleables);
}
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return StyleableProperties.STYLEABLES;
}
@Override
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
return getClassCssMetaData();
}
@Override
public Orientation getContentBias() {
final Node c = getContent();
return c == null ? super.getContentBias() : c.getContentBias();
}
@Override
public Object queryAccessibleAttribute(final AccessibleAttribute attribute, final Object... parameters) {
switch (attribute) {
case TEXT: {
final String accText = getAccessibleText();
if (accText != null && !accText.isEmpty()) {
return accText;
}
return getTitle();
}
case EXPANDED:
return isExpanded();
default:
return super.queryAccessibleAttribute(attribute, parameters);
}
}
@Override
public void executeAccessibleAction(final AccessibleAction action, final Object... parameters) {
switch (action) {
case EXPAND:
setExpanded(true);
break;
case COLLAPSE:
setExpanded(false);
break;
default:
super.executeAccessibleAction(action);
}
}
}
这是我的皮肤代码:
import com.sun.javafx.scene.control.skin.BehaviorSkinBase;
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon;
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView;
import javafx.animation.Animation.Status;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.collections.ListChangeListener;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.util.Duration;
@SuppressWarnings("restriction")
public class ContentSectionSkin extends BehaviorSkinBase<ContentSection, ContentSectionBehavior> {
private final BorderPane container;
private final HBox header;
private final BorderPane contentContainer;
private Timeline timeline;
private double transitionStartValue;
private DoubleProperty transition;
private FontAwesomeIconView expandCollapseIcon;
private Button expandCollapseButton;
private HBox sectionToolBar;
public ContentSectionSkin(final ContentSection contentSection) {
super(contentSection, new ContentSectionBehavior(contentSection));
transitionStartValue = 0;
container = createContainer();
getChildren().setAll(container);
header = createHeader();
container.setTop(header);
contentContainer = createContentContainer();
container.setCenter(contentContainer);
registerChangeListener(contentSection.contentProperty(), "CONTENT");
registerChangeListener(contentSection.expandedProperty(), "EXPANDED");
registerChangeListener(contentSection.collapsibleProperty(), "COLLAPSIBLE");
sectionToolBar.getChildren().addListener((ListChangeListener<Node>) c -> updateHeader());
if (contentSection.isExpanded()) {
setTransition(1.0f);
setExpanded(contentSection.isExpanded());
} else {
setTransition(0.0f);
if (getSkinnable().getContent() != null) {
getSkinnable().getContent().setVisible(false);
}
}
}
@Override
protected double computeMaxWidth(final double height, final double topInset, final double rightInset, final double bottomInset, final double leftInset) {
return Double.MAX_VALUE;
}
@Override
protected double computeMinHeight(final double width, final double topInset, final double rightInset, final double bottomInset, final double leftInset) {
final double headerHeight = snapSize(header.prefHeight(width));
final double contentHeight = contentContainer.minHeight(width) * getTransition();
final double minHeight = headerHeight + snapSize(contentHeight) + topInset + bottomInset;
return minHeight;
}
@Override
protected double computeMinWidth(final double height, final double topInset, final double rightInset, final double bottomInset, final double leftInset) {
final double headerWidth = snapSize(header.prefWidth(height));
final double contentWidth = snapSize(contentContainer.minWidth(height));
final double minWidth = Math.max(headerWidth, contentWidth) + leftInset + rightInset;
return minWidth;
}
@Override
protected double computePrefHeight(final double width, final double topInset, final double rightInset, final double bottomInset, final double leftInset) {
final double headerHeight = snapSize(header.prefHeight(width));
final double contentHeight = contentContainer.prefHeight(width) * getTransition();
final double prefHeight = headerHeight + snapSize(contentHeight) + topInset + bottomInset;
return prefHeight;
}
@Override
protected double computePrefWidth(final double height, final double topInset, final double rightInset, final double bottomInset, final double leftInset) {
final double headerWidth = snapSize(header.prefWidth(height));
final double contentWidth = snapSize(contentContainer.prefWidth(height));
final double prefWidth = Math.max(headerWidth, contentWidth) + leftInset + rightInset;
return prefWidth;
}
private BorderPane createContainer() {
final BorderPane container = new BorderPane();
container.setMinSize(0.0, 0.0);
return container;
}
private BorderPane createContentContainer() {
final BorderPane contentContainer = new BorderPane();
contentContainer.getStyleClass().setAll("content");
contentContainer.setMinSize(0.0, 0.0);
if (getSkinnable().getContent() != null) {
contentContainer.setCenter(getSkinnable().getContent());
}
return contentContainer;
}
private Button createExpandCollapseButton() {
final Button expandCollapseButton = new Button();
expandCollapseButton.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
expandCollapseButton.setOnAction(event -> {
getSkinnable().setExpanded(!getSkinnable().isExpanded());
});
final StackPane expandCollapseIconContainer = new StackPane();
expandCollapseIconContainer.getStyleClass().setAll("icon-container");
expandCollapseIconContainer.setId("last");
expandCollapseIconContainer.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
expandCollapseIconContainer.setPrefSize(21, 21);
expandCollapseIconContainer.setMinSize(21, 21);
expandCollapseButton.setGraphic(expandCollapseIconContainer);
expandCollapseIcon = new FontAwesomeIconView(FontAwesomeIcon.CHEVRON_DOWN);
expandCollapseIcon.setStyleClass("icon");
expandCollapseIconContainer.getChildren().add(expandCollapseIcon);
return expandCollapseButton;
}
private HBox createHeader() {
final HBox header = new HBox();
header.getStyleClass().setAll("header");
header.setPrefHeight(getSkinnable().getHeaderSize().doubleValue());
header.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
header.getChildren().add(createIconContainer());
header.getChildren().add(createTitleLabel());
header.getChildren().add(createPlaceholder());
header.getChildren().add(createSectionToolBar());
return header;
}
private FontAwesomeIconView createIcon() {
final FontAwesomeIconView iconView = new FontAwesomeIconView();
iconView.setStyleClass("icon");
iconView.setIcon(getSkinnable().getIcon());
iconView.glyphSizeProperty().bind(getSkinnable().iconSizeProperty());
return iconView;
}
private StackPane createIconContainer() {
final StackPane iconContainer = new StackPane();
iconContainer.getStyleClass().setAll("icon-container");
iconContainer.setPrefSize(50, 50);
iconContainer.getChildren().add(createIcon());
return iconContainer;
}
private Pane createPlaceholder() {
final Pane placeholder = new Pane();
HBox.setHgrow(placeholder, Priority.ALWAYS);
return placeholder;
}
private HBox createSectionToolBar() {
sectionToolBar = new HBox();
sectionToolBar.getStyleClass().setAll("section-tool-bar");
expandCollapseButton = createExpandCollapseButton();
if (getSkinnable().isCollapsible()) {
sectionToolBar.getChildren().add(sectionToolBar.getChildren().size(), expandCollapseButton);
}
return sectionToolBar;
}
private Label createTitleLabel() {
final Label titleLabel = new Label();
titleLabel.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
titleLabel.textProperty().bind(getSkinnable().titleProperty());
return titleLabel;
}
private void disableCache() {
getSkinnable().getContent().setCache(false);
}
private void doAnimationTransition() {
if (getSkinnable().getContent() == null) {
return;
}
Duration duration;
if (timeline != null && (timeline.getStatus() != Status.STOPPED)) {
duration = timeline.getCurrentTime();
timeline.stop();
} else {
duration = getSkinnable().getAnimationDuration();
}
timeline = new Timeline();
timeline.setCycleCount(1);
KeyFrame k1, k2;
if (getSkinnable().isExpanded()) {
k1 = new KeyFrame(Duration.ZERO, event -> {
// start expand
getSkinnable().getContent().setVisible(true);
}, new KeyValue(transitionProperty(), transitionStartValue));
k2 = new KeyFrame(duration, event -> {
// end expand
}, new KeyValue(transitionProperty(), 1, Interpolator.LINEAR)
);
} else {
k1 = new KeyFrame(Duration.ZERO, event -> {
// Start collapse
}, new KeyValue(transitionProperty(), transitionStartValue));
k2 = new KeyFrame(duration, event -> {
// end collapse
getSkinnable().getContent().setVisible(false);
}, new KeyValue(transitionProperty(), 0, Interpolator.LINEAR));
}
timeline.getKeyFrames().setAll(k1, k2);
timeline.play();
}
private void enableCache() {
getSkinnable().getContent().setCache(true);
}
public BorderPane getContentContainer() {
return contentContainer;
}
private double getTransition() {
return transition == null ? 0.0 : transition.get();
}
@Override
protected void handleControlPropertyChanged(final String property) {
super.handleControlPropertyChanged(property);
if ("CONTENT".equals(property)) {
final Node content = getSkinnable().getContent();
if (content == null) {
contentContainer.setCenter(null);
} else {
contentContainer.setCenter(content);
}
} else if ("EXPANDED".equals(property)) {
setExpanded(getSkinnable().isExpanded());
} else if ("COLLAPSIBLE".equals(property)) {
updateHeader();
}
}
private void setExpanded(final boolean expanded) {
if (!getSkinnable().isCollapsible()) {
setTransition(1.0f);
return;
}
// we need to perform the transition between expanded / hidden
if (getSkinnable().isAnimated()) {
transitionStartValue = getTransition();
doAnimationTransition();
} else {
if (expanded) {
setTransition(1.0f);
} else {
setTransition(0.0f);
}
if (getSkinnable().getContent() != null) {
getSkinnable().getContent().setVisible(expanded);
}
getSkinnable().requestLayout();
}
}
private void setTransition(final double value) {
transitionProperty().set(value);
}
private DoubleProperty transitionProperty() {
if (transition == null) {
transition = new SimpleDoubleProperty(this, "transition", 0.0) {
@Override
protected void invalidated() {
container.requestLayout();
updateExpandCollapseIconRotation();
}
};
}
return transition;
}
private void updateExpandCollapseIconRotation() {
expandCollapseIcon.setRotate(180 * getTransition());
}
private void updateHeader() {
if (getSkinnable().isCollapsible() && !sectionToolBar.getChildren().contains(expandCollapseButton)) {
sectionToolBar.getChildren().add(sectionToolBar.getChildren().size(), expandCollapseButton);
} else if (sectionToolBar.getChildren().contains(expandCollapseButton)) {
sectionToolBar.getChildren().remove(expandCollapseButton);
}
}
}
最后但并非最不重要的行为:
import static javafx.scene.input.KeyCode.SPACE;
import java.util.ArrayList;
import java.util.List;
import com.sun.javafx.scene.control.behavior.BehaviorBase;
import com.sun.javafx.scene.control.behavior.KeyBinding;
import javafx.scene.input.MouseEvent;
@SuppressWarnings("restriction")
public class ContentSectionBehavior extends BehaviorBase<ContentSection> {
private final ContentSection contentSection;
public ContentSectionBehavior(final ContentSection contentSection) {
super(contentSection, CONTENT_SECTION_BINDINGS);
this.contentSection = contentSection;
}
/***************************************************************************
* *
* Key event handling *
* *
**************************************************************************/
private static final String PRESS_ACTION = "Press";
protected static final List<KeyBinding> CONTENT_SECTION_BINDINGS = new ArrayList<KeyBinding>();
static {
CONTENT_SECTION_BINDINGS.add(new KeyBinding(SPACE, PRESS_ACTION));
}
@Override
protected void callAction(final String name) {
switch (name) {
case PRESS_ACTION:
if (contentSection.isCollapsible() && contentSection.isFocused()) {
contentSection.setExpanded(!contentSection.isExpanded());
contentSection.requestFocus();
}
break;
default:
super.callAction(name);
}
}
/***************************************************************************
* *
* Mouse event handling *
* *
**************************************************************************/
@Override
public void mousePressed(final MouseEvent e) {
super.mousePressed(e);
final ContentSection contentSection = getControl();
contentSection.requestFocus();
}
/**************************************************************************
* State and Functions *
*************************************************************************/
public void expand() {
contentSection.setExpanded(true);
}
public void collapse() {
contentSection.setExpanded(false);
}
public void toggle() {
contentSection.setExpanded(!contentSection.isExpanded());
}
}
为了测试我的实验,我刚创建了一个简单的fxml文件:
<?xml version="1.0" encoding="UTF-8"?>
<?import org.test.ContentSection?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.layout.StackPane?>
<ScrollPane fx:id="root" fitToWidth="true" fitToHeight="true" xmlns="http://javafx.com/javafx/8.0.60" xmlns:fx="http://javafx.com/fxml/1">
<content>
<VBox styleClass="content">
<children>
<ContentSection fx:id="contentSection" collapsible="false" VBox.vgrow="ALWAYS">
<content>
<StackPane>
<children>
<Label text="Test" />
</children>
</StackPane>
</content>
</ContentSection>
</children>
</VBox>
</content>
</ScrollPane>
现在,在使用SceneBuilder打开此FXML文件时,唯一发生的事情是我在SceneBuilder的顶部区域发出警告,说明&#34;布局失败(&#39; null&#39;)& #34;
有人知道接下来该做什么吗? 我没有看到搜索问题的选项......
祝你好运 帕特里克
答案 0 :(得分:0)
我刚刚遇到同样的错误,问题是 css 样式。我不得不删除样式,然后将它们添加到FXML中。