This discussion is archived
3 Replies Latest reply: Jul 4, 2013 6:38 PM by eno g. - oracle RSS

Thumbnail view of a TableView populated by an ObservableList

eno g. - oracle Newbie
Currently Being Moderated

Hello,

 

I'm populating a TableView dynamically with objects in an ObservableList.  I'd like to mark some rows with varying colors depending on their content and would like to display the marked rows in a thumbnail view of the TableView.  The idea is to basically do something like the awesome Beyond Compare app does in the top left side of their window (see http://www.scootersoftware.com/images/TextCompare.png). A click on the thumbnail would basically scroll your tableview to that location.  The square on the thumbnail represents what data is displayed on the screen and is sized proportionally with how long the TableView is and how many rows can be displayed on the screen at that time.

 

Perhaps I can bind a ListView to the same ObservableList that's populating the TableView but just show an image (thin colored line) for each row of the TableView that has been marked.  Any ideas on how I could achieve something like this?

 

Thanks

  • 1. Re: Thumbnail view of a TableView populated by an ObservableList
    James_D Guru
    Currently Being Moderated

    In JavaFX8 you can get a filtered list from the ObservableList, and use that to populate the ListView (or whatever you decide to display). Just call table.getItems().filtered(...).

     

    In JavaFX 2.x, you can create an new ObservableList for your filtered data. Create a ListChangeListener and register it with table.getItems() and update your filtered data as necessary when the list changes. The tricky part is to update the filtered list when any relevant properties in the elements in the list change. For this you can create an observable list with an extractor.

     

    Here's an example. Note the way the observable list is created in the createData() method.

     

     

    import java.util.Arrays;
    import java.util.List;
    import javafx.application.Application;
    import javafx.beans.Observable;
    import javafx.beans.property.BooleanProperty;
    import javafx.beans.property.SimpleBooleanProperty;
    import javafx.beans.property.SimpleStringProperty;
    import javafx.beans.property.StringProperty;
    import javafx.beans.value.ChangeListener;
    import javafx.beans.value.ObservableValue;
    import javafx.collections.FXCollections;
    import javafx.collections.ListChangeListener;
    import javafx.collections.ObservableList;
    import javafx.scene.Scene;
    import javafx.scene.control.ListView;
    import javafx.scene.control.TableColumn;
    import javafx.scene.control.TableRow;
    import javafx.scene.control.TableView;
    import javafx.scene.control.cell.CheckBoxTableCell;
    import javafx.scene.control.cell.PropertyValueFactory;
    import javafx.scene.layout.BorderPane;
    import javafx.stage.Stage;
    import javafx.util.Callback;
    public class TableWithThumbnail extends Application {
      @Override
      public void start(Stage primaryStage) {
        final BorderPane root = new BorderPane();
        final TableView<Player> table = new TableView<Player>();
        table.setItems(createData());
        final TableColumn<Player, String> firstNameColumn = new TableColumn<>("First Name");
        final TableColumn<Player, String> lastNameColumn = new TableColumn<>("Last Name");
        final TableColumn<Player, Boolean> injuredColumn = new TableColumn<>("Injured");
        firstNameColumn.setCellValueFactory(new PropertyValueFactory<Player, String>("firstName"));
        lastNameColumn.setCellValueFactory(new PropertyValueFactory<Player, String>("lastName"));
        injuredColumn.setCellValueFactory(new PropertyValueFactory<Player, Boolean>("injured"));
        injuredColumn.setCellFactory(CheckBoxTableCell.forTableColumn(injuredColumn));
        injuredColumn.setEditable(true);
       
        table.setEditable(true);
        table.getColumns().addAll(Arrays.asList(firstNameColumn, lastNameColumn, injuredColumn));
       
        table.setRowFactory(new Callback<TableView<Player>, TableRow<Player>>() {
          @Override
          public TableRow<Player> call(TableView<Player> table) {
            return new PlayerTableRow();
          }
        });
       
        // Create a filtered list: only the injured players appear:
        final ObservableList<Player> injuredList = FXCollections.observableArrayList();
        buildInjuredList(table.getItems(), injuredList);
        table.getItems().addListener(new ListChangeListener<Player>() {
          @Override
          public void onChanged(ListChangeListener.Change<? extends Player> cahnge) {
            // Just rebuild injured list from scratch.
            // Might need to be more efficient: e.g. examine change(s) and update injuredList accordingly
            injuredList.clear();
            buildInjuredList(table.getItems(), injuredList);
          }
        });
       
        ListView<Player> injuredListView = new ListView<>(injuredList);
        // select and scroll in the table when selection changes in the list view:
        injuredListView.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<Player>() {
          @Override
          public void changed(ObservableValue<? extends Player> observable, Player oldSelection,
              Player newSelection) {
            if (newSelection != null) {
              final int index = table.getItems().indexOf(newSelection);
              table.scrollTo(index);
              table.getSelectionModel().select(index);
            }
          }
        });
       
        root.setCenter(table);
        root.setRight(injuredListView);
       
        final Scene scene = new Scene(root, 800, 250);
        scene.getStylesheets().add(getClass().getResource("tableWithThumbnail.css").toExternalForm());
        primaryStage.setScene(scene);
        primaryStage.show();   
      }
      private void buildInjuredList(final ObservableList<Player> fullList, ObservableList<Player> injuredList) {
        for (Player player : fullList) {
          if (player.isInjured()) {
            injuredList.add(player);
          }
        }
      }
      public static void main(String[] args) {
        launch(args);
      }
      private ObservableList<Player> createData() {
        List<Player> players = Arrays.asList(
            new Player("Hugo" ,"Lloris", false),
            new Player("Brad", "Friedel", false),
            new Player("Kyle", "Naughton", false),
            new Player("Younes", "Kaboul", true),
            new Player("Benoit", "Assou-Ekotto", false),
            new Player("Jan", "Vertonghen", false),
            new Player("Michael", "Dawson", false),
            new Player("William", "Gallas", true),
            new Player("Kyle", "Walker", false),
            new Player("Scott", "Parker", false),
            new Player("Mousa", "Dembele", false),
            new Player("Sandro", "Cordeiro", true),
            new Player("Tom", "Huddlestone", false),
            new Player("Gylfi","Sigurdsson", false),
            new Player("Gareth", "Bale", false),
            new Player("Aaron", "Lennon", false),
            new Player("Paulinho", "Maciel", false),
            new Player("Jermane", "Defoe", false),
            new Player("Emmanuel", "Adebayor", true)
        );
       
        // Note use of "extractor": this list will notify ListChangeListeners when the list changes, or when the
        // injuredProperty of any elements change
        ObservableList<Player> data =  FXCollections.<Player>observableArrayList(new Callback<Player, Observable[]>() {
          @Override
          public Observable[] call(Player player) {
            return new Observable[] {player.injuredProperty()};
          }
        });
        data.addAll(players);
       
        return data ;
      }
      private static class PlayerTableRow extends TableRow<Player> {
        final String INJURED_STYLE_CLASS = "injured";
        final ChangeListener<Boolean> injuryListener = new ChangeListener<Boolean>() {
          @Override
          public void changed(ObservableValue<? extends Boolean> observable,
              Boolean oldValue, Boolean newValue) {
            if (newValue && !getStyleClass().contains(INJURED_STYLE_CLASS)) {
              getStyleClass().add(INJURED_STYLE_CLASS);
            } else {
              getStyleClass().remove(INJURED_STYLE_CLASS);
            }
          }
        };
        @Override
        protected void updateItem(Player player, boolean empty) {
          if (getItem() != null) {
            getItem().injuredProperty().removeListener(injuryListener);
          }
          super.updateItem(player, empty);
          if (player != null) {
            player.injuredProperty().addListener(injuryListener);
            if (player.isInjured()) {
              if (! getStyleClass().contains(INJURED_STYLE_CLASS)) {
                getStyleClass().add(INJURED_STYLE_CLASS);
              }
            } else {
              getStyleClass().remove(INJURED_STYLE_CLASS);
            }
          } else {
            getStyleClass().remove(INJURED_STYLE_CLASS);
          }
        }
      };
      public static class Player {
        private final StringProperty firstName ;
        private final StringProperty lastName ;
        private final BooleanProperty injured ;
        Player(String firstName, String lastName, boolean international) {
          this.firstName = new SimpleStringProperty(this, "firstName", firstName);
          this.lastName = new SimpleStringProperty(this, "lastName", lastName);
          this.injured = new SimpleBooleanProperty(this, "injured", international);
        }
        public String getFirstName() { return firstName.get(); }
        public void setFirstName(String firstName) { this.firstName.set(firstName);}
        public StringProperty firstNameProperty() { return firstName ; }
        public String getLastName() { return lastName.get(); }
        public void setLastName(String lastName) { this.lastName.set(lastName); }
        public StringProperty lastNameProperty() { return lastName ; }   
        public boolean isInjured() { return injured.get(); }
        public void setInjured(boolean international) { this.injured.set(international); }
        public BooleanProperty injuredProperty() { return injured ; }
        @Override
        public String toString() {
          return firstName.get() + " " + lastName.get();
        }
      }
    }
    

     

    Here's the (very minimal) css file:

    @CHARSET "US-ASCII";
    .injured .table-cell {
     -fx-background-color: red;
    }
    
  • 2. Re: Thumbnail view of a TableView populated by an ObservableList
    James_D Guru
    Currently Being Moderated

    Yeah, so I looked at this again, and that doesn't even come close to answering what you asked. I was looking at the bottom of the image (which looks like a filtered version of the main display) rather than the thumbnail at the top.

     

    For the thumbnail, I think I would try a VBox filled with thin horizontal rectangles. Something like:

     

    final VBox thumbnail = new VBox();
    thumbnail.setPrefWidth(50);
    table.getItems().addListener(new ListChangeListener<T>() {
         public void onChanged(Change<? extends T> change) {
              buildThumbnail(table, thumbnail);
         }
    });
    buildThumbnail(table, thumbnail);
    // ...
    private void buildThumbnail(Table<T> table, VBox thumbnail) {
         thumbnail.getChildren().clear();
         int index = 0 ;
         for (T item : table.getItems()) {
              Rectangle rect = new Rectangle();
              rect.setHeight(2);
              rect.widthProperty().bind(thumbnail.widthProperty().subtract(2));
              if ( itemShouldBeHighlighted()) {
                   rect.setFill(Color.RED);
              } else {
                   rect.setFill(Color.WHITE);
              }
              final int itemIndex = index ;
              rect.setOnMouseClicked(new EventHandler<MouseEvent>() {
                   @Override
                   public void handle(MouseEvent event) {
                        table.scrollTo(itemIndex);
                   }
              });
              thumbnail.getChildren().add(rect);
              index++;
         }
    
    }
    

     

    Obviously replace T with the type for your TableView. Again, this just rebuilds the thumbnail from scratch every time something changes, which isn't optimized but will likely work fine unless you have a huge table. In that case you can iterate through the changes in the onChanged method, and just update the parts of the thumbnail as necessary. As before, using the extractor when creating the ObservableList for the table items ensures the thumbnail is updated when an individual item changes.

  • 3. Re: Thumbnail view of a TableView populated by an ObservableList
    eno g. - oracle Newbie
    Currently Being Moderated

    Awesome!

     

    Thanks James

Legend

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