第十一章AWT编程(2)

Java学习笔记

AWT编程

AWT菜单

  • 前面介绍了创建GUI界面的方式:将AWT组件按某种布局摆放在容器中即可。创建AWT菜单的方式与此完全类似:将菜单条、菜单、菜单项组合在一起即可。
菜单条、菜单和菜单项
  • AWT的菜单由如下几个类组合而成
    • MenuBar:菜单条、菜单的容器
    • Menu:菜单组件,菜单项的容器。它也是MenuItem的子类,所以可作为菜单项使用
    • PopupMenu:上下文菜单组件(右键菜单组件)
    • MenuItem:菜单项组件
    • CheckboxMenuItem:复选框菜单组件
    • MenuShortcut:菜单快捷键组件
  • Menu、MenuItem的构造器都可接受一个字符串参数,该字符串作为其对应菜单、菜单项上的标签文本。除此之外,MenuItem还可以接受一个MenuShortcut对象,该对象用于指定该菜单的快捷键。MenuShortcut类使用虚拟键代码来创建快捷键。例如,Ctrl+A:
MenuShortcut ms = new MenuShortcut(KeyEvent.VK_A);
  • 如果该快捷键还需要Shift键的帮助,则可使用如下代码:
MenuShortcut ms = new MenuShortcut(KeyEvent.VK_A, true);
  • 有时候程序还希望对某个菜单进行分组,将功能相似的菜单分成一组,此时需要使用菜单分隔符。AWT中添加菜单分隔符有如下两种用法。
    • 调用Menu对象的addSeparator()方法来添加菜单分隔符
    • 使用添加new MenuItem("-")的方式来添加菜单分隔符
  • 创建了MenuItem、Menu和MenuBar对象之后,调用Menu的add()方法将多个MenuItem组合成菜单(也可将另一个Menu组合进来,形成二级菜单),在调用MenuBar的add()方法将多个Menu组合成菜单条,最后调用Frame对象的setMenuBar()方法为该窗口添加菜单条。
import javax.swing.*;
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 SimpleMenu {
    private Frame f = new Frame("test");
    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("-"));
        edit.add(format);
        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 SimpleMenu().init();
    }
}

第十一章AWT编程(2)_第1张图片

右键菜单
  • 右键菜单使用PopupMenu对象表示,创建右键菜单的步骤如下:
    • 创建PopupMenu的实例
    • 创建多个MenuItem的多个实例,依次将这些实例加入PopupMenu中
    • 将PopupMenu加入到目标组件中
    • 为需要出现上下文菜单的组件编写鼠标监听器,当用户释放鼠标右键时弹出菜单
  • 下面程序创建了一个右键菜单,该右键菜单就是“借用”前面SimpleMenu中的edit菜单下的所有菜单项。
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("格式");
    MenuItem commentItem = new MenuItem("注释",
            new MenuShortcut(KeyEvent.VK_SLASH, true));
    MenuItem cancelItem = new MenuItem("取消注释");
    public void init()
    {
        ActionListener menuListener = e ->
        {
            String cmd = e.getActionCommand();
            ta.append("单击“"+cmd+"”菜单"+"\n");
            if(cmd.equals("退出"))
            {
                System.exit(0);
            }
        };
        commentItem.addActionListener(menuListener);
        pop.add(autoWrap);
        pop.addSeparator();
        pop.add(copyItem);
        pop.add(pasteItem);
        format.add(commentItem);
        format.add(cancelItem);

        pop.add(new MenuItem("-"));
        pop.add(format);
        Panel p = new Panel();
        p.setPreferredSize(new Dimension(300, 160));
        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 PopupMenuTest().init();
    }
}

在AWT中绘图

  • 很多程序如各种小游戏都需要在窗口中绘制各种图形,除此之外,即使在开发Java EE项目时,有时候也必须“动态”的地向客户端生成各种图形、图标,比如图形验证码、统计图等。这都需要利用AWT的绘图功能。
