黑马程序员——GUI1:上篇

------ Java培训、Android培训、iOS培训、.Net培训、期待与您交流! -------

GUI1:上篇

1.  GUI概述

       GUI是Graphical User Interface的简称,意思是图形用户接口,其实就是Java标准类库中对外提供的用于图形化界面编程的各种组件(可以理解为各种类)。在继续介绍图形化界面编程之前,首先要说一说人机交互,以及人机交互的两种方式。

1.1  两种人机交互方式

       人机交互方式,就是人类与计算机之间进行交流的方式,就像人与人之间协同完成任务,就需要相互交流一样,人类如果希望计算机帮忙完成某个功能,也需要与其进行交流。那么交流的方式有两种,第一种就是我们常用的命令行——英文简称为CLI,是Command line User Interface的简称,意思是命令行用户接口。当我们手动编译并执行Java代码时都是通过命令行的方式向计算机发出指令,Windows操作系统下的命令行界面如下,

 

       再比如DOS操作系统的人机交互模式就是通过命令行的方式。以命令行的方式实现人机交互最大的缺点在于,对于普通用户来说很难记忆大量的命令代码,不便于操作。那么针对第一种人机交互方式的缺点,人们就开发出了第二种交互方式——图形化界面。图形化界面系统最典型代表就是Windows操作系统,所有的命令都隐藏在了一个个图标或者菜单栏下,只需要鼠标点击各种图标或菜单即可完成大多数操作,非常方便,学习成本比较低,方便一般用户掌握。

       了解了图形化界面以后,大家可以回想在之前的内容中,执行我们自己编写的程序时,无论是完成编译和执行操作,还是在程序执行过程中输入一些数据,亦或是程序执行完成后打印执行结果,都是在命令行界面内完成的。这似乎与我们日常使用的程序是不一样的,那些程序的一大特点就是具有漂亮的界面,并且大多数操作只需要鼠标点击即可完成。那么GUI编程的目的就是令我们自己编写的程序也具有图形化界面,并且可以使用鼠标操作。

1.2  GUI编程组件包——Awt和Swing

       Java语言作为面向对象编程语言,无论想要实现什么样的功能,首先都要找到与需求功能相关的类,那么Java标准类库中为GUI编程提供的类包含在了两个包中,分别是java.Awt和javax.Swing。

(1)  Awt

       其中Awt是AbstractWindow ToolKit的缩写,意思是抽象窗口工具包,这个包中提供了大量用于创建图形化界面的各种类。这个包中的类有一个共同的特点,就是需要调用本地系统方法实现功能,属于重量级控件。换句话说,Awt包中的类依赖于系统平台来提供各种图形化组件,比如我们在Win7系统下创建图形化界面,那么创建的窗口和各种按钮的形状、颜色、效果与Win7系统的窗口和按钮是一模一样的。因此,由于对所在系统平台的依赖性,在不同系统之间的移植性就稍微差一些,最直观的现象就是,在Windows操作系统创建的图形化界面class文件,在Linux系统下执行,窗口的颜色、形状、显示效果可能会有差异。

(2)  Swing

       为了提高图形化界面程序的可移植性,在Awt工具包的基础上,又开发出了Swing工具包,Swing包中的部分类直接继承自Awt包中的类,并复写了其中的方法。Swing工具包中组件的特点是完全脱离了系统平台的限制,建立了Swing特有的组件形状、大小、色彩等,因此使用Swing工具包开发的图形化界面程序,在任何系统下的显示效果都是一样的。大幅度提高了可移植性,属于轻量级控件。此外,Swing工具包中包含的组件种类要远多于Awt工具包,因此Swing工具包更常应用于图形化界面可开发中。

在介绍完GUI编程的目的,以及GUI编程相关的组件工具包的特点和区别以后,我们就来了解一下GUI编程具体会用到哪些组件、这些组件之间的体系关系,以及它们的作用和特点。

