My issue is the following : I read a .csv file and convert it into an ArrayList. In order to put this ArrayList in a javafx TableView, I need to create an object, go through ObservableList<Object> data = FXCollections.observableArrayList() and populate it with as much as new Object() as I need. The point is that in my .csv, the number of columns may differ in each run of my program, so the object definition does. I don't know how to define an object which definition is a function of the length of my ArrayList and the attributes aren't always the same (and I can't find any clue on google).
To illustrate, imagine the following code :
public class SettingData extends Application {
public void start(Stage stage) {
TableView<FileData> table = new TableView<FileData>();
final ObservableList<FileData> data = FXCollections.observableArrayList(
new FileData("file1", "D:\\myFiles\\file1.txt", "25 MB", "12/01/2017"),
new FileData("file2", "D:\\myFiles\\file2.txt", "30 MB", "01/11/2019"),
new FileData("file3", "D:\\myFiles\\file3.txt", "50 MB", "12/04/2017"),
new FileData("file4", "D:\\myFiles\\file4.txt", "75 MB", "25/09/2018")
);
TableColumn fileNameCol = new TableColumn("File Name");
fileNameCol.setCellValueFactory(new PropertyValueFactory<>("fileName"));
TableColumn pathCol = new TableColumn("Path");
pathCol.setCellValueFactory(new PropertyValueFactory("path"));
TableColumn sizeCol = new TableColumn("Size");
sizeCol.setCellValueFactory(new PropertyValueFactory("size"));
TableColumn dateCol = new TableColumn("Date Modified");
dateCol.setCellValueFactory(new PropertyValueFactory("dateModified"));
ObservableList<String> list = FXCollections.observableArrayList();
table.setItems(data);
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
table.getColumns().addAll(fileNameCol, pathCol, sizeCol, dateCol);
Assuming that we define FileData like this :
public class FileData {
SimpleStringProperty fileName;
SimpleStringProperty path;
SimpleStringProperty size;
SimpleStringProperty dateModified;
FileData(String fileName, String path, String size, String dateModified) {
this.fileName = new SimpleStringProperty(fileName);
this.path = new SimpleStringProperty(path);
this.size = new SimpleStringProperty(size);
this.dateModified = new SimpleStringProperty(dateModified);
}
public String getFileName(){
return fileName.get();
}
public void setFileName(String fname){
fileName.set(fname);
}
public String getPath(){
return path.get();
}
public void setPath(String fpath){
path.set(fpath);
}
public String getSize(){
return size.get();
}
public void setSize(String fsize){
size.set(fsize);
}
public String getDateModified(){
return dateModified.get();
}
public void setModified(String fmodified){
dateModified.set(fmodified);
}
}
How may implement my code to handle the fact that once I may not have size columns, or another time I'll have a author column. (In fact, the file I'm working with carries around 80 to 110 columns, that's why I need to have a "modular" definition of my object).
It's my first post on any forum ever, I'm not familiar with your usages, sorry for that.
Thanks for your answers.
Related
Hi StackOverflow people,
First question here, I'm stuck on this code and cannot move forward, tried different approaches but cannot figure out why this is happening.
The code is intended to be a few lists each one represents a day of the week, and each of the list has all the possible time. Now, everytime I ran the code each list, even when not update, is using the last date available. For the sake of the example, remove almost all the lists and leave only 2.
The update on the date is being done on this line, t1.setFecha(lunesDate.plusDays(i));, but if for instance, I remove this line on one of the lists, the list is getting the date updated, even if this is happening on another list, with another variable!! It is like the JVM is considering all the lists to be the same... Makes no sense for me...
Can anyone point where is the issue on the code?
Class Turno.class
import java.time.LocalDate;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
public class Turno {
private static final int LIBRE = 1;
private static final int RESERVADO = 2;
private static final int CUMPLIDO = 3;
private static final int CERRADO = 4;
private SimpleIntegerProperty id = new SimpleIntegerProperty();
private SimpleObjectProperty<LocalDate> fecha = new SimpleObjectProperty<LocalDate>();
private SimpleIntegerProperty idhorario = new SimpleIntegerProperty();
private SimpleStringProperty horario = new SimpleStringProperty();
private SimpleIntegerProperty estado = new SimpleIntegerProperty();
private SimpleIntegerProperty idProfesional = new SimpleIntegerProperty();
private SimpleStringProperty profesional = new SimpleStringProperty();;
private SimpleIntegerProperty idPaciente = new SimpleIntegerProperty();
private SimpleStringProperty paciente = new SimpleStringProperty();;
private SimpleStringProperty observaciones = new SimpleStringProperty();;
public Turno(int id, LocalDate d, int idh, String h, int e, int idpro, String pro, int idpac, String pac,
String o) {
this.setId(id);
this.setFecha(d);
this.setIdHorario(idh);
this.setHorario(h);
this.setEstado(e);
this.setIdProfesional(idpro);
this.setProfesional(pro);
this.setIdPaciente(idpac);
this.setPaciente(pac);
this.setObservaciones(o);
}
public Turno() {
}
// ID
public final SimpleIntegerProperty idProperty() {
return this.id;
}
public final int getId() {
return this.idProperty().get();
}
public final void setId(final int i) {
this.idProperty().set(i);
}
/* Bunch of getter and setters for properties, just like the one above */
Class TestTurno.class
public class TestTurnos extends Application {
private static Turno turnoSeleccionado = null;
ScrollPane scrollPane = new ScrollPane();
HBox listas = new HBox();
VBox vBoxL = new VBox();
VBox vBoxM = new VBox();
ListView<Turno> listViewTurnosL = new ListView<>();
ListView<Turno> listViewTurnosM = new ListView<>();
List<Turno> listaHorarios = new ArrayList<>();
List<Turno> listaTurnos = new ArrayList<>();
public static void Main(String[] args) {
launch(args);
}
#Override
public void start(Stage escenario) throws Exception {
// Here we get the current Monday date, in order to prepare for the current week
LocalDate lunesDate = null;
LocalDate diaSemana = null;
LocalDate diaHoy = LocalDate.now();
int d = diaHoy.getDayOfWeek().getValue();
lunesDate = diaHoy.minusDays(d - 1);
// Give the schedules to each day list
listViewTurnosL.setItems(FXCollections.observableList(listaHorarios));
listViewTurnosM.setItems(FXCollections.observableList(listaHorarios));
// Then we look for more data on the DB,
for (int i = 0; i < 2; i++) {
// Database magic happens here, we filled the listaTurnos, not relevant
// We make the lists
//
//!!! Here is where the glitch appears, debugging shows that it only gets into the switch on the right conditions,
// but it keeps on updating the date on any of the lists, even when it is updating another list
switch (i) {
case 0: {
for (Turno t1 : listViewTurnosL.getItems()) {
t1.setFecha(lunesDate.plusDays(i));
}
// Value of t1.getFecha() is 1
break;
}
case 1: {
for (Turno t2 : listViewTurnosM.getItems()) {
t2.setFecha(lunesDate.plusDays(i));
}
// Value of t1.getFecha() is 2 !!!!!
// Value of t2.getFecha() is 2
break;
}
}
}
vBoxL.getChildren().addAll(listViewTurnosL);
vBoxM.getChildren().addAll(listViewTurnosM);
listas.getChildren().addAll(vBoxL, vBoxM);
scrollPane.setContent(listas);
Scene escena = new Scene(scrollPane, 800, 800);
escenario.setScene(escena);
escenario.show();
}
}
Consider how you are creating your lists:
listViewTurnosL.setItems(FXCollections.observableList(listaHorarios));
listViewTurnosM.setItems(FXCollections.observableList(listaHorarios));
The documentation for the factory method you are using reads:
Constructs an ObservableList that is backed by the specified list.
That is - the base list provided is kept as the backing (storing) list. Since both ObservableList instances share the same original ArrayList, it is no wonder they share the content.
You may want to use the factory method FXCollections.ObservableArrayList which creates a new ObservableList (the backing list is created internally, or is the list itself).
If you really need the non-observable list instances, you should probably use two different ones if the lists are not to be equal.
If I look at your code I see the following things:
listViewTurnosL.setItems(FXCollections.observableList(listaHorarios));
listViewTurnosM.setItems(FXCollections.observableList(listaHorarios));
This means to that both listViewTurnosX contain exactly the same element references.
case 0: {
for (Turno t1 : listViewTurnosL.getItems()) {
t1.setFecha(lunesDate.plusDays(i));
}
// Value of t1.getFecha() is 1
break;
}
case 1: {
for (Turno t2 : listViewTurnosM.getItems()) {
t2.setFecha(lunesDate.plusDays(i));
}
It doesn't matter which list is iterated, both contain the same elements, so in both cases the same properties get updated.
To give some background: I now am able to load files onto my mp3 program and play them but all the values in my tableview are null?
My song class
package application;
//imports here
public class Song {
private String title;
private String artist;
private String album;
private SimpleStringProperty pTitle;
private SimpleStringProperty pArtist;
private SimpleStringProperty pAlbum;
private Media music;
private MediaPlayer mp;
private Image coverArt;
public Song(File file) {
music = new Media(file.toURI().toString());
music.getMetadata().addListener((Change<? extends String, ? extends Object> c) -> {
if (c.wasAdded()) {
if ("artist".equals(c.getKey())) {
System.out.println(c.getKey()+":"+c.getValueAdded());
this.pArtist = new SimpleStringProperty(c.getValueAdded().toString());
//pArtist.set(c.getValueAdded().toString());
artist = c.getValueAdded().toString();
} else if ("title".equals(c.getKey())) {
title = c.getValueAdded().toString();
System.out.println(c.getKey()+":"+c.getValueAdded());
} else if ("album".equals(c.getKey())) {
album = c.getValueAdded().toString();
System.out.println(c.getKey()+":"+c.getValueAdded());
} else if ("image".equals(c.getKey())) {
coverArt = (Image) c.getValueAdded();
}
}
});
mp = new MediaPlayer(music);
System.out.println(pArtist);
System.out.println(artist);
//artist = (String) mp.getMedia().getMetadata().get("artist");
//title = (String) music.getMetadata().get("title");
//album = (String) music.getMetadata().get("album");
//artist = "test";
//album = "test";
//title = "test";
}
public void play() {
mp.play();
}
public void pause() {
mp.pause();
}
public void stop() {
mp.stop();
}
public String getTitle(){
return title;
}
public void setTitle(String title){
this.title = title;
}
public String getArtist(){
return artist;
}
public void setArtist(String artist){
this.artist = artist;
}
public String getAlbum(){
return album;
}
public void setAlbum(String album){
this.album = album;
}
public Image getCover(){
return coverArt;
}
public MediaPlayer getMP(){
return mp;
}
}
Weirdly enough at first I thought it was because my String variables were not setting correctly and were set to null since it shows as null in the console when I put these print lines to test it when the Song object is being constructed. Here is a sample of the console when I test this.
null
null
artist:Foo Fighters
album:Saint Cecilia EP
title:Saint Cecilia
Here is my controller class
public class SceneController implements Initializable{
#FXML
private Button stopBtn;
#FXML
private Slider volume;
#FXML
private Button loadBtn;
#FXML
private Button playBtn;
#FXML
private TableView<Song> table;
#FXML
private Label label;
#FXML
private ProgressBar proBar;
private TableColumn songCol,artistCol,albumCol;
ObservableList<Song> songList = FXCollections.observableArrayList();
List<File> list;
FileChooser fileChooser = new FileChooser();
Desktop desktop;
Song mySong;
#Override
public void initialize(URL arg0, ResourceBundle arg1) {
TableColumn songCol = new TableColumn("Song");
TableColumn artistCol = new TableColumn("Artist");
TableColumn albumCol = new TableColumn("Album");
songCol.setCellValueFactory(
new PropertyValueFactory<Song,String>("title"));
//songCol.setCellFactory(new Callback);
artistCol.setCellValueFactory(
new PropertyValueFactory<Song,String>("artist"));
albumCol.setCellValueFactory(
new PropertyValueFactory<Song,String>("album"));
volume.setMin(0);
volume.setMax(100);
volume.setValue(100);
volume.valueProperty().addListener(new InvalidationListener() {
#Override
public void invalidated(Observable observable) {
mySong.getMP().setVolume(volume.getValue()/100.0);
}
});
}
// Event Listener on Button[#loadBtn].onAction
#FXML
public void loadFile(ActionEvent event) {
Node source = (Node) event.getSource();
Window theStage = source.getScene().getWindow();
//set fileChooser filter
FileChooser.ExtensionFilter extFilter = new FileChooser.ExtensionFilter("MP3 files", "*.mp3");
fileChooser.getExtensionFilters().add(extFilter);
fileChooser.setTitle("Select MP3 files");
//File file = fileChooser.showOpenDialog(theStage);
//mySong = new Song(file);
list = fileChooser.showOpenMultipleDialog(theStage);
if(list!=null){
for(File x: list) {
mySong = new Song(x);
System.out.println(mySong.getTitle());
songList.add(mySong);
}
}
table.setItems(songList);
}
#FXML
public void playSong(ActionEvent event) {
mySong.play();
}
#FXML
public void stopSong(ActionEvent event) {
//mySong.pause();
System.out.println("song title: "+mySong.getArtist()+mySong.getTitle());
ImageView img = new ImageView(mySong.getCover());
//img.fitWidthProperty().bind(label.widthProperty());
//img.fitHeightProperty().bind(label.heightProperty());
img.setFitHeight(120);
img.setFitWidth(200);
label.setGraphic(img);
//label.setGraphic(new ImageView(mySong.getCover()));
}
But I made another test print line for my "Stop" button in the controller class and after everything is loaded and I press it, it prints out the artist and title fine. I have saw this other thread and checked my getter methods and they seem to be correct? I am really lost on this and if anyone could provide some insight and a solution as to whether it is because my variables are null or my PropertyValueFactory is not done correctly
Also I notice that the nulls come first even though should they not be the last thing printed since when I create a new song object in my controller class the first print lines that run are in the if statements?
There are several things wrong with the way you have your current code, that are evident from the limited example you posted in the question:
Your Song class does not properly follow the JavaFX properties pattern. In particular, you store each "property" twice, once in a "traditional" JavaBean-style field, for example private String title, and once in a JavaFX property: private StringProperty pTitle;. Each property should be stored once. If you want the table to be aware when the value changes, you should use JavaFX properties, and have the "standard" getXXX() and setXXX() retrieve and set the underlying values stored in those properties.
The listener you attach to the media's metadata is called asynchronously at some indeterminate point in the future. When you add the song to the table's list, the cell value factories attached to the columns will, at some point, be executed, and retrieve the assigned property from the Song instance. With the code the way you currently have it, those property instances are only actually created once the listener on the metadata is invoked. So it is possible (perhaps likely) that the cell value factory will inspect the Song instance for its property before the JavaFX property is instantiated, making it impossible for the table to properly observe the property and respond to changes in it. You should instantiate the JavaFX properties when the Song instance is created, and set their value in the listener on the metadata.
At no point do you add the columns you create in the controller to the table. If you are creating them in the FXML file (which you didn't post in the question), you should inject those columns into the controller and initialize those columns with the cell value factories. (Since the screenshot shows there are columns in the table, I am going to assume they are defined in the FXML file, and have appropriate fx:ids.)
So your Song class should look something like this:
public class Song {
private final StringProperty title = new SimpleStringProperty();
private final StringProperty artist = new SimpleStringProperty();
private final StringProperty album = new SimpleStringProperty();
private Media music;
private MediaPlayer mp;
private Image coverArt;
public Song(File file) {
music = new Media(file.toURI().toString());
music.getMetadata().addListener((Change<? extends String, ? extends Object> c) -> {
if (c.wasAdded()) {
if ("artist".equals(c.getKey())) {
setArtist(c.getValueAdded().toString());
} else if ("title".equals(c.getKey())) {
setTitle(c.getValueAdded().toString());
} else if ("album".equals(c.getKey())) {
setAlbum(c.getValueAdded().toString());
} else if ("image".equals(c.getKey())) {
// maybe this needs to be a JavaFX property too: it is not clear from your question:
coverArt = (Image) c.getValueAdded();
}
}
});
mp = new MediaPlayer(music);
}
public void play() {
mp.play();
}
public void pause() {
mp.pause();
}
public void stop() {
mp.stop();
}
public StringProperty titleProperty() {
return title ;
}
public final String getTitle(){
return titleProperty().get();
}
public final void setTitle(String title){
titleProperty().set(title);
}
public StringProperty artistProperty() {
return artist ;
}
public final String getArtist(){
return artistProperty().get();
}
public final void setArtist(String artist){
artistProperty.set(artist);
}
public StringProperty albumProperty() {
return album ;
}
public final String getAlbum(){
return albumProperty().get();
}
public final void setAlbum(String album){
albumProperty().set(album);
}
public Image getCover(){
return coverArt;
}
public MediaPlayer getMP(){
return mp;
}
}
For your controller, I am going to assume your FXML file has defined table columns with fx:ids of "songCol", "artistCol", and "albumCol", respectively. You need to inject these into the controller as you do with the other columns. I also strongly recommend not using the PropertyValueFactory class, which uses reflection and lacks much in the way of compile-time checking, and implementing the callback yourself. Using lambda expressions makes this pretty easy.
So your controller should look like:
public class SceneController implements Initializable{
// non-table code omitted...
#FXML
private TableView<Song> table;
#FXML
private Label label;
#FXML
private ProgressBar proBar;
#FXML
private TableColumn<Song, String> songCol ;
#FXML
private TableColumn<Song, String> artistCol ;
#FXML
private TableColumn<Song, String> albumCol;
ObservableList<Song> songList = FXCollections.observableArrayList();
List<File> list;
FileChooser fileChooser = new FileChooser();
Desktop desktop;
Song mySong;
#Override
public void initialize(URL arg0, ResourceBundle arg1) {
songCol.setCellValueFactory(cellData -> cellData.getValue().titleProperty());
artistCol.setCellValueFactory(cellData -> cellData.getValue().artistProperty());
albumCol.setCellValueFactory(cellData -> cellData.getValue().albumProperty());
// ...
}
// other non-table code omitted...
}
You didn't post an minimal, complete, verifiable example, so there may well be other errors in your code which prevent the table from displaying correctly. This should get you started, however.
Normally the TableColumns would be defined in FXML and injected via #FXML.
See the Oracle TableView FXML example.
If you don't want to do it that way, you need to do:
table.getColumns().add(songCol);
And similarly for your other columns.
Also, as HypnicJerk pointed out in comments you also need to follow appropriate naming conventions when using the PropertyValueFactory.
songCol.setCellValueFactory(
new PropertyValueFactory<Song,String>("title")
);
For more details see:
Javafx tableview not showing data in all columns
I am trying to scrape google finance. The getStockInfoFromGoogle method stores two strings: (1) stock Name and (2) closing price into an ObservableList<String>. This works fine.
Then I tried to run this method in the background using ScheduledService and thus, priceService should return an ObservableList<String>.
Problem occurs when I try to retrieve the ObservableList<String> from ScheduledService method, as I am not able to individually extract the strings from the list and associate them with the lastValueProperty() and bind that to the closingPrice property.
How should I solve this problem? (I want to retain lastValueProperty() here).
public class Trade{
private final ReadOnlyStringWrapper stockName;
private final ReadOnlyDoubleWrapper closingPrice;
private final ScheduledService<ObservableList<String>> priceService = new ScheduledService<ObservableList<String>>() {
#Override
public Task<ObservableList<String>> createTask(){
return new Task<ObservableList<String>>() {
#Override
public Number call() throws InterruptedException, IOException {
return getStockInfoFromGoogle();
}
};
}
};
// constructor
public Trade(){
priceService.setPeriod(Duration.seconds(100));
priceService.setOnFailed(e -> priceService.getException().printStackTrace());
this.closingPrice = new ReadOnlyDoubleWrapper(0);
this.stockName = new ReadOnlyStringWrapper("");
// this is the part where things goes wrong for the two properties below
this.closingPrice.bind(Double.parseDouble(priceService.lastValueProperty().get().get(1)));
this.stockName.bind(priceService.lastValueProperty().get().get(0));
}
public ObservableList<String> getStockInfoFromGoogle() throws InterruptedException, IOException{
ObservableList<String> output = FXCollections.observableArrayList();
// do some web scraping
output.add(googleStockName);
output.add(googleClosingPrice);
return output;
}
Your design looks pretty confused, as the Trade class seems to be both encapsulating a trade (name and price) but also seems to be managing the service (which just updates a single trade?).
Anyway, using a List here is not the way to go, you should create a class to encapsulate the data you get back from the service:
class TradeInfo {
private final String name ;
private final double price ;
public TradeInfo(String name, double price) {
this.name = name ;
this.price = price ;
}
public String getName() {
return name ;
}
public double getPrice() {
return price ;
}
}
Now make your ScheduledService a ScheduledService<TradeInfo>, etc, and you can do
public TradeInfo getStockInfoFromGoogle() throws InterruptedException, IOException{
// do some web scraping
return new TradeInfo(googleStockName, googleClosingPrice);
}
Now create the bindings:
this.closingPrice.bind(Bindings.createDoubleBinding(() ->
priceService.getLastValue().getPrice(),
priceService.lastValueProperty());
this.stockName.bind(Bindings.createStringBinding(() ->
priceService.getLastValue().getName(),
priceService.lastValueProperty());
(You may need to deal with the case where priceService.getLastValue() is null.)
I've got a project written in JavaFX and I'm trying to get a refresh on a tableview without result.
I've googled around and tried some examples I've found but it still doesn't work.
I populate a tableview with information each row in this table can have new comments added to by double click on the row. The a new Tabpane is opened and the new comment can be added there. On close of this tabpane I'd like the one I clicked from to be refreshed.
I must be doing something wrong. I just don't know what.
In my StoreController
private void populateTableView(List<Store> stores) {
ObservableList<Store> data = FXCollections.observableArrayList(stores);
storeNumberColumn.setCellValueFactory(
new PropertyValueFactory<Store, String>("id"));
storePhoneColumn.setCellValueFactory(
new PropertyValueFactory<Store, String>("phoneNbr"));
chainColumn.setCellValueFactory(
new PropertyValueFactory<Store, String>("chainId"));
commentColumn.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<Store, ImageView>, ObservableValue<String>>() {
#Override
public ObservableValue<String> call(TableColumn.CellDataFeatures<Store, ImageView> p) {
Integer numberOfComments = p.getValue().getCommentsCount();
ReadOnlyObjectWrapper wrapper = null;
if (numberOfComments == 0) {
wrapper = null;
} else if (numberOfComments == 1) {
wrapper = new ReadOnlyObjectWrapper(new ImageView(COMMENT_SINGLE_FLAG_SOURCE));
} else {
wrapper = new ReadOnlyObjectWrapper(new ImageView(COMMENT_DOUBLE_FLAG_SOURCE));
}
return wrapper;
}
});
storeTable.setItems(data);
sortTable(storeTable, missedColumn);
}
#FXML
public void handleTableAction(MouseEvent event) {
if (event.getClickCount() == 2) {
showNewCommentStage();
}
}
private void showNewCommentStage() {
initCommentController();
Store store
= storeTable.getSelectionModel().selectedItemProperty().getValue();
commentController.showNewStage(commentPane, store);
}
It seems like the call-function doesn't get called when the commentpane is closed.
CommentController
public void showNewStage(Pane pane, Store store) {
this.store = store;
initStage(pane);
windowHandler = new WindowHandler(stage);
effectHandler.playEffect(pane);
constructCommentHeaders();
List<Comment> comments;
comments = commentService.listByStoreId(store.getId());
populateCommentTable(comments);
}
Like I said I've tried a lot of the solutions found here on Stackoverflow but with no results. The Tableview doesn't refresh. The Stores and the Comments are in different database tables if that's important
Can someone point me in the right direction?
Thanks!
****EDIT****
The Store.class
public class Store extends CommentEntity {
private String id;
private String chainId;
private String phoneNbr;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getChainId() {
return chainId;
}
public void setChainId(String chainId) {
this.chainId = chainId;
}
public String getPhoneNbr() {
return phoneNbr;
}
public void setPhoneNbr(String phoneNbr) {
this.phoneNbr = phoneNbr;
}
#Override
public String toString() {
return "Store{" + "id=" + id + ", chainId=" + chainId + '}';
}
#Override
public String getCommentIdentifier() {
return id;
}
}
The CommentEntity.Class
public abstract class CommentEntity {
private int commentsCount;
public int getCommentsCount() {
return commentsCount;
}
public void setCommentsCount(int commentsCount) {
this.commentsCount = commentsCount;
}
public abstract String getCommentIdentifier();
}
Thank you for input, I hadn't even reflected over the ImageView / String.
Two issues:
First, you need to distinguish between the data the cells in your column are displaying, and the cells that actually display those data. The cellValueFactory determines the data that are displayed. The PropertyValueFactory is a cellValueFactory implementation that references a JavaFX Property, so when you call
storeNumberColumn.setCellValueFactory(new PropertyValueFactory<Store, String>("id"));
it effectively tells the cells in the storeNumberColumn to call the idProperty() method on the Store object in the current row to get the data for the cell. (If no such method exists, it will try to use getId() as a backup plan.)
By default, you get a cellFactory that displays text resulting from calling toString() on the data generated by the cellValueFactory. In the case where your data are simply Strings, this is usually what you need. In other cases, you often need to provide a cellFactory of your own to get the correct way to display the data.
In your case, the data for the commentColumn are simply the number of comments. You are going to display that by choosing an image based on that numeric value.
So you should have
TableColumn<Store, Number> commentColumn = new TableColumn<>("Comments");
For the cellValueFactory, you can just use
commentColumn.setCellValueFactory(new PropertyValueFactory<>("commentsCount"));
Then you need a cellFactory that displays the appropriate ImageView:
commentColumn.setCellFactory(new Callback<TableColumn<Store, Number>, new TableCell<Store, Number>>() {
#Override
public TableCell<Store, Number>() {
private ImageView imageView = new ImageView();
#Override
public void updateItem(Number numberOfComments, boolean empty) {
super.updateItem(count, empty) ;
if (empty) {
setGraphic(null);
} else {
if (numberOfComments.intValue() == 0) {
setGraphic(null);
} else if (numberOfComments.intValue() == 1) {
imageView.setImage(new Image(COMMENT_SINGLE_FLAG_SOURCE));
setGraphic(imageView);
} else {
imageView.setImage(new Image(COMMENT_DOUBLE_FLAG_SOURCE));
setGraphic(imageView);
}
}
}
}
});
The second issue is actually about the update. A TableView keeps its contents "live" by observing JavaFX properties that are provided by the cellValueFactory as ObservableValues. If the value might change while the table is displayed, you must provide an actual property that can be observed: using a ReadOnlyObjectWrapper is no good (because it's read only, so it's wrapped value will not change). The PropertyValueFactory will also return a ReadOnlyObjectWrapper if you do not have JavaFX property accessor methods (i.e. if it is only using getXXX() methods to access the data). So your model class must provide JavaFX Properties.
You can make an immediate fix to this by updating CommentEntity to use an IntegerProperty:
public abstract class CommentEntity {
private final IntegerProperty commentsCount = new SimpleIntegerProperty();
public final int getCommentsCount() {
return commentsCountProperty().get();
}
public final void setCommentsCount(int commentsCount) {
commentsCountProperty().set(commentsCount);
}
public IntegerProperty commensCountProperty() {
return commentsCount ;
}
public abstract String getCommentIdentifier();
}
I would also strongly recommend updating the Store class to use JavaFX Properties in a similar manner.
I'm trying to fill a listview with the artist and title of songs using the open() method.
To achieve this I created the artist and title ArrayLists and merged them using the create() method.
The problem is, when I try to run create() inside open() nothing happens. However, if I assign the create() method to a different button and click it after using the filechooser everything works fine.
So, I would like to know if it is possible to run the create() method after the open() method using only one button via fxml or regular java code.
public class PLController implements Initializable {
#Override
public void initialize(URL location, ResourceBundle resources) {
list.setItems(visibleList);
}
List<File> filelist = new ArrayList<File>();
ArrayList<String> title = new ArrayList<String>();
ArrayList<String> artist = new ArrayList<String>();
ObservableList<String> visibleList = FXCollections.observableArrayList();
#FXML
ListView<String> list;
#FXML
Button impButton;
public void create(){
for(int i = 0; i < title.size(); i++){
visibleList.add(artist.get(i) +" - " +title.get(i));
Collections.sort(visibleList);
}
}
public void handleMetadata(String key, Object value){
if (key.equals("title")){
title.add(value.toString());
}
if (key.equals("artist")){
artist.add(value.toString());
}
}
public void open(){
FileChooser chooser = new FileChooser();
filelist = chooser.showOpenMultipleDialog(impButton.getScene().getWindow());
for(File f:filelist){
try {
Media media = new Media(f.toURI().toURL().toString());
media.getMetadata().addListener(new MapChangeListener<String, Object>(){
#Override
public void onChanged(Change<? extends String, ? extends Object> change) {
if(change.wasAdded()) {
handleMetadata(change.getKey(), change.getValueAdded());
}
}
});
} catch (MalformedURLException e) {
e.printStackTrace();
}
}create(); //Nothing happens
}
As others have pointed out, the Media object does not have its metadata initialized immediately. (It needs to read data from the URL and populate those metadata as it receives them.) That is why the metadata are exposed as an ObservableMap. When you reach the end of your open() method, it is highly unlikely that the metadata will have been initialized, so your create() method will not see any data at that point.
What you need to do is observe the map, and update the ListView once both the artist and title are available. The best way to do this, in my opinion, is to encapsulate the information you want into a separate class:
public class Video {
private final Media media ;
private final ReadOnlyStringWrapper artist = new ReadOnlyStringWrapper("Unknown");
private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper("Title");
public Video(File file) {
try {
this.media = new Media(file.toURI().toURL().toExternalForm());
artist.bind(Bindings.createStringBinding(() -> {
Object a = media.getMetadata().get("artist");
return a == null ? "Unknown" : a.toString();
}, media.getMetadata()));
title.bind(Bindings.createStringBinding(() -> {
Object t = media.getMetadata().get("title");
return t == null ? "Unknown" : t.toString();
}, media.getMetadata()));
}
catch (Exception e) {
throw new RuntimeException("Could not create Video for "+file, e);
}
}
public ReadOnlyStringProperty titleProperty() {
return title.getReadOnlyProperty();
}
public ReadOnlyStringProperty artistProperty() {
return artist.getReadOnlyProperty();
}
public final String getTitle() {
return title.get();
}
public final String getArtist() {
return artist.get();
}
public final Media getMedia() {
return media ;
}
}
Now you can create a ListView<Video> to display the videos. Use a cell factory to display the artist and the title in the format you want. You can make sure that the observable list fires updates when either the artist or title properties change, and you can keep it sorted via a SortedList.
#FXML
private ListView<Video> list ;
private ObservableList<Video> visibleList ;
public void initialize() {
visibleList = FXCollections.observableArrayList(
// make list fire updates when artist or title change:
v -> new Observable[] {v.artistProperty(), v.titleProperty()});
list.setItems(new SortedList<>(list, Comparator.comparing(this::formatVideo)));
list.setCellFactory(lv -> new ListCell<Video>() {
#Override
public void updateItem(Video item, boolean empty) {
super.updateItem(item, empty) ;
setText(formatVideo(item));
}
});
}
#FXML
private void open() {
FileChooser chooser = new FileChooser();
List<File> fileList = chooser.showOpenMultipleDialog(impButton.getScene().getWindow());
if (fileList != null) {
fileList.stream()
.map(Video::new)
.forEach(visibleList::add);
}
}
private String formatVideo(Video v) {
if (v == null) return "" ;
return String.format("%s - %s", v.getArtist(), v.getTitle());
}
Simply creating a Media object and assigning a listener to it won't fire the code in the listener. So the title list in your code remains empty. The create() method is called, but since you are iterating over an empty list, nothing actually happens.
Use a debugger or add some logging information in such cases.
Also, you should sort the list after the for loop, not every time you add an item.