I've searched through all the other issues with this, but with no luck. I have been trying different things for over four hours and have gotten absolutely no progress (or errors, which is even more frustrating) so I figured I would finally ask here.
I am working on a small survival game as a side project to teach myself more about Java UI elements. The player scavenges for food and such, and it is a race against starvation and thirst. The situation I have is such:
The game opens into a controller (MainController) that has several bars indicating the character's stats; for this example, we'll say Hunger and Thirst.
The player can click a button to open the character's inventory, which includes their food. This opens a new window on a new controller (InventoryController) that lists buttons corresponding to the character's inventory items.
When the player clicks a food item, the character's Hunger drops, and the associated ProgressBar should drop as well.
Issue is: The character's internal hunger stat drops, and the ProgressBar's progress drops, but the bar never updates visually, even though it did internally.
As a brief example:
public class MainController implements Initializable {
@FXML private ProgressBar hungerBar;
public void initialize(URL url, ResourceBundle rb) {
updateBars();
}
public void updateBars(){
hungerBar.setProgress(CC.pc.getHunger()/100);
}
}
This works at first. It successfully get's the character's hunger, divides it by 100, and then sets the bar (in this case, to .40). No issues. The bar appears and is 40% full. However, when I open and use the second Controller, nothing happens.
public class InventoryController implements Initializable {
@Override
public void initialize(URL url, ResourceBundle rb) {
}
@FXML private void feedVenison(ActionEvent event) throws Exception{
FXMLLoader loader = new FXMLLoader(getClass().getResource("Main.fxml"));
loader.load();
PlayController c = loader.<PlayController>getController();
//Stuff that does the math and changes the character's stats here;
//cut short for clarity.
c.updateBars();
}
}
I can add in a few Print commands, such as a System.out.println(hungerBar.getProgress()), and see that everything functions perfectly fine. The Character's hunger decreases, and in turn, the bar's progress is updated and is, functionally, lower. However, the bar never changes, no matter where between .01 and 1 it is set.
I've tried and tried and I'm at my wits end! I'm obviously doing something wrong, and I'm sure it is obvious, but I have no idea what. There's no errors, it just doesn't work. Am I just using the wrong tool for this job? What gives?
Update 1:
More digging brought me to this question, which is very similar to what I am trying to do. Unfortunately, it has no answers either. It has a comment that leads to this Oracle doc, which I attempted to implement.
public void updateBars(){
DoubleProperty hunger = new SimpleDoubleProperty(CC.pc.getHunger());
hungerBar.progressProperty().bind(hunger);
}
But once again, I get the exact same issue. The bar updates internally, yet the UI never changes and always displays a static amount that is incorrect. What do I do?
Update 2:
I keep digging, but every time I think I have found the answer it just does the exact same thing over and over again. Tried out this, but same result.
public void updateBars(){
maybeWillWork();
}
public void maybeWillWork()
{
Platform.runLater(new Runnable() {
@Override public void run() {
hungerBar.setProgress(CC.pc.getHunger()/100);
System.out.println("It ran!");
}
});
}
Exact same result.
Please. What am I doing wrong? I refuse to believe that the entirety of StackOverflow is stumped by a simple ProgressBar like I am; someone has to know what stupid mistake I am making.
Update 3:
Well that was frustrating, but I got it to work. I have posted the answer below.
What you're doing wrong in your approach
You're updating the value when the controller is initialized, but you're only making a "snapshot" of the value.
Even if you're using the property (Update 1) you're creating a independent property that is initially filled with the current value, but the value stored in the property is never updated.
But you're calling updateBars()
again, right? You do this but you create a new scene with a new controller instance for this purpose, so you do not adjust the scene that is displayed but some other scene that is never displayed.
For this reason the updates never reach the scene currently shown.
Of course you could store the existing controller in a convenient location and access it, but this way you increase the cohesion of your program.
A better approach would be to create a model class and add observable properties. You only create a single instance of this class and pass it everywhere where it's accessed. By observing the properties parts of your program "interested" in the value can update themselfs:
public class Model {
private final DoubleProperty hunger = new SimpleDoubleProperty();
public DoubleProperty hungerProperty() {
return hunger;
}
public void setHunger(double value) {
hunger.set(value);
}
public double getHunger() {
return hunger.get();
}
}
The model instance can be passed to the controllers by one of the approaches mentioned in this question: Passing Parameters JavaFX FXML
E.g.
public class MainController {
@FXML private ProgressBar hungerBar;
public void setModel(Model model){
hungerBar.progressProperty().bind(model.hungerProperty().divide(100d));
}
}
public class InventoryController {
@FXML private void feedVenison(ActionEvent event) throws Exception {
model.setHunger(someNewValue);
}
private Model model;
public void setModel(Model model){
this.model = model;
}
}
Make sure you're passing the same instance of Model
to both controllers using the approach in your own feedVenison
method but only when you load the a scene that is also displayed. A factory that loads the views and using a custom interface containtning the setModel
method could help reduce duplicate code...
Note: You could also achieve this by using a property in the class used for CC.pc
, but IMHO using static members to pass information should be avoided.
After nearly eight hours of trying, I finally figured out the issue. Now, I freely admit that I don't entirely understand how this works, or why, but it does function exactly as I hoped it would. It took dozens of searches over far too much time, but I finally stumbled across this page. Instead of updating the label directly, he was using something called a "binding". Well, that's different... Of course, I had no idea what this was, but I dug and dug and lo and behold...
The bar needs to be bound to a DoubleProperty, and then that gets .set() to something, which updates the bar.
I had toyed with the DoubleProperty in Update 1, but I neglected to attempt using the .set() function. Sure enough...
This is what finally worked for me.
@FXML ProgressBar hungerBar;
static DoubleProperty hungerUpdater = new SimpleDoubleProperty(.0);
public void initialize(URL url, ResourceBundle rb) {
hungerBar.progressProperty().bind(hungerUpdater);
}
We create a new DoubleProperty, then bind the bar we wish to update to it. Later, on the same controller, we have:
public void updateBars(){
hungerUpdater.set(CC.pc.getHunger()/100);
}
We grab the character's Hunger (which is between 0 and 100) and divide it by 100, so that we get a decimal between 0 and 1. This is then used to set the hungerUpdater DoubleProperty.
On the other controller, we have:
public void initialize(URL url, ResourceBundle rb) {
}
@FXML private void feedVenison(ActionEvent event) throws Exception{
FXMLLoader loader = new FXMLLoader(getClass().getResource("Main.fxml"));
loader.load();
MainController c = loader.<MainController>getController();
//Stuff that does the math and changes the character's stats here;
//cut short for clarity.
c.updateBars();
}
}
From what I can gather, this essentially forces JavaFX to update the bar immediately, because the bar is actively listening to this Double for an update. As soon as an update is seen, the program updates it.
This was an extremely frustrating evening, but I finally got it. I'm going to go drink heavily and pretend I didn't struggle this hard on something so simple and, to many, probably quite obvious.