7.7 更多的绘制和填充操作
本节中将学习几种增强绘制输出的方法,包括颜色混和和两个增强接口:Stroke接口和Paint接口。在创建了实现这些接口的对象后,它们可以被放入当前的Graphics2D容器中,Graphics2D会把这些特性当作接着被绘制的Shape对象。
7.7.1 Stroke接口
一般而言,Stroke指的是线条被绘制时应用在它们上的特性,这些特性可以是线条宽度以及其他可以应用在线条上的相关属性。当绘制形状时,Graphics2D使用Stroke属性来绘制形状的轮廓。
7.7.2 BasicStroke类
BasicStroke是目前唯一一个实现了Stroke接口的类。它允许定义线条下列属性。
q 宽度(Width):画笔的宽度或者说浓度。
q 端头(End Caps):描述笔画末尾部分。
q 联结方式(Line Joins):描述两条线相交处的处理
q 虚线模式(Dash Patten):把一条直线分透明(不可见)和不透明(可见)的小段。
这里,线条的宽度可又是任意正的浮点数,而端头可以是BasicStroke类的下列常量。
q BasicStroke.CAP_BUTT:笔画结束处没有任何附加修饰。
q BasicStroke.CAP_ROUND:笔画结束处用以画笔宽度一半为半径的圆修饰。
q BasicStroke.CAP_SQUARE:笔画结束用方形向外延伸笔画宽度一半的长度。
和端头相似,联结方式可以是BasicStroke类的下列常量。
q BasicStroke.JOIN_BEVEL:两线相交时用直线段连接
q BasicStroke.JOIN_MITER:延伸线条外部边直至它们相交。
q BasicStroke.JOIN_ROUND:在交点处又圆连接
虚线模式(Dash Patten)可能是Stroke4个属性中最复杂的一个。单个的线段尺寸和间隔尺寸是使用一个浮点数组来定义的。比如,下面定义的一个数组会使得线内的第一段长为3.0像素,第一个间隔为10.0像素,第二段长为6.0像素,第二个间隔为2.0像素:
flaot[] dashPattern={
这个模式会用同样的规则重复处理第三个部分及其间隔。
BasicStroke的属性必须在构造函数内设置。下面的applet,使用最复杂的那个构造函数演示了BasicStroke的一些属性。它设置画笔宽度为3.0像素,没有使用端头修饰,使用JOIN_MITER方式连接,使用了一些简单的虚线模式。然后,它画了一条直线和一个矩形来显示Stroke的属性。
import java.applet.*;
import java.awt.*;
import java.awt.geom.*;
public class StrokeTest extends Applet{
public void paint(Graphics g){
//把传入的Graphics容器转换为一个可用的Graphics2D对象
Graphics2D g2d=(Graphics2D)g;
//设置画笔的宽度为3像素
float penWidth=
//设置端头修饰和联结方式
int endCaps=BasicStroke.CAP_BUTT;
int lineJoins=BasicStroke.JOIN_MITER;
//限制斜角修饰为10像素
float trim=
//设置虚线模式
float []dashPattern={
//立即开始(没有像素偏差)
float dashOffset=
BasicStroke stroke=new BasicStroke(penWidth,endCaps,lineJoins,trim,dashPattern,dashOffset);
g2d.setStroke(stroke);
g2d.draw(new Line2D.Float(
g2d.draw(new Rectangle2D.Float(
}
}
默然:我分别试几个常量,端头的影响是比较明显的,原来端头是指的线段的两端,虚线的两端都出现了影响(因为很明显的原因:虚线是由多个小线段组成的嘛),可是联结方式没有看出什么变化(我甚至把线换成了实线,也没看出联结方式的作用来)可能是还有什么机关没有摸到吧,等下来又后再试试了。
7.7.3 Paint接口
Paint接口允许指定以何种方式填充图形。Color类是Paint接口的一个实现类。Java API还定义了另外两个实现了Paint接口的类:GradientPaint和TexturePaint。本节中,我们专门研究这两个类的使用方法。
注意:在第6章中,我们已经看到了怎样使用Color对象来设置applet的背景,Graphics2D类也包含一个名为setColor的方法,用来设置当前绘制的颜色。在Java 2版本中,Graphics2D类还添加了一个setPaint方法。setColor和setPaint方法是一样的,都会产生同样的效果。所以,是选择setColor还是setPaint完全由用户决定。
1.GradientPaint类
Gradient(渐变)指的是定义了两个端点的颜色带。两个端点分别使用不同的颜色定义,所有的中间点使用介于两个端点之间的颜色填充。中间点的颜色基于它和两个端点之间的距离差值产生。
要使用GradientPaint类,就必须首先有一个Shape对象和两个指定端点如何绘制的点。
GradientPaint对象的属性也必须在构造函数中设置。
下面的GradientTest applet,使用GradientPaint类绘制了一个绿色到桔黄色渐变的星形。
import java.applet.*;
import java.awt.*;
import java.awt.geom.*;
import java.util.*;
public class GradientTest extends Applet{
//要绘制的Polygon
private Polygon poly;
//定义绘制的两个端点
private Point2D p1;
private Point2D p2;
public void init(){
//两个圆的半径
final float[] radio={
//开始的点和每一小块的增量
double radians=0.0;
final double increment=Math.toRadians(15.0);
poly=new Polygon();
//形状由两个圆周上交替的两个点决定
//由于按15度递增,所以可以在开关内放置24个点
for(int i=0;i<24;i++){
poly.addPoint((int)(radio[i%2]*Math.cos(radians)),(int)(radio[i%2]*Math.sin(radians)));
radians+=increment;
}
//设置绘制的终点,这些值会被Graphics2D对象缩放
p1=new Point2D.Float(
p2=new Point2D.Float(
}
public void paint(Graphics g){
Graphics2D g2d=(Graphics2D)g;
AffineTransform at=new AffineTransform();
at.translate(100,100);
at.scale(5,5);
//绘制形状
g2d.setTransform(at);
g2d.setPaint(new GradientPaint(p1,Color.orange,p2,Color.green));
g2d.fill(poly);
}
}
要注意,定义端点的点是通过形状转化来的。在上面的例子中,为沿着图形的周界绘制而定义端点。但是,如果把绘制的端点定义在图形的内部又会怎样呢?
在p1和p2外的点的颜色是怎样的?答案取决于绘制是定义为cyclic(循环的)还是acyclic(非循环的)。如果绘制是循环的,则p1,p2外面的颜色将按p1,p2之间的颜色循环。想象一下镜面效应,和p1,p2等距的点的颜色相同。相反地,非循环的绘制使用中间点定义的颜色来绘制外部的点,因此,p1外面的点将使用p1点的颜色,p2外面的点将使用p2点的颜色。
下面的例子CycleTest演示了循环和非循环两种绘制,它还在定义渐变的点那里画了一条垂直的线。
import java.applet.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
public class CycleTest extends Applet implements ItemListener{
//要绘制的矩形
private Rectangle2D rect;
//包含两个点的直线,它将确定绘制的端点
private Line2D line;
//选择循环类型的单选按钮
private Checkbox cyclic;
private Checkbox acyclic;
public void init(){
//创建一个单位正方形
rect=new Rectangle2D.Float(
//设置终点
line=new Line2D.Float(
setBackground(Color.orange);
//创建选择渐变循环类型的单选按钮
CheckboxGroup cbg=new CheckboxGroup();
setLayout(new BorderLayout());
Panel p=new Panel();
p.setBackground(Color.GREEN);
cyclic=new Checkbox("循环",cbg,true);
p.add(cyclic);
cyclic.addItemListener(this);
acyclic=new Checkbox("非循环",cbg,false);
p.add(acyclic);
acyclic.addItemListener(this);
add(p,BorderLayout.SOUTH);
}
public void paint(Graphics g){
//缩放后的矩形宽度
final double scaleWidth=
//把传入的Graphics容器转换为一个可用的Graphics2D对象
Graphics2D g2d=(Graphics2D)g;
//平移
g2d.translate(100,100);
g2d.scale(scaleWidth,50);
//绘制
g2d.setPaint(new GradientPaint(line.getP1(),Color.black,line.getP2(),Color.white,cyclic.getState()));
g2d.fill(rect);
//画出端点处的垂线
g2d.setPaint(Color.red);
g2d.setTransform(new AffineTransform());
g2d.translate(100-0.25*scaleWidth,100);
g2d.rotate(Math.PI/2);
g2d.scale(scaleWidth/2,1);
g2d.draw(line);
g2d.setTransform(new AffineTransform());
g2d.translate(100+0.25*scaleWidth,100);
g2d.rotate(Math.PI/2);
g2d.scale(scaleWidth/2,1);
g2d.draw(line);
}
public void itemStateChanged(ItemEvent e){
//更新
repaint();
}
}
注意循环值是怎样在GradientTest类中设置的,自己试着真正搞懂两种循环绘制方式之间的差异。
2.TexturePaint类
和GradientPaint类很相似。TexturePaint类也实现了Paint接口。使用TexturePaint类,可以用纹理或者图像来填充图形。
TextPaint需要一个BufferedImage和参照矩形(anchoring rectangle),并使用Graphics2D容器的setPaint方法来设置paint。然后,使用Graphics2D容器来填充带纹理的图形。
由于第8章将对BufferedImage类做更进一步的讨论,所以,这里暂时认为BufferedImage代表一大块包含图像数据的可访问内存。
传入TexturePaint对象的第二个参数是用来作为参照物的Rectangle2D对象。这个矩形在图形中的任意方向复制,绘制出来的就是想得到的纹理。对于小矩形,纹理的重复频率高;对于大矩形,纹理的重复频率低。
TextureTest允许用户输入自己想用来作为纹理的图片文件名,然后这个applet尝试根据文件名把它加载进来,接着使用被加载的纹理作为图像的填料。
import java.applet.*;
import java.awt.*;
import java.awt.image.*;
import java.awt.geom.*;
import java.awt.event.*;
public class TextureTest extends Applet implements ActionListener{
//接受文件名的一个文本域
private TextField input;
public void init(){
//创建一个布局并添加文本域和一个OK按钮
setLayout(new BorderLayout());
Panel p=new Panel();
input=new TextField("",20);
p.add(input);
Button ok=new Button("OK");
ok.addActionListener(this);
p.add(ok);
add(p,BorderLayout.SOUTH);
}
public void paint(Graphics g){
//把传入的Graphics容器转换为一个可用的Graphics2D对象
Graphics2D g2d=(Graphics2D)g;
//j绘制形状的外形,如果文本域只包含空格则返回
if("".equals(input.getText().trim())){
g2d.translate(112,15);
g2d.rotate(Math.PI/4);
g2d.draw(new Rectangle2D.Double(0,0,104,104));
return;
}
//加载一个图像
//当讨论动画时我们会谈到MediaTracker类
MediaTracker mt=new MediaTracker(this);
Image image=getImage(getCodeBase(),input.getText());
mt.addImage(image,0);
try{
mt.waitForAll();
}catch(InterruptedException e){
//Nothing
}
//如果所创建图像的宽或者高小于等于0,则证明文件名可能是错的
if(image.getWidth(this)<=0||image.getHeight(this)<=0){
input.setText(input.getText()+":非法文件名");
return;
}
//用图像的宽和高创建一个新的BufferedImage
BufferedImage bi=new BufferedImage(image.getWidth(this),image.getHeight(this)
,BufferedImage.TYPE_INT_BGR);
//得到BufferedImage的Graphics2D容器并把原先的图像绘制在它上面。
((Graphics2D)bi.getGraphics()).drawImage(image,new AffineTransform(),this);
//为绘制的图像创建和图像等大的定位矩形
Rectangle2D bounds=new Rectangle2D.Float(0,0,bi.getWidth(),bi.getHeight());
//设置paint
g2d.setPaint(new TexturePaint(bi,bounds));
//变换并绘制
g2d.translate(112,15);
g2d.rotate(Math.PI/4);
g2d.fill(new Rectangle2D.Double(0,0,104,104));
}
public void actionPerformed(ActionEvent e){
//OK按钮按下,更新变化
repaint();
}
}
有几点需要注意,由于Graphics2D容器是创建图像数据的完全拷贝,而不是创建数据的影子拷贝或者拷贝引用,所以BufferedImage对象应该尽量的小,不要让大的纹理妨碍了程序的运行。
其他需要注意的事情与纹理的实际绘制有关。首先,纹理沿着图形的几何方向转换;其次,如果纹理不是与图形恰好吻合,那么纹理的边界将被省略掉。所以,如果效果没有预想的好,应该找一种方法来回避这种现象。
注意:就目前而言,无须注意MediaTracker类,只需知道它在程序执行前确认图像被正确加载即可。第9章将对这个类做更多的探讨。
在第8章中,我们将看到如何在BufferedImage对象上绘制图形,以及使用修改过的图像作为纹理来创建画面。下面,让我们把注意力转移到怎样合成重叠物体的颜色上来。
7.7.4 混和处理
虽然我们所创造的“世界”实际上是画在二维平面上的二维物体,但是把这些物体设想为一个物体叠加在另一个物体上并非完全不可能。如果绘制同样距离的物体,那么可以创造深度的幻觉,甚至是物体的混合。如果可以让物体变得透明,或者让物体产生让光线透过的外观,那不是很漂亮吗?
Java API提供了一个Composite接口,它可以使得像文字,图形,图像这样的物体和它们下面的组件混和。混和物是通过规则算出来的,有一个类实现了Composite接口:AlphaComposite类。AlphaComposite类允许指定alpha通道(alpha channel),或者说是物体的透明度。这些透明度的计算方法基于混和图像的Porter-Duff规则。
alpha值是浮点数字,它们是从0.0(完全透明)~1.0(完全不透明,或者说完全缺少透明性)范围内的非负数。如果没有指定alpha值,则默认为1.0.计算时,alpha混和遵从下面的公式:
(final_pixelRGB)=(alpha)*(sourc_pixelRGB)+(alpha-1)*(destination_pixelRGB)
即最终像素的RGB值=alpha*源像素RGB值+(alpha-1)*目标像素RGB值。
AlphaComposite对象不能通过显式的构造函数来创建,它必须通过getInstance方法来获取。有两个getInstance方法:一个以规则作为参数(假设alpha值为1.0),另一个以规则和alpha值(在0.0~1.0之间)为参数。传给getInstance方法的规则(rule)参数是一个常量,必须是AlphaComposite类中定义了的值。最常用的规则是SRC_OVER,此外还有CLEAR,DST_ATOP,DST_OUT,DST_OVER,SRC,SRC_ATOP,SRC_IN,SRC_OUT,SRC_OVER,XOR。
注意:记住,如果不对Graphics2D容器指定合成规则,则默认是SRC_OVER,alpha值为1.0(完全不透明)。
下面的CompositeTest applet创建了几个方块并让它们在窗体中反弹。尽量不要把注意力放在代码本身,而是放在AlphaBox对象的创建和怎样应用它们的合成值上面。特别注意一个方块叠加到另一个方块上时颜色的生成。
import java.applet.*;
import java.awt.*;
import java.awt.geom.*;
import java.util.*;
//封装方形的属性(位置,大小等)是让它可以有规律地更新的一个简单方法
class AlphaBox{
//随机生成器
private static Random random=null;
//所有的绘制都将从一个单位正方形开始
private static Rectangle2D square=null;
//恒等变换
private static AffineTransform identity=null;
//盒子的属性
private AlphaComposite alpha;
private double xPos;
private double yPos;//x,y位置
private double xVel;
private double yVel;//x,y速度
private double size;//宽和高(默然:正方形的宽高一样)
private Color color;//实例的颜色
private Dimension windowSize;
public AlphaBox(Dimension d){
windowSize=d;
//定义所有的空对象
if(random==null){
random=new Random();
}
if(square==null){
square=new Rectangle2D.Float(-0.5f,-0.5f,1.0f,1.0f);
}
if(identity==null){
identity=new AffineTransform();
}
//所有的合成都将是SRC_OVER的而且透明
//使用这些值的随机数来得到一些很不错的效果
alpha=AlphaComposite.getInstance(AlphaComposite.SRC_OVER,0.25f);
//随机得到盒子的属性
xPos=windowSize.width*random.nextDouble();
yPos=windowSize.height*random.nextDouble();
xVel=1+2*random.nextDouble();
if(random.nextDouble()>0.5){
xVel=-xVel;
}
yVel=1+2*random.nextDouble();
if(random.nextDouble()>0.5){
yVel=-yVel;
}
size=25+100*random.nextDouble();
color=new Color(random.nextInt()).brighter();
}
//根据盒子当前的属性把它绘制到所传入的Graphics2D容器中
public void paint(Graphics2D g2d){
//让盒子在窗体上弹跳
xPos+=xVel;
if(xPos>windowSize.width){//默然:如果坐标出了窗体右边框
xPos=windowSize.width;
xVel=-xVel;
}
if(xPos<0){//默然:如果坐标出了窗体左边框
xPos=0;
xVel=-xVel;
}
yPos+=yVel;
if(yPos>windowSize.height){//默然:如果坐标出了窗体下边框
yPos=windowSize.height;
yVel=-yVel;
}
if(yPos<0){//默然:如果坐标出了窗体上边框
yPos=0;
yVel=-yVel;
}
//渲染盒子
g2d.setTransform(identity);
g2d.translate(xPos,yPos);
g2d.scale(size,size);
g2d.setComposite(alpha);
g2d.setPaint(color);//默然:与调用setColor方法效果相同
g2d.fill(square);
}
}
public class CompositeTest extends Applet implements Runnable{
//动画线程(我们稍后再谈)
private volatile Thread animation;
//AlphaBox对象数组
private AlphaBox[] boxes;
public void init(){
animation=new Thread(this);
//创建盒子
final int n=10;
boxes=new AlphaBox[n];
Dimension size=this.getSize();
for(int i=0;i<n;i++){
boxes[i]=new AlphaBox(size);
}
}
//重写applet的start方法
public void start(){
animation.start();//启动线程
}
//重写applet的stop方法
public void stop(){
animation=null;
}
//重写applet的update方法,让它不要清除窗体
public void update(Graphics g){
paint(g);
}
public void paint(Graphics g){
Graphics2D g2d=(Graphics2D)g;
//绘制每一个AlphaBox
for(int i=0;i<boxes.length;i++){
boxes[i].paint(g2d);
}
}
//Runnable的run方法
public void run(){
//稍后再讲
Thread t=Thread.currentThread();
while(t==animation){
try{
t.sleep(10);
}catch(InterruptedException e){
}
repaint();
}
}
}
Applet默认的update方法在处理前清除目标绘制窗体,因此,需要重写它以便不发生窗体清除(默然:看不懂这句话?那么,你注释掉上面的update方法,运行一下就知道了。)
虽然上面的代码清单比较长,但还是建议自己输入并试着运行。对于在applet中使用生动的动画,它也是一个很好的指导。还有很多东西可以挖掘,大家可以自己设置一些值来测试。
7.8 处理文本
在游戏中,当想要把文本精确地放在一个位置时,可以使用Java 2-D类;还可以通过AffineTransform对象来操纵文本,以创造一些很好的效果。Java 2-D还允许使用指定的字体绘制文本。
按照Java 2-D的观点,字体只不过是一些Shape对象,它们像其他的Shape对象一样被转换和绘制。构成单个字母和字母组合的形状被称为“字样”(glyphs)。因此,一个字体只是一个代表所有这个字体想要表达的字符的字样的集合。
此外,一个特定的字体可以有几个变种:正常的,加粗的,斜体的和哥特式的等,这些谈何被称为字形(font faces)。某种字体的变种的集合被称为字体系列(font family)。
下面的程序FontListing使用GraphicsEnvironment对象来获知系统中可用字体的名字,以String对象的数组的形式返回,然后打印每一个字体的名字。
import java.io.*;
import java.awt.*;
public class FontListing{
public static void pause(){
System.out.println("/n按回车键继续");
try{
System.in.read();
}catch(IOException e){
}
}
public static void main(String[] args){
String[] availableFonts=GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames();
for(int i=0;i<availableFonts.length;i++){
System.out.println(availableFonts[i]);
if(i>0&&i%20==0){
pause();
}
}
}
}
上面代码清单中的pause方法只是允许用户对可能很长的字体名称清单一次查看一页(20行),否则清单可能很快闪过,根本看不到(默然:其实也没关系,如果你是在Windows2000以上平台运行的话,控制台的右边有滚动条,你可以拉到前面,就看到所有的列表了)。还有,这是一个控制台应用程序,是不需要写html文件的,直接就可以运行了。
7.8 .1 创建并画出文本
在Java中,可以使用java.awt.Font包中的Font类。先创建一个Font对象,然后告诉Graphics2D容器要使用这个字体。Font类提供了两个构造函数来创建一个Font对象。我们主要研究以3个参数为输入的构造函数,这3个参数的描述如下:
q 字体的名字:它可以是一个字形的名字,或者是一个已经定义了的“逻辑”字体。如果使用的是一个逻辑字体,那么它必须是下列值中的一个:Dialog,DialogInput,Monospaced,Serif,SansSerif。如果没有指定字体名(null),则将使用默认字形。
q 字体的样式:常见的样式有Font.PLAN,Font.BOLD,Font.ITALIC。这个参数是一个int值(常量),因此,如果你想对字体设置多于一个的样式,可以使用位或操作符,比如:粗斜体可以写成这样的形式:Font.BOLD|Font.ITALIC(默然:其实位或运算也就是加法运算,所以写成Font.BOLD+Font.ITALIC也是可以的)。
q 字体的大小:呀,这个最简单了,任意整数值,相当于指定字体的像素值,如20。数字越大,字也就越大。
下面的代码创建一个Font对象,字体为Helnetica,样式为加粗,大小为1
Font font=new Font(“Helvetica”,Font.BOLD,1);
注意,前面提到过Font的构造函数使用字体尺寸为它的第三个参数。那么,为什么把大小指定为1呢?这是因为Java只是把字样看作Shape对象。所以,当绘制文本时,它会按照Graphics2D容器的当前转换来绘制。这意味着不会局限在一个特定的字体大小上,可以把大小设为1并使用AffineTransform来缩放字体。此外,还可以平移,旋转和剪裁字体。而且,字体的绘制附着于当前的Graphics2D Paint对象。这一切给了很多对绘制文本的控制手段。
我们使用Graphics2D的drawString方法。这个方法有3个参数:一个是要绘制的String,另外两个是绘制位置的x和y。可以用int坐标,也可以用float坐标。
我们来看一个例子。下面的程序FontTest,在屏幕上绘制带颜色的文本。它使用一个变换来移动,缩放文本,并把文本旋转到不同的位置。
import java.applet.*;
import java.awt.*;
import java.awt.font.*;
public class FontTest extends Applet{
//C绘制不同颜色的字体的Color常数
static final Color[] colors={Color.red,Color.blue,Color.orange,Color.DARK_GRAY};
//在屏幕上绘制一些文本
public void paint(Graphics g){
//记住转换为一个可用的Graphics2D对象
Graphics2D g2d=(Graphics2D)g;
//我们不需要字体的显式引用,所以只是用一行代码来指定它
//那些使用多种字体的applet可能需要对每一种字体都保存一份拷贝
g2d.setFont(new Font("楷体_GB2312",Font.BOLD,1));
//字体缩放,然后把它平移到屏幕中间
g2d.translate(150,150);
g2d.scale(18,18);
//使用每一种颜色来绘制"字体很有趣"
for(int i=0;i<colors.length;i++){
//设置当前颜色
g2d.setPaint(colors[i]);
//在(0,0)绘制字符串,g2d的变换会保证实际的绘制位置
g2d.drawString("字体太有趣啦~~",0,0);
//90度旋转
g2d.rotate(Math.PI/2.0);
}
}
}
注意,这个程序把文本绘制在(0,0)位置,并允许当前的Graphics2D转换来做其他的工作。可以变通一下,在文本的摆放中设置显式的值。然而,如果当前Graphics2D容器的转换不是恒等变换,它可能会转换过度而得不到想要的结果。所以建议采用安全的方法,在同一个位置来指定所有的变换。
7.8 .2 衍生字体
衍生字体只是复制给定Font对象,然后在它上面应用新的属性的一系列操作,其结果可能是一种有特定属性的全新的Font对象。
目前,Font类提供了6种不同的deriveFont方法来获取字体,每个方法都返回一个新创建的Font对象。对于上面的创建一个斜体字形的问题,可以使用以一个AffineTransform对象为参数的方法。特别地,可以提供一个向右倾斜一定角度的转换。
下面的代码段创建一个基本字体和一个倾斜AffineTransform,然后把当前字体设为用给定转换得到的自制字体。
//创建一个基本字体
Font baseFont=new Font(“Helvetica”,Font.BOLD,1);
//为字体创建一个变换
AffineTransform fontAT=new AffineTransform();
//向右倾斜基本字体
fontAT.shear(-0.75,0.0);
//使用上面的变换创建并设置一个衍生字体
Font derivedFont=baseFont.deriveFont(fontAT);
g2d.setFont(derivedFont);
//在这里做一些绘制操作(…)
Font类中其他的5个deriveFont方法也很有用。有的方法按照给定的尺寸和样式制造字体,可以查看Java 2文档来获取更多的信息。
7.8 .3 获取字体量度
除了创建和绘制字体外,还可以通过字体量度(font metrics)得到一个特定字体的很多信息。这包括字体的ascent(不同字符,比如大写字母的最大的高度),desent(像j,g,y这样的字符向下超出基线的最大距离)和容纳一串文本的外围矩形。
可能可以查询的字体度量中最有趣也是最有用的一个是字体的外围矩形。字符串的外围矩形是包围该字符串的最小矩形。要查获文本的外围矩形,可能需要一个FontRenderContext,一个TextLayout对象和一个Rectangle2D对象。
一个FontRenderContext对象包含文本如何度量的信息。不要自己创建这个类的实例,用Graphics2D类来创建。使用这种方式,可以保证FontRenderContext在特定的系统中正确表达文本信息。要得到字体绘制的容器,只需像下面这样调用Graphics2D类的getFontRenderContext方法:
//假设g2d指向一个有效的Graphics2D对象
FontRederContext frc=g2d.getFontRenderContext();
注意:记住,并不是所有的字体都可以在所有的系统中获得。因此,在决定使用哪种字体时,应该谨慎。显然,那些在最大数量的系统中可以广泛得到的字体比较好。如果在某一特定系统中指定了一个不存在的字体,Java会选择使用默认的字体。
现在使用一个TextLayout对象来获取字体的实际度量。TextLayout类为文本提供实际的字体度量信息,我们可以通过在构造函数中指定一个Font一个FontRenderContext和一个对其容量感兴趣的文本串来创建一个有用的TextLayout对象。
//假设font指向一个有效的Font对象
TextLayout layout=new TextLayout(“TextLayouts are your friend”,font,frc);
TextLayout对象包括根据上面的字符串,字体和绘制容器得到的字体的度量信息,现在可以直接从布局中得到度量信息。既然我们对外围矩形感兴趣,那么可以像下面这样调用TextLayout的getBounds方法:
Retangle2D textBounds=layout.getBounds();
注意文本串外围矩形的位置是相对TextLayout的,而不是相对于屏幕的位置,所以,如果想在文本周围绘制它的外围矩形,必须把这个矩形平移到和文本相同的位置。
下面的FontBoundsTest applet,把一个示例String对象和它的外围矩形一起画出来:
import java.applet.*;
import java.awt.*;
import java.awt.font.*;
import java.awt.geom.*;
public class FontBoundsTest extends Applet{
private final String MESSAGE="Trapped";
public void paint(Graphics g){
Graphics2D g2d=(Graphics2D)g;
//创建一个Font对象
Font baseFont=new Font("Helvetica",Font.PLAIN,50);
//得到Graphic2D容器的FontRenderContext
FontRenderContext frc=g2d.getFontRenderContext();
//使用上面的FontRenderContext,获取消息和字体的布局
TextLayout layout=new TextLayout(MESSAGE,baseFont,frc);
//得到布局的边框
Rectangle2D textBounds=layout.getBounds();
//在(45,50)绘制消息和边界矩形
g2d.setFont(baseFont);
g2d.setPaint(Color.black);
g2d.drawString(MESSAGE,45,50);
g2d.translate(45,50);
g2d.setPaint(Color.red);
g2d.draw(textBounds);
}
}
也可以把TextLayout的内容直接绘制到Graphics2D容器上,在有些情形下,用这种方式绘制文本可能是很有用的。可以像下面这样把文本直接绘制到Graphics2D容器上:
layout.draw(g2d,45,50);
还可以对字体做很多工作,比如创建可编辑文本,高亮选择和在文本字符上进行碰撞测试等。在第8章研究剪裁路径时我们还会看一些绘制技巧。
7.9 总结
在本章中,学到了很多帮助我们提高的基础知识。理解在Java中如何使用坐标系统,变换,图形的绘制和填充以及文本绘制,是帮助我们成长为一个Java 2-D专家的良好开端。在为游戏设计图像时,有很多不同的东西必须记在心。这是为什么希望本章成为游戏中开发可视化组件的有用参考的原因。
没有其他的图形图像包可以把如此多的力量蕴藏在原始的图形和线条绘制的使用中,使用stroke,paint和填充可以更好地实现图形和文本的绘制。最充分地使用这些技术可以帮助我们在游戏中做出看起来很别致的图像,而无须仅依赖于从文件中读取事先绘制好的图像。
在一章中介绍整个Java 2-D API可能会超出我们的接收能力,所以本书把复杂的Java 2-D分隔开来,并决定到此结束,把冲突检测,几何叠加,提示绘制和图像增强操作留到下一章。所以我们应该保持耐心,把下面的练习做完后再继续前进,在第8章中将继续讨论Java 2-D API的第二部分。
7.10 练习
7.1讨论:Graphics2D类相对于Graphics类的优点。
7.2讨论:为什么FlowLayout内绘制的组件(比如按钮,标签等)大部分是和分辨率无关的操作,而Java 2-D绘制操作可以被认为是依赖于窗体分辨率的。辨析为什么依赖于分辨率的操作对于游戏编程是可行的。
7.3使用实例建模重写MouseShapeTest。在绘制Polygon轮廓时注意使用规格化线条宽度的技术。
7.4TextureTest applet旋转了整个画面——包括几何图形和纹理。重写TextureTest让纹理在右角和applet的画面保持直角相交。
7.5修改CompositeTest applet,使用它允许用户设定盒子的个数,作一个applet参数,并且使用Random类的nextFloat方法来随机产生每一个盒子的透明度。
7.6写一段代码检查一个给定的字体在系统中是否可用。为什么首先检查字体的可用性比只是简单地使用它们要好?
7.7写一段代码使得applet窗体中的文本居中。在做这道题时,TexturePaint对象来给文本绘制纹理。