2

I have a JavaFX application that uses a preloader. What I'd like to do is package it up as a native bundle (Mac app or Windows exe file that contains a copy of the Java JDK) so users who don't have the right version of Java on their computers can still run the app. I've followed Oracles instructions for creating native bundles and for adding preloaders. What I get is exactly what you'd expect—a native bundle that runs my program.

The problem is that the bundle completely ignores my preloader. It just runs the main program (after a long load time). I know the preloader is included because, when I run the jar file alone, it shows up.

Has anyone successfully bundled a JavaFX app with a preloader? Can you guide me through how to do so? I'm using Netbeans.

EDIT:

Here is the Preloader:

import javafx.application.Preloader;
import javafx.application.Preloader.ProgressNotification;
import javafx.application.Preloader.StateChangeNotification;
import javafx.scene.Scene;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
import javafx.stage.StageStyle;

public class Splash extends Preloader {

    ProgressIndicator bar;
    ImageView Background;
    Stage stage;

    private Scene createPreloaderScene() {
        bar = new ProgressIndicator();
        bar.setLayoutX(380);
        bar.setLayoutY(250);
        bar.setPrefSize(60, 60);

        Background = new ImageView("Images/Splash.png");
        Background.setEffect(null);

        Pane p = new Pane();
        p.setStyle("-fx-background-color: transparent;");
        p.getChildren().addAll(Background, bar);

        Scene scene = new Scene(p, 794, 587);      
        scene.setFill(null);
        scene.getStylesheets().add(Scrap2.class.getResource("CSS/Progress.css").toExternalForm());
        bar.setId("myprogress");
        return scene;
    }

    @Override
    public void start(Stage stage) throws Exception {
        this.stage = stage;
        stage.setScene(createPreloaderScene());    
        stage.initStyle(StageStyle.TRANSPARENT);
        stage.show();
    }

    @Override
    public void handleStateChangeNotification(StateChangeNotification scn) {
        if (scn.getType() == StateChangeNotification.Type.BEFORE_START) {
            stage.hide();
        }
    }

    @Override
    public void handleProgressNotification(ProgressNotification pn) {
        bar.setProgress(pn.getProgress());
    }    

    @Override
   public void handleApplicationNotification(PreloaderNotification arg0) {
          if (arg0 instanceof ProgressNotification) {
             ProgressNotification pn= (ProgressNotification) arg0;
             bar.setProgress(pn.getProgress());
          }
    }

}

And here is the first part of my main program:

@Override
public void init(){

    /*Root*/
    root = new Pane();
    root.setStyle("-fx-background-color: transparent;");
    root.setLayoutX(150);

    notifyPreloader(new Preloader.ProgressNotification(0.1));

    /*Create Background*/
    createBinding(stage);
    createContents();
    createSaveMessages();
    createFlipBook();

    notifyPreloader(new Preloader.ProgressNotification(0.2));

    /*Add Pages*/
    createOverview();
    createAccounts();
    notifyPreloader(new Preloader.ProgressNotification(0.3));
    createCounselors();
    createInsurance();
    notifyPreloader(new Preloader.ProgressNotification(0.4));
    createAssets();
    createPapers();
    notifyPreloader(new Preloader.ProgressNotification(0.5));
    createLoans();
    createFuneral();
    notifyPreloader(new Preloader.ProgressNotification(0.6));
    createWills();
    addAllPages();
    notifyPreloader(new Preloader.ProgressNotification(0.7));

    /*Add Toolbar on top*/
    createToolBar();
    notifyPreloader(new Preloader.ProgressNotification(0.9));

    /*Create Opening Instructions*/
    opening();

    /*Load Saved Data*/
    load();
    notifyPreloader(new Preloader.ProgressNotification(1.0));


}

@Override
public void start(Stage stage) {
    /*Scene*/
    scene = new Scene(root, 1200, 700);
    stage.setScene(scene);
    scene.setFill(null);

    /*Stage*/
    this.stage = stage;
    stage.initStyle(StageStyle.TRANSPARENT);
    stage.centerOnScreen();
    stage.show();
}
corpico
  • 617
  • 3
  • 16
  • 26
  • if it is a netbeans javafx project, simply activate native packaging in properties. If you need a more custom way, use the javapackager in the bin dir [Javapackager](https://docs.oracle.com/javase/8/docs/technotes/tools/windows/javapackager.html) – aw-think Jun 21 '15 at 18:26
  • I think there's a problem when Netbeans adds the preloader. In Properties – corpico Jun 21 '15 at 22:08
  • You have to provide a minimal code example of your solution (preloader / app). Please post your preloader and the application (maybe only an excerpt). My two cents: Don't use a preloader for a native bundled desktop app. The Preloader was intended to work for JNLP perfect. Please read [Application Startup](https://docs.oracle.com/javafx/2/deployment/deploy_user_experience.htm#BABFIECI), than you might only want a [splash screen solution](https://gist.github.com/jewelsea/2305098) – aw-think Jun 22 '15 at 09:25
  • The preloader works though when I just double click the jar. Perhaps I should ask instead how to make a jar file run with a specified JRE. I already have the app structure—it's just the launcher file that seems to be screwing up. – corpico Jun 22 '15 at 14:22
  • Just tried Jewelsea's solution. It's a nice idea but it's just too slow. The benefit of the preloader—that it starts up quickly—is lost by putting it in the main start method. – corpico Jun 22 '15 at 14:45
  • In your app class, you do `createBinding(stage);` but the stage isn't initialized. It is first initilized in your start method. And handleProgressNotification and handleStateChangeNotification should do nothing or be deleted, otherwise your app fires these events, but you won't that. – aw-think Jun 22 '15 at 18:21

1 Answers1

3

This example will work with installers exe/msi/image only (have no Mac to test dmg). This step by step assumes, that you already installed the needed tools like InnoSetup, Wix Toolset, etc. It also assumes, that you have configured the tools to run with netbeans (setting paths, edit config files, etc.).

Prerequirements:

Step 1:

I've made a new JavaFX Application Project in Netbeans like this:

step1

Step 2:

Then I gave the project a name and said, that the wizard should create a preloader project with the given name too. Additionally it should create an application class in given package name.

step2

Step 3:

After that I right clicked on the application project and select under deployment "Enable Native Packaging".

step3

Step 4:

In step 4 I've created the code for the application. The preloader will be updated in the init() method and only there. All your work for initialization the application should go here.

JavaFXPreloaderApp.java

import javafx.application.Application;
import javafx.application.Preloader;
import javafx.event.ActionEvent;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class JavaFXPreloaderApp extends Application {

  @Override
  public void start(Stage primaryStage) {
    Scene scene = new Scene(createContent(), 300, 250);
    primaryStage.setTitle("Hello World!");
    primaryStage.setScene(scene);
    primaryStage.show();
  }

  public Parent createContent() {
    Button btn = new Button();
    btn.setText("Say 'Hello World'");
    btn.setOnAction((ActionEvent event) -> {
      System.out.println("Hello World!");
    });

    StackPane root = new StackPane();
    root.getChildren().add(btn);
    return root;
  }

  @Override
  public void init() throws Exception {
    // A time consuming task simulation
    final int max = 10;
    for (int i = 1; i <= max; i++) {
      notifyPreloader(new Preloader.ProgressNotification(((double) i) / max));
      Thread.sleep(500);
    }
  }

  /**
   * @param args the command line arguments
   */
  public static void main(String[] args) {
    launch(args);
  }
}

Step 5:

The only missing part was the preloader code. Look for the only needed method handleApplicationNotification, all the other methods, like handleProgressNotification or handleStateChangeNotification, you can safely delete, or make them empty stubs.

JavaFXPreloader.java

import javafx.application.Preloader;
import javafx.application.Preloader.ProgressNotification;
import javafx.application.Preloader.StateChangeNotification;
import javafx.scene.Scene;
import javafx.scene.control.ProgressBar;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

/**
 * Simple Preloader Using the ProgressBar Control
 */
public class JavaFXPreloader extends Preloader {

  ProgressBar bar;
  Stage stage;

  private Scene createPreloaderScene() {
    bar = new ProgressBar();
    BorderPane p = new BorderPane();
    p.setCenter(bar);
    return new Scene(p, 300, 150);
  }

  @Override
  public void start(Stage stage) throws Exception {
    this.stage = stage;
    stage.setScene(createPreloaderScene());
    stage.show();
  }

  @Override
  public void handleApplicationNotification(PreloaderNotification info) {
    // Check if info is ProgressNotification
    if (info instanceof ProgressNotification) {
      // if yes, get the info and cast it
      ProgressNotification pn = (ProgressNotification) info;
      // update progress
      bar.setProgress(pn.getProgress());
      // if this was the last progress (progress reached 1), hide preloader
      // this is really important, if preloader isn't hide until app loader
      // reaches the start method of application and tries to open the stage of
      // the main app with the show() method, it will not work.
      if (pn.getProgress() == 1.0) {
        stage.hide();
      }
    }
  }
}

Step 6:

Now it was time to bundle the application to native packages (image only/exe/msi). I right clicked on the applicaton project and selected the packages to create one by one.

step6

Step 7:

After choosen to package as image only your directory should look like this:

step7

Step 8:

After digging deeper in your directory you should find the image:

step8

Step 9:

A double click on the .exe file should start your application:

step9

Remarks:

The biggest mistake you could do is, to call things in your application start methods. Normaly all have to be done in the application init method, there you load the huge files, there you will connect to the db, or there you load a huge custom layout with a lot of css or fxml files. And there is the place to say good bye to the preloader (progress = 1). Try not to do things at the preloader in your application start method. Don't think in Thread's, the preloader is there to do things before the main stage is shown, so load all in sequence.

aw-think
  • 4,723
  • 2
  • 21
  • 42
  • I'm getting the same behavior. The preloader works when the jar file is run alone but not when it's run through the App. Did you package your test program and did it work then? – corpico Jun 22 '15 at 15:22
  • If I start the jar outside of IDE only the preloader is shown and walks through the progress, after it, the preloader stage is closed and the main stage won't show up. If I show in task-manager, no java process runs anymore. – aw-think Jun 22 '15 at 15:24
  • Maybe you're hitting an exception in the main program. My jar file is working fine. It's still the packaging that wrecks it. – corpico Jun 22 '15 at 15:30
  • Updated my answer totally, now everything works as aspected. Read the JavaDoc of Preloader class, there is a mention, that the preloader should be finished in the init mehtod of main application. https://docs.oracle.com/javase/8/javafx/api/javafx/application/Preloader.html – aw-think Jun 22 '15 at 16:41
  • Thanks for the detailed response. I followed the above, making a new project from scratch with the Netbeans-generated preloader. Unfortunately it's just not working as a Mac App. The preloader still doesn't show up. – corpico Jun 22 '15 at 18:33
  • Mac has some Problems with this unsigned apps. For more information on Mac packaging look [here](https://docs.oracle.com/javase/8/docs/technotes/guides/deploy/self-contained-packaging.html) at point "7.3.5.1 OS X" – aw-think Jun 22 '15 at 18:35
  • I'll look further into signing. Thanks for all your help – corpico Jun 22 '15 at 18:44
  • I figured it out. The JRE included by Netbeans isn't complete for some reason. I laid it out in a new question to keep things neat and linked to your answer. – corpico Jun 23 '15 at 03:02
  • Ah, that's maybe about setting JAVA_HOME variable (even on Mac too) to point to the correct JDK path. – aw-think Jun 23 '15 at 03:11