JavaFX provides a powerful platform for creating rich graphical user interfaces (GUIs) in Java applications. However, managing multiple displays and screens within a JavaFX application can be a complex task. To simplify this process and provide a flexible solution, we’ve developed the DisplayManager framework. In this article, we’ll explore how to use DisplayManager to efficiently manage displays in JavaFX applications and suggest potential use cases for this powerful tool.

Introduction to DisplayManager

DisplayManager is a Java class designed to streamline the management of JavaFX displays within an application. It provides functionalities for displaying screens on different monitors, moving screens between displays, and handling the application lifecycle seamlessly.

Understanding DisplayScreen

Before diving into DisplayManager, let’s briefly discuss the DisplayScreen abstract class. DisplayScreen serves as a template for creating individual screens to be displayed within the application. It defines properties such as display name, fullscreen mode, and always-on-top behavior. Subclasses of DisplayScreen can override methods to customize screen behavior and content.

Exploring the Source Code

Let’s take a closer look at the source code of DisplayManager and DisplayScreen:

DisplayScreen.java

package name.mymiller.javafx.display;

import javafx.application.Application;
import javafx.collections.ObservableList;
import javafx.geometry.Rectangle2D;
import javafx.stage.Screen;
import javafx.stage.Stage;

import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Abstract class to aid in the creation of Displays on monitors
 *
 * @author jmiller
 */
public abstract class DisplayScreen extends Application implements Runnable {
    private String displayName;

    /**
     * Zero index display of the screens
     */
    private int display = 0;

    /**
     * Maximize the Display Window
     */
    private boolean maximized = false;

    /**
     * This instance of the displayStage.
     */
    private Stage displayStage = null;

    /**
     * This instance is always on top
     */
    private boolean onTop = false;

    /**
     * Full Screen for the Display Window
     */
    private boolean fullScreen = false;

    /**
     * Sets the display name;
     */
    public DisplayScreen(String displayName) {
        super();
        this.displayName = displayName;
    }

    /**
     * @return the display
     */
    public int getDisplay() {
        return this.display;
    }

    /**
     * @param display the display to set
     */
    public void setDisplay(int display) {
        this.display = display;

        if (this.displayStage != null) {
            final ObservableList<Screen> screens = Screen.getScreens();
            final Rectangle2D bounds = screens.get(this.getDisplay()).getVisualBounds();

            this.displayStage.setX(bounds.getMinX());
            this.displayStage.setY(bounds.getMinY());
            this.displayStage.setWidth(bounds.getWidth());
            this.displayStage.setHeight(bounds.getHeight());
        }
    }

    /**
     * @return the displayName
     */
    public synchronized String getDisplayName() {
        return this.displayName;
    }

    /**
     * @param displayName the displayName to set
     */
    protected synchronized void setDisplayName(String displayName) {
        this.displayName = displayName;
    }

    /**
     * Hide the given DisplayScreen
     */
    public void hide() {
        this.displayStage.hide();
    }

    /**
     * @return True if the DisplayScreen is FullScreen
     */
    public boolean isFullScreen() {
        return this.fullScreen;
    }

    /**
     * Set if the DisplayScreen is Full Screen
     *
     * @param fullScreen True if it should be Full Screen
     */
    public void setFullScreen(boolean fullScreen) {
        this.fullScreen = fullScreen;

        if (this.displayStage != null) {
            this.displayStage.setFullScreen(fullScreen);
        }
    }

    /**
     * Set if the DisplayScreen is Always on Top
     * @param onTop True if always on top
     */
    public void setOnTop(boolean onTop) {
        this.onTop = onTop;

        if(this.displayStage != null) {
            this.displayStage.setAlwaysOnTop(onTop);
        }
    }

    /**
     *
     * @return Boolean indicating if always on top
     */
    public boolean isOnTop() {
        return this.onTop;
    }

    /**
     * @return the maximized
     */
    public boolean isMaximized() {
        return this.maximized;
    }