1.3  GUI编程相关组件

       下图表示的就是与GUI编程相关的常用组件,继承关系图, 


       其中,最顶层类称为Component,意思就是组件,它是所有GUI相关组件的根父类。

       首先,从右边部分开始介绍,

       Button:按钮,是最为常见的一种组件,不必细说。

       Label:标签,它的作用是在各种组件中显示文字,方便用户理解各种组件的用途,简单说Label就是用于封装文字。

       Checkbox:复选框,用于同时选择多个设置选项。

       TextCompnent:文本组件,用于接收用户使用键盘录入的文本。分为两种,TextArea,可以理解为文本区域,也就是多行文本,比如记事本中的可以输入文字的部分;             TextField,可以理解为文本框,也就是单行文本,比如浏览器中的地址栏。

       以上这些组件都是直接继承自Component,除了TextComponent以外相互之间并没有继承关系。接着,我们继续介绍左边部分组件。Container,表示容器,该组件也直接继承自Component,从字面意思理解它的作用(准确说是它子类的作用)是用来容纳其他组件,将它们组合为一个整体,这是Container组件继承体系的特点——Container(及其子类)作为组件,本身又可以添加其他组件。Container有两个常见子类,Window和Panel,前者表示窗口,后者表示面板,其中Panel的作用比较特殊,后面会说到。Window又向下继承了两个子类,Frame和Dialog,分别表示框架和对话框,而Dialog的子类FileDialog表示文件对话框。其中Frame、Dialog以及FileDialog是我们实际开发真正使用到的组件(创建实例对象),那么Frame用于构成复杂对话框,通常能够独立运行,并提供多种功能;而Dialog就是包含在主程序中,并依赖于主程序存在,功能比较单一,比如用于错误提示等等。此外,当我们在记事本中点击“另存为”时,弹出的用于设置文件名、文件格式、文件保存路径等等功能的对话框称为文件对话框,也就是FileDialog。

2.  布局

2.1  常用布局简介

       当我们在创建图形化界面的时候,首先遇到的问题就是各种各样的组件在一个窗体中应该将它们放置在什么位置,这个问题我们称为布局。那么Java语言对外提供了排放组件的多种布局方式,下面我们就对其中常用的布局方式进行简单介绍和演示。

(1)  流式布局——FlowLayout

       这种布局方式的特点是将组件按照从左到右的的顺序放置到窗体中。当向窗体中添加第一个组件时,默认在第一行居中放置;继续添加第二个组件时,就会将第一个组件移至左边,并将新组件放置在其右侧;如果一行装不下,就会自动放置到第二行,以此类推。我们也可以指定组件的默认方式位置是偏左、偏右、居中等等。

(2)  边界式布局——BorderLayout

       边界式布局的特点是,将组件布置到窗体的东、南、西、北、中5个区域,并且每个区域只能放置一个组件。如果,没有为将要添加的组件设置放置位置,那么该组件将会按照与窗体相同的大小完全填充整个窗体,并且下一个添加的组件将会按照同样的大小将先前组件覆盖。边界式布局是Frame窗体的默认布局方式。

(3)  网络式布局——GridLayout

       网络式布局是以矩形网格形式对窗体的组件进行布置。这种布局方式首先将整个窗体等分为大小相同的若干矩形,每个矩形中可以放置一个组件,最典型的例子就是Windows自带的计算器软件,数字按钮和运算符按钮均整齐的布置在窗体中。

(4)  网格包式布局——GridBagLayout

       网格包式布局与网格式布局基本相同,但是每个区域的并不一定是等分的矩形,而是任意大小的长方形,并且可以很跨多个行和列。

(5)  卡片式布局——CardLayout

       在这种布局方式中,将窗体中的每个组件看作是一张卡片,一次只能看到一张卡片下的内容,如下图所示,

 

红框所选部分就是卡片式布局下,组件的布置方式。

       以上我们就对几种常见的布局方式进行了简单介绍,那么在使用时只需创建所需布局方式类的实例对象,并按照需求调用它们的方法进行设置即可完成对各种的布局。对以上这些布局方式的具体布局效果和详细的设置方法,请参阅API文档。

       实际上,布局是实际开发图形化界面过程中相对困难的部分。因为若要开发出漂亮的图形化界面,需要进行大量的调试,比较麻烦,因此在正式通过代码开发之前一般需要打一个草稿。后期为了提高图形化界面的开发效率,又研发除了所谓的“坐标式布局”。这种布局方式,可以将任意组件放置在窗体的任意位置,并且组件的对齐方式也是任意的。若要实现“坐标式布局”通常需要高级编辑器(比如JBuilder),它可以提供所谓的“画布”功能,一个画布就相当于一个窗体,只需要将各种图形化组件拖入画布中即可,并且在拖入过程中,软件自动生成对应的代码。

2.2  Panel的用途

       很多时候,一个窗体并不一定是一种布局方式,为了满足功能和美观的需求,需要两种或以上的布局混合在一起,而Panel可以帮助实现这一功能。首先为主窗体设定一种布局方式,然后向其中添加若干Panel组件,Panel组件本身就会按照窗体的布局方式分布。然后再为每个Panel设定布局方式,最后将所需组件添加到各个Panel中,这样不同区域(Panel)中的组件就会遵循不同的布局方式。

3.  创建一个简单的窗口

       创建一个窗口以前,首先需要了解创建一个窗口所需的组件(类),那么按照以往的习惯,我们还是从父类开始从整体上把握这个体系,以此快速掌握该体系各成员的共性特征与方法。由于Container类中包含有我们以后开发中常用的方法,因此直接从Container开始介绍,不再单独介绍Component。此外需要说明的是,这一篇博客中,我们不再对每个类的特点和方法进行详细的描述和介绍,一方面GUI相关类的方法过多;另一方面,实际开发中也是需要自行查阅API文档的。

