第十三章 创建窗口和程序片(中) 4. 下拉列表 下拉列表在Java 1.1版中当一个选择被改变时同样使用ItemListener去告知我们: //: ChoiceNew.java // Drop-down lists with Java 1.1 import 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 use import 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.1 import 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!"); else t.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 components import 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 high int turn = XX; // Start with x's turn public 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); } else state = (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 boxes import 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 directory d.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; Button b1 = 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 objects import 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 { TextField t = new TextField(20), mod = new TextField(20); BusinessLogic bl = new BusinessLogic(2); Button calc1 = 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 maxim |