第17章 Java多媒体
随着科技的发展,计算机的普遍使用,记录数据的载体不再只限于文本、游戏,动画以及音频和视频数据已经广泛应用。Java中JDK自带的类库只能处理简单的多媒体数据,不能处理相对复杂的多媒体数据。我们可以借助一些外在的成熟的类库或工具处理多媒体数据。
本章介绍如何实现开发2D图形、3D图形、幸运52、三维弹球、贪吃的小蛇、拼图游戏、播放器和声音处理以及采钻游戏等。
实例167 幸运52(Applet)
幸运52游戏是对某种物品或商品的价格进行估算。在估计价格确定时,会弹出一个对话框提示价格是高是低,这样可以对物品或商品的价格进行重新调整。本实例介绍如何实现对商品的价格估计。
技术要点
实现幸运52游戏的技术要点如下:
• Applet编程不同于一般的Java程序,它能够嵌入到HTML网页中,并由支持Java的Web浏览器解释执行。Applet不需要主运行方法main(),它由Web浏览器中的Java虚拟机(JVM)调用执行。
• 为了实现图像面板或按钮面板需要使类继承Panel类。实现弹出对话框功能需要类继承Frame类并实现事件监听器接口中的方法。
实现步骤
(1)新建一个类名为TextLucky52.java。
(2)代码如下所示:
package com.zf.s17; //创建一个包 import java.applet.Applet; //引入类 import java.awt.BorderLayout; import java.awt.Button; import java.awt.Color; import java.awt.Frame; import java.awt.Graphics; import java.awt.Image; import java.awt.Label; import java.awt.MediaTracker; import java.awt.Panel; import java.awt.TextField; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.net.URL; class ImagePanel extends Panel { //加载图像的类 static String IMAGENAME = "http://localhost:8080/Demo/001.jpg";//要显示的图片名称 Image ImageObject; //要显示的图片对象 public void loadImage() { //把图片从文件中装载到图片对象中 URL url = null; try { url = new URL(IMAGENAME); //创建URL对象 } catch (Exception e) { //捕获异常 System.out.println("获得网址出错"+e.getMessage()); } ImageObject = getToolkit().getImage(url); //获得图片对象 //创建跟踪多种媒体对象状态的工具类 MediaTracker myTracker = new MediaTracker(this); myTracker.addImage(ImageObject, 1); //将图像对象添加到工具类中 try { myTracker.wait(); //等待加载 myTracker.checkAll(); //查看图像状态 } catch (Exception e) { } } public void paint(Graphics g) { //重载Panel来显示图片 g.drawImage(ImageObject, 135, 30, 55, 75, this); } } class ShowDialog extends Frame implements ActionListener{//设置弹出的窗口的类 Label label = new Label(); //声明一个标签 public ShowDialog(String info) { super(); setTitle("猜测结果--"); //设置窗口的标题 Panel dialogPanel = new Panel(); //创建面板 add(dialogPanel); //将面板添加到窗口中 dialogPanel.add(label); //面板添加标签 label.setText(info); //设置标签的内容 setSize(150, 100); //设置窗口的大小 setLocation(300, 500); //设置窗口的位置 Button btnOk = new Button("确定"); //创建一个确定按钮 btnOk.addActionListener(this); //按钮监听事件 dialogPanel.add(btnOk); //将按钮添加到面板中 show(); //显示窗口 } public void actionPerformed(ActionEvent event) { //单击确定按钮引发的事件 this.dispose(); //关闭窗口 } } public class TextLucky52 extends Applet implements ActionListener {//操作猜测商品价格的类 Panel panel = new Panel(); //创建面板 ImagePanel imagePanel = new ImagePanel(); //创建图片面板 Panel buttonPanel= new Panel(); //按钮面板 TextField txtField = new TextField(10); //创建一个文本框 Label label = new Label("请输入这个商品的估价:"); //创建标签 private int truePrice = 24; public TextLucky52() { super(); this.setLayout(new BorderLayout()); //设置布局 panel.add(label); //面板添加标签 panel.add(txtField); //面板添加文本框 Button btnStart = new Button("开始游戏"); //创建铵钮 Button btnOk = new Button("确定"); //创建"确定"按钮 Button btnCancel = new Button("取消"); //创建"取消"按钮 btnStart.setActionCommand("start"); //设置按钮激发事件的命令名称 btnStart.addActionListener(this); //安装事件监听器 btnOk.setActionCommand("ok"); btnOk.addActionListener(this); btnCancel.setActionCommand("cancel"); btnCancel.addActionListener(this); buttonPanel.add(btnStart); //按钮面板添加"开始"游戏按钮 buttonPanel.add(btnOk); //按钮面板添加"确定"按钮 buttonPanel.add(btnCancel); //按钮面板添加"取消"按钮 add(panel, BorderLayout.SOUTH); //窗口添加面板并设置面板在窗口中的位置 //窗口添加图像面板并设置图像面板在窗口中的位置 add(imagePanel, BorderLayout.CENTER); add(buttonPanel, BorderLayout.NORTH); //设置按钮面板的位置 setBackground(Color.white); //设置窗口的背景颜色 } public void actionPerformed(ActionEvent event) { //按钮监听事件 if (event.getActionCommand().equals("start")) { //判断按钮的命令名称 imagePanel.loadImage(); //调用方法加载图像 label.setText("请输入这个商品的估价:"); imagePanel.repaint(); //调用方法重新加载图像 } else if (event.getActionCommand().equals("ok")) { int guessPrice = 0; try { //获得文本框中的内容转成整数 guessPrice = Integer.parseInt(txtField.getText().trim()); String guess=priceCompare(guessPrice);//调用方法进行比较 new ShowDialog(guess); //打开一个对话框窗口 } catch (Exception e) { System.out.println("获得数据出现错误:"+e.getMessage()); } } else if (event.getActionCommand().equals("cancel")) { txtField.setText(""); //文本框置空 } } public String priceCompare(int guessPrice) { //判断比较价格 if (guessPrice == truePrice) { return "你猜对了"; } else if (guessPrice > truePrice) { return "你猜的价格过高"; } else if (guessPrice < truePrice) { return "你猜的价格过低"; } return "出错了"; } }
(3)程序第一次运行的结果如图17-1所示。
(4)当单击“开始游戏”按钮时,会出现一幅图,在下面的文本输入框中输入价格,单击“确定”按钮出现的页面如图17-2所示。
图17-1 初次运行结果
图17-2 执行后的结果
源程序解读
(1)ImagePanel类继承Panel面板类来实现图像的加载与查看。Java中不提供直接用来显示图片的Panel。首先创建URL对象用来获得图片信息,再根据getToolkit()方法取得Toolkit对象,然后使用getImage()方法来取得一张图片文件,在paint()方法中使用Graphics类的drawImage()方法可显示该图像。创建跟踪多种媒体对象状态的工具类并将图像添加进去,可以等待加载图像和查看图像的状态。
(2)ShowDialog类继承Frame类和实现ActionListener接口来操作实现一个对话框,实现该接口必须实现actionPerformed()事件监听方法。在ShowDialog类的构造方法中设置对话窗口的标题并添加一个面板,面板用来装载标签和确定按钮。按钮中添加一个监听事件,当单击按钮时触发事件。actionPerformed()方法处理当单击确定按钮时如何操作窗口,窗口的dispose()方法是将窗口关闭。
(3)TextLucky52类继承Applet类实现ActionListener接口,实现该接口必须实现接口的actionPerformed()方法。TextLucky52类的构造方法中设置窗口的布局和背景颜色。在窗口南侧的面板中添加标签和文本框,在窗口的中间位置面板中添加图像用来加载图片,在窗口北侧的面板中添加“开始游戏”按钮、“确定”按钮和“取消”按钮。
(4)TextLucky52类中的actionPerformed()方法判断按下的按钮的命令名称。如果是按下“开始游戏”按钮则加载图像、重新加载图像和设置标签的内容;如果是按下“确定”按钮则获取文本框中的内容并将内容转成整数与商品的实际价格进行比较,如果大于实际价格则弹出显示价格偏高的对话框,如果小于实际价格则弹出显示价格偏低的对话框。
实例168 三维弹球游戏(Java 3D)
Java 3D是对Java的一个用来显示三维图形的扩展。用Java 3D编写的程序可以运行在很多不同类型的计算机或互联网上。本实例介绍如何制作简单的三维弹球上下运动的游戏,按j键球将向右移动,按f键球将向左移动。在制作之前需要到官方网站下载开发用的文件,包括j3d-152-windows-i586.zip压缩文件和j3d-1_5_2-windows-i586.exe可执行文件,压缩文件解压缩后将有关的jar包文件放入项目的类库中,可执行文件进行安装。
技术要点
实现三维弹球游戏的技术要点如下:
• 显示3D物体的基本步骤:创建一个用来容纳场景的虚拟宇宙,再创建一个用来放置一组物体的数据结构,向组中添加物体,放置观察者(Viewer)使之面对物体,将物体组添加到虚拟宇宙。
• 三维弹球的运动使用java.swing.Timer类实现,球跳动起来触发Timer事件,通过变换三维变换对象和三维变换组将球移动到指定的位置来改变球的高度,当事件频繁发生时,就可以看到球运动的动画效果。
实现步骤
(1)创建一个类名为Text3DBall.java。
(2)代码如下所示:
package com.zf.s17; //创建一个包 import java.awt.BorderLayout; //引入类 import java.awt.Button; import java.awt.GraphicsConfiguration; import java.awt.Panel; import java.awt.event.*; import javax.media.j3d.*; import javax.swing.JFrame; import javax.swing.Timer; import javax.vecmath.*; import com.sun.j3d.utils.geometry.Sphere; import com.sun.j3d.utils.universe.SimpleUniverse; //操作三维弹球的类 public class Text3DBall extends JFrame implements ActionListener, KeyListener { private Button start = new Button("运 动"); //创建"开始"按钮 private TransformGroup tranGroup; //声明三维变换组 private Transform3D trans = new Transform3D(); //创建三维变换对象 private float X = 0.0f; //球X横坐标,正数为向右运动 private float Y = 0.0f; //球Y纵坐标,0表示在中心,正数表示向上运动 private float symbol = 1.0f; //球上下运动方向,1表示向上运动,-1表示向下运行 private Timer timeController; //定时器,球上下运动 public Text3DBall() { //构造方法进行初始化 this.getContentPane().setLayout( new BorderLayout()); //设置布局 GraphicsConfiguration config = SimpleUniverse .getPreferredConfiguration(); //获得简单的宇宙配置 Canvas3D c = new Canvas3D(config); //根据配置创建3D画布 this.getContentPane().add("Center", c); //设置居中面板 c.addKeyListener(this); //窗口添加键盘监听器 timeController = new Timer(100, this); //创建一个定时器 Panel panel = new Panel(); //创建一个面板 panel.add(start); //面板添加"开"始按钮 this.getContentPane().add("South", panel); //面板放在窗口的南面 start.addActionListener(this); //按钮添加监听器 start.addKeyListener(this); //按钮键盘监听器 BranchGroup branch = createSceneGraph(); //创建容纳物体的结构(场景) SimpleUniverse universe = new SimpleUniverse(c);//根据3D画布创建简单虚拟宇宙 universe.getViewingPlatform() .setNominalViewingTransform(); //注视球体 universe.addBranchGraph(branch); //虚拟宇宙添加场景 } public BranchGroup createSceneGraph() { //创建三维球 BranchGroup root = new BranchGroup(); //创建容纳球的结构 Sphere sphere = new Sphere(0.25f); //创建一个球体并加入到物体组 tranGroup = new TransformGroup(); //声明三维变换组 //允许其编译或激活之后动态添加子节点 tranGroup.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); Transform3D transform3D=new Transform3D(); //创建三维变换对象 //设置Transform移动物体到指定位置 transform3D.setTranslation(new Vector3f(0.0f, 0.0f, 0.0f)); tranGroup.setTransform(transform3D); //三维变换组添加三维变换对象 tranGroup.addChild(sphere); //添加子节点 root.addChild(tranGroup); //指定一个中心点和球的半径来设置一个区域球体 BoundingSphere bounds = new BoundingSphere(new Point3d(0.0, 0.0, 0.0), 1000.0); Color3f color=new Color3f(1.8f,0.1f,0.1f); //创建一个从在点延伸100米的红色光源 Vector3f direction=new Vector3f(4.0f,-7.0f,-12.0f);//为物体指定位置 DirectionalLight light = new DirectionalLight(color, direction); //创建定向光源 light.setInfluencingBounds(bounds); root.addChild(light); //设置环境光 Color3f ambientColor=new Color3f(1.0f,1.0f,1.0f); AmbientLight ambientLightNode = new AmbientLight(ambientColor); ambientLightNode.setInfluencingBounds(bounds); root.addChild(ambientLightNode); return root; } public void keyPressed(KeyEvent e) { //键盘按下的事件 if (e.getKeyChar() == 'j') { //按下j键 X = X + 0.1f; } if (e.getKeyChar() == 'f') { //按下f键 X = X - 0.1f; } } public void keyTyped(KeyEvent e) { //有字符被输入事件 } public void keyReleased(KeyEvent e) { //键被弹起事件 } public void actionPerformed(ActionEvent e) { //监听按钮事件和定时器 if (e.getSource() == start) { //按下开始按钮 if (!timeController.isRunning()) { timeController.start(); //启动定时器 } } else { Y += 0.1 * symbol; //定时器事件 if (Math.abs(Y * 2) >= 1) { //到端点处变换方向 symbol = -1.0f * symbol; } if (Y < -0.4f) { //设置适当标度的对象 trans.setScale(new Vector3d(1.0, 0.8, 1.0)); } else { trans.setScale(new Vector3d(1.0, 1.0, 1.0)); } //将球再变换到指定的位置 trans.setTranslation(new Vector3f(X, Y, 0.0f)); tranGroup.setTransform(trans); } } public static void main(String[] args) { //Java程序主入口处 Text3DBall text = new Text3DBall(); //实例化对象 text.addKeyListener(text); //添加键盘监听器 text.setSize(500, 400); //设置窗口大小 text.setTitle("三维弹球游戏"); //设置标题 text.setVisible(true); //可视 text.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); //设置默认关闭操作 } }
(3)程序第一次运行的结果如图17-3所示。
(4)小球是动态的,它的运动轨迹如图17-4所示。
图17-3 初次运行结果
图17-4 小球的运动轨迹
源程序解读
(1)Text3DBall类中X表示球的横坐标即球的水平坐标,0.0f表示球居中,正数则表示球向右移动,负数表示球向左移动。Y表示球的纵坐标即球的高度,0.0f表示球居中,正数表示球向上运动,负数表示球向下运动。symbol表示球的运动方向。通过键盘监听事件与定时器的事件可以改变它们的属性值。创建三维变换对象和三维变换组来实现三维球的位置的变换,球的跳动效果即可实现。
(2)在类的构造方法中设置窗口的布局格式为BorderLayout边界布局,它可以将容器分成5个区域来安排组件:North、South、East、West和Center。每个区域最多只能包含一个组件,并通过相应的常量进行标识:NORTH、SOUTH、EAST、WEST和CENTER。当使用这种边界布局将一个组件添加到容器中时,要使用这5个常量之一。创建简单的宇宙配置并根据配置设置一个3D画布,居中放在当前面板上并添加键盘监听器。创建一定时器供变换球的位置时所用。创建一面板放在窗口的南侧并添加键盘监听事件和按钮监听事件。创建一个场景,根据3D画布创建简单虚拟宇宙将球体注入,并将场景添加到虚拟宇宙中。
(3)createSceneGraph()方法是创建一个三维球。先创建一个容纳球的结构和一个球体,对球体进行三维变换,指定三维球的中心点和半径并设置一个红色光源指向三维球。
(4)keyPressed()是键盘按下的事件,当按下j键时球向右移动,当按下f键时球向左移动。keyTyped()是有字符被输入时的事件,如按下Shift再按A键,如果当时Caps Lock不亮,就产生一个输入大写A的事件。keyReleased()是键被弹起时引发的事件。在Java中并没有文本监听,可以用keyTyped、keyPressed、keyReleased来实现对文本框的监听。
(5)actionPerformed()方法是类实现ActionListener接口必须实现的方法。当按下运动按钮时启动定时器。当球距离地面很近时,通过Transform3D类的setScale()方法将球缩放成一个椭球,缩放的参数是一个Vector3d对象,值为(1.0, 0.8, 1.0),即横坐标和Z方向上长度与原球一样,但纵坐标方向上长度变为原球的4/5,这样实现球被压扁的效果。
实例169 贪吃的小蛇
本实例用Applet实现4种级别的玩法,可以通过方向键控制蛇的运动来靠近前面的食物并吃掉食物,当碰到墙壁时作为游戏失败。
技术要点
实现贪吃的小蛇的技术要点如下:
• 实现4个级别,分别为PRIMARY(初级)、INTERMEDIATE(中级)、SENIOR(高级)和EXTRA(特级),选择级别进入游戏界面。
• 方向键用来控制蛇的运动。小蛇向着食物的方向前进,如果不小心碰到墙壁则游戏结束,上方显示本次的成绩(得分)。小蛇本身在直行时由小段组成,碰到拐弯变成两段,每段由黑色的方格组成。
实现步骤
(1)创建一个类名为TextEdaciousSmallSnake.java。
(2)代码如下所示:
package com.zf.s17; //创建一个包 import java.util.*; //引入类 import java.awt.*; import java.awt.event.*; import javax.swing.*; class Move { //小蛇或食物位置(方位)移动 int move_X; int move_Y; Move(int move_X, int move_Y) { //带参数构造方法进行初始化 this.move_X = move_X; this.move_Y = move_Y; } } class MoveOperate extends Observable implements Runnable{//小蛇和食物移动操作的类 public static final int LEFT = 1; //小蛇向左移动的标识 public static final int UP = 2; //小蛇向上运动的标识 public static final int RIGHT = 3; //小蛇向右运动的标识 public static final int DOWN = 4; //小蛇向下运动的标识 private boolean[][] isHave; //指示位置上是否有小蛇或食物 private LinkedList snake = new LinkedList(); //声明小蛇的双链表集合 private Move aliment; //食物 private int move_Direction = LEFT; //小蛇移动的方向向左 private boolean running = false; //运行状态 private int timeSpace = 300; //时间间隔 private double speedChange = 0.75; //每次的速度变化率 private boolean paused = false; //暂停标志 private int score = 0; //得分 private int moveCount = 0; //吃到食物前移动的次数 private int X; //横坐标 private int Y; //纵坐标 public MoveOperate(int X, int Y) { //带参数构造方法进行初始化 this.X = X; this.Y = Y; resetGame(); } public void run() { //实现Runnable接口须实现的方法 running = true; //标识设置为真 while (running) { //根据标识进行循环 try { Thread.sleep(timeSpace); //休眠300ms } catch (Exception e) { System.out.println("休眠出错:"+e.getMessage()); break; //跳出循环 } if (!paused) { //小蛇正在移动时 if (move()) { setChanged(); //更新界面数据 notifyObservers(); } else { //弹出对话框显示游戏结束 JOptionPane.showMessageDialog(null, "你失败了!", "Game Over", JOptionPane.INFORMATION_MESSAGE); break; //跳出循环 } } } if (!running) { //小蛇暂停或停止 JOptionPane.showMessageDialog(null, "你停止了游戏", "Game Over", JOptionPane.INFORMATION_MESSAGE); } } public void resetGame() { //重置游戏 move_Direction = MoveOperate.LEFT; //小蛇移动的方向向左 timeSpace = 300; //时间间隔为300毫秒 paused = false; //不暂停 score = 0; moveCount = 0; //吃到食物前移动的次数为0 isHave = new boolean[X][Y]; //初始化小蛇活动的范围 for (int i = 0; i < X; i++) { isHave[i] = new boolean[Y]; Arrays.fill(isHave[i], false); //填充数据 } //初始化小蛇,如果横向位置超过20个,则长度为10,否则为横向位置的一半 int initLength = X > 20 ? 10 : X / 2; snake.clear(); int x = X / 2; //初始居中显示 int y = Y / 2; for (int i = 0; i < initLength; i++) { snake.addLast(new Move(x, y)); //添加蛇移动的位置 isHave[x][y] = true; x++; } aliment = createAliment(); //创建食物 isHave[aliment.move_X][aliment.move_Y] = true; //在指定位置上有蛇或食物 } public boolean move() { //蛇每移动一步 Move snakeHead = (Move) snake.getFirst(); //获得蛇头的位置 int headX = snakeHead.move_X; //获得蛇的横坐标 int headY = snakeHead.move_Y; //获得蛇的纵坐标 switch (move_Direction) { //分支循环判断蛇的运动 case UP: //向上移动 headY--; //纵坐标减1 break; //跳出分支循环 case DOWN: //向下移动 headY++; //纵坐标加1 break; case LEFT: //向左移动 headX--; //横坐标减1 break; case RIGHT: //向右移动 headX++; //横坐标加1 break; } if ((0 <= headX && headX < X) && (0 <= headY && headY < Y)) { if (isHave[headX][headY]) { //如果指定位置有蛇或食物 if (headX == aliment.move_X && headY == aliment.move_Y) { snake.addFirst(aliment); //蛇头增长表示吃到食物 //分数与改变方向次数和速度有关 int scoreGet = (10000 - 200 * moveCount) / timeSpace; score += scoreGet > 0 ? scoreGet : 10; moveCount = 0; aliment = createAliment(); //创建新的食物 //设置食物所在位置 isHave[aliment.move_X][aliment.move_Y] = true; return true; } else { return false; //小蛇吃到自己 } } else { snake.addFirst(new Move(headX,headY)); //添加小蛇的身体 isHave[headX][headY] = true; snakeHead = (Move)snake.removeLast(); //获得蛇头信息 isHave[snakeHead.move_X][snakeHead.move_Y] = false; moveCount++; return true; } } return false; //碰到外壁失败 } public void changeDirection(int dir) { //改变蛇运动的方向 if (move_Direction % 2 != dir % 2) { //改变的方向不与原方向相同或相反 move_Direction = dir; } } private Move createAliment() { //创建食物 int x = 0; int y = 0; do { Random r = new Random(); //随机获取位置 x = r.nextInt(X); y = r.nextInt(Y); } while (isHave[x][y]); return new Move(x, y); //返回食物的新位置 } public void speedUp() { //加速运行 timeSpace *= speedChange; } public void speedDown() { //减速运行 timeSpace /= speedChange; } public void changePauseState() { //改变暂停状态 paused = !paused; } public boolean isRunning() { //判断是否运动 return running; } public void setRunning(boolean running) { //设置运动标识 this.running = running; } public LinkedList getMoveList() { //获得链表数据(蛇的身体) return snake; } public Move getAliment() { //获得食物 return aliment; } public int getScore() { //获得得分 return score; } } class SnakeFrame extends JFrame implements Observer { //贪吃蛇的界面 public static final int gridWidth = 10; //格子的宽度 public static final int gridHeight = 10; //格子的高度 private int gameWidth; //画面的宽度 private int gameHeight; //画面的高度 private int gameX = 0; //画面左上角横位置 private int startY = 0; //画面左上角纵坐标 JLabel score; //声明分数标签 Canvas canvas; //声明画布 public SnakeFrame() { //默认构造方法调带参数构造方法 this(30, 40, 0, 0); } public SnakeFrame(int X, int Y) { //带参数的构造方法进行初始化 this(X, Y, 0, 0); } public SnakeFrame(int X, int Y, int startX, int startY) {//带参数的构造方法进行初始化 this.gameWidth = X * gridWidth; this.gameHeight = Y * gridHeight; this.gameX = startX; this.startY = startY; init(); } private void init() { //初始化游戏界面 this.setTitle("贪吃的小蛇"); //设置界面的标题 this.setLocation(gameX, startY); //设置界面的位置(方位) Container cp = this.getContentPane(); //获得一容器 score = new JLabel("成绩:"); cp.add(score, BorderLayout.SOUTH); //将成绩放在界面的南侧 canvas = new Canvas(); //创建中间的游戏显示区域 canvas.setSize(gameWidth + 1, gameHeight + 1); //设置区域的大小 cp.add(canvas, BorderLayout.CENTER); //区域放在界面的中间 this.pack(); //根据组件的最优尺寸来进行布局 this.setResizable(false); //窗体不能最小化 this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//设置默认关闭操作 this.setVisible(true); //可视 } public void update(Observable observer, Object obj) {//实现Observer接口定义的update方法 MoveOperate operate = (MoveOperate)observer; //获取被监控的模型 Graphics graphics = canvas.getGraphics(); //获得画笔 graphics.setColor(Color.WHITE); //设置颜色为白色 graphics.fillRect(0, 0, gameWidth, gameHeight); //填充一个矩形 graphics.setColor(Color.BLACK); //重新设置颜色 LinkedList list = operate.getMoveList(); //获得蛇体 Iterator it = list.iterator(); while (it.hasNext()) { Move move = (Move) it.next(); drawMove(graphics, move); //调用方法画方格 } graphics.setColor(Color.RED); //设置颜色表示食物 Move move = operate.getAliment(); //获得食物 drawMove(graphics, move); this.updateScore(operate.getScore()); //更新成绩 } private void drawMove(Graphics g, Move n) { //根据移动路径画格子 g.fillRect(n.move_X * gridWidth, n.move_Y * gridHeight, gridWidth - 1, gridHeight - 1); //填充一个矩形 } public void updateScore(int score) { //更新成绩 String s = "成绩:" + score; this.score.setText(s); //设置成绩标签的值 } } class ControlSnake implements KeyListener { //控制蛇的运动 private MoveOperate snake; //贪吃的小蛇模型 private SnakeFrame frame; //蛇的视图对象 private int X; //蛇移动区域横坐标 private int Y; //蛇移动区域纵坐标 public ControlSnake() { //默认构造方法进行初始化 this.X = 30; this.Y = 40; } public ControlSnake(int X, int Y) { //带参数的构造方法进行初始化 this(); if ((10 < X) && (X < 200) && (10 < Y) && (Y < 200)) { //判断移动区域 this.X = X; this.Y = Y; } else { System.out.println("初始化参数出错!"); } initSnake(); //调用初始化方法 } private void initSnake() { //初始化 this.snake = new MoveOperate(X, Y); //创建蛇模型 this.frame = new SnakeFrame(X, Y, 500, 200); this.snake.addObserver(this.frame); //为模型添加对象 this.frame.addKeyListener(this); //添加键盘事件 (new Thread(this.snake)).start(); //启动线程蛇运动 } public void keyPressed(KeyEvent e) { //键盘按下事件 int keyCode = e.getKeyCode(); //只有在贪吃蛇处于运行状态下,才处理的按键事件 if (snake.isRunning()) { switch (keyCode) { case KeyEvent.VK_ADD: //按下数字键盘上的+ case KeyEvent.VK_PAGE_UP: //按下PageUp键 snake.speedUp(); break; case KeyEvent.VK_SUBTRACT: //按下数字键盘上的- case KeyEvent.VK_PAGE_DOWN: //按下PageDown snake.speedDown(); break; case KeyEvent.VK_SPACE: //按下空格键 case KeyEvent.VK_P: //按下PauseBreak snake.changePauseState(); break; case KeyEvent.VK_UP: //按下向上键 snake.changeDirection(MoveOperate.UP); break; case KeyEvent.VK_DOWN: //按下向下键 snake.changeDirection(MoveOperate.DOWN); break; case KeyEvent.VK_LEFT: //按下向左键 snake.changeDirection(MoveOperate.LEFT); break; case KeyEvent.VK_RIGHT: //按下向右键 snake.changeDirection(MoveOperate.RIGHT); break; default: } } if (keyCode == KeyEvent.VK_F || keyCode == KeyEvent.VK_J || keyCode == KeyEvent.VK_ENTER) { //按下Enter键、F、J键开始游戏 snake.resetGame(); } if (keyCode == KeyEvent.VK_S) { //按下S键停止游戏 snake.setRunning(false); } } public void keyReleased(KeyEvent e) { //键盘弹起事件 } public void keyTyped(KeyEvent e) { //有字符被输入事件 } } public class TextEdaciousSmallSnake { //操作实现小蛇吃食物的操作 public static void main(String[] args) { //Java程序主入口处 new ControlSnake(40,30); //实例化对象 } }
(3)运行结果如图17-5所示。
(4)按下Enter键、F、J键开始游戏,就可以玩游戏,如图17-6所示。按下S键即可停止游戏。
图17-5 开始游戏界面
图17-6 进行中的游戏
源程序解读
(1)Move类设置小蛇或食物的位置,其中move_X表示小蛇或食物所在的横坐标, move_Y表示小蛇或食物所在的纵坐标。
(2)MoveOperate类实现Runnable接口,实现接口必须实现接口的run()方法,继承Observable类表时该类是一个被观察的对象,当它有任何修改操作时都在别人的观察控制中。声明isHave二维布尔数组来存储游戏区域各个位置的状态,如果数值为真则表明在该位置上有小蛇的身体或食物,否则没有。声明snake来存储蛇身体的各个部分,由于蛇身和蛇尾都要操作,而LinkedList是双向链表,在操作数据的头和尾时速度相对其他的集合都是比较快的。声明move_Direction表明小蛇初始是向左移动。声明timeSpace来控制小蛇移动的间隔时间,时间越小,小蛇移动的越快。声明running标识小蛇是否在移动。声明moveCount来获得小蛇在吃到食物前移动的次数,移动的次数越小,则分数越高。声明四个静态变量来操作小蛇移动的方向。
(3)MoveOperate类的run()方法根据标识为真进行循环判断小蛇的状态,如果小蛇正在移动,那么调用Observable类的setChanged()方法标识被观察者对象发生改变,调用Observable类的notifyObservers()方法通知该对象的观察者,否则弹出显示游戏结束的对话框。如果小蛇暂停或停止,则弹出停止游戏的对话框。
(4)MoveOperate类的resetGame()方法重置游戏,初始化小蛇移动的方向、时间间隔、在吃到食物之前移动的次数以及初始化蛇的身体和食物等。循环中Array.fill()方法将数组的元素统一赋值为false。move()方法是小蛇移动一步。通过双向链表集合LinkedList的getFirst()方法取得小蛇的蛇头的信息,包括其所在的横坐标和纵坐标。可以用来修改小蛇头的坐标,如果修改后的坐标落在游戏区域内,而且该位置上没有蛇的身体,则允许移动,将小蛇的头的新坐标构建一个新的对象,通过LinkedList类的addFirst()方法可以将新小蛇头添加到蛇的身体中,通过LinkedList类的removeLast()方法去掉小蛇的尾部,小蛇的长度保持不变,整体前进一步。如果修改后的蛇头位置上有食物,则吃掉食物,把食物位置添加到小蛇的头部,小蛇的尾部保持不变,可以看作是小蛇的身体长度加1,同时增加成绩值。成绩值与小蛇移动的次数和移动的频率有关。在计算成绩时,随机创建一个新的食物。
(5)MoveOperate类的changeDirection()方法改变小蛇运动的方向,如果小蛇的前进方向与被改变的方向不在同一直线上,才改变小蛇前进的方向。createAliment()方法在游戏区域随机生成一个食物类,Random类的nextInt()方法生成一个随机的整数作为坐标,如果坐标上没有小蛇的身体,则将该坐标上放食物,否则继续生成新的坐标,直到新的食物生成。
(6)SnakeFrame类继承JFrame类实现Observer接口,实现该接口须实现update()方法,表明该类是一个观察者对象,可以观察别人的一切变化情况。SnakeFrame类的构造方法中初始化格子的宽度和高度、画面的宽度和高度。init()方法初始化一个游戏界面。设置界面的标题和初始位置,创建一个显示分数的标签,创建一个容器用来放置小蛇和食物。
(7)SnakeFrame类的update()方法实现了Observer接口,作为一个观察者对象,当观察的对象发生变化或改变时,会调用该方法。即当被观察者MoveOperate对象调用notifyObservers ()方法时,观察者SnakeFrame对象将会收到信息,并且调用update()方法做相应的修改处理。其从方法的参数中获得被观察者对象,转换成MoveOperate对象。通过Canvas类的getGraphics()方法获得画板的上下文环境Graphics,Graphics的fillRect()方法画一个矩形,表示游戏的区域,通过MoveOperate类的getMoveList()和getAliment方法获得小蛇和食物的数据,实质上是一组Move对象,调用drawNode()方法画Move。
(8)SnakeFrame类的drawMove()方法画Move对象,一个Move表示一个坐标,赋一定宽度和高度可以画一个矩形,使用Graphics类的fillRect()方法可以画一个矩形。
(9)ControlSnake类实现KeyListener接口来处理键盘事件必须实现keyPressed()方法、keyReleased()方法和keyTyped()方法。initSnake()方法初始化游戏界面和小蛇以及食物。创建MoveOperate对象和SnakeFrame对象,再通过addObserver()方法将对象SnakeFrame添加到MoveOperate的观察者中。调用addKeyListener()方法为SnakeFrame添加键盘监听器,用于处理键盘事件。启动小蛇游戏的线程使小蛇移动起来。
(10)ControlSnake类的keyPressed()方法根据参数KeyEvent获得被按下键盘的码,不同的码调用不同的方法实现不同的操作。
实例170 有趣的拼图游戏
拼图游戏是一个Applet程序。游戏可以将一张大图打乱成9张小图,然后在游戏中任挑选8张小图放在界面中几个任意的位置。通过鼠标来移动打乱的这8张图,使其恢复到预览中看到的效果。拼凑成功则游戏结束。本实例介绍如何制作拼图游戏,并可以选择多组打乱的图片进行拼凑。
技术要点
实现有趣的拼图游戏的技术要点如下:
• 使用Math包中的random()方法,可以使游戏中的图片每次初始的状态都不一样。创建按钮图标类来加载图片。运用鼠标的监听事件方法处理单击的图片。
• Graphics类的getGraphics()方法和drawImage()等方法可以实现大图的分割。
实现步骤
(1)新建两个类,名称分别为PanelOfImage.java、TextImagePieceTogether.java。
(2)代码如下所示:
/**--------文件名:PanelOfImage.java-------------*/ package com.zf.s17; //创建一个包 import java.awt.Rectangle; //引入类 import java.awt.event.*; import javax.swing.*; class PaneButton extends JButton { //继承按钮类实现加图片的方格 PaneButton(Icon icon) { //构造方法进行初始化,设置图标 super(icon); this.setSize(100, 100); //设置每个方格的大小 } public void move(String direction, int sleep) { //方格的移动 if (direction == "UP") { //方格向上移动 this.setLocation(this.getBounds().x, this.getBounds().y - 100); } else if (direction == "DOWN") { //方格向下移动 this.setLocation(this.getBounds().x, this.getBounds().y + 100); } else if (direction == "LEFT") { //方格向左移动 this.setLocation(this.getBounds().x - 100, this.getBounds().y); } else { //方格向右移动 this.setLocation(this.getBounds().x + 100, this.getBounds().y); } } } //图片面板加载方格对象 public class PanelOfImage extends JPanel implements MouseListener { boolean hasAddActionListener = false; //设置方格动作监听器的标识 PaneButton pane[]; //声明方格 Rectangle nullPanel; //声明空方格,没有添图片 public static int currentPID = 1; //当前选择的图片编号 public PanelOfImage() { //构造方法进行初始化 this.setLayout(null); //设置面板的布局为空 this.setSize(400, 400); //设置面板的大小 nullPanel=new Rectangle(200,200,100,100); //设置空方格的位置 pane = new PaneButton[9]; //创建9个方格 Icon icon; //声明图标 for (int i = 0; i < 3; i++) { //循环为每个方格加载图片 for (int j = 0; j < 3; j++) { //循环列 icon = new ImageIcon("picture/pic_" + currentPID + "_" + (i * 3 + j + 1) + ".jpg"); //创建图标 pane[i*3+j]=new PaneButton(icon);//创建方格,在方格中加载图片 pane[i * 3 + j].setLocation(j * 100, i * 100); //设置方格的位置 this.add(pane[i * 3 + j]); //面板添加方格 } } this.remove(pane[8]); //移除多余的方格 } public boolean isFinish() { //判断是否拼凑成功 for (int i = 0; i < 8; i++) { int x = pane[i].getBounds().x; int y = pane[i].getBounds().y; if (y / 100 * 3 + x / 100 != i) return false; } return true; } public void reLoadPicture() { //重新加载图片,在重新选择图片时 Icon icon; for (int i = 0; i < 3; i++) { //循环为每个方格加载图片 for (int j = 0; j < 3; j++) { icon = new ImageIcon("picture/pic_" + currentPID + "_" + (i * 3 + j + 1) + ".jpg"); pane[i * 3 + j].setIcon(icon); } } } public void breakRank() { //方格打乱重新排序 //当第一个方格距左上角较近时 while (pane[0].getBounds().x <= 100 && pane[0].getBounds().y <= 100) { int x = nullPanel.getBounds().x; int y = nullPanel.getBounds().y; //随机产生一数字对应空方格的上下左右移动 int direction = (int) (Math.random() * 4); //空方格左移动,与左侧方格互换位置,左侧方格右移动 if (direction == 0) { x -= 100; //空方格向左移动 if (test(x, y)) { for (int j = 0; j < 8; j++) { //循环寻找左侧的按钮 //依次寻找左侧的按钮 if ((pane[j].getBounds().x == x) && (pane[j].getBounds().y == y)) { pane[j].move("RIGHT", 100); //方格向右移动一格 nullPanel.setLocation(x, y);//重新设置空方格的位置 break; //跳出循环 } } } } else if (direction == 1) { //空方格向右移动 x += 100; if (test(x, y)) { for (int j = 0; j < 8; j++) { if ((pane[j].getBounds().x == x) && (pane[j].getBounds().y == y)) { pane[j].move("LEFT", 100); //方格向左移动一格 nullPanel.setLocation(x, y); break; } } } } else if (direction == 2) { //空方格向上移动 y -= 100; if (test(x, y)) { for (int j = 0; j < 8; j++) { if ((pane[j].getBounds().x == x) && (pane[j].getBounds().y == y)) { pane[j].move("DOWN", 100); //方格向下移动一格 nullPanel.setLocation(x, y); break; } } } } else { //空方格向下移动 y += 100; if (test(x, y)) { for (int j = 0; j < 8; j++) { if ((pane[j].getBounds().x == x) && (pane[j].getBounds().y == y)) { pane[j].move("UP", 100); //方格向上移动一格 nullPanel.setLocation(x, y); break; } } } } } if (!hasAddActionListener) //判断是否添加动作事件 for (int i = 0; i < 8; i++){ //循环为每个方格添加动作事件 pane[i].addMouseListener(this); } hasAddActionListener = true; } private boolean test(int x, int y) { //检测方格是否在指定的范围内移动 if ((x >= 0 && x <= 200) || (y >= 0 && y <= 200)) return true; else return false; } public void mouseClicked(MouseEvent arg0) { //鼠标单击时调用 } public void mouseEntered(MouseEvent arg0) { //鼠标进入组件区域时调用 } public void mouseExited(MouseEvent arg0) { //控制鼠标不能移动出面板的范围 } public void mouseReleased(MouseEvent arg0) { //鼠标按键在组件上释放时调用 } public void mousePressed(MouseEvent event) { //鼠标按下时调用 PaneButton button = (PaneButton) event.getSource();//获得鼠标按下的方格按钮 int x1 = button.getBounds().x; //获得该方格按钮的横坐标 int y1 = button.getBounds().y; //获得该方格按钮的纵坐标 int nullDir_X = nullPanel.getBounds().x; //得到空方格的横坐标 int nullDir_Y = nullPanel.getBounds().y; //得到空方格的纵坐标 if (x1 == nullDir_X && y1 - nullDir_Y == 100)//进行比较,如果满足条件则交换 button.move("UP", 100); //方格向上移动 else if (x1 == nullDir_X && y1 - nullDir_Y == -100) button.move("DOWN", 100); //方格向下移动 else if (x1 - nullDir_X == 100 & y1 == nullDir_Y) button.move("LEFT", 100); //方格向左移动 else if (x1 - nullDir_X == -100 && y1 == nullDir_Y) button.move("RIGHT", 100); //方格向右移动 else return; nullPanel.setLocation(x1, y1); //重新设置空方格的位置 this.repaint(); //重新加载 if (this.isFinish()) { //进行是否完成的判断 JOptionPane.showMessageDialog(this, "恭喜你,完成拼图"); for (int i = 0; i < 8; i++){ //循环撤销鼠标事件 pane[i].removeMouseListener(this); } hasAddActionListener = false; } } } /**--------文件名:TextImagePieceTogether.java-------------*/ package com.zf.s17; //创建一个包 import java.awt.*; //引入类 import java.awt.event.*; import javax.swing.*; //操作实现拼图的游戏的类 public class TextImagePieceTogether extends JFrame implements ActionListener { PanelOfImage imagePanel; //声明图片面板 JPanel panelOfSouth, panelOfLook; //声明南侧面板和查看面板 Button startButton; //声明开始按钮 Button lookButton; //声明查看按钮 Button chooseButton; //声明选择按钮 Container container; //容器,得到内容面板 public TextImagePieceTogether() { //构造方法进行初始化 container = this.getContentPane(); //获得内容面板 startButton = new Button("开始"); //创建开始按钮 startButton.addActionListener(this); //添加监听事件 lookButton = new Button("查看"); lookButton.addActionListener(this); chooseButton = new Button("选择"); chooseButton.addActionListener(this); panelOfLook = new JPanel(); //创建查看面板 panelOfLook.setLayout(null); //设置布局 Icon icon = new ImageIcon("pictrue/pic_" + PanelOfImage.currentPID + ".jpg"); //创建图标 JLabel label = new JLabel(icon); //创建图标标签 label.setBounds(0, 0, 300, 300); //设置标签的位置 panelOfLook.add(label); //添加标签 panelOfSouth = new JPanel(); //创建南侧面板 panelOfSouth.setBackground(Color.red); //设置背景颜色 panelOfSouth.add(startButton); //添加开始按钮 panelOfSouth.add(lookButton); //添加查看按钮 panelOfSouth.add(chooseButton); //添加选择按钮 imagePanel = new PanelOfImage(); //创建图片面板 container.add(imagePanel, BorderLayout.CENTER); container.add(panelOfSouth, BorderLayout.SOUTH); this.setTitle("拼图游戏"); //设置标题 this.setLocation(300, 200); //设置位置 this.setSize(308, 365); //设置大小 this.setResizable(false); //设置是否可以通过某个用户操作调整 this.setVisible(true); //设置可视 this.setDefaultCloseOperation(3); //设置默认关闭操作 } public void actionPerformed(ActionEvent event) { //按钮触发的事件 Button button = (Button) event.getSource(); //获得事件按钮源 if (button == startButton) { //如果是开始按钮 imagePanel.breakRank(); //调用图片方格打乱方法 } else if (button == lookButton) { //如果是查看事件 if (button.getLabel() == "查看") { //如果按钮标签为"查看" container.remove(imagePanel); //容器移除图片面板 container.add(panelOfLook); //容器添加查看标签 panelOfLook.updateUI(); //不用调整大小就可以出现新增加或删除的组件 container.repaint(); //重绘 button.setLabel("返回"); //设置按钮标签 } else { container.remove(panelOfLook); //容器移除查看面板 container.add(imagePanel); //容器添加图片面板 container.repaint(); //重绘 button.setLabel("查看"); } } else if (button == chooseButton) { //如果是选择的按钮 Choice choice = new Choice(); //创建选择器 choice.add("--小猫--"); //添加列表项 choice.add("--QQ--"); int i = JOptionPane.showConfirmDialog(this, choice, "选择图片", JOptionPane.OK_CANCEL_OPTION);//弹出对话框 if (i == JOptionPane.YES_OPTION) { //选择对话框的确定按钮 //获得列表项的编号 PanelOfImage.currentPID = choice.getSelectedIndex() + 1; imagePanel.reLoadPictrue(); //图片重载 Icon icon = new ImageIcon("pictrue/pic_" + PanelOfImage.currentPID + ".jpg");//获得图片图标 JLabel label = new JLabel(icon); //根据图标设置标签 label.setBounds(0, 0, 300, 300); //设置标签的方位 panelOfLook.removeAll(); panelOfLook.add(label); panelOfLook.repaint(); } } } public static void main(String[] args) { //Java程序主入口处 new TextImagePieceTogether(); //实例化对象 } }
(3)运行结果如图17-7所示。
(4)游戏完成之后,也就是将图组好后的页面如图17-8所示。
图17-7 组图的运行界面
图17-8 完成组图
源程序解读
(1)PanelOfImage类继承按钮类实现按钮添加图片的方格的功能。其构造方法设置每个方格的大小并设置图标。move()方法是移动指定的方格,根据传递的参数设置方格在窗口的位置来看作是移动方格。
(2)PanelOfImage类继承面板类实现鼠标监听事件接口实现面板加载方格按钮。hasAddActionListener标识判断是否添加动作事件,声明pane面板按钮数组在面板中添加方格按钮;声明nullPanel表示一个空方格,空方格上没有动作事件和图片。该类的构造方法中设置面板的布局为空,则该面板中的组件可以随意摆放,并设置面板的大小为400×400,设置空方格的方位和创建几个方格。运用双重循环创建图片图标并将图标放置在方格上,同时设置每个方格的位置,再将设置好的方格添加到面板中。
(3)isFinish()方法判断方格拼凑的图形是否与原图片相同。运用循环获得每个方格的横、纵坐标,如果拼凑的每个图片位置与加载时每个方格的位置相同,则拼凑成功。
(4)reLoadPicture()方法在选择图片时面板重新加载按钮图片面板。运用双重循环获得图片图标,再将图片图标添加到按钮面板(每个方格)。
(5)breakRank()方法是将加载到面板中的按钮方格打乱排列。运用第一个方格距左上角近时进行循环,获得空方格的横、纵坐标。再运用Math类的random()方法随机产生一个0~3之间的整数。如果整数是0,则将空方格左移动,与左侧的方格互换位置,左侧的方格右移动;如果整数是1,则将空方格右移动与右侧的方格互换位置,右侧的方格左移动;如果整数是2,则将空方格向上移动,与上侧的方格互换位置,上侧的方格向下移动;如果整数是3,则将空方格向下移动,与下侧的方格互换位置,下侧的方格向上移动。判断方格是否添加动作事件,如果没有,则运用循环对每个方格添加动作事件监听器。
(6)test()方法检测每个方格是否在指定的面板范围内移动。如果方格的横坐标或纵坐标超出面板的范围则返回假,否则为真。
(7)PanelOfImage类实现MouseListener()接口,必须实现它的5个方法:mouseClicked()、mouseEntered()、mouseExited()、mouseReleased()和mousePressed()。当鼠标按下时触发mousePressed()方法,该方法根据鼠标事件获得单击的方格按钮的横、纵坐标以及空方格的横、纵坐标。如果单击的方格与空方格的横坐标相同,单击方格的纵坐标比空方格的纵坐标大100,则单击的方格位于空方格的下方,单击的方格要向上移动;如果单击的方格与空方格的横坐标相同,单击方格的纵坐标比空方格的纵坐标小100,则单击的方格位于空方格的上方,单击的方格要向下移动;如果单击的方格与空方格纵坐标相同,单击方格的横坐标比空方格的横坐标大100,则单击的方格位于空方格的右方,单击的方格要向左移动;如果单击的方格与空方格的纵坐标相同,单击方格的横坐标比空方格的横坐标小100,则单击的方格位于空方格的左方,单击的方格要向右移动。如果拼凑图形完成,调用isFinish()方法判断拼凑的图形与原加载的图形是否一致,如果一致则弹出对话框提示成功拼凑图形。再运用循环移除每个方格的鼠标监听器。
(8)TextImagePieceTogether类继承JFrame类实现ActionListener接口,实现接口必须实现其actionPerformed()方法。在该类的构造方法中创建容器面板,创建南侧面板并在面板中添加开始按钮、查看按钮以及选择按钮并设置面板的背景为红色,每个按钮都添加事件监听器,创建查看面板并设置其布局为空,这样组件可以在其面板中随意摆放。创建图片面板放在容器面板的中间位置。设置窗口的标题、位置、大小以及可视化等。
(9)TextImagePieceTogether类的actionPerformed()方法是处理单击按钮触发的事件。根据事件的getSource()方法获得单击的按钮。如果单击“开始”按钮,则调用PanelOfImage类的breakRank()打乱图片的方法。如果单击“查看”按钮,则判断按钮的标签,如果标签为“查看”则移除图片面板,添加查看标签,对容器面板进行重绘以及重新设置标签为“返回”;如果标签不是“查看”,则移除查看面板添加图片面板,对容器面板进行重绘以及重新设置标签为“查看”。如果单击“选择”按钮,创建选择器,并弹出对话框对图片进行选择。如果单击“确定”按钮则获得选择器中选择的图片的编号,根据编号获得指定的图片,再根据图片创建图标标签,查看面板添加图标标签并进行重绘来显示图片。
实例171 滚动的文字
动画技术的原理是多幅图画每隔一定时间间隔连续进行显示,本实例将演示一个简单的文字滚动的Applet的显示区域自右至左滚动显示。
技术要点
滚动的文字的技术要点如下:
• Applet的生命周期和主要方法使用。
• Applet和Runnable的结合使用。
• 调用repaint方法可以实现重画组件的作用。
• 双缓冲技术的工作原理是:先将组件画在一个背景图片上,然后再把背景图片画在当前屏幕上,在组件重画的过程中屏幕内容不变,这就大大减少了屏幕被刷新的次数。
• 双缓冲技术可以解决多次调用repaint方法重画组件会引起屏幕闪烁的问题。
实现步骤
(1)创建一个类名为Rolling.java。
(2)代码如下所示:
package chp17; import java.applet.Applet; import java.awt.*; public class Rolling extends Applet implements Runnable { String msg = "欢迎光临"; //需要实现滚动的文字 String messg = ""; //临时存储信息的变量 Thread trd = null; public void init() { //Applet初始化 this.setBackground(Color.black); //设置背景颜色 this.setForeground(Color.pink); //设置字体颜色 setFont(new Font("", Font.BOLD, 24)); //设置字体和字号 } public void start() { //Applet开始 trd = new Thread(this); //创建线程 trd.start(); //启动线程 } public void run() { //线程运行,因为继承了Runnable接口,所以必须实现此方法 while (true) { try { repaint(); //绘制组件 Thread.sleep(500); //休眠500毫秒 if (messg.equals("")) { //此句的作用是可以形成循环滚动 messg = msg; } else { messg = messg.substring(1, messg.length()); //此句的作用是形成渐变的效果 } } catch (Exception e) { System.out.println("出错了"); } } } public void paint(Graphics g){ //绘图 g.drawString(messg, 50, 30); //用此图形上下文的当前字体和颜色绘制messg中的 string 类型的文本 } }
(3)运行结果的初始页面如图17-9所示。
(4)运行0.5秒之后就出现第2个页面,如图17-10所示。
(5)再运行0.5秒之后就出现第3个页面,如图17-11所示。
(6)再运行0.5秒之后就出现第4个页面,如图17-12所示。
图17-9滚动的文字初始页面
图17-10第2个页面
图17-11第3个页面
图17-12 第4个页面
源程序解读
由于Rolling类继承了Applet,所以它具有Applet程序的5个相对重要的方法:初始化init()、开始执行start()、停止执行stop()、退出destroy()和绘画paint()。
(1)init():Applet初始化,在启动一个Applet小程序时,此方法只执行一次。它的主要作用是:
• 下载该Applet。
• 创建一个该Applet主类的实例对象。
• 调用init()对Applet自身进行初始化。
(2)start():启动Applet程序,在整个Applet生命可以执行多次。它被调用的情况如下:
• Applet第一次载入时。
• 刷新该页面。
• 单击IE浏览器的标准按钮后退键和前进键。
(3)run():由于Rolling实现Runnable接口,所以必须重写此接口的run()。在此方法中,利用sleep方法令当前线程处于休眠状态,令文字产生一定的时间间隔。
(4)paint():绘制方法,IE浏览器会根据自身的需要多次调用paint()。可以调用paint()的要求如下:
• Web页中含有Applet的部分被卷入窗口。
• Applet显示区域在视线范围内调整浏览器窗口的大小,缩放、移动等重绘窗口。
实例172 简单的GIF动画效果
技术要点
制作GIF动画效果的技术要点如下:
• Applet小程序的使用方法。
• MediaTracker类的使用。
• Image类的使用。
• Graphics类的使用。
实现步骤
(1)创建一个类名为Animation.java。
(2)代码如下所示:
package chp17; import java.applet.Applet; import java.awt.*; import java.net.MalformedURLException; import java.net.URL; public class Animation extends Applet implements Runnable { Image igs[]; //用于装载组成GIF动画的JPG图片 Thread th; //声明了线程对象 int MAX_hight = 200, MAX_width = 200; //定义了程序界面的大小 Image image; //声明了一个图片 Graphics gh; //声明了一个Graphics对象 MediaTracker mt; //声明了一个MediaTracker对象 boolean flag = false; //其作用是标志图片是否加载成功 int id = 0; //定义了一个图像索引 public static void main(String[] args) { new Animation(); } public void init() { igs = new Image[8]; //对图像数组进行实例化,其长度为8 mt = new MediaTracker(this); //对媒体跟踪器mt进行实例化 image = this.createImage(MAX_width, MAX_hight); //创建一个用于双缓冲的、可在屏幕外绘制的图像image gh = image.getGraphics(); //对图形对象gh进行实例化 gh.setColor(Color.white); //将gh图形的当前颜色设置成白色 gh.fillRect(0, 0, MAX_width, MAX_hight); //指定要绘制的矩形的位置和大小 this.setSize(MAX_width, MAX_hight); //设置Applet的尺寸 for (int i = 0; i < igs.length; i++) { //此循环的目的是获取每个JPG图片,并将其放入到mt中 String file_name = (i + 1) + ".jpg"; igs[i] = this.getImage(getCodeBase(), file_name); mt.addImage(igs[i], i); } try { mt.waitForAll(); //开始加载由此媒体跟踪器跟踪的所有图像,也就是那些JPG图片 } catch (InterruptedException e) { e.printStackTrace(); } flag = true; //将标识改成true,即加载完毕 } public void paint(Graphics g) { if (flag) { //判断加载是否完毕 g.drawImage(image, 0, 0, this); //画出由上面定义好了的矩形 } } public void start() { if (mt.checkID(id)) { //判断处于id位置的图像是否加载完毕 gh.drawImage(igs[id], 0, 0, this); //画出该图像 } th = new Thread(this); //为线程th实例化 th.start(); //启动线程 } public void run() { while ((th != null)) { if (mt.checkID(id)) { //判断处于id位置的图像是否加载完毕 gh.fillRect(0, 0, MAX_width, MAX_hight);//指定要绘制的矩形的位置和大小 gh.drawImage(igs[id], 0, 0, this); //画出位于id位置的图像 id++;//id加1 if (id >= igs.length) //如果图像绘制完毕 id = 0; //id清零 } } try { th.sleep(100); //线程休眠0.1秒 } catch (InterruptedException e) { } this.repaint(); //重新绘制 } } }
(3)运行结果的初始页面如图17-13所示。
(4)程序运行的第2个页面如图17-14所示。
图17-13 GIF动画
图17-14 第2个页面
源程序解读
(1)MediaTracker类是一个跟踪多种媒体对象状态的实用工具类。媒体对象可以包括音频剪辑和图像,但目前仅支持图像。要使用媒体跟踪器,需要创建一个MediaTracker实例,然后对每个要跟踪的图像调用其addImage方法。另外,还可以为每个图像分配一个唯一的标识符。此标识符可控制获取图像的优先级顺序。它还可用于标识可单独等待的唯一图像子集。具有较低ID的图像比具有较高ID的图像优先加载。
由于动画图像加载和绘制的多部分特性,跟踪动画图像可能不是始终可用的,但这一功能的确受支持。MediaTracker在完成加载动画图像的第一帧之后就会认为动画图像已经加载完毕。这时,MediaTracker会向所有等待者发出图像已完全加载的信号。如果在第一帧加载完之后没有ImageObserver查看此图像,则该图像可能会自我刷新来保存资源。在MediaTracker类中常用的方法如下:
• void addImage(Image image, int id):向此媒体跟踪器正在跟踪的图像列表添加一个图像。
• boolean checkAll():查看此媒体跟踪器正在跟踪的所有图像是否已完成加载。
• boolean checkID(int id):检查由此媒体跟踪器跟踪且使用指定标识符标记的所有图像是否已完成加载。
• boolean waitForAll():开始加载由此媒体跟踪器跟踪的所有图像。
• void waitForID(int id):开始加载由此媒体跟踪器跟踪且具有指定标识符的所有图像。
(2)Image类是一个抽象类,是表示图形图像的所有类的超类。它的直接子类有BufferedImage。在Image类中常用的方法如下:
• abstract void flush():刷新此 Image 对象正在使用的为呈现到屏幕而缓存的所有像素数据和用来存储图像数据或像素的所有系统资源。
• Graphics getGraphics():创建供绘制图像使用的图形上下文。
• int getHeight(ImageObserver observer):获取图像的高度。
• int getWidth(ImageObserver observer):获取图像的宽度。
(3)Graphics类是所有图形上下文的抽象基类,允许应用程序可以在组件,以及图像上,进行绘制。在Groplics类中常用的方法如下:
• Graphics create(int x, int y, int width, int height):用当前的Graphics对象创建一个新的Graphics对象,但是使用新的转换和剪贴区域。参数:x表示x坐标;y表示y坐标;width表示剪贴矩形的宽度;height表示剪贴矩形的高度。
• boolean drawImage(Image img, int x, int y, ImageObserver observer):绘制指定图像中当前可用的图像。参数:img表示要绘制的指定图像;x表示x 坐标;y表示y 坐标;observer表示当转换了更多图像时要通知的对象。
• void drawLine(int x1, int y1, int x2, int y2):在此图形上下文的坐标系统中,使用当前颜色在点(x1, y1)和(x2, y2)之间画一条线。参数:x1表示第一个点的x坐标;y1表示第一个点的y坐标;x2表示第二个点的x坐标;y2表示第二个点的y坐标。
• void drawRect(int x, int y, int width, int height):绘制指定矩形的边框。参数:x表示要绘制矩形的x坐标;y表示要绘制矩形的y坐标;width 表示要绘制矩形的宽度;height表示要绘制矩形的高度。
• void drawString(String str, int x, int y):使用此图形上下文的当前字体和颜色绘制由指定string给定的文本。参数:str表示要绘制的string;x表示x坐标;y表示y坐标。
• void setFont(Font font):将此图形上下文的字体设置为指定字体。
• void setColor(Color c):将此图形上下文的当前颜色设置为指定颜色。
实例173 简单的声音播放
Java的一大特色就是支持声音的播放。它可支持应用程序和Applet中的音频播放。本实例将演示如何使用AudioClip接口实现声音的播放,可实现通过选择播放文件来执行播放、停止播放、循环播放等操作。
技术要点
声音播放的技术要点如下:
• AudioClip接口的作用。
• play()的作用。
• loop()的作用。
• stop()的作用。
• 如何使用URL获取播放方件的地址。
• ActionListener事件监听的使用。
实现步骤
(1)创建一个类名为SoundPlayer.java。
(2)代码如下所示:
package chp17; import java.applet.*; import java.awt.*; import java.awt.event.*; import java.io.File; import java.net.MalformedURLException; import java.net.URL; import javax.media.ControllerEvent; import javax.media.ControllerListener; import javax.swing.*; public class SoundPlayer extends JFrame implements ActionListener { AudioClip adc; //声明音频剪辑对象adc URL url; //声明资源定位符对象url JLabel lb = new JLabel(); //创建一个标签对象lb JLabel la = new JLabel(); //创建一个标签对象la File file; //声明文件对象file String fileName; //声明fileName变量,用于记录文件的名称 boolean loop = false; //定义loop变量,用来判断是否进行循环操作 JFileChooser chooser = new JFileChooser(); //创建一个文件选择器对象chooser Icon a = new ImageIcon("D:\\workspace\\New\\chp17\\image\\ying.jpg"); //创建一个组件图片 JPanel pn1 = new JPanel(); //创建一个面板容器对象,默认布局为FlowLayout public static void main(String[] args) { new SoundPlayer("Applet 音乐播放器"); //给程序传入title } public SoundPlayer(String title) { //本类的构造器,起到初始化的作用,相当于Applet的init方法 super(title); //调用JFrame的构造方法,其作用是创建一个新的、初始不可见的、具有指定标题的 Frame addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) { //用户单击窗口系统菜单的关闭按钮,调用dispose以执行windowClosed dispose(); if (adc != null) { //在音频剪辑adc不为null的情况下,关闭窗口的同时,声音也消失 adc.stop(); } } }); JButton[] bn = new JButton[3]; //创建JButton数组 JPanel pn = new JPanel(new GridLayout()); //创建一个面板容器对象pn,布局为GridLayout Icon[] ic = new Icon[3]; //创建Icon数组 for (int i = 0; i < ic.length; i++) { //为每个Icon对象赋图像 ic[i] = new ImageIcon("D:\\workspace\\New\\chp17\\image\\" + (i + 1) + ".gif"); } bn[0] = new JButton("开始", ic[0]); //创建一个有文字,带图标的按钮对象 bn[0].addActionListener(this); //添加事件监听 bn[1] = new JButton("停止", ic[1]); bn[1].addActionListener(this); bn[2] = new JButton("循环", ic[2]); bn[2].addActionListener(this); pn.add(bn[0], 0); //将开始按钮添加到面板pn的第1个位置上 pn.add(bn[1], 1); //同上 pn.add(bn[2], 2); //同上 this.add(pn, BorderLayout.SOUTH); //将面板pn添加到Frame中 //创建播放器的菜单 JMenu fileMenu = new JMenu("文件"); JMenuItem openMemuItem = new JMenuItem("打开"); openMemuItem.addActionListener(this); fileMenu.add(openMemuItem); fileMenu.addSeparator(); //添加一个分隔条 JMenuItem exitMemuItem = new JMenuItem("退出"); exitMemuItem.addActionListener(this); fileMenu.add(exitMemuItem); JMenuBar menuBar = new JMenuBar(); menuBar.add(fileMenu); String flag = "欢迎光临"; this.setFrame(flag); //调用setFrame方法 this.setJMenuBar(menuBar); this.setSize(300, 360); //设置Frame的大小 this.setVisible(true); //将Frame设置为可见 } public void actionPerformed(ActionEvent e) { if (e.getActionCommand().equals("退出")) {//如果单击退出菜单项,关闭Windows窗口 dispose(); //调用dispose以便执行WindowClosed return; } if (e.getActionCommand().equals("打开")) {//如果单击打开菜单项,选择音乐播放文件 int val = chooser.showOpenDialog(this); //接收文件选择器的返回状态 if (val == JFileChooser.APPROVE_OPTION) {//如果返回状态为APPROVE_OPTION file = chooser.getSelectedFile(); //返回选中的文件 fileName = file.getName(); //获取选中的文件的文件名 String flag = "您正在欣赏:" + fileName; this.setFrame(flag); try { URL url = new URL("file:" + file.getPath()); //创建资源定位符 adc = JApplet.newAudioClip(url);//为音频剪辑对象adc赋值 adc.play(); //开始播放此音频剪辑 } catch (MalformedURLException e1) { System.out.println("不能播放此文件"); } } } //如果用户放弃选择文件,则返回 if (e.getActionCommand().equals("开始")) { String flag = "您正在欣赏:" + fileName; if (adc == null) { flag = "请选择播放文件"; this.setFrame(flag); return; } adc.play(); this.setFrame(flag); } if (e.getActionCommand().equals("停止")) { adc.stop(); //停止播放此音频剪辑 String flag = "停止播放:" + fileName; this.setFrame(flag); } if (e.getActionCommand().equals("循环")) { loop = !loop; String flag = ""; if (loop) { adc.loop(); //以循环方式开始播放此音频剪辑 flag = "循环播放:" + fileName; } else { flag = "顺序播放:" + fileName; } this.setFrame(flag); } } public void setFrame(String flag) { la.setText(flag); lb.setIcon(a); pn1.add(la, 0); pn1.add(lb, 1); this.add(pn1, FlowLayout.CENTER); } }
(3)运行结果的初始页面如图17-15所示。
(4)当单击“开始”按钮时,出现一个页面,如图17-16所示。
(5)当单击“文件”菜单时,出现一个页面,如图17-17所示。
图17-15 初始页面
图17-16 开始页面
图17-17 选择文件页面
(6)当单击“停止”按钮时,出现一个页面,如图17-18所示。
(7)当单击“循环”按钮时,出现一个页面,如图17-19所示。
(8)当再次单击“循环”按钮时,出现一个页面,如图17-20所示。
图17-18 停止页面
图17-19 循环页面
图17-20 顺序页面
源程序解读
AudioClip 接口用于播放音频剪辑。在AudioClip接口中声明了三个需要实现的方法:play()、loop()和stop()。
(1)play()方法的主要作用是开始播放声音文件,当每次调用此方法时,声音文件都会从头开始播放。需在此方法中指定播放声音文件的URL地址。获取URL有两种方法。
• getCodeBase():获得基URL。这是包含此applet的目录的URL,也就是放在Eclipse工程的根目录下。
• getDocument():返回HTML标记中命名参数的值。例如,如果此applet被指定为:
<applet code="getImage " width=50 height=50> <param name="image " value="Pic1.jpg"> </applet>
那么对getParameter("image")的调用将返回值"Pic1.jpg"。
(2)loop()方法的主要作用是循环播放声音文件。
(3)stop()方法的主要作用停止播放声音文件。如果在某程序中没有调用此方法,即使关闭了Web页面,声音文件还在继续播放,而不会停止。
实例174 多媒体播放器
JMF是一种Java媒体框架,它可以在Java Applet和Application中使用音频、视频或者其他基于时间的多媒体。利用它可以捕获帧,顺序播放,回放多种媒体格式,并对之编码。JMF所支持的多媒体格式主要包括AIFF(.aiff)、AVI(.avi)、GSM(.gsm)、HotMedia(.mvr)、MID(.mid)、MPEG-1(.mpg)(即常见的VCD文件)、MPEG-2(.mp2)(即常见的DVD文件)、QuickTime(.mov)、Sun Audio(.au)和Wave(.wav)。
本实例将演示如何使用JMF实现一个媒体播放器,能够处理音频和视频,在程序演示之前,需要去http:java.sun.com/products/java-media/jmf/index.jsp下载最新的JMF类库和文档。
技术要点
用JMF实现媒体播放器的技术要点如下:
(1)数据源。在JMF中,DataSource对象就是数据源,它可以是一个多媒体文件,也可以是数据流。一旦确定了DataSource对象的位置和类型,该对象中就包含了多媒体的位置信息和能够播放该多媒体的软件信息。
(2)截取设备指的是可以截取到音频或视频数据的硬件,如麦克风、摄像机等。截取到的数据可以被送入Player对象中进行处理。
(3)在JMF中对应播放器的接口是Player。Player对象将音频/视频数据流作为输入,然后将数据流输出到音箱或屏幕上。Player对象有多种状态,下面是Player的6种状态的说明。
• Unrealized:表示Player对象已经被实例化,但是并不知道它需要播放的多媒体的任何信息。
• Realizing:表示Player对象的状态从Unrealized转变为Realizing。
• Realized:表示Player对象已经知道需要播放的多媒体的类型。
• Prefetching:表示Player对象的状态从Realized变为Prefetching。
• Prefetched:表示Player对象完成了预取操作后就到达了该状态。
• Started:表示调用start()方法后,Player对象就进入了该状态并播放多媒体。
(4)处理器对应的接口是Processor,它一种播放器。Processor接口继承了Player接口。Processor对象除了支持Player对象支持的所有功能,还可以控制对于输入的多媒体数据流进行何种处理以及通过数据源向其他的Player对象或Processor对象输出数据。
(5)Format对象中保存了多媒体的格式信息。该对象中本身没有记录多媒体编码的相关信息,但是它保存了编码的名称。Format的子类包括AudioFormat和VideoFormat类, VideoFormat又有6个子类:H261Format、H263Format、IndexedColorFormat、JPEGFormat、RGBFormat和YUVFormat类。
(6)JMF提供了下面4种管理器:
• Manager:Manager相当于两个类之间的接口。例如当你需要播放一个DataSource对象,可以通过使用Manager对象创建一个Player对象来播放它。使用Manager对象可以创建Player、Processor、DataSource和DataSink对象。
• PackageManager:该管理器中保存了JMF类注册信息。
• CaptureDeviceManager:该管理器中保存了截取设备的注册信息。
• PlugInManager:该管理器中保存了JMF插件的注册信息。
(7)可以通过Manager类的createPlayer()方法创建Player对象。创建Player对象后,可以调用getVisualComponent()方法得到Player对象的图像部件。然后将图像部件加入到应用程序或Applet的界面上。Player对象还包含一个控制面板,在上面可以控制媒体的播放、停止和暂停等。
(8)截取多媒体数据。多媒体数据的截取是JMF程序中另一个非常重要的功能。可以按照下面的步骤截取数据:
• 通过查询CaptureDeviceManager获得你希望使用的截取设备。通过调用getDeviceList()方法可以获得设备的列表。
• 获得设备对应的CaptureDeviceInfo对象,调用CaptureDeviceManager对象的getDevice()方法来获得特定的CaptureDeviceInfo对象。
• 从CaptureDeviceInfo对象中获得MediaLocator对象,然后用它创建一个DataSource对象。
• 使用DataSource对象创建Player对象或Processor对象。
• 调用start()方法,开始截取多媒体数据。
实现步骤
(1)创建一个类名为MediaPlay.java。
(2)代码如下所示:
package chp17; import javax.media.*; import javax.swing.JCheckBoxMenuItem; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JTabbedPane; import java.awt.*; import java.awt.event.*; public class MediaPlay extends Frame implements ActionListener, ControllerListener, ItemListener { Player player; //JMF的播放器 Component vedio_Component; //播放器的视频组件 Component control_Component; //播放器的控制组件 boolean first = true; //标示是否是第一次打开播放器 boolean loop_flag = false; //标示是否需要循环 String currentDirectory; //文件当前目录 FileDialog fileDialog; public MediaPlay(String title) { //构造方法 super(title); addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) {//用户单击窗口系统菜单的关闭按钮 dispose(); //调用dispose以执行windowClosed } public void windowClosed(WindowEvent e) { if (player != null) { player.close(); //关闭JMF播放器对象 } System.exit(0); } }); Menu menu = new Menu("文件"); //创建播放器的菜单 MenuItem open_Item = new MenuItem("文件选择"); open_Item.addActionListener(this); menu.add(open_Item); open_Item.setActionCommand("open"); menu.addSeparator(); //添加分隔条 CheckboxMenuItem loop_Item=new CheckboxMenuItem("循环播放"); //创建一个复选框菜单项 loop_Item.addItemListener(this); loop_Item.setActionCommand("loop"); menu.add(loop_Item); menu.addSeparator(); //添加分隔条 MenuItem exit_Item = new MenuItem("退出播放器"); exit_Item.addActionListener(this); exit_Item.setActionCommand("exit"); menu.add(exit_Item); MenuBar menuBar = new MenuBar(); menuBar.add(menu); this.setMenuBar(menuBar); this.setSize(300, 250); this.setVisible(true); } /** 实现了ActionListener接口,处理组件的活动事件 */ public void actionPerformed(ActionEvent e) { this.setSize(300, 250); if (e.getActionCommand().equals("exit")) { dispose(); //调用dispose以便执行windowClosed } else { fileDialog = new FileDialog(this, "打开媒体文件", FileDialog.LOAD); fileDialog.setDirectory(currentDirectory); fileDialog.setVisible(true); if (fileDialog.getFile() == null) { //如果用户放弃选择文件,则返回 return; } currentDirectory = fileDialog.getDirectory(); if (player != null) { player.close(); //关闭已经存在的JMF播放器对象 } try { //创建一个打开选择文件的播放器 player = Manager.createPlayer(new MediaLocator("file:" + fileDialog.getDirectory() + fileDialog.getFile())); } catch (java.io.IOException e2) { System.out.println("播放文件错误."); return; } catch (NoPlayerException e2) { System.out.println("不能找到播放器."); return; } if (player == null) { System.out.println("无法创建播放器."); return; } first = false; this.setTitle(fileDialog.getFile()); player.addControllerListener(this); //播放器的控制事件处理 player.prefetch(); //预读文件内容 } } /** 实现ControllerListener接口的方法,处理播放器的控制事件 */ public void controllerUpdate(ControllerEvent e) { this.setSize(300, 250); //调用player.close()时ControllerClosedEvent事件出现。 //如果存在视觉部件,则该部件应该拆除(为一致起见, //我们对控制面板部件也执行同样的操作) if (e instanceof ControllerClosedEvent) { if (vedio_Component != null) { remove(vedio_Component); this.vedio_Component = null; } if (control_Component != null) { remove(control_Component); this.control_Component = null; } return; } if (e instanceof EndOfMediaEvent) { //如果是媒体文件到达尾部事件 if (loop_flag) { //如果允许循环,则重新开始播放 player.setMediaTime(new Time(0)); player.start(); } return; } //如果是播放器预读事件 if (e instanceof PrefetchCompleteEvent) { //启动播放器 player.start(); return; } //如果是文件打开完全事件,则显示视频组件和控制器组件 if (e instanceof RealizeCompleteEvent) { vedio_Component = player.getVisualComponent(); if (vedio_Component != null) { add(vedio_Component); } control_Component = player.getControlPanelComponent(); if (control_Component != null) { add(control_Component, BorderLayout.SOUTH); } this.pack(); } } //处理循环复选框菜单项的单击事件 public void itemStateChanged(ItemEvent e) { loop_flag = !loop_flag; } public void paint(Graphics g) { int w = getSize().width; int h = getSize().height; g.setColor(Color.black); g.fillRect(0, 0, w, h); if (first) { Font f = new Font("DialogInput", Font.BOLD, 14); g.setFont(f); FontMetrics fm = g.getFontMetrics(); int swidth = fm.stringWidth("*** 欢迎使用JMF多媒体播放器 ***"); g.setColor(Color.yellow); g.drawString("*** 欢迎使用JMF多媒体播放器 ***", (w - swidth) / 2, (h + getInsets().top) / 2); } else { String s = fileDialog.getFile(); Font f = new Font("DialogInput", Font.BOLD, 16); g.setFont(f); FontMetrics fm = g.getFontMetrics(); int swidth = fm.stringWidth("您现在欣赏的曲目是:" + s); g.setColor(Color.pink); g.drawString("您现在欣赏的曲目是:" + s, (w - swidth) / 2, (h + getInsets().top) / 2); } //调用超类Frame的paint()方法,该paint()方法将调用Frame包含的各个容器 //和部件(包括控制面板部件)的paint()方法 super.paint(g); } //不执行背景清除操作,以免控制面板部件闪烁 public void update(Graphics g) { paint(g); } public static void main(String[] args) { new MediaPlay("JMF 多媒体播放器"); } }
(3)运行程序,媒体播放器初始界面如图17-21所示。
用媒体播放器播放.mp3文件时的界面如图17-22所示。
用媒体播放器播放.avi文件时的界面如图17-23所示。
图17-21 媒体播放器初始界面
图17-22 播放mp3音频文件
图17-23 播放avi视频文件
源程序解读
(1)MediaPlay构造方法中初始化菜单栏,其中loop_Item是一个带复选框的菜单项,当单击该菜单项时,在菜单项前面打一个对勾,当再次单击该菜单时对勾消失,带复选框的菜单项由java.swing.JCheckBoxMenuItem定义,并为其菜单项添加addActionListener。
(2)actionPerformed()方法实现了ActionListener接口,处理组件的活动事件。通过getActionCommand()方法返回事件动作指令,如果指令等于exit,表示执行的是退出播放器菜单项,调用窗口的dispose()方法将窗口关闭。如果指令等于open,表示执行的是文件选择,弹出一个FileDialog对话框,选择待播放的文件,然后通过javax.media.Manager的createPlayer方法创建一个Player对象,该方法支持的参数有两种,一种是文件的URL,另一种是javax.media.MediaLocator对象,也是一种文件定位类。需要为Player注册事件处理器,类型为ControllerListener,Player播放器产生的事件都需要在ControllerListener定义的controllerUpdate方法中处理。调用Player的prefetch方法预读文件内容。
(3)controllerUpdate方法实现了ControllerListener接口,参数为ControllerEvent对象,处理播放器的控制事件,对于ControllerClosedEvent事件,清除播放器的视频和控制组件;对于EndOfMediaEvent事件,如果允许循环,则调用Player的start方法重新播放文件;对于PrefetchCompleteEvent事件,表示预读取文件结束,调用Player的start方法开始播放;对于RealizeCompleteEvent事件,表示文件完全打开,则通过Player的getVisualComponent和getControlPanelComponent方法获得播放器的视频和控制组件,并显示在播放器界面上。
实例175 有趣的采钻游戏
本实例将演示一个有趣的采钻游戏,采钻游戏的游戏规则是对钻石进行消除,最终目标是将所有钻石全部消掉,如果两个或两个以上的钻石在同一水平线,游戏便会结束。
技术要点
实现采钻游戏的技术要点如下:
• keyDown事件的应用。
• ImageFilter类的使用。
• MediaTracker类的使用。
实现步骤
(1)创建一个类名为Mining.java。
(2)代码如下所示:
package chp17; import java.applet.*; import java.awt.*; import java.awt.image.*; import java.net.*; import java.util.*; public class Mining extends Applet implements Runnable { static int MAX_X = 12, MAX_Y = 12, MAX_LVL = 100, MAX_IMG = 11, MAX_HIST = 100; static int X_0 = 5, Y_0 = 5; //静态变量说明 Thread thread = null; private Image[] sImage = new Image[MAX_IMG + 1]; private int xPos, yPos, gLevel, mvPos, maxMove; private byte[][] MinCanvas = new byte[MAX_Y + 1][MAX_X + 1]; private byte[] xMove = new byte[MAX_HIST]; private byte one_Time; private Button btnR = new Button("返回"); private Button btnN = new Button("下一级"); private Button btnP = new Button("上一级"); private static String stGame = " [GAME over]"; //定义按钮 private static String stLost = " [YOU LOST]"; //定义游戏状态 private static String widthe = " # *.oO@+,123456789abcdefghij"; private static int xCoord[][] = { { 0, 0, 20, 20 }, { 20, 0, 20, 20 }, //截取图像时所需要的图素 { 40, 0, 20, 20 }, { 60, 0, 20, 20 }, { 80, 0, 20, 20 }, { 100, 0, 20, 20 }, { 100, 20, 20, 20 }, { 0, 20, 20, 20 }, { 20, 20, 20, 20 }, { 40, 20, 20, 20 }, { 60, 20, 20, 20 }, { 80, 20, 20, 20 } }; private static String data_grade[] = { //定义游戏的级数 ", #7, #o.o.o.#, # 5#, # # # #1, # # 3#, # 4#1, #6,", ",, 1#5, 1#.* 1#, 1# 3#, 1#. 2#, 1# 1.#1, 1#5,", ", 2#3, 1#1 .#1, 1# 3#, 1#. o #, 1# 1# #, 1# 1. #, 1#1o #1, 2#3,", ", 1#2, #1 #2, # . 1#1, #* . 1#1, # 1* . #, # # 1#2, #5,", ", 4#2, #4.#1, #. 4#, #1 2* #, 1#1 # #1, 2# #2, 2# 1.#, 2#4,", ", #6, # 4#, #1 .* #, #. 3#, #* 2.#, # 2* #, #6,", " #7, # 1. 2#,#1 1*# 1#,# 2.# 1#,#1 #1 2#, # 1* 2#, # 1. 2#, # 5#, # 5#, #7,", ", #6, # .# o#1, #1 3o#, 1# 3#1, 1#* 2.#, 1#.o 1#1, 1#5,", ", 2#4, 1#1 O #, #1 3#1,#1.O 3#,# 2o 1#1,# 1#2.#,# O o 1#,#7,", " #2,#1 #4,#. 2O #,# 2#1.#1,# 1Oo# 1#,# o# 2#1,# 2. o #,#O 1#1 #1,#7,", " 1#2, 1# #2, 1# 1o#1, 1#*. o#1, 1# #O o#, #1 2# #, # . 2O#, #1O 2#1, 1#5,", "#8,# Oo.oO #,# 2o 2#,# 2O 2#,# 2o 2#,# o . O #,#1 1# 1#1, #6,", "1#4, #1 # #1, # .# 1#1, #1 #. 1#, # 5#, #1 * 1.#, 1#1 2#1, 2#1 #1, 3#2,", " 1#5, #1 1# #1, #.*o.*.#1, # 6#, # #1 # 1#,#1 2#1 1#,# o 4#1,#1 4#1, #6,", " #3,#1 1#3,# 2. 1#,#1 .#.#1, # 4#, # #o#o#, #1 1o #, 1#1 2#, 2#1 #1, 3#2,", ", #5, # 1o.#1, # . #o#1, # * 3#, #o 4#, #1 2.#1, 1#1 1#1, 2#3,", ", #5, # 1o #1, # O# o#, #o 1O.#1, # 5#, #1. 2O#, 1#6,", " #5, # O .#1, #1 2O#1, 1# o#. #, #1 3o#, #O #O 1#, # o 3#, #1 #4, 1#2,", " 1#4, #1O 1#, # o 1#1, #1. 2#, # O.o*#, #1 2.#1, 1# 2o #, 1#1 3#, 2#5,", " #7,#1 o 1O #,# 4o#1,#1 5#,# 1O 1.#1,#1. 2*#, #1o 2#, # . 1O#, #6,", }; private static int aMove[] = { 4, 4, 4, 4, 6, 6, 6, 6, //移动的像素 7, 7, 7, 7, 8, 8, 8, 8, 9, 9, 9, 10, }; private Image mainImg; private Graphics g; private TextField txt1; public void init() { //Applet初始化 String param; int i; if (g == null) g = getGraphics(); URL url = getCodeBase(); MediaTracker tracker = new MediaTracker(this); //创建媒体跟踪器tracker mainImg = getImage(url, "zhuan.gif"); //获取指定路径下的图像 tracker.addImage(mainImg, 0); //将该图片加载到tracker中 for (i = 0; i <= MAX_IMG; i++) { sImage[i] = splitImage(xCoord[i]); //根据像素截取相应的图像 tracker.addImage(sImage[i], i + 1);//将截取的图像载入到tracker指定的位置 } try { tracker.waitForAll(); //等待此媒体跟踪器跟踪加载所有图像 } catch (InterruptedException e) { } //设置布局 setLayout(new BorderLayout()); txt1 = new TextField(17); txt1.setForeground(new Color(255, 255, 204)); //设置前景色 txt1.setBackground(new Color(102, 51, 51)); //设置背景色 txt1.setEditable(false); //设置文本输入框为不可编辑 Panel p = new Panel(); //创建面板容器 //将组件加入到面板中 p.add(txt1); p.add(btnP); p.add(btnN); p.add(btnR); add("South", p); //布局格式 gLevel = 0; dat2Canvas(0); //调用自定义方法 one_Time = 1; this.resize(310, 250); } public void start() //启动线程 { if (thread == null) { thread = new Thread(this); thread.start(); } } public void stop() { //终止线程 if (thread != null) { thread.stop(); thread = null; } } public void run() { } public boolean dat2Canvas(int dlvl) { int i = 0, x, y, tmp0; byte z = 0, oz = 0; String param = null; try { gLevel += dlvl; if (gLevel < 0) gLevel = 0; if (i > 0) maxMove = i; if (param == null || i <= 0) { param = data_grade[gLevel]; maxMove = aMove[gLevel]; } } catch (ArrayIndexOutOfBoundsException exc) { gLevel -= dlvl; return false; } for (y = 0; y <= MAX_Y; y++) { for (x = 0; x <= MAX_X; x++) { MinCanvas[y][x] = 0; } } x = y = 0; for (i = 0; i < param.length(); i++) { z = (byte) (widthe.indexOf(param.charAt(i))); if (z > MAX_IMG) { if ((tmp0 = (z - MAX_IMG - 1)) > 0) { for (int j = 0; j < tmp0; j++) { try { MinCanvas[y][x++] = oz; } catch (ArrayIndexOutOfBoundsException exc) { j = tmp0; } } } else { x = 0; y++; if (y > MAX_Y) break; } } else if (x <= MAX_X) { if (z < 0) z = 0; MinCanvas[y][x++] = z; oz = z; } } for (i = 0; i < MAX_HIST; i++) { xMove[i] = 0; } mvPos = 0; txt1.setText(" Level " + gLevel + " (" + maxMove + " step max.)"); if (one_Time == 1) one_Time = 0; return true; } public boolean action(Event e, Object arg) {//button的动作处理 int h = -1; if (e.target == btnP) //判断事件目标是否为btnP h = 44; else if (e.target == btnN) //判断事件目标是否为btnN h = 46; else if (e.target == btnR) //判断事件目标是否为btnR h = 27; if (h > 0) keyDown(e, h); //调用keyDown方法 return true; } public boolean keyDown(Event e, int c) //键盘动作处理 { int dx = 0; int dy = 0; int ch = 0; int h = 0; switch (c) { case Event.UP: //UP:方向键上 dy = -1; h = 1; break; case Event.DOWN: //DOWN:方向键下 dy = 1; h = 3; break; case Event.LEFT: //LEFT:方向键左 dx = -1; h = 5; break; case Event.RIGHT: //RIGHT:方向键右 dx = 1; h = 7; break; case 27: if (mvPos == 0 && one_Time == 0) { one_Time = 1; paint(null); return true; } h = -100; break; case 44: h = -101; break; case 46: h = -99; break; case 14: h = -90; break; case 16: h = -110; break; default: txt1.setText(" Use ^P,<,>,^N,^R, or BS key."); break; } if (dx == 0 && dy == 0) { if (h < 0) { if (dat2Canvas(h + 100)) paint(null); } return true; } if (mvPos >= maxMove) { txt1.setText(stLost); return true; } h = 1; ch = 0; while (h > 0) { ch = 0; if ((h = moveBall(dx, dy)) > 0) { ch = 1; h = checkBoard(); } } if (ch == 1) { mvPos++; txt1.setText(" Have " + (maxMove - mvPos) + " more step."); } else return true; h = GameEnd(); if (h == 1) { txt1.setText(stGame); h = -98; } else if (h == -1 || mvPos >= maxMove) { txt1.setText(stLost); h = -99; } if (h <= -98) { try { thread.sleep(1500); } catch (InterruptedException exc) { } if (dat2Canvas(h + 99)) paint(null); } return true; } public int moveBall(int dx, int dy) {// int x, y, lx, ly, x2, y2; byte z0, z1; int h = 0; if (dx != 0) { ly = Y_0; for (y = 0; y <= MAX_Y; y++) { if (dx == 1) x = MAX_X - 1; else x = 1; while (x > 0 && x < MAX_X) { if ((z0 = MinCanvas[y][x]) >= 6) { x2 = x; lx = X_0 + 20 * x; while (x2 > 0 && x2 < MAX_X && (z1 = MinCanvas[y][x2 + dx]) == 0) { try { thread.sleep(10); } catch (InterruptedException e) { } h++; MinCanvas[y][x2] = 0; g.drawImage(sImage[0], lx, ly, this); x2 += dx; lx += 20 * dx; MinCanvas[y][x2] = z0; g.drawImage(sImage[z0], lx, ly, this); } } x -= dx; } ly += 20; } } if (dy != 0) { lx = X_0; for (x = 0; x <= MAX_X; x++) { if (dy == 1) y = MAX_Y - 1; else y = 1; while (y > 0 && y < MAX_Y) { if ((z0 = MinCanvas[y][x]) >= 6) { y2 = y; ly = Y_0 + 20 * y; while (y2 > 0 && y2 < MAX_Y && (z1 = MinCanvas[y2 + dy][x]) == 0) { try { thread.sleep(10); } catch (InterruptedException e) { } h++; MinCanvas[y2][x] = 0; g.drawImage(sImage[0], lx, ly, this); y2 += dy; ly += 20 * dy; MinCanvas[y2][x] = z0; g.drawImage(sImage[z0], lx, ly, this); } } y -= dy; } lx += 20; } } return h; } public int checkBoard() { byte z0, z1, z2; int h = 0; int h2 = 0; z1 = 100; for (int y = 0; y < MAX_Y; y++) { for (int x = 0; x < MAX_X; x++) { z0 = (byte) (MinCanvas[y][x] % z1); if (z0 >= 7) { z2 = (byte) (z0 + z1); if (z0 == (byte) (MinCanvas[y][x + 1] % z1)) { MinCanvas[y][x] = MinCanvas[y][x + 1] = z2; h++; } if (z0 == (byte) (MinCanvas[y + 1][x] % z1)) { MinCanvas[y][x] = MinCanvas[y + 1][x] = z2; h++; } } } } if (h == 0) return 0; for (int z = 2; z <= 6; z++) { try { thread.sleep(100); } catch (InterruptedException e) { } int ly = Y_0; for (int y = 0; y < MAX_Y; y++) { int lx = X_0; for (int x = 0; x < MAX_X; x++) { if ((z0 = MinCanvas[y][x]) > MAX_IMG) { if (z < 6) g.drawImage(sImage[z], lx, ly, this); else { MinCanvas[y][x] = 0; g.drawImage(sImage[0], lx, ly, this); } } lx += 20; } ly += 20; } } return h; } public int GameEnd() { int x, y; int h = 1; int h0[] = new int[MAX_IMG + 1]; for (y = 0; y <= MAX_IMG; y++) h0[y] = 0; for (y = 0; y <= MAX_Y; y++) { for (x = 0; x <= MAX_X; x++) { h0[MinCanvas[y][x]]++; } } y = 7; while (y <= MAX_IMG) { x = h0[y]; if (x == 1) { h = -1; y = MAX_IMG + 1; } else if (h == 1 && x > 1) h = 0; y++; } return h; } private Image splitImage(int[] xyCoord) { Image newImage; ImageFilter filter; ImageProducer producer; filter = new CropImageFilter(xyCoord[0], xyCoord[1], xyCoord[2], xyCoord[3]); producer = new FilteredImageSource(mainImg.getSource(), filter); newImage = createImage(producer); return newImage; } public void paint(Graphics g) { if (g == null) g = getGraphics(); int y2 = Y_0; for (int y = 0; y <= MAX_Y; y++) { int x2 = X_0; for (int x = 0; x <= MAX_X; x++) { byte z = MinCanvas[y][x]; if (one_Time == 1 || z > MAX_IMG) z = 0; g.drawImage(sImage[z], x2, y2, this); x2 += 20; } y2 += 20; } if (one_Time == 1) Description(); } public void Description() //进入游戏时设置,说明规则 { int y = 30, x = 15; g.setColor(new Color(255, 69, 0)); String str = "* 采钻游戏 *"; g.drawString(str, x, y); txt1.setText(str); y += 10; g.drawString("", x, y); y += 10; g.drawString("游戏目标:采走所有的钻石", x, y); y += x; g.drawString("", x, y); y += x; g.drawString("移动方式:方向键", x, y); y += x; g.drawString("", x, y); y += x; g.drawString("采钻规则:水平或竖直方向两个以上的钻", x, y); y += x; g.drawString("", x, y); y += x; g.drawString("", x, y); y += x; g.drawString("WIN:所有钻消失", x, y); y += 20; g.drawString("", x, y); y += x; g.drawString("LOST:钻石有剩余", x, y); y += x; g.drawString("", x, y); } }
(3)运行结果:第一次进入游戏的界面如图17-24所示,选择游戏等级后的界面如图17-25所示。
图17-24 采钻游戏界面
图17-25 运行中的游戏界面
源程序解读
(1)init()方法Applet初始化,创建媒体跟踪器MediaTracker,getImage()方法获取指定路径下的图像;调用addImage()方法将指定的图片加载到tracker中;调用splitImage()方法为sImage数组赋值,sImage数组是一个Image类型的;再次调用addImage()方法依次将sImage数组中的值加载到tracker中;调用waitForAll()让线程处于休眠状态,一直等待此媒体跟踪器跟踪加载完所有图像。接着对Applet程序的外观进行设置。
(2)start()方法启动线程。
(3)stop()方法终止线程。
(4)dat2Canvas()方法的具体作用是根据传入的游戏级别,将data_grade数组中定义好的游戏数据依次取出放入MinCanvas数组中。
(5)keyDown()方法的作用是对键盘中上、下、左、右键的处理,每个按键下都隐含一个int型数字,根据这些数字来计算钻石在每走一步后的位置。
(6)moveBall()方法的作用是根据传进来的两个参数值,规定通过键盘按键后,钻石出现的位置。
(7)splitImage()方法的作用是根据xyCoord数组提供的图像的坐标和起始位置对指定的图片进行分隔,也就是一个图片上取下一部分。