3.1  Container

       Container作为容器,需要通过接收各种组件对象来完成图形化界面的开发,因此该类对外提供了若干add方法重载形式,用于添加其他组件对象,而这也是Container体系分支最常用的方法。我们只强调其中有一个add方法重载形式:add(Component comp, int index),将指定组件添加到此容器的指定位置上。那么从第二个参数index可以看出,Container及其子类作为一种“容器”,其方法行为看起来像一个集合,而实际上Container内部确实封装了一个集合对象,用于存储各个组件对象。

3.2  Frame

API文档:

       Frame是带有标题和边框的顶层窗口,默认布局为BouderLayout。

构造方法:

       public Frame()throws HeadlessException:构造一个最初不可见的Frame新实例。Frame标题为空。

       public Frame(Stringtitle) throws HeadlessException:构造一个新的、最初不可见的、具有指定标题的Frame对象。所谓标题指的就是窗体的名称,比如双击打开“我的电脑”图标,窗体左上角显示的“我的电脑”就是标题。

       关于Frame组件我们不再作过多描述,而是直接演示该类的使用方法,并在演示过程中再一一进行说明。

(1)  创建一个窗体

代码1:

import java.awt.*;
 
class AwtDemo
{
	public static void main(String[] args)
	{
		//创建一个窗体对象,并指定窗体名称
		Frame f = new Frame("Prime");
             
		//设置窗体大小
		//其中第一个参数表示宽度,第二个参数表示高度
		f.setSize(500,650);
             
		//设置窗口出现在屏幕上的坐标
		f.setLocation(300,300);
		//设置窗口可见
		f.setVisible(true);
 
		//故意保留输出语句,用以观察执行结果
		System.out.println("HelloWorld!");
	}
}
下面我们按照代码的执行顺序,解释每一步的作用。

       首先,创建了一个Frame对象,并为其指定了标题,一个Frame对象代表一个实际的窗体;

       第二,设置窗体大小,setSize方法第一个参数表示窗体宽度,第二个参数表示窗体高度,如果不设置窗体大小那么开启的窗体默认处于最小化状态,如下图所示,

 

       第三,设置窗体默认出现的位置,setLocation方法第一个参数表示窗体左上角距屏幕左边界的距离,第二个参数表距上边界的距离。若不设置,则默认出现在屏幕最左上角。

       最后,设置窗体可见,调用setVisible的同时,传递true即可。这一步是必须要执行的,因为Frame类的构造方法告诉我们:构造一个最初不可见的Frame新实例。对于以上代码,若不设置窗体可见,那么执行结果就是打印“Hello World!”后,等待一段时间程序将自行结束,而不开启任何窗体。

       代码1的执行结果为,在命令行中打印“Hello World!”,接着在屏幕指定位置弹出指定大小、指定标题的窗体。注意到,此时命令行仍处于等待状态,表示程序没有执行完毕,至少还有一个前台线程正在运行,而这个线程就是专门用于控制图形化界面的独立的线程。此外,目前我们还无法通过点击右上角的红叉来关闭这个窗体,只能在命令行中输入“Ctrl+C”结束程序运行的同时关闭窗体。

(2)  向窗体中添加组件

       窗体创建完了,自然就想到向其中添加一些组件,比如按钮等。

代码2:

import java.awt.*;
 
class AwtDemo2
{
	publicstatic void main(String[] args)
	{
		Frame f = new Frame("Prime");
		f.setSize(300,450);
		f.setLocation(300,100); 
		//设置窗体布局
		f.setLayout(newFlowLayout());
 
		//创建按钮组件
		Button but = new Button("按钮");
      
		//将按钮添加到窗体中
		f.add(but);
		f.setVisible(true);
 
		//故意保留输出语句,用以观察执行结果
		System.out.println("HelloWorld!");
	}
}
       我们在代码1的基础上为主窗体创建并通过add方法添加了一个按钮组件。但是在添加组件以前首先要做的是指定主窗体的的布局(这里以流式布局为例),否则,就会因为Frame的默认布局为边界式布局,并且在没有指定组件添加区域的前提下,这些组件将会充满整个窗体,并且之后添加的组件会覆盖先前的组件。那么执行代码2以后,窗体的 终显示效果为,

 

最后,我们对创建图形化界面的步骤做一简单总结:

第一步,创建Frame窗体;

第二步,对窗体进行各项设置,比如大小、位置、布局等;

第三步,创建各个组件;

第四步,将组件通过窗体的add方法添加到窗体中;

最后,通过setVisiable方法,设置窗体可见。

4.  事件监听机制——针对用户操作作出响应

