9 votes

Est-ce que java.awt.Robot.waitForIdle() attend que les événements soient distribués ?

J'utilise java.awt.Robot pour les tests d'intégration de mon application Swing, mais j'ai du mal à exécuter mes actions dans le bon ordre. Comment puis-je dire au thread qui appelle robot.mousePressed(...) pour bloquer jusqu'à ce que Swing ait fini de distribuer cet événement ? Apparemment, robot.setAutoWaitForIdle(true) ne sert à rien.

Voici ma démo. Je m'attends à ce que le message "robot terminé !" arrive toujours après "Action terminée de bloquer.", mais au lieu de cela, il arrive souvent trop tôt.

import java.awt.AWTException;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.sql.Date;
import java.text.DateFormat;
import java.util.logging.ConsoleHandler;
import java.util.logging.Formatter;
import java.util.logging.LogManager;
import java.util.logging.LogRecord;
import java.util.logging.Logger;

import javax.swing.GroupLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;

public class RobotWaitForIdleDemo {
    /**
     * Create the device that contains the given point in screen coordinates.
     * Robot has to be constructed differently for each monitor.
     */
    public static GraphicsDevice getDevice(Point p) {
        GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
        GraphicsDevice[] gs = ge.getScreenDevices();

        // Search the devices for the one that draws the specified point.
        for (GraphicsDevice device : gs) {
            GraphicsConfiguration configuration = device.getDefaultConfiguration();
            Rectangle bounds = configuration.getBounds();
            if (bounds.contains(p)) {
                return device;
            }
        }
        return null;
    }
    public static final Logger logger = Logger.getLogger(RobotWaitForIdleDemo.class.getName());
    public static void main(String[] args) {
        LogManager.getLogManager().reset();
        Formatter formatter = new Formatter() {
            @Override
            public String format(LogRecord arg0) {
                Date date = new Date(arg0.getMillis());
                DateFormat.getTimeInstance().format(date);
                return String.format("%s %s %s %s%n",
                        DateFormat.getTimeInstance().format(date),
                        arg0.getLoggerName(),
                        arg0.getLevel(),
                        arg0.getMessage());
            }
        };
        ConsoleHandler consoleHandler = new ConsoleHandler();
        consoleHandler.setFormatter(formatter);
        logger.addHandler(consoleHandler);

        final JFrame jframe = new JFrame("Robot experiment");
        GroupLayout groupLayout = new GroupLayout(jframe.getContentPane());

        final JButton jbutton = new JButton("Click me!");
        jbutton.addActionListener(new ActionListener() {
            @Override public void actionPerformed(ActionEvent e) {
                // Simulate a heavy Swing event handler.
                logger.info("(swing thread) Action starting to block...");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e1) {}
                logger.info("(swing thread) Action finished blocking.");
            }
        });

        JButton tryAgainBUtton = new JButton("Automatically click above button.");
        tryAgainBUtton.addActionListener(new ActionListener() {
            @Override public void actionPerformed(ActionEvent e) {
                new Thread(new Runnable() {
                    @Override public void run() {
                        try {
                            Point point = new Point(jbutton.getWidth()/2,jbutton.getHeight()/2);
                            SwingUtilities.convertPointToScreen(point, jbutton);
                            GraphicsDevice device = getDevice(point);
                            Point offset = device.getDefaultConfiguration().getBounds().getLocation();

                            Robot robot = new Robot(device);
                            robot.setAutoWaitForIdle(true);
                            robot.setAutoDelay(30);

                            robot.mouseMove(point.x - offset.x, point.y - offset.y);
                            String threadName = Thread.currentThread().getName();
                            logger.info(String.format("(%s) robot.mousePress(%d)", threadName, InputEvent.BUTTON1_MASK));
                            robot.mousePress(InputEvent.BUTTON1_MASK);
                            logger.info(String.format("(%s) robot.mouseRelease(%d)", threadName, InputEvent.BUTTON1_MASK));
                            robot.mouseRelease(InputEvent.BUTTON1_MASK);
                            logger.info(String.format("(%s) robot finished!", threadName, InputEvent.BUTTON1_MASK));
                        } catch (AWTException ex) {
                            ex.printStackTrace();
                        }
                    }
                }, "robot thread").start();
            }
        });

        jframe.getContentPane().setLayout(groupLayout);
        groupLayout.setAutoCreateGaps(true);
        groupLayout.setAutoCreateContainerGaps(true);
        groupLayout.setVerticalGroup(
                groupLayout.createSequentialGroup()
                    .addComponent(jbutton)
                    .addComponent(tryAgainBUtton));
        groupLayout.setHorizontalGroup(
                groupLayout.createParallelGroup()
                    .addComponent(jbutton)
                    .addComponent(tryAgainBUtton)                           );

        jframe.setSize(300, 300);
        jframe.setVisible(true);
        jframe.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
    }
}

