This discussion is archived
11 Replies Latest reply: Nov 28, 2012 5:03 PM by James_D RSS

Replacing row content in GridPanes

restenbro Newbie
Currently Being Moderated
I have a GridPane where I want to be able to seamlessly add rows, delete rows, and change row indices.

For example, I want to be able to "push" a new item in at the top of the GridPane, subsequently pushing all other rows down by one index.

The GridPane class does not have any replace() method or interface to my knowledge. I can reference the individual nodes in the Pane, so I suspect one possible solution would be to use getRowIndex(Node), and then update all the nodes on that row directly (all I need is a Label.setText). However, due to the size of the Pane, it would not really be desirable to run all this code on the application thread. For a MxN pane, I would have to loop through N lists of M nodes, looking for the right row, then updating, and repeat this for all nodes, for a total of MxN updates.

Another solution might be to just redraw the entire GridPane with updated information, dump the current scene, and redraw with a new scene containing the updated GridPane.

So it can be done, but I think both suggested approaches seem a bit kludgy, and was wondering if anybody could suggest a more elegant solution.
  • 1. Re: Replacing row content in GridPanes
    MiPa Pro
    Currently Being Moderated
    I'd probably keep all grid nodes in a separate array and when I want to make a change I'd first change the array and then I'd loop over the whole array and update the grid constraints of each node. This should work for even larger Ms and Ns.
  • 2. Re: Replacing row content in GridPanes
    973901 Newbie
    Currently Being Moderated
    Thanks to the fact that constraints are held in the node and not the pane, you will unfortunately have to iterate through every child node no matter how you slice it. However, even though you have to do it manually, it's a rather simple operation.
    public void insert(int index, Node...toInsert)
    {
         ObservableList<Node> children = gridPane.getChildren();
         for (int i = children.size() - 1; i >= 0; --i)
              if (GridPane.getRowIndex(children.get(i)) >= index)
                   GridPane.setRowIndex(children.get(i), GridPane.getRowIndex(children.get(i)) + 1);
         gridPane.addRow(index, toInsert);
    }
    
    public void replace(int index, Node...toReplace)
    {
         ArrayList<Node> childrenInRow = new ArrayList<Node>();
         ObservableList<Node> children = gridPane.getChildren();
         for (int i = 0; i < children.size(); ++i)
              if (GridPane.getRowIndex(children.get(i)) == index)
              {
                   childrenInRow.add(children.get(i));
                   children.remove(i);
              }
         gridPane.addRow(index, toReplace);
         //do something with childrenInRow if needed.
    }
    This does not take that long to complete under normal circumstances, but it seems like you have a ton of nodes. If it takes too long to complete, kick it to a new thread; redrawing the scene is probably not necessary. A non-application thread will be able to update the node constraints without being bothered, and you can toss it back to the application thread to do the final insert once the restructure is done. If you iterate through the children starting at the end, provided you don't have vertical padding, nodes won't overlap in the middle and there won't even be a flicker.
    public void insert(final int index, final Node...toInsert)
    {
         new Thread(new Runnable()
         {
              public void run()
              {
                   ObservableList<Node> children = gridPane.getChildren();
                   for (int i = children.size() - 1; i >= 0; --i)
                        if (GridPane.getRowIndex(children.get(i)) >= index)
                             GridPane.setRowIndex(children.get(i), GridPane.getRowIndex(children.get(i)) + 1);
                   Platform.runLater(new Runnable()
                   {
                        public void run()
                        {
                             gridPane.addRow(index, toInsert);
                        }
                   });
              }
         }).start();
    }
    If none of that works for you, adding the nodes to a new GridPane in the background and replacing the whole thing might be the only feasible option.

    All that said, I can't help thinking that GridPane might not be the best fit for you. If you used HBox's nested in a VBox, you could do insert/replace operations effortlessly, as VBox supports it by default. You could add min/max/preferred width property binding to that if node sizing is an issue. In the end only you can make that determination though.
  • 3. Re: Replacing row content in GridPanes
    973901 Newbie
    Currently Being Moderated
    MiPa:
    I'd probably keep all grid nodes in a separate array and when I want to make a change I'd first change the array and then I'd loop over the whole array and update the grid constraints of each node.
    One quick note: the logical order in which nodes are saved in the list doesn't impact the layout; only the constraints specific to each node are considered. Reordering the list to match what you see visually on the screen would be wasted effort; there shouldn't be any reason to keep a separate list unless you are buffering a replacement set of nodes to swap in later.
  • 4. Re: Replacing row content in GridPanes
    MiPa Pro
    Currently Being Moderated
    I should have said "two dimensional array or matrix" to be clearer. I think that makes the bookkeeping easier where each node is located in the grid because, as you said, the location is only held in the grid constraints.
  • 5. Re: Replacing row content in GridPanes
    restenbro Newbie
    Currently Being Moderated
    The code above fails in my case;

    java.lang.IllegalArgumentException: Children: duplicate children added: parent = Grid hgap=2.0, vgap=2.0, alignment=TOP_LEFT
         at javafx.scene.Parent$1.onProposedChange(Parent.java:307)
         at com.sun.javafx.collections.VetoableObservableList.addAll(VetoableObservableList.java:106)
         at com.sun.javafx.collections.ObservableListWrapper.addAll(ObservableListWrapper.java:160)
         at com.sun.javafx.collections.ObservableListWrapper.addAll(ObservableListWrapper.java:309)
         at javafx.scene.layout.GridPane.addRow(GridPane.java:931)

    Anyway, when I insert, I always want to insert on index 1. Index 0 is reserved for headers.

    I'm currently experimenting to see if I find a workable solution, will post if I do.
  • 6. Re: Replacing row content in GridPanes
    973901 Newbie
    Currently Being Moderated
    Well, yeah, you've gotta add a new control. You can't add nodes that already exist in the pane. I assumed the arguments passed to insert() would be new nodes.
  • 7. Re: Replacing row content in GridPanes
    restenbro Newbie
    Currently Being Moderated
    They are new nodes. To clarify, here is what is basically my call to insert():

    final Label moduleName = new Label(module.getName());
    final Label signalName = new Label(signal.getName());
    final Label currVal = new Label(Double.toString(signal.getValue()));
    final Label prevVal = new Label(Double.toString(module.getPrevSignalValue(index)));
    final Label time = new Label(Double.toString(currentTimeStamp));

    Platform.runLater(new Runnable() {
    @Override
    public void run() {
    insert(1, moduleName, signalName, currVal, prevVal, time);
    }
    });
  • 8. Re: Replacing row content in GridPanes
    restenbro Newbie
    Currently Being Moderated
    It seems GridPane does not quite work this way. It doesn't wipe the existing row content with a call to addRow, it just continues adding columns to that row.

    So I don't think this approach will work.
  • 9. Re: Replacing row content in GridPanes
    James_D Guru
    Currently Being Moderated
    [Edited: changed my mind on the best way to do this.]

    The javadocs for GridPane explicitly state that nodes may overlap in the grid, so there's no harm in having nodes temporarily have the same row and column constraints. Since you seem to be adding near the "top" and consequently will be needing to update the row index for almost all the nodes anyway, there is little or no performance gain to only iterating through the nodes "below" the new row. So you lose nothing by using Kyle's approach instead of keeping a "cache" of the child nodes of the GridPane.

    This works just fine. The performance seems ok up to a couple of thousand nodes; after about 4000 nodes it gets a bit sluggish so if you have more than 2000 or so you may need a more sophisticated approach.
    import javafx.application.Application;
    import javafx.event.ActionEvent;
    import javafx.event.EventHandler;
    import javafx.scene.Node;
    import javafx.scene.Scene;
    import javafx.scene.control.Button;
    import javafx.scene.control.Label;
    import javafx.scene.control.ScrollPane;
    import javafx.scene.layout.BorderPane;
    import javafx.scene.layout.GridPane;
    import javafx.scene.layout.Priority;
    import javafx.stage.Stage;
    
    
    public class MutableGridPane extends Application {
      
      private final int NUM_COLS = 8 ;
    
      @Override
      public void start(Stage primaryStage) throws Exception {
        final GridPane gridPane = new GridPane();
        final ScrollPane scrollPane = new ScrollPane();
        scrollPane.setContent(gridPane);
        addHeaderRow(gridPane);
        
        // Add 250 rows to test performance:
        for (int i=0; i<250; i++) {
          addNewRow(gridPane, 1);
        }
        
        final Button addButton = new Button("Add a row");
        
        addButton.setOnAction(new EventHandler<ActionEvent>() {
          @Override
          public void handle(ActionEvent event) {
            addNewRow(gridPane, 1);
          }
        });
        
        final BorderPane root = new BorderPane();
        root.setCenter(scrollPane);
        root.setBottom(addButton);
        
        Scene scene = new Scene(root, 600, 600);
        primaryStage.setScene(scene);
        primaryStage.show();
      }
      
      private void addHeaderRow(GridPane gridPane) {
        for (Node node : gridPane.getChildren()) {
          GridPane.setRowIndex(node, GridPane.getRowIndex(node)+1);
        }
        for (int colIndex=0; colIndex<NUM_COLS; colIndex++) {
          Label header = new Label("Header "+(colIndex+1));
          header.setStyle("-fx-background-color: lightgray;");
          header.setMaxWidth(Double.POSITIVE_INFINITY);
          gridPane.add(header, colIndex, 0);
        }
      }
      
      private void addNewRow(GridPane gridPane, int rowIndex) {
        int numRows = 1 ;
        for (Node node : gridPane.getChildren()) {
          int currentRow = GridPane.getRowIndex(node);
          if (currentRow >= rowIndex) {
            GridPane.setRowIndex(node, currentRow+1);
            if (currentRow+1 > numRows) {
              numRows = currentRow + 1;
            }
          }
        }
        String color = numRows % 2 == 0 ? "lightskyblue" : "cornflowerblue" ; 
        for (int i=0; i<NUM_COLS; i++) {
          Label label = new Label(String.format("Label [%d, %d]", numRows, i+1)); 
          label.setStyle("-fx-background-color: "+color);
          label.setMaxWidth(Double.POSITIVE_INFINITY);
          GridPane.setHgrow(label, Priority.ALWAYS);
          gridPane.add(label, i, rowIndex);
        }
      }
    
      public static void main(String[] args) { launch(args); }
    
    }
  • 10. Re: Replacing row content in GridPanes
    restenbro Newbie
    Currently Being Moderated
    The above works just fine (with minor modification), thank you.

    I had to add a check to see if the Node extracted from getChildren() was a control or not, or I'd get NullPointers, maybe from trying to extract the row index of the other objects that gets added to the children of the GridPane (seems to be one Group pr. row).

    So I just did this:

    for (Node n : pane.getChildren()) {
    if (n instance of Control) {
    ... get row index and start swapping indices etc ...

    and it worked like a charm.
  • 11. Re: Replacing row content in GridPanes
    James_D Guru
    Currently Being Moderated
    It might be safer to do
    if (GridPane.getRowIndex(node) != null) {
      int currentRow = GridPane.getRowIndex(node);
      // ... etc
    }
    (or something similar, this is not quite optimal). It's possible for any node to have a null value set for its GridPane constraints.

Legend

  • Correct Answers - 10 points
  • Helpful Answers - 5 points