4.1  事件监听机制原理

       通过上述5个步骤,我们创建了一个非常简单的带有按钮的窗口。但是,这个窗口除了可以放大缩小,没有其他任何用处,点击按钮没有反应是因为我们并没有为按钮定义任何效果,但是右上角的红叉竟然也起不到关闭窗口的作用,说明这也需要程序员去定义才能够实现,而若要对用户的各种操作作出针对性的响应就必须引入到事件监听机制。

       所谓事件监听,简单说就是在当外部动作——比如鼠标点击、键盘录入等操作——施加到窗体某个组件时,监听器监听这些动作的发生后,就会启动预先定义好的各种响应效果。下图所示为试件监听机制原理图,

 

       下面我们根据以上原理图,按照序号顺序一步步说明事件监听机制的原理。

       首先,需要将一个所谓的监听器“注册”到事件源(组件)中。所谓监听器就是监听各个组件中所发生的各种特定动作的一个对象,而“注册”的意思就是将某个事件源设定为被监听对象。通常情况下,当开启一个窗口的同时将会有多个监听器分别监听不同的组件;

       接着,一旦有外部动作(鼠标点击、键盘录入等)施加到某事件源后,该事件源就会将这一外部动作按照面向对象的原则封装为“事件”对象并传递给监听这一组件的监听器,那么这也就是将组件称为事件源的原因——“事件”对象都是由各个组件发出的。将事件封装为对象的目的就是可以封装各种事件相关信息,比如事件所属于哪个事件源。

       最终,监听器接收到特定“事件”对象后,就会针对不同事件按照预先定义的事件处理方式作出不同的响应,而这些事件处理方式是需要程序员定义到监听器中的。需要注意的是,监听器并非对每个动作都会做出响应,比如对于某个按钮,监听器中预定义的响应动作只有在鼠标左键被按下时才能作出响应,那么即使接受到“鼠标右键点击事件”也不会作出任何响应。

       举一个简单的例子,假设事先设定好了窗口右上角“红叉”的关闭功能,并且按钮监听器已经在监听这个按钮,那么当鼠标左键点击“红叉”时,按钮将“鼠标点击事件”封装为一个事件对象并发送给监听器,监听器接收到该“事件”后根据预先定义好的事件处理方式将窗口关闭掉。当然,在Windows系统中若想关闭一个窗口,还可以按下组合键“alt+F4”,那么当监听器接收到键盘同时按下这两个按键的事件对象,同样会引发关闭窗口的动作。

       实际上,虽然上述事件监听机制乍看起来比较复杂,但是在实际开发中真正需要我们去定义的只有事件响应动作,换句话说,我们只需要去决定当用户进行某些操作后,程序应该作出怎样的反应就好,而诸如事件源(各个组件)、事件对象以及组件监听器,在Java标准类库中都已经为我们准备好了。

4.2  事件监听机制的代码实现

(1)  实现事件监听机制的事前准备

       下面我们就按照上述事件监听机制的原理,通过代码先来实现窗口右上角“红叉”的关闭功能。在此之前,首先需要了解监听器类的特点和相关方法,以及监听器实现其功能的方式。

       第一步就是将监听器注册到窗口上,需要调用Frame的addWindowListener方法,该方法继承自Window组件,其方法名已经告诉我们了它的作用:

       public void addWindowListener(WindowListener l):添加指定的窗口侦听器(也就是监听器),以从此窗口接收窗口事件。该方法的参数类型为WindowListener,意思就是窗口监听器,需要我们手动创建定义好响应动作的监听器对象,并在调用Frame组件的addWindowListener方法时传递到其中。

为了定义监听器内部的响应动作,我们还需要了解WindowListener接口的各个抽象方法——用以定义响应动作。以监听关闭窗口动作的方法为例:

       void windowClosing(WindowEvent e):用户试图从窗口的系统菜单中关闭窗口时调用。该方法中将要定义的就是接收到窗口事件对象后作出的响应动作。需要注意的是,方法参数类型WindowEvent代表的就是“窗口事件”类型,那么该类对象就是在窗口组件受到外部的动作以后由组件创建并自动传递给监听器的,而不是我们手动创建的,这一机制有点类似于异常机制的try-catch处理方式。

       由于WindowListener是一个接口,因此当我们自定义它的实现类时,需要复写包括windowClosing方法在内的所有7个抽象方法(可查阅API文档)。那么问题就来了,为了创建窗口监听器实例对象,需要将这7个方法全部复写一遍,非常麻烦,尤其是我们仅需要其中一两个方法的功能的时候。因此为了提高开发效率,Java标准类库又为我们提供了一个WindowListener接口的实现抽象类WindowAdapter:接收窗口事件的抽象适配器类。此类的方法体为空,也就是说该抽象类实现WindowListener接口的同时复写了所有抽象方法时,但没有定义具体的方法体。这样做的好处是,我们定义WindowAdapter子类时,只要按需复写方法即可,因此此类存在的目的就是方便创建侦听器对象。

