AWT是可视化编程,就是说代码写上去的内容自己运行是可以直接看到的,这样就比较有趣,所以AWT编程的学习乐趣还是非常多的。
本章要点

  • 图形用户界面编程的概念
  • AWT的概念
  • AWT容器的常见布局管理
  • 使用AWT基本组件
  • 使用对话框
  • 使用文件对话框
  • Java的事件机制
  • 事件源、事件、事件监听器的关系
  • 使用菜单条、菜单、菜单项创建菜单
  • 创建并使用右键菜单
  • 重写paint()方法实现绘图
  • 使用Graphics类
  • 使用BufferedImage和ImageIO处理位图
  • 使用剪切板
  • 剪切板数据分格
  • 拖放功能
  • 拖放目标与拖放源
    本章和下一章的内容会比较“有趣”,因为可以看到非常熟悉的窗口、按钮、动画等效果,而这些图形界面的元素不仅会让开发者感到更“有趣”,对最终用户也是一种诱惑,用户总是喜欢功能丰富、操作简单的应用,图形用户界面的程序就可以满足用户的这种渴望。
    Java使用AWT和Swing类完成图形用户界面编程,其中AWT的全程是抽象窗口工具类(AbstractWindowToolkit),它是Sun最早提供的GUI库,这个GUI库提供了一些基本功能,但这个GUI库的功能比较有限,所以后来又提供了Swing库。通过AWT和Swing提供的图形界面组件库,Java的图形用户界面编程非常简单,程序只要依次创建所需的图形组件,并以何时的方式将这些组件组织在一起,就可以开发出非常美观的用户界面。
    程序以一种“堆积木”的方式将这些图形用户组件组织在一起,就是实际可用的图形用户界面,但这些图形用户界面还不能与用户交互,为了实现图形用户界面与用户交互操作,还应为程序提供事件处理,事件处理负责让程序可以响应用户动作。
    通过学习本章,读者应该能开发出简单的图形用户界面引用,并提供相应的时间相应机制。本章也会介绍Java中的图形处理、剪切板等操作知识。

Java改进的GUI(图形用户界面)和AWT

前面所介绍的所有程序都是基于命令行的,基于命令行的程序可能只有一些“专业”的计算机人士才会使用。例如前面编程的五子棋、梭哈游戏等程序,恐怕只有程序员自己才愿意玩这么“糟糕”的游戏,很少有最终用户愿意对着黑糊糊的命令行界面敲命令。
相反,如果为程序提供直观的图形用户界面(Graphics User Interface,GUI),最终用户通过鼠标拖动、单机等动作就可以操作整个应用,整个应用程序就会受欢迎的多(实际上,Windows之所以广为人知,其最初的吸引力就是来自于它所提供的图形用户界面)。作为一个程序设计者,必须优先考虑用户的感受,一定要让用户感受到“爽”,程序才会被需要、被使用,这样的程序才有价值。
当JDK1.0发布时,Sun提供了一套基本的GUI类库,这个GUI类库希望可以在所有平台下都能运行,这套基本类库被称为“抽象窗口工具集”(Abstract Window Toolkit),它为Java应用程序提供了基本的图形组件、AWT是窗口框架,它从不同平台的窗口系统中提取出共同组件,当程序运行时,将这些组件的创建和动作委托给程序所在的运行平台。简而言之,当使用AWT编写图形界面引用时,程序仅指定了界面组件的位置和行为,并未提供真正的实现,JVM调用操作系统本地的图形界面来创建和平台一致的对等体。
使用AWT创建的图形界面引用和所在的运行平台有相同的界面风格,比如在Windows操作系统上,它就表现出Windows风格,在UNIX操作系统上,它就表现出UNIX风格。Sun希望采用这种方式来实现“Write Once,Run Anywhere”的目标。
但在实际引用中,AWT出现了如下几个问题。

  • 使用AWT做出的图形用户界面在所有的平台上都显得很丑陋,功能也非常有限。
  • AWT为了迎合所有主流操作系统的界面设计,AWT组件只能使用这些操作系统上图形界面组件的交集,所以不能使用特定操作系统上复杂的图形界面组件,最多只能使用4种字体。
  • AWT用的是非常笨拙、非面向对象的编程模式。
    1996年,Netscape公司开发了一套工作方式完全不同的GUI库,简称为IFC(Internet Foundation Classes),这套GUI的所有图形界面组件,例如文本框、按钮都是绘制在空白窗口上的,只有窗口本身需要借助于操作系统的窗口实现。IFC真正实现了各平台上的界面一致性。不久,Sun和Netscape合作完善了这种方法,并创建了一套新的用户界面库:Swing。AWT、Swing、辅助功能 API、2D API以及拖放API共同组成了JFC(Java Foundation Classes,Java基础类库),其中Swing组件全面替代了Java1.0中的AWT组件,但保留了Java1.1中的AWT事件模型。总体上,AWT是图形用户界面编程的基础,Swing组件替代了绝大部分AWT组件,对AWT图形用户界面编程有极好的补充和加强。
    Java9的AWT和Swing组件可以自适应高分辨率屏。在Java9之前,如果使用高分辨率屏,由于这种屏幕的像素密度可能是传统显式设备的2-3倍(即单位面积里显式像素更多),而AWT和Swing组件都是基于屏幕像素计算大小的。因此这些组件在高分辨率屏上比较小。
    Java9对此进行了改进,如果AWT或Swing组件在高分辨率屏幕上显示,那么组件的大小可能会以实际屏幕的2个或3个像素作为“逻辑像素”,这样就可保证AWT或Swing在高分辨率屏上也具有正常大小。另外,Java9也支持OS X设备的视网膜屏。
    简而言之,Java9改进后的AWT和Swing组件完全可以在高分辨率屏、视网膜屏上具有正常大小。
    Swing并没有完全替代AWT,而是建立在AWT基础之上,Swing仅提供了能力更强大的用户界面组件,即使是完全采用Swing编写的GUI程序,也依然需要使用AWT的事件处理机制。本章主要介绍AWT组件,这些AWT组件在Swing里将有对应的实现,二者用法基本相似,下一章会有更详细的介绍。
    所有和AWT编程相关的类都放在 java.awt包以及它的子包中,AWT编程中有两个基类,Component和MenuComponent。下图显示了AWT图形组件之间的继承关系。


在java.awt包中提供了两种基类表示图形界面元素:Component和MenuComponent,其中Component代表一个能以图形化方式显示出来,并可与用户交互的对象,例如Button代表一个按钮,TextField代表一个文本框等;而MenuComponent则代表图形界面的菜单组件,包括MenuBar(菜单条)、MenuItem(菜单项)等子类。
除此之外,AWT图形用户界面编程里还有两个重要的概念:Container和LayoutManager,其中Container是一种特殊的Component,它代表一种容器,可以盛装普通的Component;而LayoutManager则是容器管理其他组件布局的方式。

AWT容器

如果从程序员的角度来看一个窗口时,这个窗口不是一个整体(有点庖丁解牛的感觉),而是由多个部分组合而成。

从上图中可以看出,任何窗口都可被分解成一个空的容器,容器里盛装了大量的基本组件,通过设置这些基本组件的大小、位置等属性,就可以将该空的容器和基本组件组成一个整体的窗口。实际上,图形界面编程非常简单,它非常类似于小朋友玩的拼图游戏,容器类似于拼图的“母版”,而普通组件(如Button、List之类)则类似于拼图的图块。创建图形用户界面的过程就是完成拼图的过程。
容器(Container)是Component的子类,因此容器对象本身也是一个组件,具有组件的所有特性,可以调用Component类的所有方法。Component类提供了如下几个常用方法来设置组件的大小、位置和可见性。

setLocation(int x,int y);//设置组件的位置
setSize(int width,int height);//设置组件的大小
setBounds(int x,int y,int width,int height);//同时设置组件的位置、大小
setvisible(Boolean b);//设置该组件的可见性
容器还可以盛装其他组件,容器类(Container)提供
了如下几个常用方法来访问容器里的组件
Component add(Component comp);
//向容器添加其他组件(该组件既可以是普通组件,也可以是容器),并返回被添加的组件
Component getComponentAt(int x,int y);
//返回指定点的组件
int getComponentCount();
//返回该容器内组件的数量
Componet[]getComponets();
//返回该容器内所有的组件。

AWT主要提供了如下两个主要的容器类型。

  • Window:可独立存在的顶级窗口
  • Panel:可作为容器容纳其他组件,但不能独立存在,必须被添加到其他容器中(如Window、Panel或者Applet等)。
    AWT容器的继承关系图如下所示。

    图中显示了AWT容器之间的继承层次,其中以粗黑圈出的容器是AWT编程中常用的组件。Frame代表最常见的窗口,它是Window类的子类,具有如下几个特点。
  • Frame对象由标题,允许通过拖拉来改变窗口的位置、大小。
  • 初始化时为不可见,可用setVisible(true)使其显式出来
  • 默认使用BorderLayout作为布局管理器
    关于布局管理器的知识,请参考下一节的介绍。
    下面的例子程序通过Frame创建了一个窗口。

    运行上面程序会看到这样的简单窗口。
    从上图窗口可以看出,该窗口是Windows10窗口的风格,这也证明了AWT确实是调用程序运行平台的本地API创建了该窗口,如果点击所示窗口右上角的"X"按钮,该窗口不会关闭,这是因为还未添加该窗口编写任何事件相应。如果向关闭该窗口,可以通过软件(我的是IDEA)的程序结束按钮或任务管理器来关闭该窗口。


    点击这个红色按钮代表结束程序的运行

    变黑代表程序结束
    正如前面介绍的,创建图形用户界面的过程类似于拼图游戏,拼图游戏中的母版、图块都需要购买,而Java程序中的母版(容器)、图块(普通组件)则无须购买,直接采用 new 关键字创建一个对象即可。
    Panel是AWT中另一个典型的容器,它代表不能独立存在、必须放在其他容器中的容器。Panel外在表现为一个矩形区域,该区域内可盛装其他组件。Panel容器存在的意义在于为其他组件提供空间,Panel容器具有如下几个特点。
  • 可作为容器来盛装其他组件,为放置组件提供空间。
  • 不能单独存在,必须放置到其他容器中。
  • 默认使用FlowLayout作为其布局管理器。
    下面的例子程序使用Panel作为容器来盛装一个文本框和一个按钮,并将该Panel对象添加到Frame对象中。
