lördag 27 april 2013

JavaFX - Creating a Windows 7 screen saver using JavaFX

Is it possible to use JavaFX to create a screen saver which shows a specific web page in Windows 7? Yes it actually is, but there are some bumps on the road. In this post we will look at the various steps we need to perform to actually create a full functional Windows screen saver.

Before we dig into the code we first we need to understand what a screen saver is in Microsofts world. According to this support page from Microsoft a screen saver is a Windows 32bit application with the file-ending .scr, which is started using different command line arguments. The screen saver should be able to show a configuration dialog when given a empty argument, or the '/c' argument. Likewise should it be possible to get a preview using the '/p' argument, and the screen saver should be shown using the '/s' command. Here will we have some problem if we like to solve the preview function since it is supposed to be shown in a given window using a window handler, which we can not use from JavaFX. However the screen saver can be made without the support for the preview.

So we need to create a application which by command line supports the '/c' (configuration), and the '/s' (screen saver) commands, the application needs to store the user settings, when the application runs in screen saver mode it should be terminated upon input and be in full screen, create a executable jar, and finally we need to package the executable jar as a windows 32 application. Some things to do so let us get going.

We start by focusing on the application, later on we package the application, and last we create a Windows batch script which we convert to a executable Windows 32bit application.

A screen saver implemented using JavaFX
For the screen saver I would like to show a web page, so using the WebView in full screen will suit us well. The configuration dialog will only have a simple URL field and a save button. The settings will be saved in a property file in the users home folder.

I have chosen to implement the application using FXML. One could argue if FXML is needed for the actual screen saver, since it will only be one WebView. Let us start by looking into the code which will convert the command line arguments into a understandable "mode" for the application.
@Override
public void init() throws Exception {
 List<string> rawParameters = getParameters().getRaw();
 if (rawParameters == null || rawParameters.isEmpty()) {  
  mode = Mode.SETTINGS;
 } else if(!rawParameters.isEmpty()) { 
  switch (rawParameters.get(0).trim().substring(0, 2)) { 
   case "/c":
    mode = Mode.SETTINGS;
    break;
   case "/s":
    mode = Mode.SCREEN_SAVER;       
    break;
   default:
    mode = Mode.NA;
    break;
  }
 }
}
Line 1-2: By overriding the init()-method you can perform tasks which will be executed before the start()-method in a JavaFX application.
Line 3: By calling getParamters() we can get the parameters submitted to the application.
Line 4: If we call the application without any parameters we should start the configuration dialog.
Line 7: We only use the first characters of the parameter since Windows will give us a string like '/c:12345' when we call configuration from the screen saver dialog.
Line 8: '/c' for settings so lets get into the settings mode.
Line 11: '/s' for the screen saver mode.

The code for the parameter parsing and start up of the application is quite straight forward and the mode is later used in the start()-method for a more readable way of starting the different parts of the application.
@Override
public void start(Stage stage) throws Exception {
 ResourceBundle resourceBundle = ResourceBundle.getBundle(RESOURCE_TEXTS);
 switch (mode) {
  case SCREEN_SAVER:
   stage.initStyle(StageStyle.UNDECORATED);
   stage.setResizable(false);
   stage.setIconified(false);
   stage.setFullScreen(true);
   stage.setScene(new Scene(FXMLLoader.<Parent>load(
     getClass().getResource("/fxml/screensaver.fxml"), resourceBundle)));
   
   // Close the screen saver when any key is being pressed.
   stage.getScene().setOnKeyTyped(new EventHandler<KeyEvent>() {
    @Override
    public void handle(KeyEvent event) {
     event.consume();
     Platform.exit();
    }
   });
   
   // Close the screen saver when the mouse is moved.
   stage.getScene().setOnMouseMoved(new EventHandler<MouseEvent>() {
    private long firstMouseMove = -1;
    
    @Override
    public void handle(MouseEvent event) {
     if (firstMouseMove != -1 
                                && firstMouseMove + 1000 < System.currentTimeMillis()) {
      event.consume();
      Platform.exit();
     } else {
      firstMouseMove = System.currentTimeMillis();
     }
    }
   });
   stage.show();
   break;
  case SETTINGS:
   stage.setTitle(resourceBundle.getString("title"));
   stage.initStyle(StageStyle.UTILITY);
   stage.setResizable(false);
   stage.setIconified(false);
   stage.setScene(new Scene(FXMLLoader.<Parent>load(
     getClass().getResource("/fxml/configuration.fxml"), resourceBundle)));
   stage.show();
   break;
  default:
   System.err.println("Unkown command.");
   Platform.exit();
   break;
 }
}
Line 3: Here we load the internationalized strings used in the application.
Line 5: Time to show the screen saver.
Line 6-9: Define the stage to be a un-resizable full screen window without a icon.
Line 10-11: Load the FXML for the scene.
Line 14-20: When the screen saver is active we like to be able to hide it by typing a key like a standard screen saver. On a KeyEvent we colse the screen saver sicne it has full fitted its purpose.
Line 23-36: Likewise with the MouseEvents we like to terminate the screen saver when we move the mouse. However we need to do a lite trick since when the screen saver is started some "old" mouse events are propagated into the program. Instead of terminate the screen saver on the first mouse event we wait a second before we start to listen for events. This will not be a problem when the screen saver is activated due to inactivity, however when you "test" the screen saver it would be nice to actually see it.