(2)  基本代码实现

       下面我们就按照以上思路,定义试图通过“红叉”按钮关闭窗口时的响应动作。

代码3:

import java.awt.*;
 
/*
       监听器类WindowAdapter包含在java.awt.event中
       因此也需要导入这个包
*/
import java.awt.event.*;
 
class AwtDemo3
{
	public static void main(String[] args)
	{
		Frame f = new Frame("Prime");
		f.setSize(300,450);
		f.setLocation(300,100); 
		//设置窗体布局
		f.setLayout(newFlowLayout());
 
		//创建按钮组件
		Button but = new Button("按钮");
      
		//将按钮添加到窗体中
		f.add(but);
 
		//将窗口监听器注册到窗口上
		f.addWindowListener(newMyWindowListener());
 
		f.setVisible(true);
	}
}
//定义自定义窗口监听器类,继承WindowAdapter
class MyWindowListener extends WindowAdapter
{
	public void windowClosing(WindowEvent e)
	{
		//为证明窗口确实接收到了外部动作,这里打印窗体事件对象中存储的事件信息
		System.out.println("Windowclosing!\n"+e.toString());
		//退出虚拟机,同时也就关闭了该窗体
		System.exit(0);
	}
}
执行以上代码在命令行中的打印结果为:

Window closing!

sun.awt.TimedWindowEvent[WINDOW_CLOSING,opposite=null,oldState=0,newState=0]on

frame0

       打印完以上信息后,退出了Java虚拟机,那么窗口也就被关闭了。以上打印信息的第二行就是监听器接收的窗体事件对象所存储的事件信,这里不详细说明。

       此外还需要提醒大家的是,各个监听器接口和监听适配器类均在java.awt.event包中,因此一般要同时导入java.awt和java.awt.event两个包。

(3)  匿名内部类在事件监听机制中的应用

       在以上代码中“f.addWindowListener(newMyWindowListener());”的作用就是将我们自定义的窗口监听器对象注册到窗口中。为了创建监听器对象,在测试类AwtDemo3外部,自定义了WindowAdapter的子类。在这一过程中我们注意到,既然我们只需要复写WindowAdapter的一两个方法,那么为了简化代码书写,可以使用匿名内部类的方式,如下代码所示,

代码4:

//将窗口监听器注册到窗口上
f.addWindowListener(new WindowAdapter(){
       publicvoid windowClosing(WindowEvent e)
       {
              System.exit(0);
       }
});
以上代码中直接创建了WindowAdapter的子类对象,并复写了windowClosing方法,其效果与代码3是相同的,但代码量大大降低了,当然随之而来的就是阅读性的下降,因此通常只需复写3个及以下数量的方法时可以使用匿名内部类,否则最好还是单独定义一个监听器类,以方便其他人阅读。

(4)  GUI程序代码优化

       注意到,在代码3中,我们将定义组建对象引用的代码各组件的创建和设置代码以及事件监听代码,全部写到主函数当中了。而这样编写代码显得很混乱,因为不同功能的代码混在一起,不仅阅读性差,而且不利于后期的修改。为了提高代码的阅读性,我们队代码3进行一个简单地优化——将上述三部分代码分开来编写。这样的做的优点是将相似功能的代码集中起来,便于维护,尤其是在实际开发中面对非常的庞大的代码量时更是如此。优化后的代码如下,

代码5:

package test2;
 
import java.awt.*;
import java.awt.event.*;
 
//将需要创建的窗体单独定义为一个类
class MyFrame
{
	//定义窗口所需组件对象的引用,方便整个类中调用
	private Frame f;
	private Button but;
 
	MyFrame()
	{
		//在构造方法中调用init方法,完成对窗体的初始化动作
		init();
	}
 
	public void init()
	{
		f = new Frame("Prime");
 
		//同时设置窗体大小和初始化位置
		f.setBounds(450,200,600,450);
		//设置窗体布局为边界式布局
		f.setLayout(newBorderLayout());
 
		but = new Button("Cancel");
		//将按钮添加到窗体的同时,指定按钮在窗体的位置为最低端
		f.add(but,BorderLayout.SOUTH);
 
		//加载窗体监听器
		myEvent();
 
		//显示窗体
		f.setVisible(true);
	}
	private void myEvent()
	{
		f.addWindowListener(newWindowAdapter(){
			public void windowClosing(WindowEvent e)
			{
				System.exit(0);
			}
		});
	}
}
class AwtDemo4
{
	public static void main(String[] args)
	{
		MyFramem f = new MyFrame();
	}
}
执行以上代码后创建的窗口与代码3基本相同,这里不再演示。

(5)  其他常见窗口事件监听方法演示

       除了将窗口关闭以外,窗口监听器还能用于监听其他各种窗口动作,比如激活窗口、打开窗口等,以下我们对其中几种常见的方法进行演示,如下代码所示,