import java.awt.*;public class Demo{public static void main(String[] args) {var f = new Frame("测试窗口");//创建一个Panel容器var p = new Panel();//向Panel容器中添加两个组件p.add(new TextField(20));p.add(new Button("单击我"));//将Panel容器添加到Frame窗口中f.add(p);//设置窗口藕断大小、位置f.setBounds(30,30,250,120);//将窗口显示出来(Frame对象默认处于隐藏状态)f.setVisible(true);}
}


这里可能你的按钮文字是三个正方体,这个问题的解决方案之一是:




原选项可能是UTF-8,改为GBK后再运行程序就会正常显示出来,这个方法只是之一不代表所有问题都可以解决,总之JButton就没有这样的问题。
编译运行上面程序,会看到如上的运行窗口。
从上图可以看出,使用AWT创建窗口很简单,程序只需要通过Frame创建,然后再创建一些AWT组件,把这些组件添加到Frame创建的窗口中即可。
ScrollPane是一个带滚动条的容器,它也不能独立存在,必须被添加到其他容器中。ScroolPane容器具有如下几个特点。

  • 可作为容器来盛装其他组件,当组件占用空间过大时,ScrollPane自动产生滚动条。当然也可以通过指定特定的构造器参数来指定默认具有滚动条。
  • 不能独立存在,必须放置到其他容器中。
  • 默认使用BorderLayout作为其布局管理器。ScrollPane通常用于盛装其他容器,所以通常不允许改变ScrollPane的布局管理器。
    下面的例子程序使用ScrollPane容器来替代Panel容器。

    运行上面程序,会看到如上的窗口。
    图所示窗口具有水平、垂直滚动条,这符合使用ScrollPane后的效果。程序明明向ScrollPane容器中添加了一个文本框和一个按钮,但只能看到一个按钮,却看不到文本框,这是为什么呢?这是因为ScrollPane使用BorderLayout布局管理器的缘故,而BorderLayout导致了该容器只有一个组件被显示出来。下一节将详细介绍布局管理器的知识。

布局管理器

为了使生成图形用户界面具有良好的平台无关性,Java语言提供了布局管理器这个工具来管理组件在容器中的布局,而不使用直接设置组件位置和大小的方式。
例如通过如下语句定义了一个标签(Lable):

var hello = new Label("Hello Java");

为了让这个hello标签里刚好可以容纳“Hello Java”字符串,也就是实现该标签的最佳大小(既没有冗余空间,也没有内容被遮挡),Windows可能应该设置为长100像素,高20像素,但换到UNIX上则可能需要设置长120像素、高24像素。当一个应用程序从Windows移植到UNIX上时,程序需要做大量工作来调整图形界面。
对于不同的组件而言,它们都有一个最佳大小,这个最佳大小通常是平台相关的,程序在不同平台上运行时,相同内容的大小可能不一样。如果让程序员手动控制每个组件的大小、位置,这将给编程带来巨大的困难。为了解决这个问题,Java提供了LayoutManager,LayoutManager可以根据运行平台来调整组件大小,程序员要做的,只是为容器选择合适的布局管理器。
所有的AWT容器都有默认的布局管理器,如果没有为容器指定布局管理器,则该容器使用默认的布局管理器。为容器指定布局管理器通过调用容器对象的setLayout(LayoutManager lm)方法来完成。如下代码所示:

c.setLayout(new XxxLayout);

AWT提供了FlowLayout、BorderLayout、GirdLayout、GridBagLayout、CardLayout5个常用布局管理器,Swing还提供了一个BoxLayout布局管理器。下面将详细介绍这几个布局管理器。

FlowLayout布局管理器

在FlowLayout布局管理器中,组件像水流一样向某方向流动(排列),遇到障碍(边界)就折回,重头开始排列。在默认情况下,FlowLayout布局管理器从左向右排列所有的组件,遇到边界就会折回下一行重新开始。
当读者在电脑上输入一篇文章时,所使用的就是FlowLayout布局管理器,所有文章字默认从左向右排列,遇到边界就会折回下一行重新开始。AWT中的FlowLayout布局管理器与此完全类似,只是此时排列的是AWT组件,而不是文字。
FlowLayout有如下三个构造器。

  • FlowLayout():使用默认的对齐方式及默认的垂直间距、水平间距创建FlowLayout布局管理器。
  • FlowLayout(int align):使用指定的对齐方式及默认的垂直间距、水平间距创建FlowLayout布局管理器。
  • FlowLayout(int align,int hgap,int vgap):使用指定的堆砌方式及指定的垂直间距、水平间距创建FlowLayout布局管理器。
    上面三个构造器的hgap、vgap代表水平间距、垂直间距,为这两个参数传入整数值即可。其中align表明FlowLayout中组件的排列方向(从左向右、从右向左,从中间向两边等),该参数应该使用FlowLayout类的静态常量:FlowLayout.LEFT、FlowLayout.CENTER、FlowLayout.RIGHT。
    Panel和Applet默认使用FlowLayout布局管理器,下面程序将一个Frame改为使用FlowLayout布局管理器。
import java.awt.*;public class Demo{public static void main(String[] args) {var f = new Frame("TestFrame");//设置Frame容器使用FlowLayout布局管理器f.setLayout(new FlowLayout(FlowLayout.LEADING,20,5));//向窗口中添加10个按钮for(var i=0;i<10;i++){f.add(new Button("Button"+i));}//设置窗口为最佳大小f.pack();//将窗口显示出来(Frame对象默认处于隐藏状态)f.setVisible(true);}
}

运行上面程序,会看到如图所示的窗口效果。
图显示了各组件左对齐、水平间距为20、垂直间距为5的分布效果。
上面程序中执行了f.pack()代码,pack()方法是Window容器提供的一个方法,该方法用于将窗口调整到最佳大小。通过Java编写图形用户界面时,很少直接设置窗口的大小,通常都是调用pack()方法来将窗口调整到最佳大小。

BorderLayout管理器

BorderLayout将容器分为EAST、SOUTH、WEST、NORTH、CENTER五个区域,普通组件可以被放置在这5个区域的任意一个中。BorderLayout布局管理器如图所示。

当改变使用BorderLayout的容器大小时,NORTH、SOUTH和CENTER区域水平调整,而EAST、WEST和CENTER区域垂直调整。使用BorderLayout有如下两个注意点。

  • 当向使用BorderLayout布局管理器的容器中添加组件时,需要指定要添加到哪个区域中。如果没有指定添加到哪个区域中,则默认添加到中间区域中。
  • 如果向一个区域添加多个组件时,后放入的组件会覆盖先放入的组件。
    第二个注意点就可以解释为什么在ScrollPaneTest.java中向ScrollPane中添加两个组件后,但运行结果只能看到最后一个按钮,因为最后添加的组件把前面添加的组件覆盖了。
    Frame、Dialog、ScrollPane默认使用BorderLayout布局管理器,BorderLayout有如下两个构造器。
  • BorderLayout():使用默认的水平间距、垂直间距创建BorderLayout布局管理器。
  • BorderLayout(int hgap,int vgap):使用指定的水平间距、垂直间距创建BorderLayout布局管理器。
    当向使用BorderLayout布局管理器的容器中添加组件时,应该使用BorderLayout类的几个静态常量来指定要添加到哪个区域中。BorderLayout有如下几个静态常量:EAST(东)、NORTH(北)、WEST(西)、SOUTH(南)、CENTER(中)。如下例子程序示范了BorderLayout的用法。


    上北 下南 左西 右东
    关于这里可能会出现一个添加问题:

    这里SOUTH并不是字符串参数,所以应该在括号外面,这里是按钮构造器的String字符串参数,这个大多是书写过少的普通错误,看下面的错误:

    很奇怪,明明我导入了 awt.*所有包而且写法也和书上一模一样,为什么就报错了呢?而且好像需要改为这样才不报错:
f.add(new Button("Nan"),BorderLayout.SOUTH);


这是为何?虽然这样的写法确实是调用该类常量没有问题,可是为什么我做不到直接像书上那样直接调用该类常量?
这里的问题很微妙,我曾经第一次看这本书的时候,比较骄傲,很多内容都喜欢跳过,到最后也是一堆没学明白,这里明明自己学习了很久的Frame窗体,但是遇到这个问题也是不懂(当初还以为是书上代码写错了还很苦恼这个代码),这就是Java基础的学习内容了!
我们都知道 import是导入包
是不是还有一个叫 import static的关键字呢?
没错
import static是导入类的所有常量等

import static java.awt.BorderLayout.*;


Java基础真的很重要,不要觉得很多小内容没有什么作用,任何内容都是基础的扩展!

import java.awt.*;
import static java.awt.BorderLayout.*;
public class Demo{public static void main(String[] args) {var f = new Frame("TestFrame");//设置Frame容器使用BorderLayout布局管理器f.setLayout(new BorderLayout(30,5));f.add(new Button("Nan"),SOUTH);f.add(new Button("Bei"),NORTH);//默认添加到中间区域中f.add(new Button("Zhong"));f.add(new Button("Dong"),EAST);f.add(new Button("Xi"),WEST);//设置窗口为最佳大小f.pack();//将窗口显示出来(Frame对象默认处于隐藏状态)f.setVisible(true);}
}

当使用BorderLayout布局管理器时,每个区域的组件都会尽力去占据整个区域,所以中间的按钮比较大。
BorderLayout最多只能放置5个组件吗?那它也太不实用了把?
BorderLayout最多只能放置5个组件,但可以放置少于5个组件,如果某个预期没有放置组件,该区域并不会出现空白,旁边区域的组件会自动占据该区域,从而保证窗口有较好的外观。虽然BorderLayout最多只能放置5个组件,但因为容器也是一个组件,所以我们可以先向Panel里添加多个组件,再把Panel添加到BorderLayout布局管理器中,从而让BorderLayout布局管理器中的实际组件数远远超出5个。下面程序可以证实这一点。

import java.awt.*;
import static java.awt.BorderLayout.*;
//import static java.awt.BorderLayout.SOUTH;
//import static java.awt.BorderLayout.EAST;
//import static java.awt.BorderLayout.WEAST;
//import static java.awt.BorderLayout.NORTH;
//这里没有必要挨个导入 直接.后面加上*星号即可
public class Demo{public static void main(String[] args) {var f = new Frame("TestFrame");//设置Frame容器使用BorderLayout布局管理器f.setLayout(new BorderLayout(30,5));f.add(new Button("Nan"),SOUTH);f.add(new Button("Bei"),NORTH);//创建一个Panel对象var p = new Panel();//向Panel对象中添加两个组件p.add(new TextField(20));p.add(new Button("onclick"));//onclick是JavaScprit里很常用的单击事件的函数关键字之一//这里虽然通过更换编码可以解决AWT组件的乱码 但是再次运行也可能会变成乱码 所以这里推荐姑且先使用英文命名 到了Swing就没有这些烦恼了//默认添加到中间区域中,向中间区域添加一个Panel容器f.add(p);f.add(new Button("Don"),EAST);//设置窗口为最佳大小f.pack();//将窗口显示出来(Frame对象默认处于隐藏状态)f.setVisible(true);}
}


上面程序没有向WEST区域添加组件,但向CENTER区域添加了一个Panel容器,该Panel容器中包含了一个文本框和一个按钮。运行上面程序,会看到如图所示的窗口界面。
虽然程序没有向WEST区域添加组件,但是窗口中依然有五个组件,因为CENTER区域添加的是Panel,而该Panel里包含了2个组件,所以会看到此界面的效果。

GridLayout布局管理器

GridLayout布局管理器时将容器分割成纵横线分隔的网格,每个网格所占的区域大小相同。当向使用GridLayout布局管理器的容器中添加组件时,默认从左向右、从上向下依次添加到每个网格中。与FlowLayout不同的是,放置在GridLayout布局管理器中的各组件的大小由组件所处的区域来决定(每个组件将自动沾满整个区域)。
GridLayout有如下两个构造器。

  • GridLayout(int rows,int cols):采用指定的行数、列数,以及默认的横向间距、纵向间距将容器分割成多个网格。
  • GridLayout(int rows,int cols,int hgap,int vgap):采用指定的行数、列数,以及指定的横向间距、纵向间距将容器分割成多个网格。
    如下程序结合BorderLayout和GirdLayout开发了一个计算器可视化窗口。
import java.awt.*;
import static java.awt.BorderLayout.*;public class Demo{public static void main(String[] args) {var f = new Frame("Calculator");//计算器的英文直译var p1 = new Panel();p1.add(new TextField(30));//计算器的输入框f.add(p1,NORTH);//记得静态导入一下该类所有的常量Panel p2 = new Panel();//设置Panel使用GridLayout布局管理器p2.setLayout(new GridLayout(3,5,4,4));//三行五列、横向间距4和纵向间距4String[]name = {"0","1","2","3","4","5","6","7","8","9","+","-","*","/","."};//向Panel中依次添加15个按钮for(var i=0;i<name.length;i++){p2.add(new Button(name[i]));//循环遍历添加数组中的内容添加到按钮中}//默认将Panel对象添加到Frame窗口的中间f.add(p2);//设置窗口的最佳大小f.pack();//将窗口显示出来(Frame对象处于默认隐藏状态)f.setVisible(true);}
}


上面程序的Fra,e采用默认的BorderLayout布局管理器时,程序向BorderLayout中添加了两个组件:NORTH区域添加了一个文本框,CENTER区域添加了一个Panel容器,该容器采用GirdLayout布局管理器时,Panel容器中添加了15个按钮。运行上面程序,会看到如上所示的运行窗口。
图中所示的效果是结合两种管理器的例子:Frame使用BorderLayout布局管理器,CENTER区域的Panel使用GridLayout布局管理器。实际上,大部分应用窗口都不能使用一个布局管理器时直接做出来,必须采用这种嵌套的方式。

GridBagLayout布局管理器

GridBagLayout布局管理器时的功能最强大但也最复杂,与GridLayout布局管理器不同的是,在GridBagLayout布局管理器时中,一个组件可以跨越或多个网格,并可以设置网格的大小互不相同,从而增加了布局的灵活性。当窗口的大小发生变化时,GridBagLayout布局管理器也可以准确地控制窗口各部分的拉伸。
为了处理GridBagLayout中GUI组件的大小、跨越性,Java提供了GridBagConstraints对象,该对象与特定的GUI组件关联,用于控制该GUI组件的大小、跨越性。
使用GridBagLayout布局管理器的步骤如下:

var gb = new GridBagLyout();
container.setLayout(gb);

创建GridConstraints对象,并设置该对象的相关属性(用于设置受该对象控制的GUI组件的代大小、跨越性等)。

gbc.gride=2;//设置受该对象控制的GUI组件位于网格的横向索引
gbc.gridy=1;//设置受该对象控制的GUI组件位于网格的纵向索引
gbc.gridwidth=2;//设置受该对象控制的GUI组件横向跨越多少网格
gbc.gridheight=1;//设置受该对象控制的GUI组件纵向跨越多少网格

调用GridBagLayout对象的方法来建立GridBagConstraints对象和受控制组件之间的关联。

gb.setConstraints(c,gbc);//设置c组件受gbc对象控制

添加组件,与采用普通布局管理器添加组件的方法完全一样。

container.add(c);

如果需要向一个容器中添加多个GUI组件,则需要多次重复2~4。由于GridBagConstraints对象可以多次重用,所以实际上只需要创建一个GridBagConstraints对象,每次添加GUI组件之前先改变GridBagConstraints对象的属性即可。
从上面介绍可以看出,使用GridBagLayout布局管理器的关键在于GridBagConstraints,它才是精确控制每个GUI组件的核心类,该类具有如下几个属性。

  • gridx、gridy:受设置该对象控制的GUI组件左上角所在网格的横向索引、纵向索引(GridBagLayout左上角的网格的索引为0、0)。这两个值还可以是GridBagConstraints.RELATIVE(默认值),它表明当前组件紧跟在上一个组件之后。
  • gridwidth、gridheight:设置受该对象控制的GUI组件横向、纵向跨越多个网格,两个属性值的默认值都是1。如果设置这两个属性值为GridBagConstraints.REMAINDER,这表明受该对象控制的GUI组件是横向的、纵向倒数第二个组件。
  • fill:设置受该对象控制的GUI组件如何占据空白区域。该属性的取值如下。
    -GridBagConstraints.NONE:GUI组件不扩大
    GridBagConstraints.HORIZONTAL:GUI组件水平扩大以占据空白区域。
  • GridBagConstraints.VERTICAL:GUI组件垂直扩大以占据空白区域。
  • GridBagConstraints.BOTH:GUI组件的水平、垂直同时扩大以占据空白区域。
  • ipadx、ipady:受该对象控制的GUI组件横向、纵向内部填充的大小,即在该组件最小尺寸的继承上还需要增加多少。如果设置了这两个属性,则组件横向大小最小宽度再加ipadx2像素,纵向大小为最小高度再加ipady2像素。
  • insets:设置受该对象控制的GUI组件的外部填充的大小,即该组件边界和显示区域边界之间的距离。
  • anchor:设置受该对象控制的GUI组件在其显式区域中的定位方式。定位方式如下。
  • GridBagConstraints.CENTER(中间)
  • GridBagConstraints.NORTH(上中)
  • GridBagConstraints.NORTHWEST(左上角)
  • GridBagConstraints.NORTHEAST(右上角)
  • GridBagConstraints.SOUTH(下中)
  • GridBagConstraints.SOUTHWEST(右下角)
  • GridBagConstraints.SOUTHWEST(左下角)
  • GridBagConstraints.EAST(右中)
  • GridBagConstraints.(左中)
  • Weightx、weighty:设置受该对象控制的GUI组件占据多余空间的水平、垂直增加比例(也叫权重,即weight的直译),这两个属性的默认值是0,即该组件不占据多余空间。假设某个容器的水平线上包括三个GUI组件,它们的水平增加比例分别是1、2、3,但容器宽度增加60个像素时,第一个组件宽度为10像素,第二个组件宽度为20像素,第三个组件宽度为30像素。如果其增加比例为0,则表示不会增加。
    如果希望某个组件的大小随容器的增大而增大,则必须同时设置控制该组件的GridBagConstraints对象的fill属性和weightx、Weighty属性。
    下面程序示范了如何使用GridBagLayout布局管理器时来管理窗口中的10个按钮。
import java.awt.*;public class Demo{private Frame f = new Frame("TestFrame");private GridBagLayout gb = new GridBagLayout();private GridBagConstraints gbc = new GridBagConstraints();private Button[] bs = new Button[10];public void init(){f.setLayout(gb);for(var i=0;i<bs.length;i++){bs[i] = new Button("Button"+i);}//所有组件都可以在横向、纵向上扩大gbc.fill=GridBagConstraints.BOTH;gbc.weightx=1;addButton(bs[0]);addButton(bs[1]);addButton(bs[2]);//该GridBagConstraints控制的GUI组件将会成为横向最后一个组件gbc.gridwidth=GridBagConstraints.REMAINDER;addButton(bs[3]);//该GridBagConstraints控制的GUI组件将在横向上不会扩大gbc.weightx=0;addButton(bs[4]);//该GridBagConstraints控制的GUI组件将横跨两个网格gbc.gridwidth=2;addButton(bs[5]);//该GridBagConstraints控制的GUI组件将横跨一个网格gbc.gridwidth=1;//该GridBagConstraints控制的GUI组件将在纵向上跨两个网格gbc.gridheight=2;//该GridBagConstraints控制的GUI组件将会称为横向最后一个组件gbc.gridwidth=GridBagConstraints.REMAINDER;addButton(bs[6]);//该GridBagConstraints控制的GUI组件将横向跨越一个网格,纵向跨越两个网格gbc.gridwidth=1;gbc.gridheight=2;//该GridBagConstraints控制的GUI组件纵向扩大的权重是1gbc.weighty=0;addButton(bs[7]);//设置下面的按钮在纵向上不会扩大gbc.weighty=0;//该GridBagConstraints控制的GUI组件将会成为横向最后一个组件gbc.gridwidth=GridBagConstraints.REMAINDER;//该GridBagConstraints控制的GUI组件将在纵向上横跨一个网格gbc.gridheight=1;addButton(bs[8]);addButton(bs[9]);f.pack();f.setVisible(true);}private void addButton(Button button){gb.setConstraints(button,gbc);f.add(button);}public static void main(String[] args) {new Demo().init();}
}

从图中可以看出,虽然设置了按钮4、按钮5横向上不会扩大,但因为按钮4、按钮5的宽度会受上一行4个按钮的影响,所以它们实际上依然会变大;同理,虽然设置了按钮8、按钮9纵向不会扩大,但因为受按钮7的影响,所以按钮9纵向依然会变大(但按钮8不会变高)。
上面程序把需要重复访问的AWT组件设置成成员变量,然后使用init()方法来完成界面的初始化工作,这种做法比前面那种在main方法里把AWT组件定义成局部变量的方式更好。

CarLayout布局管理器

CardLayout布局管理器以时间而非空间来管理它里面的组件,它将加入容器的所有组件看成一叠卡片,每次只有最上面的那个Component才可见。就好像一副扑克牌,它们叠在一起,每次只有最上面的一张扑克牌才可见。CardLayout提供了如下两个构造器。

  • CardLayout():创建默认的CardLayout布局管理器。
  • CardLayout(int hgap,int vgap):通过指定卡片与容器左右边界的间距(hgap)、上下边界(vgap)的间距来创建CardLayout布局管理器。
    CardLayout用于控制组件可见的5个常用方法如下。
  • first(Container target):显示target容器中的第一种卡片。
  • last(Container target):显示target容器中的最后一张卡片。
  • previous(Container target):显示target容器中的前一张卡片。
  • next(Container target):显示target容器中的后一张卡片。
  • show(Container target,String name):显示target容器中指定名字的卡片。
    下面程序示范了CardLayout布局管理器时的用法。
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionListener;public class Demo{Frame f = new Frame("TestFrame");String[]names={"one","tow","three","four","five"};Panel p1 = new Panel();public void init(){final var c = new CardLayout();p1.setLayout(c);for(var i=0;i<names.length;i++){p1.add(names[i],new Button(names[i]));}var p = new Panel();ActionListener listener = e->{switch (e.getActionCommand()){case "Previous"://上一张c.previous(p1);break;case "Next"://下一张c.next(p1);break;case "First one"://第一张c.first(p1);break;case "Last one"://最后一张c.last(p1);break;case "Third sheet"://第三张c.show(p1,"three");break;}};//控制显示上一张的按钮var previous = new Button("Previous");previous.addActionListener(listener);//控制显示下一张的按钮var next = new Button("Next");next.addActionListener(listener);//控制显示第一张的按钮var first = new Button("First one");first.addActionListener(listener);//控制显示最后一张的按钮var last = new Button("Last one");last.addActionListener(listener);//根据Card名显示的按钮var third = new Button("Third sheet");third.addActionListener(listener);p.add(previous);p.add(next);p.add(first);p.add(last);p.add(third);f.add(p1);f.add(p, BorderLayout.SOUTH);f.pack();f.setVisible(true);}public static void main(String[] args) {new Demo().init();}
}


上面程序通过Frame创建了一个窗口,该窗口被分为上下两个部分,其中上面的Panel使用CardLayout布局管理器,该Panel中放置了5张卡片,每张打卡里放一个按钮;下面的Panel使用FlowLayout布局管理器,依次放置3个按钮,用于控制上面Panel中卡片的显示。运行上面程序,会看到如上的运行窗口。
单机5个按钮,将可以看到上面Panel中的5张卡片发生改变。
上面程序使用了AWT的事件编程,关于事件编程看文章接下来的内容。

绝对定位

很多曾经学过VB、Delphi的读者可能比较怀念哪种随意拖动控件的感觉,对Java的布局管理器非常不习惯,实际上,Java也提供了那种拖动式控件的方式,即Java也对GUI组件进行了绝对定位。在Java容器中采用绝对定位的步骤如下。

  1. 将Container的布局管理器时设成null:setLayout(null)。
  2. 向容器中添加组件时,先调用setBounds()或者Size()方法来设置组件的大小、位置,或者直接创建GUI组件时通过构造参数指定该组件的大小、位置,然后将该组件添加到容器中。
    下面程序示范了如何使用绝对定位来控制窗口中的GUI组件。

import java.awt.*;public class Demo{Frame f = new Frame("TestFrame");Button b1 = new Button("one Button");Button b2 = new Button("tow Button");public void init(){//设置使用null布局管理器f.setLayout(null);//下面强制设置每个按钮的大小、位置b1.setBounds(20,30,90,28);f.add(b1);b2.setBounds(50,45,120,35);f.add(b2);f.setBounds(50,50,200,100);f.setVisible(true);}public static void main(String[] args) {new Demo().init();}
}

可以看出,使用绝对布局定位时甚至可以使两个按钮重叠,可见使用绝对定位确实非常灵活,而且很简捷,但这种方式是以丧失跨平台性作为代码的。
采用绝对定位绝不是最好的方法,它可能导致该GUI界面失去跨平台特性。

BoxLayout布局管理器

GridBagLayout布局管理器虽然功能强大,但它实在太复杂了,所以Swing引入了一个新的布局管理器:BoxLayout,它保留了GridBagLayout的很多优点,但是却没那么复杂。BoxLayout可以在垂直和水平两个方向上摆放GUI,BoxLayout提供了如下一个简单的构造器。

  • BoxLayout(Container target,int axis):指定创建基于target容器的BoxLayout布局管理器,该布局管理器里的组件按axis方向排列。其中axis有BoxLayout.X_AXIS(横向)和BoxLayout.Y_AXIS(纵向)两个方向。
import javax.swing.*;
import java.awt.*;public class Demo{private Frame f = new Frame("Test");public void init(){f.setLayout(new BoxLayout(f,BoxLayout.Y_AXIS));//下面按钮会垂直排列f.add(new Button("one Button"));f.add(new Button("tow Button"));f.pack();f.setVisible(true);}public static void main(String[] args) {new Demo().init();}
}

BoxLayout通常和Box容器结合使用,Box是一个特殊的容器,它有点像Panel容器,但该容器默认使用BoxLayout布局管理器。Box提供了如下两个静态方法来创建Box对象。

  • createHorizeontalBox():创建一个水平排列组件的Box容器。
  • createVerticalBox():创建一个垂直排列组件的Box容器。
    一旦获得了Box容器之后,就可以使用Box来盛装普通的GUI组件,然后将这些Box组件添加到其他容器中,从而形成整体的窗口布局。下面的例子程序示范了如何使用Box容器。
import javax.swing.*;
import java.awt.*;public class Demo{private Frame f = new Frame("Test");//定义水平拜访组件的Box对象private Box horizontal = Box.createHorizontalBox();//定义垂直摆放组件的Box对象private Box vertical = Box.createVerticalBox();public void init(){horizontal.add(new Button("horizontalButton1"));horizontal.add(new Button("horizontalButton2"));vertical.add(new Button("vertical1"));vertical.add(new Button("vertical2"));f.add(horizontal,BorderLayout.NORTH);f.add(vertical);f.pack();f.setVisible(true);}public static void main(String[] args) {new Demo().init();}
}

上面程序创建了一个水平摆放组件的Box容器和一个垂直摆放组件的Box容器,并将这两个Box容器添加到Frame窗口中。运行该程序会看到如上运行窗口。
如果两个程序显示的所有按钮都紧挨在一起,如果希望像FlowLayout、GridLayout等布局管理器那样指定组件的间距应该怎么办?
BoxLayout没有提供设置间距的构造器和方法,因为BoxLayout采用另一种方式来控制对组件的间距——BoxLayout使用Glue(橡胶)、Strut(支架)、和RigidArea(刚性区域)的组件来控制组件间的距离。其中Glue代表可以在横向、纵向两个方向上同时拉伸空白组件(间距),Strut代表可以在横向、纵向任意一个方向上拉伸空白的组件(间距),RigidArea代表不可拉伸的空白组件(间距)。
Box提供了如下5个静态方法来创建Glue、Strut和RigidArea。

  • createHorizontalGlue():创建一条水平Glue(可在两个方向上同时拉伸的间距)。
  • cteateVerticalGlue():创建一条垂直Glue(可在两个方向上同时拉伸的间距)。
  • createHorizontalStrut(int width):创建一条指定宽度的水平Strut(可在垂直方向上拉伸的间距)。
  • crateVerticalStrut(int height):创建一条指定高度的垂直Strur(可在水平上拉伸的间距)。
  • crateRigidArea(Dimesion d):创建指定宽度、高度的RigidArea(不可拉伸的间距)。
    不管Glue、Strut、RigidArea的翻译多么奇怪,这些名称多么古怪,但读者没有必要去纠结它们的名称,只要知道它们就是代表组件之间的几种间距即可。
    上面5个方法都返回Component对象(代表间距),程序可以将这些分隔Component添加两个普通的GUI之间,用以控制组件的间距。下面程序使用上面三种间距来分隔Box中的按钮。
import javax.swing.*;
import java.awt.*;public class Demo{private Frame f = new Frame("Test");//定义水平摆放组件的Box对象private Box horizontal = Box.createHorizontalBox();//定义垂直摆放组件的Box对象private Box vertical = Box.createVerticalBox();public void init(){horizontal.add(new Button("horizeontal1"));horizontal.add(Box.createHorizontalGlue());horizontal.add(new Button("horizeontal2"));//水平方向不可拉伸的间距,其宽度为10pxhorizontal.add(Box.createHorizontalStrut(10));horizontal.add(new Button("horizeontal3"));vertical.add(new Button("vertical1"));vertical.add(Box.createVerticalGlue());vertical.add(new Button("vertical2"));//垂直方向不可拉伸的间距,其高度为10pxvertical.add(Box.createHorizontalStrut(10));vertical.add(new Button("vertical3"));f.add(horizontal,BorderLayout.NORTH);f.add(vertical);f.pack();f.setVisible(true);}public static void main(String[] args) {new Demo().init();}
}

从图中可以看出,Glue可以在两个方向上同时拉伸,但Strut只能在一个方向上拉伸,RigidArea则不可拉伸。
因为BoxLayout是Swing提供的布局管理器,所以用于管理Swing组件将会有更好的表现。

AWT常用组件

AWT组件需要调用运行平台的图形界面来创建和平台一致的对等体,因此AWT只能使用所有平台都支持的公共组件,所以AWT只提供了一些常用的GUI组件。

基本组件

AWT提供了如下基本组件

  • Button:按钮,可接受单击操作。
  • Canvas:用于绘画的画布。
  • Checkbox:复选框组件(也可变单选框组件)。
  • CheckboxGroup:用于将多个Checkbox组件组合成一组,一组Checkbox组件将只有一个可以被选中,即全部变成单选框组件。
  • Choice:下拉式单选框组件。
  • Frame:窗口,在GUI程序里通过该类创建窗口。
  • Label:标签类,用于放置提示性文本。
  • List:列表框组件,可以添加多个项目
  • Panel:不能单独存在的基本容器类,必须放到其他容器中。
  • Scrollbar:滑动条组件。如果需要用户输入位于某个范围的值,就可以使用滑动条组件,比如调色板中设置RGB的三个值所用的滑动条。当创建一个滑动条时,必须指定它的方向、初始值、滑块的大小、最小值和最大值。
  • ScrollPane:带水平及垂直滚动条的容器组件。
  • TextArea:多行文本域。
  • TextField:单行文本框。
    这些AWT组件的用法比较简单,读者可以查阅API文档来获取它们各自的构造器、方法等详细信息。下面的例子程序示范了它们的基本用法。
import javax.swing.*;
import java.awt.*;public class Demo{Frame f = new Frame("Test");//定义一个按钮Button ok = new Button("Ok");CheckboxGroup cbg = new CheckboxGroup();//定义一个单选框(处于cbg一组),初始化处于被选中状态Checkbox male = new Checkbox("boy",cbg,true);//定义一个单选框(处于cbg一组),初始化处于没有选中状态Checkbox female = new Checkbox("gril",cbg,false);//定义一个复选框,初始处于没有选中状态Checkbox married = new Checkbox("married",false);//是否结婚?//定义一个下拉选择框Choice colorChooser = new Choice();//定义一个列表选择框List colorList = new List(6,true);//定义一个5行、20列的多行文本域TextArea ta = new TextArea(5,20);//定义一个50列的单行文本域TextField name = new TextField(50);public void init(){colorChooser.add("red");colorChooser.add("green");colorChooser.add("blue");colorList.add("red");colorList.add("green");colorList.add("blue");//创建一个装载了文本框、按钮的Panelvar bottom = new Panel();bottom.add(name);bottom.add(ok);f.add(bottom,BorderLayout.SOUTH);//创建一个装载了下拉选择框、三个Checkbox的Panelvar checkPanel1 = new Panel();checkPanel1.add(colorChooser);checkPanel1.add(male);checkPanel1.add(female);checkPanel1.add(married);//创建一个垂直排列组件的Box,盛装多行文本域、Panelvar topLeft = Box.createVerticalBox();topLeft.add(ta);topLeft.add(checkPanel1);//创建一个水平排列组件的Box,盛装topLeft、colorListvar top  = Box.createHorizontalBox();top.add(topLeft);top.add(colorList);//将top Box容器添加到窗口的中间f.add(top);f.pack();f.setVisible(true);}public static void main(String[] args) {new Demo().init();}
}

关于AWT常用组件的用法,以及布局管理器的用法,读者可以参考API文档来逐渐熟悉它们。一旦掌握了它们的用法之后,就可以借助与IDEA工具来设计GUI界面,使用IDE工具可以更快地设计出更美观的GUI界面。

对话框(Dialog)

Dialog是Window类的子类,是一个容器类,属于特殊组件。对话框是可以独立存在的顶级窗口,因此用法与普通窗口的用法几乎完全一样。但对话框有如下两点需要注意。

  • 对话框通常依睐于其他窗口,就是通常有一个parent窗口。
  • 对话框有非模式(non-modal)和模式(modal)两种,当某个模式对话框被打开之后,该模式对话框总是位于它依睐的窗口之上;在模式对话框被关闭之前,它依睐的窗口无法获得焦点。
    对话框有多个重载的构造器。它的构造器可能有如下三个参数。
  • owner:指定该对话框所依睐的窗口,既可以是窗口,也可以是对话框。
  • title:指定该对话框的窗口标题。
  • modal:指定该对话框是否是模式的,可以是true或false。
    下面的例子程序示范了模式对话框和非模式对话框的用法。

    这里的模式对话框打开后,阻塞了后面的父窗体(可以这么称呼),使得后面的窗体无法被选中和拥有焦点;

    这里打开了非模式对话框,发现后面的父窗体并没有被阻塞可以正常使用和选中。
import java.awt.*;public class Demo{//由于AWT的鸡肋编码问题导致只能用英文名词来命名万物 很苦哔(被迫)Frame f = new Frame("Test Dialog");Dialog d1 = new Dialog(f,"pattern Dialog",true);//模式对话框Dialog d2 = new Dialog(f,"Non mode",false);//非模式对话框Button b1 = new Button("open pattern Dialog");//打开模式对话框Button b2 = new Button("open Non mode");//打开非模式对话框public void init(){d1.setBounds(20,30,300,400);d2.setBounds(20,30,300,400);b1.addActionListener(e->d1.setVisible(true));b2.addActionListener(e->d2.setVisible(true));f.add(b1);f.add(b2,BorderLayout.SOUTH);f.pack();f.setVisible(true);}public static void main(String[] args) {new Demo().init();}
}

上面程序创建了d1和d2两个对话框,其中d1是一个模式对话框,而d2是一个非模式对话框(两个对话框都是空的)。该窗口还提供了两个按钮,分别用于打开模式对话框和非模式对话框。打开模式对话框后鼠标无法激活原来的“测试窗口”;但打开非模式对话框后还可以激活原来的“测试窗口”。
上面程序使用了AWT的事件处理来打开对话框,关于事件处理参考文章下一段内容。
不管是模式对话框还是非模式对话框,打开后都无法关闭它们,因为程序没有为这两个对话框编写事件监听。还有,如果主程序需要对话框里接收的输入值,则应该把该对话框设置成模式对话框,因为模式对话框会阻塞该程序;如果把对话框设置成非模式对话框,则可能造成对话框被打开了,但用户并没有操作该对话框,也没有向对话框里输入,这就会引起主程序的异常。
Dialog类还有一个子类:FileDialog,它代表一个文件对话框,用于打开或保存文件。FileDialog也提供了几个构造器,可分别支持parent、title和mode三个构造参数,其中parent、title指定文件对话框的所属父类窗口和标题;而mode指定该窗口用于打开文件或保存文件,该参数支持如下两个参数值:FileDialog.LOAD、FileDialog.SAVE。
FileDialog不能指定是模式对话框或非模式对话框,因为FileDialog依睐于运行平台的实现,如果运行平台的文件对话框是模式的,那么FileDialog也是模式的;否则就是非模式的。
FileDialog提供了两个方法来获取被打开/保存文件的路径。

  • getDirectory():获取FileDialog被打开/保存文件的绝对路径。
  • getFile():获取FileDialog被打开/保存文件的文件名
    下面程序分别示范了使用FileDialog来创建打开/保存文件的对话框。


    运行上面的程序,单机主窗口中的“打开文件按钮”,将看到如上的文件对话框窗口。
    可以看出这个文件对话框本身就是Windows(即Java程序所在的运行平台)提供的文件对话框,所以当单机其中图标、元素时,该对话框都能提供相应的动作。当选中某个文件后,单击“打开”按钮,将看到程序控制台打印出该文件的绝对路径(文件路径+文件名),这就是由FileDialog的getDirectory()和getFile()方法提供的。

事件处理

前面介绍了如何放置各种组件,从而得到了丰富多彩的图形界面,但这些界面还不能相应用户的任何操作。比如单击前面所有窗口右上角的“x”按钮,但窗口依然不会关闭。因为在AWT编程中,所有时间必须由特定对象(事件监听器)来处理,而Frame和组件本身没有事件处理能力。

Java事件模型的流程

为了使图形界面能够接收用户的操作,必须给各个组件加上事件处理机制。
在事件处理的过程中,主要涉及三类对象。

  • Event Source(事件源):事件发生的场所,通常就是各个组件,例如按钮、窗口、菜单等。
  • Event(事件):事件封装了GUI组件上发生的特定事件(通常就是一次用户操作)。如果程序需要获得GUI组件上发生事件的相关信息,都通过Event对象来取得。
  • Event Listener(时间监听器):负责监听事件源所发生的事件,并对各种事件做出响应处理。
    有过JavaScript、VB等编程经验的读者都知道,事件响应的动作实际上就是一系列程序语句,通常以方法的形式组织起来。但Java是面向对象的编程语言,方法不能独立存在,因此必须以类的形式来组织这些方法,所以事件监听器的核心就是它所包含的方法——这些方法也被称为事件处理器(Event Handler)。当事件源上的事情发生时,事件对象会作为参数传给事件处理器(即事件监听器的实例方法)。
    当用户单击一个按钮,或者单击某个菜单项,或者单击窗口右上角的状态按钮时,这些动作就会触发一个相应的事件,该事件由AWT封装成相应的Event对象,该事件会触发事件源上注册的事件监听器(特殊的Java对象),事件监听器调用对于的事件处理器(事件监听器里的实例方法)来做出相对应的响应。
    AWT的事件机制是一种委派式(Delegation)事件处理方式——普通组件(事件源)将事件的处理工作委托给特定对象(事件监听器);当该事件源发生指定的事件时,就通知委托的时间监听器,由事件监听器来处理这个事件。
    每个组件均可以针对特定的事件指定一个或多个事件监听对象,每个事件监听器也可以监听一个或多个事件源。因为同一个事件源上可能发生多种事件,委派式事件处理方式可以把事件源上可能发生的不同的事件分别授权给不同的事件监听器来处理;同时也可以让一类事件都使用同一个事件监听器来处理。
    委派式事件处理方式明显“抄袭”了人类社会的分工协作,例如某个单位发生了火灾,该单位通常自己不会处理该事件,而是将该事件委派给消防局(事件监听器)处理;如果发生了打架群殴的事件,则委派给公安局(事件监听器)处理;而消防局、公安局也会同时监听多个单位的火灾、打架斗殴事件。这种委托式处理方式将事件源和事件监听器分离,从而提供更好的程序模型,有利于提供程序的可维护性。
    下图显示了AWT的事件处理流程示意图。

    下面以一个简单的HelloWorld程序来示范AWT事件处理。




    这里输入中文乱码就很讨厌。
    上面程序中代码用于注册事件监听器,ok.addActionListener(new OKListener());//①
    ③号代码定义的方法就是事件处理器。当程序中的OK按钮被单击时,该处理器被触发,将看到程序中tf文本框内变为“Hello World”,而程序控制台会打印出 用户单击了OK按钮意思的字符串。
    从上面程序中可以看出,实现AWT事件处理机制的步骤如下。
  1. 实现事件监听器类,该监听器类是一个特殊的Java类,必须实现一个XxxListener接口。
  2. 创建普通组件(事件源),创建事件监听器对象。
  3. 调用addXxxListener()方法将事件监听器对象注册给普通组件(事件源)。当事件源上发生指定事件时,AWT会触发事件监听器,由事件监听器调用相应的方法(事件处理器i)来处理事件,事件源上所发生的时也会作为参数传入事件处理器。

事件和事件监听器

从图中可以看出,当外部动作在AWT组件上进行操作时,系统会自动生成事件对象,这个事件对象是EventObject子类的实例,该事件对象会触发注册到事件源上的事件监听器。
AWT事件机制涉及三个成员:事件源、事件和事件监听器,其中事件源最容易创建,只要通过new来创建一个AWT组件,该组件就是事件源,事件是由系统自动产生的,无须程序员关心,所以,实现事件监听器是整个事件处理的核心。
事件监听器必须实现事件监听器接口,AWT提供了大量的时间监听器接口。用于监听不同类型的事件。AWT中提供了丰富的事件类,用于封装不同组件上所发生特定操作——AWT的时间类都是AWTEvent类的子类,AWTEvent是EventObject的子类。
EventObject类代表更广义的事件爱你对象,包括Swing组件上所触发的事件、数据库连接所触发的事件等。
AWT事件分为两大类:低级事件和高级事件。

  1. 低级事件
    低级事件是指基于特定动作的事件。比如进入、点击、拖放等鼠标事件,当组件得到焦点、失去焦点时触发焦点事件。
  • componentEvent:组件事件,当组件尺寸发生变化、位置发生移动、显示/隐藏状态发生改变时触发该事件。
  • ContainerEvent:容器事件,当容器里发生添加组件、删除组件时触发该事件。
  • WindowEvent:窗口事件,当窗口状态发生改变(如打开、关闭、最大化、最小化)时触发该事件。
  • FocusEvent:焦点事件,当组件得到焦点或失去焦点时触发该事件。
  • KeyEvent:键盘事件,当按键被按下、松开、单击时触发该事件。
  • MouseEvent:鼠标事件,当进行单击、按下、松开、移动鼠标等动作时触发该事件。
  • PaintEvent:组件绘制事件,该事件是一个特殊的事件类型,当GUI组件调用update/paint方法来呈现自身时触发该事件,该事件并非用于事件处理模型。
  1. 高级事件(语义事件)
    高级事件是基于语义的事件,它可以不和特定的动作相关联,而依睐于触发此事件的类。比如,在TextField中按Enter键会触发ActionEvent事件,在滑动条上移动滑块会触发AdjustmentEvent事件,选中项目列表的某一项就会触发ItemEvent事件。
  • ActionEvent:动作事件,当按钮、菜单项被单击,在TextField中按Enter键时触发该事件。
  • AdjustmentEvent:调节事件,在滑动条上移动滑块以调节数值时触发该事件。
  • ItemEvent:选项事件,当用户选中某些,或取消选中某项时触发该事件。
  • TextEvent:文本事件,当文本框、文本域里的文本发生改变时触发该事件。
    AWT事件继承层次图如下。

    上图中常用的AWT事件是黑色的矩形,对于不是黑色矩形的事件而言,程序员很少使用它们,它们可能被作为事件基类或作为系统内部实现来使用。
    不同的事件需要使用不同的监听器监听,不同的监听器需要实现不同的监听器接口,当指定事件发生后,事件监听器就会调用所包含的事件处理器(实例方法)来处理事件。下表显示了事件、监听器接口和处理器之间的对应关系。


    通过上表可以大致知道常用组件可能发生哪些事件,以及该事件对应的监听器接口,通过实现该监听器接口就可以实现对应的事件爱你处理器,然后通过addXxxListener()方法将事件监听器注册给指定的组件(事件源)。当事件源组件上发生特定时间时,被注册到该组件的事件监听器里的对应方法(事件处理器)将被触发。
    ActionListener、AdjustmentListener等事件监听器接口只包含一个抽象方法,这种接口也就是前面介绍的函数式接口,因此可用Lambda表达式来创建监听对象。
    实际上,可以如下理解事件处理模型,当事件源组件上发生事件时,系统将会执行该事件源组件的所有监听器里的对应方法。与前面编程方式不同的是,普通Java程序里的方法由程序主动调用,事件处理中的事件处理器方法由系统负责调用。
    下面程序示范了一个监听多个组件,一个组件被多个监听器监听的效果。


    这里因为编码限制用的都是英文打印内容
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;public class Demo{private Frame f = new Frame("ActionListener Test");private TextArea ta = new TextArea(6,40);private Button bt1 = new Button("Button1");private Button bt2 = new Button("Button2");public void init(){//创建FirstListener监听器的实例var fl = new FirstListener();//给b1按钮注册两个事件监听器bt1.addActionListener(fl);bt1.addActionListener(new SecondListener());//将f1事件监听器注册给b2按钮bt2.addActionListener(fl);f.add(ta);var p = new Panel();p.add(bt1);p.add(bt2);f.add(p,BorderLayout.SOUTH);f.pack();f.setVisible(true);}class FirstListener implements ActionListener{public void actionPerformed(ActionEvent e){ta.append("firstListener is activation!the ActionListener is:"+e.getActionCommand()+"\n");//第一个事件监听器被激活!事件源是:}}class SecondListener implements ActionListener{@Overridepublic void actionPerformed(ActionEvent e) {ta.append("onlick is:"+e.getActionCommand()+"Button\n ");//单击了 按钮}}public static void main(String[] args) {new Demo().init();}
}

上面程序中b1按钮增加了两个事件监听器,当用户单击b1按钮时,两个监听器的actionPerform()方法都会被触发:而且fl监听器同时监听b1、b2两个按钮,当b1、b2任意一个按钮被点击时,fl监听器的actionPerform()方法都会被触发
上面程序中调用了ActionEvent对象的getActionCommand()方法,用于获取被单击按钮上的文本。
下面程序为窗口添加窗口监听器,从而示范窗口监听器的用法,并允许用户单机窗口上的“X”按钮来结束程序。


import javax.swing.*;
import java.awt.*;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;public class Demo extends JFrame {TextArea ta = new TextArea(6, 40);public Demo(String title) {setTitle(title);}public void init() {//为窗口添加窗口事件监听器this.addWindowListener(new MyListener());this.add(ta);this.pack();this.setVisible(true);}class MyListener implements WindowListener {@Overridepublic void windowOpened(WindowEvent e) {//窗口被打开事件ta.append("窗口初次被打开!");}@Overridepublic void windowClosing(WindowEvent e) {//窗口被关闭事件ta.append("用户关闭窗口!");System.exit(0);}@Overridepublic void windowClosed(WindowEvent e) {ta.append("窗口被成功关闭!\n");}@Overridepublic void windowIconified(WindowEvent e) {ta.append("窗口被最小化!\n");}@Overridepublic void windowDeiconified(WindowEvent e) {ta.append("窗口恢复!");}@Overridepublic void windowActivated(WindowEvent e) {ta.append("窗口被激活!\n");}@Overridepublic void windowDeactivated(WindowEvent e) {ta.append("窗口失去焦点!\n");}}public static void main(String[] args) {new Demo("测试窗体").init();}
}

这里注意,使用的是JFrame而不是Frame而且其写法也很特别,可以发现是使用extends让类继承JFrame(Swing类)然后直接再构造器调用那些方法,相当于把类变成了一个窗体,关于这里为什么使用了JFrame也就是Swing类是因为AWT的编码问题可能会影响本次程序的观看以及可读性,于是便直接切换到了Swing类的写法,顺便也展示了一种不一样的写法!
上面程序详细监听了窗口的每一个东西,当用户单机窗口右上角时的每个按钮时,程序都会做出相应的响应,当用户点击窗口中的“X”按钮时,程序将正常退出。
大部分时候,程序无须监听窗口的每个动作,只需要为用户单击窗口的“X”提供响应即可;无须为每个窗口事件提供响应——即程序指向重写windwoClosing事件处理器,但因为该监听器实现了WindowListener接口,实现该接口就不得不实现该接口里的每个抽象方法,这是非常烦琐的事情。为此,AWT提供了事件适配器。

事件适配器

事件适配器是监听器接口的空实现——事件适配器实现了监听器接口,并为该接口里的每个方法都提供了实现,这种实现是一种空实现(方法体内没有任何代码的实现)。当需要创建监听器时,可以通过继承事件适配器,若不是实现监听器接口。因为事件适配器已经为监听器接口的每个方法提供了空实现,所以程序自己的监听器无须实现监听器接口里的每个方法,只需要重写自己感兴趣的方法,从而以简化事件监听器的实现类代码。
如果某个监听器接口只有一个方法,则该监听器接口就无须提供适配器,因为该接口对应的监听器别无选择,只能重写该方法!如果不重写该方法,就没有必要实现该监听器。
虽然表中只列出了常用的监听器接口对应的事件适配器,实际上,所有包含多个方法的监听器接口都有对应的事件适配器,包括Swing中监听器接口也是如此。
从表中可以看出,所有包含多个监听器接口都有一个对应的适配器,但只能包含一个方法的监听器接口则没有对应的适配器。

下面程序通过事件适配器来创建事件监听器。


从上面程序可以看出,窗口监听器继承WindowAdapter事件适配器,只需要重写windowClosing方法即可,这个方法才是该程序所关心的——当用户点击“X”按钮时,程序退出。

使用内部类实现监听器

事件监听器是一种特殊的Java对象,实现事件监听器对象由如下几种形式。

  • 内部类形式:将事件监听器类定义成当前类的内部类。
  • 外部类形式:将事件监听器类定义成一个外部类。
  • 类本身作为事件监听器类:让当前类本身实现监听器接口或继承事件适配器。
  • 匿名内部类形式:使用匿名内部类创建事件监听器对象。
    前面示范程序中的所有事件监听器都是内部类形式,使用内部类可以很好地复用该监听器类,如

    这个程序所示;监听器类是外部类的内部类,所以可以自由访问外部类的所有GUI组件,这也是内部类的两个优势。
    使用内部类来定义事件监听器类的例子可以参考前面的程序,此处不再赘述。

使用外部类实现监听器

使用外部类实现定义事件监听器类的形式比较少见,主要有如下两个原因。

  • 事件监听器通常属于特定的GUI界面,定义成外部类不利于提高程序的内聚性。
  • 外部类形式的事件监听器不能自由访问创建GUI界面类中的组件,编程不够简洁。
    但如果某个事件监听器确实需要被多个GUI界面所共享,而且主要是完成某种业务逻辑的实现,则可以考虑使用外部类形式来定义监听器类。下面程序定义了一个外部类作为事件监听器类,该事件监听器实现了发送邮件的功能。
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;public class Demo implements ActionListener{//该TextField文本框用于输入发生邮件的地址private TextField mailAddress;public Demo(){}public Demo(TextField mailAddress){this.mailAddress=mailAddress;}public void setMailAddress(TextField mailAddress){this.mailAddress=mailAddress;}//实现发送邮件@Overridepublic void actionPerformed(ActionEvent e) {System.out.print("towards:"+mailAddress.getText()+"Send mail...\n");//程序向...发送邮件...//发送邮件的真是实现}
}

上面事件监听器类没有与任何GUI界面耦合,创建该监听器对象时传入一个TextField对象,该文本框里的字符串将被作为收件人的地址。下面程序使用了该事件监听器来监听窗口中的按钮。





这里使用了窗口事件监听器和对应的适配器(真好用)来完成关闭窗口的操作。
上面程序为“发送”按钮添加事件监听器时,将该窗口中的TextField对象传入事件监听器,从而允许事件监听器访问该文本框里的内容。运行上面程序看到如上的一系列运行界面和结果。
实际上并不推荐将业务逻辑实现写在事件监听器中,包含业务逻辑的事件监听器将导致程序的显示逻辑和业务逻辑耦合,从而增加程序后期的维护难度。如果确实哟多个事件监听器需要实现相同的业务逻辑功能,则可以考虑使用逻辑组件来定义业务逻辑功能,再让事件监听器来调用业务组件的业务逻辑方法。

类本身作为事件监听器类

类本身作为事件监听器类这种形式使用GUI界面类直接作为监听器类,可以直接再GUI界面类中定义事件处理器方法。这种形式非常简洁,也是早期AWT事件编程里比较喜欢采用的形式。但这种做法有如下两个缺点。

  • 这种形式可能造成混乱的程序结构,GUI界面的职责主要是完成界面初始化工作,但此时还需要包含事件处理器方法,从而降低了程序的可读性。
  • 如果GUI界面类需要继承事件适配器,将会导致该GUI界面类不能继承其他父类。

下面程序使用GUI界面作为事件监听器类。


上面程序让GUI界面类继承了WindowAdapter事件适配器,从而可以在该GUI界面类中直接定义事件处理器方法:windowClosing()。当为了某个组件添加该事件监听器对象时,直接使用this作为事件监听器对象即可。

匿名内部类实现监听器

大部分时候,事件处理器都没有复用价值(可复用代码通常会被抽象成业务逻辑方法),因此大部分事件监听器只是临时使用一次,所以匿名内部类形式的事件监听器更合适。实际上这种形式是目前使用最广泛的事件监听器形式。下面程序使用匿名内部类来创建事件监听器。


这里其实按钮那个监听器几乎也是这样用的:

我几乎每次都是这样写的,没有说专门做过可复用的实例对象来作为监听器使用。



还有就是关于Lambda表达式,Lambda表达式可用于函数式接口,也就是说这个监听器也可以用Lambada表达式来创建,更为简洁:


效果完全一致,这样的写法更简单和可读性更好!
还有就是上面好多东西目前文章还都没有介绍,不要着急慢慢往后看!都是要学会的东西!

AWT菜单

前面介绍了创建GUI界面的方法:将AWT组件按某种布局摆放在容器内即可。创建AWT菜单的方式与此完全类似:将菜单条、菜单、菜单项组合在一起即可。

菜单条、菜单和菜单项

AWT中的菜单由如下几个类组合而成

  • MenuBar:菜单条,菜单的容器。
  • Menu:菜单组件,菜单项的容器。它也是MenuItem的子类,所以可以作为菜单项使用。
  • PopupMenu:上下文菜单组件(右键菜单组件)。
  • MenuItem:菜单项组件。
  • CheckboxMenuItem:复选框菜单项组件。
  • MenuShortcut:菜单快捷键组件。
    下图显示了AWT菜单组件类之间的继承、组合关系。

    从图中可以看出,MenuBar和Menu都实现了菜单容器接口,所以MenuBar可用于盛装Menu,而Menu可用于盛装MenuItem(包括Menu和CheckboxMenuItem两个子类对象)。Menu还有一个子类:PopupMenu,代表上下文菜单,上下文菜单无须使用MenuBar盛装。
    Menu、MenuItem的构造器都可以接收一个字符串参数,该字符串作为其对应菜单、菜单项上的标签文本。除此之外,MenuItem还可以接收一个MenuShortcut对象,该对象用于指定该菜单的快捷键。MenuShortcut类使用虚拟键代码(而不是字符)来创建快捷键。例如Ctrl+A(通常都以Ctrl键作为快捷键的辅助键)快捷方式通过以下代码创建。
MenuShortcut ms = new MenuShortcut(KeyEvent.VK_A);

如果该快捷键还需要Shift键辅助,则可使用如下代码。

MenuShortcut ms = new MenuShortcut(KeyEvent.VK_A,true);

有时候程序还希望对某个菜单进行分组,将功能类似的菜单分为一组,此时需要使用菜单分隔符。AWT中添加菜单分隔符有如下两种方法。

  • 调用Menu对象的addSpearator()方法来添加菜单分隔符。
  • 使用添加new MenuItem(“-”)的方式来添加菜单分隔符。
    创建了MenuItem、Menu和MenuBar对象之后,调用Menu的add方法将多个MenuItem组成成菜单(也可将另一个Menu对象组合进来,从而形成二级菜单),再调用MenuBar的add()方法将多个Menu组合成菜单条,最后调用Frame对象的setMenuBar()方法为该窗口添加菜单条。
    下面程序示范了为窗口添加菜单的完整程序。



import java.awt.*;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;public class Demo{private Frame f = new Frame("Menu Test");private MenuBar mb = new MenuBar();Menu file = new Menu("File");//文件菜单Menu edit = new Menu("Edit");//编辑菜单MenuItem newItem = new MenuItem("New");//新建MenuItem saveItem = new MenuItem("Save File");//保存//创建exitItem菜单项,指定使用"Ctrl+X"快捷键MenuItem exitItem = new MenuItem("Exit",new MenuShortcut(KeyEvent.VK_X));//退出CheckboxMenuItem autoWrap = new CheckboxMenuItem("AutoWarp");//自动换行MenuItem copyItem = new MenuItem("Copy");//复制MenuItem pasteItem = new MenuItem("Paste");//粘贴Menu format = new Menu("ForMat");//格式//创建commpentItem菜单项,指定使用“Ctrl+Shift+/”快捷键MenuItem commpentItem = new MenuItem("Notes",new MenuShortcut(KeyEvent.VK_SLASH,true));//注释MenuItem cancelItem = new MenuItem("NotesOff");//取消注释private TextArea ta = new TextArea(6,40);public void init(){//以Lambada表达式创建菜单事件监听器ActionListener menuListener = e->{var cmd = e.getActionCommand();ta.append("onclick:"+cmd+"Menu"+"\n");//单击 + cmd + 菜单 换行if(cmd.equals("Exit")){System.exit(0);}};//为commentItem菜单项添加事件监听器commpentItem.addActionListener(menuListener);exitItem.addActionListener(menuListener);//为file菜单添加菜单项file.add(newItem);file.add(saveItem);file.add(exitItem);//为edit菜单添加菜单项edit.add(autoWrap);//使用addSeparaotr方法来添加菜单分隔线edit.addSeparator();edit.add(copyItem);edit.add(pasteItem);//为format菜单添加菜单项format.add(commpentItem);format.add(cancelItem);//使用添加new MenuItem("-")的方式添加菜单分隔线edit.add(new MenuItem("-"));//将format菜单组合到edit菜单中,从而形成二级菜单edit.add(format);//将file、edit菜单添加到mb菜单条中mb.add(file);mb.add(edit);//为f窗口设置菜单条f.setMenuBar(mb);//以内部类的形式来创建事件监听器对象f.addWindowListener(new WindowAdapter() {@Overridepublic void windowClosing(WindowEvent e) {System.exit(0);}});f.add(ta);f.pack();f.setVisible(true);}public static void main(String[] args) {new Demo().init();}
}

上面程序中菜单既有复选框菜单项和菜单分隔符,也有二级菜单,并为两个菜单项添加了快捷键,为commentItem、exitItem两个菜单项添加了事件监听器。运行该程序并按“Ctrl+Shitf+/”快捷键将会看到程序做出响应。
AWT的菜单组件不能创建图标菜单,如果希望创建带图标的菜单,则应该使用Swing的菜单组件:JMenuBar、JMenu、JMenuItem和JPopupMenu组件。Swing的组件菜单和AWT的菜单组件的用法基本相似。

右键菜单

右键菜单使用PopupMenu对象表示,创建右键菜单步骤如下。

  1. 创建PopupMenu的实例。
  2. 创建多个MenuItem的多个实例,依次将这些实例加入到PopupMenu中。
  3. 将PopupMenu加入到目标组件中。
  4. 为需要出现上下文的组件编写鼠标监听器,当用户释放鼠标右键时弹出右键菜单。
    下面程序创建了一个右键菜单,该右键菜单就是“借用”前面程序中下的所有菜单项。


import java.awt.*;
import java.awt.event.*;public class Demo{private TextArea ta = new TextArea(4,30);//创建一个文本域private Frame f = new Frame("Test");//Frame 对象PopupMenu pop = new PopupMenu();//创建一个上下文对象CheckboxMenuItem autoWrap = new CheckboxMenuItem("AutoWarp");//自动换行 这里是复选框型菜单选项MenuItem copyItem = new MenuItem("Copy");//复制MenuItem pasteItem = new MenuItem("Paste");//粘贴Menu format = new Menu("ForMat");//格式//创建commentItem菜单项,指定使用“Ctrl+Shift+/”快捷键MenuItem commentItem = new MenuItem("Notes",new MenuShortcut(KeyEvent.VK_SLASH,true));MenuItem cancelItem = new MenuItem("NotesOff");//取消注释public void init(){ActionListener menuListener = e->{var cmd = e.getActionCommand();ta.append("onclick:"+cmd+"Menu!"+"\n");//单击 + cmd + 菜单 +换行转义符if(cmd.equals("Exit")){System.exit(0);}};//为commentItem菜单项添加事件监听commentItem.addActionListener(menuListener);//为pop菜单添加菜单项pop.add(autoWrap);//使用addSeparator方法来添加菜单分隔线pop.addSeparator();pop.add(copyItem);pop.add(pasteItem);//为format菜单添加菜单项format.add(commentItem);format.add(cancelItem);//使用添加new MenuItem("-")的方式添加菜单分隔线pop.add(new MenuItem("-"));//将format菜单项组合到pop菜单中,从而形成耳机菜单pop.add(format);final var p = new Panel();p.setPreferredSize(new Dimension(300,160));//向p窗口中添加PopupMenu对象p.add(pop);//添加鼠标事件监听器p.addMouseListener(new MouseAdapter() {@Overridepublic void mouseReleased(MouseEvent e) {//如果释放的是鼠标右键if(e.isPopupTrigger()){pop.show(p,e.getX(),e.getY());}}});f.add(p);f.add(ta,BorderLayout.NORTH);//以匿名内部类的形式来创建事件监听器对象f.addWindowListener(new WindowAdapter() {@Overridepublic void windowClosing(WindowEvent e) {System.exit(0);}});f.pack();f.setVisible(true);}public static void main(String[] args) {new Demo().init();}
}

为什么即使我没有给多行文本域编写右键菜单,但当我在多行文本域上单击右键也会一样会弹出右键菜单?
记住AWT实现的机制!AWT并没有为GUI组件提供实现,添加仅仅是调用运行平台的GUI组件来创建和平台一致的对等体。因此程序中的TextArea实际上是Windows(假设在Windows平台上运行)的多行文本域组件的对等体,具有和它相同的行为,所以该TextArea默认就具有右键菜单。

在AWT中绘图

很多程序如各种小游戏都需要在窗口中绘制各种图形,除此之外,即使在开发JavaEE项目时也必须“动态”地向用户端生成各种图形、图表,比如图形验证码、统计图等,这都需要利用AWT的绘图功能。

画图的实现原理

在Component类里提供了和绘图有关的三个方法。

  • paint(Graphics g):绘制组件的外观。
  • update(Graphics g):调用paint()方法,刷新组件外观。
  • repaint():调用update方法,刷新组件外观。

上面三个方法的调用关系为:repaint()方法调用update()方法;update()方法调用paint()方法。
Container类中的update()方法先以组件的背景色填充整个组件区域,然后调用paint()方法重画组件。
Container类的update()方法代码如下:

import java.awt.*;public class Demo {public void update(Graphics g){//以组件的背景色填充整个组件区域if(!(peer instanceof LightweightPeer)){g.clearRect(0,0,width,heigth);}paint(g);}
}

普通组件的update()方法则直接调用paint()方法。

public void update(Graphics g){paint(g);
}

下图显示了paint()、repaint()和update()三个方法之间的调用关系。

从上图可以看出,程序不应该主动调用组件的paint()和update()方法,这两个方法都由AWT系统负责调用。如果程序希望AWT系统重新绘制组件,则调用该组件的repaint()方法即可。而paint()和update()方法通常被重写。在通常情况下,程序通过重写paint()实现在AWT组件上绘图。
重写update或paint()方法时,该方法里包含了一个Graphics类型的参数,通过该Graphics参数就可以实现绘图功能。

使用Graphics类

Graphics是一个抽象的画笔对象,Graphics可以在组件上绘制丰富多彩的几何图形和位图。Graphics类提供了如下几个方法用于绘制几何图形和位图。

  • drawLine():绘制直线。
  • drawString():绘制字符串。
  • drawRect():绘制矩形。
  • drawoundRect():绘制圆角矩形。
  • drawOval():绘制椭圆的形状。
  • drawPolygon():绘制多边形边框。
  • drawArc():绘制一段圆弧(可能是椭圆的圆弧)。
  • drawPolyline():绘制折线。
  • fillRect():填充一个矩形区域。
  • fillRoundRect():填充一个圆角矩形区域。
  • fillOval():填充一个多边形区域。
  • fillPolygon():填充一个多边形区域。
  • fillArc():填充圆弧和圆弧两个端点到中心连接所包围的区域。
  • drawImage():绘制位图。

除此之外,Graphics还提供了setColor()和setFont()两个方法用于设置画笔的颜色和字体(仅当绘制字符串时有效),其中setColor()方法需要传入一个Color参数,它可以使用RGB、CMYK等方式设置一个颜色;而setFont()方法需要传入一个Font参数,Font参数需要指定字体名、字体样式、字体大小三个属性。
实际上,不仅Graphics对象可以使用setColor()和setFont()来设置画笔的颜色和字体,AWT普通组件也可以通过Color()和Font()方法来改变它的前景色和字体。除此之外,所有组件都有一个setBackground()方法用于设置组件的背景色。
AWT专门提供了一个Canvas类作为绘图的画布,程序可以通过创建Canvas的子类,并重写它的paint()方法来实现绘图。下面程序示范了一个简单的绘图程序。



位置是随机的。

import java.awt.*;
import java.util.Random;public class Demo{private final String RECT_SHAPE = "rect";private final String OVAL_SHAPE = "oval";private Frame f = new Frame("SimpleDraw");private Button rect = new Button("draw rect");//绘制矩形private Button oval = new Button("draw oval");//绘制圆形private MyCanvas drawArea = new MyCanvas();private String shape = "";public void init(){var p =new Panel();rect.addActionListener(e->{//设置shape变量为RECT_SHAPEshape = RECT_SHAPE;//重画MyCanvas对象,即调用它的repait()方法drawArea.repaint();});oval.addActionListener(e -> {//设置shape变量为OVAL_SHAPEshape=OVAL_SHAPE;//重画MyCanvas对象,即调用它的repait()方法drawArea.repaint();});p.add(rect);p.add(oval);drawArea.setPreferredSize(new Dimension(250,180));f.add(drawArea);f.add(p,BorderLayout.SOUTH);f.pack();f.setVisible(true);}public static void main(String[] args) {new Demo().init();}class MyCanvas extends Canvas{@Overridepublic void paint(Graphics g) {var rand = new Random();if(shape.equals(RECT_SHAPE)){//设置画笔的颜色g.setColor(new Color(220,100,80));//随机地绘制一个矩形框g.drawRect(rand.nextInt(200),rand.nextInt(120),40,60);}if(shape.equals(OVAL_SHAPE)){//设置画笔的颜色g.setColor(new Color(80,100,200));//随机地填充一个实心圆形g.fillOval(rand.nextInt(200),rand.nextInt(120),50,40);}}}
}

上面程序定义了一个MyCanvas类,它继承了Canvas类,重写了Canvas类的paint()方法,该方法根据shape变量值随机地绘制矩形或填充椭圆区域。窗口中还定义了两个按钮,当用户单机任意一个按钮时,程序调用了drawArea对象的repaint()方法,该方法导致画布重绘(即调用drawArea对象的update()方法,该方法再调用paint()方法)。
运行上面程序时,如果改变窗口大小,或者让该窗口隐藏后重新显示都会导致drawArea重新绘制形状——这是因为这些动作都会触发组件的update()方法。
Java也可用于开发一些动画。所谓动画,就是间隔一定的时间(通常小于0.1秒)重新绘制新的图像,两次绘制的图像之间差异较小,肉眼看起来就成了所谓的动画。为了实现间隔一定的时间就重新调用组件的repaint()方法,可以借助于Swing提供的Timer类,Timer类是一个定时器,它有如下一个构造器。

  • Timer(int delay,ActionListener listener):每间隔delay毫秒,系统自动触发ActionListener监听器里的事件处理器(actionPerformed()方法)。
    下面程序示范了一个比较简单的弹球游戏,其中小球和球拍分别以圆形区域和矩形区域代替,小球开始以随机速度向下运动,遇到边框或球拍时小球反弹;球拍则由用户控制,当用户按下向左、向右键是,球拍将会向左、向右移动。


    运行上面程序将看到一个简单的弹球小游戏。
    上面的弹球游戏还比较简陋,如果为该游戏增加位图背景,使用更逼真的小球位图替代小球,更逼真的球拍图代替球拍,并再弹球桌面增加一些障碍物,整个弹球游戏将会更有趣味。细心的读者可能会发现上面的游戏有轻微的闪烁,这是由于AWT组件的绘图没有采用双缓冲技术,当重写paint()方法来绘制图行时,所有的图形都是直接绘制到GUI组件上的,所以多次重新调用paint()方法进行绘制时会发生闪烁现象。使用Swing组件就可避免这种种闪烁,Swing组件没有提供Canvas对应的组件,使用Swing的Panel组件作为画布即可。

处理位图

如果仅仅绘制一些简单的几何图形,程序的图形效果依然比较单调。AWT也允许再组件上绘制位图。Craphics提供了drawImage方法用于绘制位图,该方法需要一个Image参数——代表位图,通过该方法就可以绘制出指定的位图。

Image抽象类和BufferedImage实现类

Image类代表位图,但它是一个抽象类,无法直接创建Image对象,为此Java为它提供了一个BufferedImage子类,这个子类是一个可访问图像数据缓冲区的Image实现类。该类提供了一个简单的构造器,用于创建一个BufferedImage对象。

  • BufferedImage(int width,int height,int imageType):创建指定大小、指定图像类型的BufferedImage对象,其中imageType可以是BufferedImage.TYPE_INT_RGB、BufferedImage.TYPE_GRAY等值。
    除此之外,BufferedImage还提供了一个getGraphics()方法返回该对象的Graphics对象,从而允许通过该Graphics对象向Image中添加图形。
    借助BufferedImage可以再AWT中实现缓冲技术——当需要向GUI组件上绘制图形时,不要直接绘制到GUI组件上,而是先将图形绘制到BufferedImage对象中,然后再调用组件的drawImage方法一次性地将BufferedImage对象绘制到特定组件上。
    下面程序通过BufferedImage类实现了图形缓冲,并实现了一个简单的手绘程序。


import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;public class Demo{//画图区的宽度private final int AREA_WIDTH = 500;//画图区的高度private final int AREA_HEIGHT = 400;//下面的preX、preY保存了上一次鼠标拖动事件的鼠标坐标private int preX = -1;//约等于无值private int preY = -1;//约等于无值//定义一个右键菜单用于设置画笔颜色PopupMenu pop = new PopupMenu();MenuItem redItem = new MenuItem("Red");MenuItem greenItem = new MenuItem("Green");MenuItem blueItem = new MenuItem("Blue");//定义一个BufferedImage对象BufferedImage image = new BufferedImage(AREA_WIDTH,AREA_HEIGHT,BufferedImage.TYPE_INT_RGB);//获取image对象的GRAPHICSGraphics g = image.getGraphics();private Frame f = new Frame("SimpleSAI");private DrawCanvas drawArea = new DrawCanvas();//用于保存画笔的颜色private Color foreColor = new Color(255,0,0);public void init(){//定义右键菜单的事件监听器ActionListener menuListener = e->{if(e.getActionCommand().equals("Green")){foreColor = new Color(0,255,0);}if(e.getActionCommand().equals("Red")){foreColor = new Color(255,0,0);}if(e.getActionCommand().equals("Blue")){foreColor = new Color(0,0,255);}};//为三个菜单添加事件监听器redItem.addActionListener(menuListener);greenItem.addActionListener(menuListener);blueItem.addActionListener(menuListener);//将菜单组合成右键菜单pop.add(redItem);pop.add(greenItem);pop.add(blueItem);//将右键菜单添加到drawArea对象中drawArea.add(pop);//将image对象的背景色填充成白色g.fillRect(0,0,AREA_WIDTH,AREA_HEIGHT);drawArea.setPreferredSize(new Dimension(AREA_WIDTH,AREA_HEIGHT));drawArea.addMouseMotionListener(new MouseAdapter() {@Overridepublic void mouseDragged(MouseEvent e) {//如果preX和preY大于0if(preX>0&&preY>0){//设置当前颜色g.setColor(foreColor);//绘制从上一次鼠标拖动事件点到本次鼠标拖动事件点的线段g.drawLine(preX,preY,e.getX(),e.getY());}preX = e.getX();preY = e.getY();//重绘drawArea对象drawArea.repaint();}});drawArea.addMouseListener(new MouseAdapter() {//弹出右键菜单@Overridepublic void mouseReleased(MouseEvent e) {if(e.isPopupTrigger()){pop.show(drawArea,e.getX(),e.getY());}//松开鼠标是,把上一次鼠标拖动事件的X、Y坐标设为 -1preX = -1;preY = -1;//约等于归零}});f.add(drawArea);f.pack();f.setVisible(true);f.addWindowListener(new WindowAdapter() {@Overridepublic void windowClosing(WindowEvent e) {System.exit(0);}});}public static void main(String[] args) {new Demo().init();}class DrawCanvas extends Canvas{//重写Canvas的paint方法,实现绘画@Overridepublic void paint(Graphics g) {//将image绘制到该组件上g.drawImage(image,0,0,null);}}
}

实现手绘功能其实是一种假象:表面上看起来可以随鼠标移动自由画曲线,实际上依然利用Graphics的drawLine()方法画直线,每条直线都是从上一次鼠标拖动事件发生点画到本次鼠标拖动事件发生点。当鼠标拖动时,两次鼠标拖动事件发生点的距离很小,多条极短的直线连接起来,肉眼看起来就是鼠标拖动的轨迹了。上面程序还增加了右键菜单来选择画笔颜色。
上面程序进行手绘时只能选择红、绿、蓝三种颜色,不能调出像Windows的颜色选择对话框那种“专业”的颜色选择工具。实际上,Swing提供了颜色选择对话框的支持,如果结合Swing提供的颜色选择对话框,就可以选择任意的颜色进行画图,并可以提供一些按钮让用户绘制直线、折线、多边形等几何图形。如果为该程序分别建立多个BufferedImage对象,就可实现多图层效果(每个BufferedImage代表一个图层)。

Java9增强的ImageIO

如果希望可以访问磁盘上的位图文件,例如GIF、JPG等格式的位图。则需要利用ImageIO工具类。ImageIO利用ImageReader和ImageWriter读写图形文件,通常程序无须关心该底层的细节,只需要利用该工具类来读写图形文件即可。
ImageIO类并不支持全部格式的图形文件,程序可以通过ImageIO()类的如下几个静态方法来访问该类所支持读写的图形文件格式。

  • static String[]getReaderFileSuffixes():返回一个String数组,该数组列出ImageIO所有能读的图形文件的文件后缀。
  • static String[]getReaderFormatNames():返回一个String数组,该数组列出ImageIO所有能读的图形文件的非正式格式名称。
  • static String[]getWriterFileSuffixes():返回一个String数组,该数组列出ImageIO所有能写的图形文件的文件后缀。
  • static String[]getWriterFormatNames():返回一个String数组,该数组列出ImageIO所有能写的图形文件的非正式格式名称。

下面程序测试了ImageIo所支持读写的全部文件格式。

import javax.imageio.ImageIO;public class Demo{public static void main(String[] args) {String[]readFormat = ImageIO.getReaderFormatNames();System.out.println("-----Image能读的所有图形文件格式-----");for(var tmp : readFormat){System.out.println(tmp);}String[]writeFormat = ImageIO.getWriterFormatNames();System.out.println("-----Image能写的所有图形文件格式-----");for(var tmp:writeFormat){System.out.println(tmp);}}
}
"C:\Program Files\Java\jdk-11.0.11\bin\java.exe" "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=59687:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\DemoProject\out\production\DemoProject Demo
-----Image能读的所有图形文件格式-----
JPG
jpg
tiff
bmp
BMP
gif
GIF
WBMP
png
PNG
JPEG
tif
TIF
TIFF
wbmp
jpeg
-----Image能写的所有图形文件格式-----
JPG
jpg
tiff
bmp
BMP
gif
GIF
WBMP
png
PNG
JPEG
tif
TIF
TIFF
wbmp
jpegProcess finished with exit code 0

运行上面程序就可以看到Java所支持的图形文件格式,通过运行结果可以看出,AWT并不支持ico等图标格式。因此,如果需要再Java程序中位按钮、菜单等指定图标,也不要使用ico格式的图标文件。而应该使用JPG、GIF等格式的图形文件。
Java9增强了ImageIO的功能,ImageIO可以读写TIFF(Tag Image File Format)格式的图片。
ImageIO类包含两个静态方法:read()和write(),通过着两个方法即可完成对位图文件的读写,调用wirte()方法输出图形文件时需要指定输出的图形格式,例如GIF、JPEG等。下面程序可以将第一个原始位图缩小成另一个位图后输出。

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;public class Demo{//下面两个常量设置缩小后图片的大小private final int WIDTH = 80;private final int HEIGHT = 60;//定义一个BufferedImage对象,用于保存缩小后的位图BufferedImage image = new BufferedImage(WIDTH,HEIGHT,BufferedImage.TYPE_INT_RGB);Graphics g = image.getGraphics();public void zoom()throws Exception{//读取原始位图Image srcImage = ImageIO.read(new File("C:\\Users\\YueDie\\Pictures\\Saved Pictures\\依睐2021.jpg"));g.drawImage(srcImage,0,0,WIDTH,HEIGHT,null);//将image的对象输出到磁盘文件中ImageIO.write(image,"jpeg",new File(System.currentTimeMillis()+".jpg"));}public static void main(String[] args) throws Exception{new Demo().zoom();}
}


上面程序中第一行代码从磁盘中读取一个位图文件,第二行代码则将原始位图按照原始大小绘制到image对象中,第三行代码再将image对象输出,着就完成了位图的缩小(实际上不一定时缩小,程序总是将原始位图缩小到WIDTH、HEIGHT常量指定的大小)并输出。
上面程序总是使用board.jpg文件作为原始文件,总是缩放到80 x 60 的尺寸,且总是以当前时间作为文件名来输出该文件,这是位了简化程序。如果为该程序增加图形界面,允许用户选择需要缩放的原始图片文件和缩放后的目标文件名,并可以设置缩放后的尺寸,该程序将具有很好的实用性。对位图文件进行缩放是非常使用的功能,大部分Web应用都运行用户上传图片,而Web应用则需要对用户上传的位图生成相应的缩略图,这就需要对位图进行缩放。
利用ImageIO读取磁盘上的位图,然后将这图绘制再AWT组件上,就可以做出更加丰富多彩的图形界面程序。
关于第四章的五子棋游戏,可以位该游戏增加图形用户界面,这里略,因为时间的问题,我们的进度要抓紧!所有这个程序的更改可以通过自己无穷的想象力来进行修改。

剪贴板

当进行复制、剪切、粘贴等Window操作时,也许读者从未想过这些操作的实现过程。实际上这是一个看似简单的过程:复制、剪切把一个程序中的数据放置到剪贴板中,而粘贴则读取剪贴板中的数据,并将该数据放入另一个程序中。
剪贴板的复制、剪切和粘贴的过程看似很简单,但实现起来则存在一些具体问题需要处理——假设程序只希望粘贴数值和可以复制图像并粘贴。
因为AWT时实现依赖于底层运行平台的实现,因此AWT剪贴板再不同平台上所支持的传输对象也完全不同。其中Microsoft、Macintosh的剪贴板支持传输富格式文本、图像、纯文本等信息,而X Window的剪贴板功能则比较有限,它仅仅支持纯文本的剪切和粘贴。
AWT支持两种剪贴板:本地剪贴板和系统剪切板。如果在同一个虚拟机的不同窗口之间进行数据传递,则使用AWT自己的本地剪贴板就可以了。本地剪贴板与运行平台无关,可以传送任意格式的数据。如果需要再不同的虚拟机之间传递数据,或者需要在Java与第三方程序之间传递数据,那就需要使用系统的剪贴板了。

数据传递的类和接口

AWT中剪贴板相关操作的接口和类被放在 java.awt.datatransfer包下,下面时该包下重要的接口和类的相关说明。

  1. Clipboard:代表一个剪贴板实例,这个剪贴板既可以是系统剪贴板,也可以时本地剪贴板。
  2. ClipboardOwner:剪贴板内容的所有者接口,当剪贴板内容的所有权被修改时,系统将会触发该所有者的lostOwnership事件处理器。
  3. Transferable:该接口的实例代表放进剪贴板中的传输对象。
  4. DataFlavor:用于表述剪贴板中的数据格式。
  5. StringSelection:Transferable的实现类,用于传输文本字符串。
  6. FlavorListener:数据格式监听器接口。
  7. FlavorEvent:该类的实例封装了数据格式改变的事件。

传递文本

传递文本是最简单的情形,因为AWT已经提供了一个StringSelection用于传输文本字符串。将一段文本内容(字符串对象)放进剪贴板中的步骤如下。

  1. 创建一个Clipboard实例,既可以创建系统剪贴板,也可以创建本地剪贴板。创建系统剪贴板通过如下代码:
var clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();

创建本地剪贴板通过如下代码:

var clipboard = new Clipboard("cb");
  1. 将需要放入剪贴板中的字符串封装成StringSlection对象,如下代码所示:
var st = new StringSelection(targetStr);
  1. 调用剪贴板对象的setContents()方法将StringSelection放进剪贴板中,该方法需要两个参数,第一个参数是Transferable对象,代表放进剪贴板中的对象;第二个参数是ClipboradOwner对象,代表剪贴板数据的所有者,通常无须关心剪贴板数据的所有者,所以把第二个参数设置为null。
clipboard.setContents(st.null);

从剪贴板中取出数据则比较简单,调用Clipboard对象的getData(DataFlavor flavor)方法即可取出剪贴板中指定格式的内容,如果指定flavor的数据不存在,该方法将引发UnsupportedFlavorException异常。为了避免出现异常,可以先调用对象的isDataFlavorAvaliable(DataFlavor flavor)来判断指定flavor的数据是否存在。如下代码提示:

if(clipboard.isDataFlavorAvaliable(DataFlavor.stringFlavor)){String content = (String)clipboard.getData(DataFlavor.stringFlavor);
}

下面程序是一个利用系统剪贴板进行复制、剪贴的简单程序。





上面程序中的“复制”按钮的事件监听器负责将第一个文本域的内容复制到系统剪贴板中,“粘贴”按钮的事件监听器负责取出系统粘贴板中的stringFlavor内容,并将其添加到第二个文本域内。运行上面程序看到如上结果。
因为程序是系统剪贴板,因此可以通过Windows的剪贴薄查看来查看程序放入剪贴板中的内容。在Window的“开始”菜单中运行 “clipbrd”程序加看到如下所示的窗口。

Win10系统的话可以通过 Win+V快速查看。

使用系统剪贴板传递图像

前面已经介绍了,Transferable接口代表可以放入剪贴板的传输对象,所以如果希望图像放入剪贴板内,则必须提供一个Transferable接口的实现类,该实现类其实很简单,它封装一个image对象,并且向外表现为imageFlavor内容。
JDK为Transferable接口提供了一个StringSelection实现类,用于封装字符串内容。但JDK在DataFlavor类中提供了一个imageFlavor常量,用于代表图像格式的DataFlavor,并负责执行所有的复杂操作,以便进行Java图像和剪贴板图像的转换。
下面程序实现了一个ImageSelection类,该类实现了Transferable接口,并实现了该接口所包含的三个方法。

import java.awt.*;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.IOException;public class ImageSelection implements Transferable {private Image image;public ImageSelection(Image image){this.image = image;}//返回该Transferable对象所支持的所有DataFlavor@Overridepublic DataFlavor[] getTransferDataFlavors() {return new DataFlavor[]{DataFlavor.imageFlavor};}//返回该Transferable对象是否支持指定的DataFlavor@Overridepublic boolean isDataFlavorSupported(DataFlavor flavor) {return flavor.equals(DataFlavor.imageFlavor);}//取出该Transferable对象里实际的数组@Overridepublic Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {if(flavor.equals(DataFlavor.imageFlavor)){return image;}else {throw new UnsupportedFlavorException(flavor);}}
}

有了ImageSelection封装类后,程序就可以将指定的Image对象包装成ImageSelection对象放入剪切板中。下面程序对前面的HandDraw程序进行了改进,改进后的程序允许用户手绘的图像复制到剪贴板中,也可以把剪贴板里的图像粘贴到该程序中。

import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.ArrayList;public class CopyImage {//系统剪贴板private Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();//使用ArrayLsit来保存所有粘贴进来的Image——就是当成图层处理java.util.List<Image>imageList = new ArrayList<>();//下面代码与前面HanDraw程序中控制绘图的代码一样//画图区的宽度private final int AREA_WIDTH = 500;//画图区的高度private final int AREA_HEIGHT = 400;//下面的preX、preY保存了上一次鼠标拖动事件的鼠标坐标private int preX = -1;//约等于无值private int preY = -1;//约等于无值//定义一个右键菜单用于设置画笔颜色PopupMenu pop = new PopupMenu();MenuItem redItem = new MenuItem("Red");MenuItem greenItem = new MenuItem("Green");MenuItem blueItem = new MenuItem("Blue");//定义一个BufferedImage对象BufferedImage image = new BufferedImage(AREA_WIDTH,AREA_HEIGHT, BufferedImage.TYPE_INT_RGB);//获取image对象的GRAPHICSGraphics g = image.getGraphics();private Frame f = new Frame("SimpleSAI");private DrawCanvas drawArea = new DrawCanvas();//用于保存画笔的颜色private Color foreColor = new Color(255,0,0);public void init() {//定义右键菜单的事件监听器ActionListener menuListener = e -> {if (e.getActionCommand().equals("Green")) {foreColor = new Color(0, 255, 0);}if (e.getActionCommand().equals("Red")) {foreColor = new Color(255, 0, 0);}if (e.getActionCommand().equals("Blue")) {foreColor = new Color(0, 0, 255);}};//为三个菜单添加事件监听器redItem.addActionListener(menuListener);greenItem.addActionListener(menuListener);blueItem.addActionListener(menuListener);//将菜单组合成右键菜单pop.add(redItem);pop.add(greenItem);pop.add(blueItem);//将右键菜单添加到drawArea对象中drawArea.add(pop);//将image对象的背景色填充成白色g.fillRect(0, 0, AREA_WIDTH, AREA_HEIGHT);drawArea.setPreferredSize(new Dimension(AREA_WIDTH, AREA_HEIGHT));drawArea.addMouseMotionListener(new MouseAdapter() {@Overridepublic void mouseDragged(MouseEvent e) {//如果preX和preY大于0if (preX > 0 && preY > 0) {//设置当前颜色g.setColor(foreColor);//绘制从上一次鼠标拖动事件点到本次鼠标拖动事件点的线段g.drawLine(preX, preY, e.getX(), e.getY());}preX = e.getX();preY = e.getY();//重绘drawArea对象drawArea.repaint();}});drawArea.addMouseListener(new MouseAdapter() {//弹出右键菜单@Overridepublic void mouseReleased(MouseEvent e) {if (e.isPopupTrigger()) {pop.show(drawArea, e.getX(), e.getY());}//松开鼠标是,把上一次鼠标拖动事件的X、Y坐标设为 -1preX = -1;preY = -1;//约等于归零}});f.add(drawArea);var p =new Panel();var copy = new Button("Copy");var paste = new Button("Paste");copy.addActionListener(event->{//将image对象封装成ImageSelection对象var contents = new ImageSelection(image);//将ImageSelection对象放入剪贴板clipboard.setContents(contents,null);});paste.addActionListener(event->{//如果剪贴板中包含imageFlavor内容if(clipboard.isDataFlavorAvailable(DataFlavor.imageFlavor)){try{//取出剪贴板中的imageFlavor内容,并将其添加到List几何中imageList.add((Image)clipboard.getData(DataFlavor.imageFlavor));drawArea.repaint();} catch (UnsupportedFlavorException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}}});p.add(copy);p.add(paste);f.add(p,BorderLayout.SOUTH);f.pack();f.setVisible(true);f.validate();f.addWindowListener(new WindowAdapter() {@Overridepublic void windowClosing(WindowEvent e) {System.exit(0);}});}public static void main(String[] args) {new CopyImage().init();}class DrawCanvas extends Canvas{@Overridepublic void paint(Graphics g) {g.drawImage(image,0,0,null);//将List里所有Image对象都绘制出来for(var img:imageList){g.drawImage(img,0,0,null);}}}
}


这个封装类也是不可缺少的。
上面程序实现复制、粘贴的代码也很简单,就是程序中两端代码:第一段代码实现了图像复制功能,将image对象封装成了ImageSelection对象,然后调用Clipboard的setContents()方法将该对象放入剪贴板中;第二段代码实现了图像粘贴功能。取出剪贴板中的imageFlavor内容,返回一个Image对象,将Image对象添加到程序的imageList集合中。
上面程序使用了“图层”的概念。使用imageList集合来保存所有粘贴到程序中的Image——每个Image就是一个图层,重绘Canvas对象时需要绘制imageList集合中的每个image图像。
如果在其他程序中复制一块图像区域(由其他程序负责将图片放入系统剪贴板中),然后单击本程序中的“粘贴”按钮,就可以将该图像粘贴到本程序中。如上图所示。[图片:几何繁华——洛天依]。

使用本地粘贴板传递对象引用

本地剪贴板可以保存任何类型的Java对象,包括自定义类型的对象。为了将任意类型的Java对象保存到剪贴板中,DataFlavor里提供了一个java.JVMLocalObjectMimeType的常量,该常量是一个MIME类型字符串:application/x-java-jvm-local-objecteef,将Java对象放入本地粘贴板中必须使用该MIME类型。该MIME类型表示仅将对象引用复制到剪贴板中,对象引用只有在同一个虚拟机中才有效,所以只能使用本地剪贴板。创建本地剪贴板的代码如下:

var clipboard = new Clipboard("cp");

创建本地剪贴板时需要传入一个字符串,该字符串是粘贴板的名字,通过这种方式允许在一个程序中创建本地剪贴板,就可以实现像Word那种多次复制,选择剪贴板粘贴的功能。
本地剪贴板是JVM负责维护的内存区,因此本地剪贴板会随虚拟机的结束而销毁。因此一旦Java程序退出,本地剪贴板中的内容将会丢失。
Java没有提供封装对象引用的Transferable实现类,因此必须自己实现该接口。实现该接口与前面的ImageSelection基本相似,一样要实现该三个接口,并持有某个对象的引用。看如下代码。

import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.IOException;public class LocalObjectSelection implements Transferable {private Object obj;public LocalObjectSelection(Object obj){this.obj=obj;}@Overridepublic DataFlavor[] getTransferDataFlavors() {var flavors = new DataFlavor[2];//获取被封装对象的类型Class clazz = obj.getClass();String mimeType = "application/x-java-jvm-local-objectref;"+"class="+clazz.getName();try{flavors[0] = new DataFlavor(mimeType);flavors[1] = DataFlavor.stringFlavor;return flavors;}catch (ClassNotFoundException e){e.printStackTrace();return null;}}//取出该Transferable对象封装的数据@Overridepublic boolean isDataFlavorSupported(DataFlavor flavor) {return flavor.equals(DataFlavor.stringFlavor)||flavor.getPrimaryType().equals("application")&& flavor.getSubType().equals("x-java-jvm-local-objectref")&&flavor.getRepresentationClass().isAssignableFrom(obj.getClass());}@Overridepublic Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {if(!isDataFlavorSupported(flavor)){throw new UnsupportedFlavorException(flavor);}if(flavor.equals(DataFlavor.stringFlavor)){return obj.toString();}return obj;}
}

上面程序创建了一个DataFlavor对象,用于标识本地Person对象引用的数据格式。创建DataFlavor对象可以使用如下构造器。

  • DataFlavor(String mimeType):根据mimeType字符串构造DataFlavor。

程序上面使用构造器创建了MIME类型为“application/x-java-jvm-local-objectref;class=”+clazz.getName()的DataFlavor对象,它表示封装本地对象引用的数据格式。
有了上面的LocalObjectSelection封装类后,就可以使用该类来封装某个对象的引用,从而将该对象的引用放入本地剪贴板中。下面程序示范了如何将一个Person对象放入本地剪贴板中,以及从本地剪贴板中读取该Person对象。

import org.w3c.dom.Text;import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;public class CopyPerson {Frame f = new Frame("Copy Object");//复制对象Button copy = new Button("Copy");//复制Button paste = new Button("paste");//粘贴TextField name = new TextField(15);//名字文本框TextField age = new TextField(15);//年龄文本框TextArea ta = new TextArea(3,30);//创建本地剪贴板Clipboard clipboard = new Clipboard("cp");public void init(){var p =new Panel();p.add(new Label("Name"));p.add(name);p.add(new Label("Age"));p.add(age);f.add(p,BorderLayout.NORTH);f.add(ta);var bp = new Panel();//为"复制"按钮添加事件监听器copy.addActionListener(e->);}public void copyPerson(){//以name、age文本框的内容创建Person对象var p = new Person(name.getText()),Interger.parseInt(age.getText());//将Person对象封装成LoacalObjectSelection对象var ls = new LocalObjectSelection(p);//将LocalObjectSelection对象放入本地剪贴板中clipboard.setContents(ls,null);}public void readPerson() thorws Exception{//创建保存Person对象引用的DataFlavor对象var personFlavor = new DataFlavor("application/x-java-jvm-local-objectfer;class=Person");//取出本地剪贴板中的内容if(clipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)){var p =(Person)clipboard.getData(personFlavor);ta.setText(p.toString());}}} }   }
public static void main(String[]args){new CopyPerson().init();}
}

上面程序代码实现了复制、粘贴对象的功能,这两段代码与前面复制、粘贴图像的代码并没有太大区别,只是前面程序使用了Java本身提供的Data.imageFlavor数据格式,而此处必须自己创建一个DataFlavor,用以标识封装Person引用的DataFlavor。运行上面程序,在“姓名”文本框内随意输入一个字符串,在“年龄”文本框内输入年龄数字,然后单击“复制”按钮,就可以将根据两个文本框的内容创建的Person对象放入本地粘贴板中。
本节略过

拖放功能

拖放是非常常见的操作,人们经常会通过拖放来完成复制、剪切功能,但这种复制、剪切操作无须剪切板支持,程序将数据从拖放源直接传递给拖放目标。这种通过拖放实现的复制、剪切效果也被称为复制、移动。
人们在拖放源中选中一项或多项元素,然后用鼠标将这些元素在拖放目标上松开鼠标按键时,拖放目标将会查询拖放源,进而访问到这些元素的相关信息,并会相应地启动一些动作。例如,从Windows资源管理器中把一个文件图标拖放到WinPad图标上,WinPad将会打开该文件。如果在Eclipse中选中一段代码,然后将这段代码拖放到另一个位置,系统将会把这段代码从初始位置删除,并将这段代码放到拖放的目标位置。
除此之外,拖放操作还可以与三种键组合使用,用以完成特殊功能。

  • 与Ctrl键组合使用:表示该拖放操作完成复制功能。例如,可以在Eclipse中通过将一段代码剪切到另一个地方,如果在拖放过程中按住Ctrl键,系统将完成代码复制,而不是剪切。
  • 与Shift键组合使用:表示该拖放操作完成移动功能。有些时候直接拖放默认就是进行复制,例如,从Windows资源管理器的一个路径将文件图标拖放到另一个路劲,默认就是进行文件复制。此时可以结合Shift键组合使用来进行操作,用以完成移动功能。
  • 与Ctrl、Shift键组合使用:表示为目标对象建立快捷方式(在UNIX等平台上成为链接)。

在拖放操作中,数据从拖放源直接传递给拖放目标,因此拖放操作主要涉及两个对象:拖放源和拖放目标。AWT已经提供了对拖放源和拖放目标的支持,分别由DragSource和DropTarget两个类来表示。下面将具体介绍如何在程序中建立拖放源和拖放目标。
实际上,拖放操作与前面介绍的剪贴板有一定的类似之处,它们之间的差别在于:拖放操作将数据从拖放源直接传递给拖放目标,而剪贴板操作则是先将数据传递到剪贴板上,然后再从剪贴板传递给目标。剪贴板操作中被传递的内容使用Transferable接口来封装,与此类似的时,拖放操作中被传递的内容也使用Transferable来封装;剪贴板操作中被传递的数据格式使用DataFlavor来表示,拖放操作中同样使用DataFlavor来表示被传递的数据格式。

拖放目标

再GUI界面中创建拖放目标非常简单,AWT提供了DropTarget类来表示拖放目标,可以通过该类提供的如下构造器来创建一个拖放目标。

  • DropTarget(Componet c,int ops,DropTargetListener dtl):将c组件创建成一个拖放目标,该拖放目标默认可接受ops值指定的拖放操作。其中DropTargetListener是拖放操作的关键,它负责对拖放操作做出相应的相应。ops可接受如下几个值。
  • DnDconstants.ACTION_COPY:表示“复制”操作的int值。
  • DnDConstants.ACTION_COPY_OR_MOVE:表示“复制”或“移动”操作的int值。
  • DnDConstants.ACTION_LINK:表示建立“快捷方式”操作的int值。
  • DnDConstants.ACTION_MOVE:表示“移动”操作的int值。
  • DnDConstants.ACTION_NONE:表示无任何操作的int值。

例如,下面代码将创建一个JFrame对象创建成拖放目标。

//将当前窗口创建成拖放目标
new DropTarget(jf,DnDConstants.ACTION_COPY,new ImageDropTargetListener());

正如从上面代码中所看到的,创建拖放目标时需要传入一个DropTargetListener监听器,该监听器负责处理用户的拖放动作。该监听器里包含如下5个事件处理器。

  • dragEnter(DropTargetDragEvent dtde):当光标进入拖放目标时将触发DropTargetListener监听器的该方法。
  • dragExit(DropTargetDragEvent dtde):当光标移除拖放目标时将触发DropTargetListener监听器的该方法。
  • dragOver(DropTargetDragEvent dtde):当光标再拖放目标上移动时将触发DropTargetListener监听器的该方法。
  • drop(DropTargetDropEvent dtde):当用户再拖放目标上松开鼠标键,拖放结束时将触发DropTargetListener监听器的该方法。
  • dropActionChanged(DropTargetDragEvent dtde):当用户在拖放目标上改变了拖放操作,例如按下或松开了Ctrl等辅助键时将触发DropTargetListener监听器的该方法。
    通常程序不想为上面每个方法提供响应,即不想重写DropTargetListener监听器的每个方法,只想重写我们关心的方法,可以通过继承DropTargetAdapter适配器来创建拖放监听器。下面程序利用拖放目标创建了一个简单的图片浏览工具,当用户把一个或多个图片文件拖入到该窗口时,该窗口将会自动打开每个图片文件。
import javax.imageio.ImageIO;
import javax.swing.*;
import javax.xml.crypto.Data;
import java.awt.*;
import java.awt.datatransfer.DataFlavor;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetAdapter;
import java.awt.dnd.DropTargetDropEvent;
import java.io.File;
import java.io.IOError;
import java.io.IOException;
import java.security.cert.CertificateNotYetValidException;public class DropTargetTest{final int DESKTOP_WIDTH = 480;final int DESKTOP_HEIGHT = 360;final int FRAME_DISTANCE = 30;JFrame jf = new JFrame("测试拖放目标——把图片文件拖入该窗口");//定义一个虚拟桌面private JDesktopPane desktop = new JDesktopPane();//保存下一个内部窗口的坐标点private int nextFrameX;private int nextFrameY;//定义内部窗口为虚拟桌面的1/2大小private int width = DESKTOP_WIDTH/2;private int height = DESKTOP_HEIGHT/2;public void init(){desktop.setPreferredSize(new Dimension(DESKTOP_WIDTH,DESKTOP_HEIGHT));//将当前窗口创建成拖放目标new DropTarget(jf, DnDConstants.ACTION_COPY,new ImageDropTargetListener());jf.add(desktop);jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);jf.pack();jf.setVisible(true);}class ImageDropTargetListener extends DropTargetAdapter {@Overridepublic void drop(DropTargetDropEvent event) {//接受复制操作event.acceptDrop(DnDConstants.ACTION_COPY);//获取拖放的内容var transferable = event.getTransferable();DataFlavor[]flavors = transferable.getTransferDataFlavors();for(var i=0;i<flavors.length;i++){DataFlavor d = flavors[i];try{//如果拖放内容的数据格式是文件列表if(d.equals(DataFlavor.javaFileListFlavor)){List fileList = (List)transferable.getTransferData(d);for(List f : fileList){showImage((File)f, event);}}}catch (Exception e){e.printStackTrace();}event.dropComplete(true);}}private void showImage(File f,DropTargetDropEvent event)throws IOException {Image image = ImageIO.read(f);if(image==null){//强制拖放操作结束停止阻塞拖放目标event.dropComplete(true);JOptionPane.showInternalMessageDialog(desktop,"系统不支持这种类型文件");//方法返回,不会继续操作return;}var icon = new ImageIcon(image);//创建内部窗口显示该图片var iframe = new JInternalFrame(f.getName(),true,true,true,true);var imageLable = new JLabel(icon);iframe.add(new JScrollPane(imageLable));desktop.add(iframe);//设置内部窗口的原始位置(内部窗口默认大小时0x0,放在0,0位置)iframe.reshape(nextFrameX,nextFrameY,width,height);//使该窗口可见,并尝试选中它iframe.show();//计算出下一个内部窗口的位置nextFrameX += FRAME_DISTANCE;nextFrameY += FRAME_DISTANCE;if(nextFrameX+width>desktop.getWidth()){nextFrameX=0;}if(nextFrameY + height > desktop.getHeight()){nextFrameX = 0;}}}public static void main(String[] args) {new DropTargetTest().init();}
}

最后几个程序有点模糊,这里看看就行了,不做细讲
下一章就是Swing了,关于Swing,Swing是AWT的升级,功能和操作比AWT更强大和顺手,在过完Swing后将又是那些理论操作了,所以AWT和Swing还是相对有趣点的,我认为这AWT和Swing是有点麻烦,而且可能实际开发中很少用到,所以这两章以后心血来潮照着API细学也不晚,看看就好!

下一章 Swing 第十二章

第十一章 AWT编程相关推荐

  1. CSAPP:第十一章 网络编程

    CSAPP:第十一章 网络编程 11.1 客户端服务器模型11.2 全球IP因特网11.3 套接字接口 11.1 客户端服务器模型   每个网络应用都是基于客户端-服务器模型.采用这个模型,一个应用是 ...

  2. 【正点原子Linux连载】第十一章 网络编程 摘自【正点原子】I.MX6U嵌入式Qt开发指南V1.0.2

    1)实验平台:正点原子阿尔法Linux开发板 2)平台购买地址:https://item.taobao.com/item.htm?id=603672744434 2)全套实验源码+手册+视频下载地址: ...

  3. Linux(b站视频兄弟连)自学笔记第十一章——shell编程

    Linux(b站视频兄弟连)自学笔记第十一章--shell基础 正则表达式 字符截取命令 cut命令 printf命令 awk命令 sed命令 字符处理命令 判断条件 流程控制 if语句 case语句 ...

  4. 第二十一章 异步编程

    异步编程的常规方法的问题是异步程序要么做完所有的事情,要么一件事也没有做完.重写所有的代码是为了保证程序不会阻塞,否则只是在浪费时间. -------Alvaro Videla & Jason ...

  5. CSAPP第十一章 网络编程

    客户端-服务器编程模型 套接字对  套接字接口 基于套接字接口的网络应用概述

  6. 尚学堂 高琪JAVA300集第十一章作业 编程题答案

    本人 JAVA初学者 在寻找这一方面的答案时没有看见 ,本着分享的精神 自己做了出来 也就传上来了 水平有限 存在有错的地方或者改进的方法 ,望大佬们可以提出 万分感谢. 1.1. 设计一个多线程的程 ...

  7. [转]Windows Shell 编程 第十一章 【来源:http://blog.csdn.net/wangqiulin123456/article/details/7987992】...

    第十一章 探索Shell 我们现在将注意力从API转向Windows Shell本身.从这一章往后,我们的目标主要集中在清晰和全面的揭示探测器的工作原理和Shell 命名空间的构成对象上,最后给出客户 ...

  8. 《Python游戏编程快速上手》第十一章猜数字,推理游戏Bagels

    <Python游戏编程快速上手>的第十一章的小游戏也非常简单,话不多少,简单介绍下: 系统随机生成几个数字,由玩家来猜,若有一个数字猜对但位置不对,输出一个Pico:若一个数字即猜对了位置 ...

  9. 程序员编程艺术第十一章:最长公共子序列(LCS)问题

    程序员编程艺术第十一章:最长公共子序列(LCS)问题 0.前言 程序员编程艺术系列重新开始创作了(前十章,请参考程序员编程艺术第一~十章集锦与总结).回顾之前的前十章,有些代码是值得商榷的,因当时的代 ...

最新文章

  1. jforum oracle报错,JForum安装在Oracle数据库上
  2. python面对对象编程------3:写集合类的三种方法
  3. django框架中的模型
  4. 树莓派之Ubuntu安装远程桌面
  5. 迭代时移除List中的元素的正确方式
  6. html日期选择框源码,日期选择控件实例源码(带节假日)
  7. 职场7条小tips,一定有一条说到你心坎里
  8. 手机如何将PDF文件拆分?分享两种手机拆分文件方法
  9. 第一次安装Microsoft SharePoint Protal Server 2003遇到的问题
  10. 今日学习在线编程题:弓形半径
  11. 三、Solr管理控制台(二)
  12. .NET JWT Token验证
  13. Linux怎么查看软件安装路径 查看mysql安装在哪
  14. NLP数据预处理的一般方法
  15. 火狐浏览器添加自动阻止加密挖矿选项
  16. 直击|国美将要进军社交电商?回应:属实
  17. 为什么时钟信号比数据信号更容易引起辐射超标
  18. 银河麒麟服务器ZYJ操作系统,文件储存inode节点占用根目录/空间满了解决办法
  19. Linux VPS服务器根据CPU负载及内存占用自动重启的bash shell脚本
  20. PHPstudy配置多个域名

热门文章

  1. 吞吐量与并发的公式,优化和参考值
  2. 云服务器除了阿里云外其他哪个比较好?
  3. 【Linux】资源查看top显示信息说明|top、iftop、iotop、htop、atop工具
  4. 【自动控制原理】根轨迹法之绘制根轨迹
  5. Python用pyexiv2读写图片元数据(EXIF、IPTC、XMP)
  6. 《c++ Primer Plus 第6版》读书笔记(4)
  7. 丰沛数_不足数_完全数
  8. 9. Data Manipulation with dplyr in R
  9. 用C语言打印一个等腰三角形
  10. Mendix与JEECG对比