第三章 人机交互和用户接口
AWT事件模型
正如前面提到的,AWT有自己的事件分配线程。这个线程分配来自于操作系统的各种事件,例如,点击鼠标、按下键盘键。
AWT是在哪里分配这些事件的呢?当某个组件发生一个事件时,AWT检查是否有该事件的listener。Listener是一个对象,它接受来自另一个对象的事件。这里,事件来自于AWT事件分配线程。
不同的事件有不同的listener。例如,键盘输入事件有KeyListener接口。
下面的例子说明了键盘按键的事件模型:
1. 用户按下一个键
2. 操作系统向Java运行时发送键盘事件
3. Java运行时将收到的事件放入AWT事件队列
4. AWT事件分配线程将事件分配给KeyListener
5. KeyListener收到键盘事件,完成键盘事件所要求的工作
所有Listener都是接口,因此任何对象都可以通过实现listener接口成为listener。还要注意到,同一个类型的事件可以有几个listener。例如,几个对象都在侦听鼠标事件。这个特点是有用的,但是你不必在你的代码中处理同类型事件多个listener的情况。
有一个方法可以捕获所有AWT事件。虽然这样做对实际的游戏没有用,但是,这对调试代码或弄清分配了什幺事件是有帮助的。下面的代码通过创建AWTEventListener捕获所有事件,并将事件输出到控制台:
Toolkit.getDefaultToolkit().addAWTEventListener(
new AWTEventListener() {
public void eventDispatched(AWTEvent event) {
System.out.println(event);
}
}, -1);
要记住的是,不要在交付的游戏代码中使用上面的代码段;只在测试时使用这样的代码。
键盘输入
游戏中会用到许多键,例如,箭头键控制运动方向,Ctrl键发射武器。我们确实不打算处理文本输入这样的事件---这留给本章后面讨论的Swing组件处理。
要捕获键盘事件需要做两件事情:创建KeyListener和注册listener以便接收事件。要注册listener,只要调用接收键盘事件的组件的addKeyListener()方法。对游戏来说,这个组件就是全屏幕窗口:
Window window = screen.getFullScreenWindow();
window.addKeyListener(keyListener);
要创建KeyListener,只要创建一个实现了KeyListener接口的对象。KeyListener接口有三个方法:keyPressed()、keyReleased()和keyTyped()。接收“Typed”事件对游戏几乎没有什幺用处,因此,我们只讨论按下键和释放键事件。
这三个方法都以KeyEvent作为参数。KeyEvent对象使你可以检查按下或释放了什幺键,得到的是虚拟键码。虚拟键码是Java中与键盘键对应的代码,但是,它不同于字符。例如,虽然Q和q是不同的字符,但是,它们有相同的虚拟键码。
所有虚拟键码都以VK_xxx的形式定义在KeyEvent中。例如,Q键的键码是KeyEvent.VK_Q。大部分键码都是可想而知的(例如,VK_ENTER或VK_1),完整的虚拟键码可在Java API文档的KeyEvent类中查找到。
现在,让我们试一试。代码3.2中的KeyTest类是KeyListener接口的一个实现。它将按下的键和释放的键显示在屏幕上。按ESC键退出程序。
代码3.2 KeyTest.java
import java.awt.event.KeyListener;
import java.awt.event.KeyEvent;
import java.util.LinkedList;
import com.brackeen.javagamebook.graphics.*;
import com.brackeen.javagamebook.test.GameCore;
/**
A simple keyboard test. Displays keys pressed and released to
the screen. Useful for debugging key input, too.
*/
public class KeyTest extends GameCore implements KeyListener {
public static void main(String[] args) {
new KeyTest().run();
}
private LinkedList messages = new LinkedList();
public void init() {
super.init();
Window window = screen.getFullScreenWindow();
// allow input of the TAB key and other keys normally
// used for focus traversal
window.setFocusTraversalKeysEnabled(false);
// register this object as a key listener for the window
window.addKeyListener(this);
addMessage("KeyInputTest. Press Escape to exit");
}
// a method from the KeyListener interface
public void keyPressed(KeyEvent e) {
int keyCode = e.getKeyCode();
// exit the program
if (keyCode == KeyEvent.VK_ESCAPE) {
stop();
}
else {
addMessage("Pressed: " +
KeyEvent.getKeyText(keyCode));
// make sure the key isn't processed for anything else
e.consume();
}
}
// a method from the KeyListener interface
public void keyReleased(KeyEvent e) {
int keyCode = e.getKeyCode();
addMessage("Released: " + KeyEvent.getKeyText(keyCode));
// make sure the key isn't processed for anything else
e.consume();
}
// a method from the KeyListener interface
public void keyTyped(KeyEvent e) {
// this is called after the key is released - ignore it
// make sure the key isn't processed for anything else
e.consume();
}
/**
Add a message to the list of messages.
*/
public synchronized void addMessage(String message) {
messages.add(message);
if (messages.size() >= screen.getHeight() / FONT_SIZE) {
messages.remove(0);
}
}
/**
Draw the list of messages
*/
public synchronized void draw(Graphics2D g) {
Window window = screen.getFullScreenWindow();
g.setRenderingHint(
RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
// draw background
g.setColor(window.getBackground());
g.fillRect(0, 0, screen.getWidth(), screen.getHeight());
// draw messages
g.setColor(window.getForeground());
int y = FONT_SIZE;
for (int i=0; i<messages.size(); i++) {
g.drawString((String)messages.get(i), 5, y);
y+=FONT_SIZE;
}
}
}
你应当注意到了两点。第一,init()方法用到了下面一行代码:
window.setFocusTraversalKeysEnabled(false);
这行代码屏蔽了focus traversal 键。focus traversal 键是按下后改变键盘焦点的键。例如,在Web页面上,按Tab键可在表单的元素之间切换。Tab键事件被AWT的focus traversal代码掩盖,但是,这里我们想要接收到Tab键事件。调用这个方法就可以了。
如果你想知道有哪些focus traversal 键,就调用getFocusTraversalKeys()方法。
Tab键不是唯一会引起奇怪现象的键。Alt键也会引起问题。在大多数系统上,Alt键用于激活记忆键,所谓记忆键就是特定的用户接口的快捷键。例如Alt+F激活大多数带有菜单条应用的文件菜单。AWT会认为在Alt键后按下的键是记忆的而忽略了这个键。为了避免这种情况,在KeyTest中用下面的代码防止KeyListener中的KeyEvent按照缺省的方式处理:
e.consume();
这确保没有其它对象处理Alt键,因此,Alt键就象其它键一样处理。
KeyTest还有一个作用,就是用来测试不同系统上键的反应。“哇!你是说在不同的系统上键输入有可能不同。”是的,确实如此。
让我们以重复按键为例。当用户按住一个键时,操作系统发出多个事件。例如,在文本编辑器中,当你按住Q键,就一直输入Q。在有些系统上(例如Linux)这样的操作会产生按下键和释放键事件。在另一些系统上(例如Windows)只产生按下键事件,用户释放了键后才产生释放键事件。
还有一些其它的细小差别,例如,不同版本的Java虚拟机的键事件也会有点不同。
幸运的是差别不大。大多数时候你不需管它。
鼠标输入
键盘只不过是排列在一起的一组键,但是,鼠标就复杂多了。鼠标不仅有键(有一键、两键、三键或更多键鼠标),还可以移动,可能还有滚轮。
也就是说,你可能收到三种类型的鼠标事件:
l 点击鼠标按键
l 滑动鼠标
l 转动鼠标滚轮
点击鼠标按键与按下键盘键相同,但是没有重复键。鼠标的位置用屏幕的x、y座标表示。鼠标的滚轮事件给出了滚轮转动了多少。
每一种鼠标事件都有自己的listener:MouseListener、MouseMotionListener和MouseWheelListener。它们都以MouseEvent作为参数。
象KeyListener一样,MouseListener接口的方法可以检测鼠标键的按下、释放和点击(按下,接着释放)。我们在游戏中不考虑点击,就象我们不考虑KeyTyped事件一样,只考虑按下和释放。可以调用MouseEvent的getButton()方法获得那个键被按下了或释放了。
MouseListener接口的方法还可以检测到鼠标进入或退出组件。因为我们所用的组件覆盖了整个屏幕,我们也不考虑这样的方法。
对于鼠标的移动,我们可以用MouseMotionListener接口检测到两种移动:通常的移动和拖动。当用户按住一个鼠标键同时移动鼠标时,就发生拖动事件。这两种移动都可以用MouseEvent的getX()和getY()方法获得当前的位置。
MouseWheelListener使用了MouseEvent的子类,MouseWheelEvent。它的getWheelRotation()方法检测鼠标滚轮转动了多少。负值表示向上转动,正值表示向下转动。
好了,已经介绍了鼠标输入基础。让我们编写一个程序试一试。
代码3.3中的MouseTest在鼠标所在的位置上画出“Hello World”。当点击鼠标时,画出最后10个鼠标的位置做成轨迹,鼠标改变到“轨迹模式”。转动鼠标的滚轮会改变文本的颜色。如前所述,按ESC键退出程序。
代码3.3 MouseTest.java
import java.awt.*;
import java.awt.event.*;
import java.util.LinkedList;
import com.brackeen.javagamebook.graphics.*;
import com.brackeen.javagamebook.test.GameCore;
/**
A simple mouse test. Draws a "Hello World!" message at
the location of the cursor. Click to change to "trail mode"
to draw several messages. Use the mouse wheel (if available)
to change colors.
*/
public class MouseTest extends GameCore implements KeyListener,
MouseMotionListener, MouseListener, MouseWheelListener
{
public static void main(String[] args) {
new MouseTest().run();
}
private static final int TRAIL_SIZE = 10;
private static final Color[] COLORS = {
Color.white, Color.black, Color.yellow, Color.magenta
};
private LinkedList trailList;
private boolean trailMode;
private int colorIndex;
public void init() {
super.init();
trailList = new LinkedList();
Window window = screen.getFullScreenWindow();
window.addMouseListener(this);
window.addMouseMotionListener(this);
window.addMouseWheelListener(this);
window.addKeyListener(this);
}
public synchronized void draw(Graphics2D g) {
int count = trailList.size();
if (count > 1 && !trailMode) {
count = 1;
}
Window window = screen.getFullScreenWindow();
// draw background
g.setColor(window.getBackground());
g.fillRect(0, 0, screen.getWidth(), screen.getHeight());
// draw instructions
g.setRenderingHint(
RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g.setColor(window.getForeground());
g.drawString("MouseTest. Press Escape to exit.", 5,
FONT_SIZE);
// draw mouse trail
for (int i=0; i<count; i++) {
Point p = (Point)trailList.get(i);
g.drawString("Hello World!", p.x, p.y);
}
}
// from the MouseListener interface
public void mousePressed(MouseEvent e) {
trailMode = !trailMode;
}
// from the MouseListener interface
public void mouseReleased(MouseEvent e) {
// do nothing
}
// from the MouseListener interface
public void mouseClicked(MouseEvent e) {
// called after mouse is released - ignore it
}
// from the MouseListener interface
public void mouseEntered(MouseEvent e) {
mouseMoved(e);
}
// from the MouseListener interface
public void mouseExited(MouseEvent e) {
mouseMoved(e);
}
// from the MouseMotionListener interface
public void mouseDragged(MouseEvent e) {
mouseMoved(e);
}
// from the MouseMotionListener interface
public synchronized void mouseMoved(MouseEvent e) {
Point p = new Point(e.getX(), e.getY());
trailList.addFirst(p);
while (trailList.size() > TRAIL_SIZE) {
trailList.removeLast();
}
}
// from the MouseWheelListener interface
public void mouseWheelMoved(MouseWheelEvent e) {
colorIndex = (colorIndex + e.getWheelRotation()) %
COLORS.length;
if (colorIndex < 0) {
colorIndex+=COLORS.length;
}
Window window = screen.getFullScreenWindow();
window.setForeground(COLORS[colorIndex]);
}
// from the KeyListener interface
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
// exit the program
stop();
}
}
// from the KeyListener interface
public void keyReleased(KeyEvent e) {
// do nothing
}
// from the KeyListener interface
public void keyTyped(KeyEvent e) {
// do nothing
}
}
MouseTest的代码简单易懂,没有什幺需要解释的。当鼠标移动时,就向trailList中增加一个新的Point对象。Point对象含有x和y的值。TrailList中最多可有10个Point。如果开启了轨迹模式,draw()方法就在trailList的每个Point的位置画出“Hello World”。否则,“Hello World”就只画在第一个Point的位置。点击鼠标切换轨迹模式。