Java KeyAdapter против привязок клавиш Swing? - PullRequest
2 голосов
/ 04 августа 2020

У меня есть программа java Swing, которую я ранее контролировал с помощью класса KeyAdapter. По нескольким причинам я решил перейти на использование встроенной системы привязки клавиш Swing (используя InputMap и ActionMap). При переключении я столкнулся с некоторыми непонятными моментами.

Для тестирования этих систем у меня есть простая панель JPanel:

public class Board extends JPanel {

    private final int WIDTH = 500;
    private final int HEIGHT = 500;
    
    private boolean eventTest = false;

    public Board() {
        initBoard();
        initKeyBindings();
    }

    // initialization
    // -----------------------------------------------------------------------------------------
    private void initBoard() {
        setPreferredSize(new Dimension(WIDTH, HEIGHT));
        setFocusable(true);
    }

    private void initKeyBindings() {

        getInputMap().put((KeyStroke.getKeyStroke(KeyEvent.VK_SHIFT, 0), "Shift Pressed");

        getActionMap().put("Shift Pressed", new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                eventTest = true;
            }
        });

    }

    // drawing
    // -----------------------------------------------------------------------------------------
    @Override
    protected void paintComponent(Graphics g) {
        // paint background
        super.paintComponent(g);

        g.setColor(Color.black);
        g.drawString("Test: " + eventTest, 10, 10);
        eventTest = false;
    }

Также в моей программе есть al oop, вызывающий метод repaint() 10 раз в секунду, так что я можно увидеть обновление eventTest. Я ожидаю, что эта система будет отображать eventTest как истинное в кадре, где нажата клавиша Shift, и как ложное в противном случае. Я также протестировал другие ключи, изменив соответствующие коды клавиш.

Когда я хочу протестировать KeyAdapter, я добавляю этот блок к методу initBoard() и закомментирую initKeyBindings() в конструкторе:

this.addKeyListener(new KeyAdapter() {
    @Override
    public void keyPressed(KeyEvent e) {
        if (e.getKeyCode() == KeyEvent.VK_SHIFT) {
            eventTest = true;
        }
    }
});

При использовании класса KeyAdapter это работает как положено. Однако когда я перехожу на использование привязок клавиш, это сбивает с толку. По какой-то причине eventTest отображается как истинное, только когда я нажимаю обе клавиши Shift. Если я удерживаю одну из клавиш Shift нажатой, тест события становится истинным в кадре, когда я нажимаю другую, а затем возвращается к ложному. Я бы хотел, чтобы это происходило при нажатии одной клавиши Shift, без необходимости удерживать другую.

Вдобавок, когда я устанавливаю его на срабатывание при нажатии стрелки вправо, происходит немного другое поведение. Как в режиме KeyAdapter, так и в режиме привязки клавиш происходит то, что eventTest становится истинным в кадре, когда я нажимаю стрелку вправо, на короткое время возвращается в значение false, а затем становится истинным, пока я удерживаю стрелку. Из чтения документации в Интернете видно, что это вызвано зависимым от ОС поведением (я использую Ubuntu 18.04) для продолжения отправки событий KeyPressed, пока клавиша удерживается. Что меня смущает, так это то, почему это поведение будет отличаться для клавиши Shift, чем для стрелки вправо. Если возможно, я хотел бы найти способ сделать eventTest истинным только в первом кадре, когда нажата клавиша.

Есть идеи относительно того, что вызывает это? Спасибо!

1 Ответ

1 голос
/ 14 августа 2020

Я нашел по крайней мере частичный ответ.

Для проблемы, когда мне приходилось удерживать обе клавиши Shift для генерации события нажатия клавиши при использовании привязки клавиш, есть простое исправление. Все, что нужно сделать, это изменить то, что добавлено к InputMap, с:

getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_SHIFT, 0), "pressed");

на

getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_SHIFT, KeyEvent.SHIFT_DOWN_MASK), "pressed");

Я не совсем уверен, почему карта ввода учитывает нажатие одинарная клавиша Shift как KeyEvent с кодом клавиши VK_SHIFT И SHIFT_DOWN_MASK, но, похоже, это именно то, что он делает. Для меня было бы более интуитивно понятным, если бы маска применялась только в том случае, если уже была нажата одна клавиша Shift, и пользователь пытается нажать другую, но, что интересно, эта привязка больше не обнаруживает события, если одна клавиша Shift удерживается и другой нажимается. Странно.

Проблемы с другими ключами имеют несколько менее чистое решение. Относительно вопроса, почему shift ведет себя иначе, чем другие клавиши. Я считаю, что это намеренный дизайн, встроенный в ОС. Например, если пользователь нажимает и удерживает стрелку вправо (или многие другие клавиши, такие как клавиша каждого текстового символа), разумно предположить, что он хочет повторить действие, привязанное к этой клавише. То есть, если пользователь печатает, нажимает и удерживает «а», он, вероятно, захочет ввести несколько символов «а» в быстрой последовательности в текстовый документ. Однако автоматическое повторение клавиши Shift аналогичным образом (в большинстве случаев) бесполезно для пользователя. Следовательно, имеет смысл, что никакие повторяющиеся события для клавиши Shift не генерируются. У меня нет никаких источников, подтверждающих это, это всего лишь гипотеза, но для меня это имеет смысл.

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

Чуть лучшее решение (IMO) может быть достигнуто с использованием KeyAdapter вместо ключа Привязки. Ключ к этому решению заключается в том, что нажатие одной клавиши при удерживании другой прервет поток событий автоповтора, и он больше не возобновится для исходной клавиши (даже если вторая клавиша будет отпущена). Из-за этого нам действительно нужно отслеживать только последнюю нажатую клавишу, чтобы точно отфильтровать все события автоповтора, потому что это единственный ключ, который может отправлять эти события.

Код будет выглядеть примерно так вот так:

addKeyListener(new KeyAdapter() {
    @Override
    public void keyPressed(KeyEvent e) {
        int keyCode = e.getKeyCode();
        
        if (keyCode != lastKeyPressed && keyCode != KeyEvent.VK_UNDEFINED) {
            // do some action
            lastKeyPressed = keyCode;
        }
    }

    @Override
    public void keyReleased(KeyEvent e) {
        // do some action
        lastKeyPressed = -1; // indicates that it is not possible for any key 
                             // to send auto-repeat events currently
    }
});

Это решение, конечно, теряет некоторую гибкость, обеспечиваемую системой привязки клавиш Swing, но здесь есть более простой обходной путь. Вы можете создать свою собственную карту от int до Action (или действительно любого другого типа, который удобен для описания того, что вы хотите сделать), и вместо добавления привязок клавиш к InputMap s и ActionMap s вы положи их туда. Затем вместо того, чтобы помещать прямой код действия, которое вы хотите выполнить, внутри KeyAdapter, введите что-то вроде myMap.get(e.getKeyCode()).actionPerformed();. Это позволяет добавлять, удалять и изменять привязки клавиш, выполнив соответствующую операцию на карте.

...