画图的实现原理
  • 在Component类里提供了和绘图有关的三个方法
    • paint(Graphics g):绘制组件的外观
    • update(Graphics g):调用paint()方法,刷新组件外观
    • repaint():调用update()方法,刷新组件外观
  • 上面三个方法的调用关系为:repaint()方法调用update()方法,update()方法调用paint()方法。
  • Container类中的update()方法先以组件的背景色填充整个组件区域,然后调用paint()方法重画组件,Container类的update()方法代码如下
public void update(Graphics g) {
    if (isShowing()) {
        //以组件的背景色填充整个组件区域
        if (! (peer instanceof LightweightPeer)) {
            g.clearRect(0, 0, width, height);
        }
        paint(g);
    }
}
  • 普通组件的update()方法则是直接调用paint()方法。
public void update(Graphics g) {
    paint(g);
}
  • 如果程序希望AWT系统重新绘制组件,调用该组件的repaint()方法即可。而paint()和update()方法通常被重写。在通常情况下,程序通过重写paint()方法实现在AWT组件上绘图。
  • 重写update()或paint()方法时,该方法里包含了一个Graphics类型的参数,通过该参数就可以实现绘图功能。
使用Graphics类
  • Graphics是一个抽象的画笔对象,Graphics可以在组件上绘制丰富多彩的几何图形和位图。Graphics类提供了如下几个方法用于绘制几何图形和位图。
    • drawLine():绘制直线
    • drawString():绘制字符串
    • drawRect():绘制矩形
    • drawRoundRect():绘制圆角矩形
    • drawOval():椭圆
    • drawPolygon():多边形边框
    • drawArc():圆弧
    • drawPolyline():折线
    • fillRect():填充矩形
    • fillRoundRect():填充圆角矩形
    • fillOval():椭圆
    • fillPolygon():多边形
    • fillArc():填充圆弧和其两个端点到中心连线所包围的区域
    • drawImage():绘制位图
  • 除此之外,Graphics还提供饿了setColor()和setFont()两个方法用于设置画笔的颜色和字体(仅当绘制字符串时有效),其中前者需要传入一个Color参数,可以使用RGB、CMYK等方式设置一个颜色;而后者需要传入一个Font参数,Font参数需要指定字体名、字体样式、字体大小三个属性。

