Search code examples
javaswingawtawtrobotsystem-tray

Java Robot launched from Windows System Tray


I'd like my program to perform a screen capture from a System Tray menu.

I got it to work, but the MenuItem itself does not hide fast enough and is captured along with the rest of the screen.

Here's a MCVE, based on a stripped down version of The Java Tutorial's TrayIconDemo sample: (Edit: made even more minimal thanks to Holger's comment)

import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.IOException;
import java.net.URL;

public class TrayCapture {
    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                try {
                    createAndShowGUI();
                }
                catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    private static void createAndShowGUI() throws Exception {
        //Check the SystemTray support
        if (!SystemTray.isSupported()) {
            System.err.println("SystemTray is not supported");
            return;
        }
        final PopupMenu popup = new PopupMenu();
        final TrayIcon trayIcon = new TrayIcon(ImageIO.read(new URL("https://i.sstatic.net/wCF8S.png")));
        trayIcon.setImageAutoSize(true);

        // Create a popup menu components
        MenuItem captureItem = new MenuItem("Capture");
        MenuItem exitItem = new MenuItem("Exit");

        //Add components to popup menu
        popup.add(captureItem);
        popup.add(exitItem);

        trayIcon.setPopupMenu(popup);

        SystemTray.getSystemTray().add(trayIcon);

        captureItem.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                try {
                    File outputFile = new File("capture.png");
                    ImageIO.write(new Robot().createScreenCapture(
                            new Rectangle(Toolkit.getDefaultToolkit().getScreenSize())),
                            "png",
                            outputFile);
                    JOptionPane.showMessageDialog(null, "Screenshot saved to: " + outputFile.getAbsolutePath());
                }
                catch (AWTException | IOException ex) {
                    ex.printStackTrace();
                }
            }
        });

        exitItem.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                SystemTray.getSystemTray().remove(trayIcon);
                System.exit(0);
            }
        });
    }
}

And here is a resulting capture.

Capture including the menu item

As you can see, the PopupMenu is closed but the "Capture" MenuItem was still visible when the Robot took the capture and is part of the captured image.

Is there a way to force a repaint to hide the MenuItem or get a notification when it's hidden so I can make sure the Robot will not pick it up ? I'd rather avoid an arbitrary Thread.sleep() because it's just a blind race condition...

KR, Vicne


Solution

  • This is not an artifact of your Java application, as it is a Windows-specific animation of the selected menu item that continues while the popup menu is already closed.

    As far as I know, there is no way to monitor the animation progress from the Java side (apart from capturing the screen and looking whether the artifact is still there).

    The best option is to have a configurable delay (with a reasonable default of, e.g. 300 ms) before doing the screenshots. All screenshot tools I know of, use a configurable delay anyway.