    /**
     * @param maximized the maximized to set
     */
    public void setMaximized(boolean maximized) {
        this.maximized = maximized;

        if (this.displayStage != null) {
            this.displayStage.setMaximized(this.maximized);
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see java.lang.Runnable#run()
     */
    @Override
    public void run() {
        if (this.displayStage == null) {
            this.displayStage = new Stage();
            try {
                Logger.getLogger(DisplayScreen.class.getName()).info("Creating Display Stage");
                this.start(this.displayStage);
            } catch (final Exception e) {
                Logger.getLogger(DisplayScreen.class.getName()).log(Level.SEVERE, "Failed to open Display", e);
            }
        } else {
            this.show();
        }
    }

    /**
     * Show the DisplayScreen
     */
    public void show() {
        this.displayStage.show();
    }

    /*
     * (non-Javadoc)
     *
     * @see javafx.application.Application#start(javafx.stage.Stage)
     */
    @Override
    public void start(Stage stage) throws Exception {
        stage.setMaximized(this.isMaximized());
        stage.setFullScreen(this.isFullScreen());
        stage.setAlwaysOnTop(this.onTop);

        final ObservableList<Screen> screens = Screen.getScreens();
        final Rectangle2D bounds = screens.get(this.getDisplay()).getVisualBounds();

        stage.setX(bounds.getMinX());
        stage.setY(bounds.getMinY());
        stage.setWidth(bounds.getWidth());
        stage.setHeight(bounds.getHeight());

        this.startDisplay(stage, bounds.getHeight(), bounds.getWidth());
    }

    /**
     * Method to construct the Stage. Stage will be set
     *
     * @param stage  Stage to use in your Display
     * @param height the Height of this display
     * @param width  the Width of this display.
     */
    abstract protected void startDisplay(Stage stage, double height, double width);

    /*
     * (non-Javadoc)
     *
     * @see javafx.application.Application#stop()
     */
    @Override
    public void stop() throws Exception {
        this.displayStage.close();
        super.stop();
    }

}

DisplayManager.java

package name.mymiller.javafx.display;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.collections.ObservableList;
import javafx.stage.Screen;
import name.mymiller.lang.singleton.Singleton;
import name.mymiller.lang.singleton.SingletonInterface;
import name.mymiller.task.AbstractService;
import name.mymiller.task.TaskManager;

import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Manages the the JavaFX Displays.
 *
 * @author jmiller
 */
@Singleton
public class DisplayManager extends AbstractService implements SingletonInterface<DisplayManager> {
    /**
     * Global Instance
     */
    static private DisplayManager globalInstance = null;
    /**
     * Indicates if the system has be initialized.
     */
    private boolean inited = false;
    /**
     * Holds the Diplays for each screen
     */
    private DisplayScreen[] displayScreens = null;

    /**
     *
     */
    public DisplayManager() {
        super();

    }

    /**
     * @return Global Instance
     */
    public static DisplayManager getInstance() {
        if (DisplayManager.globalInstance == null) {
            DisplayManager.globalInstance = new DisplayManager();
        }

        return DisplayManager.globalInstance;
    }

    /**
     * @return total number of displays available on this system.
     */
    public int availableDisplays() {
        this.waitOnInited();

        int i = 0;
        for (final DisplayScreen screen : this.displayScreens) {
            if (screen == null) {
                i++;
            }
        }

        return i;
    }

    /**
     * Clear the display on given screen
     *
     * @param screen Screen number to clear
     * @return the displaced DisplaceScreen
     */
    public DisplayScreen clearDisplay(int screen) {
        final DisplayScreen displaced = this.getDisplay(screen);

        Platform.runLater(displaced::hide);
        this.displayScreens[screen] = null;

        return displaced;
    }

    /**
     * Display this Screen on the system.
     *
     * @param display DisplayScreen to show
     * @throws NoDisplayAvailableException No Open Displays available
     */
    public void display(DisplayScreen display) throws NoDisplayAvailableException {
        this.waitOnInited();

        if (this.availableDisplays() == 0) {
            throw new NoDisplayAvailableException(this.displayScreens.length);
        }

        for (int i = 0; i < this.displayScreens.length; i++) {
            if (this.displayScreens[i] == null) {
                this.display(display, i);
                break;
            }
        }
    }

    /**
     * Display this Screen on the system.
     *
     * @param display DisplayScreen to show
     * @param screen  Screen number to display on
     */
    public void display(DisplayScreen display, int screen) {
        Logger.getLogger(DisplayManager.class.getName()).info("Display: " + display + " Screen: " + screen);
        this.waitOnInited();
        this.displayScreens[screen] = display;
        if (display != null) {
            Logger.getLogger(DisplayManager.class.getName()).info("Running Display Later");
            this.displayScreens[screen].setDisplay(screen);
            Platform.runLater(display);
        }
    }

    /**
     * Get the DisplayScreen for the given display
     *
     * @param screen Screen Number to get the display on
     * @return Display Screen for the given display
     */
    public DisplayScreen getDisplay(int screen) {
        return this.displayScreens[screen];
    }

    /**
     *
     * @return a map showing screens and the named of displays
     */
    public Map<Integer,String> getDisplayList() {
        Map<Integer,String> map = new HashMap<>();
        ObservableList<Screen> screens = Screen.getScreens();

        for(int i = 0; i < screens.size(); i++ ) {
            if(this.displayScreens[i] != null) {
                map.put(i,this.displayScreens[i].getDisplayName());
            } else {
                map.put(i," EMPTY");
            }
        }

        return map;
    }

    /**
     * Initialize the Display Manager
     *
     * @param displayCount          Number of displays available
     */
    public void init(int displayCount) {
        this.displayScreens = new DisplayScreen[displayCount];

        this.inited = true;
    }

    /**
     * Move the display from one screen to another
     *
     * @param fromScreen Screen to move from
     * @param toScreen   Screen to move to
     * @return the displaced Screen
     */
    public DisplayScreen moveDisplay(int fromScreen, int toScreen) {
        final DisplayScreen displaced = this.getDisplay(toScreen);
        this.displayScreens[toScreen] = this.displayScreens[fromScreen];
        if (this.displayScreens[toScreen] != null) {
            this.displayScreens[fromScreen] = null;
            if (displaced != null) {
                displaced.hide();
            }
            this.displayScreens[toScreen].setDisplay(toScreen);
            return displaced;
        } else {
            System.err.println("No Screen to move");
        }
        return null;
    }

    @Override
    protected void service() {
        Application.launch(ProcessingApplication.class);
    }

    @Override
    public void start() {
        Logger.getLogger(DisplayManager.class.getName()).info("Starting Display Manager");
        this.setShutdown(false);
        TaskManager.getInstance().createService("Display Manager", DisplayManager.getInstance());
    }

    @Override
    protected void stop(int delay) {
        Platform.runLater(() -> {
            try {
                for (final DisplayScreen screen : this.displayScreens) {
                    if (screen != null) {
                        screen.stop();
                    }
                }
                Platform.exit();
            } catch (final Exception e) {
                Logger.getLogger(DisplayManager.class.getName()).log(Level.SEVERE, "Failed to stop Display Manager", e);
            }
        });
    }

    /**
     * Method to wait on Display Manager to be initilized.
     */
    private void waitOnInited() {
        while (!this.inited) {
            try {
                Thread.sleep(50);
            } catch (final InterruptedException e) {
            }
        }
    }
}

Key Features of DisplayManager

  1. Singleton Pattern: DisplayManager is implemented as a singleton, ensuring that only one instance exists throughout the application’s lifecycle. This simplifies access to display management functionalities from different parts of the codebase.
  2. Display Management: DisplayManager tracks available displays and handles the allocation of screens to specific monitors. It provides methods for displaying screens, clearing screens, moving screens between displays, and retrieving information about available displays.
  3. Initialization: DisplayManager initializes the display management system, allowing developers to specify the number of displays available in the system.
  4. Lifecycle Management: DisplayManager seamlessly integrates with the application lifecycle. It starts the display management system when the application starts and gracefully shuts it down when the application exits.

How to Use DisplayManager

Using DisplayManager in your JavaFX application is straightforward. Here’s a step-by-step guide:

  1. Create Display Screens: Extend the DisplayScreen abstract class to create custom screens for your application.
  2. Initialize DisplayManager: Call the init(displayCount) method of DisplayManager to initialize the display management system with the specified number of displays.
  3. Display Screens: Use the display(screen) or display(screen, screenNumber) methods to display screens on available monitors.
  4. Manage Screens: Utilize methods such as clearDisplay(screen), moveDisplay(fromScreen, toScreen), and getDisplayList() to manage screens dynamically during runtime.
  5. Integrate with Application Lifecycle: Override the start() and stop() methods of DisplayManager to start and stop the display management system during application startup and shutdown.

Potential Use Cases

DisplayManager offers a wide range of applications across various industries and domains. Here are some potential use cases:

  1. Digital Signage Systems: DisplayManager can be used to create sophisticated digital signage systems where content is displayed across multiple monitors in public spaces.
  2. Control Room Dashboards: DisplayManager can power control room dashboards for monitoring complex systems, allowing operators to visualize data on separate screens.
  3. Interactive Kiosks: Developers can use DisplayManager to build interactive kiosks with touch-screen interfaces, where different screens provide information or services to users.
  4. Multi-User Environments: In environments with multiple users accessing the same application simultaneously, DisplayManager can help allocate screens to different users based on their preferences or permissions.

DisplayManager simplifies the management of JavaFX displays, offering a flexible and efficient solution for handling multiple screens within an application. By providing a centralized framework for display management, developers can focus on building rich and engaging user interfaces without worrying about the complexities of screen allocation and management. Whether you’re developing digital signage solutions, control room dashboards, interactive kiosks, or multi-user applications, DisplayManager can streamline your development process and enhance the user experience. Start using DisplayManager in your JavaFX projects today and unlock the full potential of multi-display applications.

By Jeffery Miller

I am known for being able to quickly decipher difficult problems to assist development teams in producing a solution. I have been called upon to be the Team Lead for multiple large-scale projects. I have a keen interest in learning new technologies, always ready for a new challenge.