J'utilise Java 1.6 sur Ubuntu.

6voto

mKorbel Points 90340

Peut-être que celui-ci peut vous aider, avis non testé en Java7

vous pouvez tester cela dans chacune des étapes pour isEventDispatchThread()

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import sun.awt.SunToolkit;

public class TestMenu {

    /**
     * Without a delay, SunToolkit may encounter a problem in SunToolkit (at
     * least in JDK 6, where the drop down size problem is not present).
     * 
     * Note: SunToolkit also has some mechanism to delay, but I forgot how it
     * worked.
     * 
     * <pre>
     * Exception in thread "main" sun.awt.SunToolkit$InfiniteLoop
     *         at sun.awt.SunToolkit.realSync(Unknown Source)
     *         at TestMenu.syncAndDelay(TestMenu.java:172)
     *         at TestMenu.click(TestMenu.java:88)
     *         at TestMenu.moveAndClickCenter(TestMenu.java:150)
     *         at TestMenu.main(TestMenu.java:45)
     * </pre>
     * 
     * As a bonus, the delay makes the scenario better visible for the human
     * eye.
     */
    private static int delay = 500;
    private static JMenu[] menus = new JMenu[5];
    private static Dimension[] parentSizes;
    private static Robot robot;
    private static SunToolkit toolkit;

    public static void main(String[] args) throws Exception {
        robot = new Robot();
        toolkit = (SunToolkit) Toolkit.getDefaultToolkit();
        parentSizes = new Dimension[menus.length];
        createGUI(); // Open the first menu. Then get the drop down size of all menu's
        moveAndClickCenter(menus[0]);
        for (int index = 0; index < menus.length; index++) {
            parentSizes[index] = getDropDownSize(index);
        }// Click the last item on the last menu.        
        Component item = menus[menus.length - 1].getMenuComponent(menus[menus.length - 1].getMenuComponentCount() - 1);
        moveAndClickCenter(item);
        // Open the last drop down again. Then get the drop down sizes once more. If size not equal to previous size, then it's a bug.
        boolean bug = false;
        moveAndClickCenter(menus[menus.length - 1]);
        for (int index = menus.length - 1; index >= 0; index--) {
            Dimension currentSize = getDropDownSize(index);
            System.out.print("old: " + parentSizes[index] + ", new: " + currentSize);
            if (!parentSizes[index].equals(currentSize)) {
                bug = true;
                System.out.println(" ERROR");
            } else {
                System.out.println();
            }
        }
        if (bug) {
            throw new RuntimeException("JMenu drop down size is changed for no reason.");
        }

    }

    private static Dimension getDropDownSize(int index) throws Exception {
        moveToCenter(menus[index]);
        return menus[index].getMenuComponent(0).getParent().getSize();
    }

    private static void click() throws Exception {
        robot.mousePress(InputEvent.BUTTON1_MASK);
        robot.mouseRelease(InputEvent.BUTTON1_MASK);
        syncAndDelay();
    }

