在 JavaFx 中自动调整作为图形分配给单元格的形状

Auto-sizing a Shape Assigned as a Graphic to a Cell in JavaFx

在工作中,我创建了一个 TableView,它需要让特定的单元格同时从一种颜色闪烁到另一种颜色。使用 Rectangles、FillTransitions 和 ParallelTransition 相对容易,如下面的玩具示例所示。将矩形分配给 FillTransition 后,我将 TableCell 的图形设置为矩形。然后我只需要 add/remove 来自 ParallelTransition 的 FillTransition 取决于单元格是否应该闪烁。

然而,我遇到很多困难的地方是想出一种方法来将矩形缩放到包含它作为图形的 TableCell 的大小。我遇到的问题是 TableCell 总是会自行调整大小,使其边界和矩形边界之间为空 space。

我不得不以一种非常乏味和迂回的方式解决这个问题:我不得不调用 setFixedCellSize 将 table 的单元格高度固定为我的矩形的高度,将矩形重新定位到通过调用它的 setTranslateX/Y 进行反复试验,并将列的最小宽度和最小高度设置为略小于我设置的矩形宽度和高度。它解决了问题,但我希望能有一些不那么乏味和烦人的东西。

我原以为可以通过对单元格执行以下一项或多项操作来避免这种情况:

遗憾的是,其中 none 产生了明显的效果...

我的问题是三部分:

  1. 是否有更好的方法来调整指定为单元格的图形的形状以填充单元格的边界?

  2. 为什么上述三种方法中的 none 对我的情况有任何影响,它们的实际目的是什么?他们只申请用 setShape() 分配的形状而不是 setGraphic() 吗?

  3. JavaFx 不支持设置 Region 子类以外的节点的首选宽度或高度是否有任何正当理由?自动调整似乎应该对层次结构中的所有节点都通用,并且任何父节点都应该能够在必要时决定其子节点的大小似乎很直观。

import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import javafx.animation.Animation;
import javafx.animation.FillTransition;
import javafx.animation.ParallelTransition;
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderStroke;
import javafx.scene.layout.BorderStrokeStyle;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;


public class FlashingPriorityTable extends Application {

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

  @Override
  public void start(Stage primaryStage) throws Exception {

    // periodically add prioritized items to an observable list
    final ObservableList<PItem> itemList = FXCollections.observableArrayList();
    class ItemAdder {
      private int state, count = 0; private final int states = 3;
      public synchronized void addItem() {
        state = count++ % states;
        PItem item;
        if(state == 0)
          item = new PItem(Priority.LOW, count, "bob saget");
        else if(state == 1)
          item = new PItem(Priority.MEDIUM, count, "use the force");
        else
          item = new PItem(Priority.HIGH, count, "one of us is in deep trouble");
        itemList.add(item);
      }
    };
    final ItemAdder itemAdder = new ItemAdder();
    Executors.newScheduledThreadPool(1).scheduleAtFixedRate(
        () -> itemAdder.addItem(),
        0, // initial delay
        1, // period
        TimeUnit.SECONDS); // time unit

    // set up a table view bound to the observable list
    final TableColumn<PItem, Priority> priCol = new TableColumn<>("Priority");
    priCol.setCellValueFactory(new PropertyValueFactory<PItem, Priority>("priority"));
    priCol.setCellFactory((col) -> new PriorityCell()); // create a blinking cell
    priCol.setMinWidth(50);
    priCol.setMaxWidth(50);

    final TableColumn<PItem, Integer> indexCol = new TableColumn<>("Index");
    indexCol.setCellValueFactory(new PropertyValueFactory<PItem, Integer>("index"));
    indexCol.setCellFactory((col) -> makeBorderedTextCell());

    final TableColumn<PItem, String> descriptionCol = new TableColumn<>("Description");
    descriptionCol.setCellValueFactory(new PropertyValueFactory<PItem, String>("description"));
    descriptionCol.setCellFactory((col) -> makeBorderedTextCell());
    descriptionCol.setMinWidth(300);

    final TableView<PItem> table = new TableView<>(itemList);
    table.getColumns().setAll(priCol, indexCol, descriptionCol);
    table.setFixedCellSize(25);

    // display the table view
    final Scene scene = new Scene(table);
    primaryStage.setScene(scene);
    primaryStage.show();
  }

  // render a simple cell text and border
  private <T> TableCell<PItem, T> makeBorderedTextCell() {
    return new TableCell<PItem, T>() {
      @Override protected void updateItem(T item, boolean empty) {
        super.updateItem(item, empty);
        if(item == null || empty) {
          setText(null);
        } else {
          setBorder(new Border(new BorderStroke(Color.GREEN, BorderStrokeStyle.SOLID, null, null)));
          setText(item.toString());
        }
      }
    };
  }

