基于GUI的学生成绩管理系统

一、项目背景

同学们经过一个学期的Java学习,顺利的参加了期末考试,并取得了不错的成绩。软件工程系里安排了几位同学统计大家的成绩,由于同学们的宿舍住得比较分散,负责统计分数的同学记录下了每个宿舍同学的班级、姓名和分数并记录在一个文本文件上,每位同学的班级、姓名和分数记录为一行,以单个空格分隔。几位同学统计完成后所有的文件放在了D:\javatest目录下,该文件夹下只有分数统计文件,文件名为class1.txt、class2.txt、…,文件数量不超过9个,具体数量不确定。

其实背景是狗子我要做期末作业了~

二、项目要求

设计并开发一个工程项目,统计各班的平均分,并将班级和对应的平均分依次存入名为test数据库中名为classScore的表中。classScore表包含两个字段,分别为班级名和平均分,一共有4个班级,名称分别为“软件1班”、“软件2班”、“软件3班”、“软件4班”,平均分保留两位小数。

类名描述
UserInterface图形用户界面,用于接收文件存放在目录,以及整个年级的平均分显示。该图形用户界面实现了事件处理,可以响应用户的操作。
DirectoryHandler用于接收用户界面传入的目录,通过文件相关操作循环处理该目录下的全部txt文件。DirectoryHandler类包含一个名为directoryHandler的方法。
fileHandler用于处理文件中的数据。
DatabaseHandler用于将被directoryHandler()方法和databaseHandler()处理过的数据存放到数据库中.
Student用于封装学生考试成绩表中的基本信息
JdbcUtil用于配置数据库

现有以下需要注意的点:

  1. 班级名称和数量是确定的;
  2. 每个txt中包含的学生记录不一定来自同一个班;
  3. 每个学生只被记录一次,不存在重复记录。

基于GUI的学生成绩管理系统

三、前期准备

1、软硬件平台

  • IDE:IntelliJ IDEA 2020.1
  • JDK:Java8
  • 运行环境:Window10
  • 数据库:MySQL-V5.7

2、 MySQL驱动

到MySQL驱动官网下载对应的驱动 – > 传送门,这里我下载的是mysql-connector-java-5.1.47.jar,大家可以参考一下。

对应的MySQL安装教程可观看往期博客:MySQL-V5.7 压缩包版安装教程

在下载之后直接复制到IDEA中,建议根目录下新建一个lib文件夹,再将驱动复制进去,最后需要在该文件夹鼠标右击,选择Add as Library。这里需要注意的是直接将下载的文件复制进去不需要进行其余操作。

image-20211225233703168

四、核心类介绍

1、Student类

本类是用于封装学生考试成绩表中的基本信息,其主要封装了学生的名字、班级以及成绩,同时具有下列方法

  • 无参构造器;
  • 带三个参数的有参构造器;
  • 对应的 get/set 方法;
  • 重写的 equals()/hashCode()/toString() 方法。
/* 省略了上述方法 */
private String className;   // 班级名称
private String stuName;     //学生名字
private double grade;       //学生成绩

2、UserInterface类

本类搭建了基本的图形用户界面,用于接收文件存放在目录,以及整个年级的平均分显示。该图形用户界面实现了事件处理,可以响应用户的操作,而在这里主要使用了Swing来编写界面,因此在该类中继承的是JFrame类。

IMG_256

2.1、变量定义
private JTextField filePathChoose = null;       // 文件夹所在路径的文本框
private JTextField resultText = null;           // 结果输出框
2.2、初始化窗体界面

image-20211230141050801

