目录
一、事件处理基础
事件源有一些向其注册事件监听器的方法。当某个事件源产生事件时, 事件源会向为事 件注册的所有事件监听器对象发送一个通告。
像 Java 这样的面向对象语言,都将事件的相关信息封装在一个事件对象(event object) 中。在 Java 中,所有的事件对象都最终派生于java.util.EventObject 类。当然,每个事件类型还有子类。
综上所述,下面给出 AWT 事件处理机制的概要:
- 监听器对象是一个实现了特定监听器接口(listener interface) 的类的实例
- 事件源是一个能够注册监听器对象并发送事件对象的对象
- 当事件发生时,事件源将事件对象传递给所有注册的监听器。
- 监听器对象将利用事件对象中的信息决定如何对事件做出响应
为了实现 ActionListener 接口,监听器类必须有一个被称为 actionPerformed 的方法,该 方法接收一个 ActionEvent 对象参数。只要用户点击按钮,JButton对象就会创建一个 ActionEvent 对象, 然后调用 listener, action Performed(event) 传递事件对象。可以将多个监听器对象添加到一个像按钮这样的事件源中。这样一来, 只要用户点击按钮,按钮就会调用所有监听器的 actionPerformed方法。
在按钮示例中, 使用的 ActionListener 接口并不仅 限于按钮点击事件。它可以应用于很多情况:
- 当采用鼠标双击的方式选择了列表框中的一个选项时;
- 当选择一个菜单项时;
- 当在文本域中按回车键时;
- 对于一个 Timer 组件来说, 当达到指定的时间间隔时。
1.1 示例
package button;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
/**
* A frame with a button panel
*/
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);
// create buttons
JButton yellowButton = new JButton("Yellow");
JButton blueButton = new JButton("Blue");
JButton redButton = new JButton("Red");
buttonPanel = new JPanel();
// add buttons to panel
buttonPanel.add(yellowButton);
buttonPanel.add(blueButton);
buttonPanel.add(redButton);
// add panel to frame
add(buttonPanel);
// create button actions
ColorAction yellowAction = new ColorAction(Color.YELLOW);
ColorAction blueAction = new ColorAction(Color.BLUE);
ColorAction redAction = new ColorAction(Color.RED);
// associate actions with buttons
yellowButton.addActionListener(yellowAction);
blueButton.addActionListener(blueAction);
redButton.addActionListener(redAction);
}
/**
* An action listener that sets the panel's background color.
*/
private class ColorAction implements ActionListener
{
private Color backgroundColor;
public ColorAction(Color c)
{
backgroundColor = c;
}
public void actionPerformed(ActionEvent event)
{
buttonPanel.setBackground(backgroundColor);
}
}
}
public class ButtonTest
{
public static void main(String[] args)
{
EventQueue.invokeLater(() -> {
JFrame frame = new ButtonFrame();
frame.setTitle("ButtonTest");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
});
}
}
1.2 简洁指定监听器
有些程序员不习惯使用内部类或lambda 表达式, 而更喜欢创建实现了 ActionListener 接口的事件源容器Q 然后这个容器再设置自身作为监听器。 如下:
blueButton.addActionListener(this);
redButton.addActionListener(this);
yellowButton.addActionListener(this);
现在这3 个按钮不再有单独的监听器。 它们共享一个监听器对象, 具体来讲就是框 架(frame)。 因此,actionPerformed 方法必须明确点击了哪个按钮:
class ButtonFrame extends JFrame implements ActionListener {
public void actionPerformed(ActionEvent event) {
Object source = event.getSource();
if (source == yellowButton)
...
else if (source = blueButton)
...
else if (source = redButton)
...
else
...
}
}
1.3 改变观感
在默认情况下,Swing 程序使用Metal 观感.可以采用两种方式改变观感。第一种方式 是在 Java 安装的子目录jre/lib下有一个文件 swing.properties。在这个文件中,将属性 swing, defaultlaf设置为所希望的观感类名。例如:
swing.defaultlaf=com.sun.java.swing.plaf.motif.MotifLookAndFeel
注意,Metal 和 Nimbus 观感位于javax.swing 包中。其他的观感包位于 com.sun.java 包 中, 并且不是在每个 Java 实现中都提供。现在, 鉴于版权的原因,Windows 和 Macintosh 的 观感包只与 Windows 和 Macintosh 版本的 Java运行时环境一起发布。采用这种方式开启观感时必须重新启动程序。Swing 程序只在启动时读取一次 swing. properties 文件。
下面程序时第二种方式:
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
/**
* A frame with a button panel for changing look-and-feel
*/
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();
}
/**
* Makes a button to change the pluggable look-and-feel.
* @param name the button name
* @param className the name of the look-and-feel class
*/
private void makeButton(String name, String className)
{
// add button to panel
JButton button = new JButton(name);
buttonPanel.add(button);
// set button action
button.addActionListener(event -> {
// button action: switch to the new look-and-feel
try
{
UIManager.setLookAndFeel(className);
SwingUtilities.updateComponentTreeUI(this);
pack();
}
catch (Exception e)
{
e.printStackTrace();
}
});
}
}
public class PlafTest
{
public static void main(String[] args)
{
EventQueue.invokeLater(() -> {
JFrame frame = new PlafFrame();
frame.setTitle("PlafTest");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
});
}
}
1.4 适配器类
窗口监听器必须是实现 WindowListener接口的类的一个对象。在 WindowListener接口 中包含 7个方法。当发生窗口事件时,框架将调用这些方法响应 7个不同的事件。从它们 的名字就可以得知其作用, 唯一的例外是在 Windows下,通常将 iconified (图标化)称为 minimized (最小化)。下面是完整的 WindowListener接口:
public interface WindowListener { void windowOpened(WindowEvent e); void windowClosing(WindowEvent e): void windowClosed(WindowEvent e); void windowlconified(WindowEvent e); void windowDeiconified(WindowEvent e); void windowActivated(WindowEvent e); void windowDeactivated(WindowEvent e); }
为了能够查看窗口是否被最大化, 需要安装 WindowStateListener 并覆盖 windowStateChanged 方法。
书写 6个没有任何操作的方法代码显然是一种乏味的工作。鉴于简化的目的,每个含有 多个方法的 AWT 监听器接口都配有一个适配器(adapter) 类,这个类实现了接口中的所有方 法, 但每个方法没有做任何事情。这意味着适配器类自动地满足了 Java 实现相关监听器接口 的技术需求。可以通过扩展适配器类来指定对某些事件的响应动作,而不必实现接口中的每 个方法(ActionListener 这样的接口只有一个方法,因此没必要提供适配器类)
class Terminator extends WindowAdapter { public void windowClosing(WindowEvent e) { if { System.exit(O); } } }
如果在扩展适配器类时将方法名拼写错了,编译器不会捕获到这个错误。例如, 如果在 WindowAdapter 类中定义一个windowIsClosing 方法, 就会得到一个包含8 个方 法的类,. 并且windowClosing 方法没有做任何事情。可以使用 @Override 注角(在第 5 章已经介绍过)避免这种错误。
二、动作
通常, 激活一个命令可以有多种方式。用户可以通过菜单、击键或工具栏上的按钮选择 特定的功能。在 AWT 事件模型中实现这些非常容易:将所有事件连接到同一个监听器上。 例如, 假设 blueAction是一个动作监听器,它的 actionPerformed方法可以将背景颜色改变成 蓝色。将一个监听器对象加到下面几个事件源上:
- 标记为 Blue 的工具栏按钮
- 标记为 Blue 的菜单项
- 按键 CTRL+B
Swing包提供了一种非常实用的机制来封装命令,并将它们连接到多个事件源,这就是 Action接口。一个动作是一个封装下列内容的对象:
- 命令的说明(一个文本字符串和一个可选图标);
- 执行命令所需要的参数(例如,在列举的例子中请求改变的颜色)。
Action接口包含下列方法:
void actionPerformed(ActionEvent event); void setEnabled(boolean b); boolean isEnabled(); void putValue(String key, Object value) ; Object getValue(String key); void addPropertyChangeListener(PropertyChangeListener listener); void removePropertyChangeListener(PropertyChangeListener listener);
Action接口扩展于 Action Listener接口,因此,可以在任何需要 ActionListener对象的地方使用 Action对象。接下来的两个方法允许启用或禁用这个动作,并检査这个动作当前是否启用。当一个连 接到菜单或工具栏上的动作被禁用时, 这个选项就会变成灰色。
putValue 和 getvalue方法允许存储和检索动作对象中的任意名 / 值。有两个重要的预定 义字符串:Action.NAME 和 Action.SMALL_ICON,用于将动作的名字和图标存储到一个动 作对象中:
Action接口的最后两个方法能够让其他对象在动作对象的属性发生变化时得到通告,尤 其是菜单或工具栏触发的动作。
需要注意,Action 是一个接口, 而不是一个类。实现这个接口的所有类都必须实现刚才 讨论的 7个方法。庆幸的是,有一个类实现了这个接口除actionPerformed方法之外的所有方法,它就是 AbstractAction。这个类存储了所有名 / 值对, 并管理着属性变更监听器。我们可 以直接扩展 AbstractAction 类,并在扩展类中实现actionPerformed方法。
InputMap不能直接地将 Keystroke对象映射到 Action对象。而是先映射到任意对象上, 然后由 ActionMap类实现将对象映射到动作上的第 2 个映射。
下面总结一下用同一个动作响应按钮、菜单项或按键的方式:
- 1 ) 实现一个扩展于 AbstractAction类的类。多个相关的动作可以使用同一个类。
- 2 ) 构造一个动作类的对象。
- 3 ) 使用动作对象创建按钮或菜单项。构造器将从动作对象中读取标签文本和图标。
- 4 ) 为了能够通过按键触发动作, 必须额外地执行几步操作。首先定位顶层窗口组件, 例如,包含所有其他组件的面板。
- 5 ) 然后, 得到顶层组件的 WHEN_ANCESTOR_OF_FOCUS_COMPONENT输入映射。 为需要的按键创建一个 KeyStrike 对象。创建一个描述动作字符串这样的动作键对象。将(按 键, 动作键)对添加到输人映射中。
- 6 ) 最后,得到顶层组件的动作映射。将(动作键,动作对象)添加到映射中。
package action;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
/**
* A frame with a pAanel that demonstrates color change actions.
*/
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();
// define actions
Action yellowAction = new ColorAction("Yellow", new ImageIcon("yellow-ball.gif"),
Color.YELLOW);
Action blueAction = new ColorAction("Blue", new ImageIcon("blue-ball.gif"), Color.BLUE);
Action redAction = new ColorAction("Red", new ImageIcon("red-ball.gif"), Color.RED);
// add buttons for these actions
buttonPanel.add(new JButton(yellowAction));
buttonPanel.add(new JButton(blueAction));
buttonPanel.add(new JButton(redAction));
// add panel to frame
add(buttonPanel);
// associate the Y, B, and R keys with names
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.blue");
imap.put(KeyStroke.getKeyStroke("ctrl R"), "panel.red");
// associate the names with actions
ActionMap amap = buttonPanel.getActionMap();
amap.put("panel.yellow", yellowAction);
amap.put("panel.blue", blueAction);
amap.put("panel.red", redAction);
}
public class ColorAction extends AbstractAction
{
/**
* Constructs a color action.
* @param name the name to show on the button
* @param icon the icon to display on the button
* @param c the background color
*/
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 event)
{
Color c = (Color) getValue("color");
buttonPanel.setBackground(c);
}
}
}
package action;
import java.awt.*;
import javax.swing.*;
/**
* @version 1.34 2015-06-12
* @author Cay Horstmann
*/
public class ActionTest
{
public static void main(String[] args)
{
EventQueue.invokeLater(() -> {
JFrame frame = new ActionFrame();
frame.setTitle("ActionTest");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
});
}
}
三、鼠标事件
如果只希望用户能够点击按钮或菜单,那么就不需要显式地处理鼠标事件。鼠标操作将由用户界面中的各种组件内部处理。然而, 如果希望用户使用鼠标画图, 就需要捕获鼠标移动点击和拖动事件。
当用户点击鼠标按钮时, 将会调用三个监听器方法: 鼠标第一次被按下时调用mousePressed ; 鼠标被释放时调用mouseReleased; 最后调用 mouseClicked。
当用户点击鼠标按钮时, 将会调用三个监听器方法: 鼠 标第一次被按下时调用mousePressed ; 鼠标被释放时调用 mouseReleased; 最后调用 mouseClicked。如果只对最终的点 击事件感兴趣, 就可以忽略前两个方法。用 MouseEvent 类 对象作为参数, 调用 getX 和 getY 方法可以获得鼠标被按下 时鼠标指针所在的 x 和 y 坐标。要想区分单击、 双击和三击(! ) ,需要使用 getClickCount 方法。
可以采用位掩码来测试已经设置了哪个修饰符:
BUTT0N2_D0WN_MASK
BUTT0N3_D0WN_MASK
SHIFT_DOWN_MASK
CTRL_DOWN_MASK
ALT_DOWN_MASK
ALT_CRAPH_DOWN_MASK
META_DOWN_MASK
getModifiersEx方法能够准确地报告鼠标事件的鼠标按钮和键盘修饰符。 需要注意,在 Windows 环境下, 使用 BUTT0N3_D0WN_MASK 检测鼠标右键(非主要 的)的状态。例如,可以使用下列代码检测鼠标右键是否被按下:
if ((event.getModifiersEx() & InputEvent.BUTTON3_D0WN_MASK) != 0) . . . II code for right click
当鼠标在窗口上移动时, 窗口将会收到一连串的鼠标移动事件。请注意:有两个独立的 接口 MouseListener 和 MouseMotionListener。这样做有利于提高效率。当用户移动鼠标时, 只关心鼠标点击 (clicks) 的监听器就不会被多余的鼠标移动 (moves) 所困扰。
列出了在 Windows环境下鼠标的形状和方法对应的常量:
还可以利用 Toolkit 类中的 createCustomCursor 方法自定义光标类型:
Toolkit tk = Toolkit.getDefaultToolkit(); Image img = tk.getlmage("dynamite.gif"); Cursor dynamiteCursor = tk.createCustomCijrsor(img, new Point(10, 10), "dynamite stick");
如果用户在移动鼠标的同时按下鼠标 , 就会调用mouseMoved 而不是调用 mouseDmgged。在测试应用程序中
只有鼠标在一个组件内部停留才会调用 mouseMoved 方法。然而, 即使鼠标拖动 到组件外面, mouseDragged 方法也会被调用。还有两个鼠标事件方法: mouseEntered 和 mouseExited。这两个方法是在鼠标进入或移出组件时被调用.
package mouse; import java.awt.*; import java.awt.event.*; import java.awt.geom.*; import java.util.*; import javax.swing.*; /** * A component with mouse operations for adding and removing squares. */ public class MouseComponent extends JComponent { private static final int DEFAULT_WIDTH = 300; private static final int DEFAULT_HEIGHT = 200; private static final int SIDELENGTH = 10; private ArrayList<Rectangle2D> squares; private Rectangle2D current; // the square containing the mouse cursor public MouseComponent() { squares = new ArrayList<>(); current = null; addMouseListener(new MouseHandler()); addMouseMotionListener(new MouseMotionHandler()); } public Dimension getPreferredSize() { return new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT); } public void paintComponent(Graphics g) { Graphics2D g2 = (Graphics2D) g; // draw all squares for (Rectangle2D r : squares) g2.draw(r); } /** * Finds the first square containing a point. * @param p a point * @return the first square that contains p */ public Rectangle2D find(Point2D p) { for (Rectangle2D r : squares) { if (r.contains(p)) return r; } return null; } /** * Adds a square to the collection. * @param p the center of the square */ 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(); } /** * Removes a square from the collection. * @param s the square to remove */ public void remove(Rectangle2D s) { if (s == null) return; if (s == current) current = null; squares.remove(s); repaint(); } private class MouseHandler extends MouseAdapter { public void mousePressed(MouseEvent event) { // add a new square if the cursor isn't inside a square current = find(event.getPoint()); if (current == null) add(event.getPoint()); } public void mouseClicked(MouseEvent event) { // remove the current square if double clicked current = find(event.getPoint()); if (current != null && event.getClickCount() >= 2) remove(current); } } private class MouseMotionHandler implements MouseMotionListener { public void mouseMoved(MouseEvent event) { // set the mouse cursor to cross hairs if it is inside // a rectangle if (find(event.getPoint()) == null) setCursor(Cursor.getDefaultCursor()); else setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR)); } public void mouseDragged(MouseEvent event) { if (current != null) { int x = event.getX(); int y = event.getY(); // drag the current rectangle to center it at (x, y) current.setFrame(x - SIDELENGTH / 2, y - SIDELENGTH / 2, SIDELENGTH, SIDELENGTH); repaint(); } } } } package mouse; import javax.swing.*; /** * A frame containing a panel for testing mouse operations */ public class MouseFrame extends JFrame { public MouseFrame() { add(new MouseComponent()); pack(); } }
四、AWT事件继承层次
有些 Swing组件将生成其他事件类型的事件对象;它们都直接扩展于 EventObject, 而不 是 AWTEvent。
AWT将事件分为底层(low-level) 事件和语义(semantic) 事件。语义事件是表示用户动 作的事件, 例如,点击按钮;因此,ActionEvent是一种语义事件。底层事件是形成那些事件 的事件。在点击按钮时,包含了按下鼠标、连续移动鼠标、 抬起鼠标(只有鼠标在按钮区中 抬起才引发)事件。或者在用户利用TAB 键选择按钮,并利用空格键激活它时,发生的敲击 键盘事件。同样,调节滚动条是一种语义事件,但拖动鼠标是底层事件
下面是 java.awt.event 包中最常用的语义事件类:
- •ActionEvent (对应按钮点击、 菜单选择、选择列表项或在文本框中 ENTER);
- •AdjustmentEvent (用户调节滚动条);
- •ItemEvem (用户从复选框或列表框中选择一项) 。
常用的 5 个底层事件类是:
- •KeyEvent (一个键被按下或释放);
- •MouseEvent (鼠标键被按下、 释放、 移动或拖动);
- •MouseWheelEvent (鼠标滚轮被转动);
- •FocusEvent (某个组件获得焦点或失去焦点);
- •WindowEvent (窗口状态被改变)