I am new to Java and OOP and got stuck in adding image to tableview column. Code seems to work, I can see the name of the student correct but images are not shown in the column. I am getting this error and could not understand how to make it work:
javafx.scene.control.cell.PropertyValueFactory getCellDataReflectively
WARNING: Can not retrieve property 'picture' in PropertyValueFactory: javafx.scene.control.cell.PropertyValueFactory#5b0da50f with provided class type: class model.StudentModel
java.lang.IllegalStateException: Cannot read from unreadable property picture
StudentModel:
package model;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.image.ImageView;
import java.util.ArrayList;
import java.util.List;
public class StudentModel {
private ImageView picture;
private String name;
private SubjectModel major;
private SubjectModel minor;
private String accountPassword;
public String getAccountPassword()
{
return accountPassword;
}
public List<LectureModel> lectureModelList = new ArrayList<>();
public StudentModel(String name, SubjectModel major, SubjectModel minor, ImageView picture, String accountPassword)
{
this.name = name;
this.major = major;
this.minor = minor;
this.picture = picture;
this.accountPassword = accountPassword;
}
public String getName()
{
return name;
}
public ObservableList<LectureModel> myObservableLectures(){
ObservableList<LectureModel> observableList = FXCollections.observableArrayList(lectureModelList);
return observableList;
}
public ImageView getPhoto(){
return picture;
}
public void setPhoto(ImageView photo)
{
this.picture = photo;
}
}
And Participants Scene which I have the tableview:
public class ParticipantsScene extends Scene {
private final StudentController studentController;
private final ClientApplication clientApplication;
private final TableView<StudentModel> allParticipantsTable;
private final ObservableList<StudentModel> enrolledStudents;
private LectureModel lecture;
public ParticipantsScene(StudentController studentController, ClientApplication application, LectureModel lecture) {
super(new VBox(), 800 ,500);
this.clientApplication = application;
this.studentController = studentController;
this.lecture = lecture;
enrolledStudents=lecture.observeAllParticipants();
TableColumn<StudentModel, String > nameCol = new TableColumn<>("Name");
nameCol.setMinWidth(200);
nameCol.setCellValueFactory(new PropertyValueFactory<>("name"));
TableColumn<StudentModel, ImageView> picCol = new TableColumn<>("Images");
picCol.setPrefWidth(200);
picCol.setCellValueFactory(new PropertyValueFactory<>("picture"));
allParticipantsTable = new TableView<>();
allParticipantsTable.getColumns().addAll(nameCol,picCol);
allParticipantsTable.setItems(enrolledStudents);
VBox vBox = new VBox(10, allParticipantsTable, createButtonBox());
vBox.setAlignment(Pos.CENTER);
setRoot(vBox);
}
private HBox createButtonBox() {
var backButton = new Button("Back");
backButton.setOnAction(event -> clientApplication.showAllLecturesScene());
var buttonBox = new HBox(10, backButton);
buttonBox.setAlignment(Pos.CENTER);
return buttonBox;
}
}
Also adding Lectures model in case it may helpful:
public class LectureModel {
private String lectureName;
private String lectureHall;
private String subjectName;
private SubjectModel subject;
private TimeSlot timeSlot;
//private Button actionButton1;
//private Button actionButton2;
private List<StudentModel> enrolledStudents = new ArrayList<>();
private String name;
public LectureModel(String lectureName, String lectureHall, SubjectModel subject, TimeSlot timeSlot){
this.lectureName = lectureName;
this.lectureHall = lectureHall;
this.subject = subject;
this.timeSlot = timeSlot;
this.subjectName = this.subject.getSubjectName();
}
public String getLectureName()
{
return lectureName;
}
public String getLectureHall()
{
return lectureHall;
}
public SubjectModel getSubject()
{
return subject;
}
public String getSubjectName()
{
return subjectName;
}
public List<StudentModel> getEnrolledStudents()
{
return enrolledStudents;
}
public ObservableList<StudentModel> observeAllParticipants() {
ObservableList<StudentModel> observableList = FXCollections.observableArrayList(getEnrolledStudents());
return observableList;
}
public TimeSlot getTimeSlot() {
return timeSlot;
}
public void addStudent(StudentModel studentModel){ enrolledStudents.add(studentModel);}
public void removeStudent(StudentModel studentModel)
{
enrolledStudents.remove(studentModel);
};
Appreciate any kind of helps,
Thanks!
You have misnamed the property name used in the PropertyValueFactory.
In general, don't use PropertyValueFactories, instead use a lambda:
Why should I avoid using PropertyValueFactory in JavaFX?
Also, as a general principle, place data in the model, not nodes. For example, instead of an ImageView, store either an Image or a URL to the image in the model. Then use nodes only in the views of the model. For example, to display an image in a table cell, use a cell factory.
An LRU cache can be used for the images if needed (it may not be needed).
Often the images displayed in a table might be smaller than the full-size image, i.e. like a thumbnail. For efficiency, you might want to load images in the background using a sizing image constructor.
If you need help placing and locating your image resources, see:
How do I determine the correct path for FXML files, CSS files, Images, and other resources needed by my JavaFX Application?
Example code
The example in this answer uses some of the principles from the answer text:
Uses a Lambda instead of PropertyValue.
The model for list items is represented as a record using immutable data.
Replace the record with a standard class if you want read/write access to data.
An Image URL is stored as a String in the model rather than as an ImageView node.
A cell factory is used to provide an ImageView node to view the image.
Images are loaded in the background and resized to thumbnail size on loading.
You can skip the thumbnail sizing and use full-size images if your app requires that.
You can load in the foreground if you want the UI to wait until the images are loaded before displaying (not recommended, but for small local images you won't see any difference).
Images are loaded in an LRU cache.
If you don't have a lot of images (e.g. thousands), you could instead store the Image (not the ImageView) directly in the model and use that, removing the LRU cache from the solution.
Though I didn't test it, this solution should scale fine to a table with thousands of rows, each with different images.
The images used in this answer are provided here:
JavaFX: ComboBox with custom cell factory - buggy rendering
import javafx.application.Application;
import javafx.beans.property.*;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.image.*;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.util.*;
public class StudentTableViewer extends Application {
public record Student(String last, String first, String avatar) {}
#Override
public void start(Stage stage) {
TableView<Student> table = createTable();
populateTable(table);
VBox layout = new VBox(
10,
table
);
layout.setPadding(new Insets(10));
layout.setPrefSize(340, 360);
layout.setStyle("-fx-font-size:20px; -fx-base: antiquewhite");
stage.setScene(new Scene(layout));
stage.show();
}
private TableView<Student> createTable() {
TableView<Student> table = new TableView<>();
TableColumn<Student, String> lastColumn = new TableColumn<>("Last");
lastColumn.setCellValueFactory(
p -> new ReadOnlyStringWrapper(p.getValue().last()).getReadOnlyProperty()
);
TableColumn<Student, String> firstColumn = new TableColumn<>("First");
firstColumn.setCellValueFactory(
p -> new ReadOnlyStringWrapper(p.getValue().first()).getReadOnlyProperty()
);
TableColumn<Student, String> avatarColumn = new TableColumn<>("Avatar");
avatarColumn.setCellValueFactory(
p -> new ReadOnlyStringWrapper(p.getValue().avatar()).getReadOnlyProperty()
);
avatarColumn.setCellFactory(
p -> new AvatarCell()
);
avatarColumn.setPrefWidth(70);
//noinspection unchecked
table.getColumns().addAll(lastColumn, firstColumn, avatarColumn);
return table;
}
public static class AvatarCell extends TableCell<Student, String> {
private final ImageView imageView = new ImageView();
private final ImageCache imageCache = ImageCache.getInstance();
#Override
protected void updateItem(String url, boolean empty) {
super.updateItem(url, empty);
if (url == null || empty || imageCache.getThumbnail(url) == null) {
imageView.setImage(null);
setGraphic(null);
} else {
imageView.setImage(imageCache.getThumbnail(url));
setGraphic(imageView);
}
}
}
private void populateTable(TableView<Student> table) {
table.getItems().addAll(
new Student("Dragon", "Smaug", "Dragon-icon.png"),
new Student("Snake-eyes", "Shifty", "Medusa-icon.png"),
new Student("Wood", "Solid", "Treant-icon.png"),
new Student("Rainbow", "Magical", "Unicorn-icon.png")
);
}
}
class ImageCache {
private static final int IMAGE_CACHE_SIZE = 10;
private static final int THUMBNAIL_SIZE = 64;
private static final ImageCache instance = new ImageCache();
public static ImageCache getInstance() {
return instance;
}
private final Map<String, Image> imageCache = new LruCache<>(
IMAGE_CACHE_SIZE
);
private final Map<String, Image> thumbnailCache = new LruCache<>(
IMAGE_CACHE_SIZE
);
public Image get(String url) {
if (!imageCache.containsKey(url)) {
imageCache.put(
url,
new Image(
Objects.requireNonNull(
ImageCache.class.getResource(
url
)
).toExternalForm(),
true
)
);
}
return imageCache.get(url);
}
public Image getThumbnail(String url) {
if (!thumbnailCache.containsKey(url)) {
thumbnailCache.put(
url,
new Image(
Objects.requireNonNull(
ImageCache.class.getResource(
url
)
).toExternalForm(),
THUMBNAIL_SIZE,
THUMBNAIL_SIZE,
true,
true,
true
)
);
}
return thumbnailCache.get(url);
}
private static final class LruCache<A, B> extends LinkedHashMap<A, B> {
private final int maxEntries;
public LruCache(final int maxEntries) {
super(maxEntries + 1, 1.0f, true);
this.maxEntries = maxEntries;
}
#Override
protected boolean removeEldestEntry(final Map.Entry<A, B> eldest) {
return super.size() > maxEntries;
}
}
}
Related
I have two different types (byte to Model and imageView to View) so I had to use two classes
"ProductTableColumnModel" (help class for the picture column ) & Model "Product"
pictureColumn.setCellValueFactory(new PropertyValueFactory<ProductTableColumnModel, ImageView>("picture"));
but other columns like this:
descColumn.setCellValueFactory(new PropertyValueFactory<Product, String>("description"));
Product class:
public class Product {
private byte[] picture;
#Column(name= "picture")
public byte[] getPicture() {
return this.picture;
}
public void setPicture(byte[] picture) {
this.picture = picture;
}
}
ProductTableColumnModel class:
public class ProductTableColumnModel {
private ImageView picture;
public ImageView getPicture() {
return this.picture;
}
public void setPicture(ImageView picture) {
this.picture = picture;
}
in ProductViewController class:
#FXML
TableView <Product> table_Products;
#FXML
TableColumn <Product, String> productName;
#FXML
TableColumn <Product, String> descColumn;
#FXML
TableColumn <ProductTableColumnModel, ImageView> pictureColumn;
.....
// show data in TableView
for(Product p : getProducts() ) {
Product product = new Product();
product.setProductName(String.valueOf(p.getProductName() ));
product.setDescription(String.valueOf(p.getDescription()));
byte[] pictureImageInByte = p.getPicture();
if(pictureImageInByte != null) {
ByteArrayInputStream imageDate = new ByteArrayInputStream(pictureImageInByte);
Image image = new Image(imageDate);
/* ???????
*/
}
tableData.addAll(product);
}
/* set table items */
table_Products.setItems(tableData);
private void intializeTableColumns() {
tableData = FXCollections.observableArrayList();
productName.setCellValueFactory(new PropertyValueFactory<Product, String>("productName"));
descColumn.setCellValueFactory(new PropertyValueFactory<Product, String>("description"));
pictureColumn.setCellValueFactory(new PropertyValueFactory<ProductTableColumnModel, ImageView>("picture"));
}
Can anyone help me to write the correct code after this?
if(pictureImageInByte != null) {
ByteArrayInputStream imageDate = new ByteArrayInputStream(pictureImageInByte);
Image image = new Image(imageDate);
/* ???????
*/
}
If you have a TableView<Product>, then every column has to be a TableColumn<Product, T> for some T.
T can be different for each column, but should always be a "data type", never a "UI type" (i.e. it should never be a subclass of Node).
Here you should have
#FXML
TableColumn <Product, byte[]> pictureColumn;
because the data you are displaying in each cell in the column is a byte[].
You can use a cellFactory, which in general describes how to display the cell's data, to create the ImageView to display in the cells in the column. If the image data are raw data, e.g. in rgb format, you might do:
private void intializeTableColumns() {
// ...
pictureColumn.setCellValueFactory(cellData ->
new SimpleObjectProperty<>(cellData.getValue().getPicture()));
// or
// pictureColumn.setCellValueFactory(new PropertyValueFactory<>("picture"));
// but the previous version is much better
pictureColumn.setCellFactory(col -> new TableCell<>() {
private WritableImage image = new WritableImage(WIDTH, HEIGHT);
private ImageView imageView = new ImageView(image);
#Override
protected void updateItem(byte[] imageData, boolean empty) {
super.updateItem(imageData, empty) ;
if (empty || imageData == null) {
setGraphic(null);
} else {
PixelWriter pw = image.getPixelWriter();
// modify the following as needed if you have a different format
PixelFormat<ByteBuffer> format = PixelFormat.getByteRgbInstance();
pw.setPixels(0, 0, WIDTH, HEIGHT, format, imageData, 0, 3*WIDTH);
setGraphic(imageView);
}
}
});
// ...
}
Obviously you need to fill in some information here, such as the width and height of the images (which I've just assumed are constant), and modify it depending on how the image is encoded in the byte[] array.
If the image data are in a recognized format, i.e. BMP, GIF, JPEG, or PNG, you can do:
pictureColumn.setCellFactory(col -> new TableCell<>() {
private ImageView imageView = new ImageView();
#Override
protected void updateItem(byte[] imageData, boolean empty) {
super.updateItem(imageData, empty) ;
if (empty || imageData == null) {
setGraphic(null);
} else {
Image image = new Image(new ByteArrayInputStream(imageData));
imageView.setImage(image);
setGraphic(imageView);
}
}
});
In my current JavaFX app, there can be different users which have different data relating to them. In different parts of the app labels, tables, graphs etc are bound to observable properties in the user class.
The problem comes when changing users. The bindings are still bound to the previous user. Is there a better way to update this other than rebinding all the parts of the UI on a user change?
The user data is stored in a DataManager class which is passed to all controllers, so they have access to the same data.
DataManager example:
public class DataManager {
private ObservableList<User> userList = FXCollections.observableArrayList();
private User currentUser;
public void addUser(String name, int age, double height, double weight) {
User newUser = new User(name, age, height, weight);
try {
DatabaseWriter.createDatabase();
newUser.setId(UserDBOperations.insertNewUser(newUser));
} catch (SQLException e) {
e.printStackTrace();
}
userList.add(newUser);
}
public void deleteUser(User user) {
try {
DatabaseWriter.createDatabase();
UserDBOperations.deleteExistingUser(user.getId());
} catch (SQLException e) {
e.printStackTrace();
}
userList.remove(user);
}
public updateCurrentUser();
public changeUser();
}
Example User class:
public class User {
private int id;
private StringProperty name;
private IntegerProperty age;
private DoubleProperty height;
private DoubleProperty weight;
private DoubleProperty bmi;
private DoubleProperty totalDistance;
private ObservableList<Event> eventList = FXCollections.observableArrayList();
Is there a better way to use model data that would work better in this situation? I can provide more code/app context if it would help in answering.
Thanks
EDIT 1:
public class ParentController {
private DataManager dataManager = new DataManager();
private ChildController childController1;
private ChildController childController2;
public void initializeChild() {
childController1.setDataManager(dataManager);
childController2.setDataManager(dataManager);
// Controllers for different FXML Files
//
}
}
public class ChildController {
/* Child controller has elements which need data
from the DataManager
*/
private DataManager dataManager;
public void setDataManager(DataManager dataManager) {
this.dataManager = dataManager;
}
}
The main problem I'm trying to overcome is how to keep the database, the model stored in memory, and all the different pages in sync with the same data model. Any suggestions or resources that could point me in the right direction would be great.
EDIT 2: Added more context about database connection
Simply put, you must rebind the controls to display the new data.
When you bind a TextProperty to a user.NameProperty, for example, that binding is specific for that one user object. Even if you change the user, the binding is still pointing back to the original user.
One possible and simple solution is to use a Singleton class to store your selected User. This will allow the selected user to be visible to your entire application:
User.java
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class User {
private StringProperty username = new SimpleStringProperty();
public User(String username) {
this.username.set(username);
}
public String getUsername() {
return username.get();
}
public StringProperty usernameProperty() {
return username;
}
}
GlobalData.java
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
public class GlobalData {
// Global property to hold the currently-selected user
private ObjectProperty<User> selectedUser = new SimpleObjectProperty<>();
private static GlobalData ourInstance = new GlobalData();
public static GlobalData getInstance() {
return ourInstance;
}
private GlobalData() {
}
public User getSelectedUser() {
return selectedUser.get();
}
public ObjectProperty<User> selectedUserProperty() {
return selectedUser;
}
public void setSelectedUser(User selectedUser) {
this.selectedUser.set(selectedUser);
}
}
Now, in your UI controllers, you would just need to create a listener to watch for changes to the selectedUser. When the user changes, just rebind the UI elements in each controller.
Here is a simple MCVE to demonstrate:
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.util.Random;
public class Main extends Application {
// The Label which holds the username value
private Label lblUsername = new Label();
// Grab a reference to the GlobalData singleton
private GlobalData globalData = GlobalData.getInstance();
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
// Create the initial sample User
globalData.setSelectedUser(new User("User #1"));
// Simple interface
VBox root = new VBox(10);
root.setPadding(new Insets(10));
root.setAlignment(Pos.CENTER);
// HBox to hold the username display
HBox hBox = new HBox(5);
hBox.setAlignment(Pos.CENTER);
// Add the username labels to the HBox
hBox.getChildren().addAll(
new Label("Username:"),
lblUsername
);
// Bind the Label to display the current selected user's username
lblUsername.textProperty().bind(globalData.getSelectedUser().usernameProperty());
// Here we use a listener on the the selectedUser property. When it changes, we
// call the rebindUser() method to update the UI
globalData.selectedUserProperty().addListener((observable, oldValue, newValue) -> {
if (newValue != null) {
rebindUser(newValue);
}
});
// Add a button that just changes the selectedUser
Button button = new Button("Change User");
// Set the action for the button to change users
button.setOnAction(this::changeUser);
// Add the HBox and button to the root layout
root.getChildren().addAll(hBox, button);
// Show the Stage
primaryStage.setWidth(400);
primaryStage.setHeight(200);
primaryStage.setScene(new Scene(root));
primaryStage.show();
}
private void changeUser(ActionEvent event) {
// Create a new user with a random # at the end
Random rnd = new Random();
int num = rnd.nextInt(50 + 2);
globalData.setSelectedUser(new User("User #" + num));
}
// This method accepts a new User object and updates all the displayed bindings
private void rebindUser(User newUser) {
lblUsername.textProperty().unbind();
lblUsername.textProperty().bind(newUser.usernameProperty());
}
}
This produces the following output:
Clicking the button will create a random new user, update the selectedUser property of the GlobalData singleton, and the Label gets updated through the rebindUser() method.
This is the first time I've had to work with JavaFX (and hopefully the last) so I don't exactly understand how everything works. I'll try to sum where I am briefly
I am trying to make my table highlight duplicate cells on a specific column
I need editable cells and no TableCell extensions I've come across work, I've been spending most of today trying to fix their bugs to no avail. I've given up on that approach.
I found TextFieldTableCell but that does not allow me to extend and override functions like updateItem. At this point I have no interest in re-implementing any of this functionality.
Currently what I do is the following:
CollectionName.setCellValueFactory(new PropertyValueFactory<>("CollectionName"));
CollectionName.setCellFactory(EditingCell.<Item>forTableColumn(this)); //At the moment this just passes though TextFieldTableCell, the parameter is totally inconsequential
CollectionName.setOnEditCommit((CellEditEvent<Item, String> t) ->
{
((Item) t.getTableView().getItems().get(
t.getTablePosition().getRow())
).setCollectionName(t.getNewValue());
System.out.println("Set on edit commit");
if(isDuplicateName(t.getNewValue()))
{
t.getTableView().getColumns().get(t.getTablePosition().getColumn()).getStyleClass().add("duplicate-cell");
System.out.println("Duplicate");
}
else
{
t.getTableView().getColumns().get(t.getTablePosition().getColumn()).getStyleClass().remove("duplicate-cell");
System.out.println("Not duplicate");
}
});
This functions as intended but highlights the entire column. I need it to highlight only the specific cell. I wish there was a way to simply call myTable.getCell(x,y).getStyleClass().add("duplicate-cell") or something. I mean it is a table after all...
The solution to any problem involving changing the appearance of table cells based on certain state of the cell's item, and other data, is always to use a cell factory which returns a cell that updates its appearance accordingly.
The problem with the approach you are trying is that you are overlooking the fact that the table view reuses cells. For example, if the table contains a large amount of data and the user scrolls, new cells will not be created but cells that are scrolled out of view will be reused for the new items that scroll into view. Since you don't update the style of the cell when this happens, scrolling will make the wrong cells highlighted.
Here the logic is a little tricky as each cell essentially has to observe all values in the column (whether they are currently displayed or not). I think the easiest solution here is to independently maintain an ObservableSet that keeps a list of duplicate entries, and have the cell observe that. Here's an implementation. You can probably factor this out into a separate class for the cell factory (or something convenient) to make it more elegant and reusable.
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import javafx.application.Application;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.collections.ObservableSet;
import javafx.css.PseudoClass;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.stage.Stage;
import javafx.util.StringConverter;
public class HighlightDuplicateTableCells extends Application {
// create an observable list that fires events if the dataProperty of any elements change:
private final ObservableList<Item> items =
FXCollections.observableArrayList(item -> new Observable[]{item.dataProperty()});
// collection of strings that are duplicated in the data properties of all the items:
private final ObservableSet<String> duplicateData = FXCollections.observableSet();
private static final PseudoClass DUPLICATE_PC = PseudoClass.getPseudoClass("duplicate");
private final StringConverter<String> identityStringConverter = new StringConverter<String>() {
#Override
public String toString(String object) {
return object;
}
#Override
public String fromString(String string) {
return string;
}
};
#Override
public void start(Stage primaryStage) {
// listener to maintain collection of duplicates:
items.addListener((Change<? extends Item> change) -> updateDuplicateData());
TableView<Item> table = new TableView<>();
table.setEditable(true);
table.setItems(items);
TableColumn<Item, Number> idColumn = new TableColumn<>("Id");
idColumn.setCellValueFactory(cellData -> new SimpleIntegerProperty(cellData.getValue().getId()));
TableColumn<Item, String> dataColumn = new TableColumn<>("Data");
dataColumn.setCellValueFactory(cellData -> cellData.getValue().dataProperty());
dataColumn.setCellFactory(tc -> {
TextFieldTableCell<Item, String> cell = new TextFieldTableCell<Item, String>(identityStringConverter) {
// boolean binding that indicates if the current item is contained in the duplicateData set:
private BooleanBinding duplicate = Bindings.createBooleanBinding(
() -> duplicateData.contains(getItem()),
duplicateData, itemProperty());
// anonymous constructor just updates CSS pseudoclass if above binding changes:
{
duplicate.addListener((obs, wasDuplicate, isNowDuplicate) ->
pseudoClassStateChanged(DUPLICATE_PC, isNowDuplicate));
}
};
return cell ;
});
table.getColumns().add(idColumn);
table.getColumns().add(dataColumn);
// note best to minimize changes to items.
// creating a temp list and using items.setAll(...) achieves this:
List<Item> tmp = new ArrayList<>();
for (int i = 1 ; i <= 70; i++) {
char c = (char)('#' + (i % 60));
String data = Character.toString(c) ;
tmp.add(new Item(i, data));
}
items.setAll(tmp);
Scene scene = new Scene(table, 600, 600);
scene.getStylesheets().add("duplicate-cell-example.css");
primaryStage.setScene(scene);
primaryStage.show();
}
private void updateDuplicateData() {
// TODO: may not be most efficient implementation
// all data:
List<String> data = items.stream().map(Item::getData).collect(Collectors.toList());
// unique data:
Set<String> uniqueData = new HashSet<>(data);
// remove unique values from data:
uniqueData.forEach(data::remove);
// remaining values are duplicates: replace contents of duplicateData with these:
duplicateData.clear();
duplicateData.addAll(data);
}
public static class Item {
private final int id ;
private final StringProperty data = new SimpleStringProperty();
public Item(int id, String data) {
this.id = id ;
setData(data);
}
public final StringProperty dataProperty() {
return this.data;
}
public final String getData() {
return this.dataProperty().get();
}
public final void setData(final String data) {
this.dataProperty().set(data);
}
public int getId() {
return id ;
}
}
public static void main(String[] args) {
launch(args);
}
}
and the duplicate-cell-example.css:
.table-cell:duplicate {
-fx-background-color: -fx-background ;
-fx-background: red ;
}
This is basically James_D's approach, but it improves the time required for updates from Ω(n²) worst case (n = list size) to O(m) where m is the number of changes (1 for updates of a property; the number of elements added/removed on a list update).
This performance is achieved by storing the number of occurances in a ObservableMap<String, Integer>:
private final ObservableMap<String, Integer> valueOccuranceCounts = FXCollections.observableHashMap();
private final ChangeListener<String> changeListener = (observable, oldValue, newValue) -> {
valueOccuranceCounts.computeIfPresent(oldValue, REMOVE_UPDATER);
valueOccuranceCounts.merge(newValue, 1, ADD_MERGER);
};
private static final BiFunction<Integer, Integer, Integer> ADD_MERGER = (oldValue, newValue) -> oldValue + 1;
private static final BiFunction<String, Integer, Integer> REMOVE_UPDATER = (key, value) -> {
int newCount = value - 1;
// remove mapping, if the value would become 0
return newCount == 0 ? null : newCount;
};
private final ListChangeListener<Item> listChangeListener = (ListChangeListener.Change<? extends Item> c) -> {
while (c.next()) {
if (c.wasRemoved()) {
for (Item r : c.getRemoved()) {
// decrease count and remove listener
this.valueOccuranceCounts.computeIfPresent(r.getData(), REMOVE_UPDATER);
r.dataProperty().removeListener(this.changeListener);
}
}
if (c.wasAdded()) {
for (Item a : c.getAddedSubList()) {
// increase count and add listener
this.valueOccuranceCounts.merge(a.getData(), 1, ADD_MERGER);
a.dataProperty().addListener(this.changeListener);
}
}
}
};
private final ObservableList<Item> items;
{
items = FXCollections.observableArrayList();
items.addListener(listChangeListener);
}
private static final PseudoClass DUPLICATE = PseudoClass.getPseudoClass("duplicate");
private static final String FIRST_COLUMN_CLASS = "first-column";
#Override
public void start(Stage primaryStage) throws Exception {
TableView<Item> tableView = new TableView<>(items);
// tableView.getSelectionModel().setCellSelectionEnabled(true);
tableView.setEditable(true);
TableColumn<Item, String> column = new TableColumn<>("data");
column.setCellValueFactory(cellData -> cellData.getValue().dataProperty());
column.setCellFactory(col -> new TextFieldTableCell<Item, String>() {
// boolean binding that indicates if the current item is contained in the duplicateData set:
private final BooleanBinding duplicate = Bindings.createBooleanBinding(
() -> valueOccuranceCounts.getOrDefault(getItem(), 1) >= 2,
valueOccuranceCounts, itemProperty());
// anonymous constructor just updates CSS pseudoclass if above binding changes:
{
duplicate.addListener((observable, oldValue, newValue)
-> pseudoClassStateChanged(DUPLICATE, newValue));
}
});
TableColumn<Item, Number> idColumn = new TableColumn<>("id");
idColumn.setCellValueFactory(cellData -> new SimpleIntegerProperty(cellData.getValue().getId()));
tableView.getColumns().addAll(idColumn, column);
tableView.getColumns().addListener((Observable observable) -> {
// keep style class marking the cells of the column as
// belonging to the first column up to date
if (tableView.getColumns().get(0) == column) {
if (!column.getStyleClass().contains(FIRST_COLUMN_CLASS)) {
column.getStyleClass().add(FIRST_COLUMN_CLASS);
}
} else {
column.getStyleClass().remove(FIRST_COLUMN_CLASS);
}
});
// note best to minimize changes to items.
// creating a temp list and using items.setAll(...) achieves this:
final int count = 70;
List<Item> tmp = Arrays.asList(new Item[count]);
for (int i = 0; i < count; i++) {
tmp.set(i, new Item(Integer.toString(i % 60)));
}
items.setAll(tmp);
Scene scene = new Scene(tableView);
scene.getStylesheets().add(getClass().getResource("style.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
}
public static class Item {
private static int counter = 0;
private final StringProperty data;
private final int id = counter++;
public Item(String data) {
this.data = new SimpleStringProperty(data);
}
public final StringProperty dataProperty() {
return this.data;
}
public final String getData() {
return this.dataProperty().get();
}
public final void setData(final String data) {
this.dataProperty().set(data);
}
public int getId() {
return id ;
}
}
style.css
.table-row-cell:filled .table-cell:duplicate {
-fx-background: yellow;
-fx-background-color: -fx-table-cell-border-color, -fx-background;
}
.table-view:focused .table-row-cell:filled .table-cell:duplicate:focused {
-fx-background-color: -fx-background, -fx-cell-focus-inner-border, -fx-background;
}
/* keep use the same background colors normally used for focused table rows */
.table-view:focused .table-row-cell:filled:focused .table-cell:duplicate {
-fx-background-color: -fx-background, -fx-cell-focus-inner-border, -fx-background;
/* frame only at top & bottom sides */
-fx-background-insets: 0, 1 0 1 0, 2 0 2 0;
}
.table-view:focused .table-row-cell:filled:focused .table-cell.first-column:duplicate {
/* frame only for top, left and bottom sides*/
-fx-background-insets: 0, 1 0 1 1, 2 0 2 2;
}
.table-row-cell:filled .table-cell:duplicate:selected,
.table-row-cell:filled:selected .table-cell:duplicate {
-fx-background: turquoise;
}
Note that some parts (creating & filling the table, creating the column) are copied from #James_D's answer, since it's simply best practice to do it this way.
Is there a possibility to use a controller with a JavaFX GUI without using FXML.
I noticed that the FXML file contains an fx-controller attribute to bind the controller but i don't find it an easy way to work with it.
Any ideas about have an MVC arch with JavaFX without using the FXML file or JavaFX Scene Builder ?
Your question isn't particularly clear to me: you just create the classes and basically tie everything together with listeners. I don't know if this helps, but here is a simple example that just has a couple of text fields and a label displaying their sum. This is what I regard as "classical MVC": the view observes the model and updates the UI elements if the model changes. It registers handlers with the UI elements and delegates to the controller if events happen: the controller in turn processes the input (if necessary) and updates the model.
Model:
package mvcexample;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ReadOnlyIntegerWrapper;
import javafx.beans.property.SimpleIntegerProperty;
public class AdditionModel {
private final IntegerProperty x = new SimpleIntegerProperty();
private final IntegerProperty y = new SimpleIntegerProperty();
private final ReadOnlyIntegerWrapper sum = new ReadOnlyIntegerWrapper();
public AdditionModel() {
sum.bind(x.add(y));
}
public final IntegerProperty xProperty() {
return this.x;
}
public final int getX() {
return this.xProperty().get();
}
public final void setX(final int x) {
this.xProperty().set(x);
}
public final IntegerProperty yProperty() {
return this.y;
}
public final int getY() {
return this.yProperty().get();
}
public final void setY(final int y) {
this.yProperty().set(y);
}
public final javafx.beans.property.ReadOnlyIntegerProperty sumProperty() {
return this.sum.getReadOnlyProperty();
}
public final int getSum() {
return this.sumProperty().get();
}
}
Controller:
package mvcexample;
public class AdditionController {
private final AdditionModel model ;
public AdditionController(AdditionModel model) {
this.model = model ;
}
public void updateX(String x) {
model.setX(convertStringToInt(x));
}
public void updateY(String y) {
model.setY(convertStringToInt(y));
}
private int convertStringToInt(String s) {
if (s == null || s.isEmpty()) {
return 0 ;
}
if ("-".equals(s)) {
return 0 ;
}
return Integer.parseInt(s);
}
}
View:
package mvcexample;
import javafx.geometry.HPos;
import javafx.geometry.Pos;
import javafx.scene.Parent;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.scene.control.TextFormatter.Change;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
public class AdditionView {
private GridPane view ;
private TextField xField;
private TextField yField;
private Label sumLabel;
private AdditionController controller ;
private AdditionModel model ;
public AdditionView(AdditionController controller, AdditionModel model) {
this.controller = controller ;
this.model = model ;
createAndConfigurePane();
createAndLayoutControls();
updateControllerFromListeners();
observeModelAndUpdateControls();
}
public Parent asParent() {
return view ;
}
private void observeModelAndUpdateControls() {
model.xProperty().addListener((obs, oldX, newX) ->
updateIfNeeded(newX, xField));
model.yProperty().addListener((obs, oldY, newY) ->
updateIfNeeded(newY, yField));
sumLabel.textProperty().bind(model.sumProperty().asString());
}
private void updateIfNeeded(Number value, TextField field) {
String s = value.toString() ;
if (! field.getText().equals(s)) {
field.setText(s);
}
}
private void updateControllerFromListeners() {
xField.textProperty().addListener((obs, oldText, newText) -> controller.updateX(newText));
yField.textProperty().addListener((obs, oldText, newText) -> controller.updateY(newText));
}
private void createAndLayoutControls() {
xField = new TextField();
configTextFieldForInts(xField);
yField = new TextField();
configTextFieldForInts(yField);
sumLabel = new Label();
view.addRow(0, new Label("X:"), xField);
view.addRow(1, new Label("Y:"), yField);
view.addRow(2, new Label("Sum:"), sumLabel);
}
private void createAndConfigurePane() {
view = new GridPane();
ColumnConstraints leftCol = new ColumnConstraints();
leftCol.setHalignment(HPos.RIGHT);
leftCol.setHgrow(Priority.NEVER);
ColumnConstraints rightCol = new ColumnConstraints();
rightCol.setHgrow(Priority.SOMETIMES);
view.getColumnConstraints().addAll(leftCol, rightCol);
view.setAlignment(Pos.CENTER);
view.setHgap(5);
view.setVgap(10);
}
private void configTextFieldForInts(TextField field) {
field.setTextFormatter(new TextFormatter<Integer>((Change c) -> {
if (c.getControlNewText().matches("-?\\d*")) {
return c ;
}
return null ;
}));
}
}
Application class:
package mvcexample;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class MVCExample extends Application {
#Override
public void start(Stage primaryStage) {
AdditionModel model = new AdditionModel();
AdditionController controller = new AdditionController(model);
AdditionView view = new AdditionView(controller, model);
Scene scene = new Scene(view.asParent(), 400, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
I use JavaFX extensively and do not use FXML or scenebuilder. So I can vouch that it can be done.
Below is the auto generated code made by my IDE to get an JavaFX main class. This will be the root of your application. You will then add to it to create your application.
public class NewFXMain extends Application {
#Override
public void start(Stage primaryStage) {
Button btn = new Button();
btn.setText("Say 'Hello World'");
btn.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
System.out.println("Hello World!");
}
});
StackPane root = new StackPane();
root.getChildren().add(btn);
Scene scene = new Scene(root, 300, 250);
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
For the rest of us... Here is a VERY simple example showing how to create a JavaFX form without the use of any FXML files. This example can be used within an app that is already running, so I've skipped the Main class and all that ... it's just meant to show the simplicity of JavaFX.
In a nutshell, you simply create your scene based on a container such as an AnchorPane, then you create your Stage and assign the Scene to the stage ... add your controls then show the stage
package javafx;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;
public class SimpleFX {
private AnchorPane anchorPane;
private TextArea textArea () {
TextArea textArea = new TextArea();
textArea.setLayoutX(20);
textArea.setLayoutY(20);
textArea.setMaxWidth(450);
textArea.setMinHeight(380);
return textArea;
}
private TextField textField () {
TextField textField = new TextField();
textField.setLayoutX(20);
textField.setLayoutY(410);
textField.setMinWidth(450);
textField.setMinHeight(25);
return textField;
}
private Button button() {
Button button = new Button("Button");
button.setLayoutX(240);
button.setLayoutY(450);
return button;
}
private void addControls () {
anchorPane.getChildren().add(0,textArea());
anchorPane.getChildren().add(1,textField());
anchorPane.getChildren().add(2,button());
}
public void startForm () {
anchorPane = new AnchorPane();
Scene scene = new Scene(anchorPane, 500, 500);
Stage stage = new Stage();
stage.setScene(scene);
addControls();
stage.show();
}
}
I'm using GXT 3 Grid with InlineEdit mode following (more or less) the example code on their site. I don't think there is a way to get the check box cell to fire the 'EditComplete' event and if so, I'm not sure how I would, upon receiving it, disable the date cell on that same row. Just look for the comment: "// not firing for checkbox:" in the code below.
The following code works in an Eclipse web application project - you just need to use it in your 'onModuleLoad' method as demonstrated here:
public void onModuleLoad() {
GridInlineEditingTest j = new GridInlineEditingTest();
}
Here's the code:
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import com.google.gwt.cell.client.DateCell;
import com.google.gwt.core.client.GWT;
import com.google.gwt.editor.client.Editor.Path;
import com.google.gwt.i18n.client.DateTimeFormat;
import com.google.gwt.i18n.client.DateTimeFormat.PredefinedFormat;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.Widget;
import com.sencha.gxt.cell.core.client.form.CheckBoxCell;
import com.sencha.gxt.core.client.ValueProvider;
import com.sencha.gxt.data.shared.ListStore;
import com.sencha.gxt.data.shared.ModelKeyProvider;
import com.sencha.gxt.data.shared.PropertyAccess;
import com.sencha.gxt.data.shared.Store;
import com.sencha.gxt.widget.core.client.FramedPanel;
import com.sencha.gxt.widget.core.client.button.TextButton;
import com.sencha.gxt.widget.core.client.container.BoxLayoutContainer.BoxLayoutPack;
import com.sencha.gxt.widget.core.client.container.VerticalLayoutContainer;
import com.sencha.gxt.widget.core.client.container.VerticalLayoutContainer.VerticalLayoutData;
import com.sencha.gxt.widget.core.client.container.Viewport;
import com.sencha.gxt.widget.core.client.event.CompleteEditEvent;
import com.sencha.gxt.widget.core.client.event.CompleteEditEvent.CompleteEditHandler;
import com.sencha.gxt.widget.core.client.event.SelectEvent;
import com.sencha.gxt.widget.core.client.event.SelectEvent.SelectHandler;
import com.sencha.gxt.widget.core.client.form.CheckBox;
import com.sencha.gxt.widget.core.client.form.DateField;
import com.sencha.gxt.widget.core.client.form.DateTimePropertyEditor;
import com.sencha.gxt.widget.core.client.grid.ColumnConfig;
import com.sencha.gxt.widget.core.client.grid.ColumnModel;
import com.sencha.gxt.widget.core.client.grid.Grid;
import com.sencha.gxt.widget.core.client.grid.Grid.GridCell;
import com.sencha.gxt.widget.core.client.grid.GridView;
import com.sencha.gxt.widget.core.client.grid.editing.GridEditing;
import com.sencha.gxt.widget.core.client.grid.editing.GridInlineEditing;
public class GridInlineEditingTest {
public GridInlineEditingTest() {
VerticalLayoutContainer vlc = new VerticalLayoutContainer();
vlc.add(createGrid(), new VerticalLayoutData(1, 1));
Viewport vp = new Viewport();
vp.add(vlc);
RootPanel.get().add(vp);
}
interface PlaceProperties extends PropertyAccess<Plant> {
ValueProvider<Plant, Date> available();
#Path("id")
ModelKeyProvider<Plant> key();
ValueProvider<Plant, String> name();
ValueProvider<Plant, Boolean> indoor();
}
private static final PlaceProperties properties = GWT.create(PlaceProperties.class);
protected Grid<Plant> grid;
private FramedPanel panel;
private ListStore<Plant> store;
private DateField dateField;
public Widget createGrid() {
if (panel == null) {
ColumnConfig<Plant, String> nameCol = new ColumnConfig<Plant, String>( properties.name(), 220, "Name" );
ColumnConfig<Plant, Date> dateCol = new ColumnConfig<Plant, Date>( properties.available(), 95, "Date" );
ColumnConfig<Plant, Boolean> indorCol = new ColumnConfig<Plant, Boolean>( properties.indoor(), 55, "Indoor");
// display formatting
DateCell dateCell = new DateCell(DateTimeFormat.getFormat(PredefinedFormat.DATE_SHORT));
dateCol.setCell(dateCell);
// display a checkbox in the gridview
indorCol.setCell(new CheckBoxCell());
List<ColumnConfig<Plant, ?>> l = new ArrayList<ColumnConfig<Plant, ?>>();
l.add(nameCol);
l.add(dateCol);
l.add(indorCol);
ColumnModel<Plant> columns = new ColumnModel<Plant>(l);
store = new ListStore<Plant>(properties.key());
store.setAutoCommit(false);
store.addAll(getPlants());
GridView<Plant> gridView = new GridView<Plant>();
grid = new Grid<Plant>(store, columns, gridView);
grid.getView().setAutoExpandColumn(nameCol);
// EDITING//
final GridEditing<Plant> editing = new GridInlineEditing<Plant>(grid);
dateField = new DateField(new DateTimePropertyEditor(DateTimeFormat.getFormat(PredefinedFormat.DATE_SHORT)));
dateField.setClearValueOnParseError(false);
editing.addEditor(dateCol, dateField);
CheckBox checkField = new CheckBox();
editing.addEditor(indorCol, checkField);
editing.addCompleteEditHandler( new CompleteEditHandler<Plant>(){
// not firing for checkbox:
#Override
public void onCompleteEdit(CompleteEditEvent<Plant> event) {
GridCell cell = event.getEditCell();
int row = cell.getRow();
int col = cell.getCol();
System.out.println("got here. row "+row+", col "+col);
}
});
panel = new FramedPanel();
panel.setHeadingText("Editable Grid Example");
panel.setPixelSize(600, 400);
panel.addStyleName("margin-10");
VerticalLayoutContainer con = new VerticalLayoutContainer();
con.setBorders(true);
con.add(grid, new VerticalLayoutData(1, 1));
panel.setWidget(con);
panel.setButtonAlign(BoxLayoutPack.CENTER);
panel.addButton(new TextButton("Reset", new SelectHandler() {
#Override
public void onSelect(SelectEvent event) {
store.rejectChanges();
}
}));
panel.addButton(new TextButton("Save", new SelectHandler() {
#Override
public void onSelect(SelectEvent event) {
store.commitChanges();
}
}));
}
return panel;
}
private static int AUTO_ID = 0;
public class Plant {
private DateTimeFormat df = DateTimeFormat.getFormat("MM/dd/y");
private int id;
private String name;
private String light;
private double price;
private Date available;
private boolean indoor;
private String color;
private int difficulty;
private double progress;
public Plant() {
id = AUTO_ID++;
difficulty = (int) (Math.random() * 100);
progress = Math.random();
}
public Plant(String name, String light, double price, String available, boolean indoor) {
this();
setName(name);
setLight(light);
setPrice(price);
setAvailable(df.parse(available));
setIndoor(indoor);
}
public int getId() { return id; }
public double getProgress() { return progress; }
public String getColor() { return color; }
public int getDifficulty() { return difficulty; }
public Date getAvailable() { return available; }
public String getLight() { return light; }
public String getName() { return name; }
public double getPrice() { return price; }
public boolean isIndoor() { return indoor; }
public void setId(int id) { this.id = id; }
public void setProgress(double progress) { this.progress = progress; }
public void setAvailable(Date available) { this.available = available; }
public void setDifficulty(int difficulty) { this.difficulty = difficulty; }
public void setColor(String color) { this.color = color; }
public void setIndoor(boolean indoor) { this.indoor = indoor; }
public void setLight(String light) { this.light = light; }
public void setName(String name) { this.name = name; }
public void setPrice(double price) { this.price = price; }
#Override
public String toString() {
return name != null ? name : super.toString();
}
}
public List<Plant> getPlants() {
List<Plant> plants = new ArrayList<Plant>();
plants.add(new Plant("Bloodroot", "Mostly Shady", 2.44, "03/15/2006", true));
plants.add(new Plant("Columbine", "Shade", 9.37, "03/15/2006", true));
plants.add(new Plant("Marsh Marigold", "Mostly Sunny", 6.81, "05/17/2006", false));
plants.add(new Plant("Cowslip", "Mostly Shady", 9.90, "03/06/2006", true));
plants.add(new Plant("Dutchman's-Breeches", "Mostly Shady", 6.44, "01/20/2006", true));
plants.add(new Plant("Ginger, Wild", "Mostly Shady", 9.03, "04/18/2006", true));
return plants;
}
}
thanks. and have a great day!!
You are setting a checkbox cell in the column, and then also attaching a field as an inline editor for the column. So if the user clicks the checkbox (cell), you are expecting that click to be ignored, but instead a checkbox (field) to show up over it, which the user may then click?
Instead what is happening is that the checkbox (cell) is reporting that it is using that click event to do something useful - it is changing its value. As a result, the grid editing mechanism ignores the click, so the checkbox (field) never goes into edit mode, and so of course it doesn't complete edit mode.
What are you trying to achieve by making it the purpose of two different checkboxes to be drawn in the same place, and function differently? If you are trying to use the CheckBoxCell instance as a way to always draw the checkbox symbol in the grid cell, there are two main choices:
Skip the CheckBox field in the inline editing, and just let the cell take care of it. It will not fire the editing events, but it will still directly interact with the store. You can listen to the cell's events if you need to, or just to the record change events from the store, or you can subclass the cell to modify behavior.
Removing the event handing guts of the CheckBoxCell to prevent it from handling the event - this may be as simple as overriding onBrowserEvent to do nothing, though I suspect that you actually will want to prevent its check changing behavior entirely so that the Inline Editing version takes care of it
Finally, remember that the purpose of inline editing is to keep the grid from being a mass of fields, and to make it only draw those fields when the user actually interacts with it. This means that the user must first click a field to get something like a checkbox to show up, then interface with the field to change it. Looking one more time at the CheckBox field in an inline editable grid (though this time with a custom cell) at http://www.sencha.com/examples/#ExamplePlace:inlineeditablegrid you'll see that this means two clicks to change a value and get the CompleteEditing event (as well as the various other field change events) that you are after - is this really what you have in mind?
As per the Source Code of CheckBoxCell#isEditing() that says:
A checkbox is never in "edit mode". There is no intermediate state between checked and unchecked.
Find the alternate solution here How to get the row index of selected checkbox on grid GXT.
Please have a look at GXT checkbox in grid