JButton остается нажатым, когда фокус украден JOptionPane

Я новичок в Swing и у меня есть ситуация. Я разрабатываю приложение, которое визуализирует компоненты GUI динамически на основе ввода XML-файла (метаданных) . Теперь для большинства моих JTextFields установлен InputVerifier для целей проверки. Верификатор ввода выскакивает JOptionPane всякий раз, когда есть недопустимый ввод.

Теперь, если пользователь вводит недействительные данные и перемещается вперед и нажимает кнопку на панели, появляется диалоговое окно, и пользователь должен ответить на него. но после этого также кнопка не рисует, чтобы освободить состояние. Это все еще выглядело как нажато, но на самом деле это не так. Поскольку весь код довольно грязный, я помещаю сценарий проблемы в код ниже:-

Что я должен сделать, чтобы JButton выглядел несжатым? Буду признателен, если логика также объяснена.

Заранее спасибо.

package test;

import java.awt.BorderLayout;
import java.awt.Frame;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import javax.swing.InputVerifier;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JTextField;

public class VerifierTest extends JFrame {

    private static final long serialVersionUID = 1L;

    public VerifierTest() {
        JTextField tf;
        tf = new JTextField("TextField1");

        getContentPane().add(tf, BorderLayout.NORTH);
        tf.setInputVerifier(new PassVerifier());

        final JButton b = new JButton("Button");
        b.setVerifyInputWhenFocusTarget(true);
        getContentPane().add(b, BorderLayout.EAST);
        b.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                if (b.hasFocus())
                    System.out.println("Button clicked");
            }
        });

        addWindowListener(new MyWAdapter());
    }

    public static void main(String[] args) {
        Frame frame = new VerifierTest();
        frame.setSize(400, 200);
        frame.setVisible(true);
        //frame.pack();
    }

    class MyWAdapter extends WindowAdapter {

        public void windowClosing(WindowEvent event) {
            System.exit(0);
        }
    }

    class PassVerifier extends InputVerifier {

        public boolean verify(JComponent input) {
            JTextField tf = (JTextField) input;
            String pass = tf.getText();
            if (pass.equals("Manish"))
                return true;
            else {
                String message = "illegal value: " + tf.getText();
                JOptionPane.showMessageDialog(tf.getParent(), message,
                        "Illegal Value", JOptionPane.ERROR_MESSAGE);

                return false;
            }
        }
    }
}

5 ответов

Решение

Метод verify на самом деле не очень хорошее место для открытия JOptionPane.

Есть несколько подходов, которые вы могли бы рассмотреть, чтобы решить вашу проблему:

  1. Вы хотите, чтобы этот JOptionPane появлялся каждый раз, когда текстовое поле теряет фокус и ввод неправильный: используйте FocusListener на JTextField и действуйте в соответствии с соответствующими событиями
  2. Вы хотите, чтобы эта JOptionPane появлялась при каждом нажатии кнопок: используйте ActionListener, чтобы сделать это, если ввод неправильный.

Вот небольшой фрагмент последнего варианта:

import java.awt.BorderLayout;
import java.awt.Frame;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.InputVerifier;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JTextField;

public class VerifierTest extends JFrame {

    private static final long serialVersionUID = 1L;

    public VerifierTest() {
        final JTextField tf = new JTextField("TextField1");

        getContentPane().add(tf, BorderLayout.NORTH);
        tf.setInputVerifier(new PassVerifier());

        final JButton b = new JButton("Button");
        b.setVerifyInputWhenFocusTarget(true);
        getContentPane().add(b, BorderLayout.EAST);
        b.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                if (!tf.getInputVerifier().verify(tf)) {
                    JOptionPane.showMessageDialog(tf.getParent(), "illegal value: " + tf.getText(), "Illegal Value",
                            JOptionPane.ERROR_MESSAGE);
                }
                if (b.hasFocus()) {
                    System.out.println("Button clicked");
                }
            }
        });
        setDefaultCloseOperation(EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {
        Frame frame = new VerifierTest();
        frame.setSize(400, 200);
        frame.setVisible(true);
    }

    class PassVerifier extends InputVerifier {

        @Override
        public boolean verify(JComponent input) {
            final JTextField tf = (JTextField) input;
            String pass = tf.getText();
            return pass.equals("Manish");
        }
    }
}

Также рассмотрите возможность установки операции закрытия JFrame по умолчанию вместо добавления прослушивателя окна (но это хороший подход - использовать WindowListener, если вы хотите открыть диалоговое окно, спрашивающее пользователя, уверен ли он, что он хочет выйти из вашего приложения).

Во-первых: все реализации InputVerifier, которые открывают диалог в verify(), недопустимы. Они нарушили свой контракт, API док:

Этот метод не должен иметь побочных эффектов.

с "должен" действительно означает "не должен". Правильное место для побочных эффектов следует делать YieldFocus.

Второе: правильное перемещение побочного эффекта (показ диалогового окна сообщения) в shouldYieldFocus также не работает... из-за ошибки (ОНИ называют это запросом функции;-), это старше десяти лет и в топ-10 RFEs

Будучи хакерской ошибкой, mouseListener @ dareurdrem так же хорош, как и любой работоспособный хак:-)

Обновить

После того, как мы немного поигрались с различными вариантами взлома вокруг ошибки, вот еще один взлом - он такой же хрупкий, как и все взломы (и не выдерживает переключения LAF, должен быть переустановлен, если требуется динамическое переключение)

Для взлома поведения мыши основной подход заключается в подключении слушателя, установленного пользовательским интерфейсом:

  • найти оригинал
  • реализовать пользовательский слушатель, который делегирует большинство событий непосредственно оригиналу
  • для нажатых событий сначала запрашивается фокус: если выдан делегат оригиналу, если нет, ничего не делать

Последний пункт немного более сложный, потому что события фокуса могут быть асинхронными, поэтому мы должны вызвать проверку на фокусировку. Для вызова, в свою очередь, необходимо отправить релиз, если никто не возражал.

Еще одна изюминка - нажатие действия rootPane (для его defaultButton): оно выполняется без учета любых inputVerifiers, безоговорочно вызывая doClick. Это можно взломать, подключившись к действию, следуя тому же шаблону, что и подключение к mouseListener:

  • найти нажатие действия rootPane
  • реализовать пользовательское действие, которое проверяет наличие потенциального вето на inputVerifier: делегировать оригиналу, если нет, иначе ничего не делать

Пример, измененный по тем же направлениям:

public class VerifierTest implements Runnable {

    private static final long serialVersionUID = 1L;

    @Override
    public void run() {
        InteractiveTestCase.setLAF("Win");
        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(400, 200);

        JTextField tf = new JTextField("TextField1");
        tf.setInputVerifier(new PassVerifier());
        frame.add(tf, BorderLayout.NORTH);

        final JButton b = new JButton("Button");
        frame.add(b);
        b.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
               System.out.println("Button clicked");
            }
        });
        // hook into the mouse listener
        replaceBasicButtonListener(b);
        frame.add(new JTextField("not validating, something else to focus"),
                BorderLayout.SOUTH);
        frame.getRootPane().setDefaultButton(b);
        // hook into the default button action
        Action pressDefault = frame.getRootPane().getActionMap().get("press");
        frame.getRootPane().getActionMap().put("press", new DefaultButtonAction(pressDefault));
        frame.setVisible(true);
    }

    protected void replaceBasicButtonListener(AbstractButton b) {
        final BasicButtonListener original = getButtonListener(b);
        if (original == null) return;
        Hacker l = new Hacker(original);
        b.removeMouseListener(original);
        b.addMouseListener(l);
    }

    public static class Hacker implements MouseListener {
        private BasicButtonListener original;

        /**
         * @param original the listener to delegate to.
         */
        public Hacker(BasicButtonListener original) {
            this.original = original;
        }

        /**
         * Hook into the mousePressed: first request focus and
         * check its success before handling it.
         */
        @Override
        public void mousePressed(final MouseEvent e) {
            if (SwingUtilities.isLeftMouseButton(e)) {
                if(e.getComponent().contains(e.getX(), e.getY())) {
                    // check if we can get the focus
                    e.getComponent().requestFocus();
                    invokeHandleEvent(e);
                    return;
                }
            }
            original.mousePressed(e);
        }

        /**
         * Handle the pressed only if we are focusOwner.
         */
        protected void handlePressed(final MouseEvent e) {
            if (!e.getComponent().hasFocus())  {
                // something vetoed the focus transfer
                // do nothing
                return;
            } else {
                original.mousePressed(e);
                // need a fake released now: the one from the
                // original cycle might never has reached us
                MouseEvent released = new MouseEvent(e.getComponent(), MouseEvent.MOUSE_RELEASED,
                        e.getWhen(), e.getModifiers(), 
                        e.getX(), e.getY(), e.getClickCount(), e.isPopupTrigger()
                        );
                original.mouseReleased(released);
            }
        }


        /**
         * focus requests might be handled
         * asynchronously. So wrap the check 
         * wrap the block into an invokeLater.
         */
        protected void invokeHandleEvent(final MouseEvent e) {
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    handlePressed(e);
                }
            });
        }

        @Override
        public void mouseClicked(MouseEvent e) {
            original.mouseClicked(e);
        }

        @Override
        public void mouseReleased(MouseEvent e) {
            original.mouseReleased(e);
        }

        @Override
        public void mouseEntered(MouseEvent e) {
            original.mouseEntered(e);
        }

        @Override
        public void mouseExited(MouseEvent e) {
            original.mouseExited(e);
        }
    }
    public static class DefaultButtonAction extends AbstractAction {

        private Action original;

        /**
         * @param original
         */
        public DefaultButtonAction(Action original) {
            this.original = original;
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            JRootPane root = (JRootPane) e.getSource();
            JButton owner = root.getDefaultButton();
            if (owner != null && owner.getVerifyInputWhenFocusTarget()) {
                Component c = KeyboardFocusManager
                        .getCurrentKeyboardFocusManager()
                         .getFocusOwner();
                if (c instanceof JComponent && ((JComponent) c).getInputVerifier() != null) {
                    if (!((JComponent) c).getInputVerifier().shouldYieldFocus((JComponent) c)) return;
                }


            }
            original.actionPerformed(e);
        }

    }
    /**
     * Returns the ButtonListener for the passed in Button, or null if one
     * could not be found.
     */
    private BasicButtonListener getButtonListener(AbstractButton b) {
        MouseMotionListener[] listeners = b.getMouseMotionListeners();

        if (listeners != null) {
            for (MouseMotionListener listener : listeners) {
                if (listener instanceof BasicButtonListener) {
                    return (BasicButtonListener) listener;
                }
            }
        }
        return null;
    }

    public static void main(String[] args) {
       SwingUtilities.invokeLater(new VerifierTest());
    }


    public static class PassVerifier extends InputVerifier {
        /**
         * Decide whether or not the input is valid without
         * side-effects.
         */
        @Override
        public boolean verify(JComponent input) {
            final JTextField tf = (JTextField) input;
            String pass = tf.getText();
            if (pass.equals("Manish"))
                return true;
            return false;
        }

        /**
         * Implemented to ask the user what to do if the input isn't valid.
         * Note: not necessarily the best usability, it's mainly to
         * demonstrate the different effects on not/agreeing with
         * yielding focus transfer.
         */
        @Override
        public boolean shouldYieldFocus(final JComponent input) {
            boolean valid = super.shouldYieldFocus(input);
            if (!valid) {
                String message = "illegal value: " + ((JTextField) input).getText();
                int goAnyWay = JOptionPane.showConfirmDialog(input, "invalid value: " +
                        message + " - go ahead anyway?");
                valid = goAnyWay == JOptionPane.OK_OPTION;
            }
            return valid;
        }
    }
}

Я добавил звонок SwingUtilities чтобы убедиться, что GUI находится в потоке событий, и я удалил вашу ссылку на Frame.

GUI у меня работает на Windows XP.

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import javax.swing.InputVerifier;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;

public class VerifierTest implements Runnable {

    private static final long serialVersionUID = 1L;

    public VerifierTest() {

    }

    @Override
    public void run() {
        JFrame frame = new JFrame();
        frame.setSize(400, 200);

        JTextField tf;
        tf = new JTextField("TextField1");
        tf.setInputVerifier(new PassVerifier());
        frame.getContentPane().add(tf, BorderLayout.NORTH);

        final JButton b = new JButton("Button");
        b.setVerifyInputWhenFocusTarget(true);
        frame.getContentPane().add(b, BorderLayout.EAST);
        b.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                if (b.hasFocus())
                    System.out.println("Button clicked");
            }
        });

        frame.addWindowListener(new MyWAdapter());
        frame.setVisible(true);
    }

    public static void main(String[] args) {
       SwingUtilities.invokeLater(new VerifierTest());
    }

    class MyWAdapter extends WindowAdapter {
        @Override
        public void windowClosing(WindowEvent event) {
            System.exit(0);
        }
    }

    class PassVerifier extends InputVerifier {
        @Override
        public boolean verify(JComponent input) {
            JTextField tf = (JTextField) input;
            String pass = tf.getText();
            if (pass.equals("Manish"))
                return true;
            else {
                String message = "illegal value: " + tf.getText();
                JOptionPane.showMessageDialog(tf.getParent(), message,
                        "Illegal Value", JOptionPane.ERROR_MESSAGE);

                return false;
            }
        }
    }
}

Я добавил нового слушателя мыши к кнопке, как показано ниже, и теперь она мне подходит, но я не уверен, что это хороший способ исправить состояние выбора кнопок.

package test;

import java.awt.BorderLayout;
import java.awt.Frame;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import javax.swing.InputVerifier;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JTextField;
import javax.swing.plaf.basic.BasicButtonListener;

