13.16 新型AWT
在Java 1.1中一个显著的改变就是完善了新AWT的创新。大多数的改变围绕在Java 1.1中使用的新事件模型:老的事件模型是糟糕的、笨拙的、非面向对象的,而新的事件模型可能是我所见过的最优秀的。难以理解一个如此糟糕的(老的AWT)和一个如此优秀的(新的事件模型)程序语言居然出自同一个集团之手。新的考虑事件的方法看来中止了,因此争议不再变成障碍,从而轻易进入我们的意识里;相反,它是一个帮助我们设计系统的工具。它同样是Java Beans的精华,我们会在本章后面部分进入讲述。
新的方法设计对象做为“事件源”和“事件接收器”以代替老AWT的非面向对象串联的条件语句。正象我们将看到的内部类的用途是集成面向对象的原始状态的新事件。另外,事件现在被描绘为在一个类体系以取代单一的类并且我们可以创建自己的事件类型。
我们同样会发现,如果我们采用老的AWT编程,Java 1.1版会产生一些看起来不合理的名字转换。例如,setsize()改成resize()。当我们学习Java Beans时这会变得更加的合理,因为Beans使用一个独特的命名协议。名字必须被修改以在Beans中产生新的标准AWT组件。
剪贴板操作在Java 1.1版中也得到支持,尽管拖放操作“将在新版本中被支持”。我们可能访问桌面色彩组织,所以我们的Java可以同其余桌面保持一致。可以利用弹出式菜单,并且为图像和图形作了改进。也同样支持鼠标操作。还有简单的为打印的API以及简单地支持滚动。
13.16.1 新的事件模型
在新的事件模型的组件可以开始一个事件。每种类型的事件被一个个别的类所描绘。当事件开始后,它受理一个或更多事件指明“接收器”。因此,事件源和处理事件的地址可以被分离。
每个事件接收器都是执行特定的接收器类型接口的类对象。因此作为一个程序开发者,我们所要做的是创建接收器对象并且在被激活事件的组件中进行注册。event-firing组件调用一个addXXXListener()方法来完成注册,以描述XXX事件类型接受。我们可以容易地了解到以addListened名的方法通知我们任何的事件类型都可以被处理,如果我们试图接收事件我们会发现编译时我们的错误。Java Beans同样使用这种addListener名的方法去判断那一个程序可以运行。
我们所有的事件逻辑将装入到一个接收器类中。当我们创建一个接收器类时唯一的一点限制是必须执行专用的接口。我们可以创建一个全局接收器类,这种情况在内部类中有助于被很好地使用,不仅仅是因为它们提供了一个理论上的接收器类组到它们服务的UI或业务逻辑类中,但因为(正像我们将会在本章后面看到的)事实是一个内部类维持一个引用到它的父对象,提供了一个很好的通过类和子系统边界的调用方法。
一个简单的例子将使这一切变得清晰明确。同时思考本章前部Button2.java例子与这个例子的差异。
//: Button2New.java// Capturing button pressesimport java.awt.*;import java.awt.event.*; // Must add thisimport java.applet.*;public class Button2New extends Applet {Buttonb1 = new Button("Button 1"),b2 = new Button("Button 2");public void init() {b1.addActionListener(new B1());b2.addActionListener(new B2());add(b1);add(b2);}class B1 implements ActionListener {public void actionPerformed(ActionEvent e) {getAppletContext().showStatus("Button 1");}}class B2 implements ActionListener {public void actionPerformed(ActionEvent e) {getAppletContext().showStatus("Button 2");}}/* The old way:public boolean action(Event evt, Object arg) {if(evt.target.equals(b1))getAppletContext().showStatus("Button 1");else if(evt.target.equals(b2))getAppletContext().showStatus("Button 2");// Let the base class handle it:elsereturn super.action(evt, arg);return true; // We've handled it here}*/} ///:~
我们可比较两种方法,老的代码在左面作为注解。在init()方法里,只有一个改变就是增加了下面的两行:
b1.addActionListener(new B1());b2.addActionListener(new B2());
按钮按下时,addActionListener()通知按钮对象被激活。B1和B2类都是执行接口ActionListener的内部类。这个接口包括一个单一的方法actionPerformed()(这意味着当事件激活时,这个动作将被执行)。注意actionPreformed()方法不是一个普通事件,说得更恰当些是一个特殊类型的事件,ActionEvent。如果我们想提取特殊ActionEvent的信息,因此我们不需要故意去测试和向下转换参数。
对编程者来说一个最好的事便是actionPerformed()十分的简单易用。它是一个可以调用的方法。同老的action()方法比较,老的方法我们必须指出发生了什么和适当的动作,同样,我们会担心调用基类action()的版本并且返回一个值去指明是否被处理。在新的事件模型中,我们知道所有事件测试推理自动进行,因此我们不必指出发生了什么;我们刚刚表示发生了什么,它就自动地完成了。如果我们还没有提出用新的方法覆盖老的方法,我们会很快提出。
13.16.2 事件和接收者类型
所有AWT组件都被改变成包含addXXXListener()和removeXXXListener()方法,因此特定的接收器类型可从每个组件中增加和删除。我们会注意到XXX在每个场合中同样表示参数的方法,例如,addFooListener(FooListener fl)。下面这张表格总结了通过提供addXXXListener()和removeXXXListener()方法,从而支持那些特定事件的相关事件、接收器、方法以及组件。
- 事件,接收器接口及添加和删除方法
- 支持这个事件的组件
ActionEventActionListeneraddActionListener( )removeActionListener( )Button,List,TextField,MenuItem, and its derivatives includingCheckboxMenuItem,Menu, andPopupMenu
AdjustmentEventAdjustmentListeneraddAdjustmentListener( )removeAdjustmentListener( )Scrollbar- Anything you create that implements the
Adjustableinterface
- Anything you create that implements the
ComponentEventComponentListeneraddComponentListener( )removeComponentListener( )Componentand its derivatives, includingButton,Canvas,Checkbox,Choice,Container,Panel,Applet,ScrollPane,Window,Dialog,FileDialog,Frame,Label,List,Scrollbar,TextArea, andTextField
ContainerEventContainerListeneraddContainerListener( )removeContainerListener( )Containerand its derivatives, includingPanel,Applet,ScrollPane,Window,Dialog,FileDialog, andFrame
FocusEventFocusListeneraddFocusListener( )removeFocusListener( )Componentand its derivatives, includingButton,Canvas,Checkbox,Choice,Container,Panel,Applet,ScrollPane,Window,Dialog,FileDialog,Frame ``Label,List,Scrollbar,TextArea,and ``TextField
KeyEventKeyListeneraddKeyListener( )removeKeyListener( )Componentand its derivatives, includingButton,Canvas,Checkbox,Choice,Container,Panel,Applet,ScrollPane,Window,Dialog,FileDialog,Frame,Label,List,Scrollbar,TextArea, andTextField
MouseEvent(for ``both ``clicks ``and ``motion)MouseListeneraddMouseListener( )removeMouseListener( )Componentand its derivatives, includingButton,Canvas,Checkbox,Choice,Container,Panel,Applet,ScrollPane,Window,Dialog,FileDialog,Frame,Label,List,Scrollbar,TextArea, andTextField
MouseEvent[55] (for ``both ``clicks ``and ``motion)MouseMotionListeneraddMouseMotionListener( )removeMouseMotionListener( )Componentand its derivatives, includingButton,Canvas,Checkbox,Choice,Container,Panel,Applet,ScrollPane,Window,Dialog,FileDialog,Frame,Label,List,Scrollbar,TextArea, andTextField
WindowEventWindowListeneraddWindowListener( )removeWindowListener( )Windowand its derivatives, includingDialog,FileDialog, andFrame
ItemEventItemListeneraddItemListener( )removeItemListener( )Checkbox,CheckboxMenuItem,Choice,List, and anything that implements theItemSelectableinterface
TextEventTextListeneraddTextListener( )removeTextListener( )- Anything derived from
TextComponent, includingTextAreaandTextField
- Anything derived from
⑤:即使表面上如此,但实际上并没有MouseMotiionEvent(鼠标运动事件)。单击和运动都组合到MouseEvent里,所以MouseEvent在表格中的这种另类行为并非一个错误。
可以看到,每种类型的组件只为特定类型的事件提供了支持。这有助于我们发现由每种组件支持的事件,如下表所示:
- 组件类型
- 支持的事件
AdjustableAdjustmentEvent
AppletContainerEvent,FocusEvent,KeyEvent,MouseEvent,ComponentEvent
ButtonActionEvent,FocusEvent,KeyEvent,MouseEvent,ComponentEvent
CanvasFocusEvent,KeyEvent,MouseEvent,ComponentEvent
CheckboxItemEvent,FocusEvent,KeyEvent,MouseEvent,ComponentEvent
CheckboxMenuItemActionEvent,ItemEvent
ChoiceItemEvent,FocusEvent,KeyEvent,MouseEvent,ComponentEvent
ComponentFocusEvent,KeyEvent,MouseEvent,ComponentEvent
ContainerContainerEvent,FocusEvent,KeyEvent,MouseEvent,ComponentEvent
DialogContainerEvent,WindowEvent,FocusEvent,KeyEvent,MouseEvent,ComponentEvent
FileDialogContainerEvent,WindowEvent,FocusEvent,KeyEvent,MouseEvent,ComponentEvent
FrameContainerEvent,WindowEvent,FocusEvent,KeyEvent,MouseEvent,ComponentEvent
LabelFocusEvent,KeyEvent,MouseEvent,ComponentEvent
ListActionEvent,FocusEvent,KeyEvent,MouseEvent,ItemEvent,ComponentEvent
MenuActionEvent
MenuItemActionEvent
PanelContainerEvent,FocusEvent,KeyEvent,MouseEvent,ComponentEvent
PopupMenuActionEvent
ScrollbarAdjustmentEvent,FocusEvent,KeyEvent,MouseEvent,ComponentEvent
ScrollPaneContainerEvent,FocusEvent,KeyEvent,MouseEvent,ComponentEvent
TextAreaTextEvent,FocusEvent,KeyEvent,MouseEvent,ComponentEvent
TextComponentTextEvent,FocusEvent,KeyEvent,MouseEvent,ComponentEvent
TextFieldActionEvent,TextEvent,FocusEvent,KeyEvent,MouseEvent,ComponentEvent
WindowContainerEvent,WindowEvent,FocusEvent,KeyEvent,MouseEvent,ComponentEvent
一旦知道了一个特定的组件支持哪些事件,就不必再去寻找任何东西来响应那个事件。只需简单地:
(1) 取得事件类的名字,并删掉其中的Event字样。在剩下的部分加入Listener字样。这就是在我们的内部类里需要实现的接收器接口。
(2) 实现上面的接口,针对想要捕获的事件编写方法代码。例如,假设我们想捕获鼠标的移动,所以需要为MouseMotiionListener接口的mouseMoved()方法编写代(当然还必须实现其他一些方法,但这里有捷径可循,马上就会讲到这个问题)。
(3) 为步骤2中的接收器类创建一个对象。随自己的组件和方法完成对它的注册,方法是在接收器的名字里加入一个前缀add。比如addMouseMotionListener()。
下表是对接收器接口的一个总结:
- 接收器接口
- 接口中的方法
ActionListeneractionPerformed(ActionEvent)
AdjustmentListeneradjustmentValueChanged(AdjustmentEvent)
ComponentListenerComponentAdaptercomponentHidden(ComponentEvent)componentShown(ComponentEvent)componentMoved(ComponentEvent)componentResized(ComponentEvent)
ContainerListenerContainerAdaptercomponentAdded(ContainerEvent)componentRemoved(ContainerEvent)
FocusListenerFocusAdapterfocusGained(FocusEvent)focusLost(FocusEvent)
KeyListenerKeyAdapterkeyPressed(KeyEvent)keyReleased(KeyEvent)keyTyped(KeyEvent)
MouseListenerMouseAdaptermouseClicked(MouseEvent)mouseEntered(MouseEvent)mouseExited(MouseEvent)mousePressed(MouseEvent)mouseReleased(MouseEvent)
MouseMotionListenerMouseMotionAdaptermouseDragged(MouseEvent)mouseMoved(MouseEvent)
WindowListenerWindowAdapterwindowOpened(WindowEvent)windowClosing(WindowEvent)windowClosed(WindowEvent)windowActivated(WindowEvent)windowDeactivated(WindowEvent)windowIconified(WindowEvent)windowDeiconified(WindowEvent)
ItemListeneritemStateChanged(ItemEvent)
TextListenertextValueChanged(TextEvent)
(1) 用接收器适配器简化操作
在上面的表格中,我们可以注意到一些接收器接口只有唯一的一个方法。它们的执行是无轻重的,因为我们仅当需要书写特殊方法时才会执行它们。然而,接收器接口拥有多个方法,使用起来却不太友好。例如,我们必须一直运行某些事物,当我们创建一个应用程序时对帧提供一个WindowListener,以便当我们得到windowClosing()事件时可以调用System.exit(0)以退出应用程序。但因为WindowListener是一个接口,我们必须执行其它所有的方法即使它们不运行任何事件。这真令人讨厌。
为了解决这个问题,每个拥有超过一个方法的接收器接口都可拥有适配器,它们的名我们可以在上面的表格中看到。每个适配器为每个接口方法提供默认的方法。(WindowAdapter的默认方法不是windowClosing(),而是System.exit(0)方法。)此外我们所要做的就是从适配器处继承并重载唯一的需要变更的方法。例如,典型的WindowListener我们会像下面这样的使用。
class MyWindowListener extends WindowAdapter {public void windowClosing(WindowEvent e) {System.exit(0);}}
适配器的全部宗旨就是使接收器的创建变得更加简便。
但所谓的“适配器”也有一个缺点,而且较难发觉。假定我们象上面那样写一个WindowAdapter:
class MyWindowListener extends WindowAdapter {public void WindowClosing(WindowEvent e) {System.exit(0);}}
表面上一切正常,但实际没有任何效果。每个事件的编译和运行都很正常——只是关闭窗口不会退出程序。您注意到问题在哪里吗?在方法的名字里:是WindowClosing(),而不是windowClosing()。大小写的一个简单失误就会造成一个崭新的方法。但是,这并非我们关闭窗口时调用的方法,所以当然没有任何效果。
13.16.3 用Java 1.1 AWT制作窗口和程序片
我们经常都需要创建一个类,使其既可作为一个窗口调用,亦可作为一个程序片调用。为做到这一点,只需为程序片简单地加入一个main()即可,令其在一个Frame(帧)里构建程序片的一个实例。作为一个简单的示例,下面让我们来看看如何对Button2New.java作一番修改,使其能同时作为应用程序和程序片使用:
//: Button2NewB.java// An application and an appletimport java.awt.*;import java.awt.event.*; // Must add thisimport java.applet.*;public class Button2NewB extends Applet {Buttonb1 = new Button("Button 1"),b2 = new Button("Button 2");TextField t = new TextField(20);public void init() {b1.addActionListener(new B1());b2.addActionListener(new B2());add(b1);add(b2);add(t);}class B1 implements ActionListener {public void actionPerformed(ActionEvent e) {t.setText("Button 1");}}class B2 implements ActionListener {public void actionPerformed(ActionEvent e) {t.setText("Button 2");}}// To close the application:static class WL extends WindowAdapter {public void windowClosing(WindowEvent e) {System.exit(0);}}// A main() for the application:public static void main(String[] args) {Button2NewB applet = new Button2NewB();Frame aFrame = new Frame("Button2NewB");aFrame.addWindowListener(new WL());aFrame.add(applet, BorderLayout.CENTER);aFrame.setSize(300,200);applet.init();applet.start();aFrame.setVisible(true);}} ///:~
内部类WL和main()方法是加入程序片的唯一两个元素,程序片剩余的部分则原封未动。事实上,我们通常将WL类和main()方法做一结小的改进复制和粘贴到我们自己的程序片里(请记住创建内部类时通常需要一个外部类来处理它,形成它静态地消除这个需要)。我们可以看到在main()方法里,程序片明确地初始化和开始,因为在这个例子里浏览器不能为我们有效地运行它。当然,这不会提供全部的浏览器调用stop()和destroy()的行为,但对大多数的情况而言它都是可接受的。如果它变成一个麻烦,我们可以:
(1) 使程序片引用为一个静态类(以代替局部可变的main()),然后:
(2) 在我们调用System.exit()之前在WindowAdapter.windowClosing()中调用applet.stop()和applet.destroy()。
注意最后一行:
aFrame.setVisible(true);
这是Java 1.1 AWT的一个改变。show()方法不再被支持,而setVisible(true)则取代了show()方法。当我们在本章后面部分学习Java Beans时,这些表面上易于改变的方法将会变得更加的合理。
这个例子同样被使用TextField修改而不是显示到控制台或浏览器状态行上。在开发程序时有一个限制条件就是程序片和应用程序我们都必须根据它们的运行情况选择输入和输出结构。
这里展示了Java 1.1 AWT的其它小的新功能。我们不再需要去使用有错误倾向的利用字符串指定BorderLayout定位的方法。当我们增加一个元素到Java 1.1版的BorderLayout中时,我们可以这样写:
aFrame.add(applet, BorderLayout.CENTER);
我们对位置规定一个BorderLayout的常数,以使它能在编译时被检验(而不是对老的结构悄悄地做不合适的事)。这是一个显著的改善,并且将在这本书的余下部分大量地使用。
(2) 将窗口接收器变成匿名类
任何一个接收器类都可作为一个匿名类执行,但这一直有个意外,那就是我们可能需要在其它场合使用它们的功能。但是,窗口接收器在这里仅作为关闭应用程序窗口来使用,因此我们可以安全地制造一个匿名类。然后,main()中的下面这行代码:
aFrame.addWindowListener(new WL());
会变成:
aFrame.addWindowListener(new WindowAdapter() {public void windowClosing(WindowEvent e) {System.exit(0);}});
这有一个优点就是它不需要其它的类名。我们必须对自己判断是否它使代码变得易于理解或者更难。不过,对本书余下部分而言,匿名内部类将通常被使用在窗口接收器中。
(3) 将程序片封装到JAR文件里
一个重要的JAR应用就是完善程序片的装载。在Java 1.0版中,人们倾向于试法将它们的代码填入到单个的程序片类里,因此客户只需要单个的服务器就可适合下载程序片代码。但这不仅使结果凌乱,难以阅读(当然维护也然)程序,但类文件一直不能压缩,因此下载从来没有快过。
JAR文件将我们所有的被压缩的类文件打包到一个单个儿的文件中,再被浏览器下载。现在我们不需要创建一个糟糕的设计以最小化我们创建的类,并且用户将得到更快地下载速度。
仔细想想上面的例子,这个例子看起来像Button2NewB,是一个单类,但事实上它包含三个内部类,因此共有四个。每当我们编译程序,我会用这行代码打包它到一个JAR文件:
jar cf Button2NewB.jar *.class
这是假定只有一个类文件在当前目录中,其中之一来自Button2NewB.java(否则我们会得到特别的打包)。
现在我们可以创建一个使用新文件标签来指定JAR文件的HTML页,如下所示:
<head><title>Button2NewB Example Applet</title></head><body><applet code="Button2NewB.class"archive="Button2NewB.jar"width=200 height=150></applet></body>
与HTML文件中的程序片标记有关的其他任何内容都保持不变。
13.16.4 再研究一下以前的例子
为注意到一些利用新事件模型的例子和为学习程序从老到新事件模型改变的方法,下面的例子回到在本章第一部分利用事件模型来证明的一些争议。另外,每个程序包括程序片和应用程序现在都可以借助或不借助浏览器来运行。
(1) 文本字段
这个例子同TextField1.java相似,但它增加了显然额外的行为:
//: TextNew.java// Text fields with Java 1.1 eventsimport java.awt.*;import java.awt.event.*;import java.applet.*;public class TextNew extends Applet {Buttonb1 = new Button("Get Text"),b2 = new Button("Set Text");TextFieldt1 = new TextField(30),t2 = new TextField(30),t3 = new TextField(30);String s = new String();public void init() {b1.addActionListener(new B1());b2.addActionListener(new B2());t1.addTextListener(new T1());t1.addActionListener(new T1A());t1.addKeyListener(new T1K());add(b1);add(b2);add(t1);add(t2);add(t3);}class T1 implements TextListener {public void textValueChanged(TextEvent e) {t2.setText(t1.getText());}}class T1A implements ActionListener {private int count = 0;public void actionPerformed(ActionEvent e) {t3.setText("t1 Action Event " + count++);}}class T1K extends KeyAdapter {public void keyTyped(KeyEvent e) {String ts = t1.getText();if(e.getKeyChar() ==KeyEvent.VK_BACK_SPACE) {// Ensure it's not empty:if( ts.length() > 0) {ts = ts.substring(0, ts.length() - 1);t1.setText(ts);}}elset1.setText(t1.getText() +Character.toUpperCase(e.getKeyChar()));t1.setCaretPosition(t1.getText().length());// Stop regular character from appearing:e.consume();}}class B1 implements ActionListener {public void actionPerformed(ActionEvent e) {s = t1.getSelectedText();if(s.length() == 0) s = t1.getText();t1.setEditable(true);}}class B2 implements ActionListener {public void actionPerformed(ActionEvent e) {t1.setText("Inserted by Button 2: " + s);t1.setEditable(false);}}public static void main(String[] args) {TextNew applet = new TextNew();Frame aFrame = new Frame("TextNew");aFrame.addWindowListener(new WindowAdapter() {public void windowClosing(WindowEvent e) {System.exit(0);}});aFrame.add(applet, BorderLayout.CENTER);aFrame.setSize(300,200);applet.init();applet.start();aFrame.setVisible(true);}} ///:~
当TextField t1的动作接收器被激活时,TextField t3就是一个需要报告的场所。我们注意到仅当我们按下enter键时,动作接收器才会为TextField所激活。
TextField t1附有几个接收器。T1接收器从t1复制所有文字到t2,强制所有字符串转换成大写。我们会发现这两个工作同是进行的,并且如果我们增加T1K接收器后我们再增加T1接收器,它就不那么重要:在文字字段内的所有的字符串将一直被强制变为大写。这看起来键盘事件一直在文字组件事件前被激活,并且如果我们需要保留t2的字符串原来输入时的样子,我们就必须做一些特别的工作。
T1K有着其它的一些有趣的活动。我们必须测试backspace(因为我们现在控制着每一个事件)并执行删除。caret必须被明确地设置到字段的结尾;否则它不会像我们希望的运行。最后,为了防止原来的字符串被默认的机制所处理,事件必须利用为事件对象而存在的consume()方法所“耗尽”。这会通知系统停止激活其余特殊事件的事件处理器。
这个例子同样无声地证明了设计内部类的带来的诸多优点。注意下面的内部类:
class T1 implements TextListener {public void textValueChanged(TextEvent e) {t2.setText(t1.getText());}}
t1和t2不属于T1的一部分,并且到目前为止它们都是很容易理解的,没有任何的特殊限制。这是因为一个内部类的对象能自动地捕捉一个引用到外部的创建它的对象那里,因此我们可以处理封装类对象的方法和内容。正像我们看到的,这十分方便(注释⑥)。
⑥:它也解决了“回调”的问题,不必为Java加入任何令人恼火的“方法指针”特性。
(2) 文本区域
Java 1.1版中Text Area最重要的改变就滚动条。对于TextArea的构造器而言,我们可以立即控制TextArea是否会拥有滚动条:水平的,垂直的,两者都有或者都没有。这个例子更正了前面Java 1.0版TextArea1.java程序片,演示了Java 1.1版的滚动条构造器:
//: TextAreaNew.java// Controlling scrollbars with the TextArea// component in Java 1.1import java.awt.*;import java.awt.event.*;import java.applet.*;public class TextAreaNew extends Applet {Button b1 = new Button("Text Area 1");Button b2 = new Button("Text Area 2");Button b3 = new Button("Replace Text");Button b4 = new Button("Insert Text");TextArea t1 = new TextArea("t1", 1, 30);TextArea t2 = new TextArea("t2", 4, 30);TextArea t3 = new TextArea("t3", 1, 30,TextArea.SCROLLBARS_NONE);TextArea t4 = new TextArea("t4", 10, 10,TextArea.SCROLLBARS_VERTICAL_ONLY);TextArea t5 = new TextArea("t5", 4, 30,TextArea.SCROLLBARS_HORIZONTAL_ONLY);TextArea t6 = new TextArea("t6", 10, 10,TextArea.SCROLLBARS_BOTH);public void init() {b1.addActionListener(new B1L());add(b1);add(t1);b2.addActionListener(new B2L());add(b2);add(t2);b3.addActionListener(new B3L());add(b3);b4.addActionListener(new B4L());add(b4);add(t3); add(t4); add(t5); add(t6);}class B1L implements ActionListener {public void actionPerformed(ActionEvent e) {t5.append(t1.getText() + "\n");}}class B2L implements ActionListener {public void actionPerformed(ActionEvent e) {t2.setText("Inserted by Button 2");t2.append(": " + t1.getText());t5.append(t2.getText() + "\n");}}class B3L implements ActionListener {public void actionPerformed(ActionEvent e) {String s = " Replacement ";t2.replaceRange(s, 3, 3 + s.length());}}class B4L implements ActionListener {public void actionPerformed(ActionEvent e) {t2.insert(" Inserted ", 10);}}public static void main(String[] args) {TextAreaNew applet = new TextAreaNew();Frame aFrame = new Frame("TextAreaNew");aFrame.addWindowListener(new WindowAdapter() {public void windowClosing(WindowEvent e) {System.exit(0);}});aFrame.add(applet, BorderLayout.CENTER);aFrame.setSize(300,725);applet.init();applet.start();aFrame.setVisible(true);}} ///:~
我们发现只能在构造TextArea时能够控制滚动条。同样,即使TE AR没有滚动条,我们滚动光标也将被制止(可通过运行这个例子中验证这种行为)。
(3) 复选框和单选钮
正如早先指出的那样,复选框和单选钮都是同一个类建立的。单选钮和复选框略有不同,它是复选框安置到CheckboxGroup中构成的。在其中任一种情况下,有趣的ItemEvent事件为我们创建一个ItemListener项目接收器。
当处理一组复选框或者单选钮时,我们有一个不错的选择。我们可以创建一个新的内部类去为每个复选框处理事件,或者创建一个内部类判断哪个复选框被单击并注册一个内部类单独的对象为每个复选对象。下面的例子演示了两种方法:
//: RadioCheckNew.java// Radio buttons and Check Boxes in Java 1.1import java.awt.*;import java.awt.event.*;import java.applet.*;public class RadioCheckNew extends Applet {TextField t = new TextField(30);Checkbox[] cb = {new Checkbox("Check Box 1"),new Checkbox("Check Box 2"),new Checkbox("Check Box 3") };CheckboxGroup g = new CheckboxGroup();Checkboxcb4 = new Checkbox("four", g, false),cb5 = new Checkbox("five", g, true),cb6 = new Checkbox("six", g, false);public void init() {t.setEditable(false);add(t);ILCheck il = new ILCheck();for(int i = 0; i < cb.length; i++) {cb[i].addItemListener(il);add(cb[i]);}cb4.addItemListener(new IL4());cb5.addItemListener(new IL5());cb6.addItemListener(new IL6());add(cb4); add(cb5); add(cb6);}// Checking the source:class ILCheck implements ItemListener {public void itemStateChanged(ItemEvent e) {for(int i = 0; i < cb.length; i++) {if(e.getSource().equals(cb[i])) {t.setText("Check box " + (i + 1));return;}}}}// vs. an individual class for each item:class IL4 implements ItemListener {public void itemStateChanged(ItemEvent e) {t.setText("Radio button four");}}class IL5 implements ItemListener {public void itemStateChanged(ItemEvent e) {t.setText("Radio button five");}}class IL6 implements ItemListener {public void itemStateChanged(ItemEvent e) {t.setText("Radio button six");}}public static void main(String[] args) {RadioCheckNew applet = new RadioCheckNew();Frame aFrame = new Frame("RadioCheckNew");aFrame.addWindowListener(new WindowAdapter() {public void windowClosing(WindowEvent e) {System.exit(0);}});aFrame.add(applet, BorderLayout.CENTER);aFrame.setSize(300,200);applet.init();applet.start();aFrame.setVisible(true);}} ///:~
ILCheck拥有当我们增加或者减少复选框时自动调整的优点。当然,我们对单选钮使用这种方法也同样的好。但是,它仅当我们的逻辑足以普遍的支持这种方法时才会被使用。如果声明一个确定的信号——我们将重复利用独立的接收器类,否则我们将结束一串条件语句。
(4) 下拉列表
下拉列表在Java 1.1版中当一个选择被改变时同样使用ItemListener去告知我们:
//: ChoiceNew.java// Drop-down lists with Java 1.1import java.awt.*;import java.awt.event.*;import java.applet.*;public class ChoiceNew extends Applet {String[] description = { "Ebullient", "Obtuse","Recalcitrant", "Brilliant", "Somnescent","Timorous", "Florid", "Putrescent" };TextField t = new TextField(100);Choice c = new Choice();Button b = new Button("Add items");int count = 0;public void init() {t.setEditable(false);for(int i = 0; i < 4; i++)c.addItem(description[count++]);add(t);add(c);add(b);c.addItemListener(new CL());b.addActionListener(new BL());}class CL implements ItemListener {public void itemStateChanged(ItemEvent e) {t.setText("index: " + c.getSelectedIndex()+ " " + e.toString());}}class BL implements ActionListener {public void actionPerformed(ActionEvent e) {if(count < description.length)c.addItem(description[count++]);}}public static void main(String[] args) {ChoiceNew applet = new ChoiceNew();Frame aFrame = new Frame("ChoiceNew");aFrame.addWindowListener(new WindowAdapter() {public void windowClosing(WindowEvent e) {System.exit(0);}});aFrame.add(applet, BorderLayout.CENTER);aFrame.setSize(750,100);applet.init();applet.start();aFrame.setVisible(true);}} ///:~
这个程序中没什么特别新颖的东西(除了Java 1.1版的UI类里少数几个值得关注的缺陷)。
(5) 列表
我们消除了Java 1.0中List设计的一个缺陷,就是List不能像我们希望的那样工作:它会与单击在一个列表元素上发生冲突。
//: ListNew.java// Java 1.1 Lists are easier to useimport java.awt.*;import java.awt.event.*;import java.applet.*;public class ListNew extends Applet {String[] flavors = { "Chocolate", "Strawberry","Vanilla Fudge Swirl", "Mint Chip","Mocha Almond Fudge", "Rum Raisin","Praline Cream", "Mud Pie" };// Show 6 items, allow multiple selection:List lst = new List(6, true);TextArea t = new TextArea(flavors.length, 30);Button b = new Button("test");int count = 0;public void init() {t.setEditable(false);for(int i = 0; i < 4; i++)lst.addItem(flavors[count++]);add(t);add(lst);add(b);lst.addItemListener(new LL());b.addActionListener(new BL());}class LL implements ItemListener {public void itemStateChanged(ItemEvent e) {t.setText("");String[] items = lst.getSelectedItems();for(int i = 0; i < items.length; i++)t.append(items[i] + "\n");}}class BL implements ActionListener {public void actionPerformed(ActionEvent e) {if(count < flavors.length)lst.addItem(flavors[count++], 0);}}public static void main(String[] args) {ListNew applet = new ListNew();Frame aFrame = new Frame("ListNew");aFrame.addWindowListener(new WindowAdapter() {public void windowClosing(WindowEvent e) {System.exit(0);}});aFrame.add(applet, BorderLayout.CENTER);aFrame.setSize(300,200);applet.init();applet.start();aFrame.setVisible(true);}} ///:~
我们可以注意到在列表项中无需特别的逻辑需要去支持一个单击动作。我们正好像我们在其它地方所做的那样附加上一个接收器。
(6) 菜单
为菜单处理事件看起来受益于Java 1.1版的事件模型,但Java生成菜单的方法常常麻烦并且需要一些手工编写代码。生成菜单的正确方法看起来像资源而不是一些代码。请牢牢记住编程工具会广泛地为我们处理创建的菜单,因此这可以减少我们的痛苦(只要它们会同样处理维护任务!)。另外,我们将发现菜单不支持并且将导致混乱的事件:菜单项使用ActionListeners(动作接收器),但复选框菜单项使用ItemListeners(项目接收器)。菜单对象同样能支持ActionListeners(动作接收器),但通常不那么有用。一般来说,我们会附加接收器到每个菜单项或复选框菜单项,但下面的例子(对先前例子的修改)演示了一个联合捕捉多个菜单组件到一个单独的接收器类的方法。正像我们将看到的,它或许不值得为这而激烈地争论。
//: MenuNew.java// Menus in Java 1.1import java.awt.*;import java.awt.event.*;public class MenuNew extends Frame {String[] flavors = { "Chocolate", "Strawberry","Vanilla Fudge Swirl", "Mint Chip","Mocha Almond Fudge", "Rum Raisin","Praline Cream", "Mud Pie" };TextField t = new TextField("No flavor", 30);MenuBar mb1 = new MenuBar();Menu f = new Menu("File");Menu m = new Menu("Flavors");Menu s = new Menu("Safety");// Alternative approach:CheckboxMenuItem[] safety = {new CheckboxMenuItem("Guard"),new CheckboxMenuItem("Hide")};MenuItem[] file = {// No menu shortcut:new MenuItem("Open"),// Adding a menu shortcut is very simple:new MenuItem("Exit",new MenuShortcut(KeyEvent.VK_E))};// A second menu bar to swap to:MenuBar mb2 = new MenuBar();Menu fooBar = new Menu("fooBar");MenuItem[] other = {new MenuItem("Foo"),new MenuItem("Bar"),new MenuItem("Baz"),};// Initialization code:{ML ml = new ML();CMIL cmil = new CMIL();safety[0].setActionCommand("Guard");safety[0].addItemListener(cmil);safety[1].setActionCommand("Hide");safety[1].addItemListener(cmil);file[0].setActionCommand("Open");file[0].addActionListener(ml);file[1].setActionCommand("Exit");file[1].addActionListener(ml);other[0].addActionListener(new FooL());other[1].addActionListener(new BarL());other[2].addActionListener(new BazL());}Button b = new Button("Swap Menus");public MenuNew() {FL fl = new FL();for(int i = 0; i < flavors.length; i++) {MenuItem mi = new MenuItem(flavors[i]);mi.addActionListener(fl);m.add(mi);// Add separators at intervals:if((i+1) % 3 == 0)m.addSeparator();}for(int i = 0; i < safety.length; i++)s.add(safety[i]);f.add(s);for(int i = 0; i < file.length; i++)f.add(file[i]);mb1.add(f);mb1.add(m);setMenuBar(mb1);t.setEditable(false);add(t, BorderLayout.CENTER);// Set up the system for swapping menus:b.addActionListener(new BL());add(b, BorderLayout.NORTH);for(int i = 0; i < other.length; i++)fooBar.add(other[i]);mb2.add(fooBar);}class BL implements ActionListener {public void actionPerformed(ActionEvent e) {MenuBar m = getMenuBar();if(m == mb1) setMenuBar(mb2);else if (m == mb2) setMenuBar(mb1);}}class ML implements ActionListener {public void actionPerformed(ActionEvent e) {MenuItem target = (MenuItem)e.getSource();String actionCommand =target.getActionCommand();if(actionCommand.equals("Open")) {String s = t.getText();boolean chosen = false;for(int i = 0; i < flavors.length; i++)if(s.equals(flavors[i])) chosen = true;if(!chosen)t.setText("Choose a flavor first!");elset.setText("Opening "+ s +". Mmm, mm!");} else if(actionCommand.equals("Exit")) {dispatchEvent(new WindowEvent(MenuNew.this,WindowEvent.WINDOW_CLOSING));}}}class FL implements ActionListener {public void actionPerformed(ActionEvent e) {MenuItem target = (MenuItem)e.getSource();t.setText(target.getLabel());}}// Alternatively, you can create a different// class for each different MenuItem. Then you// Don't have to figure out which one it is:class FooL implements ActionListener {public void actionPerformed(ActionEvent e) {t.setText("Foo selected");}}class BarL implements ActionListener {public void actionPerformed(ActionEvent e) {t.setText("Bar selected");}}class BazL implements ActionListener {public void actionPerformed(ActionEvent e) {t.setText("Baz selected");}}class CMIL implements ItemListener {public void itemStateChanged(ItemEvent e) {CheckboxMenuItem target =(CheckboxMenuItem)e.getSource();String actionCommand =target.getActionCommand();if(actionCommand.equals("Guard"))t.setText("Guard the Ice Cream! " +"Guarding is " + target.getState());else if(actionCommand.equals("Hide"))t.setText("Hide the Ice Cream! " +"Is it cold? " + target.getState());}}public static void main(String[] args) {MenuNew f = new MenuNew();f.addWindowListener(new WindowAdapter() {public void windowClosing(WindowEvent e) {System.exit(0);}});f.setSize(300,200);f.setVisible(true);}} ///:~
在我们开始初始化节(由注解Initialization code:后的右大括号指明)的前面部分的代码同先前(Java 1.0版)版本相同。这里我们可以注意到项目接收器和动作接收器被附加在不同的菜单组件上。
Java 1.1支持“菜单快捷键”,因此我们可以选择一个菜单项目利用键盘替代鼠标。这十分的简单;我们只要使用重载菜单项构造器设置第二个参数为一个MenuShortcut(菜单快捷键事件)对象即可。菜单快捷键构造器设置重要的方法,当它按下时不可思议地显示在菜单项上。上面的例子增加了Control-E到Exit菜单项中。
我们同样会注意setActionCommand()的使用。这看似一点陌生因为在各种情况下“action command”完全同菜单组件上的标签一样。为什么不正好使用标签代替可选择的字符串呢?这个难题是国际化的。如果我们重新用其它语言写这个程序,我们只需要改变菜单中的标签,并不审查代码中可能包含新错误的所有逻辑。因此使这对检查文字字符串联合菜单组件的代码而言变得简单容易,当菜单标签能改变时“动作指令”可以不作任何的改变。所有这些代码同“动作指令”一同工作,因此它不会受改变菜单标签的影响。注意在这个程序中,不是所有的菜单组件都被它们的动作指令所审查,因此这些组件都没有它们的动作指令集。
大多数的构造器同前面的一样,将几个调用的异常增加到接收器中。大量的工作发生在接收器里。在前面例子的BL中,菜单交替发生。在ML中,“寻找ring”方法被作为动作事件(ActionEvent)的资源并对它进行转换送入菜单项,然后得到动作指令字符串,再通过它去贯穿串联组,当然条件是对它进行声明。这些大多数同前面的一样,但请注意如果Exit被选中,通过进入封装类对象的引用(MenuNew.this)并创建一个WINDOW_CLOSING事件,一个新的窗口事件就被创建了。新的事件被分配到封装类对象的dispatchEvent()方法,然后结束调用windowsClosing()内部帧的窗口接收器(这个接收器作为一个内部类被创建在main()里),似乎这是“正常”产生消息的方法。通过这种机制,我们可以在任何情况下迅速处理任何的信息,因此,它非常的强大。
FL接收器是很简单尽管它能处理特殊菜单的所有不同的特色。如果我们的逻辑十分的简单明了,这种方法对我们就很有用处,但通常,我们使用这种方法时需要与FooL,BarL和BazL一道使用,它们每个都附加到一个单独的菜单组件上,因此必然无需测试逻辑,并且使我们正确地辨识出谁调用了接收器。这种方法产生了大量的类,内部代码趋向于变得小巧和处理起来简单、安全。
(7) 对话框
在这个例子里直接重写了早期的ToeTest.java程序。在这个新的版本里,任何事件都被安放进一个内部类中。虽然这完全消除了需要记录产生的任何类的麻烦,作为ToeTest.java的一个例子,它能使内部类的概念变得不那遥远。在这点,内嵌类被嵌套达四层之深!我们需要的这种设计决定了内部类的优点是否值得增加更加复杂的事物。另外,当我们创建一个非静态的内部类时,我们将捆绑非静态类到它周围的类上。有时,单独的类可以更容易地被复用。
//: ToeTestNew.java// Demonstration of dialog boxes// and creating your own componentsimport java.awt.*;import java.awt.event.*;public class ToeTestNew extends Frame {TextField rows = new TextField("3");TextField cols = new TextField("3");public ToeTestNew() {setTitle("Toe Test");Panel p = new Panel();p.setLayout(new GridLayout(2,2));p.add(new Label("Rows", Label.CENTER));p.add(rows);p.add(new Label("Columns", Label.CENTER));p.add(cols);add(p, BorderLayout.NORTH);Button b = new Button("go");b.addActionListener(new BL());add(b, BorderLayout.SOUTH);}static final int BLANK = 0;static final int XX = 1;static final int OO = 2;class ToeDialog extends Dialog {// w = number of cells wide// h = number of cells highint turn = XX; // Start with x's turnpublic ToeDialog(int w, int h) {super(ToeTestNew.this,"The game itself", false);setLayout(new GridLayout(w, h));for(int i = 0; i < w * h; i++)add(new ToeButton());setSize(w * 50, h * 50);addWindowListener(new WindowAdapter() {public void windowClosing(WindowEvent e){dispose();}});}class ToeButton extends Canvas {int state = BLANK;ToeButton() {addMouseListener(new ML());}public void paint(Graphics g) {int x1 = 0;int y1 = 0;int x2 = getSize().width - 1;int y2 = getSize().height - 1;g.drawRect(x1, y1, x2, y2);x1 = x2/4;y1 = y2/4;int wide = x2/2;int high = y2/2;if(state == XX) {g.drawLine(x1, y1,x1 + wide, y1 + high);g.drawLine(x1, y1 + high,x1 + wide, y1);}if(state == OO) {g.drawOval(x1, y1,x1 + wide/2, y1 + high/2);}}class ML extends MouseAdapter {public void mousePressed(MouseEvent e) {if(state == BLANK) {state = turn;turn = (turn == XX ? OO : XX);}elsestate = (state == XX ? OO : XX);repaint();}}}}class BL implements ActionListener {public void actionPerformed(ActionEvent e) {Dialog d = new ToeDialog(Integer.parseInt(rows.getText()),Integer.parseInt(cols.getText()));d.show();}}public static void main(String[] args) {Frame f = new ToeTestNew();f.addWindowListener(new WindowAdapter() {public void windowClosing(WindowEvent e) {System.exit(0);}});f.setSize(200,100);f.setVisible(true);}} ///:~
由于“静态”的东西只能位于类的外部一级,所以内部类不可能拥有静态数据或者静态内部类。
(8) 文件对话框
这个例子是直接用新事件模型对FileDialogTest.java修改而来。
//: FileDialogNew.java// Demonstration of File dialog boxesimport java.awt.*;import java.awt.event.*;public class FileDialogNew extends Frame {TextField filename = new TextField();TextField directory = new TextField();Button open = new Button("Open");Button save = new Button("Save");public FileDialogNew() {setTitle("File Dialog Test");Panel p = new Panel();p.setLayout(new FlowLayout());open.addActionListener(new OpenL());p.add(open);save.addActionListener(new SaveL());p.add(save);add(p, BorderLayout.SOUTH);directory.setEditable(false);filename.setEditable(false);p = new Panel();p.setLayout(new GridLayout(2,1));p.add(filename);p.add(directory);add(p, BorderLayout.NORTH);}class OpenL implements ActionListener {public void actionPerformed(ActionEvent e) {// Two arguments, defaults to open file:FileDialog d = new FileDialog(FileDialogNew.this,"What file do you want to open?");d.setFile("*.java");d.setDirectory("."); // Current directoryd.show();String yourFile = "*.*";if((yourFile = d.getFile()) != null) {filename.setText(yourFile);directory.setText(d.getDirectory());} else {filename.setText("You pressed cancel");directory.setText("");}}}class SaveL implements ActionListener {public void actionPerformed(ActionEvent e) {FileDialog d = new FileDialog(FileDialogNew.this,"What file do you want to save?",FileDialog.SAVE);d.setFile("*.java");d.setDirectory(".");d.show();String saveFile;if((saveFile = d.getFile()) != null) {filename.setText(saveFile);directory.setText(d.getDirectory());} else {filename.setText("You pressed cancel");directory.setText("");}}}public static void main(String[] args) {Frame f = new FileDialogNew();f.addWindowListener(new WindowAdapter() {public void windowClosing(WindowEvent e) {System.exit(0);}});f.setSize(250,110);f.setVisible(true);}} ///:~
如果所有的改变是这样的容易那将有多棒,但至少它们已足够容易,并且我们的代码已受益于这改进的可读性上。
13.16.5 动态绑定事件
新AWT事件模型给我们带来的一个好处就是灵活性。在老的模型中我们被迫为我们的程序动作艰难地编写代码。但新的模型我们可以用单一方法调用增加和删除事件动作。下面的例子证明了这一点:
//: DynamicEvents.java// The new Java 1.1 event model allows you to// change event behavior dynamically. Also// demonstrates multiple actions for an event.import java.awt.*;import java.awt.event.*;import java.util.*;public class DynamicEvents extends Frame {Vector v = new Vector();int i = 0;Buttonb1 = new Button("Button 1"),b2 = new Button("Button 2");public DynamicEvents() {setLayout(new FlowLayout());b1.addActionListener(new B());b1.addActionListener(new B1());b2.addActionListener(new B());b2.addActionListener(new B2());add(b1);add(b2);}class B implements ActionListener {public void actionPerformed(ActionEvent e) {System.out.println("A button was pressed");}}class CountListener implements ActionListener {int index;public CountListener(int i) { index = i; }public void actionPerformed(ActionEvent e) {System.out.println("Counted Listener " + index);}}class B1 implements ActionListener {public void actionPerformed(ActionEvent e) {System.out.println("Button 1 pressed");ActionListener a = new CountListener(i++);v.addElement(a);b2.addActionListener(a);}}class B2 implements ActionListener {public void actionPerformed(ActionEvent e) {System.out.println("Button 2 pressed");int end = v.size() -1;if(end >= 0) {b2.removeActionListener((ActionListener)v.elementAt(end));v.removeElementAt(end);}}}public static void main(String[] args) {Frame f = new DynamicEvents();f.addWindowListener(new WindowAdapter() {public void windowClosing(WindowEvent e){System.exit(0);}});f.setSize(300,200);f.show();}} ///:~
这个例子采取的新手法包括:
(1) 在每个按钮上附着不少于一个的接收器。通常,组件把事件作为多转换处理,这意味着我们可以为单个事件注册许多接收器。当在特殊的组件中一个事件作为单一转换被处理时,我们会得到TooManyListenersException(即太多接收器异常)。
(2) 程序执行期间,接收器动态地被从按钮B2中增加和删除。增加用我们前面见到过的方法完成,但每个组件同样有一个removeXXXListener()(删除XXX接收器)方法来删除各种类型的接收器。
这种灵活性为我们的编程提供了更强大的能力。
我们注意到事件接收器不能保证在命令他们被增加时可被调用(虽然事实上大部分的执行工作都是用这种方法完成的)。
13.16.6 将事务逻辑与UI逻辑区分开
一般而言,我们需要设计我们的类如此以至于每一类做“一件事”。当涉及用户接口代码时就更显得尤为重要,因为它很容易地封装“您要做什么”和“怎样显示它”。这种有效的配合防止了代码的重复使用。更不用说它令人满意的从GUI中区分出我们的“事物逻辑”。使用这种方法,我们可以不仅仅更容易地重复使用事物逻辑,它同样可以更容易地重复使用GUI。
其它的争议是“动作对象”存在的完成分离机器的多层次系统。动作主要的定位规则允许所有新事件修改后立刻生效,并且这是如此一个引人注目的设置系统的方法。但是这些动作对象可以被在一些不同的应用程序使用并且因此不会被一些特殊的显示模式所约束。它们会合理地执行动作操作并且没有多余的事件。
下面的例子演示了从GUI代码中多么地轻松的区分事物逻辑:
//: Separation.java// Separating GUI logic and business objectsimport java.awt.*;import java.awt.event.*;import java.applet.*;class BusinessLogic {private int modifier;BusinessLogic(int mod) {modifier = mod;}public void setModifier(int mod) {modifier = mod;}public int getModifier() {return modifier;}// Some business operations:public int calculation1(int arg) {return arg * modifier;}public int calculation2(int arg) {return arg + modifier;}}public class Separation extends Applet {TextFieldt = new TextField(20),mod = new TextField(20);BusinessLogic bl = new BusinessLogic(2);Buttoncalc1 = new Button("Calculation 1"),calc2 = new Button("Calculation 2");public void init() {add(t);calc1.addActionListener(new Calc1L());calc2.addActionListener(new Calc2L());add(calc1); add(calc2);mod.addTextListener(new ModL());add(new Label("Modifier:"));add(mod);}static int getValue(TextField tf) {try {return Integer.parseInt(tf.getText());} catch(NumberFormatException e) {return 0;}}class Calc1L implements ActionListener {public void actionPerformed(ActionEvent e) {t.setText(Integer.toString(bl.calculation1(getValue(t))));}}class Calc2L implements ActionListener {public void actionPerformed(ActionEvent e) {t.setText(Integer.toString(bl.calculation2(getValue(t))));}}class ModL implements TextListener {public void textValueChanged(TextEvent e) {bl.setModifier(getValue(mod));}}public static void main(String[] args) {Separation applet = new Separation();Frame aFrame = new Frame("Separation");aFrame.addWindowListener(new WindowAdapter() {public void windowClosing(WindowEvent e) {System.exit(0);}});aFrame.add(applet, BorderLayout.CENTER);aFrame.setSize(200,200);applet.init();applet.start();aFrame.setVisible(true);}} ///:~
可以看到,事物逻辑是一个直接完成它的操作而不需要提示并且可以在GUI环境下使用的类。它正适合它的工作。区分动作记录了所有UI的详细资料,并且它只通过它的公共接口与事物逻辑交流。所有的操作围绕中心通过UI和事物逻辑对象来回获取信息。因此区分,轮流做它的工作。因为区分中只知道它同事物逻辑对象对话(也就是说,它没有高度的结合),它可以被强迫同其它类型的对象对话而没有更多的烦恼。 思考从事物逻辑中区分UI的条件,同样思考当我们调整传统的Java代码使它运行时,怎样使它更易存活。
13.16.7 推荐编码方法
内部类是新的事件模型,并且事实上旧的事件模型连同新库的特征都被它好的支持,依赖老式的编程方法无疑增加了一个新的混乱的因素。现在有更多不同的方法为我们编写讨厌的代码。凑巧的是,这种代码显现在本书中和程序样本中,并且甚至在文件和程序样本中同SUN公司区别开来。在这一节中,我们将看到一些关于我们会和不会运行新AWT的争执,并由向我们展示除了可以原谅的情况,我们可以随时使用接收器类去解决我们的事件处理需要来结束。因为这种方法同样是最简单和最清晰的方法,它将会对我们学习它构成有效的帮助。
在看到任何事以前,我们知道尽管Java 1.1向后兼容Java 1.0(也就是说,我们可以在1.1中编译和运行1.0的程序),但我们并不能在同一个程序里混合事件模型。换言之,当我们试图集成老的代码到一个新的程序中时,我们不能使用老式的action()方法在同一个程序中,因此我们必须决定是否对新程序使用老的,难以维护的方法或者升级老的代码。这不会有太多的竞争因为新的方法对老的方法而言是如此的优秀。
(1) 准则:运行它的好方法
为了给我们一些事物来进行比较,这儿有一个程序例子演示向我们推荐的方法。到现在它会变得相当的熟悉和舒适。
//: GoodIdea.java// The best way to design classes using the new// Java 1.1 event model: use an inner class for// each different event. This maximizes// flexibility and modularity.import java.awt.*;import java.awt.event.*;import java.util.*;public class GoodIdea extends Frame {Buttonb1 = new Button("Button 1"),b2 = new Button("Button 2");public GoodIdea() {setLayout(new FlowLayout());b1.addActionListener(new B1L());b2.addActionListener(new B2L());add(b1);add(b2);}public class B1L implements ActionListener {public void actionPerformed(ActionEvent e) {System.out.println("Button 1 pressed");}}public class B2L implements ActionListener {public void actionPerformed(ActionEvent e) {System.out.println("Button 2 pressed");}}public static void main(String[] args) {Frame f = new GoodIdea();f.addWindowListener(new WindowAdapter() {public void windowClosing(WindowEvent e){System.out.println("Window Closing");System.exit(0);}});f.setSize(300,200);f.setVisible(true);}} ///:~
这是颇有点微不足道的:每个按钮有它自己的印出一些事物到控制台的接收器。但请注意在整个程序中这不是一个条件语句,或者是一些表示“我想要知道怎样使事件发生”的语句。每块代码都与运行有关,而不是类型检验。也就是说,这是最好的编写我们的代码的方法;不仅仅是它更易使我们理解概念,至少是使我们更易阅读和维护。剪切和粘贴到新的程序是同样如此的容易。
(2) 将主类作为接收器实现
第一个坏主意是一个通常的和推荐的方法。这使得主类(有代表性的是程序片或帧,但它能变成一些类)执行各种不同的接收器。下面是一个例子:
//: BadIdea1.java// Some literature recommends this approach,// but it's missing the point of the new event// model in Java 1.1import java.awt.*;import java.awt.event.*;import java.util.*;public class BadIdea1 extends Frameimplements ActionListener, WindowListener {Buttonb1 = new Button("Button 1"),b2 = new Button("Button 2");public BadIdea1() {setLayout(new FlowLayout());addWindowListener(this);b1.addActionListener(this);b2.addActionListener(this);add(b1);add(b2);}public void actionPerformed(ActionEvent e) {Object source = e.getSource();if(source == b1)System.out.println("Button 1 pressed");else if(source == b2)System.out.println("Button 2 pressed");elseSystem.out.println("Something else");}public void windowClosing(WindowEvent e) {System.out.println("Window Closing");System.exit(0);}public void windowClosed(WindowEvent e) {}public void windowDeiconified(WindowEvent e) {}public void windowIconified(WindowEvent e) {}public void windowActivated(WindowEvent e) {}public void windowDeactivated(WindowEvent e) {}public void windowOpened(WindowEvent e) {}public static void main(String[] args) {Frame f = new BadIdea1();f.setSize(300,200);f.setVisible(true);}} ///:~
这样做的用途显示在下述三行里:
addWindowListener(this);b1.addActionListener(this);b2.addActionListener(this);
因为Badidea1执行动作接收器和窗中接收器,这些程序行当然可以接受,并且如果我们一直坚持设法使少量的类去减少服务器检索期间的程序片载入的作法,它看起来变成一个不错的主意。但是:
(1) Java 1.1版支持JAR文件,因此所有我们的文件可以被放置到一个单一的压缩的JAR文件中,只需要一次服务器检索。我们不再需要为Internet效率而减少类的数量。
(2) 上面的代码的组件更加的少,因此它难以抓住和粘贴。注意我们必须不仅要执行各种各样的接口为我们的主类,但在actionPerformed()方法中,我们利用一串条件语句测试哪个动作被完成了。不仅仅是这个状态倒退,远离接收器模型,除此之外,我们不能简单地重复使用actionPerformed()方法因为它是指定为这个特殊的应用程序使用的。将这个程序例子与GoodIdea.java进行比较,我们可以正好捕捉一个接收器类并粘贴它和最小的焦急到任何地方。另外我们可以为一个单独的事件注册多个接收器类,允许甚至更多的模块在每个接收器类在每个接收器中运行。
(3) 方法的混合
第二个bad idea混合了两种方法:使用内嵌接收器类,但同样执行一个或更多的接收器接口以作为主类的一部分。这种方法无需在书中和文件中进行解释,而且我可以臆测到Java开发者认为他们必须为不同的目的而采取不同的方法。但我们却不必——在我们编程时,我们或许可能会倾向于使用内嵌接收器类。
//: BadIdea2.java// An improvement over BadIdea1.java, since it// uses the WindowAdapter as an inner class// instead of implementing all the methods of// WindowListener, but still misses the// valuable modularity of inner classesimport java.awt.*;import java.awt.event.*;import java.util.*;public class BadIdea2 extends Frameimplements ActionListener {Buttonb1 = new Button("Button 1"),b2 = new Button("Button 2");public BadIdea2() {setLayout(new FlowLayout());addWindowListener(new WL());b1.addActionListener(this);b2.addActionListener(this);add(b1);add(b2);}public void actionPerformed(ActionEvent e) {Object source = e.getSource();if(source == b1)System.out.println("Button 1 pressed");else if(source == b2)System.out.println("Button 2 pressed");elseSystem.out.println("Something else");}class WL extends WindowAdapter {public void windowClosing(WindowEvent e) {System.out.println("Window Closing");System.exit(0);}}public static void main(String[] args) {Frame f = new BadIdea2();f.setSize(300,200);f.setVisible(true);}} ///:~
因为actionPerformed()动作完成方法同主类紧密地结合,所以难以复用代码。它的代码读起来同样是凌乱和令人厌烦的,远远超过了内部类方法。不合理的是,我们不得不在Java 1.1版中为事件使用那些老的思路。
(4) 继承一个组件
创建一个新类型的组件时,在运行事件的老方法中,我们会经常看到不同的地方发生了变化。这里有一个程序例子来演示这种新的工作方法:
//: GoodTechnique.java// Your first choice when overriding components// should be to install listeners. The code is// much safer, more modular and maintainable.import java.awt.*;import java.awt.event.*;class Display {public static final intEVENT = 0, COMPONENT = 1,MOUSE = 2, MOUSE_MOVE = 3,FOCUS = 4, KEY = 5, ACTION = 6,LAST = 7;public String[] evnt;Display() {evnt = new String[LAST];for(int i = 0; i < LAST; i++)evnt[i] = new String();}public void show(Graphics g) {for(int i = 0; i < LAST; i++)g.drawString(evnt[i], 0, 10 * i + 10);}}class EnabledPanel extends Panel {Color c;int id;Display display = new Display();public EnabledPanel(int i, Color mc) {id = i;c = mc;setLayout(new BorderLayout());add(new MyButton(), BorderLayout.SOUTH);addComponentListener(new CL());addFocusListener(new FL());addKeyListener(new KL());addMouseListener(new ML());addMouseMotionListener(new MML());}// To eliminate flicker:public void update(Graphics g) {paint(g);}public void paint(Graphics g) {g.setColor(c);Dimension s = getSize();g.fillRect(0, 0, s.width, s.height);g.setColor(Color.black);display.show(g);}// Don't need to enable anything for this:public void processEvent(AWTEvent e) {display.evnt[Display.EVENT]= e.toString();repaint();super.processEvent(e);}class CL implements ComponentListener {public void componentMoved(ComponentEvent e){display.evnt[Display.COMPONENT] ="Component moved";repaint();}public voidcomponentResized(ComponentEvent e) {display.evnt[Display.COMPONENT] ="Component resized";repaint();}public voidcomponentHidden(ComponentEvent e) {display.evnt[Display.COMPONENT] ="Component hidden";repaint();}public void componentShown(ComponentEvent e){display.evnt[Display.COMPONENT] ="Component shown";repaint();}}class FL implements FocusListener {public void focusGained(FocusEvent e) {display.evnt[Display.FOCUS] ="FOCUS gained";repaint();}public void focusLost(FocusEvent e) {display.evnt[Display.FOCUS] ="FOCUS lost";repaint();}}class KL implements KeyListener {public void keyPressed(KeyEvent e) {display.evnt[Display.KEY] ="KEY pressed: ";showCode(e);}public void keyReleased(KeyEvent e) {display.evnt[Display.KEY] ="KEY released: ";showCode(e);}public void keyTyped(KeyEvent e) {display.evnt[Display.KEY] ="KEY typed: ";showCode(e);}void showCode(KeyEvent e) {int code = e.getKeyCode();display.evnt[Display.KEY] +=KeyEvent.getKeyText(code);repaint();}}class ML implements MouseListener {public void mouseClicked(MouseEvent e) {requestFocus(); // Get FOCUS on clickdisplay.evnt[Display.MOUSE] ="MOUSE clicked";showMouse(e);}public void mousePressed(MouseEvent e) {display.evnt[Display.MOUSE] ="MOUSE pressed";showMouse(e);}public void mouseReleased(MouseEvent e) {display.evnt[Display.MOUSE] ="MOUSE released";showMouse(e);}public void mouseEntered(MouseEvent e) {display.evnt[Display.MOUSE] ="MOUSE entered";showMouse(e);}public void mouseExited(MouseEvent e) {display.evnt[Display.MOUSE] ="MOUSE exited";showMouse(e);}void showMouse(MouseEvent e) {display.evnt[Display.MOUSE] +=", x = " + e.getX() +", y = " + e.getY();repaint();}}class MML implements MouseMotionListener {public void mouseDragged(MouseEvent e) {display.evnt[Display.MOUSE_MOVE] ="MOUSE dragged";showMouse(e);}public void mouseMoved(MouseEvent e) {display.evnt[Display.MOUSE_MOVE] ="MOUSE moved";showMouse(e);}void showMouse(MouseEvent e) {display.evnt[Display.MOUSE_MOVE] +=", x = " + e.getX() +", y = " + e.getY();repaint();}}}class MyButton extends Button {int clickCounter;String label = "";public MyButton() {addActionListener(new AL());}public void paint(Graphics g) {g.setColor(Color.green);Dimension s = getSize();g.fillRect(0, 0, s.width, s.height);g.setColor(Color.black);g.drawRect(0, 0, s.width - 1, s.height - 1);drawLabel(g);}private void drawLabel(Graphics g) {FontMetrics fm = g.getFontMetrics();int width = fm.stringWidth(label);int height = fm.getHeight();int ascent = fm.getAscent();int leading = fm.getLeading();int horizMargin =(getSize().width - width)/2;int verMargin =(getSize().height - height)/2;g.setColor(Color.red);g.drawString(label, horizMargin,verMargin + ascent + leading);}class AL implements ActionListener {public void actionPerformed(ActionEvent e) {clickCounter++;label = "click #" + clickCounter +" " + e.toString();repaint();}}}public class GoodTechnique extends Frame {GoodTechnique() {setLayout(new GridLayout(2,2));add(new EnabledPanel(1, Color.cyan));add(new EnabledPanel(2, Color.lightGray));add(new EnabledPanel(3, Color.yellow));}public static void main(String[] args) {Frame f = new GoodTechnique();f.setTitle("Good Technique");f.addWindowListener(new WindowAdapter() {public void windowClosing(WindowEvent e){System.out.println(e);System.out.println("Window Closing");System.exit(0);}});f.setSize(700,700);f.setVisible(true);}} ///:~
这个程序例子同样证明了各种各样的发现和显示关于它们的信息的事件。这种显示是一种集中显示信息的方法。一组字符串去获取关于每种类型的事件的信息,并且show()方法对任何图像对象都设置了一个引用,我们采用并直接地写在外观代码上。这种设计是有意的被某种事件重复使用。
激活面板代表了这种新型的组件。它是一个底部有一个按钮的彩色的面板,并且它由利用接收器类为每一个单独的事件来引发捕捉所有发生在它之上的事件,除了那些在激活面板重载的老式的processEvent()方法(注意它应该同样调用super.processEvent())。利用这种方法的唯一理由是它捕捉发生的每一个事件,因此我们可以观察持续发生的每一事件。processEvent()方法没有更多的展示代表每个事件的字符串,否则它会不得不使用一串条件语句去寻找事件。在其它方面,内嵌接收类早已清晰地知道被发现的事件。(假定我们注册它们到组件,我们不需要任何的控件的逻辑,这将成为我们的目的。)因此,它们不会去检查任何事件;这些事件正好做它们的原材料。
每个接收器修改显示字符串和它的指定事件,并且调用重画方法repaint()因此将显示这个字符串。我们同样能注意到一个通常能消除闪烁的秘诀:
public void update(Graphics g) {paint(g);}
我们不会始终需要重载update(),但如果我们写下一些闪烁的程序,并运行它。默认的最新版本的清除背景然后调用paint()方法重新画出一些图画。这个清除动作通常会产生闪烁,但是不必要的,因为paint()重画了整个的外观。
我们可以看到许多的接收器——但是,对接收器输入检查指令,但我们却不能接收任何组件不支持的事件。(不像BadTechnuque.java那样我们能时时刻刻看到)。
试验这个程序是十分的有教育意义的,因为我们学习了许多的关于在Java中事件发生的方法。一则它展示了大多数开窗口的系统中设计上的瑕疵:它相当的难以去单击和释放鼠标,除非移动它,并且当我们实际上正试图用鼠标单击在某物体上时开窗口的会常常认为我们是在拖动。一个解决这个问题的方案是使用mousePressed()鼠标按下方法和mouseReleased()鼠标释放方法去代替mouseClicked()鼠标单击方法,然后判断是否去调用我们自己的以时间和4个像素的鼠标滞后作用的“mouseReallyClicked()真实的鼠标单击”方法。
(5) 蹩脚的组件继承
另一种做法是调用enableEvent()方法,并将与希望控制的事件对应的模型传递给它(许多参考书中都曾提及这种做法)。这样做会造成那些事件被发送至老式方法(尽管它们对Java 1.1来说是新的),并采用象processFocusEvent()这样的名字。也必须要记住调用基类版本。下面是它看起来的样子。
//: BadTechnique.java// It's possible to override components this way,// but the listener approach is much better, so// why would you?import java.awt.*;import java.awt.event.*;class Display {public static final intEVENT = 0, COMPONENT = 1,MOUSE = 2, MOUSE_MOVE = 3,FOCUS = 4, KEY = 5, ACTION = 6,LAST = 7;public String[] evnt;Display() {evnt = new String[LAST];for(int i = 0; i < LAST; i++)evnt[i] = new String();}public void show(Graphics g) {for(int i = 0; i < LAST; i++)g.drawString(evnt[i], 0, 10 * i + 10);}}class EnabledPanel extends Panel {Color c;int id;Display display = new Display();public EnabledPanel(int i, Color mc) {id = i;c = mc;setLayout(new BorderLayout());add(new MyButton(), BorderLayout.SOUTH);// Type checking is lost. You can enable and// process events that the component doesn't// capture:enableEvents(// Panel doesn't handle these:AWTEvent.ACTION_EVENT_MASK |AWTEvent.ADJUSTMENT_EVENT_MASK |AWTEvent.ITEM_EVENT_MASK |AWTEvent.TEXT_EVENT_MASK |AWTEvent.WINDOW_EVENT_MASK |// Panel can handle these:AWTEvent.COMPONENT_EVENT_MASK |AWTEvent.FOCUS_EVENT_MASK |AWTEvent.KEY_EVENT_MASK |AWTEvent.MOUSE_EVENT_MASK |AWTEvent.MOUSE_MOTION_EVENT_MASK |AWTEvent.CONTAINER_EVENT_MASK);// You can enable an event without// overriding its process method.}// To eliminate flicker:public void update(Graphics g) {paint(g);}public void paint(Graphics g) {g.setColor(c);Dimension s = getSize();g.fillRect(0, 0, s.width, s.height);g.setColor(Color.black);display.show(g);}public void processEvent(AWTEvent e) {display.evnt[Display.EVENT]= e.toString();repaint();super.processEvent(e);}public voidprocessComponentEvent(ComponentEvent e) {switch(e.getID()) {case ComponentEvent.COMPONENT_MOVED:display.evnt[Display.COMPONENT] ="Component moved";break;case ComponentEvent.COMPONENT_RESIZED:display.evnt[Display.COMPONENT] ="Component resized";break;case ComponentEvent.COMPONENT_HIDDEN:display.evnt[Display.COMPONENT] ="Component hidden";break;case ComponentEvent.COMPONENT_SHOWN:display.evnt[Display.COMPONENT] ="Component shown";break;default:}repaint();// Must always remember to call the "super"// version of whatever you override:super.processComponentEvent(e);}public void processFocusEvent(FocusEvent e) {switch(e.getID()) {case FocusEvent.FOCUS_GAINED:display.evnt[Display.FOCUS] ="FOCUS gained";break;case FocusEvent.FOCUS_LOST:display.evnt[Display.FOCUS] ="FOCUS lost";break;default:}repaint();super.processFocusEvent(e);}public void processKeyEvent(KeyEvent e) {switch(e.getID()) {case KeyEvent.KEY_PRESSED:display.evnt[Display.KEY] ="KEY pressed: ";break;case KeyEvent.KEY_RELEASED:display.evnt[Display.KEY] ="KEY released: ";break;case KeyEvent.KEY_TYPED:display.evnt[Display.KEY] ="KEY typed: ";break;default:}int code = e.getKeyCode();display.evnt[Display.KEY] +=KeyEvent.getKeyText(code);repaint();super.processKeyEvent(e);}public void processMouseEvent(MouseEvent e) {switch(e.getID()) {case MouseEvent.MOUSE_CLICKED:requestFocus(); // Get FOCUS on clickdisplay.evnt[Display.MOUSE] ="MOUSE clicked";break;case MouseEvent.MOUSE_PRESSED:display.evnt[Display.MOUSE] ="MOUSE pressed";break;case MouseEvent.MOUSE_RELEASED:display.evnt[Display.MOUSE] ="MOUSE released";break;case MouseEvent.MOUSE_ENTERED:display.evnt[Display.MOUSE] ="MOUSE entered";break;case MouseEvent.MOUSE_EXITED:display.evnt[Display.MOUSE] ="MOUSE exited";break;default:}display.evnt[Display.MOUSE] +=", x = " + e.getX() +", y = " + e.getY();repaint();super.processMouseEvent(e);}public voidprocessMouseMotionEvent(MouseEvent e) {switch(e.getID()) {case MouseEvent.MOUSE_DRAGGED:display.evnt[Display.MOUSE_MOVE] ="MOUSE dragged";break;case MouseEvent.MOUSE_MOVED:display.evnt[Display.MOUSE_MOVE] ="MOUSE moved";break;default:}display.evnt[Display.MOUSE_MOVE] +=", x = " + e.getX() +", y = " + e.getY();repaint();super.processMouseMotionEvent(e);}}class MyButton extends Button {int clickCounter;String label = "";public MyButton() {enableEvents(AWTEvent.ACTION_EVENT_MASK);}public void paint(Graphics g) {g.setColor(Color.green);Dimension s = getSize();g.fillRect(0, 0, s.width, s.height);g.setColor(Color.black);g.drawRect(0, 0, s.width - 1, s.height - 1);drawLabel(g);}private void drawLabel(Graphics g) {FontMetrics fm = g.getFontMetrics();int width = fm.stringWidth(label);int height = fm.getHeight();int ascent = fm.getAscent();int leading = fm.getLeading();int horizMargin =(getSize().width - width)/2;int verMargin =(getSize().height - height)/2;g.setColor(Color.red);g.drawString(label, horizMargin,verMargin + ascent + leading);}public void processActionEvent(ActionEvent e) {clickCounter++;label = "click #" + clickCounter +" " + e.toString();repaint();super.processActionEvent(e);}}public class BadTechnique extends Frame {BadTechnique() {setLayout(new GridLayout(2,2));add(new EnabledPanel(1, Color.cyan));add(new EnabledPanel(2, Color.lightGray));add(new EnabledPanel(3, Color.yellow));// You can also do it for Windows:enableEvents(AWTEvent.WINDOW_EVENT_MASK);}public void processWindowEvent(WindowEvent e) {System.out.println(e);if(e.getID() == WindowEvent.WINDOW_CLOSING) {System.out.println("Window Closing");System.exit(0);}}public static void main(String[] args) {Frame f = new BadTechnique();f.setTitle("Bad Technique");f.setSize(700,700);f.setVisible(true);}} ///:~
的确,它能够工作。但却实在太蹩脚,而且很难编写、阅读、调试、维护以及复用。既然如此,为什么还不使用内部接收器类呢?
