7. 事件处理
7.1. 事件处理基础
- 任何支持GUI的操作环境都要不断地监视按键或者点击鼠标这样的事件。操作环境将这些事件报告给正在运行的应用程序。如果有事件产生,每个应用程序将决定如何对它们做出响应。程序员对相关的特定事件编写代码,并将这些代码放置在过程中,通过人们将它们成为事件过程(event process)。
- Java AWT所知的事件范围内,完全可以控制事件从事件源(event source)例如,按钮或滚动条,到事件监听器(event listener)的传递过程,并将任何对象指派给事件监听器。
- 像Java这样的面向对象语言,都将事件的相关信息封装在一个事件对象(event object)中。在Java中,所有的事件对象都最终派生于java.util.EventObject类。
- 不同的事件源可以产生不同类别的事件。例如,按钮可以发送一个AcitonEvent对象,而窗口可以发送WindowEvent对象。
AWT事件处理机制概要:
- 监听器对象是一个实现了特定监听器接口(listener interface)的类的实例。
- 事件源是一个能够注册监听器对象并发送事件对象的对象。
- 当事件发生时,事件源将事件对象传递给所有注册的监听器。
- 监听器对象将利用事件对象中的信息决定如何对事件作出响应。
/* 事件委托模型(event delegation model)实例 */
//构造按钮->将按钮添加到面板上->构造监听器->添加动作监听器
public class ButtonFrame extends JFrame{
private JPanel buttonPanel;
private static final int DEFAULT_WIDTH=300;
private static final int DEFAULT_HEIGHT=200;
public ButtonFrame(){
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
JButton yellowButton=new JButton("yellow");
JButton redButton=new JButton("red");
JButton blueButton=new JButton("blue");
buttonPanel=new JPanel();
//把按钮添加到面板上
buttonPanel.add(blueButton);
buttonPanel.add(redButton);
buttonPanel.add(yellowButton);
//添加面板到框架里
add(buttonPanel);
//构造一个对象,并将对象设置为按钮监听器
ColorAction yellowAction=new ColorAction(Color.YELLOW);
ColorAction blueAction=new ColorAction(Color.blue);
ColorAction redAction=new ColorAction(Color.red);
//为按钮添加监听器
yellowButton.addActionListener(yellowAction);
blueButton.addActionListener(blueAction);
redButton.addActionListener(redAction);
}
//内部类。才能访问buttonPanel变量
private class ColorAction implements ActionListener{
private Color backgroundColor;
public ColorAction(Color c){
backgroundColor=c;
}
//实现ActionListener接口,监听器类必须有一个actionPerformed方法
public void actionPerformed(ActionEvent event){
buttonPanel.setBackground(backgroundColor);
}
}
}
创建一个包含方法调用的监听器
假设有一个标签为load的按钮,它的事件处理只包含下面一个方法调用:frame.loadData();
可以使用匿名内部类
loadButton.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent event){
frame.loadData();
}
});
改变观感
默认情况下,Swing程序使用Metal观感,可以采用两种方式改变观感。
- (没有成功)在Java安装的子目录jre/lib下有一个文件swing.properties.在这个文件中,将swing.defaultlaf设置为所希望的观感类名,如
swing.defaultlaf=com.sun.java.swing.plaf.motif.MotifLookAndFeel
。注意,Metal观感位于javax.swing包中。其他的观感包位于com.sun.java包中,并且不是在每个Java实现中都提供。 - 动态地改变观感。这需要调用静态的UIManager.setLookAndFeel方法,并提供所想要的观感类名,然后再调用静态方法SwingUtilities.updateComponentTreeUI刷新全部的组件集。
/* 改变观感 */
public class PlafFrame extends JFrame{
private JPanel buttonPanel;
public PlafFrame(){
buttonPanel=new JPanel();
//获取所有安装的观感实现
UIManager.LookAndFeelInfo[] infos=UIManager.getInstalledLookAndFeels();
for(UIManager.LookAndFeelInfo info: infos){
makeButton(info.getName(), info.getClassName());
}
add(buttonPanel);
pack();
}
void makeButton(String name,final String plafName){
JButton button=new JButton(name);
buttonPanel.add(button);
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
try {
//调用静态方法,设置观感
UIManager.setLookAndFeel(plafName);
//调用静态方法,刷新全部的组件集。这里需要向方法提供一个组件,并由此找到其他的所有组件
//外部对象的this引用必须将外部类名作为前缀
SwingUtilities.updateComponentTreeUI(PlafFrame.this);
pack();
} catch (ClassNotFoundException | InstantiationException
| IllegalAccessException
| UnsupportedLookAndFeelException e) {
e.printStackTrace();
}
}
});
}
}
适配器类
- 当用户试图关闭一个框架窗口时,JFrame对象就是WindowEvent的事件源。如果希望捕获这个事件,就必须有一个合适的监听器对象,将它添加到框架的窗口监听器列表中。
WindowListener listener=...;
frame.addWindowListener(listener);
- 窗口监听器必须是实现WindowListener接口的类的一个对象。在WindowListener接口中包含7个方法。当发生窗口事件时,框架将调用这些方法响应7个不同的事件。
- 在Java中,实现一个接口的任何类都必须实现其中所有的方法。在这里,意味着需要实现7个方法,然而只对名为windowClosing的方法感兴趣。可以在windowClosing方法增加对System.exit(0)的调用,其他6个方法不做任何事情。
- 书写6个没有任何操作的方法代码显然是一种乏味的工作。基于简化的目的,每个含有多个方法的AWT监听器接口都配有一个适配器(adapter)类,这个类实现了接口中所有的方法,但每个方法没有做任何事情。这意味着适配器类自动地满足了Java实现相关监听器接口的技术需求。可以通过扩展适配器类来制定对某些事件的相应动作,而不必实现接口中的每个方法(actionListener接口只有一个方法,因此没必要提供适配器类)。
/* 适配器使用实例 */
//最清晰简练的方法
frame.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent e){
...
}
});
7.2. 动作
Action接口
通常,激活一个命令可以有多种方式。Swing包提供了一种非常实用的机制来封装命令,并将它们连接到多个事件源,这就是Action接口。一个动作是一个封装下列内容的对象:命令的说明(一个文本字符串和一个可选图标) 和 执行命令所需要的参数。
Action接口的方法 | 功能 |
---|---|
actionPerformed(ActionEvent e) | 设置响应动作(Action接口扩展于ActionListener接口) |
setEnabled(boolean b) | 设置启用或禁用这个动作 |
isEnabled() | 检查动作当前是否被启用 |
putValue(String key,Object value) | 存储动作对象中的任意名/值 |
getValue(String key) | 检索动作对象中的任意名/值 |
addPropertyChangeListener(PropertyChangeListener listener) | 添加对象属性变化的监听器 |
removePropertyChangeListener(PropertyChangeListener listener) | 删除对象属性变化的监听器 |
Action是一个接口,而不是一个类。实现这个接口的所有类都必须实现7个方法。庆幸的是,有一个类实现了除actionPerformed方法之外的所有方法,它就是AbstractAction。这个类存储了所有名/值对,并管理着属性变更监听器。可以直接扩展AbstractAction类,并在扩展类中实现actionPerformed方法。
把动作和击键关联起来
- 要将动作对象添加到击键中,以便让用户敲击键盘命令来执行这项动作。为了将动作与击键关联起来,首先需要生成KeyStroke类对象。这个类封装了对键的说明。要想生成一个KeyStroke对象,不要调用构造器,而是调用KeyStroke类中的静态getKeyStroke方法:
KeyStroke ctrlKey=KeyStroke.getKeyStroke("ctrl B");
- 用户界面中包含许多按钮、菜单、滚动栏以及其他的组件。当用户敲击键盘时,这个动作会被发送给拥有焦点的组件。用户可以使用TAB键在组件之间移动焦点。当按下SPACE键时,就点击了拥有焦点的按钮。示例中不希望将击键发送给拥有焦点的组件。否则,每个按钮都需要知道如何处理CTRL+Y、CTRL+B、CTRL+R这些组合键。这是一个常见的问题,Swing设计者给出了一种很便捷的解决方案。每个JComponent有三个输入映射(input maps),每一个映射的KeyStroke对象都与动作关联。三个输入映射对应三个不同的条件。WHEN_FOCUSED:当这个组件拥有键盘焦点时激活。 WHEN_ANCESTOP_OF_FOCUSED_COMPONENT:当这个组件包含了拥有键盘焦点的组件时激活。WHEN_IN_FOCUSED_WINDOW:当这个组件被包含在一个拥有键盘焦点组件的窗口中时。可以使用getInputMap方法从组件中得到输入映射。
InputMap imap=panel.getInputMap(JComponent.WHEN_FOCUSED);
- InputMap不能直接地将KeyStroke对象映射到Action对象。而是先映射到任意对象上,然后由ActionMap类实现将对象映射到动作上的第2个映射。这样很容易实现来自不同输入映射的按键共享一个动作的目的。
- 习惯上,使用字符串none表示空动作。这样可以轻松地取消一个按键动作。
imap.put(KeyStroke.getKeyStroke("ctrl C"),"none");
总结:用同一个动作相应按钮、菜单项或按键
- 实现一个扩展于AbstractAction类的类。多个相关的动作可以使用同一个类。
- 构造一个动作类的对象。
- 使用动作对象创建按钮或菜单项。构造器将从动作对象中读取标签文本和图标。
- 为了能够通过按键触发动作,必须额外地执行几步操作。首先定位顶层窗口组件,如包含所有其他组件的面板。
- 然后得到顶层组件的WHEN_ANCESTOR_OF_FOCUSED_COMPONENT输入映射。为需要的按键创建一个KeyStroke对象。创建一个描述动作字符串这样的动作键对象。将(按键,动作键)对添加到输入映射中。
- 最后,得到顶层组件的动作映射。将(动作键,动作对象)添加到映射中。
public class ActionFrame extends JFrame{
private JPanel buttonPanel;
private static final int DEFAULT_WIDTH=300;
private static final int DEFAULT_HEIGHT=200;
public ActionFrame(){
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
buttonPanel=new JPanel();
Action yellowAction=new ColorAction("Yellow", new ImageIcon("yellow.gif"), Color.YELLOW);
Action blueAction=new ColorAction("Blue", new ImageIcon("blue.gif"), Color.BLUE);
Action redAction=new ColorAction("Red", new ImageIcon("red.gif"), Color.RED);
//用Action对象构造按钮,把动作和按钮关联起来
buttonPanel.add(new JButton(yellowAction));
buttonPanel.add(new JButton(blueAction));
buttonPanel.add(new JButton(redAction));
add(buttonPanel);
//得到顶层组件的WHEN_ANCESTOR_OF_FOCUSED_COMPONENT输入映射
InputMap imap=buttonPanel.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
//将(按键,动作键)添加到输入映射中
imap.put(KeyStroke.getKeyStroke("ctrl Y"), "panel.yellow");
imap.put(KeyStroke.getKeyStroke("ctrl B"), "panel.bule");
imap.put(KeyStroke.getKeyStroke("ctrl R"), "panel.red");
//得到顶层组件的动作映射
ActionMap amap=buttonPanel.getActionMap();
//将(动作键,动作对象)添加到映射中
amap.put("panel.yellow", yellowAction);
amap.put("panel.blue", blueAction);
amap.put("panel.red", redAction);
}
public class ColorAction extends AbstractAction{
public ColorAction(String name,Icon icon,Color c){
//存储命令的名称、图标、简要说明和需要的颜色
putValue(Action.NAME, name);
putValue(Action.SMALL_ICON, icon);
putValue(Action.SHORT_DESCRIPTION, "Set panel color to "+name.toLowerCase());//显示在工具提示里
putValue("color", c);
}
public void actionPerformed(ActionEvent arg0) {
Color c=(Color) getValue("color");
buttonPanel.setBackground(c);
}
}
}
7.3. 鼠标事件
- 如果只希望用户能够点击按钮或菜单,就不需要显式地处理鼠标事件。鼠标操作将由用户界面中的各种组件内部处理。然而,如果希望用户使用鼠标画图,就需要捕获鼠标移动点击和拖动事件。
- 本部分设计一个简单的图形编辑器应用程序,它允许用户在画布上放置、移动和擦除方块。
- 当用户点击鼠标按钮时,将会调用3个监听器方法:鼠标第一次被按下时调用mousePressed;鼠标被释放时调用mouseReleased;最后调用mouseClicked。如果只对最终的点击事件感兴趣,就可以忽略前两个方法。用MouseEvent类对象作为参数,调用getX和getY方法可以获得鼠标被按下时鼠标指针所在的x和y坐标。要想区分单击、双击和三击,需要使用getClickCount方法。
public class MouseFrame extends JFrame{
public MouseFrame(){
add(new MouseComponent());
pack();
}
}
class MouseComponent extends JComponent{
private static final int SIDELENGTH=10;
private ArrayList<Rectangle2D> squares;
private Rectangle2D current;
public MouseComponent(){
squares=new ArrayList<>();
current=null;
addMouseListener(new MouseHandler());
addMouseMotionListener(new MouseMotionHandler());
}
public void paintComponent(Graphics g){
Graphics2D g2=(Graphics2D)g;
for(Rectangle2D r:squares){
g2.draw(r);
}
}
public Rectangle2D find(Point2D p){
for(Rectangle2D r:squares){
if (r.contains(p)) return r;
}
return null;
}
public void add(Point2D p){
double x=p.getX();
double y=p.getY();
current=new Rectangle2D.Double(x-SIDELENGTH/2, y-SIDELENGTH/2, SIDELENGTH, SIDELENGTH);
squares.add(current);
repaint();
}
public void remove(Rectangle2D r){
if(r==null) return;
if(r==current) current=null;
squares.remove(r);
repaint();
}
//两个独立的接口MouseListener和MouseMotionListener,有利于提高效率。
//当用户移动鼠标时,只关心鼠标点击的监听器就不会被多余的鼠标移动所困扰。
private class MouseHandler extends MouseAdapter{
public void mousePressed(MouseEvent e){
//getPoint方法返回事件源组件左上角的x y坐标
//判断该处是否已经绘制图形
current=find(e.getPoint());
if (current==null) add(e.getPoint());
}
public void mouseClicked(MouseEvent e){
current=find(e.getPoint());
//双击鼠标,擦除方块
if(current!=null && e.getClickCount()>=2) remove(current);
}
}
private class MouseMotionHandler implements MouseMotionListener{
//移动鼠标的同时按下鼠标,调用mouseMoved
public void mouseMoved(MouseEvent e) {
//光标在一个小方块之上时变成另外一种形状(十字)
if(find(e.getPoint())==null) setCursor(Cursor.getDefaultCursor());//普通鼠标图标
else setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));//十字形图标
}
//更新光标位置
public void mouseDragged(MouseEvent e) {
if (current!=null) {
int x=e.getX();
int y=e.getY();
//设置形状坐标和大小
current.setFrame(x-SIDELENGTH/2, y-SIDELENGTH/2, SIDELENGTH, SIDELENGTH);
repaint();
}
}
}
}
7.4. AWT事件继承层次
Java事件处理采用的是面向对象方法,所有的时间都是由java.util包中EventObject类扩展而来。EventObject类有一个子类AWTEvent,它是所有AWT事件类的父类。
语义事件和低级事件
AWT将事件分成低级(low-level)时间和语义(semantic)事件。语义事件是表示用户动作的事件,如点击按钮,因此,ActionEvent是一种语义事件。低级事件是形成那些事件的事件。在点击按钮时,包含了按下鼠标、连续移动鼠标、抬起鼠标事件。调节滚动条是一种语义事件,但拖动鼠标是低级事件。
下面是java.awt.event包中最常用的语义事件类:
- ActionEvent(对应按钮点击、菜单选择、选择列表项或在文本框中ENTER)
- AdjustmentEvent(调节滚动条)
- ItemEvent(从复选框或列表框中选择一项)
常用的低级事件类是:
- KeyEvent(一个键被按下或释放)
- MouseEvent(鼠标键被按下、释放、移动或拖动)
- MouseWheelEvent(鼠标滚轮被转动)
- FocusEvent(某个组件获得焦点或失去焦点)
- WindowEvent(窗口状态被改变)
AWT监听器接口、事件和事件源:
接口 | 方法 | 参数/访问方法 | 事件源 |
---|---|---|---|
ActionListener | actionPerformed | ActionEvent -getActionCommand -getModifiers | AbstractButton JComboBox JTextField Timer |
AdjustmentListener | adjustmentValueChanged | AdjustmentEvent -getAdjustable -getAdjustmentType -getValue | JScrollbar |
ItemListener | itemStateChanged | ItemEvent -getItem -getItemSelectable -getStateChange | AbstractButton JComboBox |
FocusListener | focusGained focusLost | FocusEvent -isTemporary | Component |
KeyListener | keyPressed keyReleased keyTyped | KeyEvent -getKeyChar -getKeyCode -getKeyModifiersText -getKeyText -isActionKey | Component |
MouseListener | mousePressed mouseReleased mouseEntered mouseExited mouseClicked | MouseEvent -getClickCount -getX -getY -getPoint -transaltePoint | Component |
MouseMotionListener | mouseDragged mouseMoved | MouseEvent | Component |
MouseWheelListener | mouseWheelMoved | MouseWheelEvent -getWheelRotation -getScrollAmount | Component |
WindowListener | windowClosing windowOpened windowIconified windowDeiconified windowClosed windowActivated windowDeactiovated | WindowEvent -getWindow | Window |
WindowFocusListener | windowGainedFocus windowLostFocus | WindowEvent -getOppositeWindow | Window |
WindowStateListener | windowStateChanged | WindowEvent -getOldState -getNewState | Window |