在阅读了几个相关问题后,我认为我在这里有一些独特的情况。
我正在构建一个 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
您需要进一步解耦一些概念。 例如,
Action
API 允许您在按钮(所有按钮)以及按键绑定上使用相同的 Action
(同一 Action
的多个实例的同一实例)。
在这种情况下,您希望找到
Action
与可能的触发器分离的位置(即,如果可能,不要假设它是按钮或键绑定)
对我来说,当触发键绑定时,我想通知某种观察者或管理者该操作已经发生。 一个可能的考虑是,按下时和松开时有什么区别吗?
KeyStroke
允许您定义“按下”和“释放”触发器。 然后,我会使用某种监视器来管理状态,即一系列 boolean
,它们是 true
或 false
,具体取决于操作的状态,但是,这不能很好地扩展,所以,相反,我会考虑使用 enum
和 Set
来代替。
以下示例仅在按住关联操作的键时突出显示标签。
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);
}
}
}
}
}
您还应该注意,某些键盘存在硬件限制,限制了可以同时按下的同时按键的数量,尽管说实话,我发现在这个例子中很难按一次所有按键无论如何
我个人会使用
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();
}
}