第3章 图 形

来源:互联网 发布:流量发短信软件 编辑:程序博客网 时间:2024/04/30 04:49

第3章 图 形

  在AWT中,提供了一些用户接口构件,如按钮、列表、菜单、对话框等,但是它不包含一些类似的纯粹的绘制图形的对象,例如说在AWT中就不提供Line或Circle类。
  在AWT中,不支持Rectangle、Polygon和Point类,然而这些类作为事后产生的想法,还是被加入到原始的AWT中了。既然原始的AWT在设计时不允许纯粹的绘制图形的对象,那么Rectangle、Polygon和Point没有任何绘制图形的能力。换句话说,Rectangle、Polygon和Point不具备draw方法。您可做的,仅仅是可以设置和得到它们代表的几何实体的信息。
  为了代替那些纯粹的、可绘制图形的对象,AWT使用了一种简单的——尽管不够灵活并且不易扩展——模式。每个AWT构件完全来自于它自己的java.awt.Graphics对象,尽管其中的一些图形操作可以在与之相关的构件中实现。
  Graphics也可以向种各样的输出设备中绘制,像画面外缓冲器和打印机——参见第24章“双缓冲技术”和18.4节“打印”中的相关内容。

3.1 java.awt.Graphics

  java.awt.Graphics是一个抽象类,其作用是定义一个真正的工具,用来接受图形操作。在该类中,有47个公共方法可以用作显示图像和文本、绘制和填充开关、剪贴图像操作等等。
  几乎在所有的applet程序(和应用程序)中,都使用了AWT处理Graphics来为图像服务。例如,甚至在一个简单的“Hello World”applet程序中,都使用Graphics来显示其灵活性:

  public class HelloWorld extends Applet{
   public void paint(Graphics g){
    g.drawString("Hello Graphics Java World",75,100);
   }
  }

  另外,在构件内部执行图像操作时,每个Graphics中都保持了下面的图形属性:
  ·用来绘制和填充形状的颜色。
  ·用来描述文本的字体。
  ·剪贴矩形。
  ·绘制模式(XOR或Paint)。
  ·用于显示和剪贴坐标的平移原点。
  表3-1列出的JDK方法都传递一个对Graphics的引用。注意表3-1忽略了java.awt.peer方法。

        表3-1 传递一个对Graphics的引用的JDK方法
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   软件包     类         方 法
  ─────────────────────────────────
  java.awt   Canvas       paint(Graphics g)
         Component     paint(Graphics g)
         Component     paintAll(Graphics g)
         Component     print(Graphics g)
         Component     printAll(Graphics g)
         Component     update(Graphics g)
         Container     paint(Graphics g)
         Container     paintComponents(Graphics g)
         Container     print(Graphics g)
         Container     printComponents(Graphics g)
         ScrollPane     printComponents(Graphics g)
  java.beans  Property_Enditor paintValue(Graphics g,Rectangle r)
         Property_EnditorSupport paintValue(Graphics g,Rectangle r)
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  在java.awt.package软件包中,几乎所有的方法都传递一个对Graphics的引用。此外,我们还要注意用来绘制或打印AWT构件的一些方法。
  在表3-2中,列出了那些返回Graphics引用的方法。在该表中最常使用的方法是Component.getGraphics(),它返回和java.awt.Component相关的Graphics引用。注意表3-2同样忽略了java.awt.peer方法。

         表3-2 返回Graphics 引用的JDK方法
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   软件包              方 法
  ─────────────────────────────────
  java.awt  Component     getGraphics()
        Image       getGraphics()
        PaintJob      getGraphics()
        Graphics      create()
        Graphics      create(int x,int y,int w,int h)
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  图像和打印工作也提供了一个getGraphics方法。通过在一个画面外的缓冲区中显示,Image.getGraphics()经常用在双缓冲中——请参见6.10节“双缓冲入门”中的内容。
  当调用create()时,Graphics.create()复制Graphics。在create()中使用了四个参数来设置原点和剪贴矩形,作为新创建的副本。参数x和y是用来设置原点的。剪贴矩形是Graphics当前剪贴矩形和由传递给create()的参数所定义的矩形的交集。

3.2 Graphics参数

  Graphics类履行两个主要的职责:
  ·设置和获取图形参数。
  ·在输出设备中执行图形操作。
  ·对于Grapics类来讲,重点是互二个职责。本章的大部分内容都是针对该部分讲述的,主要是关于Graphics类中使用的四个参数,它们是:
  ·颜色。
  ·字体和字体度量。
  ·剪贴矩形。
  ·图形模式。
  ·在Graphics类中,提供获取和设置颜色、字体和剪贴矩形的方法。图形模式是一个只写属性,而字体度量是只读属性(字体尺度和特殊的字体结合在一起,所以字体尺度可以通过Graphics的字体来改变)。
  每个Graphics维护一个颜色,这个颜色可以用于下面给出的方法操作:
  ·void setColoer(Color color)
  ·Color getColor()
  用来绘制文本的字体可以用下面的方法规定和读取:
  ·void setFont(Font f)
  ·Font getFont()
  字体度量由java.awt.FontMetrics类表示,在该类中,提供了一些关于字体的详细信息,例如字体的高度、倾斜度、行间距等。在Graphics类中,提供两种方法返回对FontMetrics的引用。这两种方法如下:
  ·FontMetrics getFontMetrics()
  ·FontMetrics getFontMetrics(Font f)
  方法中的无参数方案(第一种方法)返回的字体度量,和Graphics当前的字体结合在一起。第二种方法返回的字体尺度和指定的字体结合在一起。尽管是非镶嵌方法,你同样可以通过设置特殊字体来间接地指定字体尺度。关于字体和字体度量,我们将在第4章“颜色和字体”中进行详细的讨论。
  每个Graphics也维护一个剪贴矩形。图形操作一般在由Graphics的剪贴矩形指定的矩形中执行。其具体设置如下:
  ·void setClip(int x,int y,int w,int h)
  ·Rectangle getClipBounds()
  ·void setClip(Shape)
  ·Shape getClip()
  ·void clipRect(int x,int y,int w,int h)
  从上面的讲述中,我们知道剪贴矩形是通过四个整型参数或通过java.awt.Shape来指定的,上面方法中的四个整型参数构成一个矩形的封闭框。第三个方法中的Shape接口是java 2D API的一部分,关于Java 2D API,本卷中我们将不作讨论,但我们将在本套书的第3卷中进行讨论,Shape所表示的形状可以是非矩形的,所以它可以为输出设备定义一个非矩形的剪贴矩形。
  在上面所给出的方法中,最后一个方法的作用是计算一个新的剪贴矩形,该剪贴矩形是原先剪贴矩形和方法中参数指定的剪贴矩形的交集。在早期的AWT版本中,Graphics.clipRect()是更改剪贴矩形的唯一方法。
  最后,关于绘图模式,我们将在3.8节“图形模式”中进行详细讲述,讨论如何绘制超越现有图形的文本和外形。提供下面两种设置绘图模式的方法:
  ·void setPaintMode()
  ·void set setXORMode()
  setPaintMode()设置paint图形模式,意味着后面的着色操作将改写现有的图形,setXORMode()允许绘制和擦掉现在图形而不干扰其下面的图形。Paint模式是图形模式的默认模式,而XOR模式在许多情形下是由手工设置的,例如在下一个绘图程序中使用橡皮带式或压条法生成图形。

