In my JavaFx project I have a scene with a few views. For the footer of the window I have a class extending from TableView:
public class FooterView extends TableView<Area> implements ViewTemplate {...}
This displays a Table with some data from a .csv-file.
When it comes to assigning the value of the specific presentationmodel property to the cells I do it like that:
TableColumn<Area,Double> powerColumn = new TableColumn<>("Power Overview");
powerColumn.setCellValueFactory(new PropertyValueFactory<>("powerPerArea"));
this.setItems(filePM.getAreas()); //filePm is my filehandler
this.getColumns().addAll(powerColumn, other_columns);
getAreas() looks like this:
public List<Area> readCantonsFromFile() {
try (Stream<String> stream = getStreamOfLines(AREA_FILE_NAME)) {
return stream.skip(1)
.map(l -> new Area(l.split(DELIMITER, 12)))
.collect(Collectors.toList());
}
}
In the constructor of Area i set the properties. One of the properties is the mentioned powerPerArea
private final DoubleProperty powerPerArea = new SimpleDoubleProperty();
...
public void setPowerPerCanton(double powerPerCanton) {
this.powerPerCanton.set(powerPerCanton);
}
My question: Is there a way to change the value in the FooterView before the value is displayed? I tried something like this:
powerColumn.setCellValueFactory(Math.round(new PropertyValueFactory<>("powerPerArea")));
But it seems that I mixed up DoubleProperty, Double and ObservableDouble. Can I even modify the value in here?
The problem: I can not round the value in the setter because I add a double value in a loop through this function:
public void addPowerPerArea(double power){
double sum = getPowerPerCanton() + power;
setPowerPerCanton(sum);
}
And rounding the value in here would give me a wrong result. (rounding not precise enough). I need to do it in the end when all sums are added
You should use the cellValueFactory to determine which data are displayed in the cells: in this case the data returned by your PropertyValueFactory is the actual double value returned from powerPerAreaProperty().get(), which is exactly what you want.
If you want to control how the data are displayed, you should use a cellFactory. So to display the data in a particular format, including limiting the number of decimal places, you can do:
powerColumn.setCellFactory(tc -> new TableCell<Area, Double>() {
#Override
protected void updateItem(Double power, boolean empty) {
super.updateItem(power, empty);
if (empty) {
setText(null);
} else {
setText(String.format("%.0f", power.doubleValue()));
}
}
});
The point here is that you should not modify the data based on how you want to display it; the purpose of having both cellValueFactory and cellFactory is to separate the display of the data from the actual data itself.
An alternative to returning custom cells from a cellFactory would be to use a custom cellValueFactory to return the property formatted as string:
TableColumn<Area, String> powerColumn = new TableColumn<>("Power Overview");
powerColumn.setCellValueFactory(cd -> cd.getValue().powerPerAreaProperty().asString(""%.0f""));
I see two ways to do this. You could:
use the setCellFactory method and in the updateItem method you format it. Should look something like this, haven't tested
powerColumn.setCellFactory(column -> {
return new TableCell<Area, Double>() {
#Override
protected void updateItem(Double item, boolean empty) {
super.updateItem(Math.round(item), empty);
}
};
});
OR: You could make another property of your Area class that is bound to the existing powerOfArea property but returns the value rounded. I am sure that is somehow possible, you could just override some functions of a readOnlyDoubleProperty but there should be a better way. Maybe via DoubleBindings.
Related
I am using cell factory for listview with checkboxes like:
listView.setCellFactory(CheckBoxListCell.forListView(new Callback < Bean, ObservableValue < Boolean >> () {
#Override
public ObservableValue < Boolean > call(Bean item) {
BooleanProperty observable = new SimpleBooleanProperty();
observable.addListener((obs, wasSelected, isNowSelected) -> {
if (isNowSelected) {
if (!beanChoices.contains(item.toString())) {
beanChoices.add(item.toString());
observable.setValue(true);
//listView.scrollTo(listView.getItems().size() - 1);
}
} else if (wasSelected) {
if (beanChoices.contains(item.toString())) {
beanChoices.remove(item.toString());
observable.setValue(false);
}
}
});
/* [Code] which compares values with bean item string value and select observable to true for that for edit mode
but here the observer not called for beanItem that are under scrollpane of listview. But on scroll it gets called. */
return observable;
}
}));
It works fine but not for all cases.
Case: When I have say more than 10 entries, the scrollpane comes. Say I have beanChoices to be checked that are at 8 or 9 index(you have to scroll to view them). The listener is not called for the items not visible(that are under scrollpane). On Debug, I found that listener is called when I scroll down.
Problem: when I get checked values from beanChoices for above case, it return empty.
Detail: I have beanChoices which I need to make checked for listview items (edit mode). When I update without changing anything. (Assume that the value which is under the scrollpane of listview will be selected and added to beanChoices)
The Callback is used to retrieve the property for the checked state when the item is associated with a cell. The item may be removed from a cell and put in a new one at any time. This is how ListView (and similar controls like TableView) works. CheckBoxListCell simply gets the checked state property every time a new item is associated with the cell.
The return value is also used to set the initial state of the CheckBox. Since you do not properly initialize the property with the correct value the initial state is not preserved.
Also note that it makes little sense to update the value of the property to the new value in the change listener. It happens anyway.
Since BooleanProperty is a wrapper for primitive boolean the possible values are true and false; the ChangeListener only gets called when !Objects.equals(oldValue, newValue) you can be sure that isNowSelected = !wasSelected.
Of course you also need to return the value:
#Override
public ObservableValue < Boolean > call(Bean item) {
final String value = item.toString();
BooleanProperty observable = new SimpleBooleanProperty(beanChoices.contains(value));
observable.addListener((obs, wasSelected, isNowSelected) -> {
if (isNowSelected) {
beanChoices.add(value);
} else {
beanChoices.remove(value);
}
});
return observable;
}
I also recommend using a Collection of Beans instead of relying on the string representation of the objects. toString many not produce unique results and Beans.equals would be the better choice to compare the objects.
I have a bill table where I want to list all products which are on the bill. I saved the ProductInBill objects within an ArrayList<ProductInBill> on the bill.
When I created a TableView my common approach is to create the JavaFX fields. On the controller class, I have my fields:
#FXML public TableColumn<ProductInBill, String> finishedBillProductNameColumn;
#FXML public TableColumn<Integer, Integer> finishedBillProductNumberColumn;
#FXML public TableColumn<ProductInBill, Integer> finishedBillProductPriceBruttoLabel;
#FXML public TableColumn<Integer, Integer> finishedBillProductTotalAmountColumn;
#FXML public TableView finishedBillProductTable;
Then I am using a setUp() method with the code like:
private void setUpFinishedBillProductTable() {
finishedBillProductNameColumn.setCellValueFactory(new PropertyValueFactory<ProductInBill, String>("productName"));
finishedBillProductPriceBruttoLabel.setCellValueFactory(new PropertyValueFactory<ProductInBill, Integer>("productPrice"));
}
Also there is an updateBillTable() method to load the necessary ProductInBill objects, save them to an TableList and give it to the table.
private void updateFinishedBillProductTable(Bill bill) {
LOG.info("Start reading all Products from Bill");
for(ProductInBill product : bill.getProducts()){
finishedBillProductCurrent.add(product);
}
finishedBillProductTable.getItems().clear();
if(!finishedBillProductCurrent.isEmpty()) {
for (ProductInBill p : finishedBillProductCurrent) {
finishedBillProductTableList.add(p);
}
//here i want to calculate some other Integer values based on the ProductInBill values and insert them to the table too.
finishedBillProductTable.setItems(finishedBillProductTableList);
}
}
This is all working very good. My problem now is, that I have also a field on my TableView with calculated Integer values which I don't want to save within an object.
Take for example the finishedBillProductNumberColumn. I want iterate on my ArrayList, find all products with the same name and populate the number of the same items to the table.
How can I do this? I found only solutions where I have to use a value from my object to insert something to my TableView.
You just have to write a custom CellValueFactory for those case instead of using premade ones. Using PropertyValueFactory is just an handy short cut to fill cells with members.
For your example:
finishedBillProductNameColumn.setCellValueFactory(new PropertyValueFactory<ProductInBill, String>("productName"));
is just a shorter way to do:
finishedBillProductNameColumn.setCellValueFactory( cellData -> {
ProductInBill productInBill = cellData.getValue();
return data == null ? null : new SimpleStringProperty(productInBill.getProductName());
});
That being said, i have an 100% preference for the second syntax. Because on the first one if you rename the member, and you forgot to change it there, you won't know there is a mistake until you get there in the application. Plus it allow to display different value than just the members.
As a concrete example for your finishedBillProductNumberColumn you could do:
First change the definition(the first Generic type is the one received with cellData.getValue():
#FXML public TableColumn<ProductInBill, Integer> finishedBillProductNumberColumn;
and then define the CellValueFactory you want like:
finishedBillProductNumberColumn.setCellValueFactory( cellData -> {
ProductInBill productInBill = cellData.getValue();
if(productionInBill != null){
Long nbProduct = finishedBillProductTable.getItems().stream().filter(product -> product.getProductName().equals(productInBill.getProductName())).count();
return new SimpleIntegerProperty(nbProduct.intValue()).asObject();
}
return null;
});
Hope it helped!
How can I wrap the text in JFXTreeTableView cells?
My JFoenix TableTreeView column cell factory is created as shown below.
// Set column cell factories and width preference
for (JFXTreeTableColumn<LogEntry, String> column : columnList) {
column.setCellFactory(param ->
new GenericEditableTreeTableCell<>(new TextFieldEditorBuilder()));
column.setPrefWidth(100);
}
After hours of searching, I can't figure out how to get the cells in the tree table to wrap text. I even tried to set the wrap text value for every GenericEditableTreeTableCell to true, but I think I'm also supposed to call the setWrappingWidth() method on something. I tried the following block of code, but I ended up getting a NullPointerException.
// Set column cell factories and width preference
for (JFXTreeTableColumn<LogEntry, String> column : columnList) {
column.setCellFactory(param -> {
GenericEditableTreeTableCell<LogEntry, String> cell =
new GenericEditableTreeTableCell<>(new TextFieldEditorBuilder());
Text text = (Text) cell.getGraphic();
text.setWrappingWidth(1); //null pointer exception
cell.setWrapText(true);
return cell;
});
column.setPrefWidth(100);
}
So, I'm left with the following block of code which runs perfectly fine and displays the table, but the cells do not wrap text.
// Set column cell factories and width preference
for (JFXTreeTableColumn<LogEntry, String> column : columnList) {
column.setCellFactory(param -> {
GenericEditableTreeTableCell<LogEntry, String> cell =
new GenericEditableTreeTableCell<>(new TextFieldEditorBuilder());
// I think I should call setWrappingWidth() on some object here
// but I don't know what object
cell.setWrapText(true);
return cell;
});
column.setPrefWidth(100);
}
The documentation for setting up a JFXTreeTableView can be found here. It doesn't seem to mention anything about wrapping cell text.
Edit: I tried doing it with CSS, but didn't get any results. In fact, the cell.isWrapText() returned false after using this CSS code - meaning that it didn't event set the value to true. I know the block of CSS is working correctly because I can change every element's text fill color with it.
* {
-fx-wrap-text: true;
}
Edit 2: Some people said on other semi-related posts that a scroll pane can cause a Node to think it has a much larger width than what is shown to the user. Since a JavaFX TreeTableView uses a scroll bar when the table is too large, I figured I'd try their solutions. I tried setting the preferred width of the cell - still no results.
cell.setWrapText(true);
cell.setPrefWidth(100);
//cell.setMaxWidth(100); doing this too made no difference
//cell.setMinWidth(100); doing this too made no difference
Edit 3: I think I know the problem! It seems that the row height refuses to let the cell wrap text. If I set the rows minimum height to a large enough value, the cell wraps its text! Now I just need to know how to make the row height adjust dynamically to accommodate the cell when it wants to wrap text.
Edit 4: It appears that the row doesn't allow line breaks which may be the cause of the cell failing to wrap text. It can't wrap the text because the new lines it creates are chomped.
I have used one of your approaches, but also made custom EditorBuilder which has JFXTextArea instead of JFXTextField.
The custom TextAreaEditorBuilder is the following:
public class TextAreaEditorBuilder implements EditorNodeBuilder<String> {
private JFXTextArea textArea;
#Override
public void startEdit() {
Platform.runLater(() -> {
textArea.selectAll();
});
}
#Override
public void cancelEdit() {
}
#Override
public void updateItem(String s, boolean isEmpty) {
Platform.runLater(() -> {
textArea.selectAll();
textArea.requestFocus();
});
}
#Override
public Region createNode(
String value,
DoubleBinding minWidthBinding,
EventHandler<KeyEvent> keyEventsHandler,
ChangeListener<Boolean> focusChangeListener) {
StackPane pane = new StackPane();
pane.setStyle("-fx-padding:-10 0 -10 0");
textArea = new JFXTextArea(value);
textArea.setPrefRowCount(4);
textArea.setWrapText(true);
textArea.minWidthProperty().bind(minWidthBinding.subtract(10));
textArea.setOnKeyPressed(keyEventsHandler);
textArea.focusedProperty().addListener(focusChangeListener);
pane.getChildren().add(textArea);
return ControlUtil.styleNodeWithPadding(pane);
}
#Override
public void setValue(String s) {
textArea.setText(s);
}
#Override
public String getValue() {
return textArea.getText();
}
#Override
public void validateValue() throws Exception {
}
}
and then the configuration for your column goes something like this:
negativeCasingColumn.setCellFactory(param -> {
TextAreaEditorBuilder textAreaEditorBuilder = new TextAreaEditorBuilder();
GenericEditableTreeTableCell<DiagnosticMethodFX, String> cell
= new GenericEditableTreeTableCell<>(textAreaEditorBuilder);
cell.setWrapText(true);
return cell;
});
So basically I'm wrapping the cell, but making sure I use TextArea as editing field. For me this works just fine, but if you want more elegant solution maybe you can check the cancelEdit() method in GenericEditableTreeTableCell, where it sets the content display to text only: setContentDisplay(ContentDisplay.TEXT_ONLY) which is setting of content property in the abstract class Labeled, where the text wrap is set to false. I didn't dig too deep into it, but some workaround can be made for sure.
This code binds a Label to an updating SimpleIntegerPropertyValue which is counting down from 10 - 1.
view.OVERALL_PROGRESS_LABEL.textProperty().bind(timeSeconds.divide(100).asString());
How can I bind specific values depending on what currently this timeSeconds value is? For instance if the value of timeSeconds > 500 then display "Greater" otherwise display "Less".
I have tried binding a method which returns an ObservableValue but it is not function correctly. (just manipulating the numbers to see if there is a change)
private void someMethod(){
view.OVERALL_PROGRESS_LABEL.textProperty().bind(test2());
}
private ObservableValue<? extends String> test2() {
ObservableValue<String> test;
if (timeSeconds.getValue() < 500){
test = timeSeconds.multiply(1000).asString();
} else {
test = timeSeconds.divide(1000).asString();
}
return test;
}
You can use Bindings to create bindings based on condition.
view.OVERALL_PROGRESS_LABEL.textProperty().bind(Bindings.when(timeSeconds.
greaterThan(500)).then("Greater").otherwise("Less"));
consider the following situation:
I am binding the string-property of a TextField to a float-property:
TextField txtField;
SimpleFloatProperty floatprop;
void bind(){
txtField.textProperty().bindBidiretional(floatprop, NumberFormat.getInstance);
}
Furthermore, I want to check whether the value is positive and I want to reset the value if necessary:
floatprop.addListener(new ChangeListener<Number>() {
#Override
public void changed(ObservableValue<? extends Number> ov, Number t, Number t1) {
SimpleFloatProperty prop = (SimpleFloatProperty)ov;
if(t.floatValue() <= 0)
prop.set(t1.floatValue());
else
prop.set(t.floatValue());
}
});
That does not work. No matter what I type into my textfield, the value is not checked, i.e negative values are not reset.
Edit: With a litte bit of distance, I also see why. On setting prop, the changed-event will be triggered for a handy endless recursion. How can I solve this issue?
For me, it worked to do this check at the beginning of changed:
if (t != null && t.equals(t1))
return;
Strangely, this way sometimes fails as well. Then, there is no other way than using a flag, like this:
MyListener implements ChangeListener<...>{
boolean fired;
public void changed(...){
if(fired)
return;
fired = true;
// Change the variable...
fired = false;
}
}
I don't like the latter way, but it looks like there is no alternative.