Подтвердить что ты не робот

Программно изменить внешний вид TableView

После выполнения учебника Oracle о TableView мне было интересно, есть ли способ программно применить другой стиль CSS к выбранной строке TableView, Например, пользователь выбирает определенную строку, нажимает кнопку "Выделить", а выбранная строка получает коричневый фон, заполнение белого текста и т.д. Я читал цвета таблиц JavaFX, Обновление отображения строки TableView и Фон с двумя цветами в JavaFX?, но безрезультатно =/

Здесь источник:

import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.stage.Stage;

public class TableViewSample extends Application {

    private TableView<Person> table = new TableView<Person>();
    private final ObservableList<Person> data =
        FXCollections.observableArrayList(
            new Person("Jacob", "Smith", "[email protected]"),
            new Person("Isabella", "Johnson", "[email protected]"),
            new Person("Ethan", "Williams", "[email protected]"),
            new Person("Emma", "Jones", "[email protected]"),
            new Person("Michael", "Brown", "[email protected]")
        );

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage stage) {
        Scene scene = new Scene(new Group());
        stage.setTitle("Table View Sample");
        stage.setWidth(450);
        stage.setHeight(600);

        final Label label = new Label("Address Book");
        label.setFont(new Font("Arial", 20));

        TableColumn firstNameCol = new TableColumn("First Name");
        firstNameCol.setMinWidth(100);
        firstNameCol.setCellValueFactory(
                new PropertyValueFactory<Person, String>("firstName"));

        TableColumn lastNameCol = new TableColumn("Last Name");
        lastNameCol.setMinWidth(100);
        lastNameCol.setCellValueFactory(
                new PropertyValueFactory<Person, String>("lastName"));

        TableColumn emailCol = new TableColumn("Email");
        emailCol.setMinWidth(200);
        emailCol.setCellValueFactory(
                new PropertyValueFactory<Person, String>("email"));

        table.setItems(data);
        table.getColumns().addAll(firstNameCol, lastNameCol, emailCol);

        final Button btnHighlight = new Button("Highlight selected row");
        btnHighlight.setMaxWidth(Double.MAX_VALUE);
        btnHighlight.setPrefHeight(30);

        btnHighlight.setOnAction(new EventHandler<ActionEvent>(){
            public void handle(ActionEvent e){
                // this is where the CSS should be applied
            }
        });

        final VBox vbox = new VBox();
        vbox.setSpacing(5);
        vbox.setPadding(new Insets(10, 0, 0, 10));
        vbox.getChildren().addAll(label, table, btnHighlight);

        ((Group) scene.getRoot()).getChildren().addAll(vbox);

        stage.setScene(scene);
        stage.show();
    }

    public static class Person {

        private final SimpleStringProperty firstName;
        private final SimpleStringProperty lastName;
        private final SimpleStringProperty email;

        private Person(String fName, String lName, String email) {
            this.firstName = new SimpleStringProperty(fName);
            this.lastName = new SimpleStringProperty(lName);
            this.email = new SimpleStringProperty(email);
        }

        public String getFirstName() {
            return firstName.get();
        }

        public void setFirstName(String fName) {
            firstName.set(fName);
        }

        public String getLastName() {
            return lastName.get();
        }

        public void setLastName(String fName) {
            lastName.set(fName);
        }

        public String getEmail() {
            return email.get();
        }

        public void setEmail(String fName) {
            email.set(fName);
        }
    }
} 

И application.css, из которого кнопка "Выделить выбранную строку" применяет класс highlightRow к выбранной строке таблицы:

.highlightedRow {
    -fx-background-color: brown;
    -fx-background-insets: 0, 1, 2;
    -fx-background: -fx-accent;
    -fx-text-fill: -fx-selection-bar-text;
}

Edit:

После нескольких часов попыток, лучшее, что я мог придумать, this, используя следующий код:

firstNameCol.setCellFactory(new Callback<TableColumn<Person, String>, TableCell<Person, String>>() {
    @Override
    public TableCell<Person, String> call(TableColumn<Person, String> personStringTableColumn) {
        return new TableCell<Person, String>() {
            @Override
            protected void updateItem(String name, boolean empty) {
                super.updateItem(name, empty);
                if (!empty) {
                    if (name.toLowerCase().startsWith("e") || name.toLowerCase().startsWith("i")) {
                        getStyleClass().add("highlightedRow");
                    }
                    setText(name);
                } else {
                    setText("empty");  // for debugging purposes
                }
            }
        };
    }
});

Часть, которую я действительно не понимаю, почему я не могу сделать это изнутри метода setOnAction btnHighlight? Я также попытался обновить таблицу после этого (описанный здесь), но, похоже, это не сработало. Кроме того, мое "решение" работает только для столбца firstNameCol, поэтому нужно установить новую ячейку factory для каждого столбца, чтобы применить определенный стиль, или есть более разумное решение?

4b9b3361

Ответ 1

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

Я использовал список целых чисел, поэтому он поддерживает множественный выбор, но если вам это не нужно, очевидно, вы можете просто использовать IntegerProperty.

Вот рядный завод:

    final ObservableList<Integer> highlightRows = FXCollections.observableArrayList();

    table.setRowFactory(new Callback<TableView<Person>, TableRow<Person>>() {
        @Override
        public TableRow<Person> call(TableView<Person> tableView) {
            final TableRow<Person> row = new TableRow<Person>() {
                @Override
                protected void updateItem(Person person, boolean empty){
                    super.updateItem(person, empty);
                    if (highlightRows.contains(getIndex())) {
                        if (! getStyleClass().contains("highlightedRow")) {
                            getStyleClass().add("highlightedRow");
                        }
                    } else {
                        getStyleClass().removeAll(Collections.singleton("highlightedRow"));
                    }
                }
            };
            highlightRows.addListener(new ListChangeListener<Integer>() {
                @Override
                public void onChanged(Change<? extends Integer> change) {
                    if (highlightRows.contains(row.getIndex())) {
                        if (! row.getStyleClass().contains("highlightedRow")) {
                            row.getStyleClass().add("highlightedRow");
                        }
                    } else {
                        row.getStyleClass().removeAll(Collections.singleton("highlightedRow"));
                    }
                }
            });
            return row;
        }
    });

А вот как могут выглядеть некоторые кнопки:

    final Button btnHighlight = new Button("Highlight");
    btnHighlight.disableProperty().bind(Bindings.isEmpty(table.getSelectionModel().getSelectedIndices()));
    btnHighlight.setOnAction(new EventHandler<ActionEvent>() {
        @Override
        public void handle(ActionEvent event) {
            highlightRows.setAll(table.getSelectionModel().getSelectedIndices());
        }
    });

    final Button btnClearHighlight = new Button("Clear Highlights");
    btnClearHighlight.disableProperty().bind(Bindings.isEmpty(highlightRows));
    btnClearHighlight.setOnAction(new EventHandler<ActionEvent>() {
        @Override
        public void handle(ActionEvent event) {
            highlightRows.clear();
        }
    });

Ответ 2

Как насчет создания строки factory, которая предоставляет видимый список индексов строк таблицы, которые должны быть выделены? Таким образом, вы можете просто обновить список индексами, которые вам нужно выделить: например, вызывая getSelectedIndices() в модели выбора и передавая его методу listAll (...).

Это может выглядеть примерно так:

import java.util.Collections;

import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.util.Callback;


public class StyleChangingRowFactory<T> implements
        Callback<TableView<T>, TableRow<T>> {

    private final String styleClass ;
    private final ObservableList<Integer> styledRowIndices ;
    private final Callback<TableView<T>, TableRow<T>> baseFactory ;

    public StyleChangingRowFactory(String styleClass, Callback<TableView<T>, TableRow<T>> baseFactory) {
        this.styleClass = styleClass ;
        this.baseFactory = baseFactory ;
        this.styledRowIndices = FXCollections.observableArrayList();
    }

    public StyleChangingRowFactory(String styleClass) {
        this(styleClass, null);
    }

    @Override
    public TableRow<T> call(TableView<T> tableView) {

        final TableRow<T> row ;
        if (baseFactory == null) {
            row = new TableRow<>();
        } else {
            row = baseFactory.call(tableView);
        }

        row.indexProperty().addListener(new ChangeListener<Number>() {
            @Override
            public void changed(ObservableValue<? extends Number> obs,
                    Number oldValue, Number newValue) {
                updateStyleClass(row);
            }
        });

        styledRowIndices.addListener(new ListChangeListener<Integer>() {

            @Override
            public void onChanged(Change<? extends Integer> change) {
                updateStyleClass(row);
            }
        });

        return row;
    }

    public ObservableList<Integer> getStyledRowIndices() {
        return styledRowIndices ;
    }

    private void updateStyleClass(TableRow<T> row) {
        final ObservableList<String> rowStyleClasses = row.getStyleClass();
        if (styledRowIndices.contains(row.getIndex()) ) {
            if (! rowStyleClasses.contains(styleClass)) {
                rowStyleClasses.add(styleClass);
            }
        } else {
            // remove all occurrences of styleClass:
            rowStyleClasses.removeAll(Collections.singleton(styleClass));
        }
    }

}

Теперь вы можете сделать

final StyleChangingRowFactory<Person> rowFactory = new StyleChangingRowFactory<>("highlightedRow");
table.setRowFactory(rowFactory);

И в обработчике действий вашей кнопки сделайте

    rowFactory.getStyledRowIndices().setAll(table.getSelectionModel().getSelectedIndices());

Поскольку StyleChangingRowFactory обертывает другую строку factory, вы все равно можете использовать ее, если у вас уже есть реализация пользовательской строки factory, которую вы хотите использовать. Например:

final StyleChangingRowFactory<Person> rowFactory = new StyleChangingRowFactory<Person>(
        "highlightedRow",
        new Callback<TableView<Person>, TableRow<Person>>() {

            @Override
            public TableRow<Person> call(TableView<Person> tableView) {
                final TableRow<Person> row = new TableRow<Person>();
                ContextMenu menu = new ContextMenu();
                MenuItem removeMenuItem = new MenuItem("Remove");
                removeMenuItem.setOnAction(new EventHandler<ActionEvent>() {
                    @Override
                    public void handle(ActionEvent event) {
                        table.getItems().remove(row.getItem());
                    }
                });
                menu.getItems().add(removeMenuItem);
                row.contextMenuProperty().bind(
                        Bindings.when(row.emptyProperty())
                                .then((ContextMenu) null)
                                .otherwise(menu));
                return row;
            }

        });
table.setRowFactory(rowFactory);

Здесь - полный пример кода.

Ответ 3

Вот уродливое решение для взлома. Во-первых, определите поле int, называемое highlightRow. Затем установите таблицу factory в TableView:

table.setRowFactory(new Callback<TableView<Person>, TableRow<Person>>() {
    @Override public TableRow<Person> call(TableView<Person> param) {
        return new TableRow<Person>() {
            @Override protected void updateItem(Person item, boolean empty) {
                super.updateItem(item, empty);

                if (getIndex() == highlightedRow) {
                    getStyleClass().add("highlightedRow");
                } else {
                    getStyleClass().remove("highlightedRow");
                }
            }
        };
    }
});

Затем добавьте следующий код в свою кнопку при действии (и именно здесь идет игра с уродливым взломом):

btnHighlight.setOnAction(new EventHandler<ActionEvent>(){
    public void handle(ActionEvent e){
        // set the highlightedRow integer to the selection index
        highlightedRow = table.getSelectionModel().getSelectedIndex();

        // force a tableview refresh - HACK
        List<Person> items = new ArrayList<>(table.getItems());
        table.getItems().setAll(items);
    }
});

Как только это будет сделано, вы получите коричневую подсветку в выбранной строке. Конечно, вы могли бы легко поддерживать несколько коричневых бликов, заменив int на список itns.

Ответ 4

Лучший способ найти это:

В моем CSS

.table-row-cell:feederChecked{
    -fx-background-color: #06FF00;
}

В моей инициализации таблицы с SimpleBooleanProperty содержимого объекта в моем ObservableList:

// The pseudo classes feederChecked that were defined in the css file.
PseudoClass feederChecked = PseudoClass.getPseudoClass("feederChecked");
// Set a rowFactory for the table view.
tableView.setRowFactory(tableView -> {
    TableRow<Feeder> row = new TableRow<>();
    ChangeListener<Boolean> changeListener = (obs, oldFeeder, newFeeder) -> {
        row.pseudoClassStateChanged(feederChecked, newFeeder);
    };
    row.itemProperty().addListener((obs, previousFeeder, currentFeeder) -> {
        if (previousFeeder != null) {
            previousFeeder.feederCheckedProperty().removeListener(changeListener);
        }
        if (currentFeeder != null) {
            currentFeeder.feederCheckedProperty().addListener(changeListener);
            row.pseudoClassStateChanged(feederChecked, currentFeeder.getFeederChecked());
        } else {
            row.pseudoClassStateChanged(feederChecked, false);
        }
    });
    return row;
});

Код, адаптированный из этот полный пример

Ответ 5

Я мог бы найти что-то, что работает:

При добавлении этого кода, если вы нажмете кнопку, выделенная строка изменит цвет, при выборе другой строки цвет изменится на значение по умолчанию, когда вы снова нажмете кнопку, это изменит цвет новой строки на коричневый.

final String css = getClass().getResource("style.css").toExternalForm();
final Scene scene = new Scene(new Group());


btnHighlight.setOnAction(new EventHandler<ActionEvent>() {
    @Override
     public void handle(ActionEvent e) {
         scene.getStylesheets().add(css);
     }
});
table.getSelectionModel().selectedIndexProperty()
            .addListener(new ChangeListener<Number>() {
    @Override
     public void changed(ObservableValue<? extends Number> ov, Number t, Number t1) {
         scene.getStylesheets().remove(css);
     }
});

CSS

.table-row-cell:selected
{
     -fx-background-color: brown;
     -fx-text-inner-color: white;
}

Только проблема с этим решением состоит в том, что если вы дважды нажимаете кнопку дважды, ваша следующая выбранная строка уже коричневая. Для этого вам придется использовать отдельный файл css, иначе при запуске приложения никакие правила css не будут применяться до тех пор, пока вы не нажмете кнопку.

Ответ 6

Я обнаружил, что лучшим решением будет прослушивание изменений row.itemProperty(), потому что, когда вы сортируете, например, индексы изменяют индексы, поэтому строки получают уведомление автоматически.