3.3 图形坐标系

  在图形坐标系中,其原点位于构件的左上角,坐标轴沿向下和右的方向增长,如图3-1所示。
  
  小圈代表坐标,正方形代表像素,坐标位于像素之间。
  对于输出设备来讲,坐标位于像素之间。绘制形状轮廓的操作,例如Graphics.drawRect(),用一支画笔沿着一个坐标路径移动,画笔对坐标路径下方或右方的像素进行绘画。画笔的尺寸通常是一个像素高和一个像素宽。

3.3.1 绘制图形形状

  程序范例3-1给出的是一个applet程序,在该applet程序中,通过调用Graphics.drawRect()方法,在applet的左上角给出一个矩形。

程序范例3-1 RectTest applet程序

  import java.applet.Applet;
  import java.awt.*;
  public class RectTest extends Applet{
   public void paint(Graphics g){
    g.drawRect(2,2,4,4);
   }
  }

  尽管该applet程序是一个很简单的操作,但它阐明的却是applet中的一个重点。
  乍一看该applet程序,我们可能会认为其显示的是由drawRect(2,2,4,4)中参数在像素坐标中定义的一个矩形框。实际上,其显示的结果如图3-2所示。参数指定图形笔的坐标路径。当绘制矩形时,坐标路径开始点是(2,2),而后面的两个4则分别代表坐标增长的宽度和高度,所绘制的坐标路径如下所示:
  (2,2)→(6,2)→(6,6)→(2,6)→(2,2)
  图形笔移动的路径,从起点开始,向右移动到第二个点,然后向下移动以第三个点,向左绘制至第四个点,最后回到起点。当画笔沿路径移动时,其像素颜色来自画笔,而画笔所采用的颜色则是通过调用Graphics.setColor(Color)来指定的。
  
   g.drawRect(2,2,4,4)的结果是显示本图所示的5像素单位见方的矩形。带实心箭头线指明矩形的坐标路径,画笔沿该路径绘制出矩形,其像素颜色来自画笔。

  通过调用Graphics.drawRect()绘制矩形时,其结果将会在矩形的右边和下边各存在一个额外的像素行。这是因为传递到Graphics.drawRect()中的参数定义的是画笔遵循的路径,而不是矩形自身的尺寸。由于画笔是沿上面所讲坐标路径绘制矩形的,所以g.drawRect(2,2,4,4)实际上绘制出来的矩形的宽度和高度是5个像素单位——而不是你所想象的4个像素单位。
  AWT技巧:坐标位于像素之间
  图形坐标位于像素之间,而不是在它们之上。指定坐标的图形方法指定画笔路径——通常是单位像素见方——的移动。画笔绘制像素的路径是起点→右→下→左→起点。结果,绘制图形状外形的图形方法将在矩形的右边和下边各存在一个额外的像素行。例如,语句g.drawRect(x,y,10,10)所绘制出来的矩形的宽度和高度是11个像素单位。Graphics.drawRect()中的参数指定坐标路径——而不矩形的尺寸。

3.3.2 绘制构件的四周边界

  对于AWT的初学都来讲,当第一次绘制构件的四周边界时,经常采用的方法是覆盖构件的paint方法(GetSite()是一个Component方法——请参见11-2),像下面所列出的那样:
  Public void paint(Graphics g){
   Dimension size=getSize();
    g.drawRect(0,0,size.width,size.height);
  }
  如同我们已经知道的那样,绘制出的矩形将比构件宽一个像素也高一个像素。结果,右边缘和底边缘边界将被绘制在构件的外面,因此将看不到该部分。
  当然,常用的解决方法是,从宽度和高度中都减去一个像素单位,如下所示:
  Dimension size = getSize();
  g.drawRect(0,0,size.width-1,size.height-1);

3.3.3 填充形状

  在下面的程序范例3-2中,列出了另一个简单的applet程序,该applet程序和前面的程序范例3-1相比,其生成的矩形将被填充。

程序范例3-2 FillTest applet程序

  import java.applet.Applet;
  import java.awt.*;
  public class FillTest extends Applet{
   public void paint(Graphics g){
    g.fillRect(2,2,4,4);
   }
  }

  传递给fillRect()的参数指定的坐标路径与在前面调用drawRect()时指定的坐标路径相同。但是,填充外形的Graphics方法将填充路径的内部,所以填充的矩形是4个像素宽和4个像素高,如图3-3所示。
  形状填充和形状轮廓容易引起混淆。另外,传递给drawRect()和fillRect()的参数定义的是坐标路径,但是经常被错误地描述为以像素为单位的矩形边界。所有绘制轮廓和填充形状的Graphics方法的主要差异是尺寸上的差异。关于弧形线的效果可能参见本章中的图3-10,该图中绘制出其形状并填充之。
  
  该图中,通过g.fillRect(2,2,4,4)方法,Graphics方法填充的是路径的内部。

3.4 Graphics引用

  有两种方法要用到对构件的Graphics的引用。这两种方法就是:覆盖前面表3-1中的方法(传递一个对Graphics的引用),或调用表3-2列出的方法(将返回对Graphics的引用)。
  值得注意的是从Component、Image和PrintJob中的getGraphics()方法返回的Graphics引用并不是一个对Graphics引用的引用,而是返回一个初始Graphics的副本。在下面的章节中,你将会发现这是一个很重要的概念。

3.4.1 引用副本的Graphics引用

  强调Graphics引用引用和构件相关的真实Graphics的副本是很重要的。我们先来考虑下面的程序范例3-3中列出的applet和程序,该applet程序仅仅从左上角到右下角绘制一条直线。

程序范例3-3 CopyTest applet程序

  import java.applet.Applet;
  import java.awt.*;

  public class CopyTest extends Applet {
   public void paint(Graphics g) {
    setForeground(Color.yellow);
    g.drawLine(0,0,getSize().width-1, getSize().height-1);
   }
  }
  在该applet程序中,设置其前景颜色为黄色,然后从左上角到右下角绘制一条直线。但是,该applet程序执行的结果,可能不像你所期待的那样——在开始画线时,线的颜色可能不是黄的。
  该程序的运行结果,如图3-4所示。
  在图3-4所示的两幅图中,左边那个图显示的是其初始状态;画线时使用的是applet默认的前景颜色,该颜色在Windows 95中是黑色。右边的那幅图显示的是已经被调整过大小后的线条。在applet初始着色后,紧接着调用paint()方法,结果得到黄色的线条。下面就解释原因:
  调用Component.setForeground(),改变构件中Graphics的当前颜色——在本例中,该颜色被改变成黄色。SetFroeground()影响applet的Graphics,但并不影响传递给paint()的Graphics的副本。因此,当第一次调用paint()时,线条的颜色并没有变成黄色。当调用drawLine()时,传递给paint()的Graphics和实际的Graphics并不同步。
  后来调用paint()时,传递给paint()的是applet的Graphics的一个新副本,因此setForeground()的调用结果可以将applet的Graphics的当前颜色改变成黄色。
  如果将该applet程序改写为程序范例3-4,那么一开始画出的线就是黄色的。

