Мне нужно реализовать множество пользовательских TableCell, поведение которых зависит от изменения модели. Мне удалось каким-то образом получить ожидаемый результат, но я думаю, что во многих случаях это был обходной путь, а действительно хорошее решение. Я использовал привязки/слушатели для достижения ожидаемого результата, но проблема, с которой я сталкиваюсь, заключается в том, что я могу добавлять прослушиватели/связывать свойства несколько раз, что может привести к утечке памяти.
Вот пример того, что я имею в виду.
Контроллер:
public class Controller implements Initializable {
@FXML private TableView<Model> table;
@FXML private TableColumn<Model, String> column;
@FXML private Button change;
@Override
public void initialize(URL location, ResourceBundle resources) {
column.setCellValueFactory(data -> data.getValue().text);
column.setCellFactory(cell -> new ColoredTextCell());
Model apple = new Model("Apple", "#8db600");
table.getItems().add(apple);
table.getItems().add(new Model("Banana", "#ffe135"));
change.setOnAction(event -> apple.color.setValue("#ff0800"));
}
@Getter
private class Model {
StringProperty text;
StringProperty color;
private Model(String text, String color) {
this.text = new SimpleStringProperty(text);
this.color = new SimpleStringProperty(color);
}
}
private class ColoredTextCell extends TableCell<Model, String> {
@Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty || getTableRow() == null || getTableRow().getItem() == null) {
setGraphic(null);
return;
}
Model model = (Model) getTableRow().getItem();
Text text = new Text(item);
text.setFill(Color.web(model.getColor().getValue()));
// This way I add the listener evey item updateItem is called.
model.getColor().addListener((observable, oldValue, newValue) -> {
if (newValue != null) {
text.setFill(Color.web(newValue));
} else {
text.setFill(Color.BLACK);
}
});
setGraphic(text);
}
}
}
FXML:
<?xml version = "1.0" encoding = "UTF-8"?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Button?>
<AnchorPane xmlns = "http://javafx.com/javafx"
xmlns:fx = "http://javafx.com/fxml"
fx:controller = "stackoverflow.tabpane.Controller">
<VBox>
<Button fx:id = "change" text = "Change color"/>
<TableView fx:id = "table">
<columns>
<TableColumn fx:id = "column" prefWidth = "200"/>
</columns>
</TableView>
</VBox>
</AnchorPane>
Поскольку свойство цвета не наблюдается непосредственно ячейкой, updateItem не вызывается, если оно изменяется, поэтому мне нужно как-то слушать.
Мне нужно, чтобы updateItem
срабатывал после изменения цвет. Это приведет к одному вызову содержимого слушателя.
Есть ли способ прослушать другое изменение модели в той же ячейке или как-то вызвать элемент обновления, чтобы изменение отображалось.
Я думаю, вы могли бы сделать это наоборот.
Я бы создал свойство цвета следующим образом:
ObjectBinding<Paint> colorProperty = Bindings.createObjectBinding(()->{
String color = model.getColor().get();
return Paint.valueOf(color==null?"BLACK":color);
} , model.getColor());
Затем я бы привязал свойство следующим образом:
text.fillProperty().bind(model.colorProperty);
Было бы еще проще, если бы у вас было:
SimpleObjectProperty<Paint> textColor = new SimpleObjectProperty<Paint>(Paint.valueOf("BLACK"));
а затем в геттере и сеттере вашей модели обновите такое свойство.
Использование прослушивателей и привязок не вызовет никаких проблем, если вы не забудете удалить их, когда они больше не нужны. Чтобы сделать его еще безопаснее, вы должны использовать слабые слушатели (привязки используют слабые слушатели). Поскольку вы хотите изменить цвет текста ячейки на основе другого свойства элемента строки, я думаю, что использование привязки будет проще. Обратите внимание, что TableCell
наследуется от Labeled
, что означает, что у него есть свойство textFill
; нет необходимости создавать узел Text
, чтобы изменить цвет текста.
Вот пример:
import javafx.beans.binding.Bindings;
import javafx.scene.control.TableCell;
import javafx.scene.paint.Color;
public class ColoredTextCell extends TableCell<Model, String> {
@Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
/*
* I was getting a NullPointerException without the "getTableRow() == null"
* check. I find it strange that a TableCell's "updateItem" method would be
* invoked before it was part of a TableRow... but the added null check seems
* to solve the problem (at least when only having two items in the table and
* no scrolling).
*/
if (empty || item == null || getTableRow() == null) {
setText(null);
textFillProperty().unbind();
} else {
setText(item);
Model rowItem = getTableRow().getItem();
textFillProperty().bind(Bindings.createObjectBinding(
() -> Color.valueOf(rowItem.getColor()),
rowItem.colorProperty()
));
}
}
}
Вызов textFillProperty().unbind()
предотвратит утечку памяти. А при привязке свойства предыдущая привязка, если она была, будет удалена. Если вы действительно параноик, вы также можете вызвать unbind()
перед bind(...)
. И если вы действительно параноик В самом деле, то вы можете сохранить ObjectBinding
в поле и вызвать dispose()
, когда это уместно (и даже обнулить его).
@minus Сам ObjectBinding
хранится в строгой ссылке внутри свойства textFill
. Поскольку TableCell
имеет сильную ссылку на это свойство, привязка строго достижима. Однако слушатель, который привязка добавляет к зависимостям Observable
, слаб, как и слушатель, который свойство добавляет к ObservableValue
, переданному bind(...)
.
@minus Обратите внимание, что слабые прослушиватели предназначены для предотвращения хранения объект, который добавил слушателя в памяти только потому, что объект, к которому был добавлен слушатель строго доступен.
Сначала я не хотел идти по пути привязки, потому что в моем случае это сложнее, чем просто установить цвет, есть несколько проверок, и я использую TextFlow
вместо Text
, у которого много Text
в дочерних элементах, но, наконец, я удалось пойти на это и теперь его совершенный ist не называют более чем необходимо. Я думаю, что приму решение минуса, так как он был первым, и в основном это то же самое предложение. Надеюсь, это нормально для вас ;)
@Sunflame Конечно, нет проблем. Кроме того, если ваше представление начинает сильно усложняться, подумайте о том, чтобы переместить его в отдельный класс. Затем установите graphic
как экземпляр этого класса. И обратите внимание, что ваш пример каждый раз создает новый экземпляр Text
; рассмотрите возможность кэширования объектов представления, чтобы избежать создания и выбрасывания сотен из них при прокрутке пользователем.
Конечно, я не создаю каждый раз новый экземпляр в updateItem
, но я его использую повторно, в моем примере я просто поторопился и не обратил на это должного внимания.
@Sunflame Думаю, я упомяну об этом на всякий случай (может также помочь другим, кто придет).
Что-то я не до конца понимаю.
Bindings.createObjectBinding(..)
находится в блоке else, и в javadoc говорится, что привязка использует слабый прослушиватель. У кого есть ссылка на такую привязку?