ps:本章应用少,暂时不编辑格式,请跳过本章节
在Java 1.0中,图形用户接口(GUI)库最初的设计目标是让程序员构建一个通用的GUI,使其在所有平台上都能正常显示。 但遗憾的是,这个目标并未达到。事实上,Java 1.0版的“抽象Windows工具包”(AWT)产生的是在各系统看来都同样欠佳的图形用户接口。除此之外,它还限制我们只能使用四种字体,并且不能访问操作系统中现有的高级GUI元素。同时,Jave1.0版的AWT编程模型也不是面向对象的,极不成熟。这类情况在Java1.1版的AWT事件模型中得到了很好的改进,例如:更加清晰、面向对象的编程、遵循Java Beans的范例,以及一个可轻松创建可视编程环境的编程组件模型。Java1.2为老的Java 1.0 AWT添加了Java基础类(AWT),这是一个被称为“Swing”的GUI的一部分。丰富的、易于使用和理解的Java Beans能经过拖放操作(像手工编程一样的好),创建出能使程序员满意的GUI。软件业的“3次修订版”规则看来对于程序设计语言也是成立的(一个产品除非经过第3次修订,否则不会尽如人意)。 Java的主要设计目的之一是建立程序片,也就是建立运行在WEB 浏览器上的小应用程序。由于它们必须是安全的,所以程序片在运行时必须加以限制。无论怎样,它们都是支持客户端编程的强有力的工具,一个重要的应用便是在Web上。 在一个程序片中编程会受到很多的限制,我们一般说它“在沙箱内”,这是由于Java运行时一直会有某个东西——即Java运行期安全系统——在监视着我们。Jave 1.1为程序片提供了数字签名,所以可选出能信赖的程序片去访问主机。不过,我们也能跳出沙箱的限制写出可靠的程序。在这种情况下,我们可访问操作系统中的其他功能。在这本书中我们自始至终编写的都是可靠的程序,但它们成为了没有图形组件的控制台程序。AWT也能用来为可靠的程序建立GUI接口。 在这一章中我们将先学习使用老的AWT工具,我们会与许多支持和使用AWT的代码程序样本相遇。尽管这有一些困难,但却是必须的,因为我们必须用老的AWT来维护和阅读传统的Java代码。有时甚至需要我们编写AWT代码去支持不能从Java1.0升级的环境。在本章第二部分,我们将学习Java 1.1版中新的AWT结构并会看到它的事件模型是如此的优秀(如果能掌握的话,那么在编制新的程序时就可使用这最新的工具。最后,我们将学习新的能像类库一样加入到Java 1.1版中的JFC/Swing组件,这意味着不需要升级到Java 1.2便能使用这一类库。 大多数的例程都将展示程序片的建立,这并不仅仅是因为这非常的容易,更因为这是AWT的主要作用。另外,当用AWT创建一个可靠的程序时,我们将看到处理程序的不同之处,以及怎样创建能在命令行和浏览器中运行的程序。 请注意的是这不是为了描述类的所有程序的综合解释。这一章将带领我们从摘要开始。当我们查找更复杂的内容时,请确定我们的信息浏览器通过查找类和方法来解决编程中的问题(如果我们正在使用一个开发环境,信息浏览器也许是内建的;如果我们使用的是SUN公司的JDK则这时我们要使用WEB浏览器并在Java根目录下面开始)。附录F列出了用于深入学习库知识的其他一些参考资料。
13.1 为何要用AWT? 对于本章要学习的“老式”AWT,它最严重的缺点就是它无论在面向对象设计方面,还是在GUI开发包设计方面,都有不尽如人意的表现。它使我们回到了程序设计的黑暗年代(换成其他话就是“拙劣的”、“可怕的”、“恶劣的”等等)。必须为执行每一个事件编写代码,包括在其他环境中利用“资源”即可轻松完成的一些任务。 许多象这样的问题在Java 1.1里都得到了缓解或排除,因为: (1)Java 1.1的新型AWT是一个更好的编程模型,并向更好的库设计迈出了可喜的一步。而Java Beans则是那个库的框架。 (2)“GUI构建器”(可视编程环境)将适用于所有开发系统。在我们用图形化工具将组件置入窗体的时候,Java Beans和新的AWT使GUI构建器能帮我们自动完成代码。其它组件技术如ActiveX等也将以相同的形式支持。
既然如此,为什么还要学习使用老的AWT呢?原因很简单,因为它的存在是个事实。就目前来说,这个事实对我们来说显得有些不利,它涉及到面向对象库设计的一个宗旨:一旦我们在库中公布一个组件,就再不能去掉它。如去掉它,就会损害别人已存在的代码。另外,当我们学习Java和所有使用老AWT的程序时,会发现有许多原来的代码使用的都是老式AWT。 AWT必须能与固有操作系统的GUI组件打交通,这意味着它需要执行一个程序片不可能做到的任务。一个不被信任的程序片在操作系统中不能作出任何直接调用,否则它会对用户的机器做出不恰当的事情。一个不被信任的程序片不能访问重要的功能。例如,“在屏幕上画一个窗口”的唯一方法是通过调用拥有特殊接口和安全检查的标准Java库。Sun公司的原始模型创建的信任库将仅仅供给Web浏览器中的Java系统信任关系自动授权器使用,自动授权器将控制怎样进入到库中去。 但当我们想增加操作系统中访问新组件的功能时该怎么办?等待Sun来决定我们的扩展被合并到标准的Java库中,但这不一定会解决我们的问题。Java 1.1版中的新模型是“信任代码”或“签名代码”,因此一个特殊服务器将校验我们下载的、由规定的开发者使用的公共密钥加密系统的代码。这样我们就可知道代码从何而来,那真的是Bob的代码,还是由某人伪装成Bob的代码。这并不能阻止Bob犯错误或作某些恶意的事,但能防止Bob逃避匿名制造计算机病毒的责任。一个数字签名的程序片——“被信任的程序片”——在Java 1.1版能进入我们的机器并直接控制它,正像一些其它的应用程序从信任关系自动授权机中得到“信任”并安装在我们的机器上。 这是老AWT的所有特点。老的AWT代码将一直存在,新的Java编程者在从旧的书本中学习时将会遇到老的AWT代码。同样,老的AWT也是值得去学习的,例如在一个只有少量库的例程设计中。老的AWT所包括的范围在不考虑深度和枚举每一个程序和类,取而代之的是给了我们一个老AWT设计的概貌。
13.2 基本程序片 库通常按照它们的功能来进行组合。一些库,例如使用过的,便中断搁置起来。标准的Java库字符串和矢量类就是这样的一个例子。其他的库被特殊地设计,例如构建块去建立其它的库。库中的某些类是应用程序的框架,其目的是协助我们构建应用程序,在提供类或类集的情况下产生每个特定应用程序的基本活动状况。然后,为我们定制活动状况,必须继承应用程序类并且废弃程序的权益。应用程序框架的默认控制结构将在特定的时间调用我们废弃的程序。应用程序的框架是“分离、改变和中止事件”的好例子,因为它总是努力去尝试集中在被废弃的所有特殊程序段。 程序片利用应用程序框架来建立。我们从类中继承程序片,并且废弃特定的程序。大多数时间我们必须考虑一些不得不运行的使程序片在WEB页面上建立和使用的重要方法。这些方法是: Method
Operation
init( )
Called when the applet is first created to perform first-time initialization of the applet
start( )
Called every time the applet moves into sight on the Web browser to allow the applet to start up its normal operations (especially those that are shut off by stop( )). Also called after init( ).
paint( )
Part of the base class Component (three levels of inheritance up). Called as part of an update( ) to perform special painting on the canvas of an applet.
stop( )
Called every time the applet moves out of sight on the Web browser to allow the applet to shut off expensive operations. Also called right before destroy( ).
destroy( )
Called when the applet is being unloaded from the page to perform final release of resources when the applet is no longer used
方法 作用
init() 程序片第一次被创建,初次运行初始化程序片时调用 start() 每当程序片进入Web浏览器中,并且允许程序片启动它的常规操作时调用(特殊的程序片被stop()关闭);同样在init()后调用 paint() 基础类Component的一部分(继承结构中上溯三级)。作为update()的一部分调用,以便对程序片的画布进行特殊的描绘 stop() 每次程序片从Web浏览器的视线中离开时调用,使程序片能关闭代价高昂的操作;同样在调用destroy()前调用 destroy() 程序片不再需要,将它从页面中卸载时调用,以执行资源的最后清除工作
现在来看一看paint()方法。一旦Component(目前是程序片)决定自己需要更新,就会调用这个方法——可能是由于它再次回转屏幕,首次在屏幕上显示,或者是由于其他窗口临时覆盖了你的Web浏览器。此时程序片会调用它的update()方法(在基础类Component中定义),该方法会恢复一切该恢复的东西,而调用paint()正是这个过程的一部分。没必要对paint()进行过载处理,但构建一个简单的程序片无疑是方便的方法,所以我们首先从paint()方法开始。 update()调用paint()时,会向其传递指向Graphics对象的一个句柄,那个对象代表准备在上面描绘(作图)的表面。这是非常重要的,因为我们受到项目组件的外观的限制,因此不能画到区域外,这可是一件好事,否则我们就会画到线外去。在程序片的例子中,程序片的外观就是这界定的区域。 图形对象同样有一系列我们可对其进行的操作。这些操作都与在画布上作图有关。所以其中的大部分都要涉及图像、几何菜状、圆弧等等的描绘(注意如果有兴趣,可在Java文档中找到更详细的说明)。有些方法允许我们画出字符,而其中最常用的就是drawString()。对于它,需指出自己想描绘的String(字串),并指定它在程序片作图区域的起点。这个位置用像素表示,所以它在不同的机器上看起来是不同的,但至少是可以移植的。 根据这些信息即可创建一个简单的程序片: //: Applet1.java // Very simple applet package c13; import java.awt.; import java.applet.;
public class Applet1 extends Applet { public void paint(Graphics g) { g.drawString("First applet", 10, 10); } } ///:~
注意这个程序片不需要有一个main()。所有内容都封装到应用程序框架中;我们将所有启动代码都放在init()里。 必须将这个程序放到一个Web页中才能运行,而只能在支持Java的Web浏览器中才能看到此页。为了将一个程序片置入Web页,需要在那个Web页的代码中设置一个特殊的标记(注释①),以指示网页装载和运行程序片。这就是applet标记,它在Applet1中的样子如下: <applet code=Applet1 width=200 height=200>
①:本书假定读者已掌握了HTML的基本知识。这些知识不难学习,有许多书籍和网上资源都可以提供帮助。
其中,code值指定了.class文件的名字,程序片就驻留在那个文件中。width和height指定这个程序片的初始尺寸(如前所述,以像素为单位)。还可将另一些东西放入applet标记:用于在因特网上寻找其他.class文件的位置(codebase)、对齐和排列信息(align)、使程序片相互间能够通信的一个特殊标识符(name)以及用于提供程序片能接收的信息的参数。参数采取下述形式:
13.2.1 程序片的测试 我们可在不必建立网络连接的前提下进行一次简单的测试,方法是启动我们的Web浏览器,然后打开包含了程序片标签的HTML文件(Sun公司的JDK同样包括一个称为“程序片观察器”的工具,它能挑出html文件的
②;由于程序片观察器会忽略除APPLET标记之外的任何东西,所以可将那些标记作为注释置入Java源码: // 这样就可直接执行“appletviewer MyApplet.java”,不必再创建小的HTML文件来完成测试。
若想在Web站点上试验,还会碰到另一些麻烦。首先,我们必须有一个Web站点,这对大多数人来说都意味着位于远程地点的一家服务提供商(ISP)。然后必须通过某种途径将HTML文件和.class文件从自己的站点移至ISP机器上正确的目录(WWW目录)。这一般是通过采用“文件传输协议”(FTP)的程序来做成的,网上可找到许多这样的免费程序。所以我们要做的全部事情似乎就是用FTP协议将文件移至ISP的机器,然后用自己的浏览器连接网站和HTML文件;假如程序片正确装载和执行,就表明大功告成。但真是这样吗? 但这儿我们可能会受到愚弄。假如Web浏览器在服务器上找不到.class文件,就会在你的本地机器上搜寻CLASSPATH。所以程序片或许根本不能从服务器上正确地装载,但在你看来却是一切正常的,因为浏览器在你的机器上找到了它需要的东西。但在其他人访问时,他们的浏览器就无法找到那些类文件。所以在测试时,必须确定已从自己的机器删除了相关的.class文件,以确保测试结果的真实。 我自己就遇到过这样的一个问题。当时是将程序片置入一个package(包)中。上载了HTML文件和程序片后,由于包名的问题,程序片的服务器路径似乎陷入了混乱。但是,我的浏览器在本地类路径(CLASSPATH)中找到了它。这样一来,我就成了能够成功装载程序片的唯一一个人。后来我花了一些时间才发现原来是package语句有误。一般地,应该将package语句置于程序片的外部。
13.2.2 一个更图形化的例子 这个程序不会太令人紧张,所以让我们试着增加一些有趣的图形组件。 //: Applet2.java // Easy graphics import java.awt.; import java.applet.;
public class Applet2 extends Applet { public void paint(Graphics g) { g.drawString("Second applet", 10, 15); g.draw3DRect(0, 0, 100, 20, true); } } ///:~
这个程序用一个方框将字符串包围起来。当然,所有数字都是“硬编码”的(指数字固定于程序内部),并以像素为基础。所以在一些机器上,框会正好将字串围住;而在另一些机器上,也许根本看不见这个框,因为不同机器安装的字体也会有所区别。 对Graphic类而言,可在帮助文档中找到另一些有趣的内容。大多数涉及图形的活动都是很有趣的,所有我将更多的试验留给读者自己去进行。
13.2.3 框架方法的演示 观看框架方法的实际运作是相当有趣的(这个例子只使用init(),start()和stop(),因为paint()和destroy()非常简单,很容易就能掌握)。下面的程序片将跟踪这些方法调用的次数,并用paint()将其显示出来: //: Applet3.java // Shows init(), start() and stop() activities import java.awt.; import java.applet.;
public class Applet3 extends Applet { String s; int inits = 0; int starts = 0; int stops = 0; public void init() { inits++; } public void start() { starts++; } public void stop() { stops++; } public void paint(Graphics g) { s = "inits: " + inits + ", starts: " + starts + ", stops: " + stops; g.drawString(s, 10, 10); } } ///:~
正常情况下,当我们过载一个方法时,需检查自己是否需要调用方法的基础类版本,这是十分重要的。例如,使用init()时可能需要调用super.init()。然而,Applet文档特别指出init()、start()和stop()在Applet中没有用处,所以这里不需要调用它们。 试验这个程序片时,会发现假如最小化WEB浏览器,或者用另一个窗口将其覆盖,那么就不能再调用stop()和start()(这一行为会随着不同的实现方案变化;可考虑将Web浏览器的行为同程序片观察器的行为对照一下)。调用唯一发生的场合是在我们转移到一个不同的Web页,然后返回包含了程序片的那个页时。
13.3 制作按钮 制作一个按钮非常简单:只需要调用Button构建器,并指定想在按钮上出现的标签就行了(如果不想要标签,亦可使用默认构建器,但那种情况极少出现)。可参照后面的程序为按钮创建一个句柄,以便以后能够引用它。 Button是一个组件,象它自己的小窗口一样,会在更新时得以重绘。这意味着我们不必明确描绘一个按钮或者其他任意种类的控件;只需将它们纳入窗体,以后的描绘工作会由它们自行负责。所以为了将一个按钮置入窗体,需要过载init()方法,而不是过载paint(): //: Button1.java // Putting buttons on an applet import java.awt.; import java.applet.;
public class Button1 extends Applet { Button b1 = new Button("Button 1"), b2 = new Button("Button 2"); public void init() { add(b1); add(b2); } } ///:~
但这还不足以创建Button(或其他任何控件)。必须同时调用Applet add()方法,令按钮放置在程序片的窗体中。这看起来似乎比实际简单得多,因为对add()的调用实际会(间接地)决定将控件放在窗体的什么地方。对窗体布局的控件马上就要讲到。
13.4 捕获事件 大家可注意到假如编译和运行上面的程序片,按下按钮后不会发生任何事情。必须进入程序片内部,编写用于决定要发生什么事情的代码。对于由事件驱动的程序设计,它的基本目标就是用代码捕获发生的事件,并由代码对那些事件作出响应。事实上,GUI的大部分内容都是围绕这种事件驱动的程序设计展开的。 经过本书前面的学习,大家应该有了面向对象程序设计的一些基础,此时可能会想到应当有一些面向对象的方法来专门控制事件。例如,也许不得不继承每个按钮,并过载一些“按钮按下”方法(尽管这显得非常麻烦有有限)。大家也可能认为存在一些主控“事件”类,其中为希望响应的每个事件都包含了一个方法。 在对象以前,事件控制的典型方式是switch语句。每个事件都对应一个独一无二的整数编号;而且在主事件控制方法中,需要专门为那个值写一个switch。 Java 1.0的AWT没有采用任何面向对象的手段。此外,它也没有使用switch语句,没有打算依靠那些分配给事件的数字。相反,我们必须创建if语句的一个嵌套系列。通过if语句,我们需要尝试做的事情是侦测到作为事件“目标”的对象。换言之,那是我们关心的全部内容——假如某个按钮是一个事件的目标,那么它肯定是一次鼠标点击,并要基于那个假设继续下去。但是,事件里也可能包含了其他信息。例如,假如想调查一次鼠标点击的像素位置,以便画一条引向那个位置的线,那么Event对象里就会包含那个位置的信息(也要注意Java 1.0的组件只能产生有限种类的事件,而Java 1.1和Swing/JFC组件则可产生完整的一系列事件)。 Java 1.0版的AWT方法串联的条件语句中存在action()方法的调用。虽然整个Java 1.0版的事件模型不兼容Java 1.1版,但它在还不支持Java1.1版的机器和运行简单的程序片的系统中更广泛地使用,忠告您使用它会变得非常的舒适,包括对下面使用的action()程序方法而言。 action()拥有两个自变量:第一个是事件的类型,包括所有的触发调用action()的事件的有关信息。例如鼠标单击、普通按键按下或释放、特殊按键按下或释放、鼠标移动或者拖动、事件组件得到或丢失焦点,等等。第二个自变量通常是我们忽略的事件目标。第二个自变量封装在事件目标中,所以它像一个自变量一样的冗长。 需调用action()时情况非常有限:将控件置入窗体时,一些类型的控件(按钮、复选框、下拉列表单、菜单)会发生一种“标准行动”,从而随相应的Event对象发起对action()的调用。比如对按钮来说,一旦按钮被按下,而且没有再多按一次,就会调用它的action()方法。这种行为通常正是我们所希望的,因为这正是我们对一个按钮正常观感。但正如本章后面要讲到的那样,还可通过handleEvent()方法来处理其他许多类型的事件。 前面的例程可进行一些扩展,以便象下面这样控制按钮的点击: //: Button2.java // Capturing button presses import java.awt.; import java.applet.;
public class Button2 extends Applet { Button b1 = new Button("Button 1"), b2 = new Button("Button 2"); public void init() { add(b1); add(b2); } 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: else return super.action(evt, arg); return true; // We've handled it here } } ///:~
为了解目标是什么,需要向Event对象询问它的target(目标)成员是什么,然后用equals()方法检查它是否与自己感兴趣的目标对象句柄相符。为所有感兴趣的对象写好句柄后,必须在末尾的else语句中调用super.action(evt, arg)方法。我们在第7章已经说过(有关多形性的那一章),此时调用的是我们过载过的方法,而非它的基础类版本。然而,基础类版本也针对我们不感兴趣的所有情况提供了相应的控制代码。除非明确进行,否则它们是不会得到调用的。返回值指出我们是否已经处理了它,所以假如确实与一个事件相符,就应返回true;否则就返回由基础类event()返回的东西。 对这个例子来说,最简单的行动就是打印出到底是什么按钮被按下。一些系统允许你弹出一个小消息窗口,但Java程序片却防碍窗口的弹出。不过我们可以用调用Applet方法的getAppletContext()来访问浏览器,然后用showStatus()在浏览器窗口底部的状态栏上显示一条信息(注释③)。还可用同样的方法打印出对事件的一段完整说明文字,方法是调用getAppletConext().showStatus(evt + "")。空字串会强制编译器将evt转换成一个字符串。这些报告对于测试和调试特别有用,因为浏览器可能会覆盖我们的消息。
③:ShowStatus()也属于Applet的一个方法,所以可直接调用它,不必调用getAppletContext()。
尽管看起来似乎很奇怪,但我们确实也能通过event()中的第二个参数将一个事件与按钮上的文字相配。采用这种方法,上面的例子就变成了: //: Button3.java // Matching events on button text import java.awt.; import java.applet.;
public class Button3 extends Applet { Button b1 = new Button("Button 1"), b2 = new Button("Button 2"); public void init() { add(b1); add(b2); } public boolean action (Event evt, Object arg) { if(arg.equals("Button 1")) getAppletContext().showStatus("Button 1"); else if(arg.equals("Button 2")) getAppletContext().showStatus("Button 2"); // Let the base class handle it: else return super.action(evt, arg); return true; // We've handled it here } } ///:~
很难确切知道equals()方法在这儿要做什么。这种方法有一个很大的问题,就是开始使用这个新技术的Java程序员至少需要花费一个受挫折的时期来在比较按钮上的文字时发现他们要么大写了要么写错了(我就有这种经验)。同样,如果我们改变了按钮上的文字,程序代码将不再工作(但我们不会得到任何编译时和运行时的信息)。所以如果可能,我们就得避免使用这种方法。
13.5 文本字段 “文本字段”是允许用户输入和编辑文字的一种线性区域。文本字段从文本组件那里继承了让我们选择文字、让我们像得到字符串一样得到选择的文字,得到或设置文字,设置文本字段是否可编辑以及连同我们从在线参考书中找到的相关方法。下面的例子将证明文本字段的其它功能;我们能注意到方法名是显而易见的: //: TextField1.java // Using the text field control import java.awt.; import java.applet.;
public class TextField1 extends Applet { Button b1 = new Button("Get Text"), b2 = new Button("Set Text"); TextField t = new TextField("Starting text", 30); String s = new String(); public void init() { add(b1); add(b2); add(t); } public boolean action (Event evt, Object arg) { if(evt.target.equals(b1)) { getAppletContext().showStatus(t.getText()); s = t.getSelectedText(); if(s.length() == 0) s = t.getText(); t.setEditable(true); } else if(evt.target.equals(b2)) { t.setText("Inserted by Button 2: " + s); t.setEditable(false); } // Let the base class handle it: else return super.action(evt, arg); return true; // We've handled it here } } ///:~
有几种方法均可构建一个文本字段;其中之一是提供一个初始字符串,并设置字符域的大小。 按下按钮1 是得到我们用鼠标选择的文字就是得到字段内所有的文字并转换成字符串S。它也允许字段被编辑。按下按钮2 放一条信息和字符串s到Text fields,并且阻止字段被编辑(尽管我们能够一直选择文字)。文字的可编辑性是通过setEditable()的真假值来控制的。
13.6 文本区域 “文本区域”很像文字字段,只是它拥有更多的行以及一些引人注目的更多的功能。另外你能在给定位置对一个文本字段追加、插入或者修改文字。这看起来对文本字段有用的功能相当不错,所以设法发现它设计的特性会产生一些困惑。我们可以认为如果我们处处需要“文本区域”的功能,那么可以简单地使用一个线型文字区域在我们将另外使用文本字段的地方。在Java 1.0版中,当它们不是固定的时候我们也得到了一个文本区域的垂直和水平方向的滚动条。在Java 1.1版中,对高级构建器的修改允许我们选择哪个滚动条是当前的。下面的例子演示的仅仅是在Java1.0版的状况下滚动条一直打开。在下一章里我们将看到一个证明Java 1.1版中的文字区域的例程。 //: TextArea1.java // Using the text area control import java.awt.; import java.applet.;
public class TextArea1 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); public void init() { add(b1); add(t1); add(b2); add(t2); add(b3); add(b4); } public boolean action (Event evt, Object arg) { if(evt.target.equals(b1)) getAppletContext().showStatus(t1.getText()); else if(evt.target.equals(b2)) { t2.setText("Inserted by Button 2"); t2.appendText(": " + t1.getText()); getAppletContext().showStatus(t2.getText()); } else if(evt.target.equals(b3)) { String s = " Replacement "; t2.replaceText(s, 3, 3 + s.length()); } else if(evt.target.equals(b4)) t2.insertText(" Inserted ", 10); // Let the base class handle it: else return super.action(evt, arg); return true; // We've handled it here } } ///:~
程序中有几个不同的“文本区域”构建器,这其中的一个在此处显示了一个初始字符串和行号和列号。不同的按钮显示得到、追加、修改和插入文字。
13.7 标签 标签准确地运作:安放一个标签到窗体上。这对没有标签的TextFields和Text areas 来说非常的重要,如果我们简单地想安放文字的信息在窗体上也能同样的使用。我们能像本章中第一个例程中演示的那样,使用drawString()里边的paint()在确定的位置去安置一个文字。当我们使用的标签允许我们通过布局管理加入其它的文字组件。(在这章的后面我们将进入讨论。) 使用构建器我们能创建一条包括初始化文字的标签(这是我们典型的作法),一个标签包括一行CENTER(中间)、LEFT(左)和RIGHT(右)(静态的结果取整定义在类标签里)。如果我们忘记了可以用getText()和getalignment()读取值,我们同样可以用setText()和setAlignment()来改变和调整。下面的例子将演示标签的特点: //: Label1.java // Using labels import java.awt.; import java.applet.;
public class Label1 extends Applet { TextField t1 = new TextField("t1", 10); Label labl1 = new Label("TextField t1"); Label labl2 = new Label(" "); Label labl3 = new Label(" ", Label.RIGHT); Button b1 = new Button("Test 1"); Button b2 = new Button("Test 2"); public void init() { add(labl1); add(t1); add(b1); add(labl2); add(b2); add(labl3); } public boolean action (Event evt, Object arg) { if(evt.target.equals(b1)) labl2.setText("Text set into Label"); else if(evt.target.equals(b2)) { if(labl3.getText().trim().length() == 0) labl3.setText("labl3"); if(labl3.getAlignment() == Label.LEFT) labl3.setAlignment(Label.CENTER); else if(labl3.getAlignment()==Label.CENTER) labl3.setAlignment(Label.RIGHT); else if(labl3.getAlignment() == Label.RIGHT) labl3.setAlignment(Label.LEFT); } else return super.action(evt, arg); return true; } } ///:~
首先是标签的最典型的用途:标记一个文本字段或文本区域。在例程的第二部分,当我们按下“test 1”按钮通过setText()将一串空的空格插入到的字段里。因为空的空格数不等于同样的字符数(在一个等比例间隔的字库里),当插入文字到标签里时我们会看到文字将被省略掉。在例子的第三部分保留的空的空格在我们第一次按下“test 2”会发现标签是空的(trim()删除了每个字符串结尾部分的空格)并且在开头的左列插入了一个短的标签。在工作的其余时间中我们按下按钮进行调整,因此就能看到效果。 我们可能会认为我们可以创建一个空的标签,然后用setText()安放文字在里面。然而我们不能在一个空标签内加入文字-这大概是因为空标签没有宽度-所以创建一个没有文字的空标签是没有用处的。在上面的例子里,“blank”标签里充满空的空格,所以它足够容纳后面加入的文字。 同样的,setAlignment()在我们用构建器创建的典型的文字标签上没有作用。这个标签的宽度就是文字的宽度,所以不能对它进行任何的调整。但是,如果我们启动一个长标签,然后把它变成短的,我们就可以看到调整的效果。 这些导致事件连同它们最小化的尺寸被挤压的状况被程序片使用的默认布局管理器所发现。有关布局管理器的部分包含在本章的后面。
13.8 复选框 复选框提供一个制造单一选择开关的方法;它包括一个小框和一个标签。典型的复选框有一个小的“X”(或者它设置的其它类型)或是空的,这依靠项目是否被选择来决定的。 我们会使用构建器正常地创建一个复选框,使用它的标签来充当它的自变量。如果我们在创建复选框后想读出或改变它,我们能够获取和设置它的状态,同样也能获取和设置它的标签。注意,复选框的大写是与其它的控制相矛盾的。 无论何时一个复选框都可以设置和清除一个事件指令,我们可以捕捉同样的方法做一个按钮。在下面的例子里使用一个文字区域枚举所有被选中的复选框: //: CheckBox1.java // Using check boxes import java.awt.; import java.applet.;
public class CheckBox1 extends Applet { TextArea t = new TextArea(6, 20); Checkbox cb1 = new Checkbox("Check Box 1"); Checkbox cb2 = new Checkbox("Check Box 2"); Checkbox cb3 = new Checkbox("Check Box 3"); public void init() { add(t); add(cb1); add(cb2); add(cb3); } public boolean action (Event evt, Object arg) { if(evt.target.equals(cb1)) trace("1", cb1.getState()); else if(evt.target.equals(cb2)) trace("2", cb2.getState()); else if(evt.target.equals(cb3)) trace("3", cb3.getState()); else return super.action(evt, arg); return true; } void trace(String b, boolean state) { if(state) t.appendText("Box " + b + " Set\n"); else t.appendText("Box " + b + " Cleared\n"); } } ///:~
trace()方法将选中的复选框名和当前状态用appendText()发送到文字区域中去,所以我们看到一个累积的被选中的复选框和它们的状态的列表。
13.9 单选钮 单选钮在GUI程序设计中的概念来自于老式的电子管汽车收音机的机械按钮:当我们按下一个按钮时,其它的按钮就会弹起。因此它允许我们强制从众多选择中作出单一选择。 AWT没有单独的描述单选钮的类;取而代之的是复用复选框。然而将复选框放在单选钮组中(并且修改它的外形使它看起来不同于一般的复选框)我们必须使用一个特殊的构建器象一个自变量一样的作用在checkboxGroup对象上。(我们同样能在创建复选框后调用setCheckboxGroup()方法。) 一个复选框组没有构建器的自变量;它存在的唯一理由就是聚集一些复选框到单选钮组里。一个复选框对象必须在我们试图显示单选钮组之前将它的状态设置成true,否则在运行时我们就会得到一个异常。如果我们设置超过一个的单选钮为true,只有最后的一个能被设置成真。 这里有个简单的使用单选钮的例子。注意我们可以像其它的组件一样捕捉单选钮的事件: //: RadioButton1.java // Using radio buttons import java.awt.; import java.applet.;
public class RadioButton1 extends Applet { TextField t = new TextField("Radio button 2", 30); CheckboxGroup g = new CheckboxGroup(); Checkbox cb1 = new Checkbox("one", g, false), cb2 = new Checkbox("two", g, true), cb3 = new Checkbox("three", g, false); public void init() { t.setEditable(false); add(t); add(cb1); add(cb2); add(cb3); } public boolean action (Event evt, Object arg) { if(evt.target.equals(cb1)) t.setText("Radio button 1"); else if(evt.target.equals(cb2)) t.setText("Radio button 2"); else if(evt.target.equals(cb3)) t.setText("Radio button 3"); else return super.action(evt, arg); return true; } } ///:~
显示的状态是一个文字字段在被使用。这个字段被设置为不可编辑的,因为它只是用来显示数据而不是收集。这演示了一个使用标签的可取之道。注意字段内的文字是由最早选择的单选钮“Radio button 2”初始化的。 我们可以在窗体中拥有相当多的复选框组。
13.10 下拉列表 下拉列表像一个单选钮组,它是强制用户从一组可实现的选择中选择一个对象的方法。而且,它是一个实现这点的相当简洁的方法,也最易改变选择而不至使用户感到吃力(我们可以动态地改变单选钮,但那种方法显然不方便)。Java的选择框不像Windows中的组合框可以让我从列表中选择或输入自己的选择。在一个选择框中你只能从列表中选择仅仅一个项目。在下面的例子里,选择框从一个确定输入的数字开始,然后当按下一个按钮时,新输入的数字增加到框里。你将可以看到选择框的一些有趣的状态: //: Choice1.java // Using drop-down lists import java.awt.; import java.applet.;
public class Choice1 extends Applet { String[] description = { "Ebullient", "Obtuse", "Recalcitrant", "Brilliant", "Somnescent", "Timorous", "Florid", "Putrescent" }; TextField t = new TextField(30); 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); } public boolean action (Event evt, Object arg) { if(evt.target.equals(c)) t.setText("index: " + c.getSelectedIndex()
文本字字段中显示的“selected index,"也就是当前选择的项目的序列号,在事件中选择的字符串就像action()的第二个自变量的字串符描述的一样好。 运行这个程序片时,请注意对Choice框大小的判断:在windows里,这个大小是在我们拉下列表时确定的。这意味着如果我们拉下列表,然后增加更多的项目到列表中,这项目将在那,但这个下拉列表不再接受(我们可以通过项目来滚动观察——注释④)。然而,如果我们在第一次拉下下拉列表前将所的项目装入下拉列表,它的大小就会合适。当然,用户在使用时希望看到整个的列表,所以会在下拉列表的状态里对增加项目到选择框里加以特殊的限定。
④:这一行为显然是一种错误,会Java以后的版本里解决。
13.11 列表框 列表框与选择框有完全的不同,而不仅仅是当我们在激活选择框时的显示不同,列表框固定在屏幕的指定位置不会改变。另外,一个列表框允许多个选择:如果我们单击在超过一个的项目上,未选择的则表现为高亮度,我们可以选择象我们想要的一样的多。如果我们想察看项目列表,我们可以调用getSelectedItem()来产生一个被选择的项目列表。要想从一个组里删除一个项目,我们必须再一次的单击它。列表框,当然这里有一个问题就是它默认的动作是双击而不是单击。单击从组中增加或删除项目,双击调用action()。解决这个问题的方法是象下面的程序假设的一样重新培训我们的用户。 //: List1.java // Using lists with action() import java.awt.; import java.applet.;
public class List1 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); } public boolean action (Event evt, Object arg) { if(evt.target.equals(lst)) { t.setText(""); String[] items = lst.getSelectedItems(); for(int i = 0; i < items.length; i++) t.appendText(items[i] + "\n"); } else if(evt.target.equals(b)) { if(count < flavors.length) lst.addItem(flavors[count++], 0); } else return super.action(evt, arg); return true; } } ///:~
按下按钮时,按钮增加项目到列表的顶部(因为addItem()的第二个自变量为零)。增加项目到列表框比到选择框更加的合理,因为用户期望去滚动一个列表框(因为这个原因,它有内建的滚动条)但用户并不愿意像在前面的例子里不得不去计算怎样才能滚动到要要的那个项目。 然而,调用action()的唯一方法就是通过双击。如果我们想监视用户在我们的列表中的所作所为(尤其是单击),我们必须提供一个可供选择的方法。
13.11.1 handleEvent() 到目前为止,我们已使用了action(),现有另一种方法handleEvent()可对每一事件进行尝试。当一个事件发生时,它总是针对单独事件或发生在单独的事件对象上。该对象的handleEvent()方法是自动调用的,并且是被handleEvent()创建并传递到handleEvent()里。默认的handleEvent()(handleEvent()定义在组件里,基础类的所有控件都在AWT里)将像我们以前一样调用action()或其它同样的方法去指明鼠标的活动、键盘活动或者指明移动的焦点。我们将会在本章的后面部分看到。 如果其它的方法-特别是action()-不能满足我们的需要怎么办呢?至于列表框,例如,如果我想捕捉鼠标单击,但action()只响应双击怎么办呢?这个解答是过载handleEvent(),毕竟它是从程序片中得到的,因此可以过载任何非确定的方法。当我们为程序片过载handleEvent()时,我们会得到所有的事件在它们发送出去之前,所以我们不能假设“这里有我的按钮可做的事件,所以我们可以假设按钮被按下了”从它被action()设为真值。在handleEvent()中按钮拥有焦点且某人对它进行分配都是可能的。不论它合理与否,我们可测试这些事件并遵照handleEvent()来进行操作。 为了修改列表样本,使它会响应鼠标的单击,在action()中按钮测试将被过载,但代码会处理的列表将像下面的例子被移进handleEvent()中去: //: List2.java // Using lists with handleEvent() import java.awt.; import java.applet.;
public class List2 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); } public boolean handleEvent(Event evt) { if(evt.id == Event.LIST_SELECT || evt.id == Event.LIST_DESELECT) { if(evt.target.equals(lst)) { t.setText(""); String[] items = lst.getSelectedItems(); for(int i = 0; i < items.length; i++) t.appendText(items[i] + "\n"); } else return super.handleEvent(evt); } else return super.handleEvent(evt); return true; } public boolean action(Event evt, Object arg) { if(evt.target.equals(b)) { if(count < flavors.length) lst.addItem(flavors[count++], 0); } else return super.action(evt, arg); return true; } } ///:~
这个例子同前面的例子相同除了增加了handleEvent()外简直一模一样。在程序中做了试验来验证是否列表框的选择和非选择存在。现在请记住,handleEvent()被程序片所过载,所以它能在窗体中任何存在,并且被其它的列表当成事件来处理。因此我们同样必须通过试验来观察目标。(虽然在这个例子中,程序片中只有一个列表框所以我们能假设所有的列表框事件必须服务于列表框。这是一个不好的习惯,一旦其它的列表框加入,它就会变成程序中的一个缺陷。)如果列表框匹配一个我们感兴趣的列表框,像前面的一样的代码将按上面的策略来运行。注意handleEvent()的窗体与action()的相同:如果我们处理一个单独的事件,将返回真值,但如果我们对其它的一些事件不感兴趣,通过handleEvent()我们必须返回super.handleEvent()值。这便是程序的核心,如果我们不那样做,其它的任何一个事件处理代码也不会被调用。例如,试注解在上面的代码中返回super.handleEvent(evt)的值。我们将发现action()没有被调用,当然那不是我们想得到的。对action()和handlEvent()而言,最重要的是跟着上面例子中的格式,并且当我们自己不处理事件时一直返回基础类的方法版本信息。(在例子中我们将返回真值)。(幸运的是,这些类型的错误的仅属于Java 1.0版,在本章后面将看到的新设计的Java 1.1消除了这些类型的错误。) 在windows里,如果我们按下shift键,列表框自动允许我们做多个选择。这非常的棒,因为它允许用户做单个或多个的选择而不是编程期间固定的。我们可能会认为我们变得更加的精明,并且当一个鼠标单击被evt.shiftdown()产生时如果shift键是按下的将执行我们自己的试验程序。AWT的设计妨碍了我们-我们不得不去了解哪个项目被鼠标点击时是否按下了shift键,所以我们能取消其余部分所有的选择并且只选择那一个。不管怎样,我们是不可能在Java 1.0版中做出来的。(Java 1.1将所有的鼠标、键盘、焦点事件传送到列表中,所以我们能够完成它。)
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库包括卡片式的窗格看起来非常的棒,且可以我们处理所有的细节。)
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中增加或删除一个新的事件类型,我们只需要简单地在事件列队中增加或删除一个