程序范例3-4 CopyTest2 applet程序

  import java.applet.Applet;
  import java.awt.*;

  public class CopyTest2 extends Applet {
   public void paint(Graphics g) {
    setForeground(Color.yellow);

    // the next line would do just as well as the following
    // g.setColor(Color.yellow);

    Graphics copy = getGraphics();
    try {
     System.out.println("g=" + g.getColor() +
      " copy=" + copy.getColor());

     copy.drawLine(0,0,
      getSize().width-1, getSize().height-1);
    }
    finally {
     copy.dispose();
    }
   }
  }

  传递给paint()的Graphics将被忽略掉,通过调用getGraphics()方法得到一个新的Graphics,应用这个新的Graphics绘制直线。因为在调用setForeground()之后获取Graphics,所以Graphics当前的颜色,也就是线条的颜色,将变成黄色。
  第一次调用paint()时,它打印下面的输出(注:在Windows95下):
  g=java.awt.Color[r=0,g=0,b=0]copy=java.awt.Color[r=255,g=255,b=0]
  注意到在paint()方法返回之前调用Graphics.dispose()方法,处理由Component.getGraphics()返回的Graphics,而传递给paint()的Graphics却没有被处理。参见3.4.3节“处理Graphics”中的内容,以得到更多的关于处理Graphics方面的信息。

3.4.2 Graphics引用的寿命

  除了引用真实的副本外,传递给paint()和update()等方法的Graphics引用仅仅在方法的执行过程中才有效。一旦方法返回,引用将不再有效。
  考虑下面给出的程序范例3-5,在该applet程序中,想重用最初传递给paint()的Graphics引用。直线将在第一次调用paint()时画出,但是对paint()的后续访问将导致直线在一个无效的Graphics内绘制,因此直线不再显示(通过调整窗口大小可以强行重绘该applet)。

程序范例3-5 HoldRef applet程序

  import java.applet.Applet;
  import java.awt.*;
  import java.awt.event.*;

  public class HoldRef extends Applet {
   private Graphics oldg;
   private boolean first = true;

   public void paint(Graphics g) {
    if(first) {
     oldg = g;
     first = false;
    }
    oldg.drawLine(0,0,getSize().width-1, getSize().height-1);
   }
  }

  传递给方法的Graphics引用寿命很短,因为它们是必须处理的有限资源。每个Graphics表示的图像环境是由本地的窗口系统提供的。图像环境通常被设定在一个有限的数量内可用,而且当调用返回时传递Graphics引用的调用者会很仔细地处理掉它。例如,当调用Component.Paint()返回时,调用者处理掉传递给paint()的Graphics。
  虽然Java带有垃圾收集,但是在AWT中仍有两个地方供开发者处理有限的系统资源,处理Graphics就是其中之一(注:窗口和对话框也必须被处理,请参见第16章“窗口、框架和对话框”)。在处理Graphics有两个要注意的事项,即:什么时候需要处理和怎样处理。

3.4.3 处理Graphics

  当你在处理Graphics时,有一个简单的规则:如果调用表3-2列出的getGraphics方法中的一个得到一个对Graphics的引用,或者通过Graphics.create()创建一个Graphics,那么就有责任对它们进行处理。
  如果你覆盖传递Graphics引用的方法,像Component.paint()或Component.update(),那么然后你就可以什么也不管——处理引用则是调用者的责任。
  通过调用Graphics.dispose()处理Graphics,下面给出的程序代码将是一个有力的证明。
  public void someMethodInAComponent(){//code fragment
   Graphics g=getGraphics();
   if(g!=null){
    try{
      //do something with g- if an exception is thrown,
     //the finally block will be executed
    }
    finally{
     g.dispose()//crucial
    }
   }
  }

  调用dispose()是至关重要的,因为忽略不做将会导致窗口系统用完图形环境,这在大部分操作上会引起问题。
  在这里要注意的是对g不做null检测并不是无根据的,如果在构件的同位体创建之前调用getGraphics()则它确实返回null——参见表11-4“依赖于同位体的java.awt.Component方法”。
  对Graphics.dispose()的调用被安置在finally块中,而对Graphics的控制则在相应的try块中执行。这保证调用dispose()将被放在事件内,例外的情况则会被从try块中抛出。
  AWT技巧:传递图形引用的方法引用副本
  Graphics表示本地图形环境,是一个典型的有限资源。所以,被返回的Graphics引用必须通过调用Graphics.dispose()方法处理。
  传递Graphics引用的方法,像Component.paint(),是一种不需要手工设置处理的方法——当调用返回时,方法的调用者必须处理。

3.5 绘制和填充形状

  Graphics类为显示下面图形提供了方法:
  ·直线(Lines)。
  ·折线(Polylines)。
  ·矩形(Rectangles)。
  ·弧(Arcs)。
  ·椭圆(Ovals)。
  ·多边形(Polygons)。
  ·文本(Text)。
  ·图像(Images)。
  矩形、弧、椭圆和多边形可以被填充,在本节中,我们将探究上面列的除图像之外的所有图形。关于处理和过滤图像,我们将在第5章“加载和显示图像”和第6章“图像过滤”两章中进行详细的讨论。

3.5.1 画直线

  画直线可以通过Graphics.drawLine(int x,int y,int x2,int y2)来实现。AWT不能画不固定宽度的直线;使用图形笔所画的直线,其宽度一般是一个像素。另外,线经常被绘制成实线——没有规定线的模式,如点线或虚线。但是,在Java 2D API中,为不同的线类型和图形笔尺寸提供广泛的支持。
  如图3-5所示的是一个applet程序的运行结果,在该applet程序中,所画的直线的位置是随机的,而且画的直线的长度、方向和颜色等也都是随机的。
  程序范例3-6列出了图3-5的applet程序代码清单。点击图3-5中的“scatter”按钮,将会引起applet程序的重置。Math.random()的作用是使线的参数随机化。random()在0.0和1.0之间产生一个伪随机数(注:伪随机数发生器会复杂顺序)。这些数通过乘以因数(10或100)指定直线的位置和尺寸的合理的范围。

程序范例3-6 Pickup Sticks applet程序

  import java.applet.Applet;
  import java.awt.*;
  import java.awt.event.*;

  public class PickupSticks extends Applet {
   private static Color[] colors = {
    Color.white, Color.black, Color.blue, Color.red,
    Color.yellow, Color.orange, Color.cyan, Color.pink,
    Color.magenta, Color.green };

   public void init() {
    Button button = new Button("scatter");

    add(button);

    button.addActionListener(new ActionListener() {
     public void actionPerformed(ActionEvent event) {
      repaint();
     }
    });
   }
   public void paint(Graphics g) {
    for(int i=0; i < 500; ++i) {
     int x = (int)(Math.random()*100);
     int y = (int)(Math.random()*100);
     int deltax = (int)(Math.random()*100);
     int deltay = (int)(Math.random()*100);

     g.setColor(colors[(int)(Math.random()*10)]);
     g.drawLine(x,y,x + deltax, y + deltay);
    }
   }
  }

  Graphics.drawLine(int x,int y,int x2,int y2)中的参数所代表的是直线的端点。注意像所有有Graphics所绘制的图形一样,直线被绘制时使用的是Graphics中的当前的颜色。上面的applet程序中,使用了一个1到10之间的随机数来选择colors数组的一个下标,以使直线的颜色随机化。

3.5.2 画折线

  折线是一系列线段的连接,由drawPolyline(int[] xPoints,int[] yPoints,int numPoints)来控制其画法。在该方法中,传递两个数组,一个数组指定每个点的x坐标值,另一个数组指定点的y坐标值。另外,在该方法中,使用整形值表示要画的折线的点数(注:点数等于线段数减1)。如果起始点和终点不重合,所画的折线是不封闭的。
  如图3-6所示的是一个applet程序的运行结果,该applet程序的代码请参见程序范例3-7。

程序范例3-7 Polylines applet程序

  import java.applet.Applet;
  import java.awt.*;
  import java.awt.event.*;

  public class Polylines extends Applet {
   private static Color[] colors = {
    Color.white, Color.black, Color.blue, Color.red,
    Color.yellow, Color.orange, Color.cyan, Color.pink,
    Color.magenta, Color.green };

   public void init() {
    Button button = new Button("repaint");
    add(button);
    button.addActionListener(new ActionListener() {
     public void actionPerformed(ActionEvent event) {
      Polylines.this.repaint();
     }
    });
   }
   public void paint(Graphics g) {
    int arraySize = ((int)(Math.random()*100));
    int[] xPoints = new int[arraySize];
    int[] yPoints = new int[arraySize];

    for(int i=0; i < xPoints.length; ++i) {
     xPoints[i] = ((int)(Math.random()*200));
     yPoints[i] = ((int)(Math.random()*200));
    }
    g.setColor(colors[(int)(Math.random()*10)]);
    g.drawPolyline(xPoints, yPoints, xPoints.length);

    showStatus(arraySize + " points");
   }
  }

3.5.3 绘制矩形

  和画直线相比,Graphics类中规定了很多的绘制矩形的方法,典型的有下面列出的三种:
  ·实体的(solid)。
  ·圆角的(rounded)。
  ·3D。
  下面列出的是Graphics中绘制和填充矩形的方法:
  ·void clearRect(int x,int y,int w,int h)
  ·void drawRect(int x,int y,int w,int h)
  ·void drawRoundRect(int x,int y,int w,int h,int arcWidth,int arcHeight)
  ·void draw3DRect(int x,int y,int w,int h,boolean raise)
  ·void fillRoundRect(int x,int y,int w,int h,int arcWidth,int arcHeight)
  ·void fillRect(int x,int y,int w,int h)
  ·void fill3DRect(int x,int y,int w,int h,boolean raise)
  记住上面列表中各种方法的参数x,y,w和h是用于决定坐标路径的。关于这些参数的作用,在前面的3.3.1节“绘制图形形状”中,我们已经进行了详细的讲述,你可以参见其中的相关内容。
  绘制和填充3D矩形的方法增加了一个新的参数。参数boolean的作用是指定3D效果是凸的还是凹的;当值是ture时,表明3D效果是凸的,面false时则表示3D效果是凹的。
  在采用圆角法绘制矩形的方法中,除了x、y、w、h、外,还增加了两个参数:arcWidth和arcHeight,这两个参数都是整形参数,arcWidth的作用是设置弧的水平直径,而参数arcHeight则是指定竖直方向上的直径,图3-7中所示的就是水平直径和竖直直径。
  在图3-8显示的applet程序中,用随意产生的颜色、尺寸和位置值绘制矩形。Graphics方法中支持的三种类型的矩形都被绘制。
  applet程序中配备有三个按钮和一个复选框,每个按钮代表被绘制的矩形的类型,激活按钮将会重绘制由该按钮代表的类型的矩形。
  复选框用于指定的矩形是否被填充。
  该applet程序是相当长的,但是大部分涉及的是创建按钮和复选框以及它们相应的事件处理。而我们所关心的绘制矩形的代码被封装在applet的paint方法中。

  public class RandomRectangles extends Applet {
   ...
   private boolean fill = false, raise = false,
    round = false, threeD = false;
   ...
   public void paint(Graphics g) {
    for(int i=0; i < numRects; i++) {
     Point lhc = randomPoint(); // left hand corner
     Dimension size = randomDimension();

     g.setColor(colors[(int)(Math.random()*10)]);

     if(round) {
      if(fill)
       g.fillRoundRect(
        lhc.x,lhc.y,size.width,size.height,
        (int)(Math.random()*250),
        (int)(Math.random()*250));
      else
       g.drawRoundRect(
        lhc.x,lhc.y,size.width,size.height,
        (int)(Math.random()*250),
        (int)(Math.random()*250));
     }
     else if(threeD) {
      g.setColor(Color.lightGray);

      if(fill)
       g.fill3DRect(
        lhc.x,lhc.y,size.width,size.height,raise);
      else
       g.draw3DRect(
        lhc.x,lhc.y,size.width,size.height,raise);
     }
     else {
      if(fill)
       g.fillRect(lhc.x,lhc.y,size.width,size.height);
      else
       g.drawRect(lhc.x,lhc.y,size.width,size.height);
     }
     raise = raise ? false : true;
    }
   }
   ...
  }

  Boolean类的成员记录一些操作信息,这些信息包括哪个按钮被最后激活以及填充选项当前是否被选中。
  在applet程序中绘制所有的矩形时,采用的都是1像素宽的图形笔,因为在AWT中,只允许这个尺寸。
  当绘制3D矩形时,在绘制矩形前,Graphics的颜色被设置成亮灰色,因为draw3Drect()和fill3Drect()所绘制的矩形,只有在亮灰色下才可以看见其3D效果。
  完整的applet程序代码,请参见程序范例3-8。

程序范例3-8 RandomRectangles applet程序

  import java.applet.Applet;
  import java.awt.*;
  import java.awt.event.*;

  public class RandomRectangles extends Applet {
   private static Color[] colors = {
    Color.white, Color.black, Color.blue, Color.red,
    Color.yellow, Color.orange, Color.cyan, Color.pink,
    Color.magenta, Color.green };

   private int numRects = 10;
   private boolean fill = false, raise = false,
    round = false, threeD = false;

   public void init() {
    Button rectsButton = new Button("rectangles");
    Button roundButton = new Button("round rectangles");
    Button threeDButton = new Button("3D rectangles");
    Checkbox fillCheckbox = new Checkbox("fill");

    add(rectsButton);
    add(roundButton);
    add(threeDButton);
    add(fillCheckbox);

    rectsButton.addActionListener(new ActionListener() {
     public void actionPerformed(ActionEvent event) {
      round = false;
      threeD = false;
      repaint();
     }
    });
    roundButton.addActionListener(new ActionListener() {
     public void actionPerformed(ActionEvent event) {
      round = true;
      threeD = false;
      repaint();
     }
    });
    threeDButton.addActionListener(new ActionListener() {
     public void actionPerformed(ActionEvent event) {
      threeD = true;
      round = false;
      repaint();
     }
    });
    fillCheckbox.addItemListener(new ItemListener() {
     public void itemStateChanged(ItemEvent event) {
      fill = ((Checkbox)(event.getSource())).getState();
     }
    });
   }
   public void paint(Graphics g) {
    for(int i=0; i < numRects; i++) {
     Point lhc = randomPoint(); // left hand corner
     Dimension size = randomDimension();

     g.setColor(colors[(int)(Math.random()*10)]);

     if(round) {
      if(fill)
       g.fillRoundRect(
        lhc.x,lhc.y,size.width,size.height,
        (int)(Math.random()*250),
        (int)(Math.random()*250));
      else
       g.drawRoundRect(
        lhc.x,lhc.y,size.width,size.height,
        (int)(Math.random()*250),
        (int)(Math.random()*250));
     }
     else if(threeD) {
      g.setColor(Color.lightGray);

      if(fill)
       g.fill3DRect(
        lhc.x,lhc.y,size.width,size.height,raise);
      else
       g.draw3DRect(
        lhc.x,lhc.y,size.width,size.height,raise);
     }
     else {
      if(fill)
       g.fillRect(lhc.x,lhc.y,size.width,size.height);
      else
       g.drawRect(lhc.x,lhc.y,size.width,size.height);
     }
     raise = raise ? false : true;
    }
   }
   private Dimension randomDimension() {
    return new Dimension((int)(Math.random()*250),
     (int)(Math.random()*250));
   }
   private Point randomPoint() {
    return new Point((int)(Math.random()*250),
    (int)(Math.random()*250));
   }
  }

3.5.4 画弧

  java.awt.Graphics中,提供下面的两种方法绘制和填充弧:
  ·void drawArc(int x,int y,int w,int h,int startAngle,int endAngle)
  ·void fillArc(int x,int y,int w,int h,int startAngle,int endAngle)
  在上面列出的两种方法中,都具有相同的参数。前面的四个参数的作用是为所要画(或填充)的弧指定坐标路径,而后面的两个参数的作用则是设置所要画的弧的开始角度和结束角度。例如,在图3-9中,弧的开始角度是0°,结束角度是270&deg;,宽度和高度分别是151和101个像素。和该图对应的applet程序是程序范例3-9。

程序范例3-9 DrawArc applet程序

  import java.applet.Applet;
  import java.awt.*;

  public class DrawArc extends Applet {
   public void paint(Graphics g) {
    g.setColor(Color.black);
    g.drawArc(10,10,150,100,0,270);
   }
  }

  Graphics.fillArc()填充由参数指定的坐标路径内部。程序范例3-9中的applet程序增中了填充选项后,其显示的效果如图3-10所示。
  注意:弧的黑色外形明显在弧的右端和底部边界可见,即使调用drawArc()和fillArc()使用的是相同的坐标路径。和前面所讲的注意点一样,绘制外形的Graphics方法在图形的右边和底部多画一额外的像素行。
  弧是唯一的一种非封闭的、但可以填充的图形。你从图3-10中可以很清晰地看出如何填充弧,填充弧的范围从弧的中心与起始点的连线开始到中心与结束点的连线为止。和图3-10所对应的applet程序是程序范例3-10。

程序范例3-10 DrawAndFillArc applet程序

  import java.applet.Applet;
  import java.awt.*;

  public class DrawAndFillArc extends Applet {
   public void paint(Graphics g) {
    g.setColor(Color.black);
    g.drawArc(10,10,150,100,0,270);
    g.setColor(Color.yellow);
    g.fillArc(10,10,150,100,0,270);
   }
  }

3.5.5 绘制椭圆

  在Graphics中,提供下面两种方法用来绘制和填充椭圆:
  ·void drawOval(int x,int y,int w,int h)
  ·void fillOval(int x,int y,int w,int h)
  上面列出的两种方法中参数的作用,都是给出椭圆所在的矩形框,如果给出的宽度和高度相同,那么所绘制的图形是一个圆。所绘制的椭圆就是与给出的外框相内切的椭圆。
  和绘制外形的其他Graphics方法一样,drawOval()所绘画的椭圆,适合的矩形是w+1个像素宽和h+1个像素高。

3.5.6 绘制多边形

  在Graphics方法中,提供以下的四种方法用来绘制和填充多边形:
  ·void drawPloygon(int[] xPoints,int[] yPoints,int[] numPoints)
  ·void drawPolygon(Polygon polygon)
  ·void fillPloygon(int xPoints,int[] yPoints,int[] numPoints)
  ·void fillPolygon(Polygon polygon)
  通过规定Polygon对象或x和y数组值来设置多边形的点,可以绘制或填充多边形。如果初始点和结束点不是同一个点多边形将自动闭合。
  要注意的是:尽管在AWT中提供非图形Polygon和Rectangle类,但Graphics类不提供drawRect(Rectangle)方法,尽管存在一个drawPolygon(Polygon)方法。

3.5.7 绘制文本

  在Graphics类中,提供下面的三种方法描述文本:
  ·void drawString(String s,int x,int y)
  ·void drawChars(char[],int offset,int length,int x,int y)
  ·void drawBytes(byte[],int offset,int length,int x,int y)
  文本可以被设置为一个字符串、一个字符数组或一个字节数组,具体是哪种方式取决于具体的调用方法。
  上面提供的显示文本的三种方法传递所绘制文本的x,y位置。位置对应的是文本基线,不是文本的左上角,和矩形不一样。请参见图3-11的描述。
  传递给drawChars()和drawBytes()的offset和length参数分别用来规定开始位置在数组中的偏移量和绘制的字符数。
  Graphics类不提供旋转文本的能力。
  AWT技巧:字符串和图形的位置表示法不同
  绘制字符串所指定的位置,用来设置文本的基线。而图形的位置用来指定形状分界线的左上角。如果字符串和矩形被绘制在同一个位置,则字符串将显示在矩形的上方。
  
  drawString方法使用文本基线作为原点。drawRect方法使用左上角作为原点。

