How to separate a JavaFX Appliciation - model-view-controller

I want to seperate a JavaFX project to model, view and controller.
I use netbeans when creating a JavaFX application.
But I want the code seperate, an own GUI, own logic and a Main class just to start the application (I want 3 seperate classes).
But I am not able to solve this problem.
The automatic created code looks like this:
package at.wueschn.www;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class Main 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();
}
public static void main(String[] args) {
launch(args);
}
}

If you are using NetBeans, first choose File -> New Project. Then JavaFX -> JavaFX FXML Application
Note: This is a basic MVC setup. You could do all of this using pure code. James_D could probably help you with more advanced MCV ideas.
Note: If you are going to take this simple approach, I suggest you download SceneBuilder to help you with the view.
Tutorial

Here is a "Java-only" (i.e. no FXML) example of MVC. Note that there are many different variants of MVC, which is a very loosely-defined pattern. This is a kind of "classical" variant: the model has no knowledge of the view(s) or controller(s) (which is the common theme to all MVC-type designs), the controller has a reference to the model and invokes methods on it, implementing some simple logic, and the view has a reference to both the model and controller; observing the model and updating the view components when the data changes, and invoking methods on the controller in response to user input. Other variants of this pattern (MVVM, MVP, etc) typically vary in the relationship between the view and the controller.
This simple application implements a very basic calculator, which simply knows how to add two single-digit integers.
The model:
import javafx.beans.binding.NumberBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
public class Model {
private boolean firstNumberEntered ;
private final IntegerProperty firstNumber = new SimpleIntegerProperty();
private final IntegerProperty secondNumber = new SimpleIntegerProperty();
private final NumberBinding sum = firstNumber.add(secondNumber);
public Model() {
firstNumber.addListener((obs, oldValue, newValue) -> firstNumberEntered = true );
}
public IntegerProperty firstNumberProperty() {
return firstNumber ;
}
public int getFirstNumber() {
return firstNumberProperty().get();
}
public void setFirstNumber(int number) {
firstNumberProperty().set(number);
}
public IntegerProperty secondNumberProperty() {
return secondNumber ;
}
public int getSecondNumber() {
return secondNumberProperty().get();
}
public void setSecondNumber(int number) {
secondNumberProperty().set(number);
}
public NumberBinding sumBinding() {
return sum ;
}
public int getSum() {
return sum.intValue();
}
public boolean isFirstNumberEntered() {
return firstNumberEntered ;
}
public void reset() {
setFirstNumber(0);
setSecondNumber(0);
firstNumberEntered = false ;
}
}
The controller:
public class Controller {
private final Model model ;
public Controller(Model model) {
this.model = model ;
}
public void enterFirstNumber(int number) {
model.setFirstNumber(number);
}
public void enterSecondNumber(int number) {
model.setSecondNumber(number);
}
public void clear() {
model.reset();
}
public void enterNumber(int number) {
if (model.isFirstNumberEntered()) {
enterSecondNumber(number) ;
} else {
enterFirstNumber(number);
}
}
}
The view:
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.RowConstraints;
public class View {
private final BorderPane root ;
private final Controller controller ;
public View(Model model, Controller controller) {
this.controller = controller ;
root = new BorderPane();
GridPane buttons = new GridPane();
configureButtons(buttons);
createAndAddButtons(controller, buttons);
Label resultLabel = new Label();
configureDisplay(model, resultLabel);
root.setTop(resultLabel);
root.setCenter(buttons);
root.setStyle("-fx-font-size: 36pt;");
}
private void configureDisplay(Model model, Label resultLabel) {
BorderPane.setAlignment(resultLabel, Pos.CENTER_RIGHT);
BorderPane.setMargin(resultLabel, new Insets(5));
resultLabel.textProperty().bind(Bindings.createStringBinding(
() -> String.format("%d + %d = %d", model.getFirstNumber(), model.getSecondNumber(), model.getSum()),
model.firstNumberProperty(), model.secondNumberProperty(), model.sumBinding()));
}
private void createAndAddButtons(Controller controller, GridPane buttons) {
for (int i = 1 ; i <= 9 ; i++) {
int row = (9 - i) / 3 ;
int column = (i -1) % 3 ;
buttons.add(createNumberButton(i), column, row);
}
buttons.add(createNumberButton(0), 0, 3);
Button clearButton = createButton("C");
clearButton.setOnAction(e -> controller.clear());
buttons.add(clearButton, 1, 3, 2, 1);
}
private void configureButtons(GridPane buttons) {
for (int row = 0 ; row < 4 ; row++) {
RowConstraints rc = new RowConstraints();
rc.setFillHeight(true);
rc.setPercentHeight(100.0 / 4);
buttons.getRowConstraints().add(rc);
}
for (int column = 0 ; column < 3 ; column++) {
ColumnConstraints cc = new ColumnConstraints();
cc.setFillWidth(true);
cc.setPercentWidth(100.0 / 3);
buttons.getColumnConstraints().add(cc);
}
buttons.setVgap(5);
buttons.setHgap(5);
buttons.setPadding(new Insets(5));
}
public Parent getRoot() {
return root ;
}
private Button createNumberButton(int number) {
Button button = createButton(Integer.toString(number));
button.setOnAction(e -> controller.enterNumber(number));
return button ;
}
private Button createButton(String text) {
Button button = new Button(text);
button.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
return button ;
}
}
and finally the "main" class which creates each piece and displays the view in a window:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class TrivialCalcaulatorApp extends Application {
#Override
public void start(Stage primaryStage) {
Model model = new Model();
Controller controller = new Controller(model);
View view = new View(model, controller);
Scene scene = new Scene(view.getRoot());
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}

Related

JavaFX resets my colors when adding buttons

I am using JavaFx to create a front-end to the cli application called spicetify. I am not using an fxml file for the layout instead I am using different classes for layout purposes.
One such class is the Sidebar class. In it I define how the sidebar should look and then create an object of it on the window/page that I need. Whenever I add buttons to the sidebar The colors of the window where the Sidebar object is created go blank/white.
I am unable to find anything by googling and hope that the information provided is enough.
Screenshot of the window without buttons
Screenshot of the window with buttons
Project Structure
Sidebar class:
package spicetify;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
public class Sidebar {
BaseWindow baseWindow = new BaseWindow();
private String[] buttonIconLocation = {
"file:src/main/resources/spicetify/images/buttons/home.png",
"file:src/main/resources/spicetify/images/buttons/theme.png",
"file:src/main/resources/spicetify/images/buttons/extension.png",
"file:src/main/resources/spicetify/images/buttons/edit.png",
};
private ImageView[] buttonIcon = new ImageView[4];
private VBox sidebar;
private Button[] buttons = new Button[4];
public Sidebar(int height, int width, String html, Parent root){
this.sidebar = new VBox();
this.sidebar.setPrefHeight(height);
this.sidebar.setPrefWidth(width);
this.sidebar.setStyle("-fx-background-color: " + html);
for (int i = 0; i < buttonIcon.length; i++){
buttonIcon[i] = new ImageView(buttonIconLocation[i]);
baseWindow.transform(buttonIcon[i], 50, 50, true);
}
for (int i = 0; i < buttons.length; i++){
buttons[i] = new Button("", buttonIcon[i]);
sidebar.getChildren().add(buttons[i]);
buttons[i].setTranslateX(20);
buttons[i].setTranslateY(i * 100);
buttons[i].setStyle("-fx-background-color: " + html);
}
}
public String[] getButtonIconLocation() {
return buttonIconLocation;
}
public void setButtonIconLocation(String[] buttonIconLocation) {
this.buttonIconLocation = buttonIconLocation;
}
public ImageView[] getButtonIcon() {
return buttonIcon;
}
public void setButtonIcon(ImageView[] buttonIcon) {
this.buttonIcon = buttonIcon;
}
public VBox getSidebar() {
return sidebar;
}
public void setSidebar(VBox sidebar) {
this.sidebar = sidebar;
}
}
BaseWindow class:
package spicetify;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.image.ImageView;
import javafx.stage.Stage;
public class BaseWindow{
private Parent root;
private Scene scene;
private Stage stage;
private String title;
private int width;
private int height;
private int minWidth;
private int minHeight;
private String htmlColor;
public int getMinWidth() {
return minWidth;
}
public void setMinWidth(int minWidth) {
this.minWidth = minWidth;
}
public int getMinHeight() {
return minHeight;
}
public void setMinHeight(int minHeight) {
this.minHeight = minHeight;
}
public String getHtmlColor() {
return htmlColor;
}
public void setHtmlColor(String htmlColor) {
this.htmlColor = htmlColor;
}
public Parent getRoot() {
return root;
}
public void setRoot(Parent root) {
this.root = root;
}
public Scene getScene() {
return scene;
}
public void setScene(Scene scene) {
this.scene = scene;
}
public Stage getStage() {
return stage;
}
public void setStage(Stage stage) {
this.stage = stage;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
public void transform(ImageView view, int width, int height, boolean preserveRatio){
view.setFitWidth(width);
view.setFitHeight(height);
view.setPreserveRatio(preserveRatio);
}
public void start(Stage stage){
stage.setScene(scene);
stage.setTitle(title);
stage.show();
}
}
HomeWindow class:
package spicetify;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class HomeWindow extends BaseWindow {
private ImageView logoView;
private ImageView preview;
private Parent root;
private Scene scene;
BaseWindow baseWindow = new BaseWindow();
Sidebar sidebar = new Sidebar(baseWindow.getHeight(), 100, "#282828", root);
public HomeWindow(int width, int height, String html){
this.logoView = new ImageView(new Image("file:src/main/resources/spicetify/images/essentials/logo.png"));
this.preview = new ImageView(new Image("file:src/main/resources/spicetify/images/essentials/Preview.png"));
this.root = new BorderPane();
baseWindow.setWidth(width);
baseWindow.setMinWidth(width);
baseWindow.setHeight(height);
baseWindow.setMinHeight(height);
baseWindow.setHtmlColor(html);
}
public void start(Stage stage){
((BorderPane) root).setLeft(sidebar.getSidebar());
baseWindow.transform(logoView, 600, 700, true);
((BorderPane) root).setCenter(logoView);
baseWindow.setStage(stage);
baseWindow.setTitle("Spicetify");
baseWindow.setRoot(root);
baseWindow.setScene(new Scene(baseWindow.getRoot(), baseWindow.getWidth(), baseWindow.getHeight(), Color.web(baseWindow.getHtmlColor())));
baseWindow.getStage().setScene(baseWindow.getScene());
baseWindow.getStage().setMinWidth(baseWindow.getMinWidth());
baseWindow.getStage().setMinHeight(baseWindow.getMinHeight());
baseWindow.getStage().setTitle(baseWindow.getTitle());
baseWindow.getStage().show();
}
}
Default Modena CSS has a gray background in panes.
When controls are loaded CSS will be applied to the entire scene, as controls use CSS.
Without any controls, CSS (for performance in straight rendering of graphics primitives) will only be applied to the scene if you specifically apply it.
For more information, and steps you can take to remove the default color from pane backgrounds, see the related question:
JavaFX 8 tooltip removes transparency of stage

How to create a multi-column Combobox in JavaFX?

I am trying to create a ComboBox that displays multiple columns in its dropdown menu.
Here is a screenshot that shows how I want it to look:
Any suggestions?
The only solution that is in my head is to create a custom container by extending ComboBox and customizing it with multiple columns.
But does JavaFX even provide me the option to create a custom UI container?
How do you create a custom UI container and how to use it in FXML?
You do not need to extend ComboBox to create a similar layout. Instead, you just need to provide your own implementation of a CellFactory.
By creating a custom CellFactory, you can control how the items in your ComboBox are displayed by providing your own Listcell (the item that is actually selectable in the dropdown menu).
I am certain there are many ways to accomplish this, but for this example, I'm going to use a GridPane as the root layout for my ListCell.
The complete example below has comments throughout as well:
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class Main extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
// Simple Interface
VBox root = new VBox(10);
root.setAlignment(Pos.CENTER);
root.setPadding(new Insets(10));
// List of sample Persons
ObservableList<Person> persons = FXCollections.observableArrayList();
persons.addAll(
new Person("Maria Anders", "Sales Representative", "Zurich"),
new Person("Ana Trujillo", "Owner", "Sydney"),
new Person("Thomas Hardy", "Order Administrator", "Dallas")
);
// Create a simple ComboBox of Persons
ComboBox<Person> cboPersons = new ComboBox<>();
cboPersons.setItems(persons);
// We need a StringConverter in order to ensure the selected item is displayed properly
// For this sample, we only want the Person's name to be displayed
cboPersons.setConverter(new StringConverter<Person>() {
#Override
public String toString(Person person) {
return person.getName();
}
#Override
public Person fromString(String string) {
return null;
}
});
// Provide our own CellFactory to control how items are displayed
cboPersons.setCellFactory(cell -> new ListCell<Person>() {
// Create our layout here to be reused for each ListCell
GridPane gridPane = new GridPane();
Label lblName = new Label();
Label lblTitle = new Label();
Label lblLocation = new Label();
// Static block to configure our layout
{
// Ensure all our column widths are constant
gridPane.getColumnConstraints().addAll(
new ColumnConstraints(100, 100, 100),
new ColumnConstraints(100, 100, 100),
new ColumnConstraints(100, 100, 100)
);
gridPane.add(lblName, 0, 1);
gridPane.add(lblTitle, 1, 1);
gridPane.add(lblLocation, 2, 1);
}
// We override the updateItem() method in order to provide our own layout for this Cell's graphicProperty
#Override
protected void updateItem(Person person, boolean empty) {
super.updateItem(person, empty);
if (!empty && person != null) {
// Update our Labels
lblName.setText(person.getName());
lblTitle.setText(person.getTitle());
lblLocation.setText(person.getLocation());
// Set this ListCell's graphicProperty to display our GridPane
setGraphic(gridPane);
} else {
// Nothing to display here
setGraphic(null);
}
}
});
// Add the ComboBox to the scene
root.getChildren().addAll(
new Label("Select Person:"),
cboPersons
);
// Show the stage
primaryStage.setScene(new Scene(root));
primaryStage.setTitle("Sample");
primaryStage.show();
}
}
// Simple Person class to represent our...Persons
class Person {
private final StringProperty name = new SimpleStringProperty();
private final StringProperty title = new SimpleStringProperty();
private final StringProperty location = new SimpleStringProperty();
Person(String name, String title, String location) {
this.name.set(name);
this.title.set(title);
this.location.set(location);
}
public String getName() {
return name.get();
}
public void setName(String name) {
this.name.set(name);
}
public StringProperty nameProperty() {
return name;
}
public String getTitle() {
return title.get();
}
public void setTitle(String title) {
this.title.set(title);
}
public StringProperty titleProperty() {
return title;
}
public String getLocation() {
return location.get();
}
public void setLocation(String location) {
this.location.set(location);
}
public StringProperty locationProperty() {
return location;
}
}
The Results:

