Get-Set window position in Java Swing

I frequently need to save the window position and its size during application shutdown, in order to restore them during the application startup. That is the usual procedure for most of the applications to have them opened right where you last time closed them.

Java Swing has a standard API for reading and setting window (frame) position. Simply, you type JFrame.getLocation(), or JFrame.getSize(), and the corresponding: JFrame.setLocation(...), and JFrame.setSize(...).

This code works correctly on a single monitor configuration. However, if you have multiple monitors with different scaling, then the whole API breaks down and you get the inconsistent results. Restoring the saved position and size ends up with the frame on wrong location and with a wrong size.

How can we fix that problem? First of all, we need to create frames (or dialogs) a bit different than the usual way:

public static void main(String[] args) {
    // Read the display ID from the saved file. It is something like this: "\\Display0"
    String displayId = ini.getString("Main", "display", "\\Display0");
    GraphicsConfiguration conf = getGraphicsConfiguration(displayId);
    new TestFrame(conf);
}

public static GraphicsConfiguration getGraphicsConfiguration(String displayId) {
    GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
    GraphicsDevice[] gs = ge.getScreenDevices();
    for (int j = 0; j < gs.length; j++) {
        GraphicsDevice gd = gs[j];
        if (gd.getIDstring().equals(displayId)) {
            return gd.getDefaultConfiguration();
        }
    }
    System.out.println("##### BLAST #### Could not find graphics configuration " + displayId);
    return ge.getDefaultScreenDevice().getDefaultConfiguration();
}

As we can see, finding the graphics configuration for the given display ID is done by enumerating through the list of all graphics devices (monitors) and by comparing their device ID with the given one.

We need to make the constructor of our frame to have one important argument - the GraphicsConfiguration instance. We obtain that instance using the saved display ID. In this example I save all the data into the INI file, but it can be any type of repository, as long as it can save integers and strings. Next, we make the frame constructor with that special GraphicsConfiguration argument:

public TestFrame(GraphicsConfiguration conf) {
    super(conf);
    setTitle("Test");
    addWindowListener(new WindowAdapter() {
        @Override
        public void windowClosing(WindowEvent e) {
            // save the location and size into the INI file
            TestFrame.saveFrame(TestFrame.this, "Main", ini);
            ini.saveINI();
        }
    });
    // Restore the location from the INI file.
    TestFrame.setBounds(ini.getInt("Main", "x", 1024), ini.getInt("Main", "y", 100),
            ini.getInt("Main", "width", 400), ini.getInt("Main", "height", 700), this, ini.getString("Main", "display", "\\Display0"));
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    setVisible(true);
}

OK, now we need to look at those two important methods: saveFrame and setBounds. The former saves the location and the size of the given frame into the INI file, while the latter restores the frame location and size from the INI file. Let's look at the saveFrame method:

public static void saveFrame(Window frame, String section, IniFile ini) {
    Rectangle r = getBounds(frame);
    ini.setInt(section, "width", r.width);
    ini.setInt(section, "height", r.height);
    ini.setInt(section, "x", r.x);
    ini.setInt(section, "y", r.y);
    ini.setString(section, "display", getDisplayId(frame));
}

public static String getDisplayId(Window frame) {
    return frame.getGraphicsConfiguration().getDevice().getIDstring();
}

The actual location and size is obtained from the getBounds method, so let's look at it:

public static Rectangle getBounds(Window frame) {
    GraphicsConfiguration conf = frame.getGraphicsConfiguration();
    Rectangle r = conf.getBounds();
    double _scale = getWindowScale(frame);
    r.x = (int) (frame.getX() * _scale);
    r.y = (int) (frame.getY() * _scale);
    r.width = (int) (frame.getWidth() * _scale);
    r.height = (int) (frame.getHeight() * _scale);
    return r;
}

First of all, we need to read the location and size as given by the usual Swing API (the conf.getBounds() method). Since it doesn't care about the scaling, we need to obtain the scaling, and we do it by calling the getWindowScale() method:

public static double getWindowScale(Window window) {
    GraphicsDevice device = getWindowDevice(window);
    return device.getDisplayMode().getWidth() / (double) device.getDefaultConfiguration().getBounds().width;
}