代码6:

f.addWindowListener(new WindowAdapter(){
	//监听激活窗口动作
	public void windowActivated(WindowEvent e)
	{
		System.out.println("窗口被激活!");
	}
	//监听窗口打开动作
	public void windowOpened(WindowEvent e)
	{
		System.out.println("窗口被打开!");
	}
});
执行以上代码后,在命令行中将同时打印“窗口被激活!”和“窗口被打开!”,并且当我们最小化窗口接着恢复窗口以及该窗口被前置时都会打印“窗口被激活!”。那么这也就说明了所谓的激活状态就是将窗口置于屏幕最前端的状态,而打开就是窗口被创建的那一刻,因此打开状态只有一次。

4.3  其他事件监听演示

(1)  按钮事件监听

       除了可以对窗口事件进行监听,同样可以监听其他组件事件,比如我们这一小部分将要介绍的按钮事件监听。既然被监听对象是按钮,也即按钮是事件源,就应该将按钮监听器注册到按钮组件中,就需要去调用按钮的添加监听器方法,查找Button类的API文档可知,对应的方法为addActionListener(ActionListenerl),意思是添加活动监听器,而并非是按钮监听器,这是需要大家注意区分的。

代码7:

//只呈现事件监听代码
private void myEvent()
{
	f.addWindowListener(newWindowAdapter(){
		public void windowClosing(WindowEvent e)
		{
			//打印信息以表明是通过窗体关闭窗口
			System.out.println("窗体:退出");
			System.exit(0);
		}
	});
	//将按钮监听器注册到按钮中,并为其定义退出程序的功能
		but.addActionListener(newActionListener(){
			public void actionPerformed(ActionEvent e)
			{
				//打印信息以表明是通过按钮关闭窗口
				System.out.println("按钮:退出");
				System.exit(0);
			}
	});
}
执行以上代码后,点击“Cancel”按钮同样可以关闭窗口。可能有朋友会提出疑问:为什么按钮组件的监听器不像窗体那样使用适配器呢?查阅ActionListener接口的API文档可知,该接口只有一个方法——actionPerformed(ActionEvent e),那么创建它的实现类对象时,只需复写一个方法即可,因此没有必要提供适配器。ActionListener是少数没有提供适配器的组件监听器类,一般情况下只要监听器接口的方法超过3个,就会为其提供适配器。

(2)  鼠标事件

        当看到这个标题是,可能有朋友会有这样的疑惑:前面的内容中,被监听对象总是组件,也就是说事件对象总是由组件产生并发出,比如窗口或者按钮等,为什么还能够对计算机设备进行监听呢?实际上,如果查阅Component类的API文档,在其方法摘中就有如下两个方法:

        public voidaddKeyListener(KeyListener l):添加指定的按键(键盘)侦听器,以接收发自此组件的按键事件。如果l为null,则不会抛出异常并且不执行动作。

        public void addMouseListener(MouseListenerl):添加指定的鼠标侦听器,以接收发自此组件的鼠标事件。如果侦听器l为null,则不会抛出异常并且不执行动作。

        也就是说,一个组件受到的外部动作,可以是来自鼠标,也可以是键盘,比如一个按钮,它可以被鼠标按下,也可以被键盘按下,那么如果我们需要为来自不同设备的外部动作定义不同的响应动作的话,就有必要为这两个设备定义不同的事件,并分别为对这两个设备进行监听;当然也有可能不论是什么设备对该组件施加动作,都定义相同的响应动作时,就可以使用组件监听器,比如Button组件的addActionListener(ActionEvente)方法。

        查阅鼠标监听器接口MouseListener的API文档,方法摘要中包含了用于定义发生各种鼠标事件时的响应动作的方法,包括鼠标被按下事件、鼠标移动到组件上事件、鼠标离开组件事件、鼠标被按下但没有释放事件以及鼠标按下并被释放事件等等,我们不再一一介绍,请参考以下演示代码。由于该接口方法数量大于3个,因此同样为其定义了事件监听适配器MouseAdapter。以下我们首先演示鼠标事件监听,代码如下,

代码8:

import java.awt.*;
import java.awt.event.*;
 
//将需要创建的窗体单独定义为一个类
class MyFrame
{
	//定义窗口所需组件对象的引用,方便整个类中调用
	private Frame f;
	private Button but;
 
	MyFrame()
	{
		//在构造方法中调用init方法,完成对窗体的初始化动作
		init();
	}
 
