一个容器可以容纳另外一个组件——甚至是另一个容器。
容器分为两个大类:一类是可以自由移动的(Window),另一类保持在一个固定的位置(Panel)。
Component
|
Container
Window————+————Panel
Dialog—+—Frame |
| Applet
FileDialog
可以把几个Panel或者Container对象附着在一个父Container对象上,每一个附着在父Container上的对象可以拥有它自己的布局管理器,使用这个模型,可以在applet上创建一些更精致的布局。
下面的程序PanelTest,演示了一个Panel布局。其中,创建了两个Panel对象,每一个都有一个不同的布局方式。在每一个Panel上都添加了几个Button对象,然后这两个Panel被放到了applet中,这个applet使用默认的FlowLayout布局管理器来显示这两个Panel。
import java.awt.*;
import java.applet.*;
public class PanelTest extends Applet{
public void init(){
//设置applet的布局为默认的FlowLayout
this.setLayout(new FlowLayout());
//创建一个2×2的GridLayout的Panel,并添加4个按钮
//然后把这个Panel放到applet上
Panel p1=new Panel();
p1.setLayout(new GridLayout(2,2));
p1.add(new Button("B1"));
p1.add(new Button("B2"));
p1.add(new Button("B3"));
p1.add(new Button("B4"));
add(p1);
//创建第二个Panel,布局为BorderLayout,并添加5个按钮
//然后把这个Panel放到applet上
Panel p2=new Panel();
p2.setLayout(new BorderLayout());
p2.add(new Button("North"),BorderLayout.NORTH);
p2.add(new Button("South"),BorderLayout.SOUTH);
p2.add(new Button("East"),BorderLayout.EAST);
p2.add(new Button("West"),BorderLayout.WEST);
p2.add(new Button("Center"));
add(p2);
}
}
由于上面的程序没有保存每一个按钮的引用,所以不能捕获按钮产生的事件。当然一个实际的applet会实现ActionListener并且把按钮的引用保存为private的成员,上面的例子只是一个简单的示例。上面程序的运行结果嵌在一个150×125的窗体中。
假设正在开发一个用户可以定制其角色的角色扮演游戏。角色扮演游戏的角色开发中的一个重要方面是技能设置,给定一个初始数目的“技能点”,用户可以把点设置到不同的技能中去。一个属性得到的技能点越多,该角色的这种技能就越强。
下面的AttributeTest applet允许用户把10个技能点分配到4种技能中:力量,智慧,敏捷和魔法。这个applet的代码引入了两个类:AttributeButton和AttributePanel,它们通过继承基本AWT组件来增强程序的功能。AttributeButton继承Button类,添加了一种把它自己和AttributePanel联系起来并更新自己的内容的方法;AttributePanel类包含一个属性的描述以及分配给这个属性的技能点,它还包含两个分配属性点数的AttributeButton对象。用户可以反复修改各个属性所分配的点数直到满意为止。
import java.applet.*;
import java.awt.*;
import java.awt.event.*;
//允许改变属性
class AttributeButton extends Button{
//放这个按钮的Panel
private AttributePanel parent;
public AttributeButton(String label,AttributePanel ap){
super(label);
parent=ap;
}
//更新父控件的属性
public int updatePanel(int pointsRemaining){
//点+号为plus按钮分配一个点值
if(this.getLabel().equals("+")){
//仅在还有点剩余时分配
if(pointsRemaining>0){
parent.allocatePoints(1);
return -1;
}
else{
return 0;
}
}else{//否则扣除一个点
//当点数大于零的时候才进行扣点
if(parent.getPointsAllocated()>0){
parent.allocatePoints(-1);
return 1;
}else{
return 0;
}
}
}
}
//允许角色更改一个属性
class AttributePanel extends Panel{
//属性的文本描述
private String attribute;
//显示给这个属性所分配点值的Label
private Label pointsAllocated;
public AttributePanel(String attr,ActionListener l){
attribute=attr;
pointsAllocated=new Label("0",Label.CENTER);
//设置Panel的布局为3×1的网格
setLayout(new GridLayout(3,1));
setBackground(Color.green);
//添加描述属性的标签
add(new Label(attr,Label.CENTER));
add(pointsAllocated);
//把+/-按钮添加到父ActionListener上
Button incr=new AttributeButton("+",this);
incr.addActionListener(l);
Button decr=new AttributeButton("-",this);
decr.addActionListener(l);
//添加另一个有加,减按钮的Panel
Panel p=new Panel();
p.add(incr);
p.add(decr);
add(p);
}
//更新pointsAllocated label
public void allocatePoints(int n){
int value=getPointsAllocated()+n;
pointsAllocated.setText(""+value);
}
//返回给这个属性分配的点值
public int getPointsAllocated(){
return Integer.parseInt(pointsAllocated.getText());
}
public String toString(){
//返回这个属性的详细描述
return attribute+":"+getPointsAllocated();
}
}
public class AttributeTest extends Applet implements ActionListener{
//所有剩余的点值
Label pointsRemaining;
//这个applet的属性
private final String ATTRS[]={"力量","智慧","敏捷","魔法"};
public void init(){
pointsRemaining=new Label("点数剩余:10",Label.CENTER);
this.setLayout(new FlowLayout(FlowLayout.CENTER,5,10));
//添加组件
for(int i=0;i<ATTRS.length;i++){
add(new AttributePanel(ATTRS[i],this));
}
add(pointsRemaining);
}
public void actionPerformed(ActionEvent e){
//得到剩余的点值
int n=Integer.parseInt(pointsRemaining.getText().substring(5));
//更新按钮Panel的标签和主标签
n+=((AttributeButton)e.getSource()).updatePanel(n);
pointsRemaining.setText("点数剩余:"+n);
}
}
前面的例子演示了怎么使用基本的applet组件来为一个角色扮演游戏创建一个角色轮廓,虽然它对于开始学习applet而言已经很不错了,但是还可又做些工作来进一步改善它,比如:
q 添加更多的属性面板。如果允许用户输入名字,选择职业和性别,并且能够回顾所选属性的列表,这样就更好了。
q 增加复杂度。如果想有多个属性菜单,可能让它们从单一的基类继承会好一些,这个基类必须包含所有属性菜单所共有的方法。这种方式可又让代码更加灵活,这样使得扩展和维护applet变得更加容易。
CharacterBuiler applet的思路是创建一个让一系列按钮来回切换的CardLayout。布局中的每一个卡片由含有一个单一角色属性的面板组成,用户可又访问每一个卡片来选择自己的爱好,比如姓名,职业和性别;用户还可又分配给定的有限技能点,就像在前面的AttributeTest applet中所展示的一样。
//AttributePanel.java
import java.awt.*;
public abstract class AttributePanel extends Panel{
//属性的文本描述
protected String attribute;
public AttributePanel(String attr){
attribute=attr;
}
public final String getAttribute(){
return attribute;
}
//强制子类覆盖toString方法
public abstract String toString();
}
抽象类AttributePanel提供了两个主要用途,首先,它允许几个属性和所有含有角色属性选项的面板相关联,每一个AttributePanel都定义了一个String对象来描述这个面板,并且定义了一个String对象来描述属性本身;另一个用途是可以定义一个AttributePanel对象而无须提前知道它们最后运行时的类型。
由于AttributePanel类被声明为抽象类,不能直接实例化AttributePanel,但是这没什么问题。它被设计为功能不完善的,目的是可以根据特定的角色属性需要而被扩充。
下面的TextFieldPanel创建了允许用户输入其姓名的一个文本域(TextField类)和标签(Label类)
//TextFieldPanel.java
import java.awt.*;
//在Panel内放置String属性
public class TextFieldPanel extends AttributePanel{
//放置属性的TextField
private TextField textField;
public TextFieldPanel(String attr,String prompt,int textLength){
super(attr);
this.setLayout(new FlowLayout(FlowLayout.CENTER,15,0));
//如果提示是一个有效字符串,则添加一Label
if(prompt!=null){
add(new Label(prompt,Label.LEFT));
}
//创建TextField,并把它添加到Panel上
textField=new TextField(textLength);
add(textField);
}
public String toString(){
//返回属性,一个"不确定"的消息
if(textField.getText().trim().equals("")){
return attribute+":未定义";
}
return attribute+":"+textField.getText().trim();
}
}
CheckboxPanel类允许用户在几个选项中作出唯一选择,比如选择性别和职业。下面是它的代码
//CheckboxPanel.java
import java.awt.*;
public class CheckboxPanel extends AttributePanel{
//容纳Checkbox的CheckboxGroup
protected CheckboxGroup cbg;
//重写Applet类的init方法
public CheckboxPanel(String attr,String[]items,String selectedItem){
super(attr);
this.setLayout(new GridLayout(items.length+1,1,5,5));
add(new Label(attribute,Label.CENTER));
//创建CheckboxGroup
cbg=new CheckboxGroup();
for(int i=0;i<items.length;i++){
add(new Checkbox(items[i],cbg,items[i].equals(selectedItem)));
}
}
public String toString(){
return attribute+":"+cbg.getSelectedCheckbox().getLabel();
}
}
最后是属性选项面板的清单。SkillPanel类允许用户对不同的技能分配技能点。它实现的功能和在AttributeTest applet中实现的很类似:
//SkillPanel.java
import java.awt.*;
import java.awt.event.*;
//提供了一个可以修改技能值的按钮
class SkillButton extends Button{
//显示给技能分配点的Label
private Label pointsAllocated;
public SkillButton(String desc,Label label){
super(desc);
pointsAllocated=label;
}
public int getPointsAllocated(){
return Integer.parseInt(pointsAllocated.getText());
}
private void allocatePoints(int n){
int value=getPointsAllocated()+n;
pointsAllocated.setText(""+value);
}
//更新父属性的值
public int update(int pointsRemaining){
//如果是"+"则分配点数
if(getLabel().equals("+")){
//只在有点剩余时才分配
if(pointsRemaining>0){
allocatePoints(1);
return -1;
}
}else{
if(getPointsAllocated()>0){
allocatePoints(-1);
return 1;
}
}
//分配/回收失败
return 0;
}
}
//容纳不同角色的技能值
public class SkillPanel extends AttributePanel implements ActionListener{
//每一项技能所分配的点数
Label[] pointsAllocated;
//剩余可分配的总点数
Label pointsRemaining;
//这个applet的属性
private String[] skills;
public SkillPanel(String attr,String[] sk,int alloc){
super(attr);
skills=sk;
//创建pointsRemaining标签
pointsRemaining=new Label("点数剩余:"+alloc,Label.CENTER);
//把applet的layout设为FlowLayout
setLayout(new FlowLayout(FlowLayout.CENTER,5,10));
//添加组件
pointsAllocated=new Label[skills.length];
for(int i=0;i<skills.length;i++){
pointsAllocated[i]=new Label("0",Label.CENTER);
addSkill(skills[i],pointsAllocated[i]);
}
add(pointsRemaining);
}
private void addSkill(String skill,Label label){
Panel p=new Panel();
//设置面板布局为3×1网格
p.setLayout(new GridLayout(3,1));
p.setBackground(Color.green.darker());
//添加一个描述属性的标签
p.add(new Label(skill,Label.CENTER));
p.add(label);
//把+/-按钮添加到父ActionListener
Button incr=new SkillButton("+",label);
incr.addActionListener(this);
Button decr=new SkillButton("-",label);
decr.addActionListener(this);
//添加另一个有加,减按钮的Panel
Panel buttonPanel=new Panel();
buttonPanel.add(incr);
buttonPanel.add(decr);
p.add(buttonPanel);
add(p);
}
public String toString(){
//返回一个包含每一种技能分配情况的String
String s="";
int points=0;
for(int i=0;i<skills.length;i++){
points=Integer.parseInt(pointsAllocated[i].getText());
s=s+skills[i]+"("+points+") ";
}
return s;
}
public void actionPerformed(ActionEvent e){
//得到可分配的点数
int n=Integer.parseInt(pointsRemaining.getText().substring(5));
//更新按钮面板的主标签
n+=((SkillButton)e.getSource()).update(n);
pointsRemaining.setText("点数剩余:"+n);
}
}
前面还提过一个显示用户输入概要的面板,它对主applet的每一个AttributePanel对象都有一个引用,允许它们的toString方法定义所要显示的概要文本。
//Summarypanel.java
import java.awt.*;
//包含属性概述的Panel
public class SummaryPanel extends Panel{
//描述每一个属性的Label
private Label[] summaries;
//AttributePanel数组
private AttributePanel[] panels;
public SummaryPanel(AttributePanel[] ap){
super();
panels=ap;
setLayout(new GridLayout(panels.length+1,1,5,5));
add(new Label("描述:"));
//把Label添加到Panel
summaries=new Label[panels.length];
for(int i=0;i<panels.length;i++){
summaries[i]=new Label("",Label.LEFT);
add(summaries[i]);
}
}
/**
*由于不知道到底是哪一个panel被更新,所以让每一个
*AttributePanel更新它的标签
*/
public void update(){
for(int i=0;i<panels.length;i++){
summaries[i].setText(panels[i].toString());
}
}
}
现在可以把这些类组装到一起。下面的CharaterBuilder applet在一个CardLayout中定义了4个属性选择面板和一个概要面板,单击back和next按钮可以切换。
import java.applet.*;
import java.awt.*;
import java.awt.event.*;
//CharacterBuilder类包含几个放在一个CardLayout中的Panel,还有back和next按钮
public class CharacterBuilder extends Applet implements ActionListener{
//在cardPanel中选择下一个和上一个按钮
private Button back;
private Button next;
private Panel attributePanel;
private SummaryPanel summaryPanel;
//描述不同属性选项的常数String
private final String[] GENDERS={"男","女"};
private final String[] SKILLS={"力量","智慧","敏捷","魔法"};
private final String[] PROFESSIONS={"武士","吟游诗人","弓箭手","术士","铁匠","德鲁依"};
public void init(){
//创建一个GridLayout来容纳卡片和按钮
setLayout(new GridLayout(2,1));
//得到给角色分配的技能点数
int skillPoints;
try{
skillPoints=Integer.parseInt(getParameter("SkillPoints"));
}catch(NumberFormatException e){
skillPoints=10;
}
//创建一组属性面板,其中一个是角色的姓名
//性别,技能和职业
AttributePanel[] panels=new AttributePanel[]{
new TextFieldPanel("姓名","键入您的姓名",20),
new CheckboxPanel("性别",GENDERS,GENDERS[0]),
new SkillPanel("技能",SKILLS,skillPoints),
new CheckboxPanel("职业",PROFESSIONS,PROFESSIONS[0]),
};
//创建一个Panel来放置CardLayout
attributePanel=new Panel();
attributePanel.setLayout(new CardLayout());
//把AttributePanels添加到主面板
for(int i=0;i<panels.length;i++){
attributePanel.add(panels[i],panels[i].getAttribute());
}
//创建SummaryPanel,并把它加到CardLayout中
summaryPanel=new SummaryPanel(panels);
attributePanel.add(summaryPanel,"描述");
//添加attributePanel
add(attributePanel);
//创建并添加back和next按钮
Panel p=new Panel();
back=new Button("上一页");
back.addActionListener(this);
p.add(back);
next=new Button("下一页");
next.addActionListener(this);
p.add(next);
p.setBackground(Color.BLACK);
add(p);
}
//在单击back或者next按钮时调用
public void actionPerformed(ActionEvent e){
CardLayout cardLayout=(CardLayout)attributePanel.getLayout();
if(e.getSource()==back){
cardLayout.previous(attributePanel);
}else if(e.getSource()==next){
cardLayout.next(attributePanel);
}
//在每一次变更后更新概要
summaryPanel.update();
}
}
注意CharaterBuilder类的init方法中getParameter方法的使用。可以把参数传送给applet,就像可以把参数传给控制台程序一样。当希望不经过重新编译而给applet传送信息时,这是很有用的。上面的代码清单读入一个参数,该参数表示可分配的技能点。下面的代码演示了怎样在.html文档中包含参数:
<applet height="300" width="300" code="CharacterBuilder">
<param name="SkillPoints" value="30"/>
</applet>
如果要在applet中读入SkillPoints参数,只需像这样:
skillPoints=Integer.parseInt(getParameter("SkillPoints"));
由于点值是一个整数,这里把上述代码嵌入到一个try/catch中,只是为了处理一些聪明的玩家在网页中输入非法数据的特殊情况。applet参数是对代码作出快速改变且无须浪费时间重新编译的好方式。当希望最终用户可以快速而简单地定义参数时,使用applet参数也是很方便的。
记住,Java applet只是嵌入到另一个应用程序中的一段程序。applet是一种把游戏发布给用户的非常方便而且非常流行的方法。
本章没有讲解所有的AWT组件,主要是介绍组件和容器,在第6章中,我们将继续学习像Graphics等在本章被一带而过的主题。使用AWT组件是一种快捷地向用户表述交互内容的方法,像按钮,单选按钮和标签这样的组件可以用几行代码放到applet上。然而,AWT有几个限制,虽然它对设计独立于窗体方案的软件不错,本书还是建议对组件的放置做更好的控制。读者还应该结合对游戏的“感觉”来创造更且有吸引力的组件,而不是使用默认的窗体和绘制方式,但是不要完全低估原始的AWT,它们的柔性和使用上的便捷在后面是很灵便的。
我们已经对Applet类的作用有了初步的认识,只要几行代码,就可以把按钮,文本域和标签嵌入到applet窗体上,但是,这并没有结束。在第7章,我们将学习在applet中绘制线条,图形和文本。
5.1预计下列代码段的输出:
public B extends Applet{
public static void main(String[] args){
System.out.println(“I am a Java guru!”);
}
}
5.2在使用像Button这样的组件时,为什么实现EventListener接口对Applet类来说很重要?如果不是很清楚,可以查看Button类addActionListener方法的原型。
5.3请描述组件和容器之间的异同。
5.4修改ButtonTest applet,使得它对color数组中的每一种颜色都有一个单独的按钮,和红色关联的按钮应该以Red为标签,都以currentColor作为索引。一个更加健壮的方法是施展Button类,在它内部变换颜色。可以在自定义Button的构造函数中给它关联一个颜色并让actionPerformed方法允许Button刷新窗体。
5.5修改AudiochoiceTest applet,让它实现一个List对象来容纳声音文件的文件名。应该让List具备在任何时候多选的能力,所以应该仔细修改相应的声音控制代码。
5.6描述怎样用多个Panel对象来创建更精致的布局。
5.7什么是卸载不当引起的速度消耗?
5.8简要描述5个按钮在给定如下几种不同的布局管理器下如何排列:FlowLayout,GridLayout,CardLayout和BoderLayout。
5.9写一个applet用Label对象显示当前日期和时间。这个applet应该有规律地刷新自己,以便日期和时间可以看起来像是连续的。
5.10修改CharacterBuilder类,让它能读一个描述默认角色名字的applet参数。可能需要修改TextFieldPanel的构造函数或者添加一个方法来设置这个文本域的值,还需要在.html文件中添加如下的代码来添加一个参数标签:
<param name=”DefaultName” value=”Merlin”>