13.12 布局的控制
在Java里该方法是安一个组件到一个窗体中去,它不同我们使用过的其它GUI系统。首先,它是全代码的;没有控制安放组件的“资源”。其次,该方法的组件被安放到一个被“布局管理器”控制的窗体中,由“布局管理器”根据我们add()它们的决定来安放组件。大小,形状,组件位置与其它系统的布局管理器显著的不同。另外,布局管理器使我们的程序片或应用程序适合窗口的大小,所以,如果窗口的尺寸改变(例如,在HTML页面的程序片指定的规格),组件的大小,形状和位置都会改变。
程序片和帧类都是来源于包含和显示组件的容器。(这个容器也是一个组件,所以它也能响应事件。)在容器中,调用setLayout()方法允许我选择不同的布局管理器。
在这节里我们将探索不同的布局管理器,并安放按钮在它们之上。这里没有捕捉按钮的事件,正好可以演示如何布置这些按钮。13.12.1 FlowLayout
到目前为止,所有的程序片都被建立,看起来使用一些不可思议的内部逻辑来布置它们的组件。那是因为程序使用一个默认的方式:FlowLayout。这个简单的“Flow”的组件安装在窗体中,从左到右,直到顶部的空格全部再移去一行,并继续循环这些组件。
这里有一个例子明确地(当然也是多余地)设置一个程序片的布局管理器去FlowLayout,然后在窗体中安放按钮。我们将注意到FlowLayout组件使用它们本来的大小。例如一个按钮将会变得和它的字串符一样的大小。//: FlowLayout1.java
// Demonstrating the FlowLayout
import java.awt.*;
import java.applet.*;public class FlowLayout1 extends Applet {
  public void init() {
    setLayout(new FlowLayout());
    for(int i = 0; i < 20; i++)
      add(new Button("Button " + i));
  }
} ///:~所有组件将在FlowLayout中被压缩为它们的最小尺寸,所以我们可能会得到一些奇怪的状态。例如,一个标签会合适它自已的字符串的尺寸,所以它会右对齐产生一个不变的显示。13.12.2 BorderLayout
布局管理器有四边和中间区域的概念。当我们增加一些事物到使用BorderLayout的面板上时我们必须使用add()方法将一个字符串对象作为它的第一个自变量,并且字符串必须指定(正确的大写)“North”(上),“South”(下),“west”(左),“East”(右)或者“Center”。如果我们拼写错误或没有大写,就会得到一个编译时的错误,并且程序片不会像你所期望的那样运行。幸运的是,我们会很快发现在Java 1.1中有了更多改进。
这是一个简单的程序例子://: BorderLayout1.java
// Demonstrating the BorderLayout
import java.awt.*;
import java.applet.*;public class BorderLayout1 extends Applet {
  public void init() {
    int i = 0;
    setLayout(new BorderLayout());
    add("North", new Button("Button " + i++));
    add("South", new Button("Button " + i++));
    add("East", new Button("Button " + i++));
    add("West", new Button("Button " + i++));
    add("Center", new Button("Button " + i++));
  }
} ///:~除了“Center”的每一个位置,当元素在其它空间内扩大到最大时,我们会把它压缩到适合空间的最小尺寸。但是,“Center”扩大后只会占据中心位置。
BorderLayout是应用程序和对话框的默认布局管理器。13.12.3 GridLayout
GridLayout允许我们建立一个组件表。添加那些组件时,它们会按从左到右、从上到下的顺序在网格中排列。在构建器里,需要指定自己希望的行、列数,它们将按正比例展开。//: GridLayout1.java
// Demonstrating the GridLayout
import java.awt.*;
import java.applet.*;public class GridLayout1 extends Applet {
  public void init() {
    setLayout(new GridLayout(7,3));
    for(int i = 0; i < 20; i++)
      add(new Button("Button " + i));
  }
} ///:~在这个例子里共有21个空位,但却只有20个按钮,最后的一个位置作留空处理;注意对GridLayout来说,并不存在什么“均衡”处理。13.12.4 CardLayout
CardLayout允许我们在更复杂的拥有真正的文件夹卡片与一条边相遇的环境里创建大致相同于“卡片式对话框”的布局,我们必须压下一个卡片使不同的对话框带到前面来。在AWT里不是这样的:CardLayout是简单的空的空格,我们可以自由地把新卡片带到前面来。(JFC/Swing库包括卡片式的窗格看起来非常的棒,且可以我们处理所有的细节。)1. 联合布局(Combining layouts)
下面的例子联合了更多的布局类型,在最初只有一个布局管理器被程序片或应用程序操作看起来相当的困难。这是事实,但如果我们创建更多的面板对象,每个面板都能拥有一个布局管理器,并且像被集成到程序片或应用程序中一样使用程序片或应用程序的布局管理器。这就象下面程序中的一样给了我们更多的灵活性://: CardLayout1.java
// Demonstrating the CardLayout
import java.awt.*;
import java.applet.Applet;class ButtonPanel extends Panel {
  ButtonPanel(String id) {
    setLayout(new BorderLayout());
    add("Center", new Button(id));
  }
}public class CardLayout1 extends Applet {
  Button
    first = new Button("First"),
    second = new Button("Second"),
    third = new Button("Third");
  Panel cards = new Panel();
  CardLayout cl = new CardLayout();
  public void init() {
    setLayout(new BorderLayout());
    Panel p = new Panel();
    p.setLayout(new FlowLayout());
    p.add(first);
    p.add(second);
    p.add(third);
    add("North", p);
    cards.setLayout(cl);
    cards.add("First card", 
      new ButtonPanel("The first one"));
    cards.add("Second card", 
      new ButtonPanel("The second one"));
    cards.add("Third card", 
      new ButtonPanel("The third one"));
    add("Center", cards);
  }
  public boolean action(Event evt, Object arg) {
    if (evt.target.equals(first)) {
      cl.first(cards);
    } 
    else if (evt.target.equals(second)) {
      cl.first(cards);
      cl.next(cards);
    } 
    else if (evt.target.equals(third)) {
      cl.last(cards);
    } 
    else 
      return super.action(evt, arg);
    return true;
  }
} ///:~这个例子首先会创建一种新类型的面板:BottonPanel(按钮面板)。它包括一个单独的按钮,安放在BorderLayout的中央,那意味着它将充满整个的面板。按钮上的标签将让我们知道我们在CardLayout上的那个面板上。
在程序片里,面板卡片上将存放卡片和布局管理器CL因为CardLayout必须组成类,因为当我们需要处理卡片时我们需要访问这些句柄。
这个程序片变成使用BorderLayout来取代它的默认FlowLayout,创建面板来容纳三个按钮(使用FlowLayout),并且这个面板安置在程序片末尾的“North”。卡片面板增加到程序片的“Center”里,有效地占据面板的其余地方。
当我们增加BottonPanels(或者任何其它我们想要的组件)到卡片面板时,add()方法的第一个自变量不是“North”,“South”等等。相反的是,它是一个描述卡片的字符串。如果我们想轻击那张卡片使用字符串,我们就可以使用,虽然这字符串不会显示在卡片的任何地方。使用的方法不是使用action();代之使用first()、next()和last()等方法。请查看我们有关其它方法的文件。
在Java中,使用的一些卡片式面板结构十分的重要,因为(我们将在后面看到)在程序片编程中使用的弹出式对话框是十分令人沮丧的。对于Java 1.0版的程序片而言,CardLayout是唯一有效的取得很多不同的“弹出式”的窗体。13.12.5 GridBagLayout
很早以前,人们相信所有的恒星、行星、太阳及月亮都围绕地球公转。这是直观的观察。但后来天文学家变得更加的精明,他们开始跟踪个别星体的移动,它们中的一些似乎有时在轨道上缓慢运行。因为天文学家知道所有的天体都围绕地球公转,天文学家花费了大量的时间来讨论相关的方程式和理论去解释天体对象的运行。当我们试图用GridBagLayout来工作时,我们可以想像自己为一个早期的天文学家。基础的条例是(公告:有趣的是设计者居然在太阳上(这可能是在天体图中标错了位置所致,译者注))所有的天体都将遵守规则来运行。哥白尼日新说(又一次不顾嘲讽,发现太阳系内的所有的行星围绕太阳公转。)是使用网络图来判断布局,这种方法使得程序员的工作变得简单。直到这些增加到Java里,我们忍耐(持续的冷嘲热讽)西班牙的GridBagLayout和GridBagConstraints狂热宗教。我们建议废止GridBagLayout。取代它的是,使用其它的布局管理器和特殊的在单个程序里联合几个面板使用不同的布局管理器的技术。我们的程序片看起来不会有什么不同;至少不足以调整GridBagLayout限制的麻烦。对我而言,通过一个例子来讨论它实在是令人头痛(并且我不鼓励这种库设计)。相反,我建议您从阅读Cornell和Horstmann撰写的《核心Java》(第二版,Prentice-Hall出版社,1997年)开始。
在这范围内还有其它的:在JFC/Swing库里有一个新的使用Smalltalk的受人欢迎的“Spring and Struts”布局管理器并且它能显著地减少GridBagLayout的需要。13.13 action的替代品
正如早先指出的那样,action()并不是我们对所有事进行分类后自动为handleEvent()调用的唯一方法。有三个其它的被调用的方法集,如果我们想捕捉某些类型的事件(键盘、鼠标和焦点事件),因此我们不得不过载规定的方法。这些方法是定义在基础类组件里,所以他们几乎在所有我们可能安放在窗体中的组件中都是有用的。然而,我们也注意到这种方法在Java 1.1版中是不被支持的,同样尽管我们可能注意到继承代码利用了这种方法,我们将会使用Java 1.1版的方法来代替(本章后面有详细介绍)。组件方法 何时调用action(Event evt, Object what) 当典型的事件针对组件发生(例如,当按下一个按钮或下拉列表项目被选中)时调用
keyDown(Event evt, int key) 当按键被按下,组件拥有焦点时调用。第二个自变量是按下的键并且是冗余的是从evt.key处复制来的
keyup(Event evt, int key) 当按键被释放,组件拥有焦点时调用
lostFocus(Event evt, Object what) 焦点从目标处移开时调用。通常,what是从evt.arg里冗余复制的
gotFocus(Event evt, Object what) 焦点移动到目标时调用
mouseDown(Event evt, int x,int y) 一个鼠标按下存在于组件之上,在X,Y座标处时调用
mouseUp(Event evt, int x, int y) 一个鼠标升起存在于组件之上时调用
mouseMove(Event evt, int x, int y) 当鼠标在组件上移动时调用
mouseDrag(Event evt, int x, int y) 鼠标在一次mouseDown事件发生后拖动。所有拖动事件都会报告给内部发生了mouseDown事件的那个组件,直到遇到一次mouseUp为止
mouseEnter(Event evt, int x, int y) 鼠标从前不在组件上方,但目前在
mouseExit(Event evt, int x, int y) 鼠标曾经位于组件上方,但目前不在当我们处理特殊情况时——一个鼠标事件,例如,它恰好是我们想得到的鼠标事件存在的座标,我们将看到每个程序接收一个事件连同一些我们所需要的信息。有趣的是,当组件的handleEvent()调用这些方法时(典型的事例),附加的自变量总是多余的因为它们包含在事件对象里。事实上,如果我们观察component.handleEvent()的源代码,我们能发现它显然将增加的自变量抽出事件对象(这可能是考虑到在一些语言中无效率的编码,但请记住Java的焦点是安全的,不必担心。)试验对我们表明这些事件事实上在被调用并且作为一个有趣的尝试是值得创建一个过载每个方法的程序片,(action()的过载在本章的其它地方)当事件发生时显示它们的相关数据。
这个例子同样向我们展示了怎样制造自己的按钮对象,因为它是作为目标的所有事件权益来使用。我可能会首先(也是必须的)假设制造一个新的按钮,我们从按钮处继承。但它并不能运行。取而代之的是,我们从画布组件处(一个非常普通组件)继承,并在其上不使用paint()方法画出一个按钮。正如我们所看到的,自从一些代码混入到画按钮中去,按钮根本就不运行,这实在是太糟糕了。(如果您不相信我,试图在例子中为画布组件交换按钮,请记住调用称为super的基础类构建器。我们会看到按钮不会被画出,事件也不会被处理。)
myButton类是明确说明的:它只和一个自动事件(AutoEvent)“父窗口”一起运行(父窗口不是一个基础类,它是按钮创建和存在的窗口。)。通过这个知识,myButton可能进入到父窗口并且处理它的文字字段,必然就能将状态信息写入到父窗口的字段里。当然这是一种非常有限的解决方法,myButton仅能在连结AutoEvent时被使用。这种代码有时称为“高度结合”。但是,制造myButton更需要很多的不是为例子(和可能为我们将写的一些程序片)担保的努力。再者,请注意下面的代码使用了Java 1.1版不支持的API。//: AutoEvent.java
// Alternatives to action()
import java.awt.*;
import java.applet.*;
import java.util.*;class MyButton extends Canvas {
  AutoEvent parent;
  Color color;
  String label;
  MyButton(AutoEvent parent, 
           Color color, String label) {
    this.label = label;
    this.parent = parent;
    this.color = color;
  }
  public void paint(Graphics  g) {
    g.setColor(color);
    int rnd = 30;
    g.fillRoundRect(0, 0, size().width, 
                    size().height, rnd, rnd);
    g.setColor(Color.black);
    g.drawRoundRect(0, 0, size().width, 
                    size().height, rnd, rnd);
    FontMetrics fm = g.getFontMetrics();
    int width = fm.stringWidth(label);
    int height = fm.getHeight();
    int ascent = fm.getAscent();
    int leading = fm.getLeading();
    int horizMargin = (size().width - width)/2;
    int verMargin = (size().height - height)/2;
    g.setColor(Color.white);
    g.drawString(label, horizMargin, 
                 verMargin + ascent + leading);
  }
  public boolean keyDown(Event evt, int key) {
    TextField t = 
      (TextField)parent.h.get("keyDown");
    t.setText(evt.toString());
    return true;
  }
  public boolean keyUp(Event evt, int key) {
    TextField t = 
      (TextField)parent.h.get("keyUp");
    t.setText(evt.toString());
    return true;
  }
  public boolean lostFocus(Event evt, Object w) {
    TextField t = 
      (TextField)parent.h.get("lostFocus");
    t.setText(evt.toString());
    return true;
  }
  public boolean gotFocus(Event evt, Object w) {
    TextField t = 
      (TextField)parent.h.get("gotFocus");
    t.setText(evt.toString());
    return true;
  }
  public boolean 
  mouseDown(Event evt,int x,int y) {
    TextField t = 
      (TextField)parent.h.get("mouseDown");
    t.setText(evt.toString());
    return true;
  }
  public boolean 
  mouseDrag(Event evt,int x,int y) {
    TextField t = 
      (TextField)parent.h.get("mouseDrag");
    t.setText(evt.toString());
    return true;
  }
  public boolean 
  mouseEnter(Event evt,int x,int y) {
    TextField t = 
      (TextField)parent.h.get("mouseEnter");
    t.setText(evt.toString());
    return true;
  }
  public boolean 
  mouseExit(Event evt,int x,int y) {
    TextField t = 
      (TextField)parent.h.get("mouseExit");
    t.setText(evt.toString());
    return true;
  }
  public boolean 
  mouseMove(Event evt,int x,int y) {
    TextField t = 
      (TextField)parent.h.get("mouseMove");
    t.setText(evt.toString());
    return true;
  }
  public boolean mouseUp(Event evt,int x,int y) {
    TextField t = 
      (TextField)parent.h.get("mouseUp");
    t.setText(evt.toString());
    return true;
  }
}public class AutoEvent extends Applet {
  Hashtable h = new Hashtable();
  String[] event = {
    "keyDown", "keyUp", "lostFocus", 
    "gotFocus", "mouseDown", "mouseUp", 
    "mouseMove", "mouseDrag", "mouseEnter", 
    "mouseExit"
  };
  MyButton 
    b1 = new MyButton(this, Color.blue, "test1"),
    b2 = new MyButton(this, Color.red, "test2");
  public void init() {
    setLayout(new GridLayout(event.length+1,2));
    for(int i = 0; i < event.length; i++) {
      TextField t = new TextField();
      t.setEditable(false);
      add(new Label(event[i], Label.CENTER)); 
      add(t);
      h.put(event[i], t);
    }
    add(b1);
    add(b2);
  }
} ///:~我们可以看到构建器使用利用自变量同名的方法,所以自变量被赋值,并且使用this来区分:
this.label = label;
paint()方法由简单的开始:它用按钮的颜色填充了一个“圆角矩形”,然后画了一个黑线围绕它。请注意size()的使用决定了组件的宽度和长度(当然,是像素)。这之后,paint()看起来非常的复杂,因为有大量的预测去计算出怎样利用“font metrics”集中按钮的标签到按钮里。我们能得到一个相当好的关于继续关注方法调用的主意,它将程序中那些相当平凡的代码挑出,当我们想集中一个标签到一些组件里时,我们正好可以对它进行剪切和粘贴。
您直到注意到AutoEvent类才能正确地理解keyDown(),keyUp()及其它方法的运行。这包含一个Hashtable(译者注:散列表)去控制字符串来描述关于事件处理的事件和TextField类型。当然,这些能被静态的创建而不是放入Hashtable但我认为您会同意它是更容易使用和改变的。特别是,如果我们需要在AutoEvent中增加或删除一个新的事件类型,我们只需要简单地在事件列队中增加或删除一个字符串——所有的工作都自动地完成了。
我们查出在keyDown(),keyup()及其它方法中的字符串的位置回到myButton中。这些方法中的任何一个都用父句柄试图回到父窗口。父类是一个AutoEvent,它包含Hashtable h和get()方法,当拥有特定的字符串时,将对一个我们知道的TextField对象产生一个句柄(因此它被选派到那)。然后事件对象修改显示在TextField中的字符串陈述。从我们可以真正注意到举出的例子在我们的程序中运行事件时以来,可以发现这个例子运行起来颇为有趣的。