JavaFX:在 TreeTable 中添加 CheckBoxTreeItem?
JavaFX: Add CheckBoxTreeItem in TreeTable?
我正在尝试使用 JavaFX 并尝试在树中添加一个复选框项目 table,但看起来它只支持简单的树项目。
我的代码是 Oracle's TreeTableView Example 的修改版本:
public class TreeTableViewSample extends Application implements Runnable {
List<Employee> employees = Arrays.<Employee>asList(
new Employee("Ethan Williams", 30.0),
new Employee("Emma Jones", 10.0),
new Employee("Michael Brown", 70.0),
new Employee("Anna Black", 50.0),
new Employee("Rodger York", 20.0),
new Employee("Susan Collins", 70.0));
/* private final ImageView depIcon = new ImageView (
new Image(getClass().getResourceAsStream("department.png"))
);
*/
final CheckBoxTreeItem<Employee> root
= new CheckBoxTreeItem<>(new Employee("Sales Department", 0.0));
final CheckBoxTreeItem<Employee> root2
= new CheckBoxTreeItem<>(new Employee("Departments", 0.0));
public static void main(String[] args) {
Application.launch(TreeTableViewSample.class, args);
}
@Override
public void start(Stage stage) {
root.setExpanded(true);
employees.stream().forEach((employee) -> {
root.getChildren().add(new CheckBoxTreeItem<>(employee));
});
stage.setTitle("Tree Table View Sample");
final Scene scene = new Scene(new Group(), 400, 400);
scene.setFill(Color.LIGHTGRAY);
Group sceneRoot = (Group) scene.getRoot();
TreeTableColumn<Employee, String> empColumn
= new TreeTableColumn<>("Employee");
empColumn.setPrefWidth(150);
empColumn.setCellValueFactory(
(TreeTableColumn.CellDataFeatures<Employee, String> param)
-> new ReadOnlyStringWrapper(param.getValue().getValue().getName())
);
TreeTableColumn<Employee, Double> salaryColumn
= new TreeTableColumn<>("Salary");
salaryColumn.setPrefWidth(190);
/* salaryColumn.setCellValueFactory(
(TreeTableColumn.CellDataFeatures<Employee, String> param) ->
new ReadOnlyDoubleWrapper(param.getValue().getValue().getEmail())
);
*/
salaryColumn.setCellFactory(ProgressBarTreeTableCell.<Employee>forTreeTableColumn());
root2.getChildren().add(root);
TreeTableView<Employee> treeTableView = new TreeTableView<>(root2);
treeTableView.getColumns().setAll(empColumn, salaryColumn);
sceneRoot.getChildren().add(treeTableView);
stage.setScene(scene);
stage.show();
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
executorService.scheduleAtFixedRate(this, 3, 10, TimeUnit.SECONDS);
}
@Override
public void run() {
root2.getValue().setSalary(calcSalary(root));
}
public double calcSalary(TreeItem<Employee> t) {
Double salary = 0.0;
if (!t.isLeaf()) {
ObservableList<TreeItem<Employee>> al = t.getChildren();
for (int i = 0; i < al.size(); i++) {
TreeItem<Employee> get = al.get(i);
salary += calcSalary(get);
}
t.getValue().setSalary(salary);
}
return salary += t.getValue().getSalary();
}
public class Employee {
private SimpleStringProperty name;
private SimpleDoubleProperty salary;
public SimpleStringProperty nameProperty() {
if (name == null) {
name = new SimpleStringProperty(this, "name");
}
return name;
}
public SimpleDoubleProperty salaryProperty() {
if (salary == null) {
salary = new SimpleDoubleProperty(this, "salary");
}
return salary;
}
private Employee(String name, Double salary) {
this.name = new SimpleStringProperty(name);
this.salary = new SimpleDoubleProperty(salary);
}
public String getName() {
return name.get();
}
public void setName(String fName) {
name.set(fName);
}
public Double getSalary() {
return salary.get();
}
public void setSalary(Double fName) {
salary.set(fName);
}
}
}
有什么方法可以在上面的示例中为树项目使用复选框吗?我正在使用 JavaFx 8。
我也在尝试创建工资条,它也可以用作任务及其子任务的进度条。 (只是玩 UI)。但不知道如何将它们与员工的真实价值联系起来,因为我猜正常 table 视图与树 table 视图不同。谢谢 ! :)
没有对应于 CheckBoxTreeCell 的单元格实现:这是一个带有复选框的单元格,该复选框绑定到 CheckBoxTreeItem 的 selected/indeterminate 属性。明显的对应 CheckBoxTreeTableCell 只是一个带有复选框的单元格,它绑定到单元格数据。
需要的是 CheckBoxTreeTableRow:这是可以访问 TreeItem 并可以管理 checkBox 和 treeItem 之间的绑定的单元格层。下面是 CheckBoxTreeCell 的快速实施、简化和调整的副本。 un/binding 在 updateItem 中处理。
更新:清理解决方案(冗长!)
看起来 TableRowSkinBase 准备处理自定义行图形,它有一个方法 graphicsProperty(),用于行皮肤内的所有布局代码。
/**
* Returns the graphic to draw on the inside of the disclosure node. Null
* is acceptable when no graphic should be shown. Commonly this is the
* graphic associated with a TreeItem (i.e. treeItem.getGraphic()), rather
* than a graphic associated with a cell.
*/
protected abstract ObjectProperty<Node> graphicProperty();
TreeTableRowSkin 将其实现为 return TreeItem 的图形,因此覆盖 return tableRow 的图形应该有效。除了......它不是 - 布局是歪的,如下面的原始答案中所述。挖掘暴露了罪魁祸首:它是 TreeTableCellSkin 硬编码它自己的布局代码以解释其填充中的任何图形...... treeItem 的图形。
所以一个完整的解决方案需要
- a Tree/TableCellSkin 不对 treeItem 图形进行硬编码(下面的示例仍然不完全干净,它依赖于 super 添加图形宽度并再次减去它)
- 安装增强皮肤的Tree/TableCell
- a Tree/TableRowSkin 根据需要覆盖 graphicsProperty,在 returning 行 graphic
下方
- a Tree/TableRow 根据需要更新其图形,下面将其图形设置为一个复选框,该复选框又可能包含 treeItem 的图形
第一对叫DefaultTreeTableCell/Skin,下面是第二对CheckBoxTreeTableRow/Skin。
用法(插入 OP 示例的片段)
// just for fun, have root items with some graphic
final CheckBoxTreeItem<Employee> root = new CheckBoxTreeItem<>(
new Employee("Sales Department", 0.0), new Circle(10, Color.RED));
final CheckBoxTreeItem<Employee> root2 = new CheckBoxTreeItem<>(
new Employee("Departments", 0.0), new Circle(10, Color.BLUE));
// configure treeTableView to use the extended tableRow
treeTableView.setRowFactory(item -> new CheckBoxTreeTableRow<>());
// configure table columns to use the extended table cell
empColumn.setCellFactory(p -> new DefaultTreeTableCell<>());
// all cell types must have a skin that copes with row graphics
salaryColumn.setCellFactory(e -> {
TreeTableCell cell = new ProgressBarTreeTableCell() {
@Override
protected Skin<?> createDefaultSkin() {
return new DefaultTreeTableCell.DefaultTreeTableCellSkin<>(this);
}
};
return cell;
});
Cell/Row 实现:
/**
* TreeTableCell actually showing something. This is copied from TreeTableColumn plus
* installs DefaultTreeTableCellSkin which handles row graphic width.
*/
public class DefaultTreeTableCell<S, T> extends TreeTableCell<S, T> {
@Override
protected void updateItem(T item, boolean empty) {
if (item == getItem()) return;
super.updateItem(item, empty);
if (item == null) {
super.setText(null);
super.setGraphic(null);
} else if (item instanceof Node) {
super.setText(null);
super.setGraphic((Node)item);
} else {
super.setText(item.toString());
super.setGraphic(null);
}
}
@Override
protected Skin<?> createDefaultSkin() {
return new DefaultTreeTableCellSkin<>(this);
}
/**
* TreeTableCellSkin that handles row graphic in its leftPadding, if
* it is in the treeColumn of the associated TreeTableView.
* <p>
* It assumes that per-row graphics - including the graphic of the TreeItem, if any -
* is folded into the TreeTableRow graphic and patches its leftLabelPadding
* to account for the graphic width.
* <p>
*
* Note: TableRowSkinBase seems to be designed to cope with variations of row
* graphic - it has a method <code>graphicProperty()</code> that's always used
* internally when calculating offsets in the treeColumn.
* Subclasses override as needed, the layout code remains constant. The real
* problem is the TreeTableCell hard-codes the TreeItem as the only graphic
* owner.
*
*/
public static class DefaultTreeTableCellSkin<S, T> extends TreeTableCellSkin<S, T> {
/**
* @param treeTableCell
*/
public DefaultTreeTableCellSkin(TreeTableCell<S, T> treeTableCell) {
super(treeTableCell);
}
/**
* Overridden to adjust the padding returned by super for row graphic.
*/
@Override
protected double leftLabelPadding() {
double padding = super.leftLabelPadding();
padding += getRowGraphicPatch();
return padding;
}
/**
* Returns the patch for leftPadding if the tableRow has a graphic of
* its own.<p>
*
* Note: this implemenation is a bit whacky as it relies on super's
* handling of treeItems graphics offset. A cleaner
* implementation would override leftLabelPadding from scratch.
* <p>
* PENDING JW: doooooo it!
*
* @return
*/
protected double getRowGraphicPatch() {
if (!isTreeColumn()) return 0;
Node graphic = getSkinnable().getTreeTableRow().getGraphic();
if (graphic != null) {
double height = getCellSize();
// start with row's graphic
double patch = graphic.prefWidth(height);
// correct for super's having added treeItem's graphic
TreeItem<S> item = getSkinnable().getTreeTableRow().getTreeItem();
if (item.getGraphic() != null) {
double correct = item.getGraphic().prefWidth(height);
patch -= correct;
}
return patch;
}
return 0;
}
/**
* Checks and returns whether our cell is attached to a treeTableView/column
* and actually has a TreeItem.
* @return
*/
protected boolean isTreeColumn() {
if (getSkinnable().isEmpty()) return false;
TreeTableColumn<S, T> column = getSkinnable().getTableColumn();
TreeTableView<S> view = getSkinnable().getTreeTableView();
if (column.equals(view.getTreeColumn())) return true;
return view.getVisibleLeafColumns().indexOf(column) == 0;
}
}
}
/**
* Support custom graphic for Tree/TableRow. Here in particular a checkBox.
*
* <p>
* Basic idea: implement custom TreeTableRow that set's its graphic to the
* graphic/checkBox. Doesn't work: layout is broken, graphic appears
* over the text. All fine if we set the graphic to the TreeItem that's
* shown. Possible as long as the treeItem doesn't have a graphic of
* its own.
* <p>
* Basic problem:
* <li> TableRowSkinBase seems to be able to cope: has protected method
* graphicsProperty that should be implemented to return the graphic
* if any. That graphic is added to the children list and sized/located
* in layoutChildren.
* <li> are added the graphic/disclosureNode as needed before
* calling super.layoutChildren,
* <li> graphic/disclosure are placed inside the leftPadding of the tableCell
* that is the treeColumn
* <li> TreeTableCellSkin must cooperate in taking into account the graphic/disclosure
* when calculating its leftPadding
* <li> cellSkin is hard-coded to use the TreeItem's graphic (vs. the rowCell's)
*
* PENDING JW:
* <li>- would expect to not alter the scenegraph during layout (might lead to
* endless loops or not) but done frequently in core code
* <p>
*
* Outline of the solution as implemented:
* <li> need a TreeTableCell with a custom skin
* <li> override leftPadding in skin to add row graphic if available
* <li> need CheckBoxTreeTableRow that sets its graphic to checkBox (or a combination
* of checkBox and treeItem's)
* <li> need custom rowSkin that implements graphicProperty to return the row graphic
*
* @author Jeanette Winzenburg, Berlin
*
* @see DefaultTreeTableCell
* @see DefaultTreeTableCellSkin
*
*/
public class CheckBoxTreeTableRow<T> extends TreeTableRow<T> {
private CheckBox checkBox;
private ObservableValue<Boolean> booleanProperty;
private BooleanProperty indeterminateProperty;
public CheckBoxTreeTableRow() {
this(item -> {
if (item instanceof CheckBoxTreeItem<?>) {
return ((CheckBoxTreeItem<?>)item).selectedProperty();
}
return null;
});
}
public CheckBoxTreeTableRow(
final Callback<TreeItem<T>, ObservableValue<Boolean>> getSelectedProperty) {
this.getStyleClass().add("check-box-tree-cell");
setSelectedStateCallback(getSelectedProperty);
checkBox = new CheckBox();
checkBox.setAlignment(Pos.TOP_LEFT);
}
// --- selected state callback property
private ObjectProperty<Callback<TreeItem<T>, ObservableValue<Boolean>>>
selectedStateCallback =
new SimpleObjectProperty<Callback<TreeItem<T>, ObservableValue<Boolean>>>(
this, "selectedStateCallback");
/**
* Property representing the {@link Callback} that is bound to by the
* CheckBox shown on screen.
*/
public final ObjectProperty<Callback<TreeItem<T>, ObservableValue<Boolean>>> selectedStateCallbackProperty() {
return selectedStateCallback;
}
/**
* Sets the {@link Callback} that is bound to by the CheckBox shown on screen.
*/
public final void setSelectedStateCallback(Callback<TreeItem<T>, ObservableValue<Boolean>> value) {
selectedStateCallbackProperty().set(value);
}
/**
* Returns the {@link Callback} that is bound to by the CheckBox shown on screen.
*/
public final Callback<TreeItem<T>, ObservableValue<Boolean>> getSelectedStateCallback() {
return selectedStateCallbackProperty().get();
}
/** {@inheritDoc} */
@Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
TreeItem<T> treeItem = getTreeItem();
checkBox.setGraphic(treeItem == null ? null : treeItem.getGraphic());
setGraphic(checkBox);
// uninstall bindings
if (booleanProperty != null) {
checkBox.selectedProperty().unbindBidirectional((BooleanProperty)booleanProperty);
}
if (indeterminateProperty != null) {
checkBox.indeterminateProperty().unbindBidirectional(indeterminateProperty);
}
// install new bindings.
// this can only handle TreeItems of type CheckBoxTreeItem
if (treeItem instanceof CheckBoxTreeItem) {
CheckBoxTreeItem<T> cbti = (CheckBoxTreeItem<T>) treeItem;
booleanProperty = cbti.selectedProperty();
checkBox.selectedProperty().bindBidirectional((BooleanProperty)booleanProperty);
indeterminateProperty = cbti.indeterminateProperty();
checkBox.indeterminateProperty().bindBidirectional(indeterminateProperty);
} else {
throw new IllegalStateException("item must be CheckBoxTreeItem");
}
}
}
@Override
protected Skin<?> createDefaultSkin() {
return new CheckBoxTreeTableRowSkin<>(this);
}
public static class CheckBoxTreeTableRowSkin<S> extends TreeTableRowSkin<S> {
protected ObjectProperty<Node> checkGraphic;
/**
* @param control
*/
public CheckBoxTreeTableRowSkin(TreeTableRow<S> control) {
super(control);
}
/**
* Note: this is implicitly called from the constructor of LabeledSkinBase.
* At that time, checkGraphic is not yet instantiated. So we do it here,
* still having to create it at least twice. That'll be a problem if
* anybody would listen to changes ...
*/
@Override
protected ObjectProperty<Node> graphicProperty() {
if (checkGraphic == null) {
checkGraphic = new SimpleObjectProperty<Node>(this, "checkGraphic");
}
CheckBoxTreeTableRow<S> treeTableRow = getTableRow();
if (treeTableRow.getTreeItem() == null) {
checkGraphic.set(null);
} else {
checkGraphic.set(treeTableRow.getGraphic());
}
return checkGraphic;
}
protected CheckBoxTreeTableRow<S> getTableRow() {
return (CheckBoxTreeTableRow<S>) super.getSkinnable();
}
}
@SuppressWarnings("unused")
private static final Logger LOG = Logger
.getLogger(CheckBoxTreeTableRow.class.getName());
}
原答案:hack!
里面有一行疯狂的代码:
treeItem.setGraphics(checkBox);
这真的很古怪,最终可能会造成严重破坏 - 它是围绕 TreeTableRowSkin 中的布局故障进行的黑客攻击,由于某种原因(我无法挖掘)无法将图形集定位到单元格。无法使其在 return 直接在其 graphicProperty()
中设置复选框的自定义 CheckBoxTreeTableRowSkin 中运行 - 所以我们现在就开始吧。
/**
* @author Jeanette Winzenburg, Berlin
*/
public class CheckBoxTreeTableRowHack<T> extends TreeTableRow<T> {
private CheckBox checkBox;
private ObservableValue<Boolean> booleanProperty;
private BooleanProperty indeterminateProperty;
public CheckBoxTreeTableRowHack() {
setSelectedStateCallback(item -> {
if (item instanceof CheckBoxTreeItem<?>) {
return ((CheckBoxTreeItem<?>)item).selectedProperty();
}
return null;
});
this.checkBox = new CheckBox();
// something weird going on with layout
checkBox.setAlignment(Pos.TOP_LEFT);
}
// --- selected state callback property
private ObjectProperty<Callback<TreeItem<T>, ObservableValue<Boolean>>>
selectedStateCallback =
new SimpleObjectProperty<Callback<TreeItem<T>, ObservableValue<Boolean>>>(
this, "selectedStateCallback");
/**
* Property representing the {@link Callback} that is bound to by the
* CheckBox shown on screen.
*/
public final ObjectProperty<Callback<TreeItem<T>, ObservableValue<Boolean>>> selectedStateCallbackProperty() {
return selectedStateCallback;
}
/**
* Sets the {@link Callback} that is bound to by the CheckBox shown on screen.
*/
public final void setSelectedStateCallback(Callback<TreeItem<T>, ObservableValue<Boolean>> value) {
selectedStateCallbackProperty().set(value);
}
/**
* Returns the {@link Callback} that is bound to by the CheckBox shown on screen.
*/
public final Callback<TreeItem<T>, ObservableValue<Boolean>> getSelectedStateCallback() {
return selectedStateCallbackProperty().get();
}
/** {@inheritDoc} */
@Override
public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
//
TreeItem<T> treeItem = getTreeItem();
// PENDING JW: this is nuts but working .. certainly will pose problems
// when re-using the cell
treeItem.setGraphic(checkBox);
// this is what CheckBoxTreeCell does, setting the graphic
// of the tableRow confuses the layout
// checkBox.setGraphic(treeItem == null ? null : treeItem.getGraphic());
// setGraphic(checkBox);
// uninstall bindings
if (booleanProperty != null) {
checkBox.selectedProperty().unbindBidirectional((BooleanProperty)booleanProperty);
}
if (indeterminateProperty != null) {
checkBox.indeterminateProperty().unbindBidirectional(indeterminateProperty);
}
// install new bindings.
// We special case things when the TreeItem is a CheckBoxTreeItem
if (treeItem instanceof CheckBoxTreeItem) {
CheckBoxTreeItem<T> cbti = (CheckBoxTreeItem<T>) treeItem;
booleanProperty = cbti.selectedProperty();
checkBox.selectedProperty().bindBidirectional((BooleanProperty)booleanProperty);
indeterminateProperty = cbti.indeterminateProperty();
checkBox.indeterminateProperty().bindBidirectional(indeterminateProperty);
} else {
throw new IllegalStateException("item must be CheckBoxTreeItem");
}
}
}
}
// usage: in the example add
treeTableView.setRowFactory(f -> new CheckBoxTreeTableRowHack<>());
我正在尝试使用 JavaFX 并尝试在树中添加一个复选框项目 table,但看起来它只支持简单的树项目。
我的代码是 Oracle's TreeTableView Example 的修改版本:
public class TreeTableViewSample extends Application implements Runnable {
List<Employee> employees = Arrays.<Employee>asList(
new Employee("Ethan Williams", 30.0),
new Employee("Emma Jones", 10.0),
new Employee("Michael Brown", 70.0),
new Employee("Anna Black", 50.0),
new Employee("Rodger York", 20.0),
new Employee("Susan Collins", 70.0));
/* private final ImageView depIcon = new ImageView (
new Image(getClass().getResourceAsStream("department.png"))
);
*/
final CheckBoxTreeItem<Employee> root
= new CheckBoxTreeItem<>(new Employee("Sales Department", 0.0));
final CheckBoxTreeItem<Employee> root2
= new CheckBoxTreeItem<>(new Employee("Departments", 0.0));
public static void main(String[] args) {
Application.launch(TreeTableViewSample.class, args);
}
@Override
public void start(Stage stage) {
root.setExpanded(true);
employees.stream().forEach((employee) -> {
root.getChildren().add(new CheckBoxTreeItem<>(employee));
});
stage.setTitle("Tree Table View Sample");
final Scene scene = new Scene(new Group(), 400, 400);
scene.setFill(Color.LIGHTGRAY);
Group sceneRoot = (Group) scene.getRoot();
TreeTableColumn<Employee, String> empColumn
= new TreeTableColumn<>("Employee");
empColumn.setPrefWidth(150);
empColumn.setCellValueFactory(
(TreeTableColumn.CellDataFeatures<Employee, String> param)
-> new ReadOnlyStringWrapper(param.getValue().getValue().getName())
);
TreeTableColumn<Employee, Double> salaryColumn
= new TreeTableColumn<>("Salary");
salaryColumn.setPrefWidth(190);
/* salaryColumn.setCellValueFactory(
(TreeTableColumn.CellDataFeatures<Employee, String> param) ->
new ReadOnlyDoubleWrapper(param.getValue().getValue().getEmail())
);
*/
salaryColumn.setCellFactory(ProgressBarTreeTableCell.<Employee>forTreeTableColumn());
root2.getChildren().add(root);
TreeTableView<Employee> treeTableView = new TreeTableView<>(root2);
treeTableView.getColumns().setAll(empColumn, salaryColumn);
sceneRoot.getChildren().add(treeTableView);
stage.setScene(scene);
stage.show();
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
executorService.scheduleAtFixedRate(this, 3, 10, TimeUnit.SECONDS);
}
@Override
public void run() {
root2.getValue().setSalary(calcSalary(root));
}
public double calcSalary(TreeItem<Employee> t) {
Double salary = 0.0;
if (!t.isLeaf()) {
ObservableList<TreeItem<Employee>> al = t.getChildren();
for (int i = 0; i < al.size(); i++) {
TreeItem<Employee> get = al.get(i);
salary += calcSalary(get);
}
t.getValue().setSalary(salary);
}
return salary += t.getValue().getSalary();
}
public class Employee {
private SimpleStringProperty name;
private SimpleDoubleProperty salary;
public SimpleStringProperty nameProperty() {
if (name == null) {
name = new SimpleStringProperty(this, "name");
}
return name;
}
public SimpleDoubleProperty salaryProperty() {
if (salary == null) {
salary = new SimpleDoubleProperty(this, "salary");
}
return salary;
}
private Employee(String name, Double salary) {
this.name = new SimpleStringProperty(name);
this.salary = new SimpleDoubleProperty(salary);
}
public String getName() {
return name.get();
}
public void setName(String fName) {
name.set(fName);
}
public Double getSalary() {
return salary.get();
}
public void setSalary(Double fName) {
salary.set(fName);
}
}
}
有什么方法可以在上面的示例中为树项目使用复选框吗?我正在使用 JavaFx 8。
我也在尝试创建工资条,它也可以用作任务及其子任务的进度条。 (只是玩 UI)。但不知道如何将它们与员工的真实价值联系起来,因为我猜正常 table 视图与树 table 视图不同。谢谢 ! :)
没有对应于 CheckBoxTreeCell 的单元格实现:这是一个带有复选框的单元格,该复选框绑定到 CheckBoxTreeItem 的 selected/indeterminate 属性。明显的对应 CheckBoxTreeTableCell 只是一个带有复选框的单元格,它绑定到单元格数据。
需要的是 CheckBoxTreeTableRow:这是可以访问 TreeItem 并可以管理 checkBox 和 treeItem 之间的绑定的单元格层。下面是 CheckBoxTreeCell 的快速实施、简化和调整的副本。 un/binding 在 updateItem 中处理。
更新:清理解决方案(冗长!)
看起来 TableRowSkinBase 准备处理自定义行图形,它有一个方法 graphicsProperty(),用于行皮肤内的所有布局代码。
/**
* Returns the graphic to draw on the inside of the disclosure node. Null
* is acceptable when no graphic should be shown. Commonly this is the
* graphic associated with a TreeItem (i.e. treeItem.getGraphic()), rather
* than a graphic associated with a cell.
*/
protected abstract ObjectProperty<Node> graphicProperty();
TreeTableRowSkin 将其实现为 return TreeItem 的图形,因此覆盖 return tableRow 的图形应该有效。除了......它不是 - 布局是歪的,如下面的原始答案中所述。挖掘暴露了罪魁祸首:它是 TreeTableCellSkin 硬编码它自己的布局代码以解释其填充中的任何图形...... treeItem 的图形。
所以一个完整的解决方案需要
- a Tree/TableCellSkin 不对 treeItem 图形进行硬编码(下面的示例仍然不完全干净,它依赖于 super 添加图形宽度并再次减去它)
- 安装增强皮肤的Tree/TableCell
- a Tree/TableRowSkin 根据需要覆盖 graphicsProperty,在 returning 行 graphic 下方
- a Tree/TableRow 根据需要更新其图形,下面将其图形设置为一个复选框,该复选框又可能包含 treeItem 的图形
第一对叫DefaultTreeTableCell/Skin,下面是第二对CheckBoxTreeTableRow/Skin。
用法(插入 OP 示例的片段)
// just for fun, have root items with some graphic
final CheckBoxTreeItem<Employee> root = new CheckBoxTreeItem<>(
new Employee("Sales Department", 0.0), new Circle(10, Color.RED));
final CheckBoxTreeItem<Employee> root2 = new CheckBoxTreeItem<>(
new Employee("Departments", 0.0), new Circle(10, Color.BLUE));
// configure treeTableView to use the extended tableRow
treeTableView.setRowFactory(item -> new CheckBoxTreeTableRow<>());
// configure table columns to use the extended table cell
empColumn.setCellFactory(p -> new DefaultTreeTableCell<>());
// all cell types must have a skin that copes with row graphics
salaryColumn.setCellFactory(e -> {
TreeTableCell cell = new ProgressBarTreeTableCell() {
@Override
protected Skin<?> createDefaultSkin() {
return new DefaultTreeTableCell.DefaultTreeTableCellSkin<>(this);
}
};
return cell;
});
Cell/Row 实现:
/**
* TreeTableCell actually showing something. This is copied from TreeTableColumn plus
* installs DefaultTreeTableCellSkin which handles row graphic width.
*/
public class DefaultTreeTableCell<S, T> extends TreeTableCell<S, T> {
@Override
protected void updateItem(T item, boolean empty) {
if (item == getItem()) return;
super.updateItem(item, empty);
if (item == null) {
super.setText(null);
super.setGraphic(null);
} else if (item instanceof Node) {
super.setText(null);
super.setGraphic((Node)item);
} else {
super.setText(item.toString());
super.setGraphic(null);
}
}
@Override
protected Skin<?> createDefaultSkin() {
return new DefaultTreeTableCellSkin<>(this);
}
/**
* TreeTableCellSkin that handles row graphic in its leftPadding, if
* it is in the treeColumn of the associated TreeTableView.
* <p>
* It assumes that per-row graphics - including the graphic of the TreeItem, if any -
* is folded into the TreeTableRow graphic and patches its leftLabelPadding
* to account for the graphic width.
* <p>
*
* Note: TableRowSkinBase seems to be designed to cope with variations of row
* graphic - it has a method <code>graphicProperty()</code> that's always used
* internally when calculating offsets in the treeColumn.
* Subclasses override as needed, the layout code remains constant. The real
* problem is the TreeTableCell hard-codes the TreeItem as the only graphic
* owner.
*
*/
public static class DefaultTreeTableCellSkin<S, T> extends TreeTableCellSkin<S, T> {
/**
* @param treeTableCell
*/
public DefaultTreeTableCellSkin(TreeTableCell<S, T> treeTableCell) {
super(treeTableCell);
}
/**
* Overridden to adjust the padding returned by super for row graphic.
*/
@Override
protected double leftLabelPadding() {
double padding = super.leftLabelPadding();
padding += getRowGraphicPatch();
return padding;
}
/**
* Returns the patch for leftPadding if the tableRow has a graphic of
* its own.<p>
*
* Note: this implemenation is a bit whacky as it relies on super's
* handling of treeItems graphics offset. A cleaner
* implementation would override leftLabelPadding from scratch.
* <p>
* PENDING JW: doooooo it!
*
* @return
*/
protected double getRowGraphicPatch() {
if (!isTreeColumn()) return 0;
Node graphic = getSkinnable().getTreeTableRow().getGraphic();
if (graphic != null) {
double height = getCellSize();
// start with row's graphic
double patch = graphic.prefWidth(height);
// correct for super's having added treeItem's graphic
TreeItem<S> item = getSkinnable().getTreeTableRow().getTreeItem();
if (item.getGraphic() != null) {
double correct = item.getGraphic().prefWidth(height);
patch -= correct;
}
return patch;
}
return 0;
}
/**
* Checks and returns whether our cell is attached to a treeTableView/column
* and actually has a TreeItem.
* @return
*/
protected boolean isTreeColumn() {
if (getSkinnable().isEmpty()) return false;
TreeTableColumn<S, T> column = getSkinnable().getTableColumn();
TreeTableView<S> view = getSkinnable().getTreeTableView();
if (column.equals(view.getTreeColumn())) return true;
return view.getVisibleLeafColumns().indexOf(column) == 0;
}
}
}
/**
* Support custom graphic for Tree/TableRow. Here in particular a checkBox.
*
* <p>
* Basic idea: implement custom TreeTableRow that set's its graphic to the
* graphic/checkBox. Doesn't work: layout is broken, graphic appears
* over the text. All fine if we set the graphic to the TreeItem that's
* shown. Possible as long as the treeItem doesn't have a graphic of
* its own.
* <p>
* Basic problem:
* <li> TableRowSkinBase seems to be able to cope: has protected method
* graphicsProperty that should be implemented to return the graphic
* if any. That graphic is added to the children list and sized/located
* in layoutChildren.
* <li> are added the graphic/disclosureNode as needed before
* calling super.layoutChildren,
* <li> graphic/disclosure are placed inside the leftPadding of the tableCell
* that is the treeColumn
* <li> TreeTableCellSkin must cooperate in taking into account the graphic/disclosure
* when calculating its leftPadding
* <li> cellSkin is hard-coded to use the TreeItem's graphic (vs. the rowCell's)
*
* PENDING JW:
* <li>- would expect to not alter the scenegraph during layout (might lead to
* endless loops or not) but done frequently in core code
* <p>
*
* Outline of the solution as implemented:
* <li> need a TreeTableCell with a custom skin
* <li> override leftPadding in skin to add row graphic if available
* <li> need CheckBoxTreeTableRow that sets its graphic to checkBox (or a combination
* of checkBox and treeItem's)
* <li> need custom rowSkin that implements graphicProperty to return the row graphic
*
* @author Jeanette Winzenburg, Berlin
*
* @see DefaultTreeTableCell
* @see DefaultTreeTableCellSkin
*
*/
public class CheckBoxTreeTableRow<T> extends TreeTableRow<T> {
private CheckBox checkBox;
private ObservableValue<Boolean> booleanProperty;
private BooleanProperty indeterminateProperty;
public CheckBoxTreeTableRow() {
this(item -> {
if (item instanceof CheckBoxTreeItem<?>) {
return ((CheckBoxTreeItem<?>)item).selectedProperty();
}
return null;
});
}
public CheckBoxTreeTableRow(
final Callback<TreeItem<T>, ObservableValue<Boolean>> getSelectedProperty) {
this.getStyleClass().add("check-box-tree-cell");
setSelectedStateCallback(getSelectedProperty);
checkBox = new CheckBox();
checkBox.setAlignment(Pos.TOP_LEFT);
}
// --- selected state callback property
private ObjectProperty<Callback<TreeItem<T>, ObservableValue<Boolean>>>
selectedStateCallback =
new SimpleObjectProperty<Callback<TreeItem<T>, ObservableValue<Boolean>>>(
this, "selectedStateCallback");
/**
* Property representing the {@link Callback} that is bound to by the
* CheckBox shown on screen.
*/
public final ObjectProperty<Callback<TreeItem<T>, ObservableValue<Boolean>>> selectedStateCallbackProperty() {
return selectedStateCallback;
}
/**
* Sets the {@link Callback} that is bound to by the CheckBox shown on screen.
*/
public final void setSelectedStateCallback(Callback<TreeItem<T>, ObservableValue<Boolean>> value) {
selectedStateCallbackProperty().set(value);
}
/**
* Returns the {@link Callback} that is bound to by the CheckBox shown on screen.
*/
public final Callback<TreeItem<T>, ObservableValue<Boolean>> getSelectedStateCallback() {
return selectedStateCallbackProperty().get();
}
/** {@inheritDoc} */
@Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
TreeItem<T> treeItem = getTreeItem();
checkBox.setGraphic(treeItem == null ? null : treeItem.getGraphic());
setGraphic(checkBox);
// uninstall bindings
if (booleanProperty != null) {
checkBox.selectedProperty().unbindBidirectional((BooleanProperty)booleanProperty);
}
if (indeterminateProperty != null) {
checkBox.indeterminateProperty().unbindBidirectional(indeterminateProperty);
}
// install new bindings.
// this can only handle TreeItems of type CheckBoxTreeItem
if (treeItem instanceof CheckBoxTreeItem) {
CheckBoxTreeItem<T> cbti = (CheckBoxTreeItem<T>) treeItem;
booleanProperty = cbti.selectedProperty();
checkBox.selectedProperty().bindBidirectional((BooleanProperty)booleanProperty);
indeterminateProperty = cbti.indeterminateProperty();
checkBox.indeterminateProperty().bindBidirectional(indeterminateProperty);
} else {
throw new IllegalStateException("item must be CheckBoxTreeItem");
}
}
}
@Override
protected Skin<?> createDefaultSkin() {
return new CheckBoxTreeTableRowSkin<>(this);
}
public static class CheckBoxTreeTableRowSkin<S> extends TreeTableRowSkin<S> {
protected ObjectProperty<Node> checkGraphic;
/**
* @param control
*/
public CheckBoxTreeTableRowSkin(TreeTableRow<S> control) {
super(control);
}
/**
* Note: this is implicitly called from the constructor of LabeledSkinBase.
* At that time, checkGraphic is not yet instantiated. So we do it here,
* still having to create it at least twice. That'll be a problem if
* anybody would listen to changes ...
*/
@Override
protected ObjectProperty<Node> graphicProperty() {
if (checkGraphic == null) {
checkGraphic = new SimpleObjectProperty<Node>(this, "checkGraphic");
}
CheckBoxTreeTableRow<S> treeTableRow = getTableRow();
if (treeTableRow.getTreeItem() == null) {
checkGraphic.set(null);
} else {
checkGraphic.set(treeTableRow.getGraphic());
}
return checkGraphic;
}
protected CheckBoxTreeTableRow<S> getTableRow() {
return (CheckBoxTreeTableRow<S>) super.getSkinnable();
}
}
@SuppressWarnings("unused")
private static final Logger LOG = Logger
.getLogger(CheckBoxTreeTableRow.class.getName());
}
原答案:hack!
里面有一行疯狂的代码:
treeItem.setGraphics(checkBox);
这真的很古怪,最终可能会造成严重破坏 - 它是围绕 TreeTableRowSkin 中的布局故障进行的黑客攻击,由于某种原因(我无法挖掘)无法将图形集定位到单元格。无法使其在 return 直接在其 graphicProperty()
中设置复选框的自定义 CheckBoxTreeTableRowSkin 中运行 - 所以我们现在就开始吧。
/**
* @author Jeanette Winzenburg, Berlin
*/
public class CheckBoxTreeTableRowHack<T> extends TreeTableRow<T> {
private CheckBox checkBox;
private ObservableValue<Boolean> booleanProperty;
private BooleanProperty indeterminateProperty;
public CheckBoxTreeTableRowHack() {
setSelectedStateCallback(item -> {
if (item instanceof CheckBoxTreeItem<?>) {
return ((CheckBoxTreeItem<?>)item).selectedProperty();
}
return null;
});
this.checkBox = new CheckBox();
// something weird going on with layout
checkBox.setAlignment(Pos.TOP_LEFT);
}
// --- selected state callback property
private ObjectProperty<Callback<TreeItem<T>, ObservableValue<Boolean>>>
selectedStateCallback =
new SimpleObjectProperty<Callback<TreeItem<T>, ObservableValue<Boolean>>>(
this, "selectedStateCallback");
/**
* Property representing the {@link Callback} that is bound to by the
* CheckBox shown on screen.
*/
public final ObjectProperty<Callback<TreeItem<T>, ObservableValue<Boolean>>> selectedStateCallbackProperty() {
return selectedStateCallback;
}
/**
* Sets the {@link Callback} that is bound to by the CheckBox shown on screen.
*/
public final void setSelectedStateCallback(Callback<TreeItem<T>, ObservableValue<Boolean>> value) {
selectedStateCallbackProperty().set(value);
}
/**
* Returns the {@link Callback} that is bound to by the CheckBox shown on screen.
*/
public final Callback<TreeItem<T>, ObservableValue<Boolean>> getSelectedStateCallback() {
return selectedStateCallbackProperty().get();
}
/** {@inheritDoc} */
@Override
public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
//
TreeItem<T> treeItem = getTreeItem();
// PENDING JW: this is nuts but working .. certainly will pose problems
// when re-using the cell
treeItem.setGraphic(checkBox);
// this is what CheckBoxTreeCell does, setting the graphic
// of the tableRow confuses the layout
// checkBox.setGraphic(treeItem == null ? null : treeItem.getGraphic());
// setGraphic(checkBox);
// uninstall bindings
if (booleanProperty != null) {
checkBox.selectedProperty().unbindBidirectional((BooleanProperty)booleanProperty);
}
if (indeterminateProperty != null) {
checkBox.indeterminateProperty().unbindBidirectional(indeterminateProperty);
}
// install new bindings.
// We special case things when the TreeItem is a CheckBoxTreeItem
if (treeItem instanceof CheckBoxTreeItem) {
CheckBoxTreeItem<T> cbti = (CheckBoxTreeItem<T>) treeItem;
booleanProperty = cbti.selectedProperty();
checkBox.selectedProperty().bindBidirectional((BooleanProperty)booleanProperty);
indeterminateProperty = cbti.indeterminateProperty();
checkBox.indeterminateProperty().bindBidirectional(indeterminateProperty);
} else {
throw new IllegalStateException("item must be CheckBoxTreeItem");
}
}
}
}
// usage: in the example add
treeTableView.setRowFactory(f -> new CheckBoxTreeTableRowHack<>());