private static GraphicsDevice getWindowDevice(Window window) {
    String displayId = window.getGraphicsConfiguration().getDevice().getIDstring();
    GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
    GraphicsDevice[] gs = ge.getScreenDevices();
    for (int j = 0; j < gs.length; j++) {
        GraphicsDevice gd = gs[j];
        if (gd.getIDstring().equals(displayId)) {
            return gd;
        }
    }
    System.out.println("##### BLAST #### Could not find graphics configuration " + displayId);
    return ge.getDefaultScreenDevice();
}

And that's it. We obtain the location and size, and then we multiply them by the scaling. Then we save those results and the display ID into the INI file.

Restoring the frame location and size

Restoring the location and size is a bit more complex compared to saving them. It starts with the location and size obtained from the INI file, but then we need to do some additional calculation:

public static void setBounds(int x, int y, int width, int height, Window frame, String displayId) {
    GraphicsConfiguration conf = frame.getGraphicsConfiguration();
    Rectangle r = conf.getBounds();
    Rectangle r2 = TestFrame.getBounds(frame);

    double scale = TestFrame.getMainWindowScale();
    double scaleCurrent = TestFrame.getWindowScale(frame);

    if (scale == 1 && scaleCurrent > 1) {
        scale = scaleCurrent;
    }

    if (GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices().length > 1) {
        if (outside(x, width, (int)(r.x * scale), (int)(r.width*scale))) {
                // if x coordinate of the frame goes beyond its own display
                // we will reset it
            x = r2.x + 10;
        }
        if (outside(y, height, (int)(r.y * scale), (int)(r.height*scale))) {
                // if y coordinate of the frame goes beyond its own display
                // we will reset it
            y = r2.y + 10;
        }
        frame.setBounds((int)(x/scale), (int)(y/scale), (int)(width/scale), (int)(height/scale));
    } else {
        if (outside((int)(x/scale), width, (int)(r.x ), (int)(r.width))) {
                // if x coordinate of the frame goes beyond its own display
                // we will reset it
            x = r2.x + 10;
        }
        if (outside((int)(y/scale), height, (int)(r.y ), (int)(r.height))) {
                // if y coordinate of the frame goes beyond its own display
                // we will reset it
            y = r2.y + 10;
        }
        frame.setBounds((int) (x / scale), (int) (y / scale),
            (int) (width / scale), (int) (height / scale));
    }
}

First of all, we see that we obtain location and size of the display (conf.getBounds()) and of the scaled frame (TestFrame.getBounds(frame)). Then comes the first complication - we need to obtain the scaling of the main monitor which has its own coordinate on the (0, 0) position. It is done in the TestFrame.getMainWindowScale() method:

public static double getMainWindowScale() {
    GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
    GraphicsDevice[] gds = ge.getScreenDevices();
    for (int j = 0; j < gds.length; j++) {
        GraphicsDevice gd = gds[j];
        Rectangle r = gd.getDefaultConfiguration().getBounds();
        if (r.x == 0) {
            // we have found the main window
            return gd.getDisplayMode().getWidth() / (double) gd.getDefaultConfiguration().getBounds().width;
        }
    }
    return 1;
}

Then we need to divide the saved location and size by the scaling of the main window (the one with its own coordinates of (0, 0)). However, before that, here comes the ugly hack:

if (scale == 1 && scaleCurrent > 1) {
        scale = scaleCurrent;
}

This means that if the main window scaling is 1 and the saved window scaling is greater than 1, then we need to set the scaling to the saved window scaling instead of the main window scaling.

Then comes the second ugly hack - scaling is handled differently if we have a single monitor compared to multiple monitors. We check that using the following code: GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices().length > 1.

The last fix deals with the bad calculation which could set the restored position outside of the visible area of the monitor. To check if there is some bad calculation, we call the following code:

private static boolean outside(int x, int w, int rx, int rw) {
    if (x < rx ||  x > (rx + rw))
        return true;
    return false;
}

Conclusion

It took me surprisingly lot of time and effort to do the restoration of the frame location and size. I haven't found the complete solution for this problem on the net, and this post is a kind of compilation of efforts of many people (including myself). I have implemented all of this in my WindowUtil class here: https://github.com/milanvidakovic/FPGAEmulator32/blob/master/src/emulator/util/WindowUtil.java

Comments

Comments powered by Disqus