Javafx Tableview Keep selected row in current view

2019-03-27 13:22发布

I am using javafx tableview with active sorting and insertion of a new row every millisecond...

I want this functionality:

If I have selected a row then it should remain visible (that is should not go up or down from current visible portion of my table) when new rows are inserted.

2条回答
Emotional °昔
2楼-- · 2019-03-27 14:07

This may be the long away around it and it's kind of hackish, but it worked for me when I needed to do something similar.

The gist of the answer is that you need to get access to the VirtualFlow member of the TableViewSkin. That's not as simple as is sounds because the skin isn't loaded until the CSS is parsed. I added a Listener to the skinProperty of the TableView and was able to get the VirtualFlow that way.

tableView.skinProperty().addListener(new ChangeListener<Skin>()
{
   @Override
   public void changed(ObservableValue<? extends Skin> ov, Skin t, Skin t1)
   {
      if (t1 == null) { return; }

      TableViewSkin tvs = (TableViewSkin)t1;
      ObservableList<Node> kids = tvs.getChildrenUnmodifiable();

      if (kids == null || kids.isEmpty()) { return; }
      flow = (VirtualFlow)kids.get(1);
   }
});

After you have the VirtualFlow, you can listen to the table's ObservableList for changes and check to make sure the selected item is still in the viewport.

countries.addListener(new ListChangeListener<Country>()
{
   @Override
   public void onChanged(ListChangeListener.Change<? extends Country> change)
   {
      while (change.next())
      {
          if (change.wasAdded())
          {
             if (flow == null) { return; }
             int first = flow.getFirstVisibleCell().getIndex();
             int last = flow.getLastVisibleCell().getIndex();
             int selected = tableView.getSelectionModel().getSelectedIndex();

             if (selected < first || selected > last)
             {
                flow.show(selected);
             }
          }
       }
   }
});

There will still be a bit of bookkeeping to manage. For instance, if you wanted to place focus back on the table. Also, it's worth noting that the VirtualFlow isn't strictly bound by the visible rectangle of the table, so an item may be considered visible even if it is just outside the viewport. You can study the VirtualFlow for more details.

Here's a SSCCE.

JavaFXApplication21.java:

package javafxapplication21;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class JavaFXApplication21 extends Application
{
    @Override
    public void start(Stage stage) throws Exception
    {
        Parent root = FXMLLoader.load(getClass().getResource("Sample.fxml"));

        Scene scene = new Scene(root);

        stage.setScene(scene);
        stage.show();
    }

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

Sample.fxml:

<?xml version="1.0" encoding="UTF-8"?>

<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<AnchorPane id="AnchorPane" prefHeight="200.0" prefWidth="200.0" xmlns:fx="http://javafx.com/fxml" fx:controller="javafxapplication21.SampleController">
  <children>
    <ToolBar AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
      <items>
        <Button fx:id="insertBtn" mnemonicParsing="false" text="Insert" />
      </items>
    </ToolBar>
    <TableView fx:id="tableView" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="31.0">
      <columns>
        <TableColumn prefWidth="100.0" text="Country" fx:id="countryColumn" />
        <TableColumn prefWidth="100.0" text="Capital" fx:id="capitalColumn" />
      </columns>
    </TableView>
  </children>
</AnchorPane>

SampleController.java:

package javafxapplication21;

import com.sun.javafx.scene.control.skin.TableViewSkin;
import com.sun.javafx.scene.control.skin.VirtualFlow;
import java.net.URL;
import java.util.*;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.*;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;

public class SampleController implements Initializable
{
    @FXML private Button insertBtn;
    @FXML private TableView<Country> tableView;
    @FXML private TableColumn<Country, String> countryColumn;
    @FXML private TableColumn<Country, String> capitalColumn;

    private VirtualFlow flow;

    private ObservableList<Country> countries =
            FXCollections.observableArrayList();
    private List<Country> insertList = new ArrayList<>();

    public SampleController()
    {
        countries.addAll(new Country("AG", "Buenos Aires"),
                new Country("AU", "Vienna"),
                new Country("BY", "Minsk"),
                new Country("CO", "Bogota"),
                new Country("EG", "Cairo"));

        insertList.add(new Country("ZI", "Harare"));
        insertList.add(new Country("UK", "London"));
        insertList.add(new Country("TW", "Taipei"));
    }

    @Override
    public void initialize(URL url, ResourceBundle rb)
    {
        countryColumn.setCellValueFactory(
                new PropertyValueFactory<Country, String>("name"));
        capitalColumn.setCellValueFactory(
                new PropertyValueFactory<Country, String>("capital"));

        tableView.setItems(countries);

        tableView.skinProperty().addListener(new ChangeListener<Skin>()
        {
            @Override
            public void changed(ObservableValue<? extends Skin> ov,
                Skin t, Skin t1)
            {
                if (t1 == null) { return; }

                TableViewSkin tvs = (TableViewSkin)t1;
                ObservableList<Node> kids = tvs.getChildrenUnmodifiable();

                if (kids == null || kids.isEmpty()) { return; }

                flow = (VirtualFlow)kids.get(1);
            }
        });

        insertBtn.setOnAction(new EventHandler<ActionEvent>()
        {
            @Override
            public void handle(ActionEvent t)
            {
                if (!insertList.isEmpty())
                {
                    countries.add(2, insertList.get(0));
                    insertList.remove(0);
                }
            }
        });

        countries.addListener(new ListChangeListener<Country>()
        {
            @Override
            public void onChanged(ListChangeListener.Change<? extends Country> change)
            {
                while (change.next())
                {
                    if (change.wasAdded())
                    {
                        if (flow == null) { return; }
                        int first = flow.getFirstVisibleCell().getIndex();
                        int last = flow.getLastVisibleCell().getIndex();
                        int selected = tableView.getSelectionModel().getSelectedIndex();

                        if (selected < first || selected > last)
                        {
                            flow.show(selected);
                        }
                    }
                }
            }
        });
    }

    public class Country
    {
        private SimpleStringProperty name;
        private SimpleStringProperty capital;

        public Country(String name, String capital)
        {
            this.name = new SimpleStringProperty(name);
            this.capital = new SimpleStringProperty(capital);
        }

        public SimpleStringProperty nameProperty() { return name; }
        public SimpleStringProperty capitalProperty() { return capital; }
    }
}
查看更多
Bombasti
3楼-- · 2019-03-27 14:10

I also had the same problem .You can use cellEventHandler class to which implemets EventHandler interface to fired when a cell is selcted in table and set last selected row index to a variable. The class is as followed.

    public class CellEventHandler implements EventHandler<MouseEvent>

{
     CellEventHandler()
    {
    }

    @Override
    public void handle(MouseEvent t)
    {
        TableCell c;
        c = (TableCell) t.getSource();
        c.getIndex();
        PanelController.selectedItem = c.getIndex();
    }
}`

Then your table class render of the table in PanelController Class should be as followed.

typeCol.setCellFactory(new Callback<TableColumn<tableModel, String>, TableCell<tableModel, String>>() 
    {
        @Override
        public TableCell<tableModel, String> call(TableColumn<tableModel, String> tableColumn)
        {
            LableCell lableCell = new LableCell(Pos.CENTER, true);
            lableCell.addEventFilter(MouseEvent.MOUSE_CLICKED, new CellEventHandler());
            return lableCell;
        }
    });

The "selectedItem" field should be declared in the panelController class as static field and then that "selectedItem " should set as selected in the table when you refresh the table row data.

 myTable.getSelectionModel().select(selectedItem);    
查看更多
登录 后发表回答