AWT普通组件也可以通过Color()和Font()方法改变它的前景色和字体。且所有组件都有一个setBackground()方法用于设置组件的背景色。

  • AWT专门提供了一个Canvas类作为绘图的画布,程序可以通过创建Canvas的子类,并重写其paint()方法来实现绘图。
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.Random;

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 = "";

    class MyCanvas extends Canvas
    {
        //重写方法,实现绘画
        public void paint(Graphics g)
        {
            Random rand = new Random();
            if (shape.equals(RECT_SHAPE))
            {
                //设置画笔颜色
                g.setColor(new Color(rand.nextInt(255), rand.nextInt(255), rand.nextInt(255)));
                //随机地绘制一个矩形框
                g.drawRect(rand.nextInt(200),
                        rand.nextInt(120), 40, 60);
            }
            if (shape.equals(OVAL_SHAPE))
            {
                //设置画笔颜色
                g.setColor(new Color(rand.nextInt(255), rand.nextInt(255), rand.nextInt(255)));
                g.fillOval(rand.nextInt(200),
                        rand.nextInt(120), 50, 40);
            }
        }
    }
    public void init()
    {
        Panel p = new Panel();
        rect.addActionListener(e ->
        {
            shape = RECT_SHAPE;
            drawArea.repaint();
        });
        oval.addActionListener(e ->
        {
            shape = OVAL_SHAPE;
            drawArea.repaint();
        });
        p.add(rect);
        p.add(oval);
        drawArea.setPreferredSize(new Dimension(250, 180));
        f.add(drawArea);
        f.add(p, BorderLayout.SOUTH);
        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 SimpleDraw().init();
    }
}
  • Java也可用于开发一些动画。所谓动画,就是间隔一定时间(通常小于0.1秒)重新绘制新的图像,两次绘制的图像之间差异较小,肉眼看上去就成了所谓的动画。为了实现间隔一定的时间就重新调用组件的repaint()方法,可以借助于Swing提供的Timer类,Timer类是一个定时器,它有如下一个构造器:
    • Timer(int delay, ActionListener listener):每间隔delay毫秒,系统自动触发ActionListener监听器里的事件处理器(actionPerformed()方法)
  • 下面程序示范了一个简单地弹球游戏,其中小球和球拍分别以圆形区域和矩形区域代替,小球开始以随机速度向下运动,遇到边框或球拍时小球反弹;球拍则由用户控制,当用户按下向左、向右键时,球拍将会向左、向右移动。
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.Random;

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("love you~");
    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 int score = 0;

    //游戏是否结束的旗标
    private boolean isLose = false;

    public void init()
    {
        //设置桌面区域的最佳大小
        tableArea.setPreferredSize(new Dimension(TABLE_WIDTH, TABLE_HEIGHT));
        f.add(tableArea);
        //定义键盘监听器
        KeyAdapter keyProcessor = new KeyAdapter() {
            @Override
            public void keyPressed(KeyEvent e) {
                if (e.getKeyCode() == KeyEvent.VK_LEFT)
                {
                    if(racketX > 0)
                        racketX -= 10;
                }
                if (e.getKeyCode() == KeyEvent.VK_RIGHT)
                {
                    if(racketX < TABLE_WIDTH-RACKET_WIDTH)
                        racketX += 10;
                }
            }
        };
        //增加“×”关闭
        f.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(0);
            }
        });
        //为窗口和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();
                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();
            score ++;
        };
        timer = new Timer(100, taskPerformer);
        timer.start();
        f.pack();
        f.setVisible(true);
    }

    class MyCanvas extends Canvas
    {
        //重写paint方法
        public void paint(Graphics g)
        {
            if (isLose) {
                g.setColor(new Color(255,0,0));
                g.setFont(new Font("Times", Font.BOLD, 25));
                g.drawString("游戏结束!\n你的得分是"+score , 50, 200 );
            }
            else
            {
                g.setColor(new Color(222, 118, 222, 255));
                g.fillOval(ballX,ballY,BALL_SIZE,BALL_SIZE);
                //设置颜色,并绘制球拍
                g.setColor(new Color(80, 80, 200, 200));
                g.fillRect(racketX, RACKET_Y, RACKET_WIDTH, RACKET_HEIGHT);
            }
        }
    }

    public static void main(String[] args) {
        new PinBall().init();
    }
}
  • 运行时会有轻微的闪烁,这是因为AWT组件的绘图没有采用双缓冲技术,当重写paint()方法来绘制图形时,所有图形都是直接绘制在GUI组件上的,所以多次重新调用paint()方法进行绘制会发生闪烁现象。使用Swing组件就可避免这种闪烁,Swing组件没有提供Canvas对应的组件,使用Swing的Panel组件作为画布即可。

处理位图

  • 如果仅仅绘制一些简单的几何图形,程序的图形效果依然比较单调。AWT也允许在组件上绘制位图,Graphics提供了drawImage方法用于控制位图,该方法需要一个Image参数——代表位图,通过该方法就可以绘制出指定的位图
Image抽象类和BufferedImage实现类
  • Image类代表位图,但他是一个抽象类,无法直接创建Image对象,为此Java为他提供了一个BufferedImage子类,这个子类是一个可访问图像数据缓冲区的Image实现类。该类提供了一个简单的构造器,用于创建一个BufferedImage对象。
    • BufferedImage(int width, int height, int imageType):创建指定大小、指定图像类型的BufferedImage对象,其中imageType可以使BufferedImage.TYPE_INT_RGB、BufferedImage.TYPE_BYTE_GRAY等值。
  • 除此之外,BufferedImage还提供了一个getGraphics()方法返回该对象的Graphics对象,从而允许通过改Graphics对象向Image中添加图形
  • 借助BufferedImage可以在AWT中实现缓冲技术——当需要向GUI组件上绘制图形时,不要直接绘制到该GUI组件上,而是先将图形绘制到BufferedImage对象中,然后再调用组件的drawImage一次性地将BufferedImage对象绘制在特定组件上。
  • 下面程序通过BufferedImage类实现了图形缓冲,并实现了一个简单的手绘程序。
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;

public class 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("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("简单手绘程序");
    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);
        blueItem.addActionListener(menuListener);
        greenItem.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() {
            @Override
            public void mouseDragged(MouseEvent e) {
                if(preX > 0 && preY > 0)
                {
                    g.setColor(foreColor);
                    g.drawLine(preX, preY, e.getX(), e.getY());
                }
                preX = e.getX();
                preY = e.getY();
                drawArea.repaint();
            }
        });

        drawArea.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseReleased(MouseEvent e) {
                if (e.isPopupTrigger())
                {
                    pop.show(drawArea, e.getX(), e.getY());
                }
                preX = -1;
                preY = -1;
            }
        });
        f.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(0);
            }
        });
        f.add(drawArea);
        f.pack();
        f.setVisible(true);
    }

    class DrawCanvas extends Canvas
    {
        //重写paint
        @Override
        public void paint(Graphics g)
        {
            g.drawImage(image, 0, 0, null);
        }
    }

    public static void main(String[] args) {
        new HandDraw().init();
    }
}
Java 9增强的ImageIO
  • 如果希望可以访问磁盘上的位图文件,例如GIF、JPG等格式的位图,则需要利用ImageIO工具类。ImageIO利用ImageReader和ImageWriter读写图形文件,通常程序无需关心该类底层细节,只需要利用该工具类来读写图形文件即可。
  • ImageIO类并不支持读写全部格式的图形文件,程序可以通过ImageIO类的如下几个静态方法来访问该类所支持读写的图形文件格式
    • static String[] getReaderFileSuffixes():返回一个String数组,该数组列出ImageIO所有能读的图形文件的文件后缀
    • static String[] getReaderFormatNames():返回一个String数组,该数组列出ImageIO所有能读的图形文件的非正式格式名称
    • static String[] getWriterFileSuffixes():返回一个String数组,该数组列出ImageIO所有能写的图形文件的文件后缀
    • static String[] getWriterFormatNames():返回一个String数组,该数组列出ImageIO所有能写的图形文件的非正式格式名称。
import javax.imageio.ImageIO;

public class ImageIOTest {
    public static void main(String[] args) {
        String[] readFormat = ImageIO.getReaderFormatNames();
        for (String tmp:readFormat) {
            System.out.println(tmp);
        }
        System.out.println("--------");
        String[] writeFormat = ImageIO.getWriterFormatNames();
        for (String tmp:writeFormat) {
            System.out.println(tmp);
        }
    }
}
  • 输出结果如下:
lancibe@lancibe-PC:~/java/test/src/chapter11/src$ java ImageIOTest 
JPG
jpg
tiff
bmp
BMP
gif
GIF
WBMP
png
PNG
JPEG
tif
TIF
TIFF
wbmp
jpeg
--------
JPG
jpg
tiff
bmp
BMP
gif
GIF
WBMP
png
PNG
JPEG
tif
TIF
TIFF
jpeg
wbmp
  • ImageIO类包含两个静态方法,read()和write(),通过这两个方法即可完成对位图的读写,调用write()方法输出图形文件时需要指定输出的图像格式,例如GIF、JPEG等。下面程序可以将一个原始位图缩小成另一个位图后输出。
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;

public class ZoomImage {
    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("image/board.jpg"));
        g.drawImage(srcImage, 0, 0, WIDTH, HEIGHT, null);
        ImageIO.write(image, "jpeg", new File(System.currentTimeMillis() + ".jpg"));
    }

    public static void main(String[] args)
            throws Exception
    {
        new ZoomImage().zoom();
    }
}
  • 利用ImageIO读取磁盘上的位图,然后将这些位图绘制在AWT组件上,就可以做出更加丰富多彩的图形界面程序。

剪切板

  • 当进行复制、剪切、粘贴等简单操作时,它们的实现过程是一个看似简单的过程:复制、剪切把一个程序中的数据放置到剪贴板中,而粘贴则是读取剪贴板中的内容数据,并将该数据放入另一个程序中。
  • 剪贴板的复制、剪切、粘贴过程看似简单,但实现起来则存在一些具体问题——假设从一个文字处理程序中复制文本,然后将这段文本复制到另一个文字处理程序中,肯定希望该文字能够保持原来的风格,也就是说,剪贴板中必须保留文字原来的格式信息;如果只是将文字复制到纯文本域中,则可以无需包含文字原来的格式信息。除此之外,可能还希望将图像等其他对象复制到剪贴板中。为了处理这种复杂的剪贴板操作,数据提供者(复制、剪切内容的源程序)允许使用多种格式的剪贴板数据,而数据的使用者(粘贴内容的目标程序)则可以从多种格式中选择所需的格式。
  • AWT支持两种剪贴板:本地剪贴板和系统剪贴板。如果在同一个虚拟机的不同窗口之间进行数据传递,则使用AWT自己的本地剪贴板就可以了。本地剪贴板与运行平台无关,可以传输任意格式的数据,如果需要在不同的虚拟机之间传递数据,或者需要在Java程序与第三方程序之间传递数据,那就要使用系统剪贴板了。
数据传递的类和接口
  • AWT中剪贴板相关操作的接口和类被放在java.awt.datatransfer包下,下面是该包下重要的接口和类的相关说明。
    • Clipboard:代表一个剪贴板实例,这个剪贴板既可以是本地剪贴板也可以是系统剪贴板
    • ClipboardOwner:剪贴板内容的所有者接口,当剪贴板内容的所有权被修改时,系统将会触发该所有者的lostOwnership事件处理器
    • Transferable:该接口的实例代表放进剪贴板中的传输对象
    • DataFlaver:用于表述剪贴板中的数据格式
    • StringSelection:Transferable的实现类,用于传输文本字符串。
    • FlavorListener:数据格式监听器接口
    • FlavorEvent:该类的实例封装了数据格式改变的事件。
传递文本
  • 传递文本是最简单的情形,因为AWT以及提供了一个StringSelection用于传输文本字符串。将一段文本内容(字符串对象)放进剪贴板的步骤如下:

    1. 创建一个Clipboard实例,创建系统剪贴板如下:

      Clipboard clipboard = Toolkit.getDefaultToolkit.getSystemClipboard();
      

      创建本地剪贴板如下:

      Clipboard clipboard = new Clipboard();
      
    2. 将需要放入剪贴板中的字符串封装成StringSelection对象:

      StringSelection st = new StringSelection(targetStr);
      
    3. 调用剪贴板对象的setContents()方法将StringSelection对象放入剪切板中,该方法需要两个参数,第一个时Transferable对象,第二个是ClipboardOwner对象,代表剪贴板数据的所有者,通常我们无须关心剪贴数据的所有者,设为null。

      clipboard.setContents(st, null);
      
  • 从剪贴板中取出数据则比较简单,调用Clipboard对象的getData(DataFlavor flavor)方法即可取出剪贴板中指定格式的内容,如果指定flavor的数据不存在,该方法将引发UnsupportedFlavorException异常。为避免出现异常,应先使用Clipboard对象的isDataFlavorAvailable(DataFlavor flavor)方法作为if的判据来判断指定的flavor的数据是否存在。

if (clipboard.isDataFlavorAvailable(DataFlavor.stringFlavor))
    String content = (String)clipboard.getData(DataFlavor.stringFlavor)
  • 下面程序利用系统剪贴板进行复制、粘贴的简单程序。
import javax.swing.*;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;

