在我们备考英语考试时,时常会用到市面上的各种背单词软件,它们大都包括许多的词库供用户选择,支持在界面上显示单词并对用户进行交互考查。接下来我们就尝试自己开发一款背单词软件,一步一步进行更新迭代。
V1版本:支持在界面上操作,采用挖去英语单词中的某个字母,让用户填出缺失字母的考查方式。
步骤
一、搭建初始界面
开始使用时,我们应该弹出欢迎的界面,包括一些必要的文字说明,并给出词库的选择按钮。所以我们创建一个UI类,写一个面板方法(showUI1),创建一个窗体并设置好大小,在创建一个Panle面板,添加到窗体上去(尽量避免直接将组件添加到窗体上,否则比较难整体操作)。在本程序中,我们将panel设置为空布局(null),所有的组件位置和大小都由自己定义。
接着,我们就往创建好的面板上添加一个标签JLabel,写上欢迎语:"欢迎来到词海帆!请开始你的单词之旅吧!"然后再添加"请选择词库"的提示标签,接着就可以添加词库对应的按钮了。这里以CET-4和CET-6为例,添加到面板上。这样,初始的界面就搭建完毕了。
二、导入词库
我们可以在网上获取对应的单词表,然后以一种特定的格式保存为一个file文件,放到我们的项目根目录里面。这里特定的格式指的是用一个特定的符号分割英文和中文释义,便于后面讲这两部分分离,本程序以用#分开为例。例:abandon#v.抛弃。
三、实现监听器
我们的按钮需要实现动作监听器,所以写一个Listener类,实现ActionListener接口,然后绑定到我们添加的按钮上即可。
四、读取词库
接着,我们就需要从词库中读取出所有的内容,并将中文和英文分别保存到两个数组中。由于后面我们需要多次实现这个功能,所以应该把他包装成一个create方法。
这个方法不需要返回值,需要传入一个参数wordlib,表示词库的绝对路径。接着,创建一个File类的对象file,传入这个wordlib。接着创建FileReader类的对象fr,传入刚创建的file,表示文件读取数据。最后创建BufferedReader类的对象,传入fr,表示缓冲读取数据流。
缓冲读取完后,创建while一个循环,不断调用fr中的方法readLine方法,读取整行的文本,并设置一个String变量保存读取的内容,当这个变量为空时就退出循环。接着,我们调用变量中的split方法,以我们设置的#为分割分成两部分,再设置一个数组分别保存这两部分,再分别将这两个部分添加到英文和中文对应的动态数组中。经过循环,我们就完成了对词库的读取。
五、搭建答题界面
我们在UI类里面创建showUI2方法用于创建答题界面,参数为String类型,代表题目。
这个界面我们需要的内容主要包括:题目要求语、题目、作答框、提交按钮、下一题按钮。
那么像上面搭建界面一样,添加对应的JLabel、JButton、JTextField,并设置好位置、字体、大小即可。题目使用JLabel来呈现,并要给按钮添加动作监听器。
六、设置题目
由于该功能使用频繁,并且相对独立,我们就将其包装成一个方法giveQuiz,设置其返回值为String,返回测试题目。
首先,我们利用随机数类Random,调用其nextInt方法生成一个0~单词个数的数字代表数组的下标,这样就可以随机取出一个单词作为测试单词。接着,我们还需要找该单词的某个位置挖去,所以生成一个0~单词长度的随机数代表挖掉的字母,然后用charAt方法取出这个字母,保存到一个char类型的全局变量中(便于后面的核验答案)。最后,定义一个String类型变量,用substring方法取出未挖掉的字母,将缺失部分用_拼接,作为返回值返回即可。
接着,我们在上面读取词库的create方法里面调用这个方法,得到题目后调用showUI2方法,即可在答题界面上显示题目。
七、提交并核验答案
我们需要添加一个名为"提交"的JButton到面板上,添加动作监听器。
实现的逻辑就是当点击这个按钮时,我们用一个String变量,调用文本框JTextField的getText方法获得输入的内容并保存到变量中,然后将其转换为一个char类型的变量(方法后面会提到),再与之前设置好的全局变量(也就是正确答案)比较,如果一样,我们就可以弹出一个对话框显示"回答正确",并附上对应的中文和英文;否则,我们在弹出的框中写"回答错误",用户继续答题即可。
八、界面参数的传递和刷新
我们每完成一次答题,用户点击下一个时都要刷新一下界面以呈现出新的题目,这里就涉及到刷新的逻辑。
我们需要将showUI2里面创建的、并且需要在刷新中更改的组件传递一份给Listener类,方便我们的重置。分析可知,我们需要修改的组件有:显示单词的JLabel、输入答案的文本框(刷新时需要重置为空)。所以,我们在Listener类里面写两个方法:setWordsJla、setChJtf,在UI2里创建对象的时候调用这些方法,并将对应的组件通过参数传递,赋值保存在Listener类的全局变量里,这样,在重置的时候我们就可以直接调用和修改这些组件的信息。
接着,当用户点击"下一题"的按钮时,在动作监听器方法里我们就应该调用创建测试题的giveQuiz方法,然后调用标签中的setText方法,再将测试题传进去完成重置。然后,调用文本框的方法setText,设置为" "。这样,我们就完成了界面的刷新。
补充知识
1、空布局的用法(null)
在本程序中,由于我们面板上的组件比较多,而且希望这些组件能按照我们的想法随意放置位置以达到美化界面的效果,所以采用null布局(空布局)。
①设置布局方法:只需要组件(例如JPanel)调用setLayout方法,参数为null即可。
②设置组件的位置和大小:我们可以调用setBounds方法,四个参数分别是:x,y,width,hight,可以设置组件的坐标和宽度、高度。这样,我们就可以随意布置各组件的位置和大小。
2、读取文件内容
在本程序中,我们出的题目是来源于外部文件的内容,这时候我们就需要用到Java的文件读取方法。
①FileReader:这是文件读取数据的类,我们需要创建这个类的对象,参数是一个File类的对象。在本程序中,我们将保存词语文件的类的对象传入即可。但是,这个类只包含读取单个字符的功能,而我们需要的是读取文件整行的功能。
②BufferedReader:这是缓冲读取数据流,包含了读取整行的功能,需要的参数是我们先前创建的FileReader类的对象。
创建好BufferedReader类的对象后,我们不断调用其中的readLine方法,用一个String变量保存返回的一行内容,就可以读取文件的正行数据。在本程序中,我们加一个while循环,就可以实现读取整个单词文件的功能。
3、split方法
String类里面包含着一个split方法,可以根据字符串的一个特定字符将该字符串分成多个部分。在本程序中,我们的词库保存单词的方法是将英文和中文用一个#分开,我们读取文件是读取一行,而中文和英文是需要分开保存使用的,所以就需要用split方法。
使用方法:一个String变量直接调用spilt方法,然后给定一个字符作为参数,用一个String数组保存分出来的多个片段,然后对这个数组操作即可。
4、随机数的生成
随机数是我们常常用到的工具类,这里介绍一种最常见和实用的获得方法。
①创建随机数类Random的对象;
②用对象调用nextInt方法,给定一个参数max作为范围,就可以生成0~max的随机数。
(只以生成int类型为例,其他类型替换next后的内容即可)
5、charAt方法
String类里面包含着一个charAt方法,可以获得该字符串某个位置的字符,参数是下标。
6、刷新界面方法
在本程序中,我们需要在更新题目的时候刷新界面,这时候就涉及到刷新界面的多种操作。
①删除界面上原有的组件
我们需要调用removeAll方法,先移除掉面板上的所有组件,以便绘制新的组件到面板上。这里要特别注意的点是:我们调用所有的刷新界面方法都不要直接操作窗体,因为窗体的构成非常复杂,直接操作窗体会导致各种各样的错误,所以要先将所有组件都加到面板上,再将面板添加到窗体上,操作面板。
②重新调用写好的界面创建方法
清空面板后,我们就调用写好的界面创建方法,将新的组件添加到窗体上。注意:我们需要操作的是同一个面板,不要创建新的面板!(通过引用传递或者函数参数传递赋值的方法实现)
③调用repaint和revalidate方法
repaint和revalidate都是刷新界面的方法,可以让新绘制上去的组件在窗体上显示出来。
综合上面三个步骤,我们就可以完整实现界面的刷新了!
完整代码
UI类
java
/*
V1版本:
1、将V1版本界面化,支持在界面上操作
2、使用的模式是猜出挖空字母
3、支持在文本框中输入答案并提交,核对正误的功能
4、支持手动跳转到下一题的功能
*/
import javax.swing.*;
import java.awt.*;
public class wordUI {
JPanel jp=new JPanel();
wordListener wl=new wordListener();
public void showUI(){
JFrame jf=new JFrame("词海帆");
//将本类通过传递参数的方式传递给Listener类
wl.setWordsUI(this);
//jp使用空布局,所有的组件位置和大小都是自定义的
jp.setLayout(null);
jf.setSize(550,600);
jf.setLocationRelativeTo(null);
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JLabel jln=new JLabel("欢迎来到词海帆!请开始你的单词之旅吧!");
jln.setFont(new Font("华文行楷", Font.PLAIN,24));
jln.setBounds(30,50,500,40);
JLabel jl=new JLabel("请选择词库:");
jl.setFont(new Font("宋体", Font.PLAIN,25));
jl.setBounds(150,140,300,40);
jp.add(jl);
jp.add(jln);
//添加选项按钮
String[] button={"CET-4","CET-6"};
for (int i=0;i< button.length;i++) {
JButton jb = new JButton(button[i]);
//添加监听器
jb.addActionListener(wl);
jb.setBounds(190,200+i*60,100,40);
jp.add(jb);
}
jf.add(jp);
jf.setVisible(true);
}
public void showUI2(String quiz){
JLabel jla=new JLabel("<<请猜出缺失的字母>>");
JLabel jl=new JLabel(quiz);
JTextField jtf=new JTextField(10);
JButton summit=new JButton("提交");
JButton next=new JButton("下一题");
Font font=new Font("微软雅黑",Font.PLAIN,30);
//设置位置和大小
jla.setBounds(100, 10, 400, 50);
jl.setBounds(150,80,300,50);
jtf.setBounds(150,150,100,50);
summit.setBounds(100,250,100,50);
next.setBounds(250,250,150,50);
jp.add(jla);
jp.add(jl);
jp.add(jtf);
jp.add(summit);
jp.add(next);
//设置字体
jla.setFont(font);
jl.setFont(font);
jtf.setFont(font);
summit.setFont(font);
next.setFont(font);
summit.addActionListener(wl);
next.addActionListener(wl);
//将界面需要在更新时改变的组件对象的地址 传一份给监听器中声明的组件变量名,使得我们点击按钮时可以调用这些组件 设置文本 获取文本
wl.setWordsJla(jl);
wl.setChJtf(jtf);
}
public static void main(String[] args) {
wordUI wu=new wordUI();
wu.showUI();
}
}
Listener类
java
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.*;
import java.util.ArrayList;
import java.util.Random;
public class wordListener implements ActionListener {
Random ran = new Random();
wordUI wordUI;
JPanel panel;
JLabel wordJla;
JTextField chJtf;
int wordIndex;
char c;
ArrayList<String> English = new ArrayList<>();
ArrayList<String> Chinese = new ArrayList<>();
//重置界面
public void setWordsUI(wordUI wordUI) {
this.wordUI = wordUI;
this.panel = wordUI.jp;
}
//设置测试单词
public void setWordsJla(JLabel jl) {
wordJla = jl;
}
//设置文本框
public void setChJtf(JTextField jtf) {
chJtf = jtf;
}
@Override
public void actionPerformed(ActionEvent e) {
String name = e.getActionCommand();
if (name.equals("CET-4")) {
create("D:\\IdeaProjects\\背单词软件\\src\\words\\CET4.txt");
} else if (name.equals("CET-6")) {
create("D:\\IdeaProjects\\背单词软件\\src\\words\\CET6.txt");
} else if (name.equals("提交")) { //第二的界面的操作
String customAnswer = chJtf.getText();
char answer=customAnswer.charAt(0);
if (answer==c){
String eng=English.get(wordIndex);
String chn=Chinese.get(wordIndex);
JOptionPane.showMessageDialog(panel,"恭喜你,回答正确!\n"+eng+" "+chn);
}else{
JOptionPane.showMessageDialog(panel,"回答错误!");
}
}else if(name.equals("下一题")){ //主要思想是只改变原面板上的部分信息,不用每次都刷新面板
//重新获取一个新的测试单词
String quiz=giveQuiz();
//用新的测试单词替换面板上原来的标签
wordJla.setText(quiz);
//将文本框里的内容清空
chJtf.setText("");
}
}
public String giveQuiz() {
int size = English.size();
//随机选择一个单词
wordIndex = ran.nextInt(size);
String eng = English.get(wordIndex);
int len = eng.length();
//随机一个位置挖掉字母
int place = ran.nextInt(len);
c = eng.charAt(place);
//得到一个测试题目
String quiz = eng.substring(0, place) + "_" + eng.substring(place + 1);
return quiz;
}
public void create(String wordLib){
File file = new File(wordLib);
try {
FileReader fr = new FileReader(file); // 文件读取数据 每次读只能读一个字符
BufferedReader bf = new BufferedReader(fr); // 缓冲读取数据流 包含了读取一行的功能
while (true) {
String content = bf.readLine();
if (content == null) {
break;
}
//从井号分开,拆成两块放进对应的数组里
String[] sum = content.split("#");
English.add(sum[0]);
Chinese.add(sum[1]);
}
} catch (IOException ex) {
throw new RuntimeException(ex);
}
//先删除原窗体上的所有组件
panel.removeAll();
//开始出题
String quiz = giveQuiz();
//显示出第二窗体
wordUI.showUI2(quiz);
//刷新窗体,显示新的组件
panel.repaint();
panel.revalidate();
}
}