Very new to JavaFX and lacking a bit of knowledge in the way controllers work but here it goes.
My problem is easy. I need to update a Label on the screen during runtime.
This problem has been addressed on this site before:
Java FX change Label text
Java FX change Label text 2
Passing Parameters
Also, are these links describing the same thing but done differently?
But my program is a little different.
The flow of the program is as follows:
The Main Stage has several Objects that extends Pane with a Label inside. These Objects can be right clicked which opens a context menu. An option in the context menu opens a new window with RadioButtons.
The idea is to select one of the RadioButtons and use that string to rewrite the Label back on the Main Stage.
However my code only works once, the first time. All subsequent changes are not shown on the screen. I can even output the Label that was changed to the Console and it shows the correct value, but never updates the Label on the Stage.
Class that has the Label on the screen:
import javafx.scene.control.Label;
import javafx.scene.layout.Pane;
public class CoursePane extends Pane {
private Label courseID;
public CoursePane(Label courseID) {
this.courseID = courseID;
}
public String getCourseID() {
return courseID.getText();
}
public Label getCourseLabel() {
return courseID;
}
public void setCourseID(String ID) {
courseID.setText(ID);
}
}
The Context Menu Class that invokes the menu:
public class CourseContext {
static String fxmlfile;
private static Object paneSrc; //the CoursePane that was clicked on
public static void start(CoursePane pane, String courseSrc) {
//Context Menu
ContextMenu contextMenu = new ContextMenu();
//MenuItems
MenuItem item4 = new MenuItem("option");
//add items to context menu
contextMenu.getItems().addAll(item4);
pane.addEventHandler(MouseEvent.MOUSE_PRESSED, new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
if (event.isSecondaryButtonDown()) {
//the coursePane that was right clicked on
paneSrc = event.getSource().toString();
contextMenu.show(pane, event.getScreenX(), event.getScreenY());
item4.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
try {
FXMLLoader loader = new FXMLLoader(getClass().getClassLoader().getResource("my fxml file for the radio Buttons"));
Parent root= loader.load();
ElectiveController electiveController = loader.getController();
electiveController.start( "pass the coursePane that was right clicked on" );
Scene scene = new Scene(root);
Stage stage = new Stage();
stage.setScene(scene);
stage.setTitle("Set Elective");
stage.show();
}
catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
});
}
}
And finally, the class that has the value that Label is supposed to be set to:
public class ElectiveController {
#FXML
private Button setButton;
private RadioButton chk;
//the pane that was right clicked on
private static String courseSource;
public void start(Course courseSrc) { //courseSrc: the Pane you right clicked on
courseSource = courseSrc.getCoursenamenumber().getValue();
}//end start
//sets the course pane with the selected elective radio button
#FXML
private void setElective() {
chk = (RadioButton)humElectiveGroup.getSelectedToggle();
//This is supposed to set the value for the coursePane Object to show on the screen!
MainStage.getCoursePanes().get(courseSource).setCourseID(chk.getText());
Stage stage = (Stage) setButton.getScene().getWindow();
stage.close();
}
}
I have looked into dependency injection, tried binding and passing parameters but getting the same results. I know this is straight forward, any help is appreciated! Thanks.
Here is an mcve of how you could wire up the different parts.
- It can be copy pasted into a single file and invoked.
- Note that it is not meant to represent or mock your application. It is meant to demonstrate a (very basic and simplistic) solution for the issue
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
//main class
public class UpdateViewByMenu extends Application {
private Controller controller;
#Override
public void start(Stage stage) throws Exception {
BorderPane root = new BorderPane();
controller = new Controller();
root.setTop(controller.getMenu());
root.setBottom(controller.getView());
Scene scene = new Scene(root, 350,200);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) { launch(args);}
}
//controller which "wires" view to model
class Controller {
private Model model;
private View view;
private TopMenu menu;
public Controller() {
model = new Model();
view = new View();
menu = new TopMenu();
//wire up menu to model : menu changes update model
menu.getMenuTextProperty().addListener(
e-> model.setCourseID(menu.getMenuTextProperty().get()));
//wire model to view: change in model update view
view. geLabelTextProerty().bind(model.getCourseIDProperty());
//set initial value to show
menu.getMenuTextProperty().set("Not set");
}
Model getModel() {return model;}
Pane getView() { return view;}
MenuBar getMenu() { return menu; }
}
//model which represent the data, in this case label info
class Model{
SimpleStringProperty courseIdProperty;
Model(){
courseIdProperty = new SimpleStringProperty();
}
StringProperty getCourseIDProperty() {
return courseIdProperty;
}
void setCourseID(String id) {
courseIdProperty.set(id);
}
}
//represents main view, in this case a container for a label
class View extends HBox {
private Label courseID;
View() {
courseID = new Label();
getChildren().add(courseID);
}
StringProperty geLabelTextProerty() {
return courseID.textProperty();
}
}
//menu
class TopMenu extends MenuBar{
SimpleStringProperty menuTextProperty;
TopMenu() {
menuTextProperty = new SimpleStringProperty();
Menu menu = new Menu("Select id");
MenuItem item1 = getMenuItem("10021");
MenuItem item2 = getMenuItem("10022");
MenuItem item3 = getMenuItem("10023");
MenuItem item4 = getMenuItem("10024");
menu.getItems().addAll(item1, item2, item3, item4);
getMenus().add(menu);
}
MenuItem getMenuItem(String text) {
MenuItem item = new MenuItem(text);
item.setOnAction(e -> menuTextProperty.set(item.textProperty().get()));
return item;
}
StringProperty getMenuTextProperty() {
return menuTextProperty;
}
}
Do not hesitate to ask for clarifications as needed.
Related
How can I make a custom Event that triggers on Stage.setScene()?
In my code, the button switches the Scenes and that works fine. However, I would like to extend the Stage to have an additional Event that is triggered when a button or possibly any other Element triggers a setScene.
Example:
package sample;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.stage.Stage;
public class Main extends Application {
public static void main(String[] args) {
Application.launch(args);
}
#Override
public void start(Stage stage) {
Group g1 = new Group();
Button b1 = new Button("2");
g1.getChildren().setAll(b1);
Scene scene1 = new Scene(g1, 50, 50);
Group g2 = new Group();
Button b2 = new Button("1");
g2.getChildren().setAll(b2);
Scene scene2 = new Scene(g2, 50, 50);
stage.setScene(scene1);
stage.setTitle("JavaFX Application Life Cycle");
b1.setOnAction(actionEvent -> {
System.out.println("1");
stage.setScene(scene2);
});
b2.setOnAction(actionEvent -> {
System.out.println("2");
stage.setScene(scene1);
});
stage.show();
}
}
You can add a ChangeListener<Scene> to your Stage like this:
stage.sceneProperty().addListener((observable, oldScene, newScene) -> {
System.out.println("New scene: " + newScene);
System.out.println("Old scene: " + oldScene);
});
I believe using a listener, as shown in the answer by #M.S., is probably the best and simplest way to react to scene changes. However, you ask about how to make a "custom event" that you can fire when the scene changes; by "event" I assume you mean a subclass of javafx.event.Event. So while I recommend sticking with a simple listener, here's an example of a custom event.
First, you need a custom event class:
import javafx.event.Event;
import javafx.event.EventType;
import javafx.scene.Scene;
import javafx.stage.Window;
public class SceneChangedEvent extends Event {
public static final EventType<SceneChangedEvent> SCENE_CHANGED =
new EventType<>(Event.ANY, "SCENE_CHANGED");
public static final EventType<SceneChangedEvent> ANY = SCENE_CHANGED;
private transient Window window;
private transient Scene oldScene;
private transient Scene newScene;
public SceneChangedEvent(Window window, Scene oldScene, Scene newScene) {
super(window, window, SCENE_CHANGED);
this.window = window;
this.oldScene = oldScene;
this.newScene = newScene;
}
public Window getWindow() {
return window;
}
public Scene getOldScene() {
return oldScene;
}
public Scene getNewScene() {
return newScene;
}
}
I'm not sure what information you want to carry with the event so I just added the source Window as well as the old and new Scenes. If you're wondering about the ANY = SCENE_CHANGED, I'm just following the pattern used by javafx.event.ActionEvent (which also only has a single event-type).
Then you simply need to fire the event when the scene changes. To implement this you're still going to need a change listener. As you mention wanting to extend Stage here's an example of that:
import javafx.beans.NamedArg;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.EventHandler;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
public class CustomStage extends Stage {
private final ObjectProperty<EventHandler<? super SceneChangedEvent>> onSceneChanged =
new SimpleObjectProperty<>(this, "onSceneChanged") {
#Override
protected void invalidated() {
setEventHandler(SceneChangedEvent.SCENE_CHANGED, get());
}
};
public final void setOnSceneChanged(EventHandler<? super SceneChangedEvent> handler) {
onSceneChanged.set(handler);
}
public final EventHandler<? super SceneChangedEvent> getOnSceneChanged() {
return onSceneChanged.get();
}
public final ObjectProperty<EventHandler<? super SceneChangedEvent>> onSceneChangedProperty() {
return onSceneChanged;
}
public CustomStage() {
this(StageStyle.DECORATED);
}
public CustomStage(#NamedArg(value = "style", defaultValue = "DECORATED") StageStyle style) {
super(style);
sceneProperty().addListener((obs, ov, nv) -> fireEvent(new SceneChangedEvent(this, ov, nv)));
}
}
This would let you react to the scene changing using any of the following:
CustomStage stage = new CustomStage();
// addEventFilter/addEventHandler
stage.addEventFilter(SceneChangedEvent.SCENE_CHANGED, e -> { ... });
stage.addEventHandler(SceneChangedEvent.SCENE_CHANGED, e -> { ... });
// setOnSceneChanged
stage.setOnSceneChanged(e -> { ... });
Keep in mind that the event will only target the CustomStage instance. In other words, only event handlers added to the CustomStage instance will be notified of the event. And as you can see, this is much more complicated than simply adding a change listener to the scene property of the Stage.
My question is how to generate buttons, set with car pictures, based on the checkboxes and/or radio buttons selected by a user in javafx?
I'm simulating a car dealership website with car pictures. The user should be able to filter the pictures displayed by clicking checkboxes and/or radio buttons selection.
I'm first creating all the picture buttons with a for each loop. I could use if and if/else statements to filter through the pictures but there would be duplicates. I've heard of observablelist but I haven't learned those yet.
Can someone help me out with this one please? Thank you!
ArrayList<Car> cars;
for (Car r : cars)
{
for (int i = 0; i < SIZE; i++)
{
// create buttons and set car pictures
carButton[i] = new Button();
carButton[i].setId(String.format("%d", i));
carButton[i].setGraphic(cars.get(i).getCarPicture());
}
}
Instead of using an ArrayList for your cars, I recommend using an ObservableList:
ObservableList<Car> carsList = FXCollections.observableArrayList<>();
An ObservableList allows you to listen for changes and respond accordingly. For example, when a new Car is added to the list, you could trigger an event that automatically adds a new Button to your scene.
Here is a short demo application that shows how this would work. I did comment the code below as well and many of the concepts being used may be beyond your level, but it's one method, at least.
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.image.ImageView;
import javafx.scene.layout.FlowPane;
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) {
VBox root = new VBox(5);
root.setPadding(new Insets(10));
root.setAlignment(Pos.CENTER);
// Create an ObservableList to hold our Cars
ObservableList<Car> carsList = FXCollections.observableArrayList();
// For our sample, let's use a FlowPane to display all of our buttons. We will add new buttons to this FlowPane
// automatically as new Cars are added to carsList
FlowPane flowPane = new FlowPane();
flowPane.setHgap(10);
flowPane.setVgap(5);
flowPane.setAlignment(Pos.TOP_CENTER);
// Create a ListChangeListener for our carsList. This allows us to perform some actions whenever an item is added
// to or removed from the list. For our example, we will only do something when a new Car is added.
carsList.addListener(new ListChangeListener<Car>() {
#Override
public void onChanged(Change<? extends Car> c) {
System.out.println(carsList.size());
// Get the first change
c.next();
// If an item was added to the list...
if (c.wasAdded()) {
// Create a new button and add it to the FlowPane
// The Change (c) provides access to a List of items that were added during this change. Since we
// are only going to add one Car at a time, we only need to get the first item from the AddedSubList
Button button = new Button(c.getAddedSubList().get(0).getName());
button.setGraphic(c.getAddedSubList().get(0).getIcon());
button.setOnAction(event -> {
// The code to be executed when this button is clicked goes here
});
// Add the button to our FlowPane
flowPane.getChildren().add(button);
}
}
});
// Now we need a Button that will add a new car to the List
Button button = new Button("Add Car");
button.setOnAction(event -> {
// We'll just add a random car to the carsList
carsList.add(new Car("Car #" + (carsList.size() + 1), new ImageView("icon.png")));
});
// Add our FlowPane and Button to the root layout
root.getChildren().addAll(button, flowPane);
primaryStage.setScene(new Scene(root, 550, 250));
primaryStage.show();
}
}
class Car {
private final String name;
private final ImageView icon;
public Car(String name, ImageView icon) {
this.name = name;
this.icon = icon;
}
public String getName() {
return name;
}
public ImageView getIcon() {
return icon;
}
}
The Results: (after clicking the "Add Car" button a few times)
This is a terrible implementation but It will give you some ideas on how to do things. You need to research FilteredList, ListView, and Predicate. This implementation does not handle more than one CheckBox at a time. It will only display the last CheckBox action.
CarList
import java.util.ArrayList;
import java.util.List;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
/**
*
* #author Sedrick
*/
public class CarList extends Application {
#Override
public void start(Stage primaryStage) {
List<Car> cars = new ArrayList();
cars.add(new Car("Honda", "2004"));
cars.add(new Car("Ford", "2005"));
cars.add(new Car("Ford", "2004"));
cars.add(new Car("Honda", "2005"));
cars.add(new Car("Toyota", "2004"));
cars.add(new Car("Cadillac", "2005"));
ListView<Car> view = new ListView();
view.setCellFactory((ListView<Car> param) -> {
ListCell<Car> cell = new ListCell<Car>() {
CarView carView = new CarView();
#Override
protected void updateItem(Car item, boolean empty) {
super.updateItem(item, empty);
if (item != null) {
setText("");
carView.setMake(item.getMake());
carView.setModel(item.getModel());
carView.setImageView(item.getUrl());
setGraphic(carView);
} else {
setText("");
setGraphic(null);
}
}
};
return cell;
});
ObservableList<Car> data = FXCollections.observableArrayList(cars);
FilteredList<Car> filteredList = new FilteredList(data);
view.setItems(filteredList);
HBox.setHgrow(view, Priority.ALWAYS);
CheckBox checkBox = new CheckBox("Honda");
checkBox.selectedProperty().addListener((observable, oldValue, newValue) -> {
if(newValue)
{
filteredList.setPredicate((item) -> {
return item.getMake().equals("Honda");
});
}
else{
filteredList.setPredicate((item) -> {
return true;
});
}
});
CheckBox checkBox2 = new CheckBox("Ford");
checkBox2.selectedProperty().addListener((observable, oldValue, newValue) -> {
if(newValue)
{
filteredList.setPredicate((item) -> {
return item.getMake().equals("Ford");
});
}
else{
filteredList.setPredicate((item) -> {
return true;
});
}
});
CheckBox checkBox3 = new CheckBox("2004");
checkBox3.selectedProperty().addListener((observable, oldValue, newValue) -> {
if(newValue)
{
filteredList.setPredicate((item) -> {
return item.getModel().equals("2004");
});
}
else{
filteredList.setPredicate((item) -> {
return true;
});
}
});
CheckBox checkBox4 = new CheckBox("2005");
checkBox4.selectedProperty().addListener((observable, oldValue, newValue) -> {
if(newValue)
{
filteredList.setPredicate((item) -> {
return item.getModel().equals("2005");
});
}
else{
filteredList.setPredicate((item) -> {
return true;
});
}
});
VBox leftPanel = new VBox(checkBox, checkBox2, checkBox3, checkBox4);
HBox root = new HBox(leftPanel, view);
Scene scene = new Scene(root, 625, 500);
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
CarView
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
/**
*
* #author Sedrick
*/
final public class CarView extends HBox{
Label make = new Label();
Label model = new Label();
ImageView imageView = new ImageView();
public CarView(String make, String model, String url) {
this.make.setText(make);
this.model.setText(model);
HBox row1 = new HBox(new Label("Make: "), this.make);
HBox row2 = new HBox(new Label("Model: "), this.model);
VBox vbox = new VBox(row1, row2);
vbox.setMaxSize(USE_PREF_SIZE, USE_PREF_SIZE);
StackPane stackPane1 = new StackPane(vbox);
HBox.setHgrow(stackPane1, Priority.ALWAYS);
Image image = new Image(url);
this.imageView.setImage(image);
this.imageView.setFitHeight(100);
this.imageView.setFitWidth(200);
StackPane stackPane2 = new StackPane(this.imageView);
stackPane2.setStyle("-fx-background-color: yellow");
getChildren().addAll(stackPane1, stackPane2);
setPrefSize(500, 125);
}
public CarView()
{
HBox row1 = new HBox(new Label("Make: "), this.make);
HBox row2 = new HBox(new Label("Model: "), this.model);
VBox vbox = new VBox(row1, row2);
vbox.setMaxSize(USE_PREF_SIZE, USE_PREF_SIZE);
StackPane stackPane1 = new StackPane(vbox);
HBox.setHgrow(stackPane1, Priority.ALWAYS);
this.imageView.setFitHeight(100);
this.imageView.setFitWidth(200);
StackPane stackPane2 = new StackPane(this.imageView);
stackPane2.setStyle("-fx-background-color: yellow");
getChildren().addAll(stackPane1, stackPane2);
setPrefSize(500, 125);
}
public void setImageView(String url) {
Image image = new Image(url);
this.imageView.setImage(image);
}
public void setMake(String make) {
this.make.setText(make);
}
public void setModel(String model)
{
this.model.setText(model);
}
}
Car
/**
*
* #author Sedrick
*/
public class Car {
private String make;
private String model;
private String url = "https://cdn.pixabay.com/photo/2012/05/29/00/43/car-49278_960_720.jpg";
public Car(String make, String model) {
this.make = make;
this.model = model;
}
public String getMake() {
return make;
}
public String getModel() {
return model;
}
public String getUrl()
{
return url;
}
public void setMake(String make) {
this.make = make;
}
public void setModel(String model) {
this.model = model;
}
}
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.
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();
}
}
How may I have a customized context menu for whole entry of the document in WebEngine javafx?
Something like this
+------------+
|Reload |
|Save page |
|Hide Images |
+------------+
I like to invoke and show this context popup for whole document entry(same for every node). Thanks.
I don't see a way to interact with the default context menu. However, it's not hard to disable it and implement your own.
Disable the default context menu with
webView.setContextMenuEnabled();
Then create your own context menu, and register a mouse listener with the web view to show it on right click:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TextField;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.BorderPane;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
public class WebViewContextMenuTest extends Application {
private final String START_URL =
"http://stackoverflow.com/questions/27047447/customized-context-menu-on-javafx-webview-webengine/27047830#27047830";
#Override
public void start(Stage primaryStage) {
TextField locationField = new TextField(START_URL);
WebView webView = new WebView();
webView.getEngine().load(START_URL);
webView.setContextMenuEnabled(false);
createContextMenu(webView);
locationField.setOnAction(e -> {
webView.getEngine().load(getUrl(locationField.getText()));
});
BorderPane root = new BorderPane(webView, locationField, null, null, null);
primaryStage.setScene(new Scene(root, 800, 600));
primaryStage.show();
}
private void createContextMenu(WebView webView) {
ContextMenu contextMenu = new ContextMenu();
MenuItem reload = new MenuItem("Reload");
reload.setOnAction(e -> webView.getEngine().reload());
MenuItem savePage = new MenuItem("Save Page");
savePage.setOnAction(e -> System.out.println("Save page..."));
MenuItem hideImages = new MenuItem("Hide Images");
hideImages.setOnAction(e -> System.out.println("Hide Images..."));
contextMenu.getItems().addAll(reload, savePage, hideImages);
webView.setOnMousePressed(e -> {
if (e.getButton() == MouseButton.SECONDARY) {
contextMenu.show(webView, e.getScreenX(), e.getScreenY());
} else {
contextMenu.hide();
}
});
}
private String getUrl(String text) {
if (text.indexOf("://")==-1) {
return "http://" + text ;
} else {
return text ;
}
}
public static void main(String[] args) {
launch(args);
}
}
There's no easy solution for this, since there's no public API, and a request is still unresolved.
The hacky solution uses some private API, so it's not very advisable since it could change without notice.
The ContextMenu shown when the user right clicks on the web page is in another window, so using some lookups we'll try to find it, then access to its content and then modify existing or add more MenuItems.
These are the private classes required:
import com.sun.javafx.scene.control.skin.ContextMenuContent;
import com.sun.javafx.scene.control.skin.ContextMenuContent.MenuItemContainer;
In our application, we listen for a context menu request:
#Override
public void start(Stage primaryStage) {
WebView webView = new WebView();
WebEngine webEngine = webView.getEngine();
Scene scene = new Scene(webView);
primaryStage.setScene(scene);
primaryStage.show();
webView.setOnContextMenuRequested(new EventHandler<ContextMenuEvent>() {
#Override
public void handle(ContextMenuEvent e) {
getPopupWindow();
}
});
}
where getPopupWindow() will:
Look for the new window being instance of ContextMenu
With lookup find the CSS selector context-menu. This is a node having as its only child a ContextMenuContent instance.
This object has an VBox as a container for all the items, which are MenuItem in an special container, MenuItemContainer.
We can access to any of the existing items, like reload page, go back, ... and customize them, modifying its text or adding a graphic.
We can add our custom items to this box, providing our own actions.
Customize the items as you need to:
private PopupWindow getPopupWindow() {
#SuppressWarnings("deprecation")
final Iterator<Window> windows = Window.impl_getWindows();
while (windows.hasNext()) {
final Window window = windows.next();
if (window instanceof ContextMenu) {
if(window.getScene()!=null && window.getScene().getRoot()!=null){
Parent root = window.getScene().getRoot();
// access to context menu content
if(root.getChildrenUnmodifiable().size()>0){
Node popup = root.getChildrenUnmodifiable().get(0);
if(popup.lookup(".context-menu")!=null){
Node bridge = popup.lookup(".context-menu");
ContextMenuContent cmc= (ContextMenuContent)((Parent)bridge).getChildrenUnmodifiable().get(0);
VBox itemsContainer = cmc.getItemsContainer();
for(Node n: itemsContainer.getChildren()){
MenuItemContainer item=(MenuItemContainer)n;
// customize text:
item.getItem().setText("My Custom: "+item.getItem().getText());
// customize graphic:
item.getItem().setGraphic(new ImageView(new Image(getClass().getResource("unlock24.png").toExternalForm())));
}
// remove some item:
// itemsContainer.getChildren().remove(0);
// adding new item:
MenuItem menuItem = new MenuItem("Save page");
menuItem.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent e) {
System.out.println("Save Page");
}
});
// add new item:
cmc.getItemsContainer().getChildren().add(cmc.new MenuItemContainer(menuItem));
return (PopupWindow)window;
}
}
}
return null;
}
}
return null;
}
This is how it looks like: