AWT是可视化编程,就是说代码写上去的内容自己运行是可以直接看到的,这样就比较有趣,所以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出现了如下几个问题。
在java.awt包中提供了两种基类表示图形界面元素:Component和MenuComponent,其中Component代表一个能以图形化方式显示出来,并可与用户交互的对象,例如Button代表一个按钮,TextField代表一个文本框等;而MenuComponent则代表图形界面的菜单组件,包括MenuBar(菜单条)、MenuItem(菜单项)等子类。
除此之外,AWT图形用户界面编程里还有两个重要的概念:Container和LayoutManager,其中Container是一种特殊的Component,它代表一种容器,可以盛装普通的Component;而LayoutManager则是容器管理其他组件布局的方式。
如果从程序员的角度来看一个窗口时,这个窗口不是一个整体(有点庖丁解牛的感觉),而是由多个部分组合而成。
从上图中可以看出,任何窗口都可被分解成一个空的容器,容器里盛装了大量的基本组件,通过设置这些基本组件的大小、位置等属性,就可以将该空的容器和基本组件组成一个整体的窗口。实际上,图形界面编程非常简单,它非常类似于小朋友玩的拼图游戏,容器类似于拼图的“母版”,而普通组件(如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主要提供了如下两个主要的容器类型。
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容器具有如下几个特点。
为了使生成图形用户界面具有良好的平台无关性,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布局管理器,所有文章字默认从左向右排列,遇到边界就会折回下一行重新开始。AWT中的FlowLayout布局管理器与此完全类似,只是此时排列的是AWT组件,而不是文字。
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将容器分为EAST、SOUTH、WEST、NORTH、CENTER五个区域,普通组件可以被放置在这5个区域的任意一个中。BorderLayout布局管理器如图所示。
当改变使用BorderLayout的容器大小时,NORTH、SOUTH和CENTER区域水平调整,而EAST、WEST和CENTER区域垂直调整。使用BorderLayout有如下两个注意点。
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布局管理器的容器中添加组件时,默认从左向右、从上向下依次添加到每个网格中。与FlowLayout不同的是,放置在GridLayout布局管理器中的各组件的大小由组件所处的区域来决定(每个组件将自动沾满整个区域)。
GridLayout有如下两个构造器。
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和纵向间距4
String[]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布局管理器时的功能最强大但也最复杂,与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组件的核心类,该类具有如下几个属性。
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组件纵向扩大的权重是1
gbc.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组件定义成局部变量的方式更好。
CardLayout布局管理器以时间而非空间来管理它里面的组件,它将加入容器的所有组件看成一叠卡片,每次只有最上面的那个Component才可见。就好像一副扑克牌,它们叠在一起,每次只有最上面的一张扑克牌才可见。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容器中采用绝对定位的步骤如下。
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界面失去跨平台特性。
GridBagLayout布局管理器虽然功能强大,但它实在太复杂了,所以Swing引入了一个新的布局管理器:BoxLayout,它保留了GridBagLayout的很多优点,但是却没那么复杂。BoxLayout可以在垂直和水平两个方向上摆放GUI,BoxLayout提供了如下一个简单的构造器。
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对象。
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。
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"));
//水平方向不可拉伸的间距,其宽度为10px
horizontal.add(Box.createHorizontalStrut(10));
horizontal.add(new Button("horizeontal3"));
vertical.add(new Button("vertical1"));
vertical.add(Box.createVerticalGlue());
vertical.add(new Button("vertical2"));
//垂直方向不可拉伸的间距,其高度为10px
vertical.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只提供了一些常用的GUI组件。
AWT提供了如下基本组件
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");
//创建一个装载了文本框、按钮的Panel
var bottom = new Panel();
bottom.add(name);
bottom.add(ok);
f.add(bottom,BorderLayout.SOUTH);
//创建一个装载了下拉选择框、三个Checkbox的Panel
var checkPanel1 = new Panel();
checkPanel1.add(colorChooser);
checkPanel1.add(male);
checkPanel1.add(female);
checkPanel1.add(married);
//创建一个垂直排列组件的Box,盛装多行文本域、Panel
var topLeft = Box.createVerticalBox();
topLeft.add(ta);
topLeft.add(checkPanel1);
//创建一个水平排列组件的Box,盛装topLeft、colorList
var 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是Window类的子类,是一个容器类,属于特殊组件。对话框是可以独立存在的顶级窗口,因此用法与普通窗口的用法几乎完全一样。但对话框有如下两点需要注意。
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提供了两个方法来获取被打开/保存文件的路径。
前面介绍了如何放置各种组件,从而得到了丰富多彩的图形界面,但这些界面还不能相应用户的任何操作。比如单击前面所有窗口右上角的“x”按钮,但窗口依然不会关闭。因为在AWT编程中,所有时间必须由特定对象(事件监听器)来处理,而Frame和组件本身没有事件处理能力。
为了使图形界面能够接收用户的操作,必须给各个组件加上事件处理机制。
在事件处理的过程中,主要涉及三类对象。
从图中可以看出,当外部动作在AWT组件上进行操作时,系统会自动生成事件对象,这个事件对象是EventObject子类的实例,该事件对象会触发注册到事件源上的事件监听器。
AWT事件机制涉及三个成员:事件源、事件和事件监听器,其中事件源最容易创建,只要通过new来创建一个AWT组件,该组件就是事件源,事件是由系统自动产生的,无须程序员关心,所以,实现事件监听器是整个事件处理的核心。
事件监听器必须实现事件监听器接口,AWT提供了大量的时间监听器接口。用于监听不同类型的事件。AWT中提供了丰富的事件类,用于封装不同组件上所发生特定操作——AWT的时间类都是AWTEvent类的子类,AWTEvent是EventObject的子类。
EventObject类代表更广义的事件爱你对象,包括Swing组件上所触发的事件、数据库连接所触发的事件等。
AWT事件分为两大类:低级事件和高级事件。
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{
@Override
public 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 {
@Override
public void windowOpened(WindowEvent e) {//窗口被打开事件
ta.append("窗口初次被打开!");
}
@Override
public void windowClosing(WindowEvent e) {//窗口被关闭事件
ta.append("用户关闭窗口!");
System.exit(0);
}
@Override
public void windowClosed(WindowEvent e) {
ta.append("窗口被成功关闭!\n");
}
@Override
public void windowIconified(WindowEvent e) {
ta.append("窗口被最小化!\n");
}
@Override
public void windowDeiconified(WindowEvent e) {
ta.append("窗口恢复!");
}
@Override
public void windowActivated(WindowEvent e) {
ta.append("窗口被激活!\n");
}
@Override
public 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对象,实现事件监听器对象由如下几种形式。
使用外部类实现定义事件监听器类的形式比较少见,主要有如下两个原因。
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;
}
//实现发送邮件
@Override
public void actionPerformed(ActionEvent e) {
System.out.print("towards:"+mailAddress.getText()+"Send mail...\n");//程序向...发送邮件...
//发送邮件的真是实现
}
}
上面事件监听器类没有与任何GUI界面耦合,创建该监听器对象时传入一个TextField对象,该文本框里的字符串将被作为收件人的地址。下面程序使用了该事件监听器来监听窗口中的按钮。
这里使用了窗口事件监听器和对应的适配器(真好用)来完成关闭窗口的操作。
上面程序为“发送”按钮添加事件监听器时,将该窗口中的TextField对象传入事件监听器,从而允许事件监听器访问该文本框里的内容。运行上面程序看到如上的一系列运行界面和结果。
实际上并不推荐将业务逻辑实现写在事件监听器中,包含业务逻辑的事件监听器将导致程序的显示逻辑和业务逻辑耦合,从而增加程序后期的维护难度。如果确实哟多个事件监听器需要实现相同的业务逻辑功能,则可以考虑使用逻辑组件来定义业务逻辑功能,再让事件监听器来调用业务组件的业务逻辑方法。
类本身作为事件监听器类这种形式使用GUI界面类直接作为监听器类,可以直接再GUI界面类中定义事件处理器方法。这种形式非常简洁,也是早期AWT事件编程里比较喜欢采用的形式。但这种做法有如下两个缺点。
下面程序使用GUI界面作为事件监听器类。
上面程序让GUI界面类继承了WindowAdapter事件适配器,从而可以在该GUI界面类中直接定义事件处理器方法:windowClosing()。当为了某个组件添加该事件监听器对象时,直接使用this作为事件监听器对象即可。
大部分时候,事件处理器都没有复用价值(可复用代码通常会被抽象成业务逻辑方法),因此大部分事件监听器只是临时使用一次,所以匿名内部类形式的事件监听器更合适。实际上这种形式是目前使用最广泛的事件监听器形式。下面程序使用匿名内部类来创建事件监听器。
这里其实按钮那个监听器几乎也是这样用的:
我几乎每次都是这样写的,没有说专门做过可复用的实例对象来作为监听器使用。
还有就是关于Lambda表达式,Lambda表达式可用于函数式接口,也就是说这个监听器也可以用Lambada表达式来创建,更为简洁:
效果完全一致,这样的写法更简单和可读性更好!
还有就是上面好多东西目前文章还都没有介绍,不要着急慢慢往后看!都是要学会的东西!
前面介绍了创建GUI界面的方法:将AWT组件按某种布局摆放在容器内即可。创建AWT菜单的方式与此完全类似:将菜单条、菜单、菜单项组合在一起即可。
AWT中的菜单由如下几个类组合而成
MenuShortcut ms = new MenuShortcut(KeyEvent.VK_A);
如果该快捷键还需要Shift键辅助,则可使用如下代码。
MenuShortcut ms = new MenuShortcut(KeyEvent.VK_A,true);
有时候程序还希望对某个菜单进行分组,将功能类似的菜单分为一组,此时需要使用菜单分隔符。AWT中添加菜单分隔符有如下两种方法。
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() {
@Override
public 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对象表示,创建右键菜单步骤如下。
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() {
@Override
public 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() {
@Override
public 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默认就具有右键菜单。
很多程序如各种小游戏都需要在窗口中绘制各种图形,除此之外,即使在开发JavaEE项目时也必须“动态”地向用户端生成各种图形、图表,比如图形验证码、统计图等,这都需要利用AWT的绘图功能。
在Component类里提供了和绘图有关的三个方法。
上面三个方法的调用关系为: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还提供了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_SHAPE
shape = RECT_SHAPE;
//重画MyCanvas对象,即调用它的repait()方法
drawArea.repaint();
});
oval.addActionListener(e -> {
//设置shape变量为OVAL_SHAPE
shape=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{
@Override
public 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类是一个定时器,它有如下一个构造器。
如果仅仅绘制一些简单的几何图形,程序的图形效果依然比较单调。AWT也允许再组件上绘制位图。Craphics提供了drawImage方法用于绘制位图,该方法需要一个Image参数——代表位图,通过该方法就可以绘制出指定的位图。
Image类代表位图,但它是一个抽象类,无法直接创建Image对象,为此Java为它提供了一个BufferedImage子类,这个子类是一个可访问图像数据缓冲区的Image实现类。该类提供了一个简单的构造器,用于创建一个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对象的GRAPHICS
Graphics 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() {
@Override
public void mouseDragged(MouseEvent e) {
//如果preX和preY大于0
if(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() {
//弹出右键菜单
@Override
public void mouseReleased(MouseEvent e) {
if(e.isPopupTrigger()){
pop.show(drawArea,e.getX(),e.getY());
}
//松开鼠标是,把上一次鼠标拖动事件的X、Y坐标设为 -1
preX = -1;
preY = -1;
//约等于归零
}
});
f.add(drawArea);
f.pack();
f.setVisible(true);
f.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
}
public static void main(String[] args) {
new Demo().init();
}
class DrawCanvas extends Canvas{
//重写Canvas的paint方法,实现绘画
@Override
public void paint(Graphics g) {
//将image绘制到该组件上
g.drawImage(image,0,0,null);
}
}
}
实现手绘功能其实是一种假象:表面上看起来可以随鼠标移动自由画曲线,实际上依然利用Graphics的drawLine()方法画直线,每条直线都是从上一次鼠标拖动事件发生点画到本次鼠标拖动事件发生点。当鼠标拖动时,两次鼠标拖动事件发生点的距离很小,多条极短的直线连接起来,肉眼看起来就是鼠标拖动的轨迹了。上面程序还增加了右键菜单来选择画笔颜色。
上面程序进行手绘时只能选择红、绿、蓝三种颜色,不能调出像Windows的颜色选择对话框那种“专业”的颜色选择工具。实际上,Swing提供了颜色选择对话框的支持,如果结合Swing提供的颜色选择对话框,就可以选择任意的颜色进行画图,并可以提供一些按钮让用户绘制直线、折线、多边形等几何图形。如果为该程序分别建立多个BufferedImage对象,就可实现多图层效果(每个BufferedImage代表一个图层)。
如果希望可以访问磁盘上的位图文件,例如GIF、JPG等格式的位图。则需要利用ImageIO工具类。ImageIO利用ImageReader和ImageWriter读写图形文件,通常程序无须关心该底层的细节,只需要利用该工具类来读写图形文件即可。
ImageIO类并不支持全部格式的图形文件,程序可以通过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
jpeg
Process 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包下,下面时该包下重要的接口和类的相关说明。
传递文本是最简单的情形,因为AWT已经提供了一个StringSelection用于传输文本字符串。将一段文本内容(字符串对象)放进剪贴板中的步骤如下。
var clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
创建本地剪贴板通过如下代码:
var clipboard = new Clipboard("cb");
var st = new StringSelection(targetStr);
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
@Override
public DataFlavor[] getTransferDataFlavors() {
return new DataFlavor[]{DataFlavor.imageFlavor};
}
//返回该Transferable对象是否支持指定的DataFlavor
@Override
public boolean isDataFlavorSupported(DataFlavor flavor) {
return flavor.equals(DataFlavor.imageFlavor);
}
//取出该Transferable对象里实际的数组
@Override
public 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对象的GRAPHICS
Graphics 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() {
@Override
public void mouseDragged(MouseEvent e) {
//如果preX和preY大于0
if (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() {
//弹出右键菜单
@Override
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger()) {
pop.show(drawArea, e.getX(), e.getY());
}
//松开鼠标是,把上一次鼠标拖动事件的X、Y坐标设为 -1
preX = -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() {
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
}
public static void main(String[] args) {
new CopyImage().init();
}
class DrawCanvas extends Canvas{
@Override
public 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;
}
@Override
public 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对象封装的数据
@Override
public 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());
}
@Override
public 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对象可以使用如下构造器。
程序上面使用构造器创建了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中选中一段代码,然后将这段代码拖放到另一个位置,系统将会把这段代码从初始位置删除,并将这段代码放到拖放的目标位置。
除此之外,拖放操作还可以与三种键组合使用,用以完成特殊功能。
在拖放操作中,数据从拖放源直接传递给拖放目标,因此拖放操作主要涉及两个对象:拖放源和拖放目标。AWT已经提供了对拖放源和拖放目标的支持,分别由DragSource和DropTarget两个类来表示。下面将具体介绍如何在程序中建立拖放源和拖放目标。
实际上,拖放操作与前面介绍的剪贴板有一定的类似之处,它们之间的差别在于:拖放操作将数据从拖放源直接传递给拖放目标,而剪贴板操作则是先将数据传递到剪贴板上,然后再从剪贴板传递给目标。剪贴板操作中被传递的内容使用Transferable接口来封装,与此类似的时,拖放操作中被传递的内容也使用Transferable来封装;剪贴板操作中被传递的数据格式使用DataFlavor来表示,拖放操作中同样使用DataFlavor来表示被传递的数据格式。
再GUI界面中创建拖放目标非常简单,AWT提供了DropTarget类来表示拖放目标,可以通过该类提供的如下构造器来创建一个拖放目标。
例如,下面代码将创建一个JFrame对象创建成拖放目标。
//将当前窗口创建成拖放目标
new DropTarget(jf,DnDConstants.ACTION_COPY,new ImageDropTargetListener());
正如从上面代码中所看到的,创建拖放目标时需要传入一个DropTargetListener监听器,该监听器负责处理用户的拖放动作。该监听器里包含如下5个事件处理器。
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 {
@Override
public 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细学也不晚,看看就好!