如何设置 TableColumn 的排序箭头对齐方式?

How does one set the TableColumn's sort arrow alignment?

要点

在 JavaFX TableColumn 中,右侧有一个排序箭头。

如何设置此箭头的对齐方式?

我的用例

我问是因为我正在尝试将 Material Design 应用于 JavaFX,并且箭头需要在左侧 — 否则箭头似乎属于相邻的列。

知道的

我知道你可以像这样到达 TableColumnHeader:

for (final Node headerNode : tableView.lookupAll(".column-header")) {
    TableColumnHeader tableColumnHeader = (TableColumnHeader) headerNode;

我知道 TableColumnHeader 有一个 Label label 和一个 GridPane sortArrowGrid 作为它的 children。

如何将 sortArrowGrid 移到 children 的前面? .toFront() 只适用于 z-order 对吗?

    Node arrow = tableColumnHeader.lookup(".arrow");
    if (arrow != null) {
        GridPane sortArrowGrid = (GridPane) arrow.getParent();
        // TODO: Move the sortArrowGrid to the front of the tableColumnHeader's children
    }

我觉得我 可能 做错了——我很乐意 CSS。

对我的评论进行一些扩展(使用一些代码):如前所述,排序指示器的对齐方式(或在 fx-speak 中:内容显示)是不可配置的,既不是风格也不是任何 [= column/header 的 57=] - 相反,它是页眉布局代码中的 hard-coded。

意味着我们需要实现一个支持可配置显示的自定义columnHeader。肉在自定义 TableColumnHeader 中,它具有:

  • a 属性 sortIconDisplayProperty() 配置排序指标的相对位置
  • 重写 layoutChildren() 按配置放置标签和排序指示器
  • 为了好玩:使 属性 具有样式(需要一些围绕 StyleableProperty 的样板及其在 CSS 处理程序中的注册)

要使用,我们需要自定义 TableViewSkin、TableHeaderRow、NestedTableColumnHeader 的整个堆栈:所有这些都只是 boiler-plate 以在其相关工厂方法中创建和 return 自定义 xx 实例。

下面是一个粗略的示例(阅读:布局不完美,应该有一些填充并保证不与文本重叠......但是,核心不是那么擅长,也不是)支持设置文本左侧的图标。为了获得完整的支持,您可能需要在 top/bottom 上实现设置 .. 我现在太懒了 ;)

/**
 * 
 * position sort indicator at leading edge of column header
 * 
 * @author Jeanette Winzenburg, Berlin
 */
public class TableHeaderLeadingSortArrow extends Application {

    /**
     * Custom TableColumnHeader that lays out the sort icon at its leading edge.
     */
    public static class MyTableColumnHeader extends TableColumnHeader {

        public MyTableColumnHeader(TableColumnBase column) {
            super(column);
        }

        @Override
        protected void layoutChildren() {
            // call super to ensure that all children are created and installed
            super.layoutChildren();
            Node sortArrow = getSortArrow();
            // no sort indicator, nothing to do
            if (sortArrow == null || !sortArrow.isVisible()) return;
            if (getSortIconDisplay() == ContentDisplay.RIGHT) return;
            // re-arrange label and sort indicator
            double sortWidth = sortArrow.prefWidth(-1);
            double headerWidth = snapSizeX(getWidth()) - (snappedLeftInset() + snappedRightInset());
            double headerHeight = getHeight() - (snappedTopInset() + snappedBottomInset());

            // position sort indicator at leading edge
            sortArrow.resize(sortWidth, sortArrow.prefHeight(-1));
            positionInArea(sortArrow, snappedLeftInset(), snappedTopInset(),
                    sortWidth, headerHeight, 0, HPos.CENTER, VPos.CENTER);
            // resize label to fill remaining space
            getLabel().resizeRelocate(sortWidth, 0, headerWidth - sortWidth, getHeight());
        }

        // --------------- make sort icon location styleable
        // use StyleablePropertyFactory to simplify styling-related code
        private static final StyleablePropertyFactory<MyTableColumnHeader> FACTORY = 
                new StyleablePropertyFactory<>(TableColumnHeader.getClassCssMetaData());

        // default value (strictly speaking: an implementation detail)
        // PENDING: what about RtoL orientation? Is it handled correctly in
        // core?
        private static final ContentDisplay DEFAULT_SORT_ICON_DISPLAY = ContentDisplay.RIGHT;

        private static CssMetaData<MyTableColumnHeader, ContentDisplay> CSS_SORT_ICON_DISPLAY = 
                FACTORY.createEnumCssMetaData(ContentDisplay.class,
                        "-fx-sort-icon-display",
                        header -> header.sortIconDisplayProperty(),
                        DEFAULT_SORT_ICON_DISPLAY);

        // property with lazy instantiation
        private StyleableObjectProperty<ContentDisplay> sortIconDisplay;

        protected StyleableObjectProperty<ContentDisplay> sortIconDisplayProperty() {
            if (sortIconDisplay == null) {
                sortIconDisplay = new SimpleStyleableObjectProperty<>(
                        CSS_SORT_ICON_DISPLAY, this, "sortIconDisplay",
                        DEFAULT_SORT_ICON_DISPLAY);

            }
            return sortIconDisplay;
        }

        protected ContentDisplay getSortIconDisplay() {
            return sortIconDisplay != null ? sortIconDisplay.get()
                    : DEFAULT_SORT_ICON_DISPLAY;
        }

        protected void setSortIconDisplay(ContentDisplay display) {
            sortIconDisplayProperty().set(display);
        }

        /**
         * Returnst the CssMetaData associated with this class, which may
         * include the CssMetaData of its superclasses.
         * 
         * @return the CssMetaData associated with this class, which may include
         *         the CssMetaData of its superclasses
         */
        public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
            return FACTORY.getCssMetaData();
        }

        /** {@inheritDoc} */
        @Override
        public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
            return getClassCssMetaData();
        }

//-------- reflection acrobatics .. might use lookup and/or keeping aliases around
        private Node getSortArrow() {
            return (Node) FXUtils.invokeGetFieldValue(TableColumnHeader.class, this, "sortArrow");
        }

        private Label getLabel() {
            return (Label) FXUtils.invokeGetFieldValue(TableColumnHeader.class, this, "label");
        }

    }

    private Parent createContent() {
        // instantiate the tableView with the custom default skin
        TableView<Locale> table = new TableView<>(FXCollections.observableArrayList(
                Locale.getAvailableLocales())) {

                    @Override
                    protected Skin<?> createDefaultSkin() {
                        return new MyTableViewSkin<>(this);
                    }

        };
        TableColumn<Locale, String> countryCode = new TableColumn<>("CountryCode");
        countryCode.setCellValueFactory(new PropertyValueFactory<>("country"));
        TableColumn<Locale, String> language = new TableColumn<>("Language");
        language.setCellValueFactory(new PropertyValueFactory<>("language"));
        TableColumn<Locale, String> variant = new TableColumn<>("Variant");
        variant.setCellValueFactory(new PropertyValueFactory<>("variant"));
        table.getColumns().addAll(countryCode, language, variant);

        BorderPane pane = new BorderPane(table);

        return pane;
    }

    /**
     * Custom nested columnHeader, headerRow und skin only needed to 
     * inject the custom columnHeader in their factory methods.
     */
    public static class MyNestedTableColumnHeader extends NestedTableColumnHeader {

        public MyNestedTableColumnHeader(TableColumnBase column) {
            super(column);
        }

        @Override
        protected TableColumnHeader createTableColumnHeader(
                TableColumnBase col) {
            return col == null || col.getColumns().isEmpty() || col == getTableColumn() ?
                    new MyTableColumnHeader(col) :
                    new MyNestedTableColumnHeader(col);
        }
    }

    public static class MyTableHeaderRow extends TableHeaderRow {

        public MyTableHeaderRow(TableViewSkinBase tableSkin) {
            super(tableSkin);
        }

        @Override
        protected NestedTableColumnHeader createRootHeader() {
            return new MyNestedTableColumnHeader(null);
        }
    }

    public static class MyTableViewSkin<T> extends TableViewSkin<T> {

        public MyTableViewSkin(TableView<T> table) {
            super(table);
        }

        @Override
        protected TableHeaderRow createTableHeaderRow() {
            return new MyTableHeaderRow(this);
        }

    }

    @Override
    public void start(Stage stage) throws Exception {
        stage.setScene(new Scene(createContent()));
        URL uri = getClass().getResource("columnheader.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(TableHeaderLeadingSortArrow.class.getName());

}

columnheader.css配置:

.column-header {
    -fx-sort-icon-display: LEFT;
}

版本说明

该示例是针对 fx9 编码的 - 它将皮肤移入 public 范围以及一系列其他更改。使其与 fx8

一起使用
  • 将导入语句调整到 com.sun 中的旧位置**(无论如何都不会显示,您的 IDE 是您的朋友;)
  • 对于所有 SomethingHeader,更改构造函数以包含 tableSkin 作为参数并在所有工厂方法中传递皮肤(可能在 fx8 中,因为 getTableViewSkin() - 或类似的 - 具有受保护的范围,因此子类可以访问)