JavaFX & MVP, Object is garbage collected when it should not be - java

I have been stuck on this for a while and I am really at my last hope. I really have no idea what the problem can be. I have a GUI project using what I hope is correct MVP form and my problem is I have a presenter object that becomes null in my view class when being used within any method. So far the gui should load and user should hit the button, the method calls the presenter object's method to do something but it is always null. I tried passing into the view class's constructor, making it a object only within the view class, etc and I can't to fix it. Any help is appreciated. I have no idea what is going on and it does not seem to do this to any other object I think
main.java
package main;
import model.Model;
import presenter.Presenter;
import view.View;
public class Main
{
public static void main(String[] args)
{
View view = new View();
Model model = new Model();
Presenter presenter = new Presenter(view, model);
view.setPresenter(presenter);
// this was a way I found on stack overflow to call
// another class that inhierts the application class
// and be able to have args passed if needed
View.launch(View.class, args);
}
}
View.java
package view;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.stage.Stage;
import presenter.Presenter;
public class View extends Application
{
private Presenter presenter;
private Button test;
private Views views;
public final static String LOGINVIEW = "loginscreen";
public final static String LOGINVIEWFILE = "FXMLViewLogin.fxml";
public final static String CHATVIEW = "chatscreen";
public final static String CHATVIEWFILE = "FXMLViewChat.fxml";
public View()
{
// init Views class
views = new Views();
}
public void setPresenter(Presenter pres)
{
this.presenter = pres;
}
#Override
public void start(Stage primaryStage)
{
loadViews();
primaryStage.setScene(setView(LOGINVIEW));
primaryStage.setTitle("");
primaryStage.show();
}
private void loadViews()
{
views.loadScreen(LOGINVIEW, LOGINVIEWFILE);
views.loadScreen(CHATVIEW, CHATVIEWFILE);
}
private Scene setView(String name)
{
return views.setView(name);
}
#FXML
private void test(ActionEvent event)
{
//***********************************************************
// ALWAYS NULL IN THIS EVENT METHOD METHOD AND ANY OTHER METHOD FOR THAT FACT AFTER setPresenter
if(presenter == null)
System.out.println("Why is this null right here?");
}
}
Presenter.java
package presenter;
import view.View;
import model.Model;
public class Presenter
{
private Model model;
private View view;
public Presenter(View view, Model model)
{
this.model = model;
this.view = view;
}
// any methods start here
}
Views.java
package view;
import java.util.HashMap;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
public class Views
{
private final HashMap<String, Parent> view;
public Views()
{
this.view = new HashMap<>();
}
public void addView(String name, Parent screen)
{
view.put(name, screen);
}
public boolean loadScreen(String name, String file)
{
try
{
FXMLLoader myLoader = new FXMLLoader(getClass().getResource(file));
Parent loadScreen = (Parent) myLoader.load();
addView(name, loadScreen);
return true;
}
catch (Exception e)
{
return false;
}
}
public Scene setView(final String name)
{
Scene scene = null;
if (view.get(name) != null)
scene = new Scene(getParentNode(name));
unloadView(name);
return scene;
}
private boolean unloadView(String name)
{
return view.remove(name) != null;
}
private Parent getParentNode(String name)
{
return view.get(name);
}
}

Related

Lookup fails the first time a View is shown