public class SimpleClipboard {
    private Frame f = new Frame("test");
    private Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
    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());
            //将该对象放入剪贴板
            clipboard.setContents(contents, null);
        });
        btPaste.addActionListener(e ->
        {
            if(clipboard.isDataFlavorAvailable(DataFlavor.stringFlavor))
            {
                try
                {
                    String content = (String)clipboard.getData(DataFlavor.stringFlavor);
                    jtaPaste.append(content);
                }
                catch (Exception ex)
                {
                    ex.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.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(0);
            }
        });
        f.pack();
        f.setVisible(true);
    }

    public static void main(String[] args) {
        new SimpleClipboard().init();
    }
}
使用系统剪贴板传递图像
  • 前面介绍过,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;
    //构造器,负责持有一个Image对象
    public ImageSelection(Image image)
    {
        this.image = image;
    }
    //返回该Transferable对象所持有的所有DataFlavor
    @Override
    public DataFlavor[] getTransferDataFlavors()
    {
        return new DataFlavor[]{DataFlavor.imageFlavor};
    }
    //取出该Transferable对象里实际的数据
    @Override
    public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
        if(flavor.equals(DataFlavor.imageFlavor))
            return image;
        else
            throw new UnsupportedFlavorException(flavor);
    }
    //返回该Transferable对象是否支持指定的DataFlavor
    @Override
    public boolean isDataFlavorSupported(DataFlavor flavor) {
        return flavor.equals(DataFlavor.imageFlavor);
    }
}
  • 有了ImageSelection封装类之后,程序就可以将指定的Image对象包装成ImageSelection对象放入剪贴板中。下面程序对前面的HandDraw程序进行了改进,改进后的程序允许将用户手绘的图像复制到剪贴板中,也可以把剪贴板中的图像粘贴到该程序中。
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.List;

public class CopyImage {
    //系统剪贴板
    private Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
    //使用ArrayList数据结构来保存所有粘贴进来的Image——就是当成图层处理
    List<Image> imageList = new ArrayList<Image>();
    //画图区的宽度
    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("简单手绘程序");
    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);
        blueItem.addActionListener(menuListener);
        greenItem.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() {
            @Override
            public void mouseDragged(MouseEvent e) {
                if(preX > 0 && preY > 0)
                {
                    g.setColor(foreColor);
                    g.drawLine(preX, preY, e.getX(), e.getY());
                }
                preX = e.getX();
                preY = e.getY();
                drawArea.repaint();
            }
        });

        drawArea.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseReleased(MouseEvent e) {
                if (e.isPopupTrigger())
                {
                    pop.show(drawArea, e.getX(), e.getY());
                }
                preX = -1;
                preY = -1;
            }
        });
        f.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(0);
            }
        });
        f.add(drawArea);
        Panel p = new Panel();
        Button copy = new Button("复制");
        Button paste = new Button("粘贴");
        copy.addActionListener(e -> {
            //将image对象封装成ImageSelection对象
            ImageSelection contents = new ImageSelection(image);
            //将ImageSelection对象放入剪贴板
            clipboard.setContents(contents, null);
        });
        paste.addActionListener(e -> {
            //如果剪贴板中包含imageFlavor内容
            if (clipboard.isDataFlavorAvailable(DataFlavor.imageFlavor))
            {
                try
                {
                    //取出剪贴板中的imageFlavor内容,并将其添加到List集合中
                    imageList.add((Image)clipboard.getData(DataFlavor.imageFlavor));
                    drawArea.repaint();
                }
                catch (Exception exception)
                {
                    exception.printStackTrace();
                }
            }
        });
        p.add(copy);
        p.add(paste);
        f.add(p, BorderLayout.SOUTH);
        f.pack();
        f.setVisible(true);
    }

    class DrawCanvas extends Canvas
    {
        @Override
        public void paint(Graphics g) {
            g.drawImage(image, 0, 0, null);
            for(Image img:imageList)
                g.drawImage(img, 0, 0, null);
        }
    }

    public static void main(String[] args) {
        new CopyImage().init();
    }
}
通过系统剪贴板传递Java对象
  • 系统剪贴板不仅支持传输文本、图像的基本内容,而且支持传输序列化的Java对象和远程对象,复制到剪贴板中的序列化的Java对象和远程对象可以使用另一个Java程序(不在同一个虚拟机内的程序)来读取。DataFlavor中提供了javaSerializedObjectMimeType、javaRemoteObjectMimeType两个字符串常量来表示序列化的Java对象和远程对象的MIME类型,这两种MIME,类型提供了复制对象、读取对象所包含的复杂操作,程序只需创建对应的Transferable实现类即可。
  • 下面程序实现了一个SerialSelection类,该类与前面的ImageSelection、LocalObjectSelection实现类相似,都需要实现Transferable接口,实现该接口的三个方法,并持有一个可序列化对象。
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.IOException;
import java.io.Serializable;

public class SerialSelection implements Transferable {
    //持有一个可序列化对象
    private Serializable obj;
    //创建该类对象时,传入被持有的对象
    public SerialSelection(Serializable obj)
    {
        this.obj = obj;
    }

    @Override
    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;
        }
    }

    @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;
    }

    @Override
    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对象的程序。
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.Serializable;

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.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(0);
            }
        });
        f.pack();
        f.setVisible(true);
    }
    public void copyDog()
    {
        Dog d = new Dog(name.getText(), Integer.parseInt(age.getText()));
        SerialSelection ls = new SerialSelection(d);
        clipboard.setContents(ls, null);
    }
    public void readDog()
    {
        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();
    }
}

拖放功能

  • 拖放是常见的操作,可以完成复制、剪切功能,但这种复制剪切操作无需剪贴板支持,程序将数据从拖放源直接传递给拖放目标。这种通过拖放实现的复制、剪切效果也被称为复制、移动。
  • 拖放操作还可以与三种键组合使用,用以完成特殊功能。
    • 与Ctrl键组合使用:表示该拖放操作完成复制功能
    • 与Shift键组合使用:表示该拖放操作完成移动功能
    • 与Ctrl、Shift键组合使用:表示为目标对象建立快捷方式。(在UNIX平台上称为链接)
    • 在拖放操作中,数据从拖放源直接传递给拖放目标,因此拖放操作主要涉及两个对象:拖放源和拖放目标。AWT已经提供了对拖放源和拖放目标的支持,分别由DragSource和DropTarget两个类来表示。
拖放目标
  • 在GUI界面中创建拖放目标非常简单,AWT提供的DropTarget类来表示拖放目标,可以表示该类提供的如下构造器来创建一个拖放目标。
    • DropTarget(Component c, int ops, DropTargetListener dtl):将c组件创建成一个拖放目标,该拖放目标默认可接受ops值所指定的拖放操作。其中DropTargetListener是拖放操作的关键,他负责对拖放操作作出相应的响应。ops可接受如下几个值
      • DnDconstants.ACTION_COPY:表示复制
      • DnDconstants.ACTION_COPY_OR_MOVE:表示复制或移动
      • DnDconstants.ACTION_LINK:表示建立快捷方式
      • DnDconstants.ACTION_MOVE:表示移动
      • DnDconstants.ACTION_NONE:表示无任何操作
  • 例如,下面代码将一个JFrame对象创建成拖放目标
//将当前窗口创建成拖放目标
new DropTarget(jf, DnDconstants.ACTION_COPY, new ImageDropTargetListener());
  • 正如上面代码提到的,创建拖放目标时需要传入一个DropTargetListener监听器,该监听器负责处理用户的拖放动作。该监听器包含如下五种事件处理器。
    • dragEnter(DropTargetDragEvent dtde):当光标进入拖放目标时触发DropTargetListener监听器的该方法
    • dragExit(DropTargetEvent dtde):当光标移出拖放目标时将触发DropTargetListener监听器的该方法
    • dragOver(DropTargetEvent dtde):当光标在拖放目标上移动时将触发DropTargetListener监听器的该方法
    • drop(DropTargetDropEvent dtde):当用户在拖放目标上松开鼠标键时,拖放结束时将触发DropTargetListener监听器的该方法
    • dropActionChanged(DropTargetDragEvent dtde):当用户在拖放目标上改变了拖放操作,例如按下或松开Ctrl等辅助键时将触发DropTargetListener监听器的该方法。
  • 通常程序不想为上面每个方法提供相应,即不想重写DropTargetListener的每个方法,我们还可以通过继承DropTargetAdapter适配器来创建拖放监听器。下面程序利用拖放目标创建了一个简单的图片浏览工具,当用户把一个或多个图片文件拖入该窗口时,该窗口将会自动打开每个图片文件。
import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.dnd.*;
import java.io.File;
import java.io.IOException;
import java.util.List;

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);
            //获取拖放的内容
            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);
            //设置内部窗口的原始位置(内部窗口默认大小0*0,放在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();
    }
}

你可能感兴趣的:(java)