11.10.2 拖放源

前面程序使用DropTarget创建了一个拖放目标,直接使用系统资源管理器作为拖放源。下面介绍如何在Java程序中创建拖放源,创建拖放源比创建拖放目标要复杂一些,因为程序需要把被拖放内容封装成Transferable对象。

创建拖放源的步骤如下。

1、调用DragSource的getDefaultDragSourceO方法获得与平台关联的DragSource对象。
2、调用DragSource对象的createDefaultDragGestureRecognizer(Component c,int actions.DragGestureListener dgl)方法将指定组件转换成拖放源。其中actions用于指定该拖放源可接受哪些拖放操作,而dgl是一个拖放监听器,该监听器里只有一个方法:dragGestureRecognized(),当系统检测到用户开始拖放时将会触发该方法。

如下代码将会把一个JLabel对象转换为拖放源。

1
2
// 将srcLabel组件转换为拖放源
dragSource.createDefaultDragGestureRecognizer(srcLabel,DnDConstants.ACTION_COPY_OR_MOVE,new MyDragGestureListener())

3、 为第2步中的DragGestureListener监听器提供实现类,该实现类需要重写该接口里包含的dragGestureRecognized()方法,该方法负责把拖放内容封装成Transferable对象。

下面程序示范了如何把一个JLabel转换成拖放源。

import java.awt.*;
import java.awt.dnd.*;
import java.awt.datatransfer.*;

import javax.swing.*;

public class DragSourceTest
{
    JFrame jf = new JFrame("Swing的拖放支持");
    JLabel srcLabel = new JLabel("Swing的拖放支持.\n"
        +"将该文本域的内容拖入其他程序.\n");
    public void init()
    {
        DragSource dragSource = DragSource.getDefaultDragSource();
        // 将srcLabel转换成拖放源,它能接受复制、移动两种操作
        dragSource.createDefaultDragGestureRecognizer(srcLabel
            , DnDConstants.ACTION_COPY_OR_MOVE
            , event -> {
            // 将JLabel里的文本信息包装成Transferable对象
            String txt = srcLabel.getText();
            Transferable transferable = new StringSelection(txt);
            // 继续拖放操作,拖放过程中使用手状光标
            event.startDrag(Cursor.getPredefinedCursor(Cursor
                .HAND_CURSOR), transferable);
        });
        jf.add(new JScrollPane(srcLabel));
        jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        jf.pack();
        jf.setVisible(true);
    }
    public static void main(String[] args)
    {
        new DragSourceTest().init();
    }
}

上面程序中粗体字代码负责把一个JLabel组件创建成拖放源,创建拖放源时指定了一个DragGestureListener对象,该对象的dragGestureRecognized)方法负责将JLabel上的文本转换成Transferable对象后继续拖放。

运行上面程序后,可以把程序窗口中JLabel标签的内容直接拖到Eclipse编辑窗口中,或者直接拖到EditPlus编辑窗口中。

除此之外,如果程序希望能精确监听光标在拖放源上的每个细节,则可以调用DragGestureEvent对象的startDrag(Cursor dragCursor,Transferable transferable,DragSourceListener dsl)方法来继续拖放操作。该方法需要一个DragSourceListener监听器对象,该监听器对象里提供了如下几个方法。

  • dragDropEnd(DragSourceDropEvent dsde):当拖放操作已经完成时将会触发该方法。
  • dragEnter(DragSourceDragEvent dsde):当光标进入拖放源组件时将会触发该方法。
  • dragExit(DragSourceEvent dse):当光标离开拖放源组件时将会触发该方法。
  • dragOver(DragSourceDragEvent dsde):当光标在拖放源组件上移动时将会触发该方法。
  • dropActionChanged(DragSourceDragEvent dsde):当用户在拖放源组件上改变了拖放操作,例如按下或松开Ctrl等辅助键时将会触发该方法。

掌握了开发拖放源、拖放目标的方法之后,如果接下来在同一个应用程序中既包括拖放源,也包括拖放目标,这样即可在同一个Java程序的不同组件之间相互拖动内容。

拖放是非常常见的操作,人们经常会通过拖放操作来完成复制、剪切功能,但这种复制、剪切操作无须剪贴板支持,程序将数据从拖放源直接传递给拖放目标。这种通过拖放实现的复制、剪切效果也被称为复制、移动。

人们在拖放源中选中一项或多项元素,然后用鼠标将这些元素拖离它们的初始位置,当拖着这些元素在拖放目标上松开鼠标按键时,拖放目标将会查询拖放源,进而访问到这些元素的相关信息,并会相应地启动一些动作。例如,从Windows资源管理器中把一个文件图标拖放到WinPad图标上,WinPad将会打开该文件。如果在Eclipse中选中一段代码,然后将这段代码拖放到另一个位置,系统将会把这段代码从初始位置删除,并将这段代码放到拖放的目标位置。

除此之外,拖放操作还可以与三种键组合使用,用以完成特殊功能。

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

11.10.1 拖放目标

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

  • DropTarget(Component 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对象创建成拖放目标。

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

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

  • dragEnter(DropTargetDragEvent dtde):当光标进入拖放目标时将触发DropTargetListener监听器的该方法。
  • dragExit(DropTargetEvent dtde):当光标移出拖放目标时将触发DropTargetListener监听器的该方法。
  • dragOver(DropTargetDragEvent dtde):当光标在拖放目标上移动时将触发DropTargetListener监听器的该方法。
  • drop(DropTargetDropEvent dtde):当用户在拖放目标上松开鼠标键,拖放结束时将触发DropTargetListener监听器的该方法。
  • dropActionChanged(Drop TargetDragEvent dtde):当用户在拖放目标上改变了拖放操作,例如按下或松开了Ctrl等辅助键时将触发DropTargetListener监听器的该方法。

通常程序不想为上面每个方法提供响应,即不想重写DropTargetListener监听器的每个方法,只想重写我们关心的方法,可以通过继承DropTargetAdapter适配器来创建拖放监听器。下面程序利用拖放目标创建了一个简单的图片浏览工具,当用户把一个或多个图片文件拖入该窗口时,该窗口将会自动打开每个图片文件。

import java.util.List;
import java.io.*;

import java.awt.Dimension;
import java.awt.Image;
import java.awt.event.*;
import java.awt.dnd.*;
import javax.imageio.*;
import java.awt.datatransfer.*;
import javax.swing.*;

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
    {
        public void drop(DropTargetDropEvent event)
        {
            // 接受复制操作
            event.acceptDrop(DnDConstants.ACTION_COPY);
            // 获取拖放的内容
            Transferable transferable = event.getTransferable();
            DataFlavor[] flavors = transferable.getTransferDataFlavors();
            // 遍历拖放内容里的所有数据格式
            for (int i = 0; i < flavors.length; i++)
            {
                DataFlavor d = flavors[i];
                try
                {
                    // 如果拖放内容的数据格式是文件列表
                    if (d.equals(DataFlavor.javaFileListFlavor))
                    {
                        // 取出拖放操作里的文件列表
                        List fileList = (List)transferable
                            .getTransferData(d);
                        for (Object 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;
            }
            ImageIcon icon = new ImageIcon(image);
            // 创建内部窗口显示该图片
            JInternalFrame iframe = new JInternalFrame(f.getName()
                , true , true , true , true);
            JLabel imageLabel = new JLabel(icon);
            iframe.add(new JScrollPane(imageLabel));
            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())
                nextFrameY = 0;
        }
    }
    public static void main(String[] args)
    {
        new DropTargetTest().init();
    }
}

上面程序中粗体字代码部分创建了一个拖放目标,创建拖放目标很简单,关键是需要为该拖放目标编写事件监听器。上面程序中采用ImageDropTargetListener对象作为拖放目标的事件监听器,该监听器重写了drop()方法,即当用户在拖放目标上松开鼠标按键时触发该方法。drop()方法里通过DropTargetDropEvent对象的getTransferable()方法取出被拖放的内容,一旦获得被拖放的内容后,程序就可以对这些内容进行适当处理,本例中只处理被拖放格式是DataFlavor.javaFileListFlavor(文件列表)的内容,处理方法是把所有的图片文件使用内部窗口显示出来。

运行该程序时,只要用户把图片文件拖入该窗口,程序就会使用内部窗口显示该图片。

注意:

上面程序中①②处的event.dropComplete(true);代码用于强制结束拖放事件,释放拖放目标的阻塞,如果没有调用该方法,或者在弹出对话框之后调用该方法,将会导致拖放目标被阻塞。在对话框被处理之前,拖放目标窗口也不能获得焦,点,这可能不是程序希望的效果,所以程序在弹出内部对话框之前强制结束本次拖放操作(因为文件格式不对),释放拖放目标的阻塞。

上面程序中只处理DataFlavor.javaFileListFlavor格式的拖放内容;除此之外,还可以处理文本格式的拖放内容,文本格式的拖放内容使用DataFlavor.stringFlavor格式来表示。

更复杂的情况是,可能被拖放的内容是带格式的内容,如text/html和text/rtf等。为了处理这种内容,需要选择合适的数据格式,如下代码所示:

1
2
3
4
5
6
7
// 如果被拖放的内容是text/html格式的输入流
if (d.isMimeTypeEqual("text/html")&& d.getRepresentationClass()==Inputstream.class) {
String charset = d.getParameter ("charset");
InputStreamReader reader = new InputStreamReader(transferable.getTransferData(d),charset);
//使用IO流读取拖放操作的内容
...
}

关于如何使用I0流来处理被拖放的内容,读者需要参考本书第15章的内容。

11.9.5 通过系统剪贴板传递Java对象

系统剪贴板不仅支持传输文本、图像的基本内容,而且支持传输序列化的Java对象和远程对象,复制到剪贴板中的序列化的Java对象和远程对象可以使用另一个Java程序(不在同一个虚拟机内的程序)来读取。DataFlavor中提供了javaSerializedObjectMimeType、javaRemoteObjectMimeType两个字符串常量来表示序列化的Java对象和远程对象的MIME类型,这两种MIME类型提供了复制对象、读取对象所包含的复杂操作,程序只需创建对应的Tranferable实现类即可。

提示:

关于对象序列化请参考本书第15章的介绍——如果某个类是可序列化的,则该类的实例可以转换成二进制流,从而可以将该对象通过网络传输或保存到磁盘上。为了保证某个类是可序列化的,只要让该类实现Serializable接口即可。

下面程序实现了一个SerialSelection类,该类与前面的ImageSelection、LocalObjectSelection实现类相似,都需要实现Tranferable接口,实现该接口的三个方法,并持有一个可序列化的对象。

import java.awt.datatransfer.*;
import java.io.*;

public class SerialSelection implements Transferable
{
    // 持有一个可序列化的对象
    private Serializable obj;
    // 创建该类的对象时传入被持有的对象
    public SerialSelection(Serializable obj)
    {
        this.obj = obj;
    }
    public DataFlavor[] getTransferDataFlavors()
    {
        DataFlavor[] flavors = new DataFlavor[2];
        // 获取被封装对象的类型
        Class clazz = obj.getClass();
        try
        {
            flavors[0] = new DataFlavor(DataFlavor.javaSerializedObjectMimeType
                + ";class=" + clazz.getName());
            flavors[1] = DataFlavor.stringFlavor;
            return flavors;
        }
        catch (ClassNotFoundException e)
        {
            e.printStackTrace();
            return null;
        }
    }
    public Object getTransferData(DataFlavor flavor)
        throws UnsupportedFlavorException
    {
        if(!isDataFlavorSupported(flavor))
        {
            throw new UnsupportedFlavorException(flavor);
        }
        if (flavor.equals(DataFlavor.stringFlavor))
        {
            return obj.toString();
        }
        return obj;
    }
    public boolean isDataFlavorSupported(DataFlavor flavor)
    {
        return flavor.equals(DataFlavor.stringFlavor) ||
            flavor.getPrimaryType().equals("application")
            && flavor.getSubType().equals("x-java-serialized-object")
            && flavor.getRepresentationClass().isAssignableFrom(obj.getClass());
    }
}

上面程序也创建了一个DataFlavor对象,该对象使用的MIME类型为"application/x-java-serialized-object;class="+clazz.getName(),它表示封装可序列化的Java对象的数据格式。

有了上面的SerialSelection类后,程序就可以把一个可序列化的对象封装成SerialSelection对象,并将该对象放入系统剪贴板中,另一个Java程序也可以从系统剪贴板中读取该对象。下面复制、读取Dog对象的程序与前面的复制、粘贴Person对象的程序非常相似,只是该程序使用的是系统剪贴板,而不是本地剪贴板。

import java.awt.BorderLayout;
import java.awt.Button;
import java.awt.Frame;
import java.awt.Label;
import java.awt.Panel;
import java.awt.TextArea;
import java.awt.TextField;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.io.Serializable;

class Dog implements Serializable
{
    private String name;
    private int age;

    public Dog(){}

    public Dog(String name , int age)
    {
        this.name = name;
        this.age = age;
    }
    // name的setter和getter方法
    public void setName(String name)
    {
        this.name = name;
    }
    public String getName()
    {
         return this.name;
    }

    // age的setter和getter方法
    public void setAge(int age)
    {
        this.age = age;
    }
    public int getAge()
    {
         return this.age;
    }
    public String toString()
    {
        return "Dog [ name=" + name + " , age=" + age + " ]";
    }
}
public class CopySerializable
{
    Frame f = new Frame("复制对象");
    Button copy = new Button("复制");
    Button paste = new Button("粘贴");
    TextField name = new TextField(15);
    TextField age = new TextField(15);
    TextArea ta = new TextArea(3 , 30);
    // 创建系统剪贴板
    Clipboard clipboard = Toolkit.getDefaultToolkit()
        .getSystemClipboard();
    public void init()
    {
        Panel p = new Panel();
        p.add(new Label("姓名"));
        p.add(name);
        p.add(new Label("年龄"));
        p.add(age);
        f.add(p , BorderLayout.NORTH);
        f.add(ta);
        Panel bp = new Panel();
        copy.addActionListener(e -> copyDog());
        paste.addActionListener(e ->
        {
            try
            {
                readDog();
            }
            catch (Exception ee)
            {
                ee.printStackTrace();
            }
        });
        bp.add(copy);
        bp.add(paste);
        f.add(bp , BorderLayout.SOUTH);
        f.pack();
        f.setVisible(true);
    }
    public void copyDog()
    {
        Dog d = new Dog(name.getText()
            , Integer.parseInt(age.getText()));
        // 把dog实例封装成SerialSelection对象
        SerialSelection ls =new SerialSelection(d);
        // 把SerialSelection对象放入系统剪贴板中
        clipboard.setContents(ls , null);
    }
    public void readDog()throws Exception
    {
        DataFlavor peronFlavor = new DataFlavor(DataFlavor
            .javaSerializedObjectMimeType + ";class=Dog");
        if (clipboard.isDataFlavorAvailable(DataFlavor.stringFlavor))
        {
            // 从系统剪贴板中读取数据
            Dog d = (Dog)clipboard.getData(peronFlavor);
            ta.setText(d.toString());
        }
    }
    public static void main(String[] args)
    {
        new CopySerializable().init();
    }
}

上面程序中的两段粗体字代码实现了复制、粘贴对象的功能,复制时将Dog对象封装成SerialSelection对象后放入剪贴板中;读取时先创建application/x-java-serialized-object;class==Dog类型的DataFlavor,然后从剪贴板中读取对应格式的内容即可。运行上面程序,在“姓名”文本框内输入字符串,在“年龄”文本框内输入数字,单击“复制”按钮,即可将该Dg对象放入系统剪贴板中。

再次运行上面程序(即启动另一个虚拟机),单击窗口中的“粘贴”按钮,将可以看到系统剪贴板中的Dog对象被读取出来,启动系统剪贴板也可以看到被放入剪贴板内的D0g对象,如图11.37所示。

图11.37访问系统剪贴板中的Dog对象

上面的Dog类也非常简单,为了让该类是可序列化的,让该类实现Serializable接口即可。读者可以参考codes\11\11.9\CopySerializable.java文件来查看Dog类的代码。

11.9.4 使用本地剪贴板传递对象引用

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

1
Clipboard clipboard = new Clipboard("cp");

创建本地剪贴板时需要传入一个字符串,该字符串是剪贴板的名字,通过这种方式允许在一个程序中创建本地剪贴板,就可以实现像Word那种多次复制,选择剪贴板粘贴的功能。

本地剪贴板是JVM负责维护的内存区,因此本地剪贴板会随虚拟机的结束而销毁。因此一旦Java程序退出,本地剪贴板中的内容将会丢失

程序示例 定义Transferable接口实现类

Java并没有提供封装对象引用Transferable实现类,因此必须自己实现该接口。实现该接口与前面的ImageSelection基本相似,一样要实现该接口的三个方法,并持有某个对象的引用。看如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import java.awt.datatransfer.*;

public class LocalObjectSelection implements Transferable {
// 持有一个对象的引用
private Object obj;

public LocalObjectSelection(Object obj) {
this.obj = obj;
}

// 返回该Transferable对象支持的DataFlavor
public DataFlavor[] getTransferDataFlavors() {
DataFlavor[] 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对象封装的数据
public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException {
if (!isDataFlavorSupported(flavor)) {
throw new UnsupportedFlavorException(flavor);
}
if (flavor.equals(DataFlavor.stringFlavor)) {
return obj.toString();
}
return obj;
}

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());
}
}

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

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

程序使用上面构造器创建了MIME类型为"application/x-java-jvm-local-objectref;class="+clazz.getName()的DataFlavor对象,它表示封装本地对象引用的数据格式。

有了上面的LocalObjectSelection封装类后,就可以使用该类来封装某个对象的引用,从而将该对象的引用放入本地剪贴板中。下面程序示范了如何将一个Person对象放入本地剪贴板中,以及从本地剪贴板中读取该Person对象。

public class CopyPerson
{
    Frame f = new Frame("复制对象");
    Button copy = new Button("复制");
    Button paste = new Button("粘贴");
    TextField name = new TextField(15);
    TextField age = new TextField(15);
    TextArea ta = new TextArea(3 , 30);
    // 创建本地的剪贴板
    Clipboard clipboard = new Clipboard("cp");
    public void init()
    {
        Panel p = new Panel();
        p.add(new Label("姓名"));
        p.add(name);
        p.add(new Label("年龄"));
        p.add(age);
        f.add(p , BorderLayout.NORTH);
        f.add(ta);
        Panel bp = new Panel();
        // 为“复制”按钮添加事件监听器
        copy.addActionListener(e -> copyPerson());
        // 为“粘贴”按钮添加事件监听器
        paste.addActionListener(e ->
        {
            try
            {
                readPerson();
            }
            catch (Exception ee)
            {
                ee.printStackTrace();
            }
        });
        bp.add(copy);
        bp.add(paste);
        f.add(bp , BorderLayout.SOUTH);
        f.pack();
        f.setVisible(true);
    }
    public void copyPerson()
    {
        // 以name,age文本框的内容创建Person对象
        Person p = new Person(name.getText()
            , Integer.parseInt(age.getText()));
        // 将Person对象封装成LocalObjectSelection对象
        LocalObjectSelection ls = new LocalObjectSelection(p);
        // 将LocalObjectSelection对象放入本地剪贴板
        clipboard.setContents(ls , null);
    }
    public void readPerson()throws Exception
    {
        // 创建保存Person对象引用的DataFlavor对象
        DataFlavor peronFlavor = new DataFlavor(
            "application/x-java-jvm-local-objectref;class=Person");
        // 取出本地剪贴板内的内容
        if (clipboard.isDataFlavorAvailable(DataFlavor.stringFlavor))
        {
            Person p = (Person)clipboard.getData(peronFlavor);
            ta.setText(p.toString());
        }
    }
    public static void main(String[] args)
    {
        new CopyPerson().init();
    }
}

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

图11.36将本地对象复制到本地剪贴板中

上面程序中使用的Person类是一个普通的Java类,该Person类包含了name和age两个成员变量,并提供了一个包含两个参数的构造器,用于为这两个Field成员变量;并重写了toString()方法,用于返回该Person对象的描述性信息。关于Person类代码可以参考codes\11\11.9\CopyPerson.java文件。

11.9.3 使用系统剪贴板传递图像

前面已经介绍了,Transferable接口代表可以放入剪贴板的传输对象,所以如果希望将图像放入剪贴板内,则必须提供一个Transferable接口的实现类,该实现类其实很简单,它封装一个image对象,并且向外表现为imageFlavor内容。

注意

JDKTransferable接口仅提供了一个StringSelection实现类,用于封装字符串内容但JDKDataFlavor类中提供了一个imageFlavor常量,用于代表图像格式的DataFlavor,并负责执行所有的复杂操作,以便进行Java图像和剪贴板图像的转换。

下面程序实现了一个ImageSelection类,该类实现了Transferable接口,并实现了该接口所包含的三个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.awt.*;
import java.awt.datatransfer.*;

public class ImageSelection implements Transferable {
private Image image;

// 构造器,负责持有一个Image对象
public ImageSelection(Image image) {
this.image = image;
}

// 返回该Transferable对象所支持的所有DataFlavor
public DataFlavor[] getTransferDataFlavors() {
return new DataFlavor[] { DataFlavor.imageFlavor };
}

// 取出该Transferable对象里实际的数据
public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException {
if (flavor.equals(DataFlavor.imageFlavor)) {
return image;
} else {
throw new UnsupportedFlavorException(flavor);
}
}

// 返回该Transferable对象是否支持指定的DataFlavor
public boolean isDataFlavorSupported(DataFlavor flavor) {
return flavor.equals(DataFlavor.imageFlavor);
}
}

有了ImageSelection封装类后,程序就可以将指定的Image对象包装成ImageSelection对象放入剪贴板中。

程序示例 java程序保存位图到剪贴板 粘贴图像到java程序中

下面程序对前面的HandDraw.java程序进行了改进,改进后的程序允许将用户手绘的图像复制到剪贴板中,也可以把剪贴板里的图像粘贴到该程序中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import java.util.*;
import java.awt.*;
import java.awt.image.*;
import java.awt.event.*;
import java.awt.datatransfer.*;

public class CopyImage {
// 系统剪贴板
private Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
// 使用ArrayList来保存所有粘贴进来的Image——就是当成图层处理
java.util.List<Image> imageList = new ArrayList<>();
// 下面代码与前面HandDraw程序中控制绘图的代码一样,省略这部分代码。
// 画图区的宽度
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("红色");
MenuItem greenItem = new MenuItem("绿色");
MenuItem blueItem = new MenuItem("蓝色");
// 定义一个BufferedImage对象
BufferedImage image = new BufferedImage(AREA_WIDTH, AREA_HEIGHT, BufferedImage.TYPE_INT_RGB);
// 获取image对象的Graphics
Graphics g = image.getGraphics();
private Frame f = new Frame("简单手绘程序");
private DrawCanvas drawArea = new DrawCanvas();
// 用于保存画笔颜色
private Color foreColor = new Color(255, 0, 0);

public void init() {
// 定义右键菜单的事件监听器。
ActionListener menuListener = e -> {
if (e.getActionCommand().equals("绿色")) {
foreColor = new Color(0, 255, 0);
}
if (e.getActionCommand().equals("红色")) {
foreColor = new Color(255, 0, 0);
}
if (e.getActionCommand().equals("蓝色")) {
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 MouseMotionAdapter() {
// 实现按下鼠标键并拖动的事件处理器
public void mouseDragged(MouseEvent e) {
// 如果preX和preY大于0
if (preX > 0 && preY > 0) {
// 设置当前颜色
g.setColor(foreColor);
// 绘制从上一次鼠标拖动事件点到本次鼠标拖动事件点的线段
g.drawLine(preX, preY, e.getX(), e.getY());
}
// 将当前鼠标事件点的X、Y坐标保存起来
preX = e.getX();
preY = e.getY();
// 重绘drawArea对象
drawArea.repaint();
}
});
// 监听鼠标事件
drawArea.addMouseListener(new MouseAdapter() {
// 实现鼠标松开的事件处理器
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);
Panel p = new Panel();
Button copy = new Button("复制");
Button paste = new Button("粘贴");
copy.addActionListener(event -> {
// 将image对象封装成ImageSelection对象
ImageSelection 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 (Exception e) {
e.printStackTrace();
}
}
});
p.add(copy);
p.add(paste);
f.add(p, BorderLayout.SOUTH);
f.pack();
f.setVisible(true);
}

public static void main(String[] args) {
new CopyImage().init();
}

class DrawCanvas extends Canvas {
// 重写Canvas的paint方法,实现绘画
public void paint(Graphics g) {
// 将image绘制到该组件上
g.drawImage(image, 0, 0, null);
// 将List里的所有Image对象都绘制出来。
for (Image img : imageList) {
g.drawImage(img, 0, 0, null);
}
}
}
}

上面程序实现图像复制、粘贴的代码也很简单,就是程序中两段粗体字代码部分:第一段粗体字代码实现了图像复制功能,将image对象封装成ImageSelection对象,然后调用ClipboardsetContents()方法将该对象放入剪贴板中;第二段粗体字代码实现了图像粘贴功能,取出剪贴板中的imageFlavor内容,返回一个Image对象,将该Image对象添加到程序的imageList集合中。

上面程序中使用了“图层”的概念,使用imageList集合来保存所有粘贴到程序中的Image-一每个Image就是一个图层,重绘Canvas对象时需要绘制imageList集合中的每个image图像。运行上面程序,当用户在程序中绘制了一些图像后,单击“复制”按钮,将看到程序将该图像复制到了系统剪贴板中,如图11.34所示。

图11.34将Java程序中的图像放入系统剪贴板中(未完待续…)

如果在其他程序中复制一块图像区域(由其他程序负责将图片放入系统剪贴板中),然后单击本程序中的“粘贴”按钮,就可以将该图像粘贴到本程序中。如图11.35所示,将其他程序中的图像复制到Java程序中。

图11.35将画图程序中的图像复制到Java程序中

11.9.2 传递文本

传递文本是最简单的情形,因为AWT已经提供了一个StringSelection用于传输文本字符串。

将文本放入剪贴板

将一段文本内容(字符串对象)放进剪贴板中的步骤如下。

1. 创建一个Clipboard实例

既可以创建系统剪贴板,也可以创建本地剪贴板。

创建系统剪贴板通过如下代码:

1
Clipboard clipboard= Toolkit.getDefaultToolkit().getSystemClipboard();

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

1
Clipboard clipboard =new Clipboard("cb");

2. 将需要放入剪贴板中的字符串封装成StringSelection对象

如下代码所示:

1
StringSelection st =new StringSelection(targetStr);

3. 调用setContents方法

调用剪贴板对象的setContents()方法将StringSelection放进剪贴板中,该方法需要两个参数,

  • 第一个参数是Transferable对象,代表放进剪贴板中的对象;
  • 第二个参数是ClipboardOwner对象,代表剪贴板数据的持有者,通常无须关心剪贴板数据的持有者,所以把第二个参数设为null
1
clipboard.setContents(st,null);

从剪贴板中取出数据

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

1
2
3
4
if (clipboard.isDataFlavorAvailable(DataFlavor.stringFlavor))
{
String content =(String)clipboard.getData(DataFlavor.stringFlavor));
}

实例

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.awt.datatransfer.*;
public class SimpleClipboard
{
private Frame f = new Frame("简单的剪贴板程序");
// 获取系统剪贴板
private Clipboard clipboard = Toolkit
.getDefaultToolkit().getSystemClipboard();
// 下面是创建本地剪贴板的代码
// Clipboard clipboard = new Clipboard("cb"); // ①
// 用于复制文本的文本框
private TextArea jtaCopyTo = new TextArea(5,20);
// 用于粘贴文本的文本框
private TextArea jtaPaste = new TextArea(5,20);
private Button btCopy = new Button("复制"); // 复制按钮
private Button btPaste = new Button("粘贴"); // 粘贴按钮
public void init()
{
Panel p = new Panel();
p.add(btCopy);
p.add(btPaste);
btCopy.addActionListener(event ->
{
// 将一个多行文本域里的字符串封装成StringSelection对象
StringSelection contents = new
StringSelection(jtaCopyTo.getText());
// 将StringSelection对象放入剪贴板
clipboard.setContents(contents, null);
});
btPaste.addActionListener(event ->
{
// 如果剪贴板中包含stringFlavor内容
if (clipboard.isDataFlavorAvailable(DataFlavor.stringFlavor))
{
try
{
// 取出剪贴板中stringFlavor内容
String content = (String)clipboard
.getData(DataFlavor.stringFlavor);
jtaPaste.append(content);
}
catch (Exception e)
{
e.printStackTrace();
}
}
});
// 创建一个水平排列的Box容器
Box box = new Box(BoxLayout.X_AXIS);
// 将两个多行文本域放在Box容器中
box.add(jtaCopyTo);
box.add(jtaPaste);
// 将按钮所在Panel、Box容器添加到Frame窗口中
f.add(p,BorderLayout.SOUTH);
f.add(box,BorderLayout.CENTER);
f.pack();
f.setVisible(true);
}
public static void main(String[] args)
{
new SimpleClipboard().init();
}
}

上面程序中“复制”按钮的事件监听器负责将第一个文本域的内容复制到系统剪贴板中,“粘贴”按钮的事件监听器则负责取出系统剪贴板中的stringFlavor内容,并将其添加到第二个文本域内。运行上面程序,将看到如图11.32所示的结果。

因为程序使用的是系统剪贴板,因此可以通过Windows的剪贴簿查看器来查看程序放入剪贴板中的内容。在Windows的“开始”菜单中运行“clipbrd”程序,将可以看到如图11.33所示的窗口。

提示:
Windows7系统已经删除了默认的剪贴板查看器,因此读者可以到WindowsXPC:\windows\system32\目录下将clipbrd.exe文件复制过来。

11.9 剪贴板

当进行复制、剪切、粘贴等Windows操作时,也许读者从未想过这些操作的实现过程。

复制 剪切 粘贴操作过程

实际上这是一个看似简单的过程:

  • 复制、剪切把一个程序中的数据放置到剪贴板中,
  • 而粘贴则读取剪贴板中的数据,并将该数据放入另一个程序中。

剪贴板的复制、剪切和粘贴的过程看似很简单,但实现起来则存在一些具体问题需要处理:假设从一个文字处理程序中复制文本,然后将这段文本复制到另一个文字处理程序中,肯定希望该文字能保持原来的风格,也就是说,剪贴板中必须保留文字原来的格式信息;如果只是将文字复制到纯文本域中,则可以无须包含文字原来的格式信息。除此之外,可能还希望将图像等其他对象复制到剪贴板中。为了处理这种复杂的剪贴板操作,数据提供者(复制、剪切内容的源程序)允许使用多种格式的剪贴板数据,而数据的使用者(粘贴内容的目标程序)则可以从多种格式中选择所需的格式。

AWT剪贴板

提示:
因为AWT的实现依赖于底层运行平台的实现,因此AWT剪贴板在不同平台上所支持的传输的对象类型并不完全相同。其中MicrosoftMacintosh的剪贴板支持传输富格式文本、图像、纯文本等数据,而X Window的剪贴板功能则比较有限,它仅仅支持纯文本的剪切和粘贴。读者可以通过查看JREjre/lib/flavormap.properties文件来了解该平台支持哪些类型的对象可以在Java程序和系统剪贴板之间传递。

AWT剪贴板分类

AWT支持两种剪贴板:本地剪贴板和系统剪贴板

  • 如果在同一个虚拟机的不同窗口之间进行数据传递,则使用AWT自己的本地剪贴板就可以了。本地剪贴板则与运行平台无关,可以传输任意格式的数据。
  • 如果需要在不同的虚拟机之间传递数据,或者需要Java程序与第三方程序之间传递数据,那就需要使用系统剪贴板了。

11.9.1 数据传递的类和接口

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

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

11.8 处理位图

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

11.8.1 Image抽象类和BufferedImage实现类

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

方法 描述
BufferedImage(int width, int height, int imageType) 创建指定大小、指定图像类型的BufferedImage对象,其中imageType可以是BufferedImage.TYPE_INT_RGBBufferedImage.TYPE_BYTE_GRAY等值。

在位图上绘图

除此之外,BufferedImage还提供了一个getGraphics()方法返回该对象的Graphics对象,从而允许通过该Graphics对象向Image中添加图形。

通过BufferedImageAWT上使用缓冲

借助BufferedImage可以在AWT中实现缓冲技术:当需要向GUI组件上绘制图形时,不要直接绘制到该GUI组件上,而是先将图形绘制到BufferedImage对象中,然后再调用组件的drawImage方法一次性地将BufferedImage对象绘制到特定组件上。

程序示例 简单的手绘程序

下面程序通过BufferedImage类实现了图形缓冲,并实现了一个简单的手绘程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;

public class HandDraw {
// 画图区的宽度
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("红色");
MenuItem greenItem = new MenuItem("绿色");
MenuItem blueItem = new MenuItem("蓝色");
// 定义一个BufferedImage对象
BufferedImage image = new BufferedImage(AREA_WIDTH, AREA_HEIGHT, BufferedImage.TYPE_INT_RGB);
// 获取image对象的Graphics
Graphics g = image.getGraphics();
private Frame f = new Frame("简单手绘程序");
private DrawCanvas drawArea = new DrawCanvas();
// 用于保存画笔颜色
private Color foreColor = new Color(255, 0, 0);

public void init() {
// 定义右键菜单的事件监听器。
ActionListener menuListener = e -> {
if (e.getActionCommand().equals("绿色")) {
foreColor = new Color(0, 255, 0);
}
if (e.getActionCommand().equals("红色")) {
foreColor = new Color(255, 0, 0);
}
if (e.getActionCommand().equals("蓝色")) {
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 MouseMotionAdapter() {
// 实现按下鼠标键并拖动的事件处理器
public void mouseDragged(MouseEvent e) {
// 如果preX和preY大于0
if (preX > 0 && preY > 0) {
// 设置当前颜色
g.setColor(foreColor);
// 绘制从上一次鼠标拖动事件点到本次鼠标拖动事件点的线段
g.drawLine(preX, preY, e.getX(), e.getY());
}
// 将当前鼠标事件点的X、Y坐标保存起来
preX = e.getX();
preY = e.getY();
// 重绘drawArea对象
drawArea.repaint();
}
});
// 监听鼠标事件
drawArea.addMouseListener(new MouseAdapter() {
// 实现鼠标松开的事件处理器
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.addWindowListener(new WindowAdapter(){
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
f.setVisible(true);
}

public static void main(String[] args) {
new HandDraw().init();
}

class DrawCanvas extends Canvas {
// 重写Canvas的paint方法,实现绘画
public void paint(Graphics g) {
// 将image绘制到该组件上
g.drawImage(image, 0, 0, null);
}
}
}

实现手绘功能其实是一种假象:表面上看起来可以随鼠标移动自由画曲线,实际上依然利用GraphicsdrawLine()方法画直线,每条直线都是从上一次鼠标拖动事件发生点画到本次鼠标拖动事件发生点。当鼠标拖动时,两次鼠标拖动事件发生点的距离很小,多条极短的直线连接起来,肉眼看起来就是鼠标拖动的轨迹了。

上面程序还增加了右键菜单来选择画笔颜色。

运行上面程序,出现一个空白窗口,用户可以使用鼠标在该窗口上拖出任意的曲线

上面程序进行手绘时只能选择红、绿、蓝三种颜色,不能调岀像Windows的颜色选择对话框那种“专业”的颜色选择工具。实际上,Swing提供了对颜色选择对话框的支持,如果结合Swing提供的颜色选择对话框,就可以选择任意的颜色进行画图,并可以提供些按钮让用户选择绘制直线、折线、多边形等几何图形。如果为该程序分别建立多个BufferedImage对象,就可实现多图层效果(每个BufferedImage代表一个图层。

11.8.2 Java9增强的ImageIO

如果希望可以访问磁盘上的位图文件,例如GIFJPG等格式的位图,则需要利用ImageIO工具类。

ImageIO利用ImageReaderImageWriter读写图形文件,通常程序无须关心该类底层的细节,只需要利用该工具类来读写图形文件即可

ImageIO类并不支持读写全部格式的图形文件,程序可以通过ImageIO类的如下几个静态方法来访问该类所支持读写的图形文件格式

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

程序示例 ImageIO所支持读写的全部文件格式

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import javax.imageio.*;

public class ImageIOTest
{
public static void main(String[] args)
{
String[] readFormat = ImageIO.getReaderFormatNames();
System.out.println("-----Image能读的所有图形文件格式-----");
for (String tmp : readFormat)
{
System.out.println(tmp);
}
String[] writeFormat = ImageIO.getWriterFormatNames();
System.out.println("-----Image能写的所有图形文件格式-----");
for (String tmp : writeFormat)
{
System.out.println(tmp);
}
}
}

运行结果

运行上面程序就可以看到Java所支持的图形文件格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
-----Image能读的所有图形文件格式-----
JPG
jpg
bmp
BMP
gif
GIF
WBMP
png
PNG
jpeg
wbmp
JPEG
-----Image能写的所有图形文件格式-----
JPG
jpg
bmp
BMP
gif
GIF
WBMP
png
PNG
jpeg
wbmp
JPEG

AWT不支持.ico图标格式

通过运行结果可以看出,AWT并不支持ico等图标格式。因此,如果需要在Java程序中为按钮、菜单等指定图标,也不要使用ico格式的图标文件,而应该使用JPGGIF等格式的图形文件

Java9增强了ImageIO的功能,ImageIO可以读写TIFF(Tag Image File Format)格式的图片。
ImageIO类包含两个静态方法:read()write(),通过这两个方法即可完成对位图文件的读写:
调用wirte方法输出图形文件时需要指定输出的图形格式,例如GIFJPEG等。

程序示例 压缩位图

下面程序可以将一个原始位图缩小成另一个位图后输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.io.*;
import java.awt.*;
import java.awt.image.*;
import javax.imageio.*;

public class ZoomImage {
// 下面两个常量设置缩小后图片的大小
private final int WIDTH = 80;
private final int HEIGHT = 60;
// 定义个BuffedImage对象,用于保存缩小后的位图
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("image/board.jpg"));
// 将原始位图缩小后绘制到image图像中
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 ZoomImage().zoom();
}
}

上面程序中先从磁盘中读取一个位图文件,然后将原始位图按指定大小绘制到Image对象中,接着将Image对象输出,这就完成了位图的缩小(实际上不一定是缩小,程序总是将原始位图缩放到WIDTHHEIGHT常量指定的大小)并输出。

缩放位图的作用

上面程序总是使用board.jpg文件作为原始图片文件,总是缩放到80×60的尺寸,且总是以当前时间作为文件名来输出该文件,这是为了简化该程序。如果为该程序增加图形界面,允许用户选择需要缩放的原始图片文件和缩放后的目标文件名,并可以设置缩放后的尺寸,该程序将具有更好的实用性。

对位图文件进行缩放是非常实用的功能,大部分Web应用都允许用户上传的图片,而Web应用则需要对用户上传的位图生成相应的缩略图,这就需要对位图进行缩放

利用ImageIO读取磁盘上的位图,然后将这图绘制在AWT组件上,就可以做出更加丰富多彩的图形界面程序。

下面程序再次改写第4章的五子棋游戏,为该游戏增加图形用户界面,这种改写很简单,只需要改变如下两个地方即可:

  • 原来是在控制台打印棋盘和棋子,现在改为使用位图在窗口中绘制棋盘和棋子。
  • 原来是靠用户输入下棋坐标,现在改为当用户单击鼠标键时获取下棋坐标,此处需要将鼠标事件的X、Y坐标转换为棋盘数组的坐标。

程序示例 AWT五子棋

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
import java.awt.*;
import javax.swing.*;
import java.awt.event.*;
import java.awt.image.*;
import javax.imageio.*;
import java.io.*;

public class Gobang {
// 下面三个位图分别代表棋盘、黑子、白子
BufferedImage table;
BufferedImage black;
BufferedImage white;
// 当鼠标移动时候的选择框
BufferedImage selected;
// 定义棋盘的大小
private static int BOARD_SIZE = 15;
// 定义棋盘宽、高多少个像素
private final int TABLE_WIDTH = 535;
private final int TABLE_HETGHT = 536;
// 定义棋盘坐标的像素值和棋盘数组之间的比率。
private final int RATE = TABLE_WIDTH / BOARD_SIZE;
// 定义棋盘坐标的像素值和棋盘数组之间的偏移距。
private final int X_OFFSET = 5;
private final int Y_OFFSET = 6;
// 定义一个二维数组来充当棋盘
private String[][] board = new String[BOARD_SIZE][BOARD_SIZE];
// 五子棋游戏的窗口
JFrame f = new JFrame("五子棋游戏");
// 五子棋游戏棋盘对应的Canvas组件
ChessBoard chessBoard = new ChessBoard();
// 当前选中点的坐标
private int selectedX = -1;
private int selectedY = -1;

public void init() throws Exception {
table = ImageIO.read(new File("image/board.jpg"));
black = ImageIO.read(new File("image/black.gif"));
white = ImageIO.read(new File("image/white.gif"));
selected = ImageIO.read(new File("image/selected.gif"));
// 把每个元素赋为"╋","╋"代表没有棋子
for (int i = 0; i < BOARD_SIZE; i++) {
for (int j = 0; j < BOARD_SIZE; j++) {
board[i][j] = "╋";
}
}
chessBoard.setPreferredSize(new Dimension(TABLE_WIDTH, TABLE_HETGHT));
chessBoard.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
// 将用户鼠标事件的坐标转换成棋子数组的坐标。
int xPos = (int) ((e.getX() - X_OFFSET) / RATE);
int yPos = (int) ((e.getY() - Y_OFFSET) / RATE);
board[xPos][yPos] = "●";
/*
* 电脑随机生成两个整数,作为电脑下棋的坐标,赋给board数组。 还涉及: 1.如果下棋的点已经有棋子,不能重复下棋。 2.每次下棋后,需要扫描谁赢了
*/
chessBoard.repaint();
}

// 当鼠标退出棋盘区后,复位选中点坐标
public void mouseExited(MouseEvent e) {
selectedX = -1;
selectedY = -1;
chessBoard.repaint();
}
});
chessBoard.addMouseMotionListener(new MouseMotionAdapter() {
// 当鼠标移动时,改变选中点的坐标
public void mouseMoved(MouseEvent e) {
selectedX = (e.getX() - X_OFFSET) / RATE;
selectedY = (e.getY() - Y_OFFSET) / RATE;
chessBoard.repaint();
}
});
f.add(chessBoard);
f.pack();
f.setVisible(true);
}

public static void main(String[] args) throws Exception {
Gobang gb = new Gobang();
gb.init();
}

class ChessBoard extends JPanel {
// 重写JPanel的paint方法,实现绘画
public void paint(Graphics g) {
// 将绘制五子棋棋盘
g.drawImage(table, 0, 0, null);
// 绘制选中点的红框
if (selectedX >= 0 && selectedY >= 0)
g.drawImage(selected, selectedX * RATE + X_OFFSET, selectedY * RATE + Y_OFFSET, null);
// 遍历数组,绘制棋子。
for (int i = 0; i < BOARD_SIZE; i++) {
for (int j = 0; j < BOARD_SIZE; j++) {
// 绘制黑棋
if (board[i][j].equals("●")) {
g.drawImage(black, i * RATE + X_OFFSET, j * RATE + Y_OFFSET, null);
}
// 绘制白棋
if (board[i][j].equals("○")) {
g.drawImage(white, i * RATE + X_OFFSET, j * RATE + Y_OFFSET, null);
}
}
}
}
}
}

省略…

使用Swing组件以避免闪烁

上面程序为了避免游戏时产生闪烁感,将棋盘所用的画图区改为继承JPanel类,游戏窗口改为使用JFrame类,这两个类都是Swing组件,Swing组件的绘图功能提供了双缓冲技术,可以避免图像闪烁

标记落子点

上面游戏界面中还有一个红色选中框,提示用户鼠标所在的落棋点,这是通过监听鼠标移动事件实现的——当鼠标在游戏界面移动时,程序根据鼠标移动事件发生的坐标来绘制红色选中框。

11.6 AWT菜单

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

11.6.1 菜单条、菜单和菜单项

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

描述
MenuBar 菜单条,菜单的容器。
Menu 菜单组件,菜单项的容器。它也是MenuItem的子类,所以可作为菜单项使用。
PopupMenu 上下文菜单组件(右键菜单组件)。
MenuItem 菜单项组件。
CheckboxMenuItem 复选框菜单项组件。
MenuShortcut 菜单快捷键组件

图11.24显示了AWT菜单组件类之间的继承、组合关系。

这里有一张图片

从图中可以看出,MenuBarMenu都实现了菜单容器接口,所以MenuBar可用于盛装Menu,而Menu可用于盛装MenuItem(包括Menu和CheckboxMenuItem两个子类对象)。

Menu还有一个子类PopupMenu,代表上下文菜单,上下文菜单无须使用Menubar盛装。

MenuMenuItem的构造器都可接收一个字符串参数,该字符串作为其对应菜单、菜单项上的标签文本。

除此之外,MenuItem还可以接收一个MenuShortcut对象,该对象用于指定该菜单的快捷键。

快捷键

MenuShortcut类使用虚拟键代码(而不是字符)来创建快捷键。

Ctrl快捷键

例如,Ctrl+A(通常都以Ctrl键作为快捷键的辅助键)快捷方式通过以下代码创建:

1
MenuShortcut ms== new MenuShortcut(KeyEvent.VK_A);

Ctrl+Shift快捷键

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

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

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

  • 调用Menu对象的addSeparator()方法来添加菜单分隔线
  • 使用添加new Menuitem("-")的方式来添加菜单分隔线

创建了MenuitemMenuMenuBar对象之后,

  • 调用Menuadd()方法将多个Menuitem组合成菜单(也可将另一个Menu对象组合进来,从而形成二级菜单),
  • 再调用MenuBaradd()方法将多个Menu组合成菜单条,
  • 最后调用Frame对象的setMenuBar方法为该窗口添加菜单条

程序示例 为窗体添加菜单条

下面程序示范了为窗口添加菜单的完整程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import java.awt.*;
import java.awt.event.*;

public class SimpleMenu {
private Frame f = new Frame("测试");
private MenuBar mb = new MenuBar();
Menu file = new Menu("文件");
Menu edit = new Menu("编辑");
MenuItem newItem = new MenuItem("新建");
MenuItem saveItem = new MenuItem("保存");
// 创建exitItem菜单项,指定使用 Ctrl+X 快捷键
MenuItem exitItem = new MenuItem("退出", new MenuShortcut(KeyEvent.VK_X));
CheckboxMenuItem autoWrap = new CheckboxMenuItem("自动换行");
MenuItem copyItem = new MenuItem("复制");
MenuItem pasteItem = new MenuItem("粘贴");
Menu format = new Menu("格式");
// 创建commentItem菜单项,指定使用 Ctrl+Shift+/ 快捷键
MenuItem commentItem = new MenuItem("注释", new MenuShortcut(KeyEvent.VK_SLASH, true));
MenuItem cancelItem = new MenuItem("取消注释");
private TextArea ta = new TextArea(6, 40);

public void init() {
// 以Lambda表达式创建菜单事件监听器
ActionListener menuListener = e -> {
String cmd = e.getActionCommand();
ta.append("单击“" + cmd + "”菜单" + "\n");
if (cmd.equals("退出")) {
System.exit(0);
}
};
// 为commentItem菜单项添加事件监听器
commentItem.addActionListener(menuListener);
exitItem.addActionListener(menuListener);
// 为file菜单添加菜单项
file.add(newItem);
file.add(saveItem);
file.add(exitItem);
// 为edit菜单添加菜单项
edit.add(autoWrap);
// 使用addSeparator方法来添加菜单分隔线
edit.addSeparator();
edit.add(copyItem);
edit.add(pasteItem);
// 为format菜单添加菜单项
format.add(commentItem);
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() {
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
f.add(ta);
f.pack();
f.setVisible(true);
}

public static void main(String[] args) {
new SimpleMenu().init();
}
}

上面程序中的菜单既有复选框菜单项菜单分隔符,也有二级菜单,并为两个菜单项添加了快捷键,为commentItem,exitltem两个菜单项添加了事件监听器。

运行该程序,并按“Ctrl+Shift+/”快捷键,将看到如图11.25所示的窗口

这里有一张图片

AWT的菜单组件不能创建图标菜单,如果希望创建带图标的菜单,则应该使用Swing的菜单组件:JMenuBarJMenuMenuItemPopupMenu组件。Swing的菜单组件和AWT的菜单组件的用法基本相似,读者可参考本程序学习使用Swing的菜单组件。

11.6.2 右键菜单

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

  • 创建PopupMenu的实例。
  • 创建多个Menuitem的多个实例,依次将这些实例加入PopupMenu中。
  • PopupMenu加入到目标组件中。
  • 为需要出现上下文菜单的组件编写鼠标监听器,当用户释放鼠标右键时弹出右键菜单。

程序示例 右键菜单

下面程序创建了一个右键菜单,该右键菜单就是“借用”前面Simplemenuedit菜单下的所有菜单项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import java.awt.*;
import java.awt.event.*;

public class PopupMenuTest {
private TextArea ta = new TextArea(4, 30);
private Frame f = new Frame("测试");
PopupMenu pop = new PopupMenu();
CheckboxMenuItem autoWrap = new CheckboxMenuItem("自动换行");
MenuItem copyItem = new MenuItem("复制");
MenuItem pasteItem = new MenuItem("粘贴");
Menu format = new Menu("格式");
// 创建commentItem菜单项,指定使用 Ctrl+Shift+/ 快捷键
MenuItem commentItem = new MenuItem("注释", new MenuShortcut(KeyEvent.VK_SLASH, true));
MenuItem cancelItem = new MenuItem("取消注释");

public void init() {
// 以Lambda表达式创建菜单事件监听器
ActionListener menuListener = e -> {
String cmd = e.getActionCommand();
ta.append("单击“" + cmd + "”菜单" + "\n");
if (cmd.equals("退出")) {
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 Panel p = new Panel();
p.setPreferredSize(new Dimension(300, 160));
// 向p窗口中添加PopupMenu对象
p.add(pop);
// 添加鼠标事件监听器
p.addMouseListener(new MouseAdapter() {
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() {
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
f.pack();
f.setVisible(true);
}

public static void main(String[] args) {
new PopupMenuTest().init();
}
}

运行上面程序,会看到如图11.26所示的窗口:

这里有一张图片

为什么Windows上的AWT的TextArea组件有默认的右键菜单

AWT并没有为GUI组件提供实现,它仅仅是调用运行平台的GUI组件来创建和平台一致的对等体。假设程序在Windows平台上运行,则程序中的TextArea实际上是Windows的多行文本域组件的对等体,具有和它相同的行为,所以该TextArea默认就具有右键菜单。

11.7 在AWT中绘图

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

11.7.1 画图的实现原理

Component类绘图相关方法

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

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

上面三个方法的调用关系为:repaint方法调用update方法;update方法调用paint方法。

Container的update方法

Component类的子类Container类中的update方法先以组件的背景色填充整个组件区域,然后调用paint方法重画组件。

Container类的update方法代码如下

1
2
3
4
5
6
7
8
9
public void update(Graphics g) {
if (isShowing()){
//以组件的背景色填充整个组件区域
if(!(peer instanceof LightweightPeer)){
g.clearRect(0,0, width, height);
paint(g);
}
}
}

普通组件的update方法则直接调用paint方法:

1
2
3
public void update(Graphics g){
paint(g);
}

图11.27显示了paintrepaintupdate三个方法之间的调用关系

这里有一张图片

重写repaint方法实现重绘 重写paint方法实现绘图

从图11.27中可以看出,程序不应该主动调用组件的paintupdate方法,这两个方法都由AWT系统负责调用。如果程序希望AWT系统重新绘制该组件,则调用该组件的repaint方法即可。而paint)和update方法通常被重写。在通常情况下,程序通过重写paint方法实现在AWT组件上绘图

11.7.2 使用Graphics类

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

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

除此之外,Graphics还提供了setColor()setFont()两个方法用于设置画笔的颜色和字体(仅当绘制字符串时有效),其中setColor()方法需要传入一个Color参数,它可以使用RGBCMYK等方式设置一个颜色;而setFont()方法需要传入一个Font参数,Font参数需要指定字体名、字体样式、字体大小三个属性。

普通AWT组件如何设置 前景色 背景色 字体

实际上,不仅Graphics对象可以使用setColor()setFont()方法来设置画笔的颜色和字体。
AWT普通组件也可以通过Color()Font()方法来改变它的前景色和字体。
除此之外,所有组件都有一个setBackground()方法用于设置组件的背景色

AWT专门提供一个Canvas类作为绘图的画布,程序可以通过创建Canvas的子类,并重写它的paint方法来实现绘图。

程序示例

下面程序示范了一个简单的绘图程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import java.awt.BorderLayout;
import java.awt.Button;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Panel;
import java.util.Random;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

public class SimpleDraw {
private final String RECT_SHAPE = "rect";
private final String OVAL_SHAPE = "oval";
private Frame f = new Frame("简单绘图");
private Button rect = new Button("绘制矩形");
private Button oval = new Button("绘制圆形");
private MyCanvas drawArea = new MyCanvas();
// 用于保存需要绘制什么图形的变量
private String shape = "";

public void init() {
Panel p = new Panel();
rect.addActionListener(e -> {
// 设置shape变量为RECT_SHAPE
shape = RECT_SHAPE;
// 重画MyCanvas对象,即调用它的repaint()方法
drawArea.repaint();
});
oval.addActionListener(e -> {
// 设置shape变量为OVAL_SHAPE
shape = OVAL_SHAPE;
// 重画MyCanvas对象,即调用它的repaint()方法
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);
f.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
}

public static void main(String[] args) {
new SimpleDraw().init();
}

class MyCanvas extends Canvas {
// 重写Canvas的paint方法,实现绘画
public void paint(Graphics g) {
Random 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()方法

运行上面程序,单击“绘制圆形”按钮,将看到如图11.28所示的窗口

这里有一张图片

运行上面程序时,如果改变窗口大小,或者让该窗口隐藏后重新显示都会导致drawArea重新绘制形状,这是因为这些动作都会触发组件的update方法。

动画

Java也可用于开发一些动画。所谓动画,就是间隔一定的时间(通常小于0.1秒)重新绘制新的图像两次绘制的图像之间差异较小,肉眼看起来就成了所谓的动画。

定时器Timer

为了实现间隔一定的时间就重新调用组件的repaint方法,可以借助于Swing提供的Timer类,Timer类是一个定时器,它有如下一个构造器

方法 描述
Timer(int delay, ActionListener listener) 每间隔delay毫秒,系统自动触发ActionListener监听器里的事件处理器,也就是actionPerformed()方法

程序示例 弹球游戏

下面程序示范了一个简单的弹球游戏,其中小球和球拍分别以圆形区域和矩形区域代替,小球开始以随机速度向下运动,遇到边框或球拍时小球反弹;球拍则由用户控制,当用户按下向左、向右键时,球拍将会向左、向右移动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import java.util.Random;
import java.awt.*;
import javax.swing.*;
import java.awt.event.*;

public class PinBall {
// 桌面的宽度
private final int TABLE_WIDTH = 300;
// 桌面的高度
private final int TABLE_HEIGHT = 400;
// 球拍的垂直位置
private final int RACKET_Y = 340;
// 下面定义球拍的高度和宽度
private final int RACKET_HEIGHT = 20;
private final int RACKET_WIDTH = 60;
// 小球的大小
private final int BALL_SIZE = 16;
private Frame f = new Frame("弹球游戏");
Random rand = new Random();
// 小球纵向的运行速度
private int ySpeed = 10;
// 返回一个-0.5~0.5的比率,用于控制小球的运行方向。
private double xyRate = rand.nextDouble() - 0.5;
// 小球横向的运行速度
private int xSpeed = (int) (ySpeed * xyRate * 2);
// ballX和ballY代表小球的坐标
private int ballX = rand.nextInt(200) + 20;
private int ballY = rand.nextInt(10) + 20;
// racketX代表球拍的水平位置
private int racketX = rand.nextInt(200);
private MyCanvas tableArea = new MyCanvas();
Timer timer;
// 游戏是否结束的旗标
private boolean isLose = false;

public void init() {
// 设置桌面区域的最佳大小
tableArea.setPreferredSize(new Dimension(TABLE_WIDTH, TABLE_HEIGHT));
f.add(tableArea);
// 定义键盘监听器
KeyAdapter keyProcessor = new KeyAdapter() {
public void keyPressed(KeyEvent ke) {
// 按下向左、向右键时,球拍水平坐标分别减少、增加
if (ke.getKeyCode() == KeyEvent.VK_LEFT) {
if (racketX > 0)
racketX -= 10;
}
if (ke.getKeyCode() == KeyEvent.VK_RIGHT) {
if (racketX < TABLE_WIDTH - RACKET_WIDTH)
racketX += 10;
}
}
};
// 为窗口和tableArea对象分别添加键盘监听器
f.addKeyListener(keyProcessor);
tableArea.addKeyListener(keyProcessor);
// 定义每0.1秒执行一次的事件监听器。
ActionListener taskPerformer = evt -> {
// 如果小球碰到左边边框
if (ballX <= 0 || ballX >= TABLE_WIDTH - BALL_SIZE) {
xSpeed = -xSpeed;
}
// 如果小球高度超出了球拍位置,且横向不在球拍范围之内,游戏结束。
if (ballY >= RACKET_Y - BALL_SIZE && (ballX < racketX || ballX > racketX + RACKET_WIDTH)) {
timer.stop();
// 设置游戏是否结束的旗标为true。
isLose = true;
tableArea.repaint();
}
// 如果小球位于球拍之内,且到达球拍位置,小球反弹
else if (ballY <= 0
|| (ballY >= RACKET_Y - BALL_SIZE && ballX > racketX && ballX <= racketX + RACKET_WIDTH)) {
ySpeed = -ySpeed;
}
// 小球坐标增加
ballY += ySpeed;
ballX += xSpeed;
tableArea.repaint();
};
timer = new Timer(100, taskPerformer);
timer.start();
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 PinBall().init();
}

class MyCanvas extends Canvas {

// 重写Canvas的paint方法,实现绘画
public void paint(Graphics g) {
// 如果游戏已经结束
if (isLose) {
g.setColor(new Color(255, 0, 0));
g.setFont(new Font("Times", Font.BOLD, 30));
g.drawString("游戏已结束!", 50, 200);
}
// 如果游戏还未结束
else {
// 设置颜色,并绘制小球
g.setColor(new Color(240, 240, 80));
g.fillOval(ballX, ballY, BALL_SIZE, BALL_SIZE);
// 设置颜色,并绘制球拍
g.setColor(new Color(80, 80, 200));
g.fillRect(racketX, RACKET_Y, RACKET_WIDTH, RACKET_HEIGHT);
}
}
}
}

运行上面程序,将看到一个简单的弹球游戏,运行效果如图11.29所示。

这里有一张图片

弹球游戏升级思路

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