public class VerifierTest extends JFrame {

    private static final long serialVersionUID = 1L;

    public VerifierTest() {
        JTextField tf;
        tf = new JTextField("TextField1");

        getContentPane().add(tf, BorderLayout.NORTH);
        tf.setInputVerifier(new PassVerifier());

        final JButton b = new JButton("Button");
        b.setVerifyInputWhenFocusTarget(true);
        getContentPane().add(b, BorderLayout.EAST);
        b.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                if (b.hasFocus())
                    System.out.println("Button clicked");
            }
        });

        b.addMouseListener(new BasicButtonListener(b) {
            @Override
            public void mouseExited(MouseEvent e) {
                ((JButton)e.getSource()).getModel().setArmed(false);
                ((JButton)e.getSource()).getModel().setPressed(false);
            }

        });

        addWindowListener(new MyWAdapter());
    }

    public static void main(String[] args) {
        Frame frame = new VerifierTest();
        frame.setSize(400, 200);
        frame.setVisible(true);
        // frame.pack();
    }

    class MyWAdapter extends WindowAdapter {

        public void windowClosing(WindowEvent event) {
            System.exit(0);
        }
    }

    class PassVerifier extends InputVerifier {

        public boolean verify(JComponent input) {
            JTextField tf = (JTextField) input;
            String pass = tf.getText();
            if (pass.equals("Manish"))
                return true;
            else {
                final String message = "illegal value: " + tf.getText();
                        JOptionPane.showMessageDialog(null, message,
                                "Illegal Value", JOptionPane.ERROR_MESSAGE);

                return false;
            }
        }
    }
}

На самом деле настоящая проблема заключается в том, как взаимодействуют система фокусировки и слушатели. В Java объявлено несколько ошибок, которые разработчики обсуждают, кто несет ответственность. Слушатель мыши выполняет: processMouseEvent, и в рамках этой логики текущему FocusOwner предлагается выдать Focus. это терпит неудачу. Но поскольку половина события уже обработана, кнопка ставится на охрану, и фокус остается на поле.

Наконец-то я увидел один комментарий разработчика: не позволяйте слушателю продолжать работу, если поле не может потерять фокус.

Например: Определите поле JText с правками, чтобы разрешить только значения< 100. Сообщение появляется, когда вы теряете фокус. Я переопределил процесс processMouseEvent(MouseEvent e) моих базовых классов JButton с помощью кода:

protected void processMouseEvent(MouseEvent e) {
    if ( e.getComponent() != null && e.getComponent().isEnabled() ) { //should not be processing mouse events if it's disabled.
            if (e.getID() == MouseEvent.MOUSE_RELEASED && e.getClickCount() == 1) {
                // The mouse button is being released as per normal, and it's the first click. Process it as per normal.
                super.processMouseEvent(e);

                // If the release occured within the bounds of this component, we want to simulate a click as well
                if (this.contains(e.getX(), e.getY())) {
                    super.processMouseEvent(new MouseEvent(e.getComponent(),
                                                            MouseEvent.MOUSE_CLICKED,
                                                            e.getWhen(),
                                                            e.getModifiers(),
                                                            e.getX(),
                                                            e.getY(),
                                                            e.getClickCount(),
                                                            e.isPopupTrigger(),
                                                            e.getButton()));
                }
            }
            else if (e.getID() == MouseEvent.MOUSE_CLICKED && e.getClickCount() == 1) {
                // Normal clicks are ignored to prevent duplicate events from normal, non-moved events
            }
            else if (e.getID() == MouseEvent.MOUSE_PRESSED && e.getComponent() != null && (e.getComponent().isFocusOwner() || e.getComponent().requestFocusInWindow())) {// if already focus owner process mouse event
                super.processMouseEvent(e); 
            }
            else {
                // Otherwise, just process as per normal.
                if (e.getID() != MouseEvent.MOUSE_PRESSED) {
                    super.processMouseEvent(e); 
                }
            }
        }
}

в сущности этой логики есть простые вопросы. Кнопка: Вы уже являетесь владельцем фокуса. если нет: можете ли вы (кнопка), возможно, получить фокус GAIN (не забывайте, что для текущего держателя фокуса внутри вызова requestFocusInWindow () вызывается GAIN фокус (и будет ВСЕГДА возвращать значение false, если оно недопустимо)

Это также имеет побочный эффект появления вашего сообщения об ошибке!

Эта логика останавливает логику Java ProcessMouseEvent библиотек Java от обработки половины события, в то время как система фокусировки останавливает его завершение.

Очевидно, вам понадобится этот тип логики на всех ваших различных компонентах J, которые выполняют действие одним щелчком мыши.

Другие вопросы по тегам