У меня есть стандартный TreeView в JavaFX с CheckBoxTreeItem в нем. Я установил прослушиватель, чтобы видеть, когда кто-то проверяет/снимает флажок. Но я хочу, чтобы, когда кто-то устанавливает/снимает флажок, я запускал родительский метод updateItem этого элемента checkboxitem и изменял его CSS (например, если для родителя выбрано 3 или более дочерних элементов, тогда измените его цвет на красный, иначе зеленый).
Как я могу это сделать?
rootItem.addEventHandler(CheckBoxTreeItem.checkBoxSelectionChangedEvent(), e -> {
if (e.getTreeItem().isLeaf()) {
TreeItem<String> treeItem = (TreeItem) e.getTreeItem();
CheckBoxTreeItem<String> parentItem = (CheckBoxTreeItem<String>) treeItem.getParent();
// how to call repaint for the parentItem????
}
});
treeView.setCellFactory(p -> new CheckBoxTreeCell<>() {
@Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
// toggle the parent's CSS here
}
});
Вам не нужно менять его вручную, вы можете использовать Псевдокласс:
private PseudoClass threeChildrenClass = PseudoClass.getPseudoClass("three-children");
tree.setCellFactory(param -> new CheckBoxTreeCell<String>() {
@Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setText(null);
setGraphic(null);
} else {
setText(item);
// Change the class based on the number of parent items
pseudoClassStateChanged(threeChildrenClass, hasThreeChildren(item));
}
}
});
В вашем файле CSS:
.check-box-tree-cell:three-children {
-fx-background-color: red;
}
Похоже, CheckBoxTreeCell не имеет встроенного «проверенного» псевдокласса, вы можете добавить «проверенный» псевдокласс и применить его при проверке ячейки дерева. Затем вы можете назвать это так:
.check-box-tree-cell:three-children:checked {
-fx-background-color: green;
}
@adragomir Почему бы не добавить обработчик события изменения выбора к элементу ячейки? Ячейка будет получать события для измененных дочерних элементов и может реагировать соответствующим образом. Не забудьте удалить соответствующий обработчик (и, возможно, использовать слабые обработчики).
@Slaw: если я это сделаю, то все созданные ячейки получат событие изменения и вызовут хаос. Я имею в виду, что передам это свойство центру ячейки.
@adragomir В любой момент времени будет отображаться только ~ 10-50 ячеек; что означает, что у вас будет только ~ 10-50 обработчиков событий, зарегистрированных одновременно. Кроме того, вы можете использовать событие, чтобы остановить его восхождение до корневого элемента.
Я согласен с ответ М.С. относительно использования PseudoClass
. Однако вам не следует пытаться вручную вызывать updateItem
. Вместо этого просто добавьте EventHandler
для прослушивания событий «выбор флажка изменен». Когда событие происходит в прямом дочернем элементе, родитель должен обновить псевдокласс на основе (используя ваш пример), выбрано ли 3+ дочерних элемента.
Вот пример, который также включает «ветвь» PseudoClass
, чтобы вы могли отличить ветку от листа в файле CSS:
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.css.PseudoClass;
import javafx.event.EventHandler;
import javafx.event.WeakEventHandler;
import javafx.scene.control.CheckBoxTreeItem;
import javafx.scene.control.CheckBoxTreeItem.TreeModificationEvent;
import javafx.scene.control.cell.CheckBoxTreeCell;
public class MyCheckBoxTreeCell<T> extends CheckBoxTreeCell<T> {
private static final PseudoClass BRANCH = PseudoClass.getPseudoClass("branch");
private static final PseudoClass THREE_CHILDREN_SELECTED = PseudoClass.getPseudoClass("three-children-selected");
// event handler to listen for selection changes in direct children
private final EventHandler<TreeModificationEvent<T>> handler = event -> {
/*
* Event starts from the source TreeItem and bubbles up the to the root. This means
* the first time getTreeItem() != event.getTreeItem() will be the source TreeItem's
* parent. We then consume the event to stop it propagating to the next parent.
*/
if (getTreeItem() != event.getTreeItem()) {
event.consume();
updatePseudoClasses();
}
};
private final WeakEventHandler<TreeModificationEvent<T>> weakHandler = new WeakEventHandler<>(handler);
// Used to listen for the "leaf" property of the TreeItem and update the BRANCH pseudo-class
private final InvalidationListener leafListener = observable -> updatePseudoClasses();
private final WeakInvalidationListener weakLeafListener = new WeakInvalidationListener(leafListener);
public MyCheckBoxTreeCell() {
getStyleClass().add("my-check-box-tree-cell");
// add listener to "treeItem" property to properly register and unregister
// the "leafListener" and "handler" instances.
treeItemProperty().addListener((observable, oldValue, newValue) -> {
if (oldValue != null) {
oldValue.leafProperty().removeListener(weakLeafListener);
oldValue.removeEventHandler(CheckBoxTreeItem.checkBoxSelectionChangedEvent(), weakHandler);
}
if (newValue != null) {
newValue.leafProperty().addListener(weakLeafListener);
newValue.addEventHandler(CheckBoxTreeItem.checkBoxSelectionChangedEvent(), weakHandler);
}
updatePseudoClasses();
});
}
private void updatePseudoClasses() {
/*
* Assumes the use of CheckBoxTreeItem for each TreeItem in the TreeView.
*
* This code is not the most efficient as it will recalculate both the BRANCH and
* THREE_CHILDREN_SELECTED pseudo-classes each time either possibly changes.
*/
var item = (CheckBoxTreeItem<T>) getTreeItem();
if (item == null) {
pseudoClassStateChanged(BRANCH, false);
pseudoClassStateChanged(THREE_CHILDREN_SELECTED, false);
} else {
pseudoClassStateChanged(BRANCH, !item.isLeaf());
int selected = 0;
for (var child : item.getChildren()) {
// only need to know if *at least* 3 children are selected
if (((CheckBoxTreeItem<T>) child).isSelected() && ++selected >= 3) {
break;
}
}
pseudoClassStateChanged(THREE_CHILDREN_SELECTED, selected >= 3);
}
}
// No need to override "updateItem(T,boolean)" as CheckBoxTreeCell provides
// the necessary implementation which can be customized via the StringConverter
// property.
}
И тогда ваш файл CSS может выглядеть так:
.my-check-box-tree-cell:branch {
-fx-background-color: green;
-fx-text-fill: white;
}
.my-check-box-tree-cell:branch:three-children-selected {
-fx-background-color: red;
-fx-text-fill: white;
}
Отвечаю на вопросы в комментариях:
Why wrapping every listener inside a weak one if we take care to unsubscribe it?
Чтобы уменьшить вероятность утечек памяти. Например, если вы отбрасываете TreeView
(не очистив свойство root
), но где-то сохраняете ссылки на TreeItem
, то неслабый обработчик/слушатель будет хранить TreeCell
и TreeView
в памяти.
Why are you listening for leaf changes and when does it gets called?
Для обработки случая, когда TreeItem
динамически добавляются и/или удаляются. TreeItem
является листом тогда и только тогда, когда его children
список пуст. Если элемент добавлен, и теперь лист становится ветвью, нам нужно обновить псевдокласс BRANCH
, чтобы применить правильный CSS. То же самое, если элемент удаляется, а ветвь становится листом.
Это может иметь или не иметь отношение к вашему варианту использования. Если нет, то смело удаляйте эту часть реализации.
You check
getTreeItem() != event.getTreeItem())
in the checkbox checked handler. Why? This will be called when a checkbox gets checked/ unchecked.
Когда вы (не)отмечаете CheckBoxTreeItem
, происходит событие. Это событие начинает свое путешествие с CheckBoxTreeItem
, которое было (не)проверено. Оттуда он перемещается вверх (т. е. всплывает) по иерархии элементов до самого корня. В каждом элементе будут вызываться любые зарегистрированные обработчики. Хотя, если событие потребляется, оно не переходит к следующему родительскому элементу.
Причина, по которой мы добавляем обработчик, состоит в том, чтобы прослушивать любой дети (не)проверенный, но только прямые дети. Нас не интересуют изменения произвольно глубоких потомков или элемент, на который был зарегистрирован обработчик.
Поскольку нас интересуют только изменения в прямых дочерних элементах, нам нужно убедиться, что мы реагируем только на события, запускаемые указанными дочерними элементами. Поскольку событие сначала обрабатывается элементом, который был (не)проверен, нам не нужно ничего делать в этом первом обработчике. Мы можем сделать это, проверив, является ли TreeItem
содержащего TreeCell
тем же, что вызвало событие, и метод TreeModificationEvent#getTreeItem()
возвращает элемент, вызвавший срабатывание события (то есть элемент, который был (не)проверен). Если это один и тот же экземпляр, ничего не делайте и дайте событию подняться до родителя.
Теперь обработчик родительского элемента обрабатывает событие. Это означает, что getTreeItem() != event.getTreeItem()
вернет true
, и мы войдем в блок if
. Это вызывает обновление, если необходимо, состояния псевдоклассов. Затем мы потребляем событие, чтобы оно не перешло к следующему родителю; это эффективно заставляет обработчик прослушивать события только из прямые дети.
Обратите внимание, что если родительский элемент в данный момент не отображается в дереве, то он не будет частью ячейки. Если это не часть ячейки, к ней не будет добавлен обработчик. Таким образом, любые неотображаемые элементы не будут затронуты этим. Это нормально, поскольку все, что мы обновляем, является чисто визуальным; если элемент не отображается, то нет визуальных элементов для обновления.
Slaw Я нахожусь в сельской местности, я проверю это завтра и приму ваш ответ, если все в порядке.
Отличный ответ, я попытался привести его к правильному способу реализации этой функции.
Слав, работает отлично! Я постараюсь полностью понять ваш код!
@adragomir Рад, что смог помочь. Если в моем коде есть что-то конкретное, что вам нужно понять, дайте мне знать.
@Slaw Вы сами поняли это из исходного кода javafx или прочитали какую-нибудь хорошую книгу, которую можете порекомендовать?
@adragomir Давным-давно я прочитал несколько книг, что-то вроде «Pro JavaFX» и «Master JavaFX Controls». Помимо этого, я в основном просто читаю некоторые учебные пособия, документацию (например, Javadoc, Справочное руководство по CSS для JavaFX и Введение в FXML) и много вопросов о переполнении стека. Всякий раз, когда эти ресурсы меня не устраивают или мне просто любопытно, я изучаю исходный код JavaFX.
@Slaw Несколько вопросов, если вы любезны: 1. Зачем заворачивать каждого слушателя в слабого, если мы позаботимся о том, чтобы отписаться от него? 2. Почему вы прослушиваете изменения листьев и когда они вызываются? 3. Вы проверяете getTreeItem() != event.getTreeItem()) в обработчике Checkbox Checked. Почему? Это будет вызываться, когда флажок установлен/снят.
@adragomir Обновил мой ответ (внизу)
М. С. Насколько я помню, если не ошибаюсь: updateItem не вызывается при установленном флажке. Вот почему я хотел сделать что-то из обработчика проверки. Есть предположения? Код работает для вас?