引言
一张湘绣汇集了湘女累月的心血,我们称之为劳动密集型,一块芯片集聚着众多高新的科技,我们称之为技术密集型,一个实例承载了丰富的知识点,是否可以称为知识密集型呢:)?用一张网捞到更多的鱼是渔夫的追求,通过一个实例学到更多的知识点则是我们这些开发人员的企盼。
本文拟通过一个耳熟能详的指法练习游戏讲解如何在jbuilder 2005下开发applet应用程序,通过本文,你将可以学习到图形用户界面开发、动画处理、声音播放、事件处理、多线程、i/o读写、applet打包、applet安全模型、数字签名、jre插件制作、jdk5.0等方面的知识,并适时介绍笔者一些开发经验。
阅读导航:
指法练习applet游戏介绍
我首先介绍一下这个游戏的界面,这个applet共由11个组件组成,左边的主界面是画布canvas组件,被分隔为10个栏。程序会随机在这些栏中产生下落的字母,用户按下匹配的字母键盘按键后,即为击中。
程序框架
打开jbuilder2005,创建一个工程(file->new...->project->双击project页中的project图标),我们将工程名取为game,然后利用下面的步骤,调用applet向导生成typetrainapplet。
主体程序
游戏界面中每一个下落的字母对应一个字母下落线程dropcharthread的实例,这个线程负责将一个随机的字母在指定的画布栏中从上至下落下。在typetrainapplet内部定义这个线程类,之所以要将其作为成员内部类来定义,是因为这样可以减少类和类之间的通信,降低调用接口的复杂度。
控制游戏
至此,我们已经完成了applet主要功能的开发,剩下的工作是如何通过按钮控制游戏。在编写控制代码之前,先为开始/暂停按钮(jbutton1)和停止按钮(jbutton2) 装饰一下,再编写控制代码。
打包并进行数字签名
浏览器对applet的数字签名支持并没有一个统一的标准,但是一些著名的浏览器如ie和navigator对进行数字签名的applet都可以开放大部分的权限。要对applet进行数据签名必须先将applet类和资源打成一个jar包。
使用插件下载jre
如果客户端游览器还未安装jre或已安装的jre版本低于你applet的要求,或浏览器自带的jre不是sun公司标准的applet,你applet都可能无法正常运行。可以通过jdk自带的htmlconverter.exe工具对带applet的html文件进行转换。
总结
我们讲述了如何在jbuilder开发一个简单的applet指法练习游戏程序的过程,虽然这个游戏在功能上属于不敢见公婆型,但它涵盖了applet开发的大部分内容和技巧。我们特在applet中设置了一个不安全的功能:在客户机器中保存文件,浏览器事先毫不留情地阻截了它,尔后我们通过数字签名技术晓之以情,动之以理"说服"了浏览器取消安全限制。
大千世界,纷繁复杂,客户端浏览器的jre版本和厂家百家争鸣,百花齐放,为了使我们的applet能够在sun标准的jre1.5.0的版本上运行,我们动用了jdk自带的转换器对原html进行转换,这样标准的jer1.5.0将作为插件的形式下载并安装以支持这个难伺候applet。
指法练习applet游戏介绍
1、界面及功能
指法练习的applet游戏的界面如下图所示:
图 1 指法练习的用户界面 |
如上图所示,这个applet共由11个组件组成,左边的主界面是画布canvas组件,被分隔为10个栏。程序会随机在这些栏中产生下落的字母,用户按下匹配的字母键盘按键后,即为击中,相应的字母将消失,正确数递增1;字母落到画布底端后,还没有被击中,失败数递增1;每产生一个下落的字母,总数递增1。
游戏提供了3个jbutton的按钮,分别用于控制游戏的开始/暂停、结束以及保存游戏成绩。在未启动游戏前第一个按钮显示为三角箭头的图标,点击后启动游戏,随后按钮图标切换为暂停的图标。而第二个为停止按钮,其上显示结束的图标,当游戏处于运行或暂停的状态时,点击该按钮将停止游戏以便重新开始。而第三个按钮保存游戏的成绩到客户端的d:\result.txt文件中。
整个界面采用borderlayout布局管理器,画布位于borderlayout.center区,而右边的控制台jpanel位于borderlayout.east区。控制台的jpanel采用gridlayout布局管理器。
2、程序组成
每个下落的字母对应一个线程实例,称为dropcharthread线程,它由一个产生器定时产生出来,这个产生器也是一个线程称为generatedropthread线程,下面是这个applet的类图:
图 2 applet类图 |
typetrainapplet类继承了japplet,是游戏的主类,dropcharthread和generatedropthread都是其内部类,后两者都继承thread,以线程的方式运行,下面对这3个类重要的成员变量和成员方法进行说明。
1).typetrainapplet
继承japplet的applet主类,负责构造用户界面、响应用户操作事件、更新游戏统计数据等。
? 重要成员变量
统计数据变量
volatile int totalcount = 0;//生产下落字母的总数。 volatile int rightcount = 0;//正确击中的字母数。 volatile int errorcount = 0;//未被击中且到达画布底部的字母数。 |
这3个变量用volatile作了修饰,这是因为这3个变量会被每个字母下落线程更改,为防止各个线程通过各自的缓存更改变量值造成线程间值的不同步,需要将这3个变量设置为volatile的类型,这样这些变量的更改值对于其他线程马上可见。
字母下落速率控制变量
private static int steplen = 2; //每次下落的步长,即字母每移一步的象素。 private static int stepinterval = 50; //每两步之间的时间间隔,以毫秒为单位。 private static int columncount = 10; //画布被分隔为多个栏 private static int generateinterval = 500; //创建一个新的下落字母线程的时间间隔,以毫秒为单位 |
applet通过通过这4个变量达到控制产生字母的快慢和字母下落的速度及栏数,可以进一步规划这些值,以形成游戏的难度级别。有鉴于此,我们特地将这些参数的值通过html的<applet>参数传入。这样,只需要更改html的<applet>参数值就可以达到控制游戏难度级别的目标,而不需更改applet程序。
其他
int colwidth; //下落字母每栏的宽度,在运行期才获取这个变量值,它由画布的宽度和栏数决定。 volatile char presskeychar; //记录当前按键对应的字母。 int statuscode = 0; //记录游戏所处的状态,其中1:运行态、,2:暂停态 0:停止态。 |
? 重要成员方法
private void drawresult()//将统计结果写到界面的对应jlabel中。 private void resetgame()//重置游戏现场 |
2) dropcharthread
是一个线程,将一个随机的字母在画布的特定栏中往下落下,并实时检测是否被击中,如果击中马上消失,否则一直落到画布的底部。
?重要成员变量
char c; //对应的字母 int colindex; //对应画布的栏序号,第一栏为1,第二栏为2,以此类推 int x, y; //当前字母在画布中的坐标 |
?动作类型常量
private static final int action_draw_font = 1; //表示画字符 private static final int action_clear_font = 2; //表示清除字符 |
不应当直接用1或2表示动作的类型,而应该定义一个更有意义的常量,这样不但于理解,也便于以后的维护。
?重要成员方法
public dropcharthread(char c, int colindex)//构造函数,传入特定的字母和栏序号 private void draw(int actiontype)//在画布中特写的位置上画字母 |
3) generatedropthread
?重要成员变量
random random = new random(); //负责产生随机数 |
?重要成员方法
private char getrandomchar()//获取一个随机的字母 |
负责定时产生一个dropcharthread线程实例,通过generateinterval成员变量控制产生dropcharthread线程实例的频率。
当游戏玩家点击applet的开始按钮后,applet将启动游戏,这3个类之间的交互关系可以通过以下的顺序图来描述,如下图所示:
图 3 开始游戏的顺序图 |
1)当用户按下applet的开始按钮后激发一个事件。
2) applet响应这个事件,调用事件响应方法,在方法中实例化一个generatedropthread线程,并启动这个线程。
3) generatedropthread线程定时产生一个dropcharthread线程,并让赋予一个随机的字母和栏序号。
4)dropcharthread线程启动,将字母在特定的栏中从上至下落下。
程序框架
1、利用向导生成applet
首先创建一个工程(file->new...->project->双击project页中的project图标),我们将工程名取为game,然后利用下面的步骤,调用applet向导生成typetrainapplet。
1) 启动applet向导
file->new...->web->双击web页中applet的图标启动共4步的applet向导。
2) 向导第1步,填写applet的详细信息。
图 4 applet向导第1步 |
?classname:applet的类名,填入typetrainapplet
?package:包名,接受默认值
?base class:父类,有两个选项,一个是java.applet.applet,另一个是javax.swing.japplet。前者以awt为基础,而后者以swing为基础。如果客户端浏览器的jre版本很低,且你不希望客户下载额外的插件,则需要考虑用java.applet.applet,且不能应用高版本jdk中的特性,这里我们用javax.swing.japplet。
?generate header comments:在产生applet代码时,产生类标题头的注释说明,你大可不必生成这些注释。
?can run standalone:是否将applet设置为可独立运行,如果勾选,jbuilder为其生成了一个main函数,这样就可以在脱离浏览器或appletviewer的情况下,像一般可运行类一样运行这个applet中的功能,我们不勾选它。
?generate standard methods:是否生成applet的标准函数,大家都知道applet通过4个函数管理着applet的生命周期,它们分别是init()、start()、stop()、destroy()。如果不勾选这个选项,向导只会生成init()方法,而其他3个方法不会生成。在我们的例子中,需要用到其他3个方法,所以需要勾选。
按next到下一步。
3) 定义applet的参数
applet的参数是指通过网页中<applet>标签的<param>指定的参数值,这些值可以在applet类中引用到。这样就允许在不改变applet程序的情况下,仅通过网页中<applet>属性值的更改控制applet的表现。我们在这一步中为applet设置4个控制变量参数,如下图所示:
图 5 为applet设置参数 |
这一步的设置,不但为网页生成了参数声明,还为applet程序生成了从网页获取参数值的方法,在applet初始化时,即将网页中的参数值赋给applet的成员变量。
点击add parameter新增一行,声明一个新的参数,其中name*为网页中参数的名字,而variable*为applet类成员变量名,通过type*栏设置成员变量的数据类型。你还可以为参数在default栏中指定一个默认的值,在desc中给出描述说明信息,其中带*的栏是必填的栏。
点击next到下一步。
4) 设置包含这个applet的网页
在这一步里,我们指定包含这个applet网页的<applet>标签的一些属性,如下图所示:
图 6 设置引用applet的网页 |
jbuilder会生成一个引用applet的html网页,网页名字和applet的类名相同,网页通过<applet>标签引用applet,网页的标题及<applet>属性值在这一步中设置。
我们除将height从默认的300调整为400,其他的都保持不变。按next到最后一步。
5) 创建运行配置项
在这一步里jbuilder允许你决定是否为applet生成一个运行配置项,运行配置项是允许你配置运行时的有关属性,如运行的入口类,在运行时是否重新编译等,你也可以通过project->project properties...->run来维护运行配置项。
图 7 设置applet运行配置信息 |
点击finish完成applet的创建向导。此时jbuilder为这个applet生成了两个文件,一个是typetrainapplet.java程序文件,而另一个是引用这个applet的typetrainapplet.html网页。我们来看这两个文件的主要结构。
代码清单 1 typetrainapplet.html 引用applet的网页表
1. <html> 2. <head> 3. <meta http-equiv="content-type" content="text/html; charset=gbk"> 4. <title> 5. html test page 6. </title> 7. </head> 8. <body> 9. game.typetrainapplet will appear below in a java enabled browser.<br> 10. <applet 11. codebase = "." 12. code = "game.typetrainapplet.class" 13. name = "testapplet" 14. width = "400" 15. height = "400" 16. hspace = "0" 17. vspace = "0" 18. align = "middle" 19. > 20. <param name = "steplen" value = "2"> 21. <param name = "stepinterval" value = "50"> 22. <param name = "columncount" value = "10"> 23. <param name = "generateinterval" value = "500"> 24. </applet> 25. </body> 26. </html> |
在向导第2步所设置的applet参数悉数在网页中定义(第20~23行),在向导第3步中设置的applet属性反映在第11~18行中。
applet类的typetrainapplet.java文件代码如下所示:
代码清单 2 typetrainapplet.java
1. package game; 2. 3. import java.awt.*; 4. import java.awt.event.*; 5. import java.applet.*; 6. import javax.swing.*; 7. 8. public class typetrainapplet1 extends japplet { 9. boolean isstandalone = false; 10. borderlayout borderlayout1 = new borderlayout(); 11. int steplen; 12. int stepinterval; 13. int columncount; 14. int generateinterval; 15. 16. //get a parameter value 17. public string getparameter(string key, string def) { 18. return isstandalone ? system.getproperty(key, def) : 19. (getparameter(key) != null ? getparameter(key) : def); 20. } 21. 22. //construct the applet 23. public typetrainapplet1() { 24. } 25. 26. //initialize the applet 27. public void init() { 28. try { 29. steplen = integer.parseint(this.getparameter("steplen", "2")); 30. } catch (exception e) { 31. e.printstacktrace(); 32. } 33. try { 34. stepinterval = integer.parseint(this.getparameter("stepinterval", 35. "50")); 36. } catch (exception e) { 37. e.printstacktrace(); 38. } 39. try { 40. columncount = integer.parseint(this.getparameter("columncount", 41. "10")); 42. } catch (exception e) { 43. e.printstacktrace(); 44. } 45. try { 46. generateinterval = integer.parseint(this.getparameter( 47. "generateinterval", "500")); 48. } catch (exception e) { 49. e.printstacktrace(); 50. } 51. try { 52. jbinit(); 53. } catch (exception e) { 54. e.printstacktrace(); 55. } 56. } 57. 58. //component initialization 59. private void jbinit() throws exception { 60. this.setsize(new dimension(400, 300)); 61. this.getcontentpane().setlayout(borderlayout1); 62. } 63. 64. //get applet information 65. public string getappletinfo() { 66. return "applet information"; 67. } 68. 69. //get parameter info 70. public string[][] getparameterinfo() { 71. java.lang.string[][] pinfo = { { 72. "steplen", "int", "每次下落的步长"}, { 73. "stepinterval", "int", "每移动一个像素的间隔时间,以毫秒为单位"}, { 74. "columncount", "int", 分成多少列"}, { 75. "generateinterval", "int", 分成多少列"}, 76. }; 77. return pinfo; 78. } 79. 80. //static initializer for setting look & feel 81. static { 82. try { 83. //uimanager.setlookandfeel(uimanager.getsystemlookandfeelclassname()); 84. } catch (exception e) { 85. } 86. } 87. } |
其中第11~14行定义了对应向导第2步所定义的参数变量,第70~78行获取参数的注释信息。在applet通过init()初始化,在init()中调用方法将网页中参数的值赋给applet类的成员变量,以初始化变量的值。在第59~62行设定了applet的大小,其值应该和网页中<applet>的width和height属性值一致。
2、设计applet界面
打开typetrainapplet.java,切换到design视图页面中,设计如下的applet界面
图 8 applet界面设计 |
承继japplet的applet其默认的布局管理器是borderlayout,首先在其东区(borderlayout.east)放置一个infopnl的jpanel组件,将infopnl的布局管理器设置为gridlayout,9行1列,即在jbinit()方法中通过infopnl.setlayout(new gridlayout(9, 1));设置。再在infopnl上依次放置9个组件,这些组件的类型和用途分别说明如表所示:
表 2 组件说明
组件名 | 组件类型 | 用途 |
jbutton1 | jbutton | 开始/暂停按钮 |
jbutton2 | jbutton | 结束按钮 |
jbutton3 | jbutton | 保存按钮 |
totallbl_1 | jlabel | 总数标签 |
totallbl_2 | jlabel | 总数值显示标签 |
rightlbl_1 | jlabel | 正确数标签 |
rightlbl_2 | jlabel | 正确数值显示标签 |
errorlbl_1 | jlabel | 错误数标签 |
errorlbl_2 | jlabel | 错误数值显示标签 |
你只要从设计器窗口左边的组件库中用鼠标将组件拖到设计窗口的相应位置,并放开鼠标就可以了相应生成用户界面的代码了。
由于画布组件没有java.awt.canvas并没有列在jbuilder的组件面板中,你可以直接通过编码的方式把画布组件放到applet的中区(borderlayout.center),了可以点击jbuilder可视化设计器的bean选择器图标 (位于组件库的上方),在弹出的bean chooser对话框中选择java.awt.canvas,如下图所示:
图 9 通过bean选择器选择canvas组件 |
在bean chooser对话框中有一棵以包组织的类树,选择canvas类,再点击ok按钮,在可视化设计器的bean选择器的下拉菜单中将出现java.awt.canvas的类,如下图所示:
图 10 bean选择器中的canvas组件类 |
点击下拉菜单中的java.awt.canvas,鼠标移到applet设计界面的中心点击一下,一个画布组件就被添加到applet的中区去了。在组件树中选中这个applet中,将其命名为canvas,并确认其constaints属性是center(即位于中区)。
3、游戏的统计数据
游戏包括3个统计数据,即已产生字母的总数,被正确击中的字母数及未被击中的字母数。需要有3个变量来保存这些统计数据,同时还需要一个方法,将数据写到applet界面的统计标签组件中去。当用户点击开始按钮时调用resetgame()方法将这些统计数据归0。
我们在typetrainapplet中添加以下粗体的代码。
代码清单 3 统计数据
1. … 2. public class typetrainapplet extends japplet 3. { 4. … 5. volatile int totalcount = 0;//总数计数器 6. volatile int rightcount = 0;//正确数计数器 7. volatile int errorcount = 0;//错误数计数器 8. public typetrainapplet() 9. {} 10. … 11. //将统计结果画到界面上 12. private void drawresult() 13. { 14. totallbl_2.settext("" + totalcount); 15. rightlbl_2.settext("" + rightcount); 16. errorlbl_2.settext("" + errorcount); 17. } 18. //重置现场 19. private void resetgame() 20. { 21. totalcount = 0; 22. rightcount = 0; 23. errorcount = 0; 24. drawresult(); 25. } 26. … 27. } |
drawresult()方法以下两种情况下都应被调用:
?击中一个字母。
?一个字母下落到底端。
而resetgame()方法在点击开始按钮后调用,将3个统计变量归零,以便重新开始统计。
主体程序
1、字母下落线程
游戏界面中每一个下落的字母对应一个字母下落线程dropcharthread的实例,这个线程负责将一个随机的字母在指定的画布栏中从上至下落下。在typetrainapplet内部定义这个线程类,之所以要将其作为成员内部类来定义,是因为这样可以减少类和类之间的通信,降低调用接口的复杂度。dropcharthread需要访问到typetrainapplet的众多成员,作为内部类就可以直接访问typetrainapplet类的成员变量了。其代码如下所示:
代码清单 4 dropcharthread字母下落线程
1. … 2. public class typetrainapplet extends japplet { 3. … 4. private class dropcharthread extends thread { 5. char c; //对应的字符 6. int colindex; //在哪列下落 7. int x, y; //行列的坐标 8. private static final int action_draw_font = 1; //画字符 9. private static final int action_clear_font = 2; //清字符 10. public dropcharthread(char c, int colindex) { 11. this.c = c; 12. this.colindex = colindex; 13. this.x = (colindex - 1) * colwidth + colwidth / 2; //所在的横坐标 14. } 15. //线程方法 16. public void run() { 17. draw(action_draw_font); 18. try { 19. while (c != presskeychar && y < canvas.getheight() && statuscode != 0) { 20. synchronized (canvas) { 21. while (statuscode == 2) { 22. canvas.wait(); 23. } 24. } 25. draw(action_clear_font); 26. y += steplen; 27. draw(action_draw_font); 28. thread.sleep(stepinterval); 29. } 30. } catch (interruptedexception ex) { 31. } 32. 33. presskeychar = ' '; 34. draw(action_clear_font); 35. if (statuscode != 0) {//游戏没有停止 36. totalcount++; //统计总数 37. if (y < canvas.getheight()) { 38. rightcount++; //击中 39. } else { 40. errorcount++; //打不中 41. } 42. drawresult(); 43. } 44. } 45. 46. /** 47. * 画字母 48. * @param actiontype 1:画字母 2: 清除字母 49. */ 50. private void draw(int actiontype) { 51. synchronized (canvas) { //必须对资源canvas进行同步,否则会产生线程不安全 52. graphics g = canvas.getgraphics(); 53. if (actiontype == action_clear_font) { 54. g.setxormode(canvas.getbackground()); //清除 55. } 56. g.setfont(new font("times new roman", font.plain, 12)); 57. g.drawstring("" + c, x, y); 58. } 59. } 60. } 61. … 62. } |
由于这个类比较关键,逻辑也比较复杂,为了方便说明,我们将其流程通过一个流程图来描述,如下图所示:
图 11 字母下落线程流程图 |
1) 首先在栏序号为colindex的栏的第一个位置画出保存在变量c中的字母(第17行)。
2) 当这个字母未被击中,未到达画布底部且用户未结束游戏进行循环,这步判断对应程序的19行。如果这个判断条件通过进入第3步,即进入循环体,否则转到第5步。
3) 如果被暂停,这个线程进入等待态,直接被通知后才继续运行。需要指明一点的是,所有字母下落线程都用画布对象canvas进行同步,即用canvas进行通讯。线程间要进行通讯时,一定需要通讯线程都可以访问到的对象充当媒介将这些线程"串"起来,通过这个对象的notify()/notifyall()/wait()在线程间通讯。这个对象好比一个"月下老人",在线程的情人间传递音讯。
4) 当线程被唤醒后,或原来就没有等待,则进入下一个循环的处理过程,在这个过程中,程序将原来位置的字母清除,下移纵坐标,并在新的位置画字母,以达到字母下落的动画效果,然后下落线程睡眠指定的毫秒数,毫秒数值为typetrainapplet成员变量stepinterval的值,而这个值可以在网页的<param name = "stepinterval" value = "50">标签中定义,达到控制下落速度的效果。
因为在画布上画字母后,这个字母并不会自动消失,如果直接移动纵坐标并在新位置画字母,原位置的字母依就存在。所以在新位置画字母之前,必须先将旧位置的字母清除。我们用了一个小技巧,即使用graphics对象的setxormode()方法,该方法两图像重叠部分的颜色。我们调用这个方法将图像重叠部分的颜色设置为画布的背景色,这样在原来的位置上再次画字母时,因为前后两次画个字母刚好重叠,就达到了清除原位置字母的效果。
画字母和清除字母的程序相似,我们把它抽出到一个方法中draw(int actiontype),如第50~59行代码所示,通过入参决定是清除还是画新字母。为增强程序的可读性,我们在第8~9行中定义了两个用于表示清字母和画字母的动作常量。
5) 当程序出了循环体后,进行善后的处理:将用于保存用户按键字母的presskeychar变量置为空字符,在画布上清除移到底部的字母。如果游戏没有结束统计数据,并将数据写到界面的jlabel组件中。
2、添加击中音效
击中字母后播放一个短促的声音,将能大大提高游戏的听觉体验,这在节里,我们对字母下落线程稍作更改,以使其支持音效。
首先准备一个声音文件hit.wav,放在typetrainapplet.java相同的文件夹下。applet类中定义了一个getaudioclip(url url)方法,通过这个方法可以获取audioclip的声音文件的对象。通过audioclip的play()即可播放这个音效。
代码清单 5
1. … 2. import java.applet.audioclip; 3. public class typetrainapplet extends japplet { 4. … 5. audioclip hitsound;//声明音效对象 6. … 7. public void init() { 8. … 9. hitsound = getaudioclip( (typetrainapplet.class).getresource( 10. "hit.wav"));//初始化音效对象 11. } 12. … 13. private class dropcharthread extends thread { 14. … 15. public void run() { 16. … 17. draw(action_clear_font); 18. if (statuscode != 0) { //游戏没有停止 19. totalcount++; //统计总数 20. if (y < canvas.getheight()) { 21. hitsound.play();//击中时播放音效 22. rightcount++; //击中 23. } else { 24. errorcount++; //打不中 25. } 26. drawresult(); 27. } 28. } 29. } 30. … 31. } |
在第5行定义一个音效的对象,在applet初始化时获取音效对象,如第9行所示。更改字母下落线程,当击中下落的字母时播放音效,如第21行所示。
3、字母下落线程的产生器线程
指法练习需要"子子孙孙,无穷匮也"地不断产生字母下落线程,以使游戏持续进行,这个工作由产生器线程generatedropthread负责。generatedropthread线程出于和dropcharthread同样的原因,也作为typetrainapplet成员内部类来定义,其代码如下所示:
代码清单 6 generatedropthread 产生器线程
1. … 2. public class typetrainapplet extends japplet { 3. … 4. private class generatedropthread extends thread { 5. random random = new random(); //随机数 6. public void run() { 7. try { 8. while (statuscode != 0) { //产生下落线程 9. synchronized (canvas) { 10. while (statuscode == 2) { 11. canvas.wait(); 12. } 13. } 14. dropcharthread dropcharthread = new dropcharthread( 15. getrandomchar(), 16. random.nextint(columncount) + 1); 17. dropcharthread.start(); 18. thread.sleep(generateinterval); 19. } 20. } catch (interruptedexception ex) { 21. } 22. } 23. 24. /** 25. * <b>功能说明:</b><br> 26. * 返回一个随机字符 27. * @return 随机字符 28. */ 29. private char getrandomchar() { 30. int temp = 97 + random.nextint(26); 31. return (char) temp; 32. } 33. } … 34. } |
这个线程很简单:定期创建并启动一个dropcharthread字母下落线程。需要特别说明的是如何为字母下落线程提供一个随机字母和一个随机栏序号。我们通过一个随机对象java.util.random的nextint(int range)方法产生一个0~range-1的整数作为随机栏序号,在第29~32行定义了一个随机产生字母的getrandomchar()方法,因为小写字母a~z的ascii代码是97~112,第30行即得到一个小写字母所对应的ascii代码,通过第31行强制类型转换就可获取一个随机的小写字母字符。
在每次循环时,都判断游戏是否被暂停,如果暂停,则线程进入睡眠,暂停产生字母下落线程,如第8~13行所示。为了统一游戏总体的控制,所以这个线程也通过canvas对象进行同步,在其他地方调用canvas.notifyall()方法后,暂停的线程就苏醒出来,继续工作。
在第18行,线程睡眠一小段时间,通过typetrainapplet的generateinterval成员变量就可以控制字母下落线程下落的速度,这个参数可以直接通过网页<param name = "generateinterval" value = "500">指定其值。
4、响应用户按键事件
所谓击中下落的字母,即是用户按下键盘中的一个键所对应的字母和某个字母下落线程的字母是一致的,对应的字母下落线程结束并将击中数递增1。
要让游戏自动监测到用户所按的按键,就需要applet响应键盘按键事件,下面我们来为applet生成按键事件的处理方法。
打开typetrainapplet.java,切换到design视图页中,在结构窗格的组件树中选择this(borderlayout)节点,切换属性查看器到event标签页中,双击keypressed项,如下图所示:
图 12 为applet生成响应按键的事件处理方法 |
此时,jbuilder为applet生成了一个按键事件监听器,并切换到source视图页并将光标定位到事件处理方法中,在方法中键入如下粗体的代码。
1. … 2. public class typetrainapplet extends japplet { 3. … 4. /**获取用户点击按键所对应的字符*/ 5. public void this_keypressed(keyevent e) { 6. if (!e.isactionkey()) { 7. presskeychar = e.getkeychar(); 8. } 9. } 10. … 11. } |
第6行判断按键是否字符的按键,如果是在第7行中获取按键所对应的字符。
控制游戏
至此,我们已经完成了applet主要功能的开发,剩下的工作是如何通过按钮控制游戏。在编写控制代码之前,先为开始/暂停按钮(jbutton1)和停止按钮(jbutton2) 装饰一下,再编写控制代码。
1、为按钮添加图标
需要准备3张按钮的图标,图标为gif格式,尺寸大小为25×24象素。
?:jbutton1在结束和暂停状态的图标,命名为start.gif。
?:jbutton1在游戏处于运行状态的图标,命名为pause.gif。
?:jbutton2的图标,命名为stop.gif。当游戏处于暂停或运行状态时,jbutton2才被激活。
将这些文件放置在typecharapplet.java源文件的目录下,即<工程根目录>/src/game目录下。
下面的代码使用java.awt.imageicon引用这3个图标,并在jbinit()中将图标显示到按钮上,如下所示:
代码清单 7 定义3个图标变量
1. … 2. public class typetrainapplet extends japplet { 3. … 4. imageicon starticon = new imageicon(typetrainapplet.class.getresource("start.gif")); 5. imageicon pauseicon = new imageicon(typetrainapplet.class.getresource("pause.gif")); 6. imageicon stopicon = new imageicon(typetrainapplet.class.getresource("stop.gif")); 7. … 8. private void jbinit() throws exception { 9. … 10. jbutton1.seticon(starticon);//设置开始按钮的图标 11. jbutton2.seticon(stopicon);//设置停止按钮的图标 12. jbutton2.setenabled(false);//将停止按钮图标置为非激活态 13. … 14. } 15. } |
第4~6用前面所述的图片初始化3个图标变量,其中typetrainapplet.class.getresource()方法以typetrainapplet.class所在目录为相对目录,查询资源文件。
第10~11行分别将开始和结束图标显示到对应的按钮上,当用户点击开始按钮后,才将jbutton1的图标切换为暂停的图标pauseicon。
2、通过按钮事件控制游戏
由于字母下落线程通过监测statuscode的值决定结束或暂停,所以我们仅需要通过按钮事件更改这个控制变量就可以达到控制游戏的目的了。
首先,我们打开typetrainapplet.java切换到design的ui设计界面中,双击jbutton1按钮,jbuilder自动为jbutton1添加一个按钮点击事件监听器,并切换到source视图中,将光标定位到事件处理方法处,我们在方法中添加以下粗体的代码:
代码清单 8 开始/暂停按钮事件处理方法
1. … 2. public class typetrainapplet extends japplet { 3. … 4. public void jbutton1_actionperformed(actionevent e) { 5. if (statuscode == 0) { //从结束->开始 6. resetgame(); 7. statuscode = 1; 8. colwidth = canvas.getwidth() / columncount; 9. //实例化字母下落线程产生器 10. generatedropthread gdthread = new generatedropthread(); 11. gdthread.start();//产生器启动 12. jbutton1.seticon(pauseicon);//切换为暂停的图标 13. jbutton2.setenabled(true);//停止按钮激活 14. } else if (statuscode == 1) { //从运行->暂停 15. statuscode = 2; 16. jbutton1.seticon(starticon); 17. } else { //从暂停->运行 18. statuscode = 1; 19. jbutton1.seticon(pauseicon); 20. synchronized (canvas) {//通过canvas通知所有暂停的线程继续运行 21. canvas.notifyall(); 22. } 23. } 24. this.requestfocus();//applet接受光标,以便其接受按键事件 25. } 26. … 27. } |
在jbutton1的按钮点击事件处理方法里根据statuscode所标识的游戏状态分别进行处理:
?当statuscode=0时,游戏原处于结束或未开始的状态,表示用户执行开始游戏的命令。开始一个新游戏的命令,将统计数据归0,根据画布当前的宽度和栏数计算出每栏的宽度,实例化一个产生器线程,并切换按钮的图标为暂停图标,将停止按钮置为激活态。
?当statuscode=1时,游戏原处于运行态,表示用户执行暂停的命令。更改状态并更换按钮的图标。
?当statuscode=2时,游戏原处于暂停态,表示用户执行暂停后继续游戏的命令。更改状态并更换按钮图标,通过canvas对象通知所有暂停的线程。
其次,给停止按钮jbutton2生成以下的事件响应代码:
代码清单 9 停止游戏的事件处理代码
1. … 2. public class typetrainapplet extends japplet { 3. … 4. public void jbutton2_actionperformed(actionevent e) { 5. statuscode = 0; 6. synchronized (canvas) { 7. canvas.notifyall(); 8. } 9. jbutton2.setenabled(false); 10. jbutton1.seticon(starticon); 11. } 12. … 13. } |
首先更改游戏的状态,在第6~7行向所有处于等待状态的线程发出一个通知,防止线程"睡死"的情况。线程在循环体的判断语句中判断出statuscode为0后将纷纷退出,所有线程结束。而后,将按钮置为非激活状并将开始/暂停按钮切换为开始的图标。
3、保存游戏统计数据
为了演示通过数字签名技术突破applet安全限制的方法,我们特地设计了一个功能:将游戏的统计数字写入到客户端机器的d:\result.txt文件中。在ui设计界面中双击jbutton3的按钮,为"保存"按钮添加如下的事件处理方法:
代码清单 10 保存按钮事件处理方法
1. … 2. import java.io.*; 3. public class typetrainapplet extends japplet { 4. … 5. public void jbutton3_actionperformed(actionevent e) { 6. filewriter fw = null; 7. try { 8. file file = new file("d:\\result.txt"); 9. fw = new filewriter(file); 10. fw.write("总数:" + totalcount + "\n"); 11. fw.write("正确数:" + rightcount + "\n"); 12. fw.write("失败数:" + errorcount); 13. fw.flush(); 14. joptionpane.showmessagedialog(this, "成绩成功保存到d:\result.txt中", 15. "信息",joptionpane.ok_option); 16. } catch (ioexception ex) { 17. ex.printstacktrace(); 18. } finally { 19. try { 20. if (fw != null) { 21. fw.close(); 22. } 23. } catch (ioexception ex1) { 24. ex1.printstacktrace(); 25. } 26. } 27. } 28. … 29. } |
至此,我们就完成了整个游戏的开发过程,rebuild事件工程,在<工程根目录>/classes文件夹下双击打开applet向导为我们所生成的typetrainapplet.html网页,假如你机器已经安装了jre,我们的指法练习游戏将在网页中打开,点击开始按钮玩伙自己亲手制作的游戏,如下图所示:
图 13 在网页中运行指法练习applet游戏 |
点击停止按钮停止游戏,试着点击"保存"按钮,将统计数据保存到d:\result.txt中,你将会在java控制台中看到一个安全异常信息,如下图所示:
图 14 applet的功能被安全管理器限制 |
因为一般的applet运行在称为"沙盒"的安全模块下,applet虽然在客户端机器上运行,但至多只是一个"外来客",客户机没有将其当作"自家人"来对待。所以applet不能执行访问本地文件系统、执行本地程序,保存统计数据到文件也就发生异常了。在本章后面,我们将详细介绍如何通过数字签名的技术来绕过applet的安全限制,让客户机将这个"外来客"宾至如归。
4、关注applet生命周期
applet在浏览器中运行时,第一次加载applet,将调用init()方法,接着调用start(),当窗口关闭或页面替换时先调用stop()然后再调用destroy()。
因为我们的游戏是多线程的程序,当关闭浏览器时,如果applet的字母下落线程还在运行可能会引发异常。在jbuilder中右击typetrainapplet.html,在弹出的菜单中选择run using default,jbuilder使用appletviewer运行typetrainapplet。启动游戏后直接关闭窗口,在信息窗格中将报告以下的异常信息:
java.lang.nullpointerexception
at game.typetrainapplet$dropcharthread.draw(typetrainapplet.java:290)
at game.typetrainapplet$dropcharthread.run(typetrainapplet.java:258)
这是由于关闭appletviewer后,typetrainapplet画布的graphics对象先被销毁,而字母下落线程依然调用访问这个对象,所以抛出空指针异常。
我们可以通过applet的生命周期解决这个问题:applet在被关闭前会调用stop()和destroy()方法。我们只要利用stop()方法就可以了,在stop()方法中置一个标识,线程通过判断这个标识就可以知道当前窗口是否关闭,当发现关闭时就不再运行。
1. public class typetrainapplet extends japplet { 2. … 3. boolean isclose = false;//用于标识applet窗口有没有关闭 4. … 5. public void start() { 6. isclose = false; 7. } 8. 9. public void stop() { 10. statuscode = 0;//停止游戏 11. isclose = true;//窗口关闭 12. } 13. … 14. private class dropcharthread extends thread { 15. if(isclose) return ;//发现窗口关闭马上返回 16. draw(action_draw_font); 17. try { 18. while (c != presskeychar && y < canvas.getheight() && 19. statuscode != 0) { 20. synchronized (canvas) { 21. while (statuscode == 2) { 22. canvas.wait(); 23. } 24. } 25. draw(action_clear_font); 26. y += steplen; 27. draw(action_draw_font); 28. thread.sleep(stepinterval); 29. } 30. } catch (interruptedexception ex) { 31. } 32. if (!isclose) {//窗口没有关闭才后续处理 33. draw(action_clear_font); 34. if (statuscode != 0) { //游戏没有停止 35. totalcount++; //统计总数 36. if (y < canvas.getheight()) { 37. hitsound.play(); 38. rightcount++; //击中 39. } else { 40. errorcount++; //打不中 41. } 42. drawresult(); 43. } 44. } 45. } 46. } |
applet启动时调用start()方法,这方法里将窗口关闭标识置为false,如第6行所示,而当窗口关闭时stop()方法被调用,停止游戏并置窗口关闭标识,如第10~11行所示。字母下落线程的程序也要作相应的调整,在进入线程和结束线程都判断是否关闭了窗口。
打包并进行数字签名
浏览器对applet的数字签名支持并没有一个统一的标准,但是一些著名的浏览器如ie和navigator对进行数字签名的applet都可以开放大部分的权限。要对applet进行数据签名必须先将applet类和资源打成一个jar包。
jbuilder提供一个applet的打包向导,在向导的最后一步可以指定一个数据证书对最终生成的jar包进行签名。所以在这一节里,我们先介绍数字签名的技术,而后再讲解如何使用jbuilder的applet打包向导。
1、数字签名技术
数字签名涉及到以下几个基本的概念:
?消息摘要
消息摘要是对原始数据按照一定算法进行计算得到的结果,它主要检测原始数据是否被修改过。消息摘要与加密不同,加密是对原始数据进行变换,可以从变换后的数据中获得原始数据,而消息摘要是从原始数据中获得一部分信息,它比原始数据少得多,因此消息摘要可以看作是原始数据的指纹。
?消息验证码
当甲和乙通信时,甲将数据传给乙时,同时也将数据的消息摘要传给乙,乙收到后可以用该消息摘要验证甲传的消息是否正确。这时会产生问题,即若传递过程中别人修改了数据时,同时也修改了消息摘要。乙就无法确认数据是否正确。消息验证码可以解决这一问题。 使用消息验证码的前提是 甲和乙双方有一个共同的密钥,这样甲可以将数据计算出来的消息摘要加密后发给乙,以防止消息摘要被改。由于使用了共同的密钥,所以称为"验证码"。
?数字签名
使用消息摘要和消息验证码两种技术可以保证数据没有经过改变,但接收者还无法确定数据是否确实是某个人发来的。尽管消息码可以确定数据是某个有同样密钥的人发来的,但这要求双方具有共享的密钥,若有一组用户共享,我们就无法确定数据的来源了。
数字签名即是被设计用来解决这个问题的技术。数字签名利用非对称加密技术,发送者使用私钥加密数据产生的消息摘要(签名),接收者使用发送者的公钥解密消息摘要以验证签名是否是某个人的。由于私钥只有加密者才有,因此如果接收者用某个公钥解密了某个消息摘要,就可以确定这段消息摘要必然是对应的私钥持有者发来的。
?数字证书
使用数字签名的前提是接收数据者能够确信验证签名时(用发送者的私钥加密消息摘要)所用的公钥确实是发送者本人的,这需要通过数字证书来解决这个问题。
数字证书含有两部分数据:一部分是对应发送者(组织或个人)的信息,另一部分是这个发送者所对应的公钥。即数字证书保存了发送者和其公钥一一对应的关系。同样,数字证书也有可能被假造,有效的数字证书必须经过权威 ca的签名,即权威ca验证数字证书的内容的真实性,然后再在数字证书上使用权威ca自己的私钥签名,相当于在发送者的证书上盖章。
其实数字签名技术是现实生活在计算机领域的反映,我们不妨通过一个小故事将这此技术反向映射到生活中。
《永不消失的电波》是60年代一部著名的电影,讲述了1938年我党地下组织在国统区上海的电台被敌人破坏了,延安解放区我军电台政委李侠奉命前往的上海,加强秘密电台的工作,为了保证上海地下党组织能够正确接洽到真实的李侠而不被敌人蒙骗,我们不妨来设定这样的一个情节,李侠身上带一封介绍其身份的介绍信,这相当于消息摘要,还约定了接头暗号:地下党接头中说"山上杜鹃红艳艳",李侠则要接"山下溪水细潺潺",那么这个暗号就是"消息验证码"。而在李侠的介绍信上有延安电台台长的签名,这个就是"数字签名",签名之上还加盖了一个延安电台的公章,这个就是数字证书。还有一份证名电台公章的文件那就是ca证书了。当然实际情况可能只需要一个暗号,太多东西反而会在行动中暴露身份,但在计算机领域这些东西都是必要的。
2、数字证书的生成
在jbuilder的applet打包向导中仅需要一个数字证书,向导会为最终的jar包生成消息摘要、消息验证码并签名。通过jdk自带的keytool工具可以为生成一个数据证书,这个工具位于jdk的bin目录下。
打开dos命名窗口,定位到jbuilder 2005下自带的jdk的bin目录下,执行下面的keytool命名生成一张自己的证书:
c:\borland\jbuilder2005\jdk1.4\bin>keytool -genkey -alias chenxhca -keyalg rsa -keystore supercalib -validity 3650
命令窗口将要求你输入一些个人信息如下图所示:
图 15 生成数字证书的命令窗口 |
这里我们使用keytool工具生成了一个名为chenxhca的证书,它存放到supercalib证书库中,有效期为10年,使用的加密算法上rsa。证书库supercalib的访问密码是123456,而chenxhca证书条目的访问密码是123123。在输入作为发送者身份标识的信息后就会在当前目标,即c:\borland\jbuilder2005\jdk1.4\bin下生成一个名为supercalib的证书库文件。
keytool参数较多,使用也比较复杂,详细使用说明,请参见sun网站的帮助文档:
http://java.sun.com/j2se/1.4.2/docs/tooldocs/windows/keytool.html。
一般情况下你还需要将该证书发给权威的ca签名,这个证书才会被视为合法的证书,当然你也可以模拟创建一个ca证书,用这个ca证书为我们将用于签发applet的chenxhca证书签名,为了简单起见我们忽略这一步。
3、打包
现在已经万事俱备了,我们可以开始利用jbuilder的打包向导将applet所以文件打包并签名的过程。
1) file->new...->archive,在archive页中双击applet jar图标启动applet打包向导。
2) 在向导第1步中指定applet jar的名字和保存到目标文件,如下图所示:
图 16 指定文件名 |
为了加速网络下载速度,我们勾选上compress the contents of the archive选项,压缩jar文件,减小文件的体积。always create archive when building the project选项使用每次编辑工程时都重新创建applet jar包。点击next到下一步。
3) 在这一步里,指定jar文件中所需包含的资源文件。
由于typetrainapplet程序引用了3张图片,所以jar文件除包含typetrainapplet.class程序文件外,还需要将用于按钮图标的文件选择进来,如下图所示:
图 17 指定jar的内容 |
按next到下一步。
注意:
当你指定game.typetrainapplet.class,start.gif,pause.gif,stop.gif,hit.wav时,打成的applet jar包将不能正确运行,那些和typetrainapplet类位于同一程序文件的事件监听器类将被排除在外,所以需要通过game/*.*来打包。
4) 由于向导第3~6步,我们不需要作特别的设置,所以一直按next到第7步。
在这一步里,我们用上一小节中生成的数字证书签名applet的目标jar文件,如下图所示:
图 18 指定如何对jar进行签名 |
?digitally sign this archive选项在默认的情况下是未选中的,首先勾选该选项
?点击keystore后的…按钮,选择我们刚才在c:\borland\jbuilder2005\jdk1.4\bin目录下所生成的supercalib证书库文件。
?在keystore password中输入123456,即证书库的密码。
?点击alias后的…按钮,由于我们在supercalib证书库中仅有一个chenxhca证书,所以在弹出的select alias对话框的available alias列表中仅有一个chenxhca选项,选择chenxhca证书。
?在alias password中输入123123,即chenxhca证书的私钥密码。
?在store type中输入jks,由于keytool工具的默认证书库类型是jks,所以supercalib的类型为jks。
在设置完以后的信息后,按finish结束向导,在工程窗格的资源树中将出现一个typetrainjar的节点。右击这个节点,在弹出的菜单中选择rebuild,jbuilder将创建applet的jar包,并用chenxhca证书签名。
rebuild完成后,工程窗格的typetrainjar节点就可以展开了,展开这个节点,我们发现目标jar文件中除了资源文件以外,在meta-inf文件夹下还有3个文件,如下图所示:
图 19 目标jar中关于签名的文件 |
meta-inf文件夹下的3个文件是和数字签名有关的文件,说明如下:
?manifest.mf:这个 manifest 文件定义了与扩展和包相关的数据。
?chenxhca.sf:这是 jar 文件的签名文件,文件名标识了签名者。
?chenxhca.rsa:与签名文件相关联的签名程序块文件,它存储了用于签名 jar 文件的公共签名。
4、在文件中引用applet包文件
我们现在来更改typetrainapplet.html中<applet>的属性使其通过jar来引用applet程序。这个过程很简要,打开typetrainapplet.html文件,切换到source视图页中,将光标定位在<applet>标签中。窗口右边出现<applet>标签的属性输入编辑器,在archive中输入game.jar,按回车。
扫描关注微信公众号 |