英文:
Java KeyAdapter vs. Swing Key Bindings?
问题
我有一个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();
}
// 初始化
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;
}
});
}
// 绘制
@Override
protected void paintComponent(Graphics g) {
// 绘制背景
super.paintComponent(g);
g.setColor(Color.black);
g.drawString("Test: " + eventTest, 10, 10);
eventTest = false;
}
}
另外,在我的程序中,我有一个循环每秒调用repaint()
方法10次,这样我可以看到eventTest
被更新。我期望这个系统在按下Shift键的帧上显示eventTest
为true,否则为false。我还测试了通过更改相关的键代码来测试其他键。
当我想测试KeyAdapter
时,我将以下代码块添加到initBoard()
方法中,并在构造函数中注释掉initKeyBindings()
:
this.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_SHIFT) {
eventTest = true;
}
}
});
使用KeyAdapter
类时,这按预期工作。然而,当我切换到使用键绑定时,它变得令人困惑。由于某种原因,只有在我同时按下两个Shift键时,eventTest
才会显示为true。如果我按住其中一个Shift键,事件测试在我按下另一个键时变为true,然后返回false。我希望在按下一个Shift键时就能实现这一点,而不必按住另一个Shift键。
此外,当我将其设置为触发右箭头按键时,会发生略有不同的行为。在KeyAdapter
和键绑定模式下,发生的情况是,eventTest
在我按下右箭头时变为true,短时间后返回false,然后在我按住箭头时变为true。根据在线文档的阅读,似乎这是由于操作系统依赖的行为引起的(我正在运行Ubuntu 18.04),它会在按键被按住的同时继续发送KeyPressed
事件。我感到困惑的是为什么这种行为会对Shift键和右箭头产生不同的影响。如果可能的话,我希望找到一种方法,使eventTest
只在按下键的第一帧时为true。
有关导致这种情况的原因有什么想法吗?谢谢!
英文:
I have a java swing program that I was previously controlling with a the KeyAdapter
class. For several reasons, I have decided to switch over to using swing's built in key binding system (using InputMap
and ActionMap
) instead. While switching, I have run into some confusing behaviors. <p>
In order to test these systems, I have a simple 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;
}
Also in my program, I have a loop calling the repaint()
method 10 times per second, so that I can see eventTest get updated. I am expecting this system to display eventTest
as true on a frame where the shift key becomes pressed, and false otherwise. I also have tested other keys by changing the relevant key codes. <p>
When I want to test the KeyAdapter, I add this block to the initBoard()
method, and comment out initKeyBindings()
in the constructor:
this.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_SHIFT) {
eventTest = true;
}
}
});
When using the KeyAdapter
class, this works as expected. However, when I switch over to using key bindings, it becomes confusing. For some reason, eventTest
is only displayed as true when I press down both shift keys. If I hold either shift key down, event test becomes true on the frame when I press the other, and then returns to false. I would like it to do this when one shift key is pressed, without having to hold the other one. <p>
Additionally, when I set it to trigger on right arrow presses instead, a slightly different behavior happens. In both the KeyAdapter
and key bindings modes, what happens is that eventTest becomes true on the frame I press the right arrow, returns to false for a short time, and then becomes true for as long as I hold the arrow. From reading the documentation online, it appears that this is caused by an OS dependent behavior (I am running Ubuntu 18.04) to continue sending out KeyPressed
events while a key is held down. What I am confused about is why this behavior would be different for the shift key than for the right arrow. If possible, I would like to find a way to make eventTest
true only on the first frame a key is pressed. <p>
Any ideas as to what is causing this? Thanks!
答案1
得分: 1
我已经找到了至少部分答案。
对于当使用键绑定时,我必须同时按住两个Shift键才能生成按键事件的问题,有一个简单的解决方法。只需要将添加到InputMap
的内容更改为:
getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_SHIFT, 0), "pressed");
改为
getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_SHIFT, KeyEvent.SHIFT_DOWN_MASK), "pressed");
我不完全确定为什么输入映射会将按下单个Shift键视为带有键码VK_SHIFT
AND SHIFT_DOWN_MASK
的KeyEvent
,但似乎是这样的。对我来说更符合直觉的做法是,仅在已经按下一个Shift键的情况下,用户尝试按下另一个Shift键时应用掩码,但有趣的是,此绑定不再检测按住一个Shift键并按下另一个Shift键的事件。奇怪。
其他键的问题有稍微不那么干净的解决方法。至于为什么Shift键的行为与其他键不同的问题。我认为这是操作系统中内置的有意设计。例如,如果用户按住右箭头(或许多其他键,例如每个文本字符键),可以合理地假设他们想要重复与该键绑定的操作。即,如果用户正在输入文本,按住并按住“a”键,他们可能想要快速连续输入多个“a”字符到文本文档中。然而,在大多数情况下,以类似的方式自动重复Shift键不是对用户有用的。因此,不生成Shift键的重复事件是有道理的。我没有任何来源来支持这一观点,这只是一个假设,但对我来说是合理的。
要删除这些额外的事件,似乎没有好的解决方案。一个有效但不够精确的方法是存储当前按下的所有键的列表,然后在执行操作之前让您的动作映射检查键是否按下。另一种方法是使用计时器,并忽略在时间上接近彼此发生的事件(请参阅此帖子以获取更多详细信息)。这两种实现都需要更多的内存使用和为要跟踪的每个键编写代码,因此不是理想的解决方案。
一个稍微更好的解决方法(在我看来)可以使用KeyAdapter
而不是键绑定来实现。这个解决方法的关键在于,按下一个键而另一个键已经按下会中断自动重复事件的流程,而且不会再次为原始键生成事件(即使第二个键被释放)。因此,我们只需跟踪最后按下的键,以准确地过滤掉所有自动重复事件,因为只有该键才可能发送这些事件。
代码将类似于以下内容:
addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
int keyCode = e.getKeyCode();
if (keyCode != lastKeyPressed && keyCode != KeyEvent.VK_UNDEFINED) {
// 执行某些操作
lastKeyPressed = keyCode;
}
}
@Override
public void keyReleased(KeyEvent e) {
// 执行某些操作
lastKeyPressed = -1; // 表示当前不可能发送任何按键的自动重复事件
}
});
这个解决方案当然会失去Swing键绑定系统提供的一些灵活性,但有一个更容易的解决方法。您可以创建自己的int
到Action
映射(或任何其他方便描述您想要执行的操作的类型),而不是将它们添加到InputMap
和ActionMap
中,将它们放在映射中。接下来,而不是在KeyAdapter
中放置所需操作的直接代码,放置类似于myMap.get(e.getKeyCode()).actionPerformed();
的内容。这允许您通过在映射上执行相应的操作来添加、删除和更改键绑定。
英文:
I have found at least a partial answer.
For the issue where I had to hold down both shift keys to generate a key pressed event when using key bindings, there is a simple fix. All that needs to be done is to change the what is added to the InputMap
from:
getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_SHIFT, 0), "pressed");
to
getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_SHIFT, KeyEvent.SHIFT_DOWN_MASK), "pressed");
I am not completely sure why the input map counts pressing a single shift key as as KeyEvent
with a key code of VK_SHIFT
AND the SHIFT_DOWN_MASK
, but that appears to be what it is doing. It would make more intuitive sense to me if the mask was applied only for if there is already one shift key pressed and the user attempts to press the other one, but interestingly, this binding no longer detects events for if one shift key is held and the other is pressed. Weird.
The problems with other keys have slightly less clean solutions. As to the question of why shift behaves differently than other keys. I believe this is an intentional design built into the OS. For example, if the user presses and holds the right arrow (or many other keys, such as the every text character key), it is reasonable to assume that they want to repeat the action that is tied to that key. I.e. if a user is typing, and presses and holds "a", they likely want to input multiple "a" characters in quick succession into the text document. However, auto-repeating the shift key in a similar manner is not (in most cases) useful to the user. Therefore, it makes sense that no repeated events for the shift key are generated. I don't have any sources to back this up, it is just a hypothesis, but it makes sense to me.
In order to remove these extra events, there doesn't seem to be a good solution. One thing that works, but is sloppy, is to store a list of all keys currently pressed, and then have your action map check if the key is pressed before executing its action. Another approach would be to use timers and ignore events that occur to close in time to one another (see this post for more details). Both of these implementations require more memory usage and code for every key you wish to track, so they are not ideal.
A slightly better solution (IMO) can be achieved using KeyAdapter
instead of Key Bindings. The key to this solution lies in the fact that pressing down one key while another is held will interrupt the stream of auto-repeat events, and it will not resume again for the original key (even if the second key is released). Because of this, we really only have to track the last key pressed in order to accurately filter out all auto-repeat events, because that is the only key that could be sending those events.
The code would look something like this:
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
}
});
This solution of course looses some of the flexibility provided by swing's key binding system, but that has an easier workaround. You can create your own map of int
to Action
(or really any other type that is convenient to describe what you want to do), and instead of adding key bindings to InputMap
s and ActionMap
s, you put them in there. Next, instead of putting the direct code for the action you want to do inside of the KeyAdapter
, put something like myMap.get(e.getKeyCode()).actionPerformed();
. This allows you to add, remove, and change key bindings by performing the corresponding operation on the map.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论