Alright, I have been reading a lot of stackoverflow posts about how to PROPERLY create a JavaFX application with two windows that share data between them I can't find any clear solution. This is my current solution, basically creating a reference to the other controller which is really bad practice.
Would it be good practice to create a Model that is shared between the two controllers? If so how would I inject the Model object into the controllers when they dont have any constructors?
public class ControllerOne implements Initializable {
#FXML private TextField textField;
#Override
public void initialize(URL url, ResourceBundle rb) {
}
#FXML
private void handleButtonAction(ActionEvent event) {
if(textField.getText().equals("")){
}
else{
System.out.println(textField.getText());
String itemNumber = textField.getText();
Main.getControllerTwo().setLabel(itemNumber);
textField.setText("");
textField.requestFocus();
}
}
}
Second window
public class ControllerTwo implements Initializable {
#FXML
private Label itemLabel;
#Override
public void initialize(URL location, ResourceBundle resources) {
// TODO Auto-generated method stub
}
public void setLabel(String itemNumber){
itemLabel.setText(itemNumber);
}
public Label getLabel(){
return itemLabel;
}
}
Related
I have 2 scenes :
The first one has a "Balance" Label, which displays the balance from a variable.
The second scene is the deposit scene where the user adds to the balance.
(Each scene has its controller class)
I want the balance to be updated when the user goes back to the first scene.
what's the best way to do so? I couldn't find an event for the scene shown, I found online only a stage example which triggers an event when the window is closed, but here I am just changing scenes by changing the mainstage scene.
I have tried making an object of the first scene class inside the second scene's class and calling a method to change the label text when I click the back button but that didn't work.
Here's the code for the first scene where lbBalance is the label I want to update, and updateBal is the method I am using in the second scene class.
public class accountController extends Controller implements Initializable {
#FXML private Label gilbert;
#FXML private Label lbBalance;
#FXML private Button deposit;
#FXML private Button btn_showBalance;
private application.depositController depositController;
#Override
public void initialize(URL location, ResourceBundle resources) {
lbBalance.setText(String.valueOf(BAL));
}
#FXML
public void handleDeposit(ActionEvent event) throws IOException {
Parent depositParent = FXMLLoader.load(getClass().getResource("deposit.fxml"));
depositScene = new Scene(depositParent);
mainStage.setScene(depositScene);
mainStage.show();
}
public void updateBal() {
lbBalance.setText(String.valueOf(BAL));
}
}
Here's the second scene's class
accountController backtoscene= new accountController();
#FXML private Label info;
#FXML private Button btn_depositfinal;
#FXML private TextField depositamount;
#FXML private Button btn_back;
public void initialize(URL location, ResourceBundle resources) {
// TODO Auto-generated method stub
}
#FXML
public void handleDepositFinal(ActionEvent event) {
deposit(Integer.parseInt(depositamount.getText()));
info.setVisible(true);
}
#FXML
public void handleBackButton(ActionEvent event) throws IOException {
backtoscene.updateBal();
mainStage.setScene(newscene);
}
TL;DR calling the method is giving me a nullPointerException, is there any other way to update the balance label when getting back to previous scene?
NOTE: I haven't tested the code, I just wrote it freehand, but it gives you a general idea.
Your main issue is that you are creating a new AccountController in the DepositController. Meaning it's a different one than the one you originally instantiated.
public class AccountController extends Controller implements Initializable {
#FXML private Label gilbert;
#FXML private Label lbBalance;
#FXML private Button deposit;
#FXML private Button btn_showBalance;
private application.DepositController depositController;
#Override
public void initialize(URL location, ResourceBundle resources) {
lbBalance.setText(String.valueOf(BAL));
}
#FXML
public void handleDeposit(ActionEvent event) throws IOException {
FXMLLoader loader = new FXMLLoader(getClass().getResource("deposit.fxml"));
Parent depositParent = loader.load();
depositScene = new Scene(depositParent);
depositController = loader.getController();
depositController .setAccountController(this);
mainStage.setScene(depositScene);
mainStage.show();
}
public void updateBal() {
lbBalance.setText(String.valueOf(BAL));
}
}
Here's the second class where you need to set the AccountController to be the one you originally initialized :
public class DepositController extends Controller implements Initializable {
AccountController backtoscene;
#FXML private Label info;
#FXML private Button btn_depositfinal;
#FXML private TextField depositamount;
#FXML private Button btn_back;
public void initialize(URL location, ResourceBundle resources) {
// TODO Auto-generated method stub
}
public void setAccountController(AccountController controller){
backtoscene = controller;
}
#FXML
public void handleDepositFinal(ActionEvent event) {
deposit(Integer.parseInt(depositamount.getText()));
info.setVisible(true);
}
#FXML
public void handleBackButton(ActionEvent event) throws IOException {
backtoscene.updateBal();
mainStage.setScene(newscene);
}
}
Now you have access to the AccountController you originally initialized at the start, and the AccountController has access to the correct DepositController.
I have many FXML with different Controller and I want to access all Controller Instance in one class using static methods. The reason I am doing this because I want to change the UI of different FXML from different controller. I'm not sure if there is any better way to do it. My problem is in my SceenViews class because I don't know what datatype to use to hold FXML Controller Instance in my controllerMap variable.
Inside my Package: Main.java, ScreenViews.java, Frame.FXML, FrameController.java, Login.FXML, LoginController.java, Dashboard.FXML, DashboardController.java, Journal.FXML, and JournalController.java
Main.java
public class Main extends Application {
#Override
public void start(Stage stage) throws Exception {
ScreenViews.loadFXML("Frame", "Frame.fxml");
ScreenViews.loadFXML("Login", "Login.fxml");
ScreenViews.loadFXML("Dashboard", "Dashboard.fxml");
ScreenViews.loadFXML("Journal", "Journal.fxml");
Scene scene = new Scene((Parent) ScreenViews.getView("Frame"));
scene.setFill(Color.TRANSPARENT);
stage.initStyle(StageStyle.TRANSPARENT);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
In my ScreenViews.java what datatype can I use to hold different controller instance in my controllerMap variable?
public class ScreenViews {
private static HashMap<String, Node> viewMap = new HashMap<>();
//HASHMAP FOR CONTROLLER INSTANCE
private static HashMap<String, datatype? > controllerMap = new HashMap<>();
public static void addView(String name, Node screen) {
viewMap.put(name, screen);
}
public static Node getView(String name) {
return viewMap.get(name);
}
public static void addController(String name, datatype? controller) {
controllerMap.put(name, controller);
}
public static datatype? getController(String name) {
return controllerMap.get(name);
}
public static boolean loadFXML(String name, String resource) {
try {
FXMLLoader fxLoader = new
FXMLLoader(ScreenViews.class.getResource(resource));
Parent loadedFXML = (Parent) fxLoader.load();
addView(name, loadedFXML);
addController(name, fxLoader.getController());
return true;
} catch (IOException e) {
System.out.println(e.getMessage());
return false;
}
}
}
Sample LoginController.java code
public class LoginController implements Initializable {
#Override
public void initialize(URL url, ResourceBundle rb) {
}
#FXML
public void frameSetUI(ActionEvent event) {
ScreenViews.getController("Frame").getFramePane().getChildren().add(ScreenViews.get("Dashboard"));
}
}
Sample FrameController.java code
public class FrameController implements Initializable {
#FXML private StackPane rootPane;
#FXML private AnchorPane titleBar;
#FXML private AnchorPane framePane;
#FXML private Button toDashboard;
#FXML private AnchorPane mainPane;
public void initialize(URL url, ResourceBundle rb) {
}
public AnchorPane getFramePane(){
return framePane;
}
}
I have a method that takes a while to complete, when the jar application is started. To have some feedback, i created the form frmWaiting, that displays a simple indeterminate progress bar. I also have a controller for the form,
PrincipalController.
Entry point for the application
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
try {
Stage stagePrincipal = new Stage();
Parent parentPrincipal = FXMLLoader.load(getClass().getClassLoader().getResource("frmPrincipal.fxml"));
Scene scenePrincipal = new Scene(parentPrincipal, 300, 275);
stagePrincipal.setScene(scenePrincipal);
stagePrincipal.setHeight(400);
stagePrincipal.setWidth(500);
stagePrincipal.setResizable(false);
stagePrincipal.setTitle("Instalador");
stagePrincipal.show();
} catch (Exception e) {
e.printStackTrace();
}
}
PrincipalController - frmPrincipal.fxml:
#Override
public void initialize(URL location, ResourceBundle resources) {
try {
Stage stageWaiting = new Stage();
Parent parentWaiting;
parentWaiting = FXMLLoader.load(getClass().getClassLoader().getResource("frmWaiting.fxml"));
Scene sceneWaiting = new Scene(parentWaiting, 300, 275);
stageWaiting.setScene(sceneWaiting);
stageWaiting.setHeight(300);
stageWaiting.setWidth(400);
stageWaiting.setResizable(false);
stageWaiting.setTitle("Instalador");
stageWaiting.show();
} catch (IOException e) {
}
}
WaitingController - frmWaiting.xml:
public class WaitingController implements Initializable {
#FXML private ImageView img;
#FXML private ProgressBar progressBar;
#FXML private ProgressIndicator pgIndicator;
private Task copyTask;
#Override
public void initialize(URL location, ResourceBundle resources) {
img.setImage(new Image(getClass().getClassLoader().getResourceAsStream("image.png")));
progressBar.setProgress(ProgressBar.INDETERMINATE_PROGRESS);
ArquivoController.getInstance().copiaArquivosPadrao(); //This is the method that takes a while.
}
public ProgressBar getProgressBar() {
return progressBar;
}
I want to initialize my main form, frmPrincipal, when my method that takes a while finishes. I also want to get the progress bar working. I have tried to do it on another Thread, but i could not get the response from it when the method finishes.
All the .fxml files are correct, ommited them to make things easier if possible.
The way it is, the application waits for the method to finish, then opens the other form. But, the progressBar does not update.
Inside ArquivoController.getInstance().copiaArquivosPadrao(); you have to update the progressBar's progress.
You could do it the nice way, using a Task to run copiaArquivosPadrao(), update the tasks progress accordingly and binding to the tasks progress property.
Or you could do it the ugly way, passing progressBar to copiaArquivosPadrao(), something like this:
public void initialize(URL location, ResourceBundle resources) {
img.setImage(new Image(getClass().getClassLoader().getResourceAsStream("image.png")));
progressBar.setProgress(ProgressBar.INDETERMINATE_PROGRESS);
ArquivoController.getInstance().copiaArquivosPadrao(progressBar); //This is the method that takes a while.
}
and
public void copiaArquivosPadrao(ProgressBar progressBar) {
// call progressBar.setProgress() in here to update the progress bar
// eg.
progressBar.setProgress(0.0F);
doSomething();
progressBar.setProgress(0.20F);
doSomething();
progressBar.setProgress(0.40F);
doSomething();
progressBar.setProgress(0.60F);
doSomething();
progressBar.setProgress(0.80F);
doSomething();
progressBar.setProgress(1.00F);
}
For sure, you can do this more fine-grained in a loop or similar.
I have a program with this window structure:
Where left side has buttons and green zone is different windows that change where user clic on a left button.
I have a main class (call Estructura) thats acts as control for main window except green panel. For green panel, I inject (thanks to afterburner.fx DI) correspond panel base on user button selection from left side. Injected panel has its own controller that is lazy thanks to DI Framework base on MVP.
public class EstructuraPresenter implements Initializable {
private static final Logger LOG = getLogger(EstructuraPresenter.class.getName());
#FXML
ToggleButton btnPlantillas, btnAlumnos, btnEstadisticas;
#FXML
BorderPane pEstructura; //Correspond to all window except green zone
#FXML
StackPane pContenedor; //Green zone
//Injection of all child windows that i want to show in green zone
#Inject
private AlumnosView alumnosview;
#Inject
private PlantillasView plantillasview;
#Inject
private EstadisticasView estadisticasview;
#Override
public void initialize(URL url, ResourceBundle rb) {
//I set up green zone with alumnos panel so I call method whit request section to load
cambiarSeccion("Alumnos");
btnPlantillas.setOnAction((ActionEvent event) -> {
cambiarSeccion("Plantillas");
});
btnAlumnos.setOnAction((ActionEvent event) -> {
cambiarSeccion("Alumnos");
});
btnEstadisticas.setOnAction((ActionEvent event) -> {
cambiarSeccion("Estadisticas");
});
}
//I pass to this method, name of window/panel that I like to load in green zone
public void cambiarSeccion(String nombreVentana) {
try {
//First, reset all buttons (so when user select an option and enter in case, I select option button making like effect as a selected)
btnAlumnos.setSelected(false);
btnPlantillas.setSelected(false);
btnEstadisticas.setSelected(false);
switch (nombreVentana) {
case "Alumnos":
if (btnAlumnos.isSelected() == false) {
sepTitulo.setVisible(true);
lbTitulo.setText("Alumnos");
btnAlumnos.setSelected(true);
//I need to do this check because for first time program load because green zone hasn't got any previous panel load
if (pContenedor.getChildren().contains(alumnosview.getView())) {
pContenedor.getChildren().clear();
alumnosview.getViewAsync(pContenedor.getChildren()::add);
} else {
pContenedor.getChildren().add(alumnosview.getView());
}
}
break;
case "Plantillas":
if (btnPlantillas.isSelected() == false) {
sepTitulo.setVisible(true);
lbTitulo.setText("Plantillas");
btnPlantillas.setSelected(true);
pContenedor.getChildren().clear();
plantillasview.getViewAsync(pContenedor.getChildren()::add);
}
break;
case "Estadisticas":
if (btnEstadisticas.isSelected() == false) {
sepTitulo.setVisible(true);
lbTitulo.setText("Estadísticas");
btnEstadisticas.setSelected(true);
pContenedor.getChildren().clear();
estadisticasview.getViewAsync(pContenedor.getChildren()::add);
}
break;
//There are more cases but I use same configuration like above examples...
}
}
catch (Exception e) {
LOG.log(Level.SEVERE, e.toString());
new Dialogos().mostrarExcepcion(null, e);
}
}
}
Well, when user clic on left button, panel is load ok in green zone. But problem I have is where user for example in green zone clic on a button/link... to load another panel (similiar to above description) but in that case I need to call from green panel controller to method cambiarSeccion() that is in EstructuraPresenter controller.
So for example, on case of user select estadisticas button (i hide in image, sorry I tried to simplified), that's is estadisticaspresenter controller:
public class EstadisticasPresenter implements Initializable {
private static final Logger LOG = getLogger(EstadisticasPresenter.class.getName());
#FXML
Hyperlink linkTotalAlumnos;
#Inject
private EstructuraView estructuraview;
#Override
public void initialize(URL url, ResourceBundle rb) {
linkTotalAlumnos.setOnAction((ActionEvent event) -> {
//This call apparently works, I debug and call to method cambiarSeccion happend but screen isn't update
((EstructuraPresenter) estructuraview.getPresenter()).cambiarSeccion("Alumnos");
});
}
}
Where estadisticas window is loaded If I click on left buttons, works but If I clic on green load panel nothing happend. I resume problem in next image:
In my opinion your approach has one problem: When you call in any of your children presenters
((EstructuraPresenter) estructuraview.getPresenter())
.cambiarSeccion("Alumnos");
estructuraview.getPresenter() is actually creating a new instance of the main presenter.
This means that EstructuraPresenter.initialize() is being called again when you click on the hyperlink, and you have two calls to cambiarSeccion() (one from Initialize, one from the action), that add the children to the new instance, not to the old one, which is the one you see. That's why you don't see any change!
I'd suggest a different approach: Let the main presenter listen to changes in some properties on their childrens.
For example, add a boolean property to notify a click on the hyperlink:
public class EstadisticasPresenter implements Initializable {
#FXML Hyperlink linkTotalAlumnos;
private final BooleanProperty link = new SimpleBooleanProperty();
public boolean isLink() { return link.get(); }
public void setLink(boolean value) { link.set(value); }
public BooleanProperty linkProperty() { return link; }
#Override public void initialize(URL url, ResourceBundle rb) {
linkTotalAlumnos.setOnAction((ActionEvent event) -> {
link.set(true);
});
}
}
while on the main presenter:
#Override
public void initialize(URL url, ResourceBundle rb) {
...
EstadisticasPresenter estadisticas =
(EstadisticasPresenter)estadisticasview.getPresenter();
estadisticas.linkProperty().addListener((ob,b,b1)->{
if(b1){
cambiarSeccion("Alumnos");
// reset link property
estadisticas.setLink(false);
}
});
}
As a side note, instead of clear() and add, I'd use pContenedor.getChildren().setAll(<view>); since you only show one child at a time.
Base on #josé-pereda answer, finally I can link button/hyperlink from sub-window (green area) to main controller. The advantage of my solution is that I avoid use of boolean variables.
In EstructuraPresenter controller I add listeners to buttons from EstadisticasPresenter(which is the controller from one of the sub-window):
public class EstructuraPresenter implements Initializable {
private static final Logger LOG = getLogger(EstructuraPresenter.class.getName());
#FXML
ToggleButton btnAlumnos, btnEstadisticas;
#FXML
StackPane pContenedor;
#Inject
private AlumnosView alumnosview;
#Inject
private EstadisticasView estadisticasview;
#Inject
private DataModel datos;
#Override
public void initialize(URL url, ResourceBundle rb) {
((EstadisticasPresenter) estadisticasview.getPresenter()).getLinkTotalAlumnos().setOnAction((ActionEvent event) -> {
this.cambiarSeccion("Alumnos");
});
//This call is from first time, where program starts to show by default Alumnos sub-window
cambiarSeccion("Alumnos");
btnAlumnos.setOnAction((ActionEvent event) -> {
cambiarSeccion("Alumnos");
});
btnEstadisticas.setOnAction((ActionEvent event) -> {
cambiarSeccion("Estadisticas");
});
}
public void cambiarSeccion(String nombreVentana) {
try {
btnAlumnos.setSelected(false);
btnEstadisticas.setSelected(false);
switch (nombreVentana) {
case "Alumnos":
if (btnAlumnos.isSelected() == false) {
sepTitulo.setVisible(true);
lbTitulo.setText("Alumnos");
btnAlumnos.setSelected(true);
alumnosview.getViewAsync(pContenedor.getChildren()::setAll);
pContenedor.getChildren().setAll(alumnosview.getView());
}
break;
case "Estadisticas":
if (btnEstadisticas.isSelected() == false) {
sepTitulo.setVisible(true);
lbTitulo.setText("Estadísticas");
btnEstadisticas.setSelected(true);
estadisticasview.getViewAsync(pContenedor.getChildren()::setAll);
}
break;
}
}
catch (Exception e) {
LOG.log(Level.SEVERE, e.toString());
new Dialogos().mostrarExcepcion(null, e);
}
}
}
NOTE In addition, I change getChildren().clear() && getChildren().add() to getChildren().setAll(); These prevents re-initializate controllers from each window that I put in green area.
estadisticasview.getViewAsync(pContenedor.getChildren()::setAll);
And in EstadisticasPresenter controller, I add getters from that controls that I want to user clic to go to another window (so when user clic on control, green area change with another window/pane).
public class EstadisticasPresenter implements Initializable {
private static final Logger LOG = getLogger(EstadisticasPresenter.class.getName());
#FXML
Hyperlink linkTotalAlumnos;
#Inject
private DataModel datamodel;
#Override
public void initialize(URL url, ResourceBundle rb) {
}
//Getters that I use in EstructuraPresenter controller to set up event handlers
public Hyperlink getLinkTotalAlumnos() {
return linkTotalAlumnos;
}
}
I have my UIController class and a ButtonMethods class. The ButtonMethods class contains all the code for the button actions. I need to be able to use the buttons from the controller class in the ButtonMethods class. For example, I have these defined in the controller class
ButtonMethods button = new ButtonMethods();
#FXML Button buttonLockdown;
#FXML Button buttonRelease;
And for example, the buttonLockdown has an ActionEvent when clicked
#FXML
private void actionLockdown(ActionEvent event) {
button.lockdown();
Ideally, I want the ButtonMethods to do this:
public void lockdown() {
buttonLockdown.setDisable(true);
onLockdown = true;
buttonRelease.setDisable(false);
I can't just put that code into the action event for various reasons, and putting the button objects into the parameters would get too messy with what I'm trying to do. So how can I get FXML objects into the button class?
Try to send the UIController itself as parameter:
private UIController thisController;
#Override
public void initialize(URL url, ResourceBundle rb) {
thisController = this;
}
#FXML
private void actionLockdown(ActionEvent event) {
button.lockdown(thisController);
then
public void lockdown(UIController controller) {
controller.getButtonLockdown().setDisable(true);
onLockdown = true;
controller.getButtonRelease().setDisable(false);
You can also use bindings in appropriate situations.