	public void init()
	{
		f = new Frame("Prime");
 
		//同时设置窗体大小和初始化位置
		f.setBounds(450,200,600,450);
		//设置窗体布局为边界式布局
		f.setLayout(newBorderLayout());
 
		but = new Button("Cancel");
		//将按钮背景色初始化为绿色
		but.setBackground(Color.GREEN);
		//将按钮添加到窗体的同时,指定按钮在窗体的位置为最低端
		f.add(but,BorderLayout.SOUTH);
 
		//加载窗体监听器
		myEvent();
 
		//显示窗体
		f.setVisible(true);
	}
	private void myEvent()
	{
		f.addWindowListener(newWindowAdapter(){
			public void windowClosing(WindowEvent e)
			{
				//打印信息以表明是通过窗体关闭窗口
				System.out.println("窗体:退出");
				System.exit(0);
			}
		});
		//按钮按钮添加鼠标事件监听器
		but.addMouseListener(newMouseAdapter(){
			private int enter_count = 1,click_count = 1,exit_count = 1;
 
			//鼠标进入
			public void mouseEntered(MouseEvent e)
			{
				System.out.println("鼠标进入"+enter_count++);
				//当鼠标进入按钮时将按钮背景色改为红色
				but.setBackground(Color.RED);
			}
			//鼠标点击
			publicvoid mouseClicked(MouseEvent e)
			{
				System.out.println("鼠标点击"+click_count++);
			}
			//鼠标离开
			publicvoid mouseExited(MouseEvent e)
			{
				System.out.println("鼠标离开"+exit_count++);
				//当鼠标离开按钮时将按钮背景色改回绿色
				but.setBackground(Color.GREEN);
			}
		});
		//为按钮添加活动监听器
		but.addActionListener(newActionListener()
		{
			private int count = 1;
 
			public void actionPerformed(ActionEvent e)
			{
				System.out.println("按钮活动"+count++);
			}
		});
 
	}
}
class AwtDemo5
{
	public static void main(String[] args)
	{
		MyFramem f = new MyFrame();
	}
}
        请大家自行执行以上代码,并观察效果。需要说明的是,当为按钮添加活动监听器时,鼠标点击或者按下空格键均可以触发按钮活动,但是如果同时添加了鼠标事件监听器,那么使用鼠标按下按钮时,首先触发鼠标事件,然后才会触发活动事件。由于活动事件同时支持鼠标和键盘触发,因此建议为按钮添加活动事件监听。

        下面我们进行一个鼠标事件的练习——实现对按钮双击事件的监听及响应。显然鼠标事件监听器中并没有定义对鼠标双击事件的监听方法,但是我们可以通过MouseEvent的getClickCount()方法获取到鼠标点击次数,并通过记录鼠标点击次数来判断鼠标双击事件,并对其作出响应的响应动作。这其中点击次数就被作为事件信息封装了起来,实现代码如下,

代码9:

//鼠标双击
public void mouseClicked(MouseEvent e)
{
	//通过判断鼠标点击次数来分辨是否为鼠标双击事件
	if(e.getClickCount()== 2)
		System.out.println("鼠标双击"+click_count++);
}
执行以上代码后,如果单击按钮,只会触发按钮的活动事件,只有连续按下两次时,才会触发按钮双击事件。

(3)  键盘事件

        以同样的思路对按钮组件添加键盘事件监听器,需要调用组件的addKeyListener方法,并传入键盘事件监听适配器对象——KeyAdapter,因为键盘事件监听器接口中包含有3个方法,分别是keyPressed(按下某个键)、keyReleased(释放某个键)以及keyTyped(键入某个键),这里我们主要演示keyPressed方法。

        那么我们将要在keyPressed方法内定义响应动作的原理为,按钮组件只有当特定按键被按下时才能触发响应动作,比如只有当按下回车键时,按钮组件才能触发动作,而按下其他按键没有任何作用。这就需要对按下按键的键值进行判断,按照事件监听机制的工作原理,按键的键值信息应存储在键盘事件对象KeyEvent中。

        查阅KeyEvent类的API文档,其中定义了大量的静态整型字段,比如我们熟悉的“VK_ENTER”,代表回车键,这些字段其实就是代表了常用键盘上所有的按键名称与对应的键值(用整型值表示)。而在方法摘要中还可找到返回键盘事件对象中存储的键值信息的方法——getKeyChar和getKeyCode,那么通过对这两个方法的返回值进行判断,即可知道被按下的是否是特定按键,并据此作为是否触发动作的依据。

        下面我们在代码8的基础上,为按钮组件添加键盘事件,并将接收到的键盘事件中的按键信息打印到控制台,代码如下,

代码10:

//这里只呈现事件监听代码中的键盘事件监听部分,其余代码与代码8相同
private void myEvent()
{
	//为按钮添加键盘监听器
	but.addKeyListener(newKeyAdapter(){
		public void keyPressed(KeyEvent e)
		{
			System.out.println(e.getKeyChar()+"= "+e.getKeyCode());
		}
	});
}
执行以上代码后,当按钮为当前事件源时,按下任意按键,就会在控制台中显示键值信息,如下所示:

a = 65

p = 80

p = 80

l = 76