Recyclerview Filter not working.its not searching the elements

when i filter recyclerview it shows Not found .My Searchview not working.when i run the code its result in Not Found i think there is problem in onQueryTextChange
myfilter function also did not work
#Override
public boolean onQueryTextSubmit(String query) {
Toast.makeText(SecondActivity1.this, "Name is : " + query, Toast.LENGTH_SHORT).show();
return false;
}
#Override
public boolean onQueryTextChange(String newText) {
final List<DatabaseModel> filteredModelList = filter(dbList, newText);
if (filteredModelList.size() > 0) {
// Toast.makeText(SecondActivity1.this, "Found", Toast.LENGTH_SHORT).show();
recyclerAdapter.setFilter(filteredModelList);
return true;
} else {
Toast.makeText(SecondActivity1.this, "Not Found", Toast.LENGTH_SHORT).show();
return false;
}
private List filter(List models, String query) {
query = query.toLowerCase();
recyclerAdapter.notifyDataSetChanged();
final List<DatabaseModel> filteredModelList = new ArrayList<>();
// mRecyclerView.setLayoutManager(new LinearLayoutManager(SecondActivity1.this));
// mRecyclerView.setAdapter(RecyclerAdapter);
for (DatabaseModel model : models) {
final String text = model.getName().toLowerCase();
if (text.contains(query)) {
filteredModelList.add(model);
}
}
return filteredModelList;
//
}
here is filter method which recieve parameter(dblist,newtext) filter method recieves these method when i use toast its show that it takes newText But didnot filter this.i checked many sites but this is same in many sites points.when i enter name toast shows name which i enter but it did not filter
RecyclerAdapter.java
package com.example.prabhu.databasedemo;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Filter;
import android.widget.Filterable;
import android.widget.TextView;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.List;
/**
* Created by user_adnig on 11/14/15.
*/
public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerAdapter.ViewHolder> {
List<DatabaseModel> dbList;
static Context context;
RecyclerAdapter(Context context, List<DatabaseModel> dbList ){
this.dbList = new ArrayList<>();
this.context = context;
this.dbList = (ArrayList<DatabaseModel>) dbList;
}
#Override
public RecyclerAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemLayoutView = LayoutInflater.from(parent.getContext()).inflate(
R.layout.item_row, null);
// create ViewHolder
ViewHolder viewHolder = new ViewHolder(itemLayoutView);
return viewHolder;
}
#Override
public void onBindViewHolder(RecyclerAdapter.ViewHolder holder, int position) {
holder.name.setText(dbList.get(position).getName());
holder.email.setText(dbList.get(position).getEmail());
}
#Override
public int getItemCount() {
return dbList.size();
}
public void setFilter(List<DatabaseModel> countryModels) {
// Toast.makeText(RecyclerAdapter.this,"Method is called", Toast.LENGTH_SHORT).show();
dbList = new ArrayList<>();
dbList.addAll(countryModels);
notifyDataSetChanged();
}
public static class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
public TextView name,email;
public ViewHolder(View itemLayoutView) {
super(itemLayoutView);
name = (TextView) itemLayoutView
.findViewById(R.id.rvname);
email = (TextView)itemLayoutView.findViewById(R.id.rvemail);
itemLayoutView.setOnClickListener(this);
}
#Override
public void onClick(View v) {
Intent intent = new Intent(context,DetailsActivity.class);
Bundle extras = new Bundle();
extras.putInt("position",getAdapterPosition());
intent.putExtras(extras);
/*
int i=getAdapterPosition();
intent.putExtra("position", getAdapterPosition());*/
context.startActivity(intent);
Toast.makeText(RecyclerAdapter.context, "you have clicked Row " + getAdapterPosition(), Toast.LENGTH_LONG).show();
}
}
}
this is my recyclerAdapterCode.i also used Recycleradapter.setFilter(filterModeList) method but it did not work for me.i think in my set filter method error which i did not solve yet.
. But when I clear the search widget I don't get the full list instead I get the empty RecyclerView.

