Java2游戏编程读书笔记(12

第12章 创建自定义的可视化控件和菜单本章我们将从Component2D类开始,这个类将作为所有组件的基类。最后结束时我们将看看一些容纳自定义组件的容器类,也就是面板和菜单。12.1 为什么要重新发明轮子在创建自己的自定义组件之前,衡量一下这样做的好处与困难是很必要的。正如前面所述,游戏中的可视化组件对于吸引并保持用户的注意力是非常重要的,然而,改进或者重写现成的东西可能是一种风险。q创建自定义组件系统增加了开发的费用。q自定义组件耗费更多的内存。q自定义组件增加了加载时间。q布局管理器对于原始的AWT组件所做的摆放工作足够好。这是那些满足于普通可视化组件的人的意见。现在,让我们看一下在游戏中应用自定义控件和菜单的好处:q自定义的组件管理器可以给你的组件比原始的AWT更精确的布局。q自定义组件可以给游戏自己定义的观感。q可以很容易模仿和扩充原始AWT组件的功能。q完全是视觉享受。q自定义组件使规则变得简单游戏不会因为它们在内存上节省而变得著名,所以,为了节省内存或者加载时间而节省几个KB图像的价值不会超过使用自定义组件系统可能带来的好处,而且,虽然布局管理器确实提供了一种很好的自动化摆放组件的方法,但是没有理由说不能自己实现一个类似的布局管理器。还有,由于游戏是高度可视化的,如果能够精确指摆放组件的x,y坐标,可以对场景有更多的控制,更不必说自定义的观感。特别是创建一整套游戏或者要求自定义观感时,自定义的观感会来得更直接。如果这一切还不能说服你,考虑一下使用自定义控件的最基本的一个好处:它们看起来更漂亮。有吸引力的组件就是好组件,在游戏中这就是菜单和控件系统的目标。所以,如果可以创建功能像原始AWT组件那样并能改善游戏的观感的控件,就应该那样去做。12.2 自定义控件概述现在我们来看看如何实现它。虽然我们的系统被设计来覆盖很多已经存在的AWT组件,但是仍然使用一些现存的AWT类,比如java.awt包中的Graphics2D,Image和Font类,以及java.awt.event包中各种监听器类。我们还将用到一些已经定义了的类,比如ImageGroup和AnimationStrip类。Object |—-Component2D | |—-Button2D | |—-Container2D | | |—-Panel2D | | |—-Menu2D | |—-Label2D | |—-RadioButton2D |—-ImageGroup | |—-ButtonImageGroup |—-RadioButtonGroup2DComponent2D类应该作为我们自定义组件系统的开头。上图为Component2D类及其派生类的整体结构,可以把它和第5章的Component对于,看看Component2D类和原始Component类是怎样对应的。12.2.1 Component2D类Component2D类是一个抽象类,它是自定义组件的基类,包含所有可视化组件通用的属性和方法。Component2D类既包含一个ImageGroup对象来容纳多个图像,又包含图像组的帧宽度和高度。它还包含其他的共同的组件属性,比如位置和组件的边界,还有像定义可用状态和可视性这样的状态信息。Component2D类还定义了访问关于它的信息的方法,以及几个需要子类实现的未完成的方法。给定一个Graphics2D容器后,Component2D类可以更新和绘制自身,这一点和第10章中的Actor2D类很相似。下面是Component2D类的代码,粗略看一下代码清单后,我们将看看它的哪几个地方做得比较好。import java.awt.*;import java.awt.geom.*;//创建可视化组件的基类public abstract class Component2D{ //容纳组件外观的ImageGroup protected ImageGroup group; //组件的大小 protected int frameWidth; protected int frameHeight; //组件的位置和边界,以及缓存的变换 protected Vector2D pos; protected Rectangle2D bounds; protected AffineTransform xform; //组件是否可视,是否可用的标记 protected boolean enabled; protected boolean visible; //跟踪组件的内部状态 protected int state; //用默认的属性创建一个新的Component2D对象 protected Component2D(){ pos=new Vector2D.Double(); xform=new AffineTransform(); setEnabled(true); setVisible(true); bounds=new Rectangle2D.Double(); frameWidth=0; frameHeight=0; state=0; } //使用传入的Graphics2D容器绘制组件 public abstract void paint(Graphics2D g2d); //在给定的偏移位置绘制组件 public abstract void paint(Graphics2D g2d,double dx,double dy); //更新组件的边界矩形 public void updateBounds(){ if(frameWidth<=0&&group!=null){ frameWidth=group.getFrameWidth(); } if(frameHeight<=0&&group!=null){ frameHeight=group.getFrameHeight(); } bounds.setRect(pos.getX(),pos.getY(),frameWidth,frameHeight); } //返回一个描述组件的String public String toString(){ //输出类名和当前位置 return getClass().getName()+":"+pos.toString(); } //主要属性的访问方法 public void setEnabled(boolean e){ enabled=e; } public final boolean isEnabled(){ return enabled; } public final void setVisible(boolean v){ visible=v; } public final boolean isVisible(){ return visible; } public final void setX(double px){ pos.setX(px); } public final void setY(double py){ pos.setY(py); } public final double getX(){ return pos.getX(); } public final double getY(){ return pos.getY(); } public final void setPos(double x,double y){ pos.setX(x); pos.setY(y); } public final void setPos(int x,int y){ pos.setX(x); pos.setY(y); } public final void setPos(Vector2D v){ pos.setX(v.getX()); pos.setY(v.getY()); } public final Vector2D getPos(){ return pos; } public Rectangle2D getBounds(){ return bounds; }}//Component2DComponent2D类包含了很多未完成的方法,所以看起来它不应该很复杂。虽然这个类用来模仿Component类,但是它并非从Component类中派生而来。虽然它的行为很像Actor2D类,但是它也没有从Actor2D类派生,这是为什么呢?不让Component2D从上面所说两个基类派生的原因是不同的。对于Component类而言,Component2D类和Component类之间已经没有直接的联系。Component类包含很多不需要的底层的东西,从它扩展只会造成巨大的负担。不让Component2D从Actor2D类派生,只是为了想在语义上让这两个类保持分离,不应该像通常的画面物体一样对待自定义组件,而应该被applet区分对待。这里还想让Component2D类独立于Actor2D类,这样就不会局限于维护笔者的版本。如果不需要使用Actor2D类,或者从它的原来的形式修改了它,则完全无须修改Component2D类。为了以后可以自由修改,多余一点代码是值得的。12.2.2 Label2D类我们现在想要一个容器类,它可以将绘制一串文本所需的不同属性容纳到一起,这就是Label2D类的由来。使用Label2D类,可以把像文本串,字体,变换以及要绘制的文字的颜色这样的属性都容纳到一个类中。下面是Label2D的代码,这个类包含上面所述的所有特性,还有一些额外的特性,将在测试程序中看到。下面是Label2D的代码,这个类包含上面所述的所有特性,还有一些额外的特性,将在测试程序中看到。import java.awt.*;import java.awt.font.*;import java.awt.geom.*;import java.util.*;public class Label2D extends Component2D{ //绘制时所使用的字体 protected Font font; //实际要绘制的文本 protected String text; //为可用和不可用两种状态所准备的Paint protected Paint paint; protected Paint disabledPaint; //默认的Paint protected final Paint DEFAULT_PAINT=Color.GRAY; //用指定的字体,文本和paint构建一个新的Label2D public Label2D(Font f,String str,Paint p){ super(); setFont(f); setText(str); setPaint(p); //将默认paint设置为不可用状态 setDisabledPaint(DEFAULT_PAINT); } //使用指定的字体,文本,paint和位置构建一个新的Label2D public Label2D(Font f,String str,Paint p,Vector2D v){ super(); setFont(f); setText(str); setPaint(p); setPos(v); //将默认paint设置为不可用状态 setDisabledPaint(DEFAULT_PAINT); } // public void centerOn(Rectangle2D r,Graphics2D g2d){ //从Graphics2D容器中获取FontRenderContext FontRenderContext frc=g2d.getFontRenderContext(); //获取文本布局 TextLayout layout=new TextLayout(text,font,frc); //获取布局的边界 Rectangle2D textBounds=layout.getBounds(); //设置新的位置 setX(r.getX()+(r.getWidth()/2)-(textBounds.getWidth()/2)); setY(r.getY()+((r.getHeight()+textBounds.getHeight())/2)); //更新总体边界矩形 updateBounds(g2d); } // public void updateBounds(Graphics2D g2d){ //从Graphics2D容器中获取FontRenderContext FontRenderContext frc=g2d.getFontRenderContext(); //获取文本布局 TextLayout layout=new TextLayout(text,font,frc); //获取布局的边界 Rectangle2D textBounds=layout.getBounds(); bounds.setRect(getX(),getY(), textBounds.getWidth(),textBounds.getHeight()); } public void setFont(Font f){ font=f; } public void setText(String str){ text=str; } public String getText(){ return text; } public void setPaint(Paint p){ paint=p; } public void setDisabledPaint(Paint p){ disabledPaint=p; } // public void paint(Graphics2D g2d){ //只有可见时才绘制 if(isVisible()){ g2d.setFont(font); //根据可用状态设置paint if(isEnabled()){ g2d.setPaint(paint); }else{ g2d.setPaint(disabledPaint); } g2d.drawString(text,(int)pos.getX(),(int)pos.getY()); } } // public void paint(Graphics2D g2d,double dx,double dy){ //只是在可见时才绘制 if(isVisible()){ //设置字体 g2d.setFont(font); //根据可用状态设置paint if(isEnabled()){ g2d.setPaint(paint); }else{ g2d.setPaint(disabledPaint); } //绘制字符串,并添加到偏移位置 g2d.drawString(text,(int)(pos.getX()+dx),(int)(pos.getY()+dy)); } } // public String toString(){ // return super.toString()+ ""+text; }}//Label2Dimport java.applet.*;import java.awt.*;import java.awt.image.*;import java.awt.geom.*;import java.util.*;public class Label2DTest extends Applet implements Runnable{ //动画线程 private Thread animation; private BufferedGraphics offscreen; // private Label2D[] labels; //将要使用的各种字体的描述字符串 private final String[] fonts={"Helvetica","Arial","Courier","Terminal","Georgia"}; //方便创建TexturePaint对象使用的工具类 private TexturePaint createTexturePaint(String filename){ MediaTracker mt=new MediaTracker(this); Image image=getImage(getCodeBase(),filename); mt.addImage(image,0); try{ mt.waitForAll(); }catch(InterruptedException e){ //nothing } //使用图像的宽和高创建一个新的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); //为paint的图像创建边界矩形 Rectangle bounds=new Rectangle(0,0,bi.getWidth(),bi.getHeight()); //创建Paint return new TexturePaint(bi,bounds); } public void init(){ labels=new Label2D[fonts.length]; //创建两个TexturePaint,一个用于可用状态,一个用于不可用状态 TexturePaint tpEnabled=createTexturePaint("label1.gif"); TexturePaint tpDisabled=createTexturePaint("label2.gif"); // for(int i=0;i<fonts.length;i++){ //用指定的字体,字体样式和可用paint创建一个Label2D labels[i]=new Label2D(new Font(fonts[i],Font.PLAIN,40),fonts[i],tpEnabled); // labels[i].setPos(new Vector2D.Double(50,50+(i*50))); // labels[i].setDisabledPaint(tpDisabled); } offscreen=new BufferedGraphics(this); AnimationStrip.observer=this; }//init public void start(){ //启动动画线程 animation=new Thread(this); animation.start(); } public void stop(){ animation=null; } public void run(){ long time=System.currentTimeMillis(); Thread t=Thread.currentThread(); while(t==animation){ repaint(); try{ Thread.sleep(10); }catch(InterruptedException e){ break; } // if(System.currentTimeMillis()-time>1000){ for(int i=0;i<fonts.length;i++){ labels[i].setEnabled(!labels[i].isEnabled()); } time=System.currentTimeMillis(); } } }//run public void update(Graphics g){ paint(g); } public void paint(Graphics g){ Graphics2D bg=(Graphics2D)offscreen.getValidGraphics(); bg.setPaint(Color.black); bg.fillRect(0,0,getSize().width,getSize().height); // for(int i=0;i<fonts.length;i++){ labels[i].paint(bg); } g.drawImage(offscreen.getBuffer(),0,0,this); }//paint}//Label2DTest下节将看一个复杂一点的例子,它使用Label2D对象来绘制自定义的Button2D类。12.2.3 Button2D类Button2D类的功能就像其他按钮所实现的功能那样——单击它会产生一种动作。Button2D类还提供多达3帧的动画:一帧针对平常状态,一帧针对鼠标停留在鼠标上面的状态,一帧针对按钮实际按下的状态。按钮是使用斜角来表现按钮的状态的。任何带有斜角工具的绘图程序都可以轻易地产生这种效果。即使不用带有斜角工具的绘图程序,要创建这样的斜角也是很容易的。边上的几条亮线和暗线,以及角上的凹口,可以在很短的时间内呈现一个合理的斜角。现在看看Button2D类是如何实际工作的。在这一点上,我们吸收了原始的java.awt.event类的优点,给每一个Button2D对象一个ActionListener对象数组,实现ActionListener接口的外部对象,比如常用的Applet类,可以通过调用addActionListener方法并把自己(或者任何其他事件监听器)作为参数传入的方式来对按钮的事件注册它们自己。Button2D类还实现了MouseListener和MouseMotionListener接口,这样它们可以捕获像单击和一般运动这样的鼠标事件。Button2D对象还需要通过一个Component对象来注册鼠标监听,通常,这个Component对象也是一个applet对象,虽然像鼠标移动这样的事件可以改变按钮的内部状态。在Button2D上面按下,然后松开一个鼠标键会产生一个事件,然后这个事件会被传给所有被包含在按钮的actionListener数组中的对象。第6章曾经介绍过java.awt.event包中所包含的类和接口,比如ActionListener接口等。所以,如果需要复习一下或者需要更多帮助,可以回头看看第6章。下面将从Button2D类的代码开始,然后看一个例子。如同往常一样,这里会指出其中比较麻烦的地方。import java.awt.*;import java.awt.event.*;import java.awt.geom.*;import java.util.*;public class Button2D extends Component2D implements MouseListener, MouseMotionListener{ // protected Label2D label; //接收动作事件的监听器的数组 protected ActionListener[] actionListeners; protected final int MAX_LISTENERS=5; //按钮的3种可用状态:普通,鼠标在上和鼠标按下 public static final int BUTTON_NORMAL=0; public static final int BUTTON_OVER=1; public static final int BUTTON_DOWN=2; // public Button2D(Label2D lbl,ImageGroup grp,Vector2D p){ super(); label=lbl; group=grp; setPos(p); updateBounds(); update(); //为接收动作事件的监听器创建一个空的数组 actionListeners=new ActionListener[MAX_LISTENERS]; for(int i=0;i<MAX_LISTENERS;i++){ actionListeners[i]=null; } } //给定图像组和位置创建一个新的Button2D对象 public Button2D(ImageGroup grp,Vector2D p){ this(null,grp,p); } // public Button2D(Label2D lbl,ImageGroup grp){ this(lbl,grp,Vector2D.ZERO_VECTOR); } //尝试把传入的监听器添加到ActionListener对象数组中 //这个方法会在下列情况下失败: //(1)l为空 //(2)数组中已经包含了MAX_LISTENERS个元素 public void addActionListener(ActionListener l){ if(l==null){ return ; } for(int i=0;i<MAX_LISTENERS;i++){ if(actionListeners[i]==null){ actionListeners[i]=l; return; } } } // public void centerLabel(Graphics2D g2d){ if(label!=null){ label.centerOn(this.getBounds(),g2d); } } // public void setEnabled(boolean e){ super.setEnabled(e); if(!isEnabled()){ state=BUTTON_NORMAL; } if(label!=null){ label.setEnabled(e); } } public void mouseClicked(MouseEvent e){ } public void mouseEntered(MouseEvent e){ } public void mouseExited(MouseEvent e){ } //如果按钮可用并且单击事件在按钮上面发生,就把按钮的状态设置为"按下"状态 public void mousePressed(MouseEvent e){ if(isEnabled()){ if(bounds.contains(e.getPoint())){ state=BUTTON_DOWN; } } } //通过通知所有的动作监听器发生了单击事件的方式来模拟按钮的单击 //如果鼠标不是在按钮上面松开,那么按钮返回一般状态,没有动作发生 public void mouseReleased(MouseEvent e){ //只处理可用的按钮 if(isEnabled()){ //只有在鼠标仍然在按钮上面的情况下才触发事件 if(bounds.contains(e.getPoint())){ if(state==BUTTON_DOWN){ ActionEvent thisEvent=new ActionEvent(this, ActionEvent.ACTION_PERFORMED,""); //让所有的监听器知道有些事情被传下去了 for(int i=0;i<MAX_LISTENERS;i++){ if(actionListeners[i]!=null){ actionListeners[i].actionPerformed(thisEvent); } } } //恢复到鼠标在上状态 state=BUTTON_OVER; }else{//如果鼠标没有在按钮上面松开则将按钮的状态返回为一般状态 state=BUTTON_NORMAL; } } } public void mouseDragged(MouseEvent e){ } //根据当前的状态和鼠标的位置更新按钮的状态 public void mouseMoved(MouseEvent e){ if(isEnabled()){ //如果鼠标已经按下则不要做任何事情 if(state==BUTTON_DOWN)return; //切换到鼠标在上状态 if(bounds.contains(e.getPoint())){ state=BUTTON_OVER; }else{//如果鼠标指针刚离开按钮区域则将按钮状态恢复为一般状态 state=BUTTON_NORMAL; } } } //根据按钮的当前状态来绘制它 public void paint(Graphics2D g2d){ //只绘制可视组件 if(isVisible()){ //如果有图像,先绘制图像 if(group!=null){ g2d.drawImage(((ButtonImageGroup)group).getFrame(state), xform,AnimationStrip.observer); } // if(label!=null){ // if(state==BUTTON_DOWN){ label.paint(g2d,2,2); }else{//否则只是正常 label.paint(g2d); } } } } //根据当前的状态和传入的x,y增量来 public void paint(Graphics2D g2d,double dx,double dy){ //只绘制可见组件 if(isVisible()){ //如果有图像,先绘制图像 if(group!=null){ g2d.drawImage(((ButtonImageGroup)group).getFrame(state), AffineTransform.getTranslateInstance(pos.getX()+ dx,pos.getY()+dy), AnimationStrip.observer); } // if(label!=null){ // if(state==BUTTON_DOWN){ label.paint(g2d,dx+2,dx+2); }else{//否则只是正常 label.paint(g2d,dx,dx); } } } } //返回一个对这个按钮的描述字符串 public String toString(){ // if(label!=null){ return super.toString()+" "+label.toString(); } //否则只返回父类的toString return super.toString(); }}//Button2D至此我们应该可以描绘出Button2D类是怎样通过观察不同的鼠标事件方法而跟踪它的内部状态的。注意,如果在按钮上按下鼠标键并保持,离开按钮区域,然后在松开鼠标按键前回到按钮区域,事件仍然会触发。还要注意,通过彼此独立的mousePressed和mouseReleased方法模仿单击,可以从本质上更简单地更改按钮的状态。如果只在mouseClicked方法内部定义鼠标行为,这种效果是不可能达到的。回顾一下,Component2D对象包含了它们自己的ImageGroup来储存图像帧。下面通过创建ButtonImageGroup类来扩展ImageGroup类,这个ButtonImagGroup类包含专门的属性来储存文件名和所包含图像文件的数量。所以我们可以简单地使用这个类并把这些参数传给它,而无须把ImageGroup扩展为几个单独的类,每一次创建一个不同的按钮类型。当ButtonImageGroup类的init被调用时,所需要做的只是加载文件并把它分割为等大的块。注意,我们的按钮图像完全在一行的单元格中,还要注意,其中没有间隔物也没有网格线。按钮图像应该按照一个特定的顺序添加,正常状态第一,然后是鼠标在上面的状态,最后是鼠标被按下的状态。虽然可以加入几帧图像,但还是3张图像会产生最好的视觉效果。虽然ButtonImageGroup的代码对按钮的图像作了这些约束,但是从长远看,这使得事情变得简单。只需要简单修改几行代码,就可以把这个类改为任何想要的样子。简要审查一下ButtonImageGroup类的代码,想想它是怎样扩展ImageGroup类的。看完后,试着写一个测试程序把这些内容贯穿起来。import java.awt.*;public class ButtonImageGroup extends ImageGroup{ //图像的文件名和它所包含的帧数 protected String filename; protected int numFrames; public ButtonImageGroup(int n,String str){ super(); numFrames=n; filename=str; animations=new AnimationStrip[1]; } //加载动画条并把它分为numFrames帧 public void init(java.applet.Applet a){ ImageLoader loader; loader=new ImageLoader(a,filename,true); //得到每一帧的宽和高。注意它假设所有的图像都处于 //1*numFrames的网格中,没有任何填充单元和分隔线 frameWidth=loader.getImageWidth()/numFrames; frameHeight=loader.getImageHeight(); animations[0]=new AnimationStrip(); for(int i=0;i<numFrames;i++){ animations[0].addFrame(loader.extractCell((i*frameWidth),0, frameWidth,frameHeight)); } animations[0].setAnimator(new Animator.Looped()); } public Image getFrame(int frame){ animations[0].reset(); for(int i=0;i<frame;i++){ animations[0].animate(); } return animations[0].getCurrFrame(); }}//ButtonImageGroup下面通过一些小的演示程序把这些内容都贯穿起来,这个小程序通过几个自定义按钮和用户进行交互。下面的Button2DTest applet,在窗体上绘制了5个按钮,在每一次一个按钮被按下时向控制台打印一条简单的消息。import java.applet.*;import java.awt.*;import java.awt.event.*;public class Button2DTest extends Applet implements Runnable,ActionListener{ //动画线程 private Thread animation; private BufferedGraphics offscreen; //要绘制的Button2D对象数组 private Button2D[] buttons; //用来绘制的字体名数组 private final String[] fonts={"Helvetica","Arial", "Courier","Terminal","Georgia"}; private final int NUM_BUTTONS=fonts.length; public void init(){ //创建ButtonImageGroup供按钮使用 ButtonImageGroup group=new ButtonImageGroup(3,"buttons.gif"); group.init(this); //创建实际的按钮 buttons=new Button2D[NUM_BUTTONS]; for(int i=0;i<NUM_BUTTONS;i++){ // Label2D label=new Label2D(new Font(fonts[i],Font.PLAIN,18) ,fonts[i],Color.black); // buttons[i]=new Button2D(label,group, new Vector2D.Double(50,10+(i*55))); label.centerOn(buttons[i].getBounds(),(Graphics2D)getGraphics()); //注册按钮来接收鼠标事件 addMouseListener(buttons[i]); addMouseMotionListener(buttons[i]); //注册applet一监听按钮的动作事件 buttons[i].addActionListener(this); } offscreen=new BufferedGraphics(this); AnimationStrip.observer=this; }//init public void start(){ //启动动画线程 animation=new Thread(this); animation.start(); } public void stop(){ animation=null; } public void run(){ Thread t=Thread.currentThread(); while(t==animation){ repaint(); try{ Thread.sleep(10); }catch(InterruptedException e){ break; } } }//run public void update(Graphics g){ paint(g); } public void paint(Graphics g){ Graphics2D bg=(Graphics2D)offscreen.getValidGraphics(); bg.setPaint(Color.blue.darker()); bg.fillRect(0,0,getSize().width,getSize().height); //绘制按钮 for(int i=0;i<NUM_BUTTONS;i++){ buttons[i].paint(bg); } g.drawImage(offscreen.getBuffer(),0,0,this); }//paint //如果有动作从Button2D对象发出,只是把事件打印到控制台 public void actionPerformed(ActionEvent e){ System.out.println(e); }}Button2D产生的按钮看起来和原始AWT给我们的东西一样完美(甚至还要好)。然而,这是Component2D类体系发挥作用的结果。首先,Button2D对象比标准Button按钮用来启动和运行的代码稍微多一些,其次,必须绘制按钮才能让它们显示。虽然这不是很麻烦,但必须在头部包含它。但是虽然让Button2D对象运行起来需要做一些工作,不过这些工作不是太多。总体上说,那么一点额外的开销是很值得的。另外还要注意actionPerformed方法是如何实现的。它只是简单地把ActionEvent传回给了控制台。所以,如果按下标有Arial的按钮,下面的信息应该会打印到控制台:java.awt.event.ActionEvent[ACTION_PERFORMED,cmd=,when=0,modifiers=] on Button2D: Vector2D$Double [x=50.0,y=10.0] Label2D: Vector2D$Double [x=62.09375,y=40.5546875] Helvetica这里提这个只是因为控制台可能最小化了,或者它被其他的程序遮住了。下节将重新回顾并编写一个陈旧但很好的组件,CheckBox组件。

您好: 当您在阅读和使用我所提供的各种内容的时候,我非常感谢,您的阅读已是对我最大的支持。 我更希望您能给予我更多的支持。 1.希望您帮助我宣传我的博客,让更多的人知道它,从中获益(别忘记了提醒他们帮我点点广告,嘿嘿)。 2.希望您能多提出宝贵意见,包括我所提供的内容中的错误,建设性的意见,更希望获得哪些方面的帮助,您的经验之谈等等。 3.更希望能得到您经济上的支持。 我博客上面的内容均属于个人的经验,所有的内容均为开源内容,允许您用于任何非商业用途,并不以付费为前提,如果您觉得在阅读和使用我所提供的各种内容的过程中,您得到了帮助,并能在经济上给予我支持,我将感激不尽。

您可以通过点击我网站上的广告表示对我的支持。

您也可以通过银行转帐付款给我: 招商银行一卡通: 卡号:6225888712586894 姓名:牟勇 您也可以通过汇款的方式: 通讯地址:云南省昆明市女子(28)中学人民中路如意巷1号 收信人:陈谦转牟勇收 邮编:650021 无论您给予我怎么样的支持,我都衷心的再次感谢。 欢迎光临我的博客,欢迎宣传我的博客 http://blog.csdn.net/mouyong http://blog.sina.com.cn/mouyong EMail:mouyong@yeah.net QQ:11167603 MSN:mouyong1973@hotmail.com

成功是奋斗的结果,而奋斗是成功的必经之路。

Java2游戏编程读书笔记(12

相关文章:

你感兴趣的文章:

标签云: