Программно изменить внешний вид строки TableView
После ознакомления с Oracle по TableView мне стало интересно, есть ли способ программно применить другой стиль CSS к выбранной строке TableView. Например, пользователь выбирает определенную строку, нажимает кнопку "Выделить", и выбранная строка получает коричневый фон, белую заливку текста и т. Д. Я читал цвета таблицы JavaFX, Обновление внешнего вида строки TableView и Фон с 2 цветами в 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", "jacob.smith@example.com"),
new Person("Isabella", "Johnson", "isabella.johnson@example.com"),
new Person("Ethan", "Williams", "ethan.williams@example.com"),
new Person("Emma", "Jones", "emma.jones@example.com"),
new Person("Michael", "Brown", "michael.brown@example.com")
);
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, из которого кнопка "Выделить выделенную строку" применяет класс selectedRow к выбранной строке таблицы:
.highlightedRow {
-fx-background-color: brown;
-fx-background-insets: 0, 1, 2;
-fx-background: -fx-accent;
-fx-text-fill: -fx-selection-bar-text;
}
Редактировать:
После нескольких часов попыток самое лучшее, что я мог придумать, это использовать приведенный ниже код:
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
столбец, так нужно ли устанавливать новую фабрику ячеек для каждого столбца, чтобы применить определенный стиль, или есть более разумное решение?
6 ответов
Если вам не нужна возможность многократного использования решения, которое я выложил выше, это действительно то же самое, но использование автономного внутреннего класса для фабрики строк вместо отдельного класса. Возможно, за кодом легче следовать, так как все это в одном месте. Это своего рода гибрид между решением Джонатана и моим, но оно автоматически обновляет основные моменты, не форсируя его.
Я использовал список целых чисел, чтобы он поддерживал множественный выбор, но если вам это не нужно, очевидно, вы могли бы просто использовать вместо него 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();
}
});
Как насчет создания фабрики строк, которая предоставляет видимый список индексов строк таблицы, которые должны быть выделены? Таким образом, вы можете просто обновить список индексами, которые вам нужно выделить: например, вызвав getSelectedIndices() в модели выбора и передав его в метод setAll(...) списка.
Это может выглядеть примерно так:
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 оборачивает другую фабрику строк, вы все равно можете использовать ее, если у вас уже есть собственная реализация фабрики строк, которую вы хотите использовать. Например:
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);
Вот полный пример кода.
Вот уродливое решение взломать. Во-первых, определите целое поле с именем selectedRow. Затем установите фабрику строк в 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.
Лучший способ найти это:
В моем 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;
});
Код адаптируется из этого полного примера
Я обнаружил, что лучшим решением будет прослушивание изменений row.itemProperty(), потому что когда вы сортируете, например, строки меняют индексы, поэтому строки получают уведомление автоматически.
Я мог бы найти то, что работает:
При добавлении этого кода, если вы нажмете кнопку, выделенная строка изменит цвет, когда вы выберете другую строку, цвет снова вернется к значению по умолчанию, при повторном нажатии кнопки цвет новой строки изменится на коричневый.
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 не будут применяться, пока вы не нажмете кнопку.