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

问题描述 投票:0回答:2

信息

在阅读了几个相关问题后,我认为我在这里有一些独特的情况。

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

设置

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

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

问题

将同时按键注册为单个事件非常重要 - 并且时间安排在这里很重要。

因此,例如,如果用户同时按下H(踩镲)和S(军鼓),它将在小节中的同一位置注册为齐奏打击。

我尝试使用与此类似的 KeyListener 实现:https://stackoverflow.com/a/13529058/13113770,但是通过该设置,我遇到了焦点问题,虽然它可以检测同时按下的按键,但它会也单独处理它们。

有人可以帮我解释一下吗?

  // 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.sstatic.net/n4RzY.png

java swing jbutton key-bindings
2个回答
2
投票

您需要进一步解耦一些概念。 例如,

Action
API 允许您在按钮(所有按钮)以及按键绑定上使用相同的
Action
(同一
Action
的多个实例的同一实例)。

在这种情况下,您希望找到

Action
与可能的触发器分离的位置(即,如果可能,不要假设它是按钮或键绑定)

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

KeyStroke
允许您定义“按下”和“释放”触发器。 然后,我会使用某种监视器来管理状态,即一系列
boolean
,它们是
true
false
,具体取决于操作的状态,但是,这不能很好地扩展,所以,相反,我会考虑使用
enum
Set
来代替。

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

enter image description here

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);
                }
            }
        }
    }
}

您还应该注意,某些键盘存在硬件限制,限制了可以同时按下的同时按键的数量,尽管说实话,我发现在这个例子中很难按一次所有按键无论如何


1
投票

我个人会使用

KeyListener
界面来跟踪用户输入的内容,并在
Set<Character>
中注册已按下但未释放的按键。最后,一旦释放任何键,就收集该组的内容。

我使用

Set
而不是
List
的原因是因为当用户按住某个键时,
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();
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.