3.6 转换坐标系原点

  如果不指定,则在Graphics坐标系中,原点一般设置在左上角。请参见本章3.3节“图形坐标系”中的相关内容。
  Graphics.translate()可以被用来转换原点到一个新的位置。translate方法接受两个整数参数值,描绘在原先的坐标系统中的点,在转换后的坐标系统中将成为新的原点。
  转换坐标系统的原点有众多的原因:一个原因是容器中没有滚动条而又要滚动其内容,就像我们在下面所举的applet例子一样。
  该applet程序运行的显示结果如图3-12所示,通过点击鼠标可以滚动该图形。
  完成滚动是通过转换Graphics坐标系统中的原点来实现的,只要鼠标拖曳事件发生随后就会重绘制图形。
  图3-12中的左上图显示applet程序的初始状态。右上图显示applet程序的原点已经被转换而且图形被重绘制后的状态。下面的一幅图表明原点可以被转换x和y的值为负数。
  这个applet中的init方法加载图像,其paint方法在(0,0)处绘制图像。

   public void init() {
    image = getImage(getCodeBase(), "saint.gif");
    try {
     MediaTracker mt = new MediaTracker(this);
     mt.addImage(image, 0);
     mt.waitForID(0);
    }
    catch(InterruptedException e) {
     e.printStackTrace();
    }
   public void paint(Graphics g) {
    g.drawImage(image, 0, 0, this);
   }

  init()使用MediaTracker实例,以保证图像在显示之前被完全加载,在本章中,加载图像不是我们讨论的重点。关于圆形图像和如何使用MediaTracker,我们将在第5章“加载和显示图像”中进行讲述。
  这个applet定义鼠标和鼠标动作监听者的内部方案。当在applet中单击鼠标时,鼠标的位置被保存。

  public class TranslateTest extends Applet {
   Image image;
   Point pressed = new Point(), lastTranslate = new Point();
   ...
   public void init(){
   ...
    addMouseListener(new MouseAdapter() {
     public void mousePressed(MouseEvent e) {
      Point loc = e.getPoint();

      // adjust mouse pressed location for
      // translation ...
      pressed.x = loc.x - lastTranslate.x;
      pressed.y = loc.y - lastTranslate.y;
     }
    });
    ...
   }
   ...
  }

  此外,我们在这里讨论的焦点是转换坐标系的原点,而不涉及到事件处理或内部类——这在第9章“授权事件模型”中将有更细的讨论。事件操作代码的依据是,当在applet中点击鼠标时,上面的mousePressed()被调用。
  鼠标点击的位置被定为最后转换点。LastTransLate表示的点是原来的(0,0)点(注:Point无参数构件器设置点的位置为(0,0)),所以第一次点击鼠标时,点击的位置和鼠标点击的坐标一样。LastTranslate每次被更新后,鼠标拖曳事件的结果不坐标系发生转换。
  了解点击鼠标位置被调整为最后转换点是很重要的,因为转换申请有仅仅是构件的Graphics的副本,它是通过调用greGraphics()获得的。实际和applet发生联系的Graphics是决不转换的,而仅仅是实际Graphics的副本得到转换。
  调用Graphics.treanslate()的位置是在mouseDragged()中:

  public class TranslateTest extends Applet {
   ...
   public void init(){
    ...
    addMouseMotionListener(new MouseMotionAdapter() {
     public void mouseDragged(MouseEvent e) {
      Point loc = e.getPoint();
      Point translate = new Point(loc.x - pressed.x,
       loc.y - pressed.y);
       Graphics g = getGraphics();

      try {
       g.clearRect(0,0,
        getSize().width,getSize().height);
       g.translate(translate.x, translate.y);
       showStatus("Translating Graphics: " +
        translate);

       g.drawImage(image, 0, 0, TranslateTest.this);
      }
      finally {
       g.dispose();
      }
      lastTranslate = translate;
     }
    ]);
    ...
   }

  当拖曳鼠标时,通过从鼠标拖动事件的坐标中减去鼠标按下事件的坐标,translate点被计算出,如图3-13所示。
  从getGraphics()得到Graphics副本,将副本的原点转换到计算值。
  关于Graphics,这个例子中有两点值得重申。
  第一,调用Graphics.drawImage()来重绘制转换后的图像。尽管applet的paint方法也在(0,0)处绘制图像,也可以在转换原点之后简单地调用repaint()并让paint()负责复制图像。当然,Graphics对象是通过mouseDragged()得到,就像通过paint()方法在关联的构件中得到实际图形的副本一样。调用g.translate(translate x,translate y),转换从getGraphics()中返回的Graphics副本的原点,但不是传递给paint()的副本(注:这也是我们必须通过最后变换点的原因)。
  第二,注意调用被安排处理从Component.getGraphics()得到的Graphics。处理传递给paint()的Graphics是不必要的——请参见本章前面的“处理Graphics”一节中的相关内容。
  最后,如果你从本书后面的CD上运行该applet程序,你将注意到当图像被拽时会闪动。闪动是因为擦去和着色在屏幕上显示的图像,这个applet程序能简单地被双缓冲以除去闪动。在6.10节“双缓冲入门”中,将简单介绍双缓冲的实现。
  完整的Translate applet程序源码,请参见程序范例3-11。

程序范例3-11 TranslateTest applet程序

  import java.applet.Applet;
  import java.awt.*;
  import java.awt.event.*;

  public class TranslateTest extends Applet {
   Image image;
   Point pressed = new Point(), lastTranslate = new Point();

   public void init() {
    image = getImage(getCodeBase(), "saint.gif");
    try {
     MediaTracker mt = new MediaTracker(this);
     mt.addImage(image, 0);
     mt.waitForID(0);
    }
    catch(InterruptedException e) {
     e.printStackTrace();
    }
    addMouseListener(new MouseAdapter() {
     public void mousePressed(MouseEvent e) {
      Point loc = e.getPoint();

      // adjust mouse pressed location for
      // translation ...
      pressed.x = loc.x - lastTranslate.x;
      pressed.y = loc.y - lastTranslate.y;
     }
    });
    addMouseMotionListener(new MouseMotionAdapter() {
     public void mouseDragged(MouseEvent e) {
      Point loc = e.getPoint();
      Point translate = new Point(loc.x - pressed.x,
       loc.y - pressed.y);
       Graphics g = getGraphics();

      try {
       g.clearRect(0,0,
        getSize().width,getSize().height);
       g.translate(translate.x, translate.y);
       showStatus("Translating Graphics: " +
        translate);

       g.drawImage(image, 0, 0, TranslateTest.this);
      }
      finally {
       g.dispose();
      }
      lastTranslate = translate;
     }
    });
   }
   public void paint(Graphics g) {
    g.drawImage(image, 0, 0, this);
   }
  }

3.7 剪贴

  每个Graphics都有一个关联的剪贴矩形。剪贴矩形之所以这样命名是因为它们复制它们表示的矩形。另外,随着Java 2D API的发展,剪贴区域的开关可以被设置为任意的开关,而不只局限于矩形。
  下面的方法由java.awt.Graphics规定用于设置和得到剪贴区域。
  ·void setClip(int x,int y,int w,int)
  ·void setClip(Shape)
  ·Rectangle getClipBounds()
  ·Shape getClip()
  ·void clipRect(int x,int y,int w,int h)
  上面所列举的方法中,前两个方法用来设置所要剪贴的区域。第一个方法设置的是一个矩形区域,而第二方法中设置的是任意形状。
  第三和第四个方法返回剪贴区域——前一个返回的是一个矩形区域,而后一个返回的是一个任意形状的区域。
  最后一个方法将剪贴矩形设置为当前剪贴矩形和方法中变无指定的矩形的交集。
  如图3-14所示的是一个applet程序的运行结果,和前面的图3-8一样,其程序代码是程序范例3-8。但该applet程序中paint方法已经被更改,所显示的是传递进applet状态栏中的Graphics剪贴矩形。因为这仅仅是对applet程序的修改,所以在这里就不给出该applet程序的代码了。
  当applet程序被最初显示时,就像图3-14左边的图片那样 ,剪贴矩形等同于applet程序本身的尺寸(注意applet状态栏)。在本例中,限制矩形扩充applet程序。任何在applet程序边界外的图形操作将不被着色,也就是说,你不注意绘制在构件边框之外的部分不会引起任何可怕的结果。
  如果另一窗口被拖过applet,就像右边的图片那样,applet也被重绘制。因为applet绘制一组新的任意矩形,重绘制被剪贴给applet损坏的区域,结果是applet中一系列被重绘制折区域。认识到完整的applet事实上每次在小窗口拖动时重绘制是很重要的;当然,被剪贴的是applet窗口中被损坏的区域。
  在图3-14右边所示是一个宽度是150个像素但高只有一个像素的剪贴矩形。一像素高指导明较小的窗口最后一次被垂直拖动——如果窗口已被水平拖动,宽度将是一个像素。如果窗口被对角拖动,剪贴矩形等价于被拖动窗口下面的区域。
  当实现双缓冲和动画制作时,剪贴是尤其重要的,详细情况可以参见后面的第24章“双缓冲”和第25章“子图形动画”等章中的相关内容。当在构件表面上拖动或者动画构件时,只恢复构件中被损坏的区域将更有效,胜于重绘制整个构件和依靠 剪贴修复被损坏的区域。
  AWT技巧:向Component.paint()传递被剪贴的Graphics
  对于AWT的初学者来讲,经常向paint方法传递那些剪贴矩形小于构件的Graphics。结果,底层覆盖的paint方法不干扰它接受的Graphics的剪贴矩形。当paint()绘制构件的内容时,着色操作仍被执行。在一些实例(如双缓冲和动画设计)中,以只更新被剪贴的区域代替绘制完整的内容和依靠剪贴修复被损坏的区域,可以得到更好的效果。

3.8 图形模式

  在java.awt.Graphics中,给出了两种图形模式,即paint和XOR模式。paint模式是默认模式,在paint模式中进行图形操作执行只是简单地改写现有的图形。另一方面,XOR模式允许图形操作可以在现有图形之上执行而不干扰受影响区域。到现在,在本章中前面所有的例子都被设置成paint模式,所以在本节中我们将集中讲述XOR模式。
  Graphics.setXORMode(Color)的文档中对XOR模式做了如下的描述的:
  When drawing operations are performed,pixels which are the current color are changed to the specify color,and vice versa。
  Pixels that are of colors other than those two color are changed in an unpredictable but reversible manner;if the figure is drawn twice,then all pixels are restored to their original values。
  第二句中底部画线的部分说明:如果在XOR模式连续两次执行复制图像操作,其下面的图形是不受影响的。
  或许XOR模式最一般的用途是在现有的图形上使用橡皮带生成法。橡皮带生成法在制图程序中和选择多样的对象时是一种很普通的用法。
  图3-15显示的是使用XOR模式橡皮带生成法,在一个图像上生成一个矩形。
  图3-15所示的applet程序使用其init方法加载图像,并且在paint()中绘制出图像。在XOR模式下,所有的行为在处理鼠标拖动事件的处理程序中完成:

    addMouseMotionListener(new MouseMotionAdapter() {
     public void mouseDragged(MouseEvent e) {
      Point loc = e.getPoint();
      Graphics g = getGraphics();

      try {
       g.setXORMode(getBackground());

       if(firstRect) {
        firstRect = false;
       }
       else {
        g.drawRect(pressed.x, pressed.y,
        Math.abs(pressed.x - last.x),
        Math.abs(pressed.y - last.y));
       }
       g.drawRect(pressed.x, pressed.y,
        Math.abs(loc.x - pressed.x),
        Math.abs(loc.y - pressed.y));
        last = e.getPoint();
      }
      finally {
       g.dispose();
      }
     }
    });

  不管鼠标什么时候被拖曳,applet的图形模式被设置为XOR模式。传递背景颜色给setXORMode(),意味着后来的图形操作产生的像素,是将当前的Graphics颜色被转换成背景颜色,反之亦然。所有其他颜色改变不可预见。当然,如果图形操作在XOR模式中被执行两次,下面的像素将以原来的颜色保留下来。
  如果mouseDragged()是在鼠标压下后第一次被调用的话,firstRect被设为true,而且因此还没有明显的矩形要取消。再拖动的结果是,上一个矩形被重绘制,这样上一个矩形被取消,下面的图形恢复。然后,新的矩形在新位置给出。
  在橡皮带式生成线操作完成后,applet的背景被清除,Graphics被剪贴给橡皮式生成线的矩形,而且图形被重绘制。

     public void mouseReleased(MouseEvent e) {
      if(pressed != null) {
       Point released = e.getPoint();
       Rectangle clip = new Rectangle();
       Graphics g = getGraphics();
       Dimension size = getSize();

       try {
        clip.x = pressed.x;
        clip.y = pressed.y;
        clip.width =
         Math.abs(released.x - pressed.x);
        clip.height =
         Math.abs(released.y - pressed.y);

        g.clearRect(0,0,size.width,size.height);
        g.setClip(clip);
        g.drawImage(image, 0, 0, xortest.this);
       }
       finally {
        g.dispose();
       }
      }
     }

  当然,在程序范例3-12中的所有鼠标处理程序方法都是小心地处理通过调用Component.getGraphics()得到的Graphics。此外,在设置剪贴矩形之后,mouseReleased()通过调用setClip()绘制图形自身,而不调用paint()。原因是仅有通过getGraphics()得到的Graphics的副本会受到影响。
  用于绘制橡皮带生成线矩形的算法是不完善的,如果矩形被到鼠标压点的上面或左边,它将不会被正确地执行。有关内容,您可以参见第23章“橡皮带技术”中的有关章节。
  完整的applet程序代码,请参见程序范例3-12。

程序范例3-12 xortest applet

  import java.applet.Applet;
  import java.awt.*;
  import java.awt.event.*;

  public class xortest extends Applet {
   Point pressed, last;
   Image image;
   boolean firstRect;

   public void init() {
    image = getImage(getCodeBase(), "saint.gif");
    try {
     MediaTracker mt = new MediaTracker(this);
     mt.addImage(image, 0);
     mt.waitForID(0);
    }
    catch(InterruptedException e) {
     e.printStackTrace();
    }
    addMouseListener(new MouseAdapter() {
     public void mousePressed(MouseEvent e) {
      firstRect = true;
      pressed = e.getPoint();
     }
     public void mouseReleased(MouseEvent e) {
      if(pressed != null) {
       Point released = e.getPoint();
       Rectangle clip = new Rectangle();
       Graphics g = getGraphics();
       Dimension size = getSize();

       try {
        clip.x = pressed.x;
        clip.y = pressed.y;
        clip.width =
         Math.abs(released.x - pressed.x);
        clip.height =
         Math.abs(released.y - pressed.y);

        g.clearRect(0,0,size.width,size.height);
        g.setClip(clip);
        g.drawImage(image, 0, 0, xortest.this);
       }
       finally {
        g.dispose();
       }
      }
     }
     public void mouseClicked(MouseEvent e) {
      repaint();
     }
    });
    addMouseMotionListener(new MouseMotionAdapter() {
     public void mouseDragged(MouseEvent e) {
      Point loc = e.getPoint();
      Graphics g = getGraphics();

      try {
       g.setXORMode(getBackground());

       if(firstRect) {
        firstRect = false;
       }
       else {
        g.drawRect(pressed.x, pressed.y,
        Math.abs(pressed.x - last.x),
        Math.abs(pressed.y - last.y));
       }
       g.drawRect(pressed.x, pressed.y,
        Math.abs(loc.x - pressed.x),
        Math.abs(loc.y - pressed.y));
        last = e.getPoint();
      }
      finally {
       g.dispose();
      }
     }
    });
   }
   public void paint(Graphics g) {
    g.drawImage(image, 0, 0, this);
   }
  }

3.9 创建图形

  当实现传递Graphics引用的方法时,最好是保证方法的结果不会引起Graphics的变化。换句话说,当方法返回时,Graphics和调用前的情况应当是相同的。
  在该规则中当然也有例外,我们可以相当有把握地说,当调用paint()返回时,paint(Graphics)的调用者仅仅处理Graphics。因此,可以改变传递给paint()的Graphics,而忽视维持它的初始状态。
  但是,在另外的情况下,是否Graphics一定保留它的初始状态并没这么清楚。在这样的情况下,最好是接受安全的步骤并保证Graphics不被改变,有下面的两个方法可以实现。
  方法之一是:Graphics的所有的初始的特征可以被存储在本地,然后在方法返回之前重新设置:
  //code fragment
  public void notSurelfGraphicsShouldChangeState(Graphics g){
   Color oldColor=g.getColor();
   Font oldFont=g.getFont();
   //modify g's color and font and perform graphical operations
   g.setColor(oldColor);//restore old color
   g.setfont(oldFont)//restore old font
  当只更改一两个属性时,恢复属性只是一个次要的麻烦。然而,如果大量的属性被修改后并被重新恢复,那么创建Graphics的一个副本并用它代替传递给方法的Graphics是更方便的办法:
  public void notSurelfGraphicsShouldChangeState(Graphics g){
   Graphics copy=g.create();
   try{
    //use copy for rendering
   }
   finally{
    g.dispose();//crucial
   }
  }
  前面我们在讨论处理图形时,通过调用Graphics.create()方法得到的Graphics,其处理过程是一个强制过程,就像我们上面已经做的伪码一样。
  创建Graphics结果的另一个动机是在方法返回之后保留传递到方法中的Graphics。这样做的一个典型方法是,在一个与之关联的paint方法中,重复更改Graphics引用的属性——保留属性已适当修改的Graphics的副本是较有效的办法。
  回忆我们在前面讲述的程序范例3-5,该applet程序保留一个Graphics引用,由于传递给paint()的Graphics是由调用者来进行处理的,所以Graphics引用仅在调用paint()期间有效。但是,我们可以创建一个Graphics的副本,该副本在调用paint()之后将不被处理,如下面给出的程序范例3-13。(运行效果)

程序范例3-13 HoldRef2 applet程序

  import java.applet.Applet;
  import java.awt.*;
  import java.awt.event.*;

  public class HoldRef2 extends Applet {
   private Graphics copy;
   private boolean first = true;

   public void paint(Graphics g) {
    if(first) {
     // note: copy is never disposed of
     copy = g.create();

     copy.setColor(Color.red);
     copy.setFont(new Font("Times Roman", Font.BOLD, 14));
     first = false;
    }
    copy.drawString("Red Text", 10, 10);
   }
  }

  程序范例3-13创建的Graphics修改了它的颜色和字体,并在applet中的左上角绘制一个字符串。当然,我们正在犯一个致命错误,即没有处理Graphics的副本。在实际中,许可制造出一个类成员的副本引用,并在另一个方法中在适当电动机及时处理它。
  Graphics类提供两个方法创建Graphics:
  ·Graphics create()
  ·Graphics create(int x,int y,int w,int h)
  第一个方法中,当该方法被调用时,创建的是一个精确的Graphics副本。
  第二个方法也是创建一个副本,但是,变元指定一个平移量(x,y)和一个新的剪贴矩形(x,y,w,h)。create(int,int,int,int)返回的Graphics的原点被转换为(x,y)坐标,但是剪贴矩形转换为原剪贴矩形和指定矩形的交集。
  该applet程序运行的结果如图3-16所示,该程序显示图形两次——一次将Graphics传递给paint(),还有一次是Graphics副本,它已通过调用create()改变了原点和剪贴矩形。
  这个applet副本程序绘制同样的图形两次——一次是原先的Graphics,一次是原Graphics的副本,并通过调用create()进行原点变换和更改剪贴矩形。

程序范例3-14 CreateTest applet程序

  import java.applet.Applet;
  import java.awt.*;

  public class CreateTest extends Applet {
   private Image image;

   public void init() {
    MediaTracker mt = new MediaTracker(this);
    image = getImage(getCodeBase(), "image.gif");

    try {
     mt.addImage(image, 0);
     mt.waitForID(0);
    }
    catch(Exception e) {
     e.printStackTrace();
    }
   }
   public void paint(Graphics g) {
    Graphics copy = g.create(image.getWidth(this),0,100,100);

    try {
     System.out.println("g: " + g.getClip().toString());
     System.out.println("copy: " +
     copy.getClip().toString());

     g.drawImage(image, 0, 0, this);
     copy.drawImage(image, 0, 0, this);
    }
    finally {
     copy.dispose();
    }
   }
  }

  加载图像(参见第5章“加载和显示图像”)并通过applet的paint方法创建一个要传递的Graphics的副本,副本的原点变换到(imw,0),imw是指图形的宽度。
  副本也有它的剪贴矩形,设置为初始Graphics的剪贴矩形和由(image.getWidth(this),0,100,100)指定的矩形的交集。因为初始Graphics的剪贴矩形覆盖由applet程序占领的区域,所以两个矩形的交集由(image.getWidth(this),0,100,100)决定。
  提示:复制的Graphics已经被转换,因为调用drawImage()在(0,0)绘制图像。

3.10 小结

  不像一些面向对象的图形工具集,AWT不提供支持特殊形状的类,如Line和Circle类。来自Graphics对象中的每个AWT构件被用来在构件中执行图形操作。除构件之外,其他的输出设备(如打印机和画面外图像)也已经和Graphics发生联系用作执行图形操作。
  java.awt.Graphics提供大量的方法绘制和填充图形、绘制文本、设置图形参数,如为以后的着色操作设置的颜色和文本。但是,在某些方面Graphics也是十分有限的——例如,画笔被限定为一像素单位。在Java 2D API中弥补了Graphics中的许多短处;可是,2D API不是AWT的一部分,不在本卷讨论的范围内(您可以参见本套书的第三卷——2D API)
  图形操作的坐标系被设定在设备的左上角,x和y轴的增长方向分别是向下和向右,坐标位于像素之间,Graphics方法中绘制形状外形是通过设定图形的坐标而不是像素。图形笔在坐标路径右边和下边移动,因此形状的外形导致在图形右边和下边各有一个额外的像素行。在另一方面,形状填充图形内部,填充的尺寸和坐标路径一样。对于AWT的新学者来讲,在图形外形和填充之间的尺寸差异是一个难点。
  每个Graphics联系一个本机潜在的窗口系统中的图形环境。因此,Graphics描述的是必须被手工处理的有限资源。如果通过调用一个方法返回一个Graphics引用以得到一个Graphics,则Graphics.dispose()必须被调用为Graphics释放系统资源。另一方面,传递Graphics引用的方法通常不必处理Graphics。一般地,这些方法调用者负责处理Graphics。
  Graphics类也有很多手工功能,如转换坐标系原点,粘贴图形操作给指定外形,有能力创建现有Graphics的副本。

原创粉丝点击