  /* for cells labeled as high priority, render an animation that blinks (also include a border) */
  public static class PriorityCell extends TableCell<PItem, Priority> {
    private static final ParallelTransition pt = new ParallelTransition();
    private final Rectangle rect = new Rectangle(49.5, 24);
    private final FillTransition animation = new FillTransition(Duration.millis(100), rect);
    public PriorityCell() {
      rect.setTranslateX(-2.75);
      rect.setTranslateY(-2.7);
      animation.setCycleCount(Animation.INDEFINITE); animation.setAutoReverse(true); }
    @Override
    protected void updateItem(Priority priority, boolean empty) {
      super.updateItem(priority, empty);
      if(priority == null || empty) {
        setGraphic(null);
        return;
      }
      setGraphic(rect);
      setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
      setBorder(new Border(new BorderStroke(Color.GREEN, BorderStrokeStyle.SOLID, null, null)));
      if(priority == Priority.HIGH) {
        if(!pt.getChildren().contains(animation)) {
          animation.setFromValue(Color.BLACK);
          animation.setToValue(priority.getColor());
          animation.setShape(rect);
          pt.getChildren().add(animation);
          pt.stop(); pt.play();
        }
      } else {
        if(pt.getChildren().contains(animation)) {
          pt.getChildren().remove(animation);
          pt.stop(); pt.play();
        }
        rect.setFill(priority.getColor());
      }
    }
  }

  /* an item that has a priority assigned to it */
  public static class PItem {
    private ObjectProperty<Priority> priority = new SimpleObjectProperty<>();
    private IntegerProperty index = new SimpleIntegerProperty();
    private StringProperty description = new SimpleStringProperty();

    public PItem(Priority priority, Integer index, String description) {
      setPriority(priority); setIndex(index); setDescription(description);
    }

    public void setPriority(Priority priority_) { priority.set(priority_); }
    public Priority getPriority() { return priority.get(); }

    public void setIndex(int index_) { index.set(index_); }
    public Integer getIndex() { return index.get(); }

    public void setDescription(String description_) { description.set(description_); }
    public String getDescription() { return description.get(); }
  }

  /* a priority */
  public enum Priority {
    HIGH(Color.RED), MEDIUM(Color.ORANGE), LOW(Color.BLUE);
    private final Color color;
    private Priority(Color color) { this.color = color; }
    public Color getColor() { return color; }
  }
}

关于:

the TableCell would always resize itself to have empty space between its boundaries and the boundaries of the rectangle.

这是因为根据 modena.css:

,单元格默认有 2 像素的填充
.table-cell {
    -fx-padding: 0.166667em; /* 2px, plus border adds 1px */
    -fx-cell-size: 2.0em; /* 24 */
}

摆脱这个空白 space 的一个简单方法就是覆盖它:

@Override
protected void updateItem(Priority priority, boolean empty) {
    super.updateItem(priority, empty);
    ...
    setGraphic(rect);
    setStyle("-fx-padding: 0;");
    ...
}

您还提到的下一个问题是自动调整大小。根据 JavaDoc,对于 Node.isResizable():

If this method returns true, then the parent will resize the node (ideally within its size range) by calling node.resize(width,height) during the layout pass. All Regions, Controls, and WebView are resizable classes which depend on their parents resizing them during layout once all sizing and CSS styling information has been applied. If this method returns false, then the parent cannot resize it during layout (resize() is a no-op) and it should return its layoutBounds for minimum, preferred, and maximum sizes. Group, Text, and all Shapes are not resizable and hence depend on the application to establish their sizing by setting appropriate properties (e.g. width/height for Rectangle, text on Text, and so on). Non-resizable nodes may still be relocated during layout.

显然,Rectangle 是不可调整大小的,但这并不意味着您不能调整它的大小:如果布局不适合您,您需要自行处理.

因此,一个简单的解决方案可能是将矩形的尺寸绑定到单元格的尺寸(单元格边框减去 2 个像素):

private final Rectangle rect = new Rectangle();

@Override
protected void updateItem(Priority priority, boolean empty) {
    super.updateItem(priority, empty);
    if(priority == null || empty) {
      setGraphic(null);
      return;
    }
    setGraphic(rect);
    setStyle("-fx-padding: 0;");
    rect.widthProperty().bind(widthProperty().subtract(2));
    rect.heightProperty().bind(heightProperty().subtract(2));
    ...
}

请注意,您不需要平移矩形,也不需要固定单元格的大小和列的宽度,除非您想给它一个固定的大小。

另请注意,setShape() 旨在更改单元格形状,默认情况下它已经是一个矩形。

这可能会回答您的前两个问题。对于第三个,有时您希望节点始终可调整大小...但如果是这种情况,我们将遇到相反的问题,试图使它们受到约束...