    private static void createGUI() throws Exception {

        SwingUtilities.invokeAndWait(new Runnable() {

            @Override
            public void run() {
                UIManager.LookAndFeelInfo[] infos = UIManager.getInstalledLookAndFeels();// The L&F defines the drop down policy.
                for (final UIManager.LookAndFeelInfo info : infos) {
                    if (info.getName().toLowerCase().indexOf("metal") >= 0) {
                        if (!UIManager.getLookAndFeel().getName().equals(info.getName())) {
                            try {
                                UIManager.setLookAndFeel(info.getClassName());
                                System.out.println("Attempt to set look and feel to " + info.getName());
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                        } else {
                            System.out.println("Metal look and feel is the default");
                        }
                        break;
                    }
                }
                System.out.println("Testing with " + UIManager.getLookAndFeel().getName());  // Setup the GUI.
                JFrame frame = new JFrame("A frame");
                frame.setJMenuBar(new JMenuBar());
                for (int menuIndex = 0; menuIndex < menus.length; menuIndex++) {
                    menus[menuIndex] = new JMenu("Menu " + menuIndex);
                    frame.getJMenuBar().add(menus[menuIndex]);
                    for (int itemIndex = 0; itemIndex <= menus.length - menuIndex; itemIndex++) {
                        // It seems that the problem only occurs if the drop down is displayed outside the frame at the right
                        // (not sure though). A rather long item name.
                        JMenuItem item = new JMenuItem("Menu " + menuIndex + " item " + itemIndex);
                        menus[menuIndex].add(item);
                    }
                }
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
        syncAndDelay();
    }

    private static void moveAndClickCenter(Component c) throws Exception {
        moveToCenter(c);
        click();
    }

    private static void moveToCenter(final Component c) throws Exception {
        final Point cp = new Point();
        SwingUtilities.invokeAndWait(new Runnable() {

            @Override
            public void run() {
                Point p = new Point(c.getWidth() / 2, c.getHeight() / 2);
                SwingUtilities.convertPointToScreen(p, c);
                cp.setLocation(p);
            }
        });
        robot.mouseMove(cp.x, cp.y);
        syncAndDelay();
    }

    private static void syncAndDelay() throws Exception {
        if (delay > 0) {
            Thread.sleep(delay);
        }
        toolkit.realSync();
    }

    private TestMenu() {
    }
}

3voto

yonran Points 6952

La réponse de mKorbel ( SunToolkit.realSync() ) est correct, mais realSync est lent et lance SunToolkit.InfiniteLoop . J'ai fini par utiliser cette variante après avoir étudié realSync :

import java.awt.Toolkit;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;

import javax.swing.SwingUtilities;

import sun.awt.SunToolkit;

public class ToolkitUtils {
    private Method syncNativeQueue;
    private boolean isSyncNativeQueueZeroArguments;
    public ToolkitUtils() {
        syncNativeQueue = null;
        isSyncNativeQueueZeroArguments = true;
        try {
            // Since it's a protected method, we have to iterate over declared
            // methods and setAccessible.
            Method[] methods = SunToolkit.class.getDeclaredMethods();
            for (Method method: methods) {
                String name = method.getName();
                if ("syncNativeQueue".equals(name)) {
                    List<Class<?>> parameterTypes = Arrays.asList(method.getParameterTypes());
                    if (Arrays.<Class<?>>asList(long.class).equals(parameterTypes)) {
                        isSyncNativeQueueZeroArguments = false;
                    } else if (parameterTypes.isEmpty() && null == syncNativeQueue) {
                        isSyncNativeQueueZeroArguments = true;
                    } else {
                        continue;
                    }
                    syncNativeQueue = method;
                    syncNativeQueue.setAccessible(true);
                }
            }
        } catch (SecurityException e) {
            throw new RuntimeException(e);
        }
        if (syncNativeQueue == null)
            throw new IllegalStateException("Could not find method SunToolkit.syncNativeQueue.");
    }

    /**
     * Block until Swing has dispatched events caused by the Robot or user.
     * 
     * <p>
     * It is based on {@link SunToolkit#realSync()}. Use that method if you want
     * to try to wait for everything to settle down (e.g. if an event listener
     * calls {@link java.awt.Component#requestFocus()},
     * {@link SwingUtilities#invokeLater(Runnable)}, or
     * {@link javax.swing.Timer}, realSync will block until all of those are
     * done, or throw exception after trying). The disadvantage of realSync is
     * that it throws {@link SunToolkit.InfiniteLoop} when the queues don't
     * become idle after 20 tries.
     * 
     * <p>
     * Use this method if you only want to wait until the direct event listeners
     * have been called. For example, if you need to simulate a user click
     * followed by a stream input, then you can ensure that they will reach the
     * program under test in the right order:
     * 
     * <pre>
     * robot.mousePress(InputEvent.BUTTON1);
     * toolkitUtils.flushInputEvents(10000);
     * writer.write("done with press");
     * </pre>
     * 
     * @see {@link java.awt.Robot#waitForIdle()} is no good; does not wait for
     *      OS input events to get to the Java process.
     * @see {@link SunToolkit#realSync()} tries 20 times to wait for queues to
     *      settle and then throws exception. In contrast, flushInputEvents does
     *      not wait for queues to settle, just to flush what's already on them
     *      once.
     * @see {@link java.awt.Toolkit#sync()} flushes graphics pipeline but not
     *      input events.
     * 
     * @param syncNativeQueueTimeout
     *            timeout to use for syncNativeQueue. Something like 10000 is
     *            reasonable.
     */
    public void flushInputEvents(long syncNativeQueueTimeout) {
        SunToolkit toolkit = (SunToolkit) Toolkit.getDefaultToolkit();

        // 1) SunToolkit.syncNativeQueue: block until the operating system
        // delivers Robot or user events to the process.
        try {
            if (isSyncNativeQueueZeroArguments) {
                // java 1.6
                syncNativeQueue.invoke(toolkit);
            } else {
                // java 1.7
                syncNativeQueue.invoke(toolkit, syncNativeQueueTimeout);
            }
        } catch (IllegalArgumentException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        } catch (InvocationTargetException e) {
            throw new RuntimeException(e);
        }

        // 2) SunToolkit.flushPendingEvents: block until the Toolkit thread
        // (aka AWT-XAWT, AWT-AppKit, or AWT-Windows) delivers enqueued events
        // to the EventQueue
        SunToolkit.flushPendingEvents();

        // 3) SwingUtilities.invokeAndWait: block until the Swing thread (aka
        // AWT-EventQueue-0) has dispatched all the enqueued input events.
        try {
            SwingUtilities.invokeAndWait(new Runnable(){
                @Override public void run() {}});
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } catch (InvocationTargetException e) {
            throw new RuntimeException(e);
        }
    }
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X