Java swing - 使用键绑定处理同时按键

Java swing - Processing simultaneous key presses with key bindings

信息

看了几个相关的问题,我觉得我这里的情况有点特殊。

我正在构建一个 Java 摇摆应用程序来帮助鼓手制作简单的 shorthand 歌曲图表。有一个对话框,用户可以在其中“键入”节奏,将其录制到 MIDI 序列中 然后处理成制表或 sheet 音乐。这旨在与歌曲的一小段一起使用。

设置

想法是当绑定的 JButton 在记录序列时触发它们的动作时,它们将生成带有计时信息的 MidiMessage。我还希望按钮能够直观地表明它们已被激活。

绑定的键当前使用我已经实现的键绑定正确触发(同时按键除外)...

问题

重要的是将同时按键记录为单个事件——这里的时间很重要。

因此,例如,如果用户同时按下 H(踩镲)和 S(军鼓),它会在小节的同一个地方注册为齐奏。

我曾尝试使用与此类似的 KeyListener 实现:,但通过该设置,我 运行 遇到焦点问题,虽然它可以检测同时按键,但它也会处理他们单独。

有人能帮我解释一下吗?

  // code omitted

  public PunchesDialog(Frame owner, Song partOwner, Part relevantPart)
  {
    super(owner, ModalityType.APPLICATION_MODAL);

    this.partOwner = partOwner;
    this.relevantPart = relevantPart;

    // code omitted

    /*
     * Voices Panel
     */

    voices = new LinkedHashMap<>() {{
      put("crash",    new VoiceButton("CRASH (C)",         crashHitAction));
      put("ride",     new VoiceButton("RIDE (R)",          rideHitAction));
      put("hihat",    new VoiceButton("HI-HAT (H)",        hihatHitAction));
      put("racktom",  new VoiceButton("RACK TOM (T)",      racktomHitAction));
      put("snare",    new VoiceButton("SNARE (S)",         snareHitAction));
      put("floortom", new VoiceButton("FLOOR TOM (F)",     floortomHitAction));
      put("kickdrum", new VoiceButton("KICK DRUM (SPACE)", kickdrumHitAction));
    }};

    Action crashHitAction = new CrashHitAction();
    Action rideHitAction = new RideHitAction();
    Action hihatHitAction = new HihatHitAction();
    Action racktomHitAction = new RacktomHitAction();
    Action snareHitAction = new SnareHitAction();
    Action floortomHitAction = new FloortomHitAction();
    Action kickdrumHitAction = new KickdrumHitAction();

    KeyStroke key;
    InputMap inputMap = ((JPanel) getContentPane()).
      getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
    ActionMap actionMap = ((JPanel) getContentPane()).getActionMap();

    key = KeyStroke.getKeyStroke(KeyEvent.VK_C, 0);
    inputMap.put(key, "crashHit");
    actionMap.put("crashHit", crashHitAction);

    key = KeyStroke.getKeyStroke(KeyEvent.VK_R, 0);
    inputMap.put(key, "rideHit");
    actionMap.put("rideHit", rideHitAction);

    key = KeyStroke.getKeyStroke(KeyEvent.VK_H, 0);
    inputMap.put(key, "hihatHit");
    actionMap.put("hihatHit", hihatHitAction);

    key = KeyStroke.getKeyStroke(KeyEvent.VK_T, 0);
    inputMap.put(key, "racktomHit");
    actionMap.put("racktomHit", racktomHitAction);

    key = KeyStroke.getKeyStroke(KeyEvent.VK_S, 0);
    inputMap.put(key, "snareHit");
    actionMap.put("snareHit", snareHitAction);

    key = KeyStroke.getKeyStroke(KeyEvent.VK_F, 0);
    inputMap.put(key, "floortomHit");
    actionMap.put("floortomHit", floortomHitAction);

    key = KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0);
    inputMap.put(key, "kickdrumHit");
    actionMap.put("kickdrumHit", kickdrumHitAction);

    final JPanel pnlVoices = new JPanel(new MigLayout(
          "Insets 0, gap 0, wrap 2", "[fill][fill]", "fill"));
    pnlVoices.add(voices.get("crash"),    "w 100%, h 100%, grow");
    pnlVoices.add(voices.get("ride"),     "w 100%");
    pnlVoices.add(voices.get("hihat"),    "w 100%");
    pnlVoices.add(voices.get("racktom"),  "w 100%");
    pnlVoices.add(voices.get("snare"),    "w 100%");
    pnlVoices.add(voices.get("floortom"), "w 100%");
    pnlVoices.add(voices.get("kickdrum"), "span");

    // code omitted

  }

  private class CrashHitAction extends AbstractAction
  {
    @Override
    public void actionPerformed(ActionEvent e) {
      // voices.get("crash").doClick(100);
      kfMgr.clearFocusOwner();

      logger.debug("hit crash");
    }
  }

  private class RideHitAction extends AbstractAction
  {
    @Override
    public void actionPerformed(ActionEvent e) {
      // voices.get("ride").doClick(100);
      kfMgr.clearFocusOwner();

      logger.debug("hit ride");
    }
  }

  private class HihatHitAction extends AbstractAction
  {
    @Override
    public void actionPerformed(ActionEvent e) {
      // voices.get("hihat").doClick(100);
      kfMgr.clearFocusOwner();

      logger.debug("hit hihat");
    }
  }

  private class RacktomHitAction extends AbstractAction
  {
    @Override
    public void actionPerformed(ActionEvent e) {
      // voices.get("racktom").doClick(100);
      kfMgr.clearFocusOwner();

      logger.debug("hit racktom");
    }
  }

  private class FloortomHitAction extends AbstractAction
  {
    @Override
    public void actionPerformed(ActionEvent e) {
      // voices.get("floortom").doClick(100);
      kfMgr.clearFocusOwner();

      logger.debug("hit floortom");
    }
  }

  private class SnareHitAction extends AbstractAction
  {
    @Override
    public void actionPerformed(ActionEvent e) {
      // voices.get("snare").doClick(100);
      kfMgr.clearFocusOwner();

      logger.debug("hit snare");
    }
  }

  private class KickdrumHitAction extends AbstractAction
  {
    @Override
    public void actionPerformed(ActionEvent e) {
      // voices.get("kickdrum").doClick(100);
      kfMgr.clearFocusOwner();

      logger.debug("hit kickdrum");
    }
  }

对话框截图在这里: https://i.stack.imgur.com/n4RzY.png

我个人会使用 KeyListener 界面来跟踪用户键入的内容,然后在列表中注册已按下的键(不释放),然后在释放任何键后收集它们。

确保仅将不存在的键添加到列表中,因为当用户按住某个键时会多次触发 keyPressed 事件。

我还创建了一个小示例来为您提供思路。

public class MyClass extends JFrame implements KeyListener {

    private JTextArea textArea;
    private List<Character> listKeys;

    public MyClass() {
        setTitle("test");

        listKeys = new ArrayList<>();
        textArea = new JTextArea();
        textArea.addKeyListener(this);

        setLayout(new BorderLayout());
        add(textArea, BorderLayout.CENTER);

        setLocation(50, 50);
        setSize(500, 500);
        setVisible(true);
    }

    @Override
    public void keyTyped(KeyEvent e) {
    }

    @Override
    public void keyPressed(KeyEvent e) {
        if (!listKeys.contains(e.getKeyChar())) {
            listKeys.add(e.getKeyChar());
        }
    }

    @Override
    public void keyReleased(KeyEvent e) {
        if (listKeys.isEmpty()) {
            return;
        }

        if (listKeys.size() > 1) {
            System.out.print("The key combination ");
        } else {
            System.out.print("The key ");
        }
        for (Character c : listKeys) {
            System.out.print(c + " ");
        }
        System.out.println("has been entered");
        listKeys.clear();
    }

    public static void main(String[] args) {
        new MyClass();
    }
}

您需要将一些概念进一步解耦。例如,Action API 允许您在按钮(所有按钮)上使用相同的 Action(相同 Action 的多个实例的相同实例)作为键绑定。

在这种情况下,您想找到 Action 与可能的触发器分离的地方(即,如果可能,不要假设它是按钮或键绑定)

对我来说,当键绑定被触发时,我想通知某种观察者或管理者动作已经发生。一个可能的考虑是,按下时和松开时有什么区别?

KeyStroke 允许您定义“按下”和“释放”触发器。然后我会使用某种监视器来管理状态,即一系列 booleans,它们是 truefalse,具体取决于操作的状态,但是,这不会扩展性不好,因此,我会考虑使用 enumSet

以下示例将仅在按住相关操作的键时突出显示标签。

import java.awt.Color;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;

public class Main {
    public static void main(String[] args) {
        new Main();
    }

    public Main() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame frame = new JFrame();
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        enum UserAction {
            CRASH_HIT, RIDE_HIT, HI_HAT_HIT, RACK_TOM_HIT, SNARE_HIT, FLOOR_TOM_HIT, KICK_DRUM_HIT;
        }

        public interface Observer {
            public void didActivateAction(UserAction action);
            public void didDeactivateAction(UserAction action);
        }

        private Map<UserAction, JLabel> labels;
        private Set<UserAction> activeActions = new TreeSet<>();
        private final Set<UserAction> allActions = new TreeSet<>(Arrays.asList(UserAction.values()));

        public TestPane() {            
            labels = new HashMap<>();
            for (UserAction action : UserAction.values()) {
                JLabel label = new JLabel(action.name());
                label.setBorder(new CompoundBorder(new LineBorder(Color.DARK_GRAY), new EmptyBorder(8, 8, 8, 8)));
                label.setOpaque(true);
                add(label);

                labels.put(action, label);
            }

            InputMap inputMap = getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
            ActionMap actionMap = getActionMap();

            Observer observer = new Observer() {
                @Override
                public void didActivateAction(UserAction action) {
                    if (activeActions.contains(action)) {
                        // We don't want to deal with "repeated" key events
                        return;
                    }
                    activeActions.add(action);
                    // I could update the labels here, but this is a deliberate 
                    // example of how to decouple the action from the state
                    // so the actions can be dealt with in as a single unit
                    // of work, you can also take into consideratoin any
                    // relationships which different inputs might have as well
                    updateUIState();
                }

                @Override
                public void didDeactivateAction(UserAction action) {
                    activeActions.remove(action);
                    updateUIState();
                }
            };

            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, 0, false), "pressed-crashHit");
            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, 0, true), "released-crashHit");
            actionMap.put("pressed-crashHit", new InputAction(UserAction.CRASH_HIT, true, observer));
            actionMap.put("released-crashHit", new InputAction(UserAction.CRASH_HIT, false, observer));

            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_R, 0, false), "pressed-rideHit");
            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_R, 0, true), "released-rideHit");
            actionMap.put("pressed-rideHit", new InputAction(UserAction.RIDE_HIT, true, observer));
            actionMap.put("released-rideHit", new InputAction(UserAction.RIDE_HIT, false, observer));

            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_H, 0, false), "pressed-hihatHit");
            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_H, 0, true), "released-hihatHit");
            actionMap.put("pressed-hihatHit", new InputAction(UserAction.HI_HAT_HIT, true, observer));
            actionMap.put("released-hihatHit", new InputAction(UserAction.HI_HAT_HIT, false, observer));

            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_T, 0, false), "pressed-racktomHit");
            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_T, 0, true), "released-racktomHit");
            actionMap.put("pressed-racktomHit", new InputAction(UserAction.RACK_TOM_HIT, true, observer));
            actionMap.put("released-racktomHit", new InputAction(UserAction.RACK_TOM_HIT, false, observer));

            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_S, 0, false), "pressed-snareHit");
            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_S, 0, true), "released-snareHit");
            actionMap.put("pressed-snareHit", new InputAction(UserAction.SNARE_HIT, true, observer));
            actionMap.put("released-snareHit", new InputAction(UserAction.SNARE_HIT, false, observer));

            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F, 0, false), "pressed-floortomHit");
            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F, 0, true), "released-floortomHit");
            actionMap.put("pressed-floortomHit", new InputAction(UserAction.FLOOR_TOM_HIT, true, observer));
            actionMap.put("released-floortomHit", new InputAction(UserAction.FLOOR_TOM_HIT, false, observer));

            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0, false), "pressed-kickdrumHit");
            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0, true), "released-kickdrumHit");
            actionMap.put("pressed-kickdrumHit", new InputAction(UserAction.KICK_DRUM_HIT, true, observer));
            actionMap.put("released-kickdrumHit", new InputAction(UserAction.KICK_DRUM_HIT, false, observer));
        }

        protected void updateUIState() {
            Set<UserAction> inactiveActions = new TreeSet<>(allActions);
            inactiveActions.removeAll(activeActions);

            for (UserAction action : inactiveActions) {
                JLabel label = labels.get(action);
                label.setBackground(null);
                label.setForeground(Color.BLACK);
            }
            for (UserAction action : activeActions) {
                JLabel label = labels.get(action);
                label.setBackground(Color.BLUE);
                label.setForeground(Color.WHITE);
            }
        }

        // This could act as a base class, from which other, more dedicated
        // implementations could be built, which did focused jobs, for example
        // protected class ActivateCrashHit extends InputAction {
        //    public ActivateCrashHit(Observer observer) {
        //        super(UserAction.CRASH_HIT, true, observer);
        //    }
        //    // Override actionPerformed
        // }
        protected class InputAction extends AbstractAction {

            private UserAction action;
            private boolean activated;
            private Observer observer;

            public InputAction(UserAction action, boolean activated, Observer observer) {
                this.action = action;
                this.activated = activated;
                this.observer = observer;
            }

            @Override
            public void actionPerformed(ActionEvent e) {
                // This could perform other actions, but the intention of the
                // observer is provide an oppurunity for the interested party
                // to also make some kind of update, to allow the user to
                // see that that action occured
                if (activated) {
                    observer.didActivateAction(action);
                } else {
                    observer.didDeactivateAction(action);
                }
            }
        }
    }
}

您还应该注意,某些键盘存在硬件限制,限制了可以在任何时候同时按下的键数,但老实说,我发现很难按下所有键以任何方式为这个例子一次