Minimal classes to reproduce the issue:
import static com.gluonhq.charm.glisten.application.AppManager.*;
import javafx.application.Application;
import javafx.stage.Stage;
import com.gluonhq.charm.glisten.application.AppManager;
import com.gpsdemo.view.View1;
import com.gpsdemo.view.View2;
public class MyApplication extends Application {
AppManager appManager = AppManager.initialize();
public static final String VIEW1 = HOME_VIEW;
public static final String VIEW2 = "View2";
#Override
public void init() {
appManager.addViewFactory(VIEW1, View1::get);
appManager.addViewFactory(VIEW2, View2::get);
}
#Override
public void start(Stage stage) throws Exception {
appManager.start(stage);
if (com.gluonhq.attach.util.Platform.isDesktop()) {
stage.setHeight(600);
stage.setWidth(360);
stage.centerOnScreen();
}
}
public static void main(String args[]) {
launch(args);
}
}
import javafx.scene.control.Label;
import com.gluonhq.charm.glisten.control.AppBar;
import com.gluonhq.charm.glisten.mvc.View;
import com.gluonhq.charm.glisten.visual.MaterialDesignIcon;
import com.gpsdemo.MyApplication;
public class View1 extends View {
private static View1 INSTANCE;
public static View1 get() {
return INSTANCE != null ? INSTANCE : (INSTANCE = new View1());
}
private View1() {
setCenter(new Label("Nothing to see here"));
}
#Override
protected void updateAppBar(AppBar appBar) {
appBar.setTitleText("View1");
var optionsButton = MaterialDesignIcon.MENU.button(e -> getAppManager().switchView(MyApplication.VIEW2));
appBar.getActionItems().add(optionsButton);
}
}
import java.util.Set;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.scene.Node;
import com.gluonhq.charm.glisten.control.AppBar;
import com.gluonhq.charm.glisten.control.SettingsPane;
import com.gluonhq.charm.glisten.control.settings.DefaultOption;
import com.gluonhq.charm.glisten.mvc.View;
import com.gluonhq.charm.glisten.visual.MaterialDesignIcon;
public class View2 extends View {
private static View2 INSTANCE;
public static View2 get() {
return INSTANCE != null ? INSTANCE : (INSTANCE = new View2());
}
private View2() {
var settingsPane = new SettingsPane();
var option = new DefaultOption<>("Title", "Description", "Category", new SimpleDoubleProperty(), true);
settingsPane.getOptions().add(option);
setCenter(settingsPane);
setOnShown(e -> {
System.out.println("On shown");
Set<Node> lookup = settingsPane.lookupAll(".secondary-graphic");
System.out.println(lookup);
});
}
#Override
protected void updateAppBar(AppBar appBar) {
appBar.setTitleText("View2");
var backButton = MaterialDesignIcon.ARROW_BACK.button(e -> getAppManager().switchToPreviousView().get());
appBar.setNavIcon(backButton);
}
}
Launch the application normally, View1 will show.
Click on the button to show View2. The first time View2 is loaded the output is
On shown
[]
So the lookup fails in the onShown event.
Click on the back button and then show View2 again. The output is
On shown
[HBox#2c8d8a10[styleClass=secondary-graphic]]
which is correct.
If View2 is set as HOME_VIEW, the lookup will find the nodes correctly on the first onShown event. This looks like a bug to me. Regardless, I would like the lookup to succeed on the first time so I can configure the view correctly.
Using:
<javafx-maven-plugin-version>0.0.8</javafx-maven-plugin-version>
<gluonfx-maven-plugin-version>1.0.14</gluonfx-maven-plugin-version>
<java-version>17</java-version>
<javafx-version>18.0.1</javafx-version>
<charm-version>6.1.0</charm-version>
As mentioned in the comments, the issue is that the SHOWN event does not happen after a css pass is applied, therefore the lookup fails. A fix can be to manually do a css pass before the lookup, inside the SHOWN handler:
setOnShown(e -> {
System.out.println("On shown");
settingsPane.applyCss();
Set<Node> lookup = settingsPane.lookupAll(".secondary-graphic");
System.out.println(lookup);
});
This is confusing because the SHOWN event does not actually happen after the view is fully shown (in which case a css pass has happened). The event also behaves differently than JavaFX's dialog DIALOG_SHOWN event, that does happen after the dialog is fully shown (including a css pass).

TableView items not refreshing, but are correctly added, after adding them from a different controller

First I want to say that I already checked various similar solutions to this problem here, but the code design of the other users that posted this question is so different than mine that I don't understand how to fix the same problem using the solutions posted.
That said, I'm using javafx with gluon scene builder to create my first app. I'll post the code below. This (https://i.imgur.com/lO2mHZI.png) is how the app looks so far. The New button opens this window (https://i.imgur.com/kVZ5tjt.png).
I have a main class called WeightApp:
package application;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class WeightApp extends Application {
#Override
public void start(Stage primaryStage) throws Exception{
Parent root = FXMLLoader.load(getClass().getResource("foodTab.fxml"));
Scene main = new Scene(root);
primaryStage.setScene(main);
primaryStage.setTitle("App");
primaryStage.setMinWidth(root.minWidth(-1));
primaryStage.setMinHeight(root.minHeight(-1));
primaryStage.show();
}
public static void main(String[] args) {
launch(WeightApp.class);
}
}
A FoodTabController class which loads what's shown in the first picture without the window created by pressing New:
package application;
import application.domain.Aliment;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;
import java.io.*;
import java.util.Objects;
public class FoodTabController {
#FXML
protected AnchorPane app, foodTab, foodButtonBar;
#FXML
protected TabPane mainWindow;
#FXML
protected Tab summaryTabLabel, foodTabLabel;
#FXML
protected Label alimentsLabel;
#FXML
protected Button deleteButton, refreshButton, newButton, newMealWindow;
#FXML
protected TableView<Aliment> alimentsTableView;
#FXML
protected TableColumn<Aliment, String> alimentsNameCol;
#FXML
protected TableColumn<Aliment, Double> alimentsKcalCol, alimentsFatCol, alimentsCarbsCol, alimentsProteinCol, alimentsFiberCol;
protected ObservableList<Aliment> aliments = FXCollections.observableArrayList();
public void initialize() {
alimentsNameCol.setCellValueFactory(new PropertyValueFactory<>("name"));
alimentsKcalCol.setCellValueFactory(new PropertyValueFactory<>("calories"));
alimentsFatCol.setCellValueFactory(new PropertyValueFactory<>("fat"));
alimentsCarbsCol.setCellValueFactory(new PropertyValueFactory<>("carbohydrate"));
alimentsProteinCol.setCellValueFactory(new PropertyValueFactory<>("protein"));
alimentsFiberCol.setCellValueFactory(new PropertyValueFactory<>("fiber"));
loadAliments();
alimentsTableView.setItems(aliments);
}
// Aliments //
public void newAlimentWindow() throws IOException {
Parent newAlimentWindow = FXMLLoader.load(Objects.requireNonNull(getClass().getResource("newAlimentWindow.fxml")));
Stage stage = new Stage();
stage.setScene(new Scene(newAlimentWindow));
stage.show();
}
public void updateTableView() {
aliments.clear();
loadAliments();
}
public ObservableList<Aliment> alimentObservableList() {
return aliments;
}
public void deleteAliment() {
aliments.remove(alimentsTableView.getSelectionModel().getSelectedItem());
saveAliments();
}
public void saveAliments() {
String COMMA_DELIMITER = ",";
String NEW_LINE_SEPARATOR = "\n";
String FILE_HEADER = "aliment,calories,fat,carbs,protein,fiber";
FileWriter fw = null;
try {
fw = new FileWriter("aliments.csv");
fw.append(FILE_HEADER);
fw.append(NEW_LINE_SEPARATOR);
for (Aliment aliment : aliments) {
fw.append(String.valueOf(aliment.getName()));
fw.append(COMMA_DELIMITER);
fw.append(String.valueOf(aliment.getCalories()));
fw.append(COMMA_DELIMITER);
fw.append(String.valueOf(aliment.getFat()));
fw.append(COMMA_DELIMITER);
fw.append(String.valueOf(aliment.getCarbohydrate()));
fw.append(COMMA_DELIMITER);
fw.append(String.valueOf(aliment.getProtein()));
fw.append(COMMA_DELIMITER);
fw.append(String.valueOf(aliment.getFiber()));
fw.append(NEW_LINE_SEPARATOR);
}
} catch (Exception e) {
System.out.println("Error writing to file");
e.printStackTrace();
} finally {
try {
assert fw != null;
fw.flush();
fw.close();
} catch (IOException e) {
System.out.println("Error while flushing/closing FileWriter.");
e.printStackTrace();
}
}
}
public void loadAliments() {
String COMMA_DELIMITER = ",";
int ALIMENT_NAME = 0;
int ALIMENT_CALORIES = 1;
int ALIMENT_FAT = 2;
int ALIMENT_CARBS = 3;
int ALIMENT_PROTEIN = 4;
int ALIMENT_FIBER = 5;
BufferedReader fileReader = null;
try {
fileReader = new BufferedReader(new FileReader("aliments.csv"));
fileReader.readLine();
String line = "";
while ((line = fileReader.readLine()) != null) {
String[] tokens = line.split(COMMA_DELIMITER);
aliments.add(new Aliment(String.valueOf(tokens[ALIMENT_NAME]), Double.parseDouble(tokens[ALIMENT_CALORIES]), Double.parseDouble(tokens[ALIMENT_FAT]), Double.parseDouble(tokens[ALIMENT_CARBS]), Double.parseDouble(tokens[ALIMENT_PROTEIN]), Double.parseDouble(tokens[ALIMENT_FIBER])));
}
} catch (Exception e) {
System.out.println("Error reading aliments from CSV file");
e.printStackTrace();
} finally {
try {
assert fileReader != null;
fileReader.close();
} catch (IOException e) {
System.out.println("Error while trying to close FileReader");
e.printStackTrace();
}
}
}
// Aliments //
}
Finally, I have the newAlimentWindowController class that is the window the New button opens:
package application;
import application.domain.Aliment;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.scene.layout.Pane;
public class newAlimentWindowController extends FoodTabController {
#FXML
protected Pane newAlimentPane;
#FXML
protected TextField newAlimentSetName, newAlimentSetCal, newAlimentSetFat, newAlimentSetCarbs, newAlimentSetProtein, newAlimentSetFiber;
#FXML
protected Button addButton;
public void initialize() {
loadAliments();
}
public void addAliment() {
aliments.add(new Aliment(newAlimentSetName.getText(), Double.parseDouble(newAlimentSetCal.getText()), Double.parseDouble(newAlimentSetFat.getText()), Double.parseDouble(newAlimentSetCarbs.getText()), Double.parseDouble(newAlimentSetProtein.getText()), Double.parseDouble(newAlimentSetFiber.getText())));
saveAliments();
updateTableView();
}
}
Also, the Aliment object:
package application.domain;
import java.util.Objects;
public class Aliment {
private String name;
private double weight;
private double calories, fat, carbohydrate, protein, fiber;
public Aliment(String name, double weight, double calories, double fat, double carbohydrate, double protein, double fiber) {
this(name, calories, fat, carbohydrate, protein, fiber);
this.weight = weight;
}
public Aliment(String name, double calories, double fat, double carbohydrate, double protein, double fiber) {
this.name = name;
this.weight = 100;
this.calories = calories;
this.fat = fat;
this.carbohydrate = carbohydrate;
this.protein = protein;
this.fiber = fiber;
}
Everything works fine, except after I type in the textfields in the New window and I press the Add button, the updateTableView method inside the addAliment method doesn't trigger (the Aliment item is added correctly, the observable list just doesn't refresh on the Add button press). However, the updateTableView method does work if I trigger it from inside the FoodTabController class that I linked to the Refresh button.
I don't understand what's happening: I can interact with the aliments observable list in FoodTabController from newAlimentWindowController since aliments.add works and at the same time, the saveAliments method also works, but updateTableView method, that is in the same method as saveAliments and aliments.add, does not work. I'm very confused.
I feel like I'm missing something basic about java programming and as such I'd like to learn what's going on. Any help will be appreciated, thank you very much!

How to open popup window from an other class without button click in javaFX?

So I've got a method called 'popup' in a javaFX controller class which opens a small popup window on top of the actual application window. This method runs without problem if it's assigned to a button in fxml and the button is clicked, but this is not the way I want to use it.
I've got an other class called 'Timer' with a new task (new thread) which is counting down from a certain number, and at a point it will open a popup window with a message. My purpose is to call and run the 'popup' method from this 'Timer' class. When I call the 'popup' method from here, it starts executing, but the popup window doesn't appear at all. (The method call happens as I get the message "in popup" on console from 'popup' method. )
So why does it work when a button click calls 'popup' method from the fxml file and why not when I call it from an other class? Thanks.
Please see the controller class with 'popup' method and the Timer class below (using Gradle in project):
"SceneController" controller class:
package GradleFX;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.text.Text;
import javafx.stage.Modality;
import javafx.stage.Stage;
import java.io.IOException;
import java.net.URL;
import java.util.ResourceBundle;
//import java.awt.event.ActionEvent;
public class SceneController implements Initializable {
public static String password = "";
protected static int timercount = 20;
#FXML
private Label PWLabel;
#FXML
private Label bottomLabel;
#FXML
private PasswordField PWField;
#FXML
private Label showPWLabel;
protected static Label myBottomLabel;
private static PasswordField myPWField;
private static Label myShowPWLabel;
private static int tries;
#Override
public void initialize(URL location, ResourceBundle resources) {
Timer timerTask = new Timer();
myBottomLabel = bottomLabel;
myPWField = PWField;
myShowPWLabel = showPWLabel;
new Thread(timerTask).start();
}
**/***********************************************************************
/*This method runs if button is pressed in main application,
but can't make it work by calling it from Timer Class */
public void popup() {
System.out.println("in popup");
Stage dialogStage = new Stage();
dialogStage.initModality(Modality.WINDOW_MODAL);
VBox vbox = new VBox(new Text("Hi"), new Button("Ok."));
vbox.setAlignment(Pos.CENTER);
vbox.setPadding(new Insets(15));
dialogStage.setScene(new Scene(vbox));
dialogStage.show();
}
//****************************************************************************
public void showPW() {
myShowPWLabel.setText(myPWField.getText());
}
public void hidePW() {
myShowPWLabel.setText("");
}
public void exit() {
System.exit(0);
}
public void write() {
PWLabel.setText("Mukodik");
}
public void writeInput(String in) {
password = in;
System.out.println("final password text text: " + password);
writeFinally();
}
public void writeFinally() {
System.out.println("This is 'password' : " + password);
//bottomLabel.setText(password);
}
public void bottomLabelWrite() {
bottomLabel.setText(myPWField.getText());
}
public static void setLabel() throws InterruptedException {
myBottomLabel.setText("");
myBottomLabel.setText("Database has been permanently erased.");
//Thread.sleep(3000);
//System.exit(0);
}
public static void noKeyEnteredNote() {
myBottomLabel.setTextFill(Color.BLACK);
myBottomLabel.setText("No key entered. Type Main Key.");
}
public static void rightKey() {
myBottomLabel.setText("Yes, this is the right key.");
}
public static void wrongKey() throws InterruptedException {
tries = MasterKey.numOfTryLeft;
if (tries > 0) {
myBottomLabel.setTextFill(Color.RED);
myBottomLabel.setText("!!!Wrong key!!! You've got " + tries + " tries left!");
}
}
public void simpleTest(String in) {
System.out.println("in simpleTest and in is: " + in);
}
public void getMainKey() throws IOException, InterruptedException {
MasterKey masterKey = new MasterKey();
System.out.println("Inside SceneController");
masterKey.requestKey(myPWField.getText());
}
public void changeScreen(ActionEvent event) throws IOException, InterruptedException {
getMainKey();
if (MasterKey.isRightKey) {
Parent tableViewParent = FXMLLoader.load(getClass().getResource("Menu.fxml"));
Scene tableViewScene = new Scene(tableViewParent);
Stage window = (Stage) ((Node) event.getSource()).getScene().getWindow();
window.setScene(tableViewScene);
window.show();
}
}
}
This is Timer class:
package GradleFX;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
public class Timer extends Task {
private ActionEvent actionEvent;
#Override
protected Integer call() throws Exception {
boolean notCalled = true;
while (SceneController.timercount > 0) {
SceneController sceneController = new SceneController();
System.out.println(SceneController.timercount);
Thread.sleep(1000);
SceneController.timercount--;
if (SceneController.timercount < 19) {
System.out.println("Less than 5");
if(notCalled) {
sceneController.popup();
notCalled = false;
}
}
}
System.exit(0);
return null;
}
}
Add this to your code:
#Override
public void initialize(URL location, ResourceBundle resources) {
Timer timerTask = new Timer();
myBottomLabel = bottomLabel;
myPWField = PWField;
myShowPWLabel = showPWLabel;
new Thread(timerTask).start();
timerTask.setOnFinished(e->{
popup();
});
}

MVC - Implementation with Swing vs JavaFX

I have a pet project which is a 2D game engine.
After creating all the backend functionality, I want to implement a UI for it.
My current plan is to do this via MVC because it strikes me as the most feasible way of doing this (logic first, then UI).
Now, I am unsure how to design/implement this with either Swing or JavaFX, as I do not yet fully understand what the underlying concepts of either are.
Can someone describe to me how to implement MVC with Swing or JavaFX?
You can have a model that is a general one can be used by different views.
To demonstrate it let's first introduce a interface that can be used to listen to such model:
//Interface implemented by SwingView and used by Model
interface Observer {
void observableChanged();
}
Consider a very simple model, with one attribute only: an integer value between 0 and a certain max:
//Generic model. Not dependent on the GUI tool kit. Use by Swing as well as JAvaFX
class Model {
private int value;
private static final int MAX_VALUE = 100;
private Observer observer;
int getValue(){
return value;
}
void setValue(int value){
this.value = Math.min(MAX_VALUE, Math.abs(value));
notifyObserver();
}
int getMaxValue() {
return MAX_VALUE;
}
//-- handle observers
void setObserver(Observer observer) {
this.observer = observer;
}
private void notifyObserver() {
if(observer != null) {
observer.observableChanged();
}
}
}
Note that the model calls observer.observableChanged() when value changes it.
Now let's use this model with a Swing gui that displays a random number :
import java.awt.*;
import java.util.Random;
import javax.swing.*;
//Swing app, using a generic model
public class SwingMVC {
public static void main(String[] args) {
new SwingController();
}
}
//SwingController of the MVC pattern."wires" model and view (and in this case also worker)
class SwingController{
public SwingController() {
Model model = new Model();
SwingView swingView = new SwingView(model);
model.setObserver(swingView); //register view as an observer to model
update(model);
}
//change model
private void update(Model model) {
Random rnd = new Random();
//use swing timer so the change is performed on the Event Dispatch Thread
new Timer(1000,(e)-> model.setValue(1+rnd.nextInt(model.getMaxValue()))).start();
}
}
//view of the MVC pattern. Implements observer to respond to model changes
class SwingView implements Observer{
private final Model model;
private final JLabel label;
public SwingView(Model model) {
this.model = model;
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocationByPlatform(true);
frame.setLayout(new GridBagLayout());
label = new JLabel(" - ");
label.setFont(new Font(label.getFont().getName(), Font.PLAIN, 48));
label.setHorizontalTextPosition(SwingConstants.CENTER);
frame.add(label);
frame.pack();
frame.setVisible(true);
}
#Override
public void observableChanged() {
//update text in response to change in model
label.setText(String.format("%d",model.getValue()));
}
}
We can use the very same model and achieve the same functionality with a JavaFx gui:
import java.util.Random;
import javafx.animation.PauseTransition;
import javafx.application.Application;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Font;
import javafx.stage.Stage;
import javafx.util.Duration;
//JavaFa app, using a generic model
public class FxMVC extends Application{
#Override
public void start(Stage primaryStage) throws Exception {
FxController fxController = new FxController();
Scene scene = new Scene(fxController.getParent());
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(null);
}
}
class FxController{
private final FxView view;
FxController() {
Model model = new Model();
view = new FxView(model);
model.setObserver(view); //register fxView as an observer to model
update(model);
}
//change model
private void update(Model model) {
Random rnd = new Random();
//use javafx animation tools so the change is performed on the JvaxFx application thread
PauseTransition pt = new PauseTransition(Duration.seconds(1));
pt.play();
pt.setOnFinished(e->{
model.setValue(1+rnd.nextInt(model.getMaxValue()));
pt.play();
});
}
Parent getParent(){
return view;
}
}
//View of the MVC pattern. Implements observer to respond to model changes
class FxView extends StackPane implements Observer{
private final Model model;
private final Label label;
public FxView(Model model) {
this.model = model;
label = new Label(" - ");
label.setFont(new Font(label.getFont().getName(), 48));
getChildren().add(label);
}
#Override
public void observableChanged() { //update text in response to change in model
//update text in response to change in model
label.setText(String.format("%d",model.getValue()));
}
}
On the other hand, you can have a model that is more specific, intended to be used with a specific tool-kit, and gain some advantages by using some tool of that tool kit.
For example a model made using JavaFx properties, in this example SimpleIntegerProperty, which simplifies the listening to model changes (does not make use of the Observer interface):
import java.util.Random;
import javafx.animation.PauseTransition;
import javafx.application.Application;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ChangeListener;
import javafx.scene.*;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Font;
import javafx.stage.Stage;
import javafx.util.Duration;
//JavaFa app, using a JavaFx model
public class FxApp extends Application{
#Override
public void start(Stage primaryStage) throws Exception {
FxController fxController = new FxController();
Scene scene = new Scene(fxController.getParent());
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(null);
}
}
class FxAppController{
private final FxAppView view;
FxAppController() {
FxAppModel model = new FxAppModel();
view = new FxAppView(model);
update(model);
}
//change model
private void update(FxAppModel model) {
Random rnd = new Random();
//use javafx animation tools so the change is performed on the JvaxFx application thread
PauseTransition pt = new PauseTransition(Duration.seconds(1));
pt.play();
pt.setOnFinished(e->{
model.setValue(1+rnd.nextInt(model.getMaxValue()));
pt.play();
});
}
Parent getParent(){
return view;
}
}
//View does not need to implement listener
class FxAppView extends StackPane{
public FxAppView(FxAppModel model) {
Label label = new Label(" - ");
label.setFont(new Font(label.getFont().getName(), 48));
getChildren().add(label);
model.getValue().addListener((ChangeListener<Number>) (obs, oldV, newV) -> label.setText(String.format("%d",model.getValue())));
}
}
//Model that uses JavaFx tools
class FxAppModel {
private SimpleIntegerProperty valueProperty;
private static final int MAX_VALUE = 100;
SimpleIntegerProperty getValue(){
return valueProperty;
}
void setValue(int value){
valueProperty.set(value);
}
int getMaxValue() {
return MAX_VALUE;
}
}
A Swing gui and a JavaFx gui, each uses a different instance of the same Model:

JavaFX TreeView of multiple object types? (and more)

I currently have the following object data structure:
Item
String name
ArrayList of information
Character
String name
Collection of Item
Account
String name
Collection of Character (up to 8 max)
I want to make a TreeView that looks like the following:
Root(invisible)
======Jake(Account)
============JakesChar(Character)
==================Amazing Sword(Item)
==================Broken Bow(Item)
==================Junk Metal(Item)
======Mark(Account)
============myChar(Character)
==================Godly Axe(Item)
======FreshAcc(Account)
======MarksAltAcc(Account)
============IllLvlThisIPromise(Character)
======Jeffrey(Account)
============Jeff(Character)
==================Super Gun(Item)
==================Better Super Gun(Item)
==================Super Gun Scope(Item)
I made all those names up and such, obviously the real implementation would be a lot more complex. How can this be done? The TreeItem requires each TreeItem to be the same type as its' parent.
The ONLY solution I have is to do the following:
public class ObjectPointer
{
Object pointer;
String name;
}
My TreeView would be of type ObjectPointer and on each row I would cast the ObjectPointer to Account, Character, or Item. This is AWFUL but I think it would work.
Sub Questions:
How do I get TreeItem(s) to detect setOnMouseHover events?
How do I get TreeItem(s) to not use the toString method of their type and instead a custom way of displaying the String property that they need?
How do I get the TreeItem(s) to display colored text in the GUI instead of plain text?
Thank you!
If you look at your model and think generically, all the classes have a degree of similarity, which you could factor out into a superclass:
package model;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
public abstract class GameObject<T extends GameObject<?>> {
public GameObject(String name) {
setName(name);
}
private final StringProperty name = new SimpleStringProperty();
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);
}
private final ObservableList<T> items = FXCollections.observableArrayList();
public ObservableList<T> getItems() {
return items ;
}
public abstract void createAndAddChild(String name);
}
The type parameter T here represents the type of "child" objects. So your Account class (whose child type is GameCharacter - don't name classes the same as anything in java.lang, btw...) looks like
package model;
public class Account extends GameObject<GameCharacter> {
public Account(String name) {
super(name);
}
#Override
public void createAndAddChild(String name) {
getItems().add(new GameCharacter(name));
}
}
and similarly all the way down the hierarchy. I'd define an Information class (even though it just has a name) to make everything fit the structure, so:
package model;
public class Item extends GameObject<Information> {
public Item(String name) {
super(name);
}
#Override
public void createAndAddChild(String name) {
getItems().add(new Information(name));
}
}
and, since Information has no children, its child list is just an empty list:
package model;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
public class Information extends GameObject<GameObject<?>> {
public Information(String name) {
super(name);
}
#Override
public ObservableList<GameObject<?>> getItems() {
return FXCollections.emptyObservableList();
}
#Override
public void createAndAddChild(String name) {
throw new IllegalStateException("Information has no child items");
}
}
Now every item in your tree is a GameObject<?>, so you can basically build a TreeView<GameObject<?>>. The tricky part is that your tree items need to reflect the structure already built in the model. Since you have observable lists there, you can do this with listeners on the lists.
You can use a cell factory on the tree to customize the appearance of the cells displaying the TreeItems. If you want a different appearance for each type of item, I'd recommend defining the styles in an external CSS class, and setting a CSS PseudoClass on the cell corresponding to the type of item. If you use some naming convention (I have that the pseudo-class name is the lower case version of the class name), it can be quite slick to do that. Here's a fairly simple example:
package ui;
import static java.util.stream.Collectors.toList;
import java.util.Arrays;
import java.util.List;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import model.Account;
import model.GameCharacter;
import model.GameObject;
import model.Information;
import model.Item;
public class Tree {
private final TreeView<GameObject<?>> treeView ;
private final List<Class<? extends GameObject<?>>> itemTypes = Arrays.asList(
Account.class, GameCharacter.class, Item.class, Information.class
);
public Tree(ObservableList<Account> accounts) {
treeView = new TreeView<>();
GameObject<?> root = new GameObject<Account>("") {
#Override
public ObservableList<Account> getItems() {
return accounts ;
}
#Override
public void createAndAddChild(String name) {
getItems().add(new Account(name));
}
};
TreeItem<GameObject<?>> treeRoot = createItem(root);
treeView.setRoot(treeRoot);
treeView.setShowRoot(false);
treeView.setCellFactory(tv -> {
TreeCell<GameObject<?>> cell = new TreeCell<GameObject<?>>() {
#Override
protected void updateItem(GameObject<?> item, boolean empty) {
super.updateItem(item, empty);
textProperty().unbind();
if (empty) {
setText(null);
itemTypes.stream().map(Tree.this::asPseudoClass)
.forEach(pc -> pseudoClassStateChanged(pc, false));
} else {
textProperty().bind(item.nameProperty());
PseudoClass itemPC = asPseudoClass(item.getClass());
itemTypes.stream().map(Tree.this::asPseudoClass)
.forEach(pc -> pseudoClassStateChanged(pc, itemPC.equals(pc)));
}
}
};
cell.hoverProperty().addListener((obs, wasHovered, isNowHovered) -> {
if (isNowHovered && (! cell.isEmpty())) {
System.out.println("Mouse hover on "+cell.getItem().getName());
}
});
return cell ;
}
}
public TreeView<GameObject<?>> getTreeView() {
return treeView ;
}
private TreeItem<GameObject<?>> createItem(GameObject<?> object) {
// create tree item with children from game object's list:
TreeItem<GameObject<?>> item = new TreeItem<>(object);
item.setExpanded(true);
item.getChildren().addAll(object.getItems().stream().map(this::createItem).collect(toList()));
// update tree item's children list if game object's list changes:
object.getItems().addListener((Change<? extends GameObject<?>> c) -> {
while (c.next()) {
if (c.wasAdded()) {
item.getChildren().addAll(c.getAddedSubList().stream().map(this::createItem).collect(toList()));
}
if (c.wasRemoved()) {
item.getChildren().removeIf(treeItem -> c.getRemoved().contains(treeItem.getValue()));
}
}
});
return item ;
}
private PseudoClass asPseudoClass(Class<?> clz) {
return PseudoClass.getPseudoClass(clz.getSimpleName().toLowerCase());
}
}
Quick test, which works, but note you probably need to test more of the functionality:
package application;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import model.Account;
import model.GameCharacter;
import model.GameObject;
import model.Information;
import model.Item;
import ui.Tree;
public class Main extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
Tree tree = new Tree(createAccounts());
TreeView<GameObject<?>> treeView = tree.getTreeView();
TextField addField = new TextField();
Button addButton = new Button("Add");
EventHandler<ActionEvent> addHandler = e -> {
TreeItem<GameObject<?>> selected = treeView
.getSelectionModel()
.getSelectedItem();
if (selected != null) {
selected.getValue().createAndAddChild(addField.getText());
addField.clear();
}
};
addField.setOnAction(addHandler);
addButton.setOnAction(addHandler);
addButton.disableProperty().bind(Bindings.createBooleanBinding(() -> {
TreeItem<GameObject<?>> selected = treeView.getSelectionModel().getSelectedItem() ;
return selected == null || selected.getValue() instanceof Information ;
}, treeView.getSelectionModel().selectedItemProperty()));
Button deleteButton = new Button("Delete");
deleteButton.setOnAction(e -> {
TreeItem<GameObject<?>> selected = treeView.getSelectionModel().getSelectedItem() ;
TreeItem<GameObject<?>> parent = selected.getParent() ;
parent.getValue().getItems().remove(selected.getValue());
});
deleteButton.disableProperty().bind(treeView.getSelectionModel().selectedItemProperty().isNull());
HBox controls = new HBox(5, addField, addButton, deleteButton);
controls.setPadding(new Insets(5));
controls.setAlignment(Pos.CENTER);
BorderPane root = new BorderPane(treeView);
root.setBottom(controls);
Scene scene = new Scene(root, 600, 600);
scene.getStylesheets().add(getClass().getResource("/ui/style/style.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
private ObservableList<Account> createAccounts() {
Account jake = new Account("Jake");
Account mark = new Account("Mark");
Account freshAcc = new Account("Fresh Account");
Account marksAltAcc = new Account("Mark's alternative account");
Account jeffrey = new Account("Jeffrey");
GameCharacter jakesChar = new GameCharacter("Jakes character");
Item amazingSword = new Item("Amazing Sword");
Item brokenBow = new Item("Broken Bow");
Item junkMetal = new Item("Junk Metal");
GameCharacter myChar = new GameCharacter("Me");
Item godlyAxe = new Item("Godly Axe");
GameCharacter level = new GameCharacter("I'll level this I promise");
GameCharacter jeff = new GameCharacter("Jeff");
Item superGun = new Item("Super Gun");
Item superGunScope = new Item("Super Gun Scope");
jake.getItems().add(jakesChar);
mark.getItems().add(myChar);
marksAltAcc.getItems().add(level);
jeffrey.getItems().add(jeff);
jakesChar.getItems().addAll(amazingSword, brokenBow, junkMetal);
myChar.getItems().add(godlyAxe);
jeff.getItems().addAll(superGun, superGunScope);
return FXCollections.observableArrayList(jake, mark, freshAcc, marksAltAcc, jeffrey);
}
}
and the CSS as an example:
.tree-cell, .tree-cell:hover:empty {
-fx-background-color: -fx-background ;
-fx-background: -fx-control-inner-background ;
}
.tree-cell:hover {
-fx-background-color: crimson, -fx-background ;
-fx-background-insets: 0, 1;
}
.tree-cell:account {
-fx-background: lightsalmon ;
}
.tree-cell:gamecharacter {
-fx-background: bisque ;
}
.tree-cell:item {
-fx-background: antiquewhite ;
}
.tree-cell:selected {
-fx-background: crimson ;
}

Categories