Get row from selected cell in TableView in JavaFX when setCellSelectionEnabled(true)

I have the following code which works great when I have standard row selection (always single, never multi).
//This is needed to set the X & Y coordinates of the stage for edit.
myTable.setRowFactory(tableView -> {
TableRow<MyDTO> row = new TableRow<MyDTO>();
row.selectedProperty().addListener((obs, wasSelected, isNowSelected) -> {
if (isNowSelected) {
lastSelectedRow.set(row);
}
});
return row ;
});
I am using the row to get the bounds in parent so that when a user selects to edit that row, I can pop a modal window up under the row for them to edit this.
However, my table is also editable for the common fields where there is no look up needed, etc. In that case I want to edit in the table. All this is working, however to make it more user friendly, I want to have cell selection turned on, but when I do that, the row.selectedProptery() listener doesn't fire.
How can I accomplish that, without trying to listen to the selectedProperty() of each cell?
Thanks
I don't think there's a way to do this without registering a listener with the selection property of each cell, via a cell factory on each table column.
However, this isn't too difficult, and can be done both generically (i.e. with the same code no matter the type of the table column) and also respecting any other cell factory behavior you need. Here is a SSCCE:
import java.util.Random;
import java.util.function.Function;
import javafx.application.Application;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Callback;
public class SelectedTableCellTracking extends Application {
private final ObjectProperty<TableCell<?,?>> selectedCell = new SimpleObjectProperty<>();
#Override
public void start(Stage primaryStage) {
TableView<Item> table = new TableView<>();
TableColumn<Item, String> itemCol = column("Item", Item::nameProperty);
TableColumn<Item, Number> valueCol = column("Value", Item::valueProperty);
table.getColumns().add(itemCol);
table.getColumns().add(valueCol);
Random rng = new Random();
for (int i = 1 ; i <= 100; i++) {
table.getItems().add(new Item("Item "+i, rng.nextInt(1000)));
}
table.getSelectionModel().setCellSelectionEnabled(true);
Rectangle highlight = new Rectangle();
highlight.setManaged(false);
highlight.setHeight(12);
highlight.setFill(Color.CORAL);
StackPane root = new StackPane(table, highlight);
selectedCell.addListener((obs, oldCell, newCell) -> {
if (newCell == null) {
highlight.setVisible(false);
} else {
highlight.setVisible(true);
highlight.setX(newCell.localToScene(newCell.getBoundsInLocal()).getMinX());
highlight.setWidth(newCell.getWidth());
highlight.setY(newCell.localToScene(newCell.getBoundsInLocal()).getMaxY());
}
});
table.getColumns().forEach(this::addCellSelectionListenerToColumn);
Scene scene = new Scene(root, 800, 800);
primaryStage.setScene(scene);
primaryStage.show();
}
private <S,T> void addCellSelectionListenerToColumn(TableColumn<S,T> col) {
Callback<TableColumn<S,T>, TableCell<S,T>> currentCellFactory = col.getCellFactory();
col.setCellFactory(tc -> {
TableCell<S,T> cell = currentCellFactory.call(tc);
cell.selectedProperty().addListener((obs, wasSelected, isNowSelected) -> {
if (isNowSelected) {
selectedCell.set(cell);
}
});
return cell ;
});
}
private static <S,T> TableColumn<S,T> column(String title, Function<S, ObservableValue<T>> property) {
TableColumn<S,T> col = new TableColumn<>(title);
col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
return col ;
}
public static class Item {
private final StringProperty name = new SimpleStringProperty();
private final IntegerProperty value = new SimpleIntegerProperty();
public Item(String name, int value) {
setName(name);
setValue(value);
}
public final StringProperty nameProperty() {
return this.name;
}
public final String getName() {
return this.nameProperty().get();
}
public final void setName(final String name) {
this.nameProperty().set(name);
}
public final IntegerProperty valueProperty() {
return this.value;
}
public final int getValue() {
return this.valueProperty().get();
}
public final void setValue(final int value) {
this.valueProperty().set(value);
}
}
public static void main(String[] args) {
launch(args);
}
}

Can't stop javafx tables from ignoring my the setter function validation

I'm using javafx to do some table stuff. I want to validate my textfields in the myTextRow Class. In the "setText2" method I check the input if it is not bigger than 6 symbols, but it has no effects at all.
import java.util.ArrayList;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TextArea;
import javafx.util.Callback;
import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class Supermain extends Application {
#Override
public void start(Stage primaryStage) {
ArrayList myindizes=new ArrayList();
final TableView<myTextRow> table = new TableView<>();
table.setEditable(true);
table.setStyle("-fx-text-wrap: true;");
//Table columns
TableColumn<myTextRow, String> clmID = new TableColumn<>("ID");
clmID.setMinWidth(160);
clmID.setCellValueFactory(new PropertyValueFactory<>("ID"));
TableColumn<myTextRow, String> clmtext = new TableColumn<>("Text");
clmtext.setMinWidth(160);
clmtext.setCellValueFactory(new PropertyValueFactory<>("text"));
clmtext.setCellFactory(new TextFieldCellFactory());
TableColumn<myTextRow, String> clmtext2 = new TableColumn<>("Text2");
clmtext2.setMinWidth(160);
clmtext2.setCellValueFactory(new PropertyValueFactory<>("text2"));
clmtext2.setCellFactory(new TextFieldCellFactory());
//Add data
final ObservableList<myTextRow> data = FXCollections.observableArrayList(
new myTextRow(5, "Lorem","bla"),
new myTextRow(2, "Ipsum","bla")
);
table.getColumns().addAll(clmID, clmtext,clmtext2);
table.setItems(data);
HBox hBox = new HBox();
hBox.setSpacing(5.0);
hBox.setPadding(new Insets(5, 5, 5, 5));
Button btn = new Button();
btn.setText("Get Data");
btn.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
for (myTextRow data1 : data) {
System.out.println("data:" + data1.getText2());
}
}
});
hBox.getChildren().add(btn);
BorderPane pane = new BorderPane();
pane.setTop(hBox);
pane.setCenter(table);
primaryStage.setScene(new Scene(pane, 640, 480));
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
public static class TextFieldCellFactory
implements Callback<TableColumn<myTextRow, String>, TableCell<myTextRow, String>> {
#Override
public TableCell<myTextRow, String> call(TableColumn<myTextRow, String> param) {
TextFieldCell textFieldCell = new TextFieldCell();
return textFieldCell;
}
public static class TextFieldCell extends TableCell<myTextRow, String> {
private TextArea textField;
private StringProperty boundToCurrently = null;
public TextFieldCell() {
textField = new TextArea();
textField.setWrapText(true);
textField.setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2);
this.setGraphic(textField);
}
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (!empty) {
// Show the Text Field
this.setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
// myindizes.add(getIndex());
// Retrieve the actual String Property that should be bound to the TextField
// If the TextField is currently bound to a different StringProperty
// Unbind the old property and rebind to the new one
ObservableValue<String> ov = getTableColumn().getCellObservableValue(getIndex());
SimpleStringProperty sp = (SimpleStringProperty) ov;
if (this.boundToCurrently == null) {
this.boundToCurrently = sp;
this.textField.textProperty().bindBidirectional(sp);
} else if (this.boundToCurrently != sp) {
this.textField.textProperty().unbindBidirectional(this.boundToCurrently);
this.boundToCurrently = sp;
this.textField.textProperty().bindBidirectional(this.boundToCurrently);
}
double height = real_lines_height(textField.getText(), this.getWidth(), 30, 22);
textField.setPrefHeight(height);
textField.setMaxHeight(height);
textField.setMaxHeight(Double.MAX_VALUE);
// if height bigger than the biggest height in the row
//-> change all heights of the row(textfields ()typeof textarea) to this height
// else leave the height as it is
//System.out.println("item=" + item + " ObservableValue<String>=" + ov.getValue());
//this.textField.setText(item); // No longer need this!!!
} else {
this.setContentDisplay(ContentDisplay.TEXT_ONLY);
}
}
}
}
public class myTextRow {
private final SimpleIntegerProperty ID;
private final SimpleStringProperty text;
private final SimpleStringProperty text2;
public myTextRow(int ID, String text,String text2) {
this.ID = new SimpleIntegerProperty(ID);
this.text = new SimpleStringProperty(text);
this.text2 = new SimpleStringProperty(text2);
}
public void setID(int id) {
this.ID.set(id);
}
public void setText(String text) {
this.text.set(text);
}
public void setText2(String text) {
if(text2check(text)){
this.text2.set(text);}
else
{System.out.println("wrong value!!!");}
}
public int getID() {
return ID.get();
}
public String getText() {
return text.get();
}
public StringProperty textProperty() {
return text;
}
public String getText2() {
return text2.get();
}
public StringProperty text2Property() {
return text2;
}
public IntegerProperty IDProperty() {
return ID;
}
public boolean text2check(String t)
{
if(t.length()>6)return false;
return true;
}
}
private static double real_lines_height(String s, double width, double heightCorrector, double widthCorrector) {
HBox h = new HBox();
Label l = new Label("Text");
h.getChildren().add(l);
Scene sc = new Scene(h);
l.applyCss();
double line_height = l.prefHeight(-1);
int new_lines = s.replaceAll("[^\r\n|\r|\n]", "").length();
// System.out.println("new lines= "+new_lines);
String[] lines = s.split("\r\n|\r|\n");
// System.out.println("line count func= "+ lines.length);
int count = 0;
//double rest=0;
for (int i = 0; i < lines.length; i++) {
double text_width = get_text_width(lines[i]);
double plus_lines = Math.ceil(text_width / (width - widthCorrector));
if (plus_lines > 1) {
count += plus_lines;
//rest+= (text_width / (width-widthCorrector)) - plus_lines;
} else {
count += 1;
}
}
//count+=(int) Math.ceil(rest);
count += new_lines - lines.length;
return count * line_height + heightCorrector;
}
private static double get_text_width(String s) {
HBox h = new HBox();
Label l = new Label(s);
l.setWrapText(false);
h.getChildren().add(l);
Scene sc = new Scene(h);
l.applyCss();
// System.out.println("dubbyloop.FXMLDocumentController.get_text_width(): "+l.prefWidth(-1));
return l.prefWidth(-1);
}
}
A rule of the JavaFX Properties pattern is that for a property x, invoking xProperty().setValue(value) should always be identical to invoking setX(value). Your validation makes this not true. The binding your cell implementation uses invokes the setValue method on the property, which is why it bypasses your validation check.
(Side note: in all the code I am going to change the names so that they adhere to proper naming conventions.)
The default way to implement a property in this pattern is:
public class MyTextRow {
private final StringProperty text = new SimpleStringProperty();
public StringProperty textProperty() {
return text ;
}
public final void setText(String text) {
textProperty().set(text);
}
public final String getText() {
return textProperty().get();
}
}
By having the set/get methods delegate to the appropriate property methods, you are guaranteed these rules are enforced, even if the textProperty() methods is overridden in a subclass. Making the set and get methods final ensures that the rule is not broken by a subclass overriding those methods.
One approach might be to override the set and setValue methods in the property, as follows:
public class MyTextRow {
private final StringProperty text2 = new StringPropertyBase() {
#Override
public String getName() {
return "text2";
}
#Override
public Object getBean() {
return MyTextRow.this ;
}
#Override
public void setValue(String value) {
if (text2Check(value)) {
super.setValue(value);
}
}
#Override
public void set(String value) {
if (text2Check(value)) {
super.set(value);
}
}
}
public StringProperty text2Property() {
return text2 ;
}
public final void setText2(String text2) {
text2Property().set(text2);
}
public final String getText2() {
return text2Property().get();
}
// ...
}
however, I think this will break the bidirectional binding that you have with the text property in the TextArea (basically, there is no way to communicate back to the text area when a change is vetoed, so the text area will not know to revert to the previous value). One fix would be to implement your cell using listeners on the properties instead of bindings. You could use a TextFormatter on the text area that simply updates the property and vetoes the text change if the change doesn't occur.
Here is a complete SSCCE using this approach:
import java.util.function.Function;
import java.util.function.UnaryOperator;
import javafx.application.Application;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.property.StringPropertyBase;
import javafx.scene.Scene;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextFormatter;
import javafx.scene.control.TextFormatter.Change;
import javafx.stage.Stage;
public class VetoStringChange extends Application {
#Override
public void start(Stage primaryStage) {
TableView<Item> table = new TableView<>();
table.setEditable(true);
table.getColumns().add(column("Item", Item::nameProperty));
table.getColumns().add(column("Description", Item::descriptionProperty));
for (int i = 1; i <= 20 ; i++) {
table.getItems().add(new Item("Item "+i, ""));
}
primaryStage.setScene(new Scene(table, 600, 600));
primaryStage.show();
}
public static <S> TableColumn<S,String> column(String title, Function<S,Property<String>> property) {
TableColumn<S,String> col = new TableColumn<>(title);
col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
col.setCellFactory(tc -> new TextAreaCell<S>(property));
col.setPrefWidth(200);
return col ;
}
public static class TextAreaCell<S> extends TableCell<S, String> {
private TextArea textArea ;
public TextAreaCell(Function<S, Property<String>> propertyAccessor) {
textArea = new TextArea();
textArea.setWrapText(true);
textArea.setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2);
textArea.setMaxHeight(Double.MAX_VALUE);
UnaryOperator<Change> filter = c -> {
String proposedText = c.getControlNewText() ;
Property<String> prop = propertyAccessor.apply(getTableView().getItems().get(getIndex()));
prop.setValue(proposedText);
if (prop.getValue().equals(proposedText)) {
return c ;
} else {
return null ;
}
};
textArea.setTextFormatter(new TextFormatter<String>(filter));
this.setGraphic(textArea);
}
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (!empty) {
if (! textArea.getText().equals(item)) {
textArea.setText(item);
}
// Show the Text Field
this.setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
} else {
this.setContentDisplay(ContentDisplay.TEXT_ONLY);
}
}
}
public static class Item {
private final StringProperty name = new StringPropertyBase() {
#Override
public Object getBean() {
return Item.this;
}
#Override
public String getName() {
return "name" ;
}
#Override
public void set(String value) {
if (checkValue(value)) {
super.set(value);
}
}
#Override
public void setValue(String value) {
if (checkValue(value)) {
super.setValue(value);
}
}
};
private final StringProperty description = new SimpleStringProperty();
public Item(String name, String description) {
setName(name);
setDescription(description);
}
private boolean checkValue(String value) {
return value.length() <= 6 ;
}
public final StringProperty nameProperty() {
return this.name;
}
public final String getName() {
return this.nameProperty().get();
}
public final void setName(final String name) {
this.nameProperty().set(name);
}
public final StringProperty descriptionProperty() {
return this.description;
}
public final String getDescription() {
return this.descriptionProperty().get();
}
public final void setDescription(final String description) {
this.descriptionProperty().set(description);
}
}
public static void main(String[] args) {
launch(args);
}
}
Another approach is to allow a "commit and revert" type strategy on your property:
public class MyTextRow {
private final StringProperty text2 = new SimpleStringProperty();
public MyTextRow() {
text2.addListener((obs, oldText, newText) -> {
if (! checkText2(newText)) {
// sanity check:
if (checkText2(oldText)) {
text2.set(oldText);
}
}
});
}
public StringProperty text2Property() {
return text ;
}
public final void setText2(String text2) {
text2Property().set(text2);
}
public final String getText2() {
return text2Property().get();
}
}
In general I dislike validation by listening for an invalid value and reverting like this, because other listeners to the property will see all the changes, including changes to and from invalid values. However, this might be the best option in this case.
Finally, you could consider vetoing invalid changes as in the first option, and also setting a TextFormatter on the control in the cell that simply doesn't allow text entry that results in an invalid string. This isn't always possible from a usability perspective (e.g. if empty strings are invalid, you almost always want to allow the user to temporarily delete all the text), and it means keeping two validation checks in sync in your code, which is a pain.

Resources