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;
}
}
Related
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;
}
}
I a have tabPane with multiple tabs and inside one of the tabPane i have scrollPane. Inside the scrollPane i have a number of UI controls As shown on this image that i want to be dynamically added when i click a button.
This image here shows the UI setup of what i have. The UI controls are in an Hbox which is inside a Vbox.
When a user clicks the add button i would like to add another row with all the controls as shown in this image.
I have looked at post such as these and still cannt get to achieve the same effect on my code.
JavaFX continuous form dynamically add new row with content in gridpane,
Dynamically add elements to a fixed-size GridPane in JavaFX,
How to maintain GridPane's fixed-size after adding elemnts dynamically
When i followed the approach suggested by another stack-overflow member on the first question cited above, to use a listView the code worked. However his suggestion programmatically codes UI controls and when i try to change and use UI controls i created within scene builder it doesn't work. Also when i put the controls inside a tabPane my code doesn't work. Meaning when i click the add button it doesn't add the UI controls as expected.
My question is how can i dynamically add UI controls within a tab inside a tabPane when a button which is also in that tab is clicked.
Here is my code :
public class DynamicalyAddControlsController {
#FXML
private VBox vbox;
#FXML
private HBox hbox;
#FXML
private StackPane stack1;
#FXML
private Label taskid;
#FXML
private TextField taskname;
#FXML
private ComboBox<String> combobox;
#FXML
private TextArea textarea;
#FXML
private Button save;
private ObservableList<Car> cars = FXCollections.observableArrayList();
public void initialize(URL url, ResourceBundle rb) {
cars.addAll(new Car(CAR_TYPE.CAR1), new Car(CAR_TYPE.CAR2), new Car(CAR_TYPE.CAR3));
ListView<Car> carsListView = new ListView<>();
carsListView.setCellFactory(c -> new CarListCell());
carsListView.setItems(cars);
stack1.getChildren().add(carsListView);
}
private class CarListCell extends ListCell<Car> {
private ChoiceBox<CAR_TYPE> choiceBox = new ChoiceBox<>();
public CarListCell(){
choiceBox.setItems(FXCollections.observableArrayList
(CAR_TYPE.values()));
hbox.getChildren().addAll(taskid,taskname,combobox,choiceBox);
vbox.getChildren().addAll(hbox,textarea);
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
setGraphic(vbox);
}
#Override
protected void updateItem(Car item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setText(null);
setGraphic(null);
} else {
setGraphic(vbox);
choiceBox.setValue(item.getType());
save.setOnAction(e -> {
Car newCar = new Car(choiceBox.getValue());
cars.add(newCar);
});
}
}
}
private enum CAR_TYPE {
CAR1, CAR2, CAR3;
}
private class Car {
private CAR_TYPE type;
public Car(CAR_TYPE type) {
this.type = type;
}
public CAR_TYPE getType() {
return type;
}
public void setType(CAR_TYPE type) {
this.type = type;
}
}
public void initialize(URL url, ResourceBundle rb) {
}
}
JavaApplication main class
#Override
public void start(Stage primaryStage) throws Exception{
Parent viewB = FXMLLoader.load(getClass().getResource("DynamicalyAddControls.fxml"));
Stage stageB = new Stage();
stageB.setScene(new Scene(viewB));
stageB.show();
}
public static void main(String[] args) {
launch(args);
}
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 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.
So I am trying to handle WINDOW_SHOWN event from my controller with code like this:
#Override
public void initialize(URL url, ResourceBundle resourceBundle) {
initializeDatePickers();
System.out.println("payer number in initialize: " + payerNumber);
URL location = getClass().getResource("/createUser.fxml");
FXMLLoader loader = new FXMLLoader();
try {
Parent root = (Parent) loader.load(location.openStream());
root.getScene().getWindow().setOnShown(new EventHandler<WindowEvent>() {
#Override
public void handle(WindowEvent event) {
System.out.println("ONSHOWN");
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
But all I've got was endless cycle and program crash.
The code below didn't work either, it returns NullPointerException:
#FXML private AnchorPane createUserDialog; //my root pane
#Override
public void initialize(URL url, ResourceBundle resourceBundle) {
createUserDialog.getScene().getWindow().addEventHandler(WindowEvent.WINDOW_SHOWN,
new EventHandler<WindowEvent>() {
#Override
public void handle(WindowEvent window) {
System.out.println("ONSHOWN");
}
});
}
Implementing WindowEvent interface didn't work at all, don't know why.
So, how could I handle this event? And why I've got NullPointerException? In docs said that initialize() calling only after root pane completely processed.
When the initialize() method is being executed, the root pane is completely constructed but is not added to a scene, or a window. (The initialize() method is executed as part of the execution of your FXMLLoader's load() method; check the code where you call that and you will see that you add the root to a scene and place it in a window after that.) So during the execution of intialize(), root.getScene() will return null.
You can use a Binding to check when the window changes and attach a listener to it:
final EventHandler<WindowEvent> shownHandler = new EventHandler<WindowEvent>() {
#Override
public void handle(WindowEvent event) {
System.out.println("Shown");
}
};
Bindings.<Window>select(createUserDialog.sceneProperty(), "window").addListener(new ChangeListener<Window>() {
#Override
public void changed(ObservableValue<? extends Window> observable,
Window oldValue, Window newValue) {
if (oldValue != null) {
oldValue.removeEventHandler(WindowEvent.WINDOW_SHOWN, shownHandler);
}
if (newValue != null) {
newValue.addEventHandler(WindowEvent.WINDOW_SHOWN, shownHandler);
}
}
});
This code assumes the root is only ever added to one window; in the unlikely event you're taking the root out of one window and putting it in another during your application life cycle, you would need to remove the listener from the old window. If you need this I'll update the code, but it makes it more complex.