在上面的图片中我们可以有一个很明确的思路,就是从上到下的内容分别用四个容器进行存放,再将四个容器添加到窗体中即可。图片自己画的多少有点丑(๑´ㅂ`๑)

// 用于存放选择文件提示语的文本框并左对齐
JPanel pathTextJPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 15));  
// 用于存放选择文件的组件并居中
JPanel filePathJPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 10));  
// 用于存放显示结果的文本框并左对齐
JPanel resultTextJPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 15)); 
// 用于存放确认按钮并居中
JPanel confirmButtonJPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 5));     

在第一个容器中我们可以看出存放的是一段文字,因此我们这里可以使用文本框JTextField或者一个标签JLabel进行编写,需要注意的是,如果使用的是文本框的话需要将其设置文本框不可写以及去除文本边框

// 定义存放选择文件提示语的文本框
JTextField pathText = new JTextField("请输入你要解析的文件所在的路径:");
// 设置文本框不可写
pathText.setEditable(false);   
// 取消文本边框
pathText.setBorder(null);              

在第二个容器中存放的是一个文本框,在要求中这里是需要我们输入文件路径从而读取该路径下的数据,因此可以使用文本框JTextField

// 定义文本框
filePathChoose = new JTextField();
// 设置大小
filePathChoose.setPreferredSize(new Dimension(300,25));
// 设置边框
filePathChoose.setBorder(new MatteBorder(2, 2, 1, 1, Color.DARK_GRAY));

But,在编写测试的过程中,狗子我觉得每次都要手动输入这个路径是不是有点不够智能==(ΘへΘ),因此我在这个基础上进行了改进添加了一个文件选择器在文本框的旁边,这样是不是就高级多了呢┐( ‾᷅㉨‾᷅ )┌==

// 定义存放选择文件的组件
        filePathChoose = new JTextField();
        filePathChoose.setPreferredSize(new Dimension(300,25));
        filePathChoose.setBorder(new MatteBorder(2, 2, 1, 1, Color.DARK_GRAY));

        // 定义打开文件夹的组件
        JButton jbtOpen = new JButton("打开文件夹");
        // 使用匿名内部类为“打开文件夹”按钮添加事件
        jbtOpen.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                // 定义一个文件选择器
                JFileChooser jFileChooser = new JFileChooser();          
                // 获得FileSystemView的一个实例
                FileSystemView fileSystemView = FileSystemView.getFileSystemView(); 
                // 获取桌面的路径并将其设置为打开文件选择器的默认路径
                jFileChooser.setCurrentDirectory(fileSystemView.getHomeDirectory());         			   // 设置只能选择文件夹
                jFileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);       
                /*
                    若showOpenDialog中参数为null,则打开的对话框处于电脑显示屏的中央
                    若showOpenDialog中参数为this,则打开的对话框处于编写的程序屏幕中央
                    showOpenDialog调用之后会返回一个值
                        1. 返回0,代表已经选择了某个文件或文件夹
                        2. 返回1,代表选择了取消按钮或直接关闭了窗口
                        3. JFileChooser.APPROVE_OPTION为一个常量,代表着情况1
                 */
                if (jFileChooser.showOpenDialog(userInterface.this) == JFileChooser.APPROVE_OPTION) {
                    // 获取已经选中的文件夹
                    File selectedFile = jFileChooser.getSelectedFile();   
                    // 将选中的文件的绝对路径同步到文本框中
                    filePathChoose.setText(selectedFile.getAbsolutePath());     
                }
            }
        });

第三个容器和第一个容器相差无几,同样需要设置不可写去除边框

// 定义存放统计结果的的文本框
resultText = new JTextField("统计结果为:", 20);
// 设置文本框不可写
resultText.setEditable(false);        
// 取消文本边框
resultText.setBorder(null);           

第四个容器即最后一个容器是一个JButton按钮,并对按钮绑定点击事件,在这里使用的是匿名内部类的方式。由于在这里我们还没有数据进行读取处理,因此这里暂时是 null

// 定义确定按钮
JButton confirmButton = new JButton("确定");
// 通过构件设置按钮的大小
confirmButton.setPreferredSize(new Dimension(150,30));   
// 绑定点击事件进行计算
confirmButton.addActionListener(new ActionListener() {                    
    @Override
    public void actionPerformed(ActionEvent e) {
        Double result = null;
        System.out.println("统计结果为:" + result);
    }
});

在编写完成上述四个容器内的组件后,便需要将其分别添加到对应的容器中去了

/* 分别将四个组件添加到JPanel容器中 */
pathTextJPanel.add(pathText);               // 选择文件提示语
filePathJPanel.add(filePathChoose);         // 文件选择组件
filePathJPanel.add(jbtOpen);                // 文件夹选择器
resultTextJPanel.add(resultText);           // 统计结果文本框
confirmButtonJPanel.add(confirmButton);     // 确认按钮

获取窗体容器并将四个容器添加到其中,同时设置窗体的对应属性

// 分别将四个组件添加到JPanel容器中
pathTextJPanel.add(pathText);               // 选择文件提示语
filePathJPanel.add(filePathChoose);         // 文件选择组件
filePathJPanel.add(jbtOpen);                // 文件夹选择器
resultTextJPanel.add(resultText);           // 统计结果文本框
confirmButtonJPanel.add(confirmButton);     // 确认按钮
// 获取一个容器并设置其布局管理器
Container container = this.getContentPane();
container.setLayout(new GridLayout(4, 1));
// 分别将四个JPanel容器添加到container容器中
container.add(pathTextJPanel);          	// 添加存放选择文件提示语的文本框的JPanel容器
container.add(filePathJPanel);          	// 添加存放选择文件的组件的JPanel容器
container.add(resultTextJPanel);        	// 添加存放显示结果的文本框的JPanel容器
container.add(confirmButtonJPanel);     	// 添加存放确认按钮的JPanel容器
// 设置窗体大小并固定、默认关闭方式及可视化
this.setResizable(false);
this.setBounds(500, 300, 450, 230);
this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
this.setVisible(true);

好了,到这里userInterface类就告辞一段落了,下面是编写的界面效果图。

image-20211226110409801

3、 DirectoryHandler类

本类用于接收用户界面传入的目录,通过文件相关操作循环处理该目录下的全部txt文件。

3.1、变量定义

image-20211230155436549

由于班级数目是给定的,并且txt文件的结构如上图所示,因此定义的集合的键值类型分别为String-->DoubleString-->Integer

static final int CLASS_NUMBER = 4;           // 年级中班级的总数
private int allClassStudentCount = 0;        // 所有班级成员的个人
private double allClassStudentScore = 0.0;   // 整个年级的成绩总和
private File directoryFile = null;                          // 存放目标目录
private HashMap<String, Double> hmClassScore = null;        // 用于存放班级对应的成绩
private HashMap<String, Integer> hmClassMemberNum = null;   // 用于存放班级对应的成员人数
3.2、数据获取和保存
方法名方法描述
directoryHandler()对txt文件进行处理,并通过调用DatabaseHandler类中的databaseHandler()方法将数据存放到数据库中

在方法的开始,我们应该对前面定义的部分变量进行赋值和实例化,并对集合进行初始化,使得变量的初始值是确定的

// 新建文件夹对象
directoryFile = new File(directory);
// 文件夹下的文件形成的数组
File[] files = directoryFile.listFiles();
if (files == null) {
    return null;
}
// 用于存放每个班级对应的平均分
hmClassScore = new HashMap<String, Double>(CLASS_NUMBER);
// 用于存放每个班级对应的人数
hmClassMemberNum = new HashMap<String, Integer>(CLASS_NUMBER);
// 利用数组进行循环初始化集合
String[] allClass = {"软件1班", "软件2班", "软件3班", "软件4班"};
for (int i = 0; i < CLASS_NUMBER; i++) {
    // 设置每个班的初始成绩是0
    hmClassScore.put(allClass[i], 0.0);
    // 设置每个班的初始人数是0
    hmClassMemberNum.put(allClass[i], 0);
}

通过传递选中文件夹的路径参数并调用FileHandler类下的fileHandler()方法,实现数据的读取,同时更新Map集合中的数据。在此类过程中可能会存在异常抛出,因此需要对异常进行捕获处理。

for(File file : files) {
    try {
        // 通过逐个调用fileHandler方法获得目录下文件中的内容
        List<Student> students = new FileHandler().fileHandler(file.getAbsolutePath());
        if (students == null) {
            new ErrorDialog("文件夹为空!").setVisible(true);
            return null;
        }
        for (Student student : students) {
            // 通过班级key对班级人数进行更新
            hmClassMemberNum.put(student.getClassName(), hmClassMemberNum.get(student.getClassName()) + 1);
            // 通过班级key对班级成绩进行更新
            hmClassScore.put(student.getClassName(), hmClassScore.get(student.getClassName()) + student.getGrade());
            allClassStudentCount++;
            allClassStudentScore += student.getGrade();
        }
    } catch (IOException e) {
        System.out.println(e.getMessage());
        // 实例化错误信息提示弹窗,并返回null值
        new ErrorDialog("文件打开错误!").setVisible(true);
        return null;
    } catch (ArrayIndexOutOfBoundsException e) {
        System.out.println(e.getMessage());
        // 实例化错误信息提示弹窗,并返回null值
        new ErrorDialog("文件已损坏!").setVisible(true);
        return null;
    } catch (NumberFormatException e) {
        System.out.println(e.getMessage());
        // 实例化错误信息提示弹窗,并返回null值
        new ErrorDialog("文件打开错误!").setVisible(true);
        return null;
    }
}

调用DatabaseHandler类中的databaseHandler方法将数据存入数据库,并返回保留两位小数的平均值

// 调用DatabaseHandler类中的databaseHandler方法将数据存入数据库
new DatabaseHandler().databaseHandler(this);
// 返回保留两位小数的平均值
return Double.parseDouble(new DecimalFormat("0.00")
        .format(allClassStudentScore/allClassStudentCount));

4、FileHandler类

该类用于用于处理文件中的数据,其内含一个fileHandler()方法。

4.1、变量定义
//BufferedReader: 用于读取数据
private BufferedReader bis = null;
//fileConcent: 用于容纳学生对象
private List<Student> fileConcent = null;
4.2、读取文件
方法名方法描述备注
List<Student>fileHandler(String fileName)接收由文件目录和文件名组成的字符串参数,返回一个List泛型对象,List中存储的每一个Student对象就是一个学生实体对象,对应txt文件中一条学生记录。存在异常IOException抛出

在方法的开始,同样需要对定义的变量进行实例化。这里需要注意的是在实例化BufferedReader时需要对其设置编码,避免乱码的情况出现。

// 设置编码GBK,否则可能出现编码错误
bis = new BufferedReader(new InputStreamReader(new FileInputStream(fileName), "GBK"));
fileConcent = new ArrayList<Student>();

读取文件的核心思路如下:

  • 按行读取文件中的数据
  • 将读取到的每一行数据根据空格进行分割,封装成一个Student对象
  • 将封装好的Student对象放到fileConcent集合中
  • 重复上述操作知道文件数据读取完毕
try {
    // 存放文件中的每一行数据
    String read = null;
    while ((read = bis.readLine()) != null) {
        // 读取到的每一行信息根据空格进行切割并放到数组中
        String[] studentInfos = read.split(" ");
        // 容纳班级名称的临时变量
        String stuClassName = null;
        // 容纳学生名字的临时变量
        String stuName = null;
        // 容纳学生成绩的临时变量
        double stuGrade = 0;
        try {
            // 单条记录中学生的班级
            stuClassName = studentInfos[0];
            // 单条记录中学生的名字
            stuName = studentInfos[1];
            // 单条记录中学生的分数
            stuGrade = Double.parseDouble(studentInfos[2]);
        } catch (ArrayIndexOutOfBoundsException e) {
            throw new NumberFormatException("请选择正确的文件");
        } catch (NumberFormatException e) {
            throw new NumberFormatException("请选择正确的文件");
        }
        // 将单条数据封装成学生对象添加到集合中
        fileConcent.add(new Student(stuClassName, stuName, stuGrade));
    }
} catch (FileNotFoundException e) {
    throw new FileNotFoundException("文件可能已损坏!");
} finally {
    try {
        // 关闭文件流
        if(bis != null) {
            bis.close();
        }
    } catch (IOException e) {
        throw new IOException("文件关闭出错!");
    }
}
return fileConcent;

5、JdbcUtil类

本类用于配置数据库。

private static String driver = null;
private static String url = null;
private static String username = null;
private static String password = null;
static {
    try {
        // 读取dp.properties文件中的配置信息
        InputStream resourceAsStream = JdbcUtil.class.getClassLoader().getResourceAsStream("dp.properties");
        Properties properties = new Properties();
        properties.load(resourceAsStream);
        // 获取数据库链接驱动
        driver = properties.getProperty("driver");
        // 获取数据库连接URL地址
        url = properties.getProperty("url");
        // 获取数据库连接用户名
        username = properties.getProperty("username");
        // 获取数据库连接密码
        password = properties.getProperty("password");
        // 加载数据库驱动
        Class.forName(driver);
    } catch (Exception e) {
        e.printStackTrace();
    }
}
5.1、工具类方法
方法名方法描述备注
Connection getConnection()获取数据库连接对象获取数据库连接对象存在异常SQLException抛出
void release(Connection connection, Statement statement, ResultSet resultSet)释放资源
/**
 * 获取数据库连接对象获取数据库连接对象
 * @Author xBaozi
 * @Date 16:26 2021/12/14
 * @Param []
 * @return java.sql.Connection
 **/
public static Connection getConnection() throws SQLException {
    return DriverManager.getConnection(url, username, password);
}
/**
 * 释放资源
 * @Author xBaozi
 * @Date 16:25 2021/12/14
 * @Param [connection, statement, resultSet]
 * @return void
 **/
public static void release(Connection connection, Statement statement, ResultSet resultSet) {
    if (resultSet != null) {
        try {
            resultSet.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    if (statement != null) {
        try {
            statement.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    if (connection != null) {
        try {
            connection.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

6、DatabaseHandler类

本类用于将被DirectoryHandler类处理过的数据存放到数据库中,其内含一个databaseHandler()方法。

6.1、dp.properties文件

在本项目中,狗子我将数据库的配置文件单独写了出来放在了src下的dp.properties文件,再通过工具类进行获取即可。

driver = com.mysql.jdbc.Driver
# jdbc:mysql://所连接接口名:端口号/所需要连接的数据库库名?【配置设置】
url = jdbc:mysql://localhost:3306/spms_final_exam?useUnicode=true&characterEncoding=utf8&useSSL=true
username = root
password = 123456
6.2、变量定义
// 定义一个数据库连接
Connection connection = null;
// 定义一个负责执行SQL语句的preparedStatement对象
PreparedStatement preparedStatement = null;
// 定义一个结果集
ResultSet resultSet = null;
5.3、JDBC操作
方法名方法描述
databaseHandler(DirectoryHandler directoryHandler)用于处理HashMap中的数据,将其存入数据库当中

在这里的思路都很统一,通过工具类获取数据库连接,循环遍历集合获取班级信息,通过SQL语句将班级信息进行插入或更新,在完成之后对资源进行释放。

// 定义decimalFormat对象用于浮点数格式控制
DecimalFormat decimalFormat = new DecimalFormat("0.00");
// 获取班级对应人数的集合
HashMap<String, Integer> hmClassMemberNum = directoryHandler.getHmClassMemberNum();
// 获取班级对应成绩的集合
HashMap<String, Double> hmClassScore = directoryHandler.getHmClassScore();
try {
    // 获取一个数据库连接
    connection = JdbcUtil.getConnection();
    // 通过for-each循环读取集合中的数据
    for (Map.Entry<String, Double> entry : hmClassScore.entrySet()) {
        // 班级名字
        String className = entry.getKey();
        // 班级人数
        int classMemberNumber = hmClassMemberNum.get(entry.getKey());
        // 班级成绩
        double classAvgScore = Double.parseDouble(decimalFormat
                .format(entry.getValue()/classMemberNumber));
        // 控制台同步输出插入信息
        System.out.println("{" + className + "," +
                classAvgScore + "," +
                classMemberNumber + "}");
        // 定义要执行的SQL语句
        String sql = "INSERT INTO class_score(class_name, class_avg_score, class_menber_number) VALUES(?,?,?) ";
        // 创建一个prepareStatement对象
        preparedStatement = connection.prepareStatement(sql);
        // 为SQL语句中的第一个?设置值
        preparedStatement.setString(1, className);
        // 为SQL语句中的第二个?设置值
        preparedStatement.setDouble(2, classAvgScore);
        // 为SQL语句中的第三个?设置值
        preparedStatement.setInt(3, classMemberNumber);
        int status = 0;
        try {
            // 执行SQL语句并返回插入的行数
            status = preparedStatement.executeUpdate();
        } catch (MySQLIntegrityConstraintViolationException throwables) {
            // 定义要执行的SQL语句
            sql = "UPDATE class_score " +
                    "SET class_avg_score = ?, class_menber_number = ? " +
                    "WHERE class_name = ?";
            // 创建一个prepareStatement对象
            preparedStatement = connection.prepareStatement(sql);
            // 为SQL语句中的第一个?设置值
            preparedStatement.setDouble(1, classAvgScore);
            // 为SQL语句中的第二个?设置值
            preparedStatement.setInt(2, classMemberNumber);
            // 为SQL语句中的第三个?设置值
            preparedStatement.setString(3, className);
            status = preparedStatement.executeUpdate();
        }
        // 判断是否更新成功
        if (status > 0) {
            System.out.println("更新成功");
        } else {
            System.out.println("数据出现错误!");
        }
    }
} catch (SQLException throwables) {
    throwables.printStackTrace();
    new ErrorDialog("数据更新出错").setVisible(true);
} finally {
    JdbcUtil.release(connection, preparedStatement, resultSet);
}

五、结果演示

到这里就很happy的说明,已经完成了本次项目的开发,下面将会看到一系列的结果图片演示。

  1. 整体界面展示

image-20211231004427279

  1. 正常跑起来的结果界面展示

image-20211231004609615

  1. 路劲不存在情况展示

image-20211231004655271

  1. 文件内容错误情况展示

image-20211231004922985

六、总结

这个项目断断续续写了两天,但是如果专注写的话,应该一个下午左右就可以写完了(只是狗子我自己的一个速度猜测),收获还是挺多的,当然这个项目还是有很多地方可以进行改进的:

  1. 面向问题编程。在编写代码中,问题总比需求多的。在编写的过程中总是会遇到一些奇奇怪怪你没有见过的bug,有些是编译时异常,而有些是能跑了,但是达不到你理想中的要求,因此总是需要花大量的时间在这一部分中,但是收获往往都会比付出的多;
  2. 实践是一个检验知识的一个很好的手段。在学习过程中,眼睛好像懂了,脑子好像有印象了,但是当你手放上键盘的时候就可能出现一个“手:会你个大头,你们来!”的情况,因此,代码掉发道路,手总得动起来,键盘总得敲起来;
  3. 在几个核心类中,应该还需要对方法进行进一步的分解,还需要更进一步的理解面向对象的真谛,并对自己所学知识的一个灵活使用。

在最后,放上gitee源码仓库,需要的小伙伴可自行叉走 --> 传送门

完结撒花,走人!

mmexport7ac456729c2c7e40513e83967f4331c1_1628689601357

  • 13
    点赞
  • 72
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

陈宝子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值