通过第5章的学习知道,EventListener接口是所有具体事件监听器接口的父接口。但有一点要注意:我们将要讨论的监听器接口位于java.awt.event中,但是EventListener接口却来自于java.util包。
EventListener接口没有包含方法,它只是一个空的标志接口,所有的监听器必须从它派生。
为了帮助理解这个过程,我们做个类比。假设Joe拥有一家当铺,每天都有很多人进来看各种东西(比如家具,枪,以及其他任何东西)。星期二,Bob进来要找一只金表,由于手头没有任何货,Joe许诺当他接到金表时给Bob电话(Bob.addActionListener(Joe))。两个星期后,一个人要用他的金表交换一本少见的由W.H.Burge写的《递归编程技术》(事件触发)。Joe在获得金表之后马上给Bob打电话,告诉他到了一只金表。Bob马上赶到这家当铺来付账买下了这只金表。
6.1.1 MouseListener接口
MouseListener接口监听很多鼠标相关的事件,其中包括鼠标何时进入或者离开一个组件,何时鼠标的一个按键被按下并释放,何时一个鼠标按键被单击(被按下并被释放的组合)。
MouseListener接口定义了5个方法:mouseClicked,mouseEntered,mouseExited,mousePressed和mouseReleased,它们都是void方法,以单一的MouseEvent对象作为输入参数。记住,必须定义全部的5个方法,否则,类必须被声明为abstract。MouseListener接口并不跟踪像鼠标的显式移动这样的事件,如果需要这样做,以可参考后面的MouseMotionListener接口中。
6.1.2 MouseMotionListener接口
MouseMotionListener接口产生由鼠标移动引起的事件。它定义了两个方法:mouseDragged和mouseMoved,这两个方法都是void的,都以单一的MouseEvent作为输入参数。
下面的程序Scribble(乱涂),模仿了经典的GUI示例程序。注意ColorDescription类的使用,这里使用它来减少把Color和相应的String描述关联起来所需要的代码。为了使得程序更简单(但是更难修改和维护),可以不要ColorDescription类,而直接对所有的值进行硬编码。
import java.applet.*;
import java.awt.*;
import java.awt.event.*;
//ColorDescription类存储Color以及它的描述
//提供这个类完全是为了方便把一个Color对象和一个String对象关联起来
class ColorDescription{
//把一个文本字符串与一个Color关联,声明为公共是为了使用方便
public Color color;
public String text;
public ColorDescription(Color c,String s){
color=c;
text=s;
}
public boolean matches(String s){
return text.equals(s);
}
}
//让用户可以使用鼠标描绘点和线
//它还可以改变当前画笔的颜色
public class Scribble extends Applet implements ItemListener,ActionListener,
MouseListener,MouseMotionListener{
private Choice colorChooser;//储存颜色的下拉列表
private Color currentColor;//当前的画笔颜色
//一个数组,将Color类与对应文字关联
private final ColorDescription[] colors={
new ColorDescription(Color.BLACK,"黑色"),
new ColorDescription(Color.RED,"红色"),
new ColorDescription(Color.GREEN,"绿色"),
new ColorDescription(Color.BLUE,"蓝色"),
new ColorDescription(Color.YELLOW,"黄色"),
new ColorDescription(Color.ORANGE,"橙色"),
new ColorDescription(Color.GRAY,"灰色"),
new ColorDescription(Color.CYAN,"洋红色"),
};
//所绘制的线条的起点和终点
private Point p1;
private Point p2;
//重写init方法
public void init(){
p1=null;
p2=null;
this.setBackground(Color.white);
//记着注册自己来接收鼠标事件
addMouseListener(this);
addMouseMotionListener(this);
//把鼠标改得好看一些
setCursor(new Cursor(Cursor.CROSSHAIR_CURSOR));
currentColor=colors[0].color;
//创建一个包含applet命令的sidebar
createSidebar();
}
//重写Applet类的destroy方法
public void destroy(){
removeMouseListener(this);
removeMouseMotionListener(this);
colorChooser.removeItemListener(this);
}
//创建一个Frame,它包含颜色选项下位列表框和一个清除applet窗体的按钮
private void createSidebar(){
//对于颜色数组中的每一个ColorDesciption,提取文本
//并将它们加入选项列表
colorChooser=new Choice();
for(int i=0;i<colors.length;i++){
colorChooser.add(colors[i].text);
}
colorChooser.addItemListener(this);
//创建黑板擦按钮
Button b=new Button("黑板擦");
b.addActionListener(this);
//创建Frame
Frame f=new Frame();
f.setLocation(getX()+getSize().width+8,getY());
f.setLayout(new GridLayout(3,1,5,5));
f.setBackground(Color.YELLOW);
f.add(new Label("笔的颜色:",Label.CENTER));
f.add(colorChooser);
f.add(b);
f.pack();
f.show();
}
//使用当前画笔颜色绘制连接p1和p2
private void drawPoint(){
Graphics g=this.getGraphics();
g.setColor(currentColor);
g.drawLine(p1.x,p1.y,p2.x,p2.y);
}
//ActionListener的方法
//在工具条上的黑板擦按钮被单击时调用
public void actionPerformed(ActionEvent e){
repaint();
}
//ItemListener的方法
//在下拉列表框改变时调用
public void itemStateChanged(ItemEvent e){
String color=colorChooser.getSelectedItem();
//用当前的选项匹配画笔颜色
for(int i=0;i<colors.length;i++){
if(colors[i].matches(color)){
currentColor=colors[i].color;
break;
}
}
}
/**MouseMotionListener的方法*/
public void mouseMoved(MouseEvent e){
//Nothing
}
public void mouseDragged(MouseEvent e){
//如果点还没有设置,进行设置
if(p1==null){
p1=new Point(e.getX(),e.getY());
}
if(p2==null){
p2=new Point(e.getX(),e.getY());
}
//更新并绘制点
p1=(Point)p2.clone();
p2.x=e.getX();
p2.y=e.getY();
drawPoint();
}
/**MouseListener的方法*/
public void mouseClicked(MouseEvent e){
//Nothing
}
public void mouseEntered(MouseEvent e){
//Nothing
}
public void mouseExited(MouseEvent e){
//Nothing
}
//允许点在鼠标按钮按下时绘制,而不必拖动鼠标
public void mousePressed(MouseEvent e){
mouseDragged(e);
}
//释放点,允许不连续的绘制
public void mouseReleased(MouseEvent e){
p1=null;
p2=null;
}
}
注意:如果打算重用所写的任何类,可以总是把它放在一个属于它自己的文件中。但是记住,为了它能正确编译,必须声明这个类为public。这里把ColorDescription类加到了Scribble.java文件中,由于它可能会被再次使用,所以可以把ColorDescription类声明为public,并把它放在ColorDescription.java文件中。
还要注意一下Frame类打开一个分离窗口的用法。当想包含外部的控件或者选项但是又不想让它们挡住操作时,这样做是很方便的;当想为用户随时准备一个分离的菜单时可以试试使用Frame类。
6.1.3 KeyListever接口
这个接口需要实现三个方法:keyPressed,keyReleased和keyTyped,这些方法也是自明的。并且都是void方法,都以单一的KeyEvent对象作为参数输入。
KeyEvent类有一个特性,当判断哪一个键被按下时,可以检验实际的字符类型,或者是它实际的键码。当对实际产生的字符(比如“a”或“A”)有兴趣时,请使用getKeyChar()方法,这个方法返回char。如果不关心哪个键被输入,使用getKeyCode()方法,它返回一个int,例如用户按住Shift,然后再按住G时,getKeyChar会产生“G”(大写字母),而如果只有G被按下,则getKeyChar()返回的会是“g”(小写字母)。而两种情况,getKeyCode()都返回一个常量:VK_G。
所有的键码都以“VK_”开头,KeyEvent大概定义了200个实际键码,完整的列表可以在Java2文档中找到。
下面的applet—KeyTest演示了getKeyChar()方法和getKeyCode()方法的用法。在浏览这段源代码或者在文本编辑器中输入这些代码时,考虑一下两种方法各自的优点和缺点。
import java.applet.*;
import java.awt.*;
import java.awt.event.*;
/**
*这个applet让用户使用箭头键来移动一个黑色的矩形
*用户还可以用R,G,B,W键来改变applet的背景色
*它还允许用不同的方式来终止KeyEvent
*/
public class KeyTest extends Applet implements KeyListener{
//移动的黑色矩形
private Rectangle r;
//applet的当前背景色
private Color backColor;
//重写init方法
public void init(){
r=new Rectangle(0,0,50,30);
backColor=Color.WHITE;
this.addKeyListener(this);
}
//在更新后的位置绘制矩形
public void paint(Graphics g){
setBackground(backColor);
g.fillRect(r.x,r.y,r.width,r.height);
}
/**实现KeyListener接口中的方法*/
public void keyPressed(KeyEvent e){
//对于这个方法,用键码来产生移动
int keyCode=e.getKeyCode();
//移动矩形
if(keyCode==KeyEvent.VK_LEFT){
r.x-=5;
if(r.x<0){
r.x=0;
}
repaint();
}else if(keyCode==KeyEvent.VK_RIGHT){
r.x+=5;
if(r.x>getSize().width-r.width){
r.x=getSize().width-r.width;
}
repaint();
}else if(keyCode==KeyEvent.VK_UP){
r.y-=5;
if(r.y<0){
r.y=0;
}
repaint();
}else if(keyCode==KeyEvent.VK_DOWN){
r.y+=5;
if(r.y>getSize().height-r.height){
r.y=getSize().height-r.height;
}
repaint();
}
}
public void keyReleased(KeyEvent e){
//Nothing
}
public void keyTyped(KeyEvent e){
//对于这个方法,采用实际的键符来调用事件
char keyChar=e.getKeyChar();
//更改背景色
switch(keyChar){
case 'r':
backColor=Color.red;
repaint();
break;
case 'g':
backColor=Color.green;
repaint();
break;
case 'b':
backColor=Color.blue;
repaint();
break;
case 'w':
backColor=Color.white;
repaint();
break;
}
}
}
提醒一下,这个矩形不仅可以移动(使用方向键),而且可以把applet的背景色改为红色(r),绿色(g),蓝色(b)还有白色(w)。如果使用键盘不能在屏幕上移动矩形,只需要用鼠标单击这个applet的任何一个位置就可以了(我试过了,每次启动程序后,都需要单击一下,键盘才可以起作用)。
提示:不是所有的键盘都可以产生全部的实际键码,进一步说,不能尝试手工产生所有的这些键码。所以,如果正在尝试处理一些棘手的事情时,试试使用getKeyChar方法,很多游戏允许多个键盘动作产生相同的事件,在这种方式下,不会遗漏对任何人的考虑,还可以创建一个菜单允许用户指定哪一个键和哪一个功能关联,这完全取决于想要用户怎样与编写的游戏交互。
6.1.4 其他的EventListener类
在Java 2文档中查一下EventListener类,会得到它的子接口的一个完整列表。虽然EventListener类可能会减少一些工作量,但还可以在需要时写出自己的监听器。
注意:可以扩展和某个监听器接口等价的适配器类,来作为实现该监听器的一种变通方法。比如,可以不实现MouseListener接口,而扩展MouseAdapter,这样做的一个好处是可以只需覆盖和定义那些感兴趣的方法。记住,实现一个接口要求必须定义它所有的方法,然而,单根继承的原则规定只能直接继承一个类,但是,它有它的用处,就像其他很多事物一样,有某些优点也有某些缺点。
EventListener接口集是一个很好的例子,它们说明了一个接口应该做什么,那就是在两个组件之间提供一个桥梁。
本章讨论的这些监听器接口,鼠标和键盘监听器使用起来都是安全的,只要记住兼容性即可。要知道,由于Java是平台兼容的,总有一个好机会让世界某处的人来玩你的游戏。有时候,用户的键盘布局和你的不同。对于鼠标监听器,有各种不同类型的鼠标:三键鼠标很普遍,两键鼠标更普遍,而且甚至还有一些使用单键鼠标的系统,所以,编程要让几乎所有使用任何系统的任何人都能和你的系统交互,都能够使用你的系统。设计允许自定义的键盘鼠标配置的游戏是保证每一个人都能欣赏你的游戏的一个好的方式。
6.1写一个实现几个不同接口的applet。选择甚至可以在一个空的applet窗体接收事件的“基础”监听器,比如MouseListener,MouseMotionListener,KeyListener和WindowListener。对于接收到的每一个事件,通过调用该事件的toString方法,在标准输出中记录它,就像下面的代码:
public void mouseMoved(MouseEvnet e)
{
System.out.println(e);
}
6.2如果有兴趣,写出练习5.1所阐述的程序,把事件信息发送给TestArea而不是发送到标准输出。
6.3改进Scribble,写一个同时实现MouseListener和MouseMotionListener接口的applet。使用mouseReleased方法来允许画笔离开纸面,定义鼠标右键来随机改变画笔颜色(提示:使用一个随机数字生成器和Graphics类的setColor方法来改变颜色)。这个applet应该像原来的程序那样把光标设为十字光标。
6.4修改Scribble,让它允许用户修正画笔的宽度,应该使用另外一个文本标签和一个列有不同画笔宽度的下拉列表框,画笔的宽度最好为1~6像素,也可以按照自己的爱好来设置。如果不是很清楚怎么解决这个问题,阅读完第7章和第8章后再回来,解决方案就会变得清晰。