e = 69

        等号左边表示按键对应的字符(这里要注意,该字符并不表示按键的名称,而是键值在ASCII表中对应的字符),等号右边是按键对应的键值。如果按下,空格、回车或者Shift等按键时,虽然等号右边还能够显示按键对应的键值,但等号左边不显示任何字符,这是因为ASCII表中没有并没有为它们定义相应的字符。如果想打印按键的名称,可以调用KeyEvent类的静态方法getKeyText,并传递getKeyCode方法的返回值,代码如下,

代码11:

but.addKeyListener(new KeyAdapter(){
	public void keyPressed(KeyEvent e)
	{
		System.out.println(KeyEvent.getKeyText(e.getKeyCode())+"= "+e.getKeyCode());
	}
});
当按下空格、回车和Shift时的执行效果如下,

空格 = 32

Enter = 10

Shift = 16

大家可以自行尝试按下其他按键,并观察效果。

        如果希望当特定按钮被按下时,按钮组件能够触发响应动作,可以通过一个if语句进行判断,如下代码所示,

代码12:

but.addKeyListener(new KeyAdapter(){
	public void keyPressed(KeyEvent e)
	{
		//当按下“Esc”键时,推出虚拟机,并关闭窗口
		if(e.getKeyCode() == KeyEvent.VK_ESCAPE)
			System.exit(0);                
	}
});
以上代码中,将键盘试件中存储的键值与KeyEvent类中的“VK_ESCAPE”静态整型常量值进行比较,当两者相等时,退出虚拟机。

        在很多时候,我们还可以通过组合键对软件进行操作,比如“Alt+F4”组合键就能够关闭当前窗口,在QQ中可以通过“Ctrl+Enter”组合键发送信息等。KeyEvent的父类InputEvent中定义了一个名为“isControlDown”的方法,它能够检测“Alt”键是否被按下,通过该方法能够实现“Ctrl+Enter”组合键操作,代码如下,

代码13:

but.addKeyListener(new KeyAdapter(){
	public void keyPressed(KeyEvent e)
	{
		if(e.isControlDown() && e.getKeyCode() == KeyEvent.VK_ENTER)
		{    
			System.out.println("Ctrl+Enter");
		}
	}
});
以上代码中,if判断语句同时判断了“Ctrl”和“Enter”是否被按下,只有两个同时被按下时才会打印“Ctrl+Enter”。

        下面做一个键盘监听练习,在窗口中创建一个文本框,该文本框只能输入数字。实现该功能就需要通过键盘事件监听,获取到被按下按键的键值,如果键值不包含在9个数字对应的范围内,则不在文本框中显示,先给出完整代码,

代码14:

import java.awt.*;
import java.awt.event.*;
 
class MyFrame
{
	private Frame f;
	private Button enter;
 
	//定义一个文本框
	private TextField tf;
 
	MyFrame()
	{
		init();
	}
 
	public void init()
	{
		f = new Frame("Prime");
 
		f.setBounds(450,200,600,450);
		//要求每个组件默认居中布置
		f.setLayout(newFlowLayout(FlowLayout.CENTER));
 
		tf = new TextField(40);//创建并设置文本框长度
		tf.setFont(newFont(Font.DIALOG,Font.PLAIN,20));//为文本框设置字体
		f.add(tf);
 
		enter = new Button("Button");
		enter.setFont(newFont(Font.DIALOG,Font.PLAIN,20));//为按钮名称设置字体
		f.add(enter);
 
		//加载窗体监听器
		myEvent();
 
		//显示窗体
		f.setVisible(true);
	}
	private void myEvent()
	{
		f.addWindowListener(newWindowAdapter(){
			public void windowClosing(WindowEvent e)
			{
				//打印信息以表明是通过窗体关闭窗口
				System.out.println("窗体:退出");
				System.exit(0);
			}
		});
		//为文本框添加键盘事件监听器
		tf.addKeyListener(newKeyAdapter(){
			public void keyPressed(KeyEvent e)
			{
				int code = e.getKeyCode();
				//判断按下被键盘键值是否是字母
				if(code >= KeyEvent.VK_A && code <= KeyEvent.VK_Z)
					e.consume();//使得被按下的按键不能显示在文本框中
			}
		});
	}
}
class AwtDemo6
{
	public static void main(String[] args)
	{
		MyFrame mf = new MyFrame();
	}
}
代码说明:

        在if语句中判断了接收的键盘事件中存储的键值是否包含在字母范围中。如果确实为字母,则会调用对应键盘事件对象的consume()方法,该方法的作用就是阻止事件对象本身按照Java源代码中定义的那样操作。拿键盘事件对象为例,当文本框接收到一个键盘事件时,如果该事件对象中存储的键值对应的是能够被打印的字符,那么默认的操作方式就是显示在文本框中。但若调用了consume方法,则无法显示。


  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值