The controllers are simply FXML controllers, but the screen saver controller has a little method for hiding the scroll-bar in the WebView.
webView.getChildrenUnmodifiable().addListener(new ListChangeListener<Node>() {
 @Override
 public void onChanged(javafx.collections.ListChangeListener.Change<? extends Node> change) {
  for (Node scrollBar : webView.lookupAll(".scroll-bar")) {
   scrollBar.setVisible(false);
  }
 }
});
Line 1: When the content is change in the WebView we add a listener to keep track of the changes.
Line 4-6: Simply find all scroll-bars and hide them.

That is the application. The saving of the user settings is done by the standard Java Properties which are serialized to a XML-file. If you are interested in that part you find the source code in the end of this post. Look in the class Configuration.

Packaging the application and create a Windows 32bit application
Next step in the processes will be to create a executable jar-file. I will be using the javafx-maven-plugin to create a executable jar. All you need to do is to configure you main class and the run mvn jfx:jar. This will create a jar which will be executed when you run it. See the pom.xml in the submitted source code.

So how can we make a Windows 32bit application out of our Java application? There are actually some solutions. We could build a executable binary using the build tools in the JDK, but I think for this task a simple batch-script which we compile into a executable exe-file suits us better. For this we will use a little tool called "Bat To Exe Converter" which you find here: http://www.softpedia.com/get/System/File-Management/Batch-To-Exe-Converter.shtml. The following batch-script will be used to create a exe-file.
@echo off
IF NOT "%1" == "/p" IF NOT "%1" == "/a" (
  %JAVA_HOME%\bin\java -jar fxwss-0.0.1-SNAPSHOT-jfx.jar %1
)
Line 2: Since we can not support the preview we can skip those commands. We could have let the application handle this for us, but starting the application takes some time and the user experience will be much better if we handle it smooth and nice instead.
Line 3: Here we start the application and passes the arguments. Note: you have to use the whole path to "java", for some reason java is not found otherwise and you will not get any errors. Just a not working screen saver.

Next open the "Bat To Exe Converter" and select you bat-file as input. What this program will do is to create a exe-file for you which we later on just change the name to  a .scr-file. In the "Batch-To-Exe-Converter" I selected to create a "Invisible application" since I do not like to see the command-window when starting the screen saver or the configuration window.

"Bat To Exe Converter" in action.
Take the created exe-file and change the name of it to .scr. Place this file together with the jar in the windows/system32 folder. When you now open the screen saver settings you will see the "fx-web-screen-saver" in the drop down. That is all, now enjoy your JavaFX screen saver.

In this post we have seen how we can create our own screen saver which actually execute a JavaFX application. There are some steps involved, but when one understand the concept of the screen savers in Windows it is quite straight forward. One things which is a bit frustrated is that you can not disable the text which is shown when you show a stage in full-screen, at least it would have been nice if one could style the message. Also some error messages when running a faulty screen saver in Windows would have been nice.

You can find the source code and binaries here: http://files.loop81.com/rest/download/e972a38a-7954-4d43-8dc7-29bad09cb7ba.

2 kommentarer:

  1. Thanks for a good article. I have found several interesting points. Unfortunately this method will not work for multiple monitors. The application went to Full screen mode only on a primary monitor. Other monitors show the desktop. Do you now solution for this issue?

    SvaraRadera
    Svar
    1. Tnx for the feedback! Take a look into the Screen class, http://docs.oracle.com/javafx/2/api/javafx/stage/Screen.html. There you can find information about all connect screens and get the dimensions of them. Then setup a stage for each screen and define the stage width and position using the data in the visual bounds of the screen. Remember to set the primary screen to "full screen". Good luck!

      Radera