GUI
GUI
GUI全称是GraphicalUser Interface,即图形用户界面。顾名思义,就是应用程序提供给用户操作的图形界面,包括窗口、菜单、按钮、工具栏和其他各种屏幕元素。目前,图形用户界面已经成为一种趋势,它的好处自不必多说了,所以几乎所有的程序设计语言都提供了GUI设计功能。在Java里有两个包为GUI设计提供丰富的功能,它们是AWT和Swing。AWT是Java的早期版本,其中的AWT组件种类有限,可以提供基本的GUI设计工具,却无法完全实现目前GUI设计所需的所有功能。Swing是SUN公司对早期版本的改进版本,它不仅包括AWT中具有的所有部件,并且提供了更加丰富的部件和功能,它足以完全实现GUI设计所需的一切功能。Swing会用到AWT中的许多知识,掌握了AWT,也就基本上掌握了Swing,我们就从AWT开始我们的图形界面设计之旅吧!
8.1 初识AWT
AWT中定义了多种类和接口,用于在Java应用程序和Java Applet中进行GUI设计。我们首先通过下面的示例程序来感受一下Java的图形界面编程。
import java.awt.*;
public class TestFrame
{
public static void main(String [] args)
{
Frame f=new Frame(“IT人资讯交流网”);
f.add(new Button(“ok”));
f.setSize(300,300);
f.setVisible(true);
}
}
图形界面程序中可以使用各种各样的图形界面元素,如文本框,按钮,列表框,对话框等等,我们将这些图形界面元素称为GUI组件。AWT为各种GUI组件提供了对应的Java组件类,这些组件类都是java.awt.Component的直接或间接子类。其中,Frame类用于产生一个具有标题栏的框架窗口。Frame.setSize方法设置窗口的大小,Frame.setVisible显示或隐藏窗口,程序运行后产生一个如图8.1所示的非常标准的框架窗口。用AWT编写Java的GUI程序的图形用户界面的各种组件类都位于JDK的java.awt包中,程序开始必须导入java.awt包,可以导入整个java.awt包,也可以只导入程序中用到的那些组件类。编译运行此程序,结果如图8.1所示。
图8.1
对于众多的GUI组件,根据其作用可以又分为两大类:基本组件(下面就全部简称为组件)和容器。
组件又被称为构件,它是诸如按钮、文本框之类的图形界面元素,在这些组件上不能容纳其他的组件。容器其实也是一种组件,是一种比较特殊的组件,它可以用来容纳其他组件,如窗口,对话框等等,所有的容器类都是java.awt.Container的直接或间接子类。 Container类是Component类的一个子类,由此可见容器本身也具有组件的功能和特点,也可以被当作基本组件一样使用。在上面的程序中,Frame就是一个容器,它容纳了一个Button部件。
8.2 AWT线程
细心的读者也许注意到了,在运行上面写的那段程序时,主调用类的main方法执行f.setVisible(true)语句后就退出了,程序的main线程也随之结束了,但程序并没有结束,窗口不仅正常显示在桌面上,而且我们还可以对这些窗口进行一些常规操作,如拖动窗口,改变窗口的大小等。我们在多线程的课程中曾经讲过,对Java程序来说,只要还有一个前台线程在运行,整个进程就不会结束。这说明我们的程序还有其它线程在运行,那么其它线程是谁创建的?又是在什么时候创建的呢?读者可以简单地认为,程序在产生Frame对象时,创建了一个新的线程,我们称之为AWT线程。AWT线程的内部实现,在不同的JDK版本下不太一样,从我们对Java的使用经验上来看,在不同的版本下,我们时常碰到我们的应用程序(不仅仅是AWT)有不同的执行结果,这是令人很痛苦和无奈的事情,也是我们使用Java所要经常承担的风险。如果我们的程序调用Frame.dispose方法关闭了我们的框架窗口(具体实现细节,读者在本章稍后的部分能够看到),当程序放在JDK1.3下运行时,我们发现AWT线程没有结束,程序也不会自动结束。但在JDK1.4下运行这个程序,当框架窗口被关闭后,AWT线程也结束了,程序随之结束。
搬出那些过时的小经验来给读者讲解,并不是什么好主意,甚至会造成误导。所以在这里,我的侧重点并不是要讲解与分析AWT线程在不同JDK版本下的差异的具体案例本身,因为百分之九十九的读者都不会再用到旧的JDK版本,也不会再碰到这样的问题的,既然人家在新的版本中已经做出了修改,你就没必要白费精力、花功夫去了解那些陈年旧事了。虽然如此,但我还要在此提及这个问题,是要告诉大家下面的信息:在实际开发中,要注意到有时候碰到的一些莫名奇妙的问题,并不是我们程序本身的问题,不妨换个角度去想,可能是开发工具或系统版本的问题,在通常的应用程序开发中,不要太相信你的系统是绝对的。
从事计算机软件开发,对绝大多数人来说,不是科学研究,而是一个工程项目的实施,我们使用的开发环境,开发工具,甚至编程语言本身就是我们的工程工具,这些工具不可能是完全理想和完美的,经常会有这样或那样的小问题,往往都是软件开发人员在应用过程中发现了它们在某个应用上的问题后,系统供应商再去修改他们的的开发环境,升级他们的开发工具,完善编程语言。在这之前,我们只能想别的办法来完成我们程序中的有关任务,或是通过别的手段来回避这些开发工具、环境、语言本身的问题。当新手碰到这些开发工具和语言本身的问题后,一般都不知所措,只会从程序的角度上去找错误,而不是怀疑开发工具或程序语言本身的问题。所以,在软件开发过程中,老手的经验是非常重要的,脱离实际应用,只学习编程语言本身的人们,往往都很难独当一面地从事软件开发。
8.3 AWT事件处理
8.3.1 事件处理机制
我们在上面写的这个程序有一个明显的问题,鼠标点击窗口标题栏上的关闭按钮并不能够使窗口关闭和使程序结束,我们只能用操作系统自带的杀死进程的办法来关闭这个程序,在命令行窗口中用ctrl+c就可以强制结束正在运行的程序。其实,这也没有什么奇怪的,通过GUI组件,用户可以对应用程序进行各种操作,反之,应用程序可以通过GUI组件收集用户的操作信息,如用户在窗口上移动了鼠标、按下了键盘等。别忘了GUI的意义,是应用程序提供给用户操作的图形界面,而GUI本身并不对用户操作的结果负责。大家可以想想,我们在这个窗口上添加了一个按钮,当用户用鼠标点击这个按钮时,程序不也是什么都不做吗?如果我们想对鼠标点击按钮这个事件执行某种功能,就必须编写相应的处理程序代码。
对于这种GUI程序与用户操作的交互功能,Java使用了一种自己专门的方式,称之为事件处理机制。在事件处理机制中,我们需要理解三个重要的概念:
l 事件:用户对组件的一个操作,我们称之为一个事件。
l 事件源:发生事件的组件就是事件源。
l 事件处理器:负责处理事件的方法。
三者之间的关系如图8.2所示:
图8.2
Java程序对事件进行处理的方法是放在一个类对象中的,这个类对象就是事件监听器。
我们必须将一个事件监听器对象同某个事件源的某种事件进行关联,这样,当某个事件源上发生了某种事件后,关联的事件监听器对象中的有关代码才会被执行。我们把这个关联的过程称为向事件源注册事件监听器对象。从上面的图例中,我们能够看到,事件处理器(事件监听器)首先与组件(事件源)建立关联,当组件接受外部作用(事件)时,组件就会产生一个相应的事件对象,并把此对象传给与之关联的事件处理器,事件处理器就会被启动并执行相关的代码来处理该事件。
基本上明白了Java的事件处理机制,我们接着详细介绍事件和事件监听器的一些编程方面的有关知识。
事件用以描述发生了什么事情。AWT对各种不同的事件,按事件的动作(如鼠标操作)、效果(如窗口的关闭和激活)等进行了分类,一类事件对应一个AWT事件类。我们这里并不想象通常的书籍一样,给大家罗列各种各样的事件并解释一番。我在这里为大家简要介绍几个具有典型代表意义的事件,就足以让大家掌握相关的知识了,如果有人想了解所有的事件,不用去查找什么大全之类的书籍,在这里我告诉你一个简单的办法,你只要参阅JDK文档中的java.awt.event包,那里列出了所有的事件类。
l MouseEvent类对应鼠标事件,包括鼠标按下,鼠标释放,鼠标点击(按下后释放)等。
l WindowEvent类对应窗口事件,包括用户点击了关闭按钮,窗口得到与失去焦点,窗口被最小化等。
l ActionEvent类对应一个动作事件,它不是代表一个具体的动作,而是一种语义,如按钮或菜单被鼠标单击,单行文本框中按下回车键等都可以看作是ActionEvent事件。读者可以这么理解ActionEvent事件,如果用户的一个动作导致了某个组件本身最基本的作用发生了,这就是ActionEvent事件。菜单、按钮放在那就是用来发出某种动作或命令的,鼠标单击(也可以用键盘来操作)这些组件,只是表示要执行这种动作或命令的事情发生了,显然对于这种情况,我们并不关心是鼠标单击,还是键盘按下的。
通过各种事件类提供的方法,我们可以获得事件源对象,以及程序中对这一事件可能要了解的一些特殊信息,如对于鼠标事件,我们很可能要获得鼠标的坐标信息,经过查JDK文档就能知道用MouseEvent.getX,MouseEvent.getY这两个方法。小时候常听人讲“人有多大胆,地有多大产”,同样,对于我们编程中遇到的一般正常的需求,开发工具包都会提供,没有解决不了的,只有我们还没找到的。解决问题的关键就看我们有没有现用现找的本领了。
某一类事件,其中又包含触发这一事件的若干具体情况。对一类事件的处理由一个事件监听器对象来完成,对于触发这一事件的每一种情况,都对应着事件监听器对象中的一个不同的方法。某种事件监听器对象中的每个方法名称必须是固定的,事件源才能依据事件的具体发生情况找到事件监听器对象中对应的方法,事件监听器对象也包含事件源可能调用到的所有事件处理方法。这正是“调用者和被调用者必须共同遵守某一限定,调用者按照这个限定进行方法调用,被调用者按照这个限定进行方法实现”的应用规则,在面向对象的编程语言中,这种限定就是通过接口类来表示的。事件源和事件监听器对象就是通过事件监听器接口进行约定的,事件监听器对象就是实现了事件监听器接口的类对象。不同的事件类型对应不同的事件监听器接口。
事件监听器接口的名称与事件的名称是相对应的,非常容易记忆,如MouseEvent的监听器接口名为MouseListener,WindowEvent的监听器接口名为WindowListener,ActionEvent的监听器接口名为ActionListener。
有许多书上将众多的事件分为两大类:低级事件和语义事件(又叫高级事件),并列出了所有属于低级事件的事件,所有属于高级事件的事件。凭作者的经验,不相信真有读者能够很清楚的记住哪些是高级事件和哪些是低级事件,包括那些书的作者们本人也做不到。虽然作者认为,记住哪些是高级事件和哪些是低级事件毫无意义,但作者也可以让你不用记忆,就能够轻松地进行这种区分。如果某个事件的监听器接口中只有一个方法,那么这个事件就是语义事件,如ActionListener中只有一个方法,ActionEvent就是一种语义事件,反之,则为低级事件。另外,从字面上,我们也能够想象,语义事件关心的是一个具有特殊作用的GUI组件对应的动作发生了,而不关心这个动作是怎样发生的。
8.3.2 用事件监听器处理事件
我们来看看如何为上面的程序添加窗口关闭的代码,从而学习事件处理的具体编码实现:
程序清单:TestFrame.java
import java.awt.*;
importjava.awt.event.*;
public classTestFrame
{
public static void main(String [] args)
{
Frame f=new Frame(“IT人资讯交流网”);
f.setSize(300,300);
f.setVisible(true);
f.addWindowListener(newMyWindowListener());
}
}
classMyWindowListener implements WindowListener
{
public void windowClosing(WindowEvent e)
{
e.getWindow().setVisible(false);
((Window)e.getComponent()).dispose();
System.exit(0);
}
public void windowActivated(WindowEvent e){}
public void windowClosed(WindowEvent e){}
public void windowDeactivated(WindowEvent e){}
public void windowDeiconified(WindowEvent e){}
public void windowIconified(WindowEvent e){}
public void windowOpened(WindowEvent e){}
}
在上面的程序代码中,由于AWT中的事件类和监听器接口类都位于java.awt.event包中,所以在程序的开始处,import了java.awt.event.*。注意,import一个包中的所有类,并没有import该包的子包中的类,所以,我们 import了java.awt包,还要单独import java.awt.event包,其实,这个问题,即使我们不讲,大家也应该能够自己通过实验弄明白的。只要勇于实践,多动手编写些程序,通过观察编译运行的结果,就可以验证我们的许多想法或去除心中的疑惑,同时也锻炼和逐渐提高了我们的编程能力。
我们编写了一个新类MyWindowListener来实现窗口事件监听器对象的程序代码,并调用Window.addWindowListener方法将事件监听器对象注册到Frame类(Frame类继承java.awt.Window类)创建的框架窗口上。在WindowListener接口里有七个方法,正好对应窗口事件的七种情况,因为我们只想处理鼠标点击窗口标题栏上的关闭按钮这一事件,对其他窗口事件我们并不关心,所以,在类MyWindowListener的代码中,我们只对windowClosing方法进行了编码,其他方法只是简单实现(因为Java语法的要求,必须实现接口的所有方法),什么也没做。注意windowClosing方法和windowClosed方法的区别,windowClosing对应用户想关闭窗口的情况,而windowClosed对应窗口已经被关闭时的情况。
F指点迷津:
如我们要获得汽车类的发动机,我们可以用一个汽车类的getEngine方法来实现,显然该方法的返回值类型只能是发动机(而不能是发动机的某个子类)。现在,有个东风汽车类继承了汽车类,如果我们直接使用从汽车类继承到的getEngine方法来获得一辆东风汽车的发动机,尽管我们知道获得的发动机是东风发动机,但对编译器来说,它只能从getEngine方法的语法上知道返回的是发动机,如果对于这个返回的发动机对象,我们要使用东风发动机特有的功能,我们就需要使用以前讲过的类型转换。如果东风汽车类专门增加了一个新的getDongFengEngine方法,这个方法的返回值类型就是东风发动机,返回的对象可以直接使用东风发动机特有的功能,不用再作类型转换。
明白了东风汽车这个比喻,读者就不难理解在上面程序的windowClosing方法中,作者用了两种方式来返回那个窗口对象的代码了。下面是WindowEvent的继承层次图:
java.lang.Object
|
+--java.util.EventObject
|
+--java.awt.AWTEvent
|
+--java.awt.event.ComponentEvent
|
+--java.awt.event.WindowEvent
由于EventObject.getSource、ComponentEvent.getComponent、WindowEvent.getWindow等方法都可以返回事件源对象,由于子类可以继承父类的方法,所以WindowEvent事件对象,返回事件源对象的方法可以有多个。不管用哪种方法,返回的都是内存中的同一个对象,只是对编译器来说,返回的类型不同罢了,越是底层的getXxx方法,返回的类型越具体。
小结:要处理GUI组件上的XxxEvent事件下的某种情况,首先要编写一个实现了XxxListener接口的事件监听器类,然后在XxxListener类和要处理的具体事件情况相对应的方法中编写处理程序代码,最后将类XxxListener创建的对象通过addXxxListener方法注册到GUI组件上。Xxx可以是各种不同的事件,如Window,Mouse,Key,Action等。
小经验:所有事件监听器方法返回的返回类型都是void。
8.3.3 事件适配器
为简化编程,JDK针对大多数事件监听器接口定义了相应的实现类,我们称之为事件适配器(Adapter)类。在适配器类中,实现了相应监听器接口中所有的方法,但不做任何事情,子类只要继承适配器类,就等于实现了相应的监听器接口,如果要对某类事件的某种情况进行处理,只要覆盖相应的方法就可以了,其他的方法再也不用“简单实现”了。可见,如果想用作事件监听器的类已经继承了别的类,就不能再继承适配器类了,只能去实现事件监听器接口了。
修改MyWindowListener,代码如下:
classMyWindowListener extends WindowAdapter
{
public void WindowClosing(WindowEvent e)
{
e.getWindow().setVisible(false);
((Window)e.getComponent()).dispose();
System.exit(0);
}
}
重新编译没有错误,可是,点击关闭按钮后程序并没有退出。这是怎么回事呢?要么是WindowClosing方法中的代码有问题,要么是WindowClosing方法没有调用,怎么判定这两种情况呢?我们只要在WindowClosing方法的开始处,添加System.out.println(“cominghere!”);,编译运行后再看看命令行窗口上(不是GUI窗口上)有没有打出“coming here!”,就知道WindowClosing方法是否被调用过。运行后屏幕上并没有打印出“coming here!”,可以确定WindowClosing方法没有被调用,这时又有两种可能性,一种是还没有将MyWindowListener对象注册到框架窗口上,所以即使框架窗口上发生了WindowEvent,也不会来调用类MyWindowListener中的任何方法,仔细检查程序代码,我们可以排除这种可能。还有一种可能就是,WindowClosing方法名拼写有问题,仔细比较后发现第一个字母应该是小写,即windowClosing。哦!Java是区分大小写的,MyWindowListener.WindowClosing是一个与WindowAdapter.windowClosing毫无关系的方法,并没有起到对WindowAdapter.windowClosing方法覆盖的目的。将WindowClosing修改成windowClosing后编译运行,一切正常。
在上面的例子中,事件监听器的类MyWindowListener和产生GUI组件的类TestFrame是两个完全分开的类,事件监听器的类中的代码所访问到的对象也正好是事件源。如果我们要在件监听器的类中的代码中访问其他的非事件源的GUI组件,程序就得想别的办法了,如我们单击图8.1窗口中的ok按钮来关闭框架窗口并退出程序,这就要求程序在按钮的事件监听器代码中要访问框架窗口,我们又该怎么做呢?一种简单的办法就是将事件监听器的代码和产生GUI组件的代码放在同一个类中实现,程序代码如下:
import java.awt.*;
importjava.awt.event.*;
public classTestFrame implements ActionListener
{
public static void main(String [] args)
{
Frame f=new Frame(“IT人资讯交流网”);
Button btn=new Button(“退出”);
btn.addActionListener(newTestFrame());//注册事件监听器对象
f.add(btn);//将按钮增加到框架窗口上
f.setSize(300,300);
f.setVisible(true);
}
public void actionPerformed(ActionEvente)
{
f.setvisible(false);
f.dispose();
System.exit(0);
}
}
编译肯定有错,因为actionPerformed方法中不能访问在main方法中定义的f变量。修改程序,将Frame f=new Frame(“IT人资讯交流网”);移动到main方法的上面,也就是将f定义成一个成员变量,编译还会有错,因为main是个静态方法,不能直接访问同类中的成员变量。再次修改程序,如下所示:
import java.awt.*;
importjava.awt.event.*;
public classTestFrame implements ActionListener
{
Frame f=new Frame(“IT人资讯交流网”);
public static void main(String [] args)
{
TestFrame tf=new TestFrame();
Button btn=new Button(“退出”);
btn.addActionListener(new TestFrame());
tf.f.add(btn);
tf.f.setSize(300,300);
tf.f.setVisible(true);
}
public void actionPerformed(ActionEvente)
{
f.setvisible(false);
f.dispose();
System.exit(0);
}
}
上面的程序虽然编译没有问题,但程序代码规范极差,在main方法中,对于每一次f对象的引用,都必须在f前增加tf,如tf.f.setSize。如果添加到f上的GUI组件很多,那么将有很多的tf.f.xxx语句,这样的程序代码,相信你自己都不会满意。我们怎样将上面的代码修改得规范些呢?将程序改成下面这样。
import java.awt.*;
importjava.awt.event.*;
public classTestFrame implements ActionListener
{
Frame f=new Frame(“IT人资讯交流网”);
public static void main(String [] args)
{
TestFrame tf=new TestFrame();
tf.init();
}
public void init()
{
Buttonbtn=new Button(“退出”);
btn.addActionListener(newTestFrame());
f.add(btn);
f.setSize(300,300);
f.setVisible(true);
}
public void actionPerformed(ActionEvente)
{
f.setVisible(false);
f.dispose();
System.exit(0);
}
}
将在main方法中调用的GUI初始化代码全部放到了init方法中,在main方法中只要调用这个init方法就可以了,init方法是非静态的,可以直接访问非静态的成员变量。编译运行后,退出按钮并不能关闭窗口,读者应先自己思考问题的原因并上机调试后,再来参看下面的解答。在上面的程序中,创建了两个TestFrame对象,桌面上显示的窗口是第一个TestFrame中的f对象,而退出按钮的事件处理代码中关闭的是第二个TestFrame中的f对象,所以我们看到的窗口没有关闭,将init方法中的btn.addActionListener(new TestFrame());改为btn.addActionListener(this);就可以了。
我们在这里再次强调要面向对象,脑子里想的就是程序中各对象在各个时刻的内存布局和状态,以及每行程序代码执行后对内存中的对象的影响。
我们通过将事件监听器的代码和产生GUI组件的代码放在同一个类中实现,就很轻松的解决了我们的问题。
8.3.4 事件监听器的匿名内置类实现方式
如果一个事件监听器类只用于在一个组件上注册监听器事件对象,为了让程序代码更为紧凑,我们可以用匿名内置类的语法来产生这个事件监听器对象,这也是一种经常使用的方法。
对于上面的例子,我们用匿名内置类的语法进行改写,程序代码如下:
import java.awt.*;
importjava.awt.event.*;
class TestFrame
{
Frame f=new Frame("IT人资讯交流网");
public static void main(String [] args)
{
new TestFrame().init();
}
public void init()
{
Button btn=new Button("退出");
btn.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
f.setVisible(false);
f.dispose();
System.exit(0);
}
});
f.add(btn);
f.setSize(300,300);
f.setVisible(true);
}
}
8.3.5 事件处理的多重运用
用户的一个操作,在触发了低级事件的同时,可能也会触发语义事件,这时令初学者困惑的是,不知道该选择哪种类型事件监听器来进行处理。其实道理很简单,一个员工打伤了他的同事,如果程序代表法律部门,要处理的就是打人这一低级事件,如果程序代表公司经理,要处理的便是违犯公司纪律这一语义事件,如果程序即代表法律部门,又代表公司经理,便需对这两个事件都做处理。一般情况下,如果对语义事件的处理能够满足我们的需求,我们就不再处理低级事件。用户在一个按钮上按下鼠标,触发了鼠标事件,也触发了按钮的动作事件,我们根据我们程序的需要来编码处理某一个事件或是两者都处理。例如当按钮被点击后,我们除了要执行该按钮对应的程序功能外,还希望鼠标按下时能改变按钮标题内容,鼠标释放时能恢复标题内容,我们在按钮上就要注册两个事件监听器对象,一个处理鼠标事件,另一个处理按钮的动作事件。一个GUI组件倒底能够触发哪几种事件,我们没必要死记硬背,在一般的集成开发环境下,如JBuilder,JCreator等,当我们输入某个对象的成员分隔符,在下拉的成员列表提示框中,我们就能看到这个组件支持的事件,如图8.3所示的JCreator下的提示情况。
图8.3
我们通过查看这些addXxxListener方法,就知道了这个组件所支持的事件类型。
一个组件上的一个动作可以产生多种不同类型的事件,因而我们可以向同一个事件源上注册多种不同类型的监听器,如图8.4所示:
图8.4
一个事件监听器对象可以注册到多个事件源上,即多个事件源的同一事件都由一个对象统一来处理,如图8.5所示:
图8.5
一个事件源上也可以注册对同一事件进行处理的多个事件监听器对象,当这一事件发生时,各事件监听器对象依次被调用,如图8.6所示:
图8.6
8.3.6 高级事件处理
默认情况下,组件屏蔽了对所有事件的响应,也就是不管发生了什么情况,事件都不会在这个组件上发生,组件都不会产生任何事件对象。只有在一个组件上注册某种事件的事件监听器对象后,组件才可以对这种事件做出响应,当发生了对应这种事件的情况后,事件才能在这个组件上发生,组件才会产生这种事件对象。
当一个组件上发生了某种事件后,系统会调用这个组件对象的processEvent方法来处理,缺省的processEvent方法将根据事件的类型调用相应的processXxxEvent方法,其中Xxx代表事件类型,processXxxEvent方法接着将Xxx事件传递给注册的监听器去处理。例如,如果组件上发生了鼠标移动事件,组件对象的processEvent方法将调用processMouseMotionEvent方法进行处理。
如果我们想改变某种组件的事件处理方式,我们需要覆盖该组件的processEvent方法或processXxxEvent方法,processEvent是处理所有事件的总入口,而processXxxEvent是专用于处理某种事件的分岔入口。显然,如果要在一个方法中就改变所有事件的处理方式,我们需要覆盖processEvent,如果只想改变某种或少数几种事件的处理方式,而不想影响其它事件的处理方式,我们还是覆盖processXxxEvent方法,而不再直接覆盖processEvent方法为好。由于我们不可能直接进入到某个组件的processXxxEvent方法中去修改程序代码,我们需要定义一个继承了该组件的子类,在子类中覆盖processXxxEvent方法,并将原先创建的组件对象改为由这个子类创建,就可以达到我们的目的了。
就象前面讲的,如果我们没有在组件上注册Xxx事件监听器,组件就不会发生Xxx事件,我们的processXxxEvent方法根本就不可能被调用。即使没有在组件上注册事件监听器,我们只要调用了enableEvents函数,设置组件能够进行响应的事件,在相应的情况发生后,组件仍然能够产生对应的事件。enableEvents函数的定义如下:
protected finalvoid enableEvents(long eventsToEnable)
其中,参数eventsToEnable指定了需要组件响应的事件类型所对应的数值。在我们的程序中经常要用到一类变量,这个变量里的每一位(bit)都对应某一种特性。当该变量的某位为1时,表示有该位对应的那种特性,当该位为0时,即没有那位所对应的特性。当变量中的某几位同时为1时,就表示同时具有那几种特性的组合。那种特性在我们这里就是要响应的事件类型,我们将不同的事件类型用一个不同的long型数值来表示,且每个这些数值中只有与其事件类型对应的那一位(一个bit)为1,其余的bit都为0,这样,我们就可以对多个事件类型所对应的数值进行相或(|)操作,得到的结果正好就是这几种事件类型的组合。一个数值中的哪一位代表哪种事件类型,是不容易记忆的,所以我们就将这些表示不同事件类型的long型数值定义成常量,常量名就是根据事件类型的英文拼写去定义。可见,参数eventsToEnable可以是多个事件类型所对应的数值相或的结果,我们又怎样找到表示事件类型数值的常量名呢?如果你明白了在Java中是如何定义常量的,你就应该想到在JDK文档中去查找与事件有关的类,在其中的某个类中就会有这些常量的定义的。作者看到processEvent((AWTEvent e))方法中的参数类型是AWTEvent,就很自然地想到了去AWTEvent类中查找,果然在那里看到了这些常量的定义,如鼠标移动事件对应的常量为MOUSE_MOTION_EVENT_MASK,这样,当我们想让组件响应鼠标移动事件时,我们可以使用enableEvents(AWTEvent.MOUSE_MOTION_EVENT_MASK);语句来完成。
明白了组件内部的事件处理过程和相关知识,我们现在就来编写一个这样的程序:在一个窗口上显示一个按钮,一旦鼠标移动到这个按钮上时,按钮就移动到了其他位置,这样,鼠标就永远无法点击到这个按钮。在写这个程序之前,我先讲个故事,我们假设有两个孙悟空,刚开始时第二个孙悟空使用了隐身术,所以你只能看见第一个孙悟空。当你靠近第一个孙悟空时,这个孙悟空马上使用了隐身术而隐藏,同时通知第二个孙悟空现身,你就会误以为第一个孙悟空跑到了第二个孙悟空的位置上。同样,当你靠近第二个孙悟空时,这个孙悟空马上使用了隐身术而隐藏,同时通知第一个孙悟空现身,这时,你以为还是第一个孙悟空又跑回到了他原来的位置。如此往复,你永远也抓不到孙悟空的,你却还以为只有一个孙悟空在跑来跑去的呢!作者就使用抓孙悟空的这个思路来编写我们的程序,首先要对按钮的鼠标移动事件进行处理,所以我定义了一个Button的子类MyButton,在MyButton中对processMouseMotionEvent方法进行覆盖,在该方法中隐藏自己,显示自己的伙伴。下面是程序代码,其中的细节在前面都分析过了,就不再多说了。
程序清单:TestMyButton.java
import java.awt.*;
importjava.awt.event.*;
class MyButtonextends Button
{
private MyButton friend;
public void setFriend(MyButton friend)
{
this.friend = friend;
}
public MyButton(String name)
{
super(name);
enableEvents(AWTEvent.MOUSE_MOTION_EVENT_MASK);
}
protected voidprocessMouseMotionEvent(MouseEvent e)
{
setVisible(false);
friend.setVisible(true);
}
}
public classTestMyButton
{
public static void main(String [] args)
{
MyButton btn1 =new MyButton("你来抓我呀!");
MyButton btn2 =new MyButton("你来抓我呀!");
btn1.setFriend(btn2);
btn2.setFriend(btn1);
btn1.setVisible(false);
Frame f =new Frame("it315");
f.add(btn1, "North");//将btn1增加到f的北部
f.add(btn2, "South");//将btn2增加到f的南部
f.setSize(300,300);
f.setVisible(true);
btn1.setVisible(false);
}
}
8.4 GUI组件上的图形操作
我们有时需要在GUI组件上绘制图形,打印文字,显示图像等操作。组件对象本身不提供这些操作的方法,它只提供一个getGraphics方法,getGraphics方法返回一个包含有该组件的屏幕显示外观信息的Graphics类对象,Graphics类提供了在组件显示表面绘画图形,打印文字,显示图像等操作方法。
下面,我们通过编写一个画线程序来讲解在GUI组件上的图形操作,同时帮助大家更好地理解和掌握AWT事件处理机制。画线用的是Graphics.drawLine(int x1, int y1, int x2, int y2)方法,其中的参数意义,读者应该能够猜想得到,如果连这点想象力都没有,以后没法面对实际项目的开发。大家在使用陌生的方法时,可以反过来想,如果这个方法是你写的,你会提供给人家什么样的参数?这样一来,就基本上明白了这个方法的用法了。尽管我们有时在不得已的情况下,还是需要去仔细参看新方法的使用文档的,但经常多思考一下,我们能够潜移默化的从中体验出一些心得,借鉴到工具开发商的一些好的编码经验和思想。我们的程序要实现这样的功能:鼠标按下时的位置作为线的起始点,鼠标释放时的位置作为终止点,并在鼠标释放时画线,所以,我们需要对鼠标事件进行处理,在鼠标按下时记住鼠标的坐标,鼠标释放时画线。程序代码如下:
程序清单:DrawLine.java
import java.awt.*;
importjava.awt.event.*;
public classDrawLine
{
Frame f= new Frame("IT人资讯交流网");
public static void main(String [] args)
{
new DrawLine().init();
}
public void init()
{
f.setSize(300,300);
f.setVisible(true);
f.addMouseListener(new MouseAdapter()
{
int orgX;
int orgY;
public void mousePressed(MouseEvente)
{
orgX=e.getX();
orgY=e.getY();
}
public voidmouseReleased(MouseEvent e)
{
f.getGraphics().setColor(Color.red);
//设置绘图颜色为红色
f.getGraphics().drawLine(orgX,orgY,e.getX(),e.getY());
}
});
}
}
在上面的程序代码中,读者也必须能够做到通过查阅JDK文档看懂f.getGraphics().setColor(Color.red);这句代码为什么是这么写,特别是Color.red部分。编译运行,线条被画出来了,但颜色却不是红色。这是一个非常隐蔽的问题,程序中的两处都用f.getGraphics()返回Graphics对象引用,返回的两个引用指向的并不是同一个的Graphics对象,而是两个完全不同的对象,设置一个Graphics对象上的绘图颜色,不会影响另一个Graphics对象上的绘图输出。
我们接着为这个程序添加文本打印功能,顺便修正上面碰到的问题。
程序清单:DrawLine2.java
import java.awt.*;
importjava.awt.event.*;
public classDrawLine2
{
Frame f= new Frame("IT人资讯交流网");
public static void main(String [] args)
{
new DrawLine2().init();
}
public void init()
{
f.setSize(300,300);
f.setVisible(true);
f.addMouseListener(new MouseAdapter()
{
int orgX;
int orgY;
public void mousePressed(MouseEvente)
{
orgX=e.getX();
orgY=e.getY();
}
public void mouseReleased(MouseEvente)
{
Graphics g=f.getGraphics();
g.setColor(Color.red);//设置绘图颜色为红色
g.setFont(new Font("隶书",Font.ITALIC|Font.BOLD,30));
//设置文本的字体
g.drawString(new String(orgX+"," +orgY),orgX,orgY);
//打印鼠标按下时的坐标文本
g.drawString(newString(e.getX() +"," +e.getY())
,e.getX(),e.getY());
//打印鼠标释放时的坐标文本
g.drawLine(orgX,orgY,e.getX(),e.getY());
}
});
}
}
其实,上面的代码是作者一边查看JDK文档,一边现炒现卖的。在实际开发中,我们不可能以前就接触和掌握了程序中碰到的每个细节问题,这就需要我们在编程过程中遇到小的需求和问题时,马上能去查阅文档资料来解决,程序员就是这样工作的。上面的代码中,我们只调用了一次f.getGraphics()方法返回了一个Graphics对象,以后调用的全部是这个Graphics对象上的方法。当Y坐标小于30时,只有文本的下半部分被显示,是因为Graphics.drawString方法中的坐标是指整个文本块显示时的左下角位置,如图8.7所示:
图8.7
然而在其他语言中一般都指的是左上角位置,读者只要知道这个问题就够了,我们也不必把这作为程序问题去改正。
F指点迷津:
我们使用System.out.println()语句是不能够在图形窗口中打印字符文本的,只有使用Grapchics.drawString()语句才能够在图形窗口中打印字符文本。在GUI程序中仍然可以使用System.out.println()语句打印字符文本,只是打印的字符文本会显示在命令行窗口中。
$独家见解:
我们以前讲过,在Java的命名习惯中,常量中的每个字母都大写。我们程序中用到了Color.red这个常量,可其中red的每个字母全是小写。作者在很早以前,第一次碰到这个问题时,就断定:当初这一部分的设计者没有良好的命名习惯,弄了个监守自盗,贻笑大方了。果不其然,在JDK1.4的文档中,我们能够看到又增加了一个Color.RED常量,这正是SUN公司对以前的失误作出的弥补,所以,爱琢磨的读者不要再问Color.red与Color.RED有什么区别这个问题了,这是对同一事物的两种称呼,只是前者的命名不太正规,后者更为正规。
8.4.2 组件重绘
我们将程序窗口最小化后再恢复正常化显示,发现所绘图形全部消失了,这又是怎么回事呢?如何解决这个问题呢?在组件大小改变或隐藏后又显示,AWT线程都会重新绘制组件,组件上原来绘制的图形也就不复存在了,这一过程称为“曝光”。要想让用户感觉到所绘的图形一直存在,我们只需在组件重新绘制后,立即将原来的图形重新画上去,这个过程是肉眼感觉不到的。我碰到过有个学员对此很不高兴,说这不是在骗人吗?记住,我们程序在于效果,而不关心你是用那种方式实现的,我不知道这位学员最后的工作情况,但我真的感受到了人的思维是千奇百怪的。AWT线程在重新绘制组件后,会立即调用组件的paint方法,所以我们的图形重绘代码应该在paint方法中编写。
paint方法是这样定义的:
public voidpaint(Graphics g)
可见,AWT线程已经获得了组件上的Graphics对象,并将它传递给了paint方法,在paint方法中绘图时只能使用这个Graphics对象。
当我们想要执行paint方法中的程序代码时,应用程序不应直接调用paint方法,如果我们想执行paint方法中的程序代码,需调用Component.repaint方法,Component.repaint方法调用Component.update方法,Component.update再调用Component.paint。组件重绘的调用关系如图8.8所示:
图8.8
由于我们不可能直接进入到某个组件的paint方法中修改程序代码, 我们需要定义一个继承了该组件的子类,在子类中覆盖paint方法,在新的paint方法中编写重绘图形程序代码,并将原先创建的组件对象改为由这个子类创建,就可以达到我们的目的了。我们修改上面的程序代码,使其具有重绘效果:
import java.awt.*;
importjava.awt.event.*;
public classDrawLine extends Frame
{
intorgX;
int orgY;
int endX;
intendY;
public static void main(String [] args)
{
DrawLine dl=new DrawLine();
dl.init();
}
public void paint(Graphics g)
{
g.drawLine(orgX,orgY,endX,endY);
}
public void init()
{
/*基础差的读者如果难以理解下面这句代码,可将前面this处的注释去掉后再理解。也可这样理解,由于DrawLine是Frame的子类,所以可以直接调用Frame中的方法。*/
/*this.*/addMouseListener(newMouseAdapter()
{
publicvoid mousePressed(MouseEvent e)
{
orgX=e.getX();
orgY=e.getY();
}
publicvoid mouseReleased(MouseEvent e)
{
endX=e.getX();
endY=e.getY();
Graphicsg=/*this.*/getGraphics();
g.setColor(Color.red);//设置绘图颜色为红色
g.setFont(newFont(“隶书”,
Font.ITALIC|Font.BOLD,30));//设置文本的字体
g.drawString(newString(orgX +”,” +orgY),
orgX,orgY);//打印鼠标按下时的坐标文本
g.drawString(newString(endX +”,” + endY),
endX, endY);//打印鼠标释放时的坐标文本
g.drawLine(orgX,orgY,endX, endY);
}
});
}
}
由于在paint方法中重绘直线时,需要两个点的坐标,所以,在上面的程序,又增加了两个成员变量endX,endY用于保存鼠标释放时的坐标。上面的程序只能重绘最后的那条直线,读者可以自己想想其中的道理。
如果我们想完全重绘窗口上的内容,我们需要将每一条直线的坐标保存到一个集合类中,在paint方法中取出该集合中的每一条直线的坐标,逐一绘制。首先我们创建一个新的类MyLine,对应所画的一条直线,该类中定义了一个drawMe方法,用于将该直线在某个Graphics对应的组件上画出自己。
import java.awt.*;
importjava.awt.event.*;
importjava.util.*;
importjavax.swing.*;
class MyLine
{
private int x1;
private int y1;
private int x2;
private int y2;
public MyLine(int x1,int y1,int x2,int y2)
{
this.x1=x1;
this.y1=y1;
this.x2=x2;
this.y2=y2;
}
public void drawMe(Graphics g)
{
g.drawLine(x1,y1,x2,y2);
}
}
public classRerawAllLine extends Frame
{
Vector vLines=new Vector();
public static void main(String [] args)
{
RedrawAllLine f=new RedrawAllLine();
f.init();
}
public void paint(Graphics g)
{
g.setColor(Color.red);
Enumeration e=vLines.elements();
while(e.hasMoreElements())
{
MyLine ln=(MyLine)e.nextElement();
ln.drawMe(g);
}
}
public void init()
{
this.addWindowListener(newWindowAdapter(){
public voidwindowClosing(WindowEvent e)
{
((Window)e.getSource()).dispose();
System.exit(0);
}
});
addMouseListener(new MouseAdapter(){
int orgX;
int orgY;
publicvoid mousePressed(MouseEvent e)
{
orgX=e.getX();
orgY=e.getY();
}
public void mouseReleased(MouseEvente)
{
Graphicsg=e.getComponent().getGraphics();
g.setColor(Color.red);
g.drawLine(orgX,orgY,e.getX(),e.getY());
vLines.add(newMyLine(orgX,orgY,e.getX(),e.getY()));
}
});
this.setSize(300,300);
setVisible(true);
}
}
8.4.3 图像操作
我们可以通过Graphics.drawImage(Image img,int x,int y,ImageObserver observer)方法在组件上显示图像,其中,img参数是要显示的图像对象,x,y是图像显示的左上角坐标,observer 是用于监视图像创建进度的一个对象。drawImage是一个异步方法,即使img对应的图像还没有完全装载(在创建Image类对象时并没有在内存中创建图像数据)时,drawImage也会立即返回。如果程序想了解图像创建的进度信息,我们需要编写一个实现了ImageObserver接口的类,并将该类创建的对象传递给drawImage方法,这个过程和原理与前面讲过的事件监听器相似。如果程序不关心图像创建的进度信息,我们可以传递要显示图像的那个组件对象,因为Component类已实现了ImageObserver接口。下面是一个显示图像的例子程序。
import java.awt.*;
importjava.awt.event.*;
public classDrawImage
{
public static void main(String [] args)
{
Framef= new Frame("IT人资讯交流网");
Imageimg=f.getToolkit().getImage("c:\\test.gif");
f.getGraphics().drawImage(img,0,0,f);
f.setSize(300,300);
f.setVisible(true);
f.addWindowListener(new WindowAdapter(){
publicvoid windowClosing(WindowEvent e)
{
System.exit(0);
}
});
}
}
喜爱思考的读者都知道,在实际工作中,可能会碰到我们课程中没有讲到的许多小细节,更希望知道老手们解决问题的方法和思想,对于程序中的Image img = f.getToolkit().getImage(“c:\\test.gif”);这行代码,可能会问:“如果是我自己写程序,我怎么能够知道产生一个Image对象,要用到这些方法和这些调用关系呢?”。其实,最聪明的人是最会使用工具的人,特别是编写程序,如果我们有一个好的帮助系统,经常会有事半功倍的特效。我们在前面已经为大家讲解了chm格式的JDK文档。有了这样的帮助系统,我们就可以进行模糊查询了。由于Image是抽象类,我们不能使用构造方法,只能通过某个方法来产生一个Image对象了,这个方法名,估计就是createImage,getImage之类的英文单词组合,按照这种猜想,通过帮助的索引查询,一般都会有较大的收获,我们查到了Toolkit.getImage(String path)方法,由于Toolkit是抽象类,不能直接创建,接着用getToolkit又找到了Component.getToolkit方法返回Toolkit对象,就这样顺藤摸瓜写出这个完整的语句了,由于很多东西,特别是一些老手的工作经验都是只可意会,不可言宣的,所以自己读书和参加面授培训的效果还是有区别的。
上面的程序在运行时会报告第9行产生了java.lang.NullPointerException,一定要注意提示的行号,能够帮我们迅速定位错误。在JDK文档查找getGraphics的帮助,文档中说得非常清楚,只有在组件已显示在窗口上时,getGraphics方法才能正确返回一个Graphics对象。将程序代码改成下面这样。
importjava.awt.*;
importjava.awt.event.*;
publicclass DrawImage
{
public static void main(String [] args)
{
Framef= new Frame("IT人资讯交流网");
f.setSize(300,300);
f.setVisible(true);
f.addWindowListener(newWindowAdapter(){
public void windowClosing(WindowEvent e)
{
System.exit(0);
}
});
Imageimg=f.getToolkit().getImage("c:\\test.gif");
f.getGraphics().drawImage(img,0,0,f);
}
}
运行时不再有错,但图像并没有显示出来。这是因为在窗口初始显示时也会被调用paint方法,paint方法会擦除窗口上绘制的图形,这里的drawImage方法先于paint方法执行,所以,drawImage方法绘制的图像被paint方法擦除掉了。读者不要误以为setVisible方法执行时,paint方法就会立即执行,paint方法是由AWT线程调度和管理的。顺便告诉大家另外一个小经验,放在Frame.setVisible(true)之后的GUI程序代码,在窗口初始显示时,都看不出期望的执行效果。例如,f.setVisible(true);f.add(new Button(“test”);这样的程序代码,我们在窗口初始显示时也是看不到窗口上放置的按钮的,改为f.add(new Button(“test”); f.setVisible(true);这样的顺序则可以了。对于上面的问题,我们可以将图像放在paint方法中显示,修改后的程序如下:
importjava.awt.*;
import java.event.*;
publicclass DrawImage extends Frame
{
Image img=null;
public static void main(String [] args)
{
DrawImage f= new DrawImage();
f.init();
}
public void init()
{
setSize(300,300);
setVisible(true);
this.addWindowListener(newWindowAdapter(){
public void windowClosing(WindowEvent e)
{
System.exit(0);
}
});
img=this.getToolkit().getImage("c:\\test.gif");
}
public void paint(Graphics g)
{
getGraphics().drawImage(img,0,0,this);
}
}
编译运行后,有时候图像立即就能够正常显示出来了,有时候图像并没有显示出来,而是在命令行窗口中出现了如下错误:
C:\>javaDrawImage
java.lang.NullPointerException
atsun.java2d.pipe.DrawImage.copyImage(DrawImage.java:48)
atsun.java2d.pipe.DrawImage.copyImage(DrawImage.java:715)
at sun.java2d.pipe.ValidatePipe.copyImage(ValidatePipe.java:147)
……
改变窗口的大小,这时图像就显示出来了。原因在于,这时候正好碰到了AWT线程调用paint方法早于getImage方法的情况,而在paint方法中执行drawImage的时候,img对象仍为null。我们将调用getImage方法的语句放在setVisible语句之前,这样就万无一失,一切正常了。修改后的程序代码如下:
importjava.awt.*;
importjava.awt.event.*;
publicclass DrawImage extends Frame
{
Image img=null;
public static void main(String [] args)
{
DrawImage f= new DrawImage();
f.init();
}
public void init()
{
img=this.getToolkit().getImage("c:\\test.gif");
setSize(300,300);
setVisible(true);
this.addWindowListener(new WindowAdapter()
{
public void windowClosing(WindowEvente)
{
System.exit(0);
}
});
}
public void paint(Graphics g)
{
getGraphics().drawImage(img,0,0,this);
}
}
读者要逐渐学会作者对上面程序的反复实验和分析错误的过程,培养一种分析程序问题的思维,提高解决程序问题的手段和方法,积累调试程序问题的经验。
8.4.4 双缓冲的技术
在画线重绘程序中,我们在窗口重画时,需要逐一重新绘制窗口上原来的图形(直线,文本),如果原来的图形非常多,这个过程就显得比较慢了。一种改进的办法是,我们可以调用Component.createImage方法,在内存中创建一个Image对象,当我们在组件上绘图时,也在这个Image对象上执行同样的绘制,即Image对象中的图像是组件表面内容的复制,当组件重画时,我们只需将内存中的这个Image对象在组件上画出,不管组件上原来的图形有多少,在重绘时都只是一幅图像而已,在组件上的图形非常多时,重绘速度明显提高,这就是一种被称为双缓冲的技术。下面是这个应用的例子程序。
程序清单:DrawLine.java
import java.awt.*;
importjava.awt.event.*;
public classDrawLine extends Frame
{
Imageoimg=null;
Graphicsog=null;
public static void main(String [] args)
{
new DrawLine().init();
}
public void init()
{
setSize(300,300);
setVisible(true);
Dimensiond=getSize();
oimg=createImage(d.width,d.height);
og=oimg.getGraphics();
addMouseListener(new MouseAdapter()
{
int orgX;
int orgY;
public void mousePressed(MouseEvente)
{
orgX=e.getX();
orgY=e.getY();
}
public void mouseReleased(MouseEvente)
{
Graphics g=getGraphics();
g.setColor(Color.red);//设置绘图颜色为红色
g.setFont(new Font("隶书",Font.ITALIC|Font.BOLD,30));
//设置文本的字体
g.drawString(new String(orgX+"," +orgY),orgX,orgY);
//打印鼠标按下时的坐标文本
g.drawString(new String(e.getX()+"," +e.getY()),
e.getX(),e.getY());//打印鼠标释放时的坐标文本
g.drawLine(orgX,orgY,e.getX(),e.getY());
og.setColor(Color.red);//设置绘图颜色为红色
og.setFont(newFont("隶书",Font.ITALIC|Font.BOLD,30));
//设置文本的字体
og.drawString(newString(orgX +"," +orgY),orgX,orgY);
//打印鼠标按下时的坐标文本
og.drawString(newString(e.getX() +"," +e.getY()),
e.getX(),e.getY());//打印鼠标释放时的坐标文本
og.drawLine(orgX,orgY,e.getX(),e.getY());
}
});
}
publicvoid paint(Graphics g)
{
if(oimg!=null)
g.drawImage(oimg,0,0,this);
}
}
如果在paint方法中没有使用if语句对oimg进行是否为null的检查,就会出现如下的错误:
C:\>javaDrawLine
java.lang.NullPointerException
atsun.java2d.pipe.DrawImage.copyImage(DrawImage.java:48)
atsun.java2d.pipe.DrawImage.copyImage(DrawImage.java:715)
atsun.java2d.SunGraphics2D.drawImage(SunGraphics2D.java:2782)
atsun.java2d.SunGraphics2D.drawImage(SunGraphics2D.java:2772)
.....
createImage是Component的一个方法,只有部件显示在桌面上之后才能调用这个方法,部件显示时会调用它的paint方法,也就是createImage方法只能在第一次paint方法调用之后才能被调用。我们在paint方法中的drawImage语句中,又要调用createImage方法产生的oimg对象,这又要求createImage方法应在paint方法之前调用。上面的矛盾就成了“鸡生蛋,蛋生鸡”的问题,所以,我们在程序中使用if语句对oimg进行是否为null的检查来解决这个矛盾。
第8章 GUI(上)............................................................................................... 244
8.1 初识AWT............................................................................................... 244
8.2 AWT线程............................................................................................... 245
8.3 AWT事件处理........................................................................................ 246
8.3.1 事件处理机制............................................................................... 246
8.3.2 用事件监听器处理事件................................................................. 248
指点迷津:同一事件源的不同表示类型
小经验:事件监听器方法的返回类型
8.3.3 事件适配器................................................................................... 249
8.3.4 事件监听器的匿名内置类实现方式................................................ 252
8.3.5 事件处理的多重运用..................................................................... 253
8.3.6 高级事件处理............................................................................... 255
8.4 GUI组件上的图形操作........................................................................... 257
8.4.1 Graphics类.................................................................................... 257
指点迷津:如何打印字符文本
独家见解:JDK的失误
8.4.2 组件重绘...................................................................................... 260
8.4.3 图像操作...................................................................................... 264
8.4.4 双缓冲的技术............................................................................... 267