LOADING

加载过慢请开启缓存 浏览器默认开启

程序设计课程设计

2021/7/20 C++

程序设计课程设计

在火车上,经过满是隧道的四川地区,无网络,遂想找点事做,想起因为考试而一直鸽着的程序设计blog,就心血来潮打开笔记本开始记录起来。

背景

因为我在学期中就大概了解了一下这个课程设计是个什么东西,所以也就提前准备了一些,当然这一切都是在司徒的鼓励【鼓舞?鼓动?】下进行的。司徒给我介绍了QT编程,我大致的了解了一下过后觉得挺合适的,首先它的语言是Cpp,是我目前唯一掌握了的语言,其次它还有个好处就是可以一个代码可以编译到不同的平台上执行。再加上MFC的各种被诟病,我就放弃了这个方向,转而向Qt进发。

题目

(1)、设计一个学生类Student,包括数据成员:姓名、学号、二门课程(面向对象程序设计、高等数学)的成绩。

(2)、创建一个管理学生的类Management,包括实现学生的数据的增加、删除、修改、按课程成绩排序、保存学生数据到文件及加载文件中的数据等功能。

(3)、创建一个基于对话框的MFC应用程序,程序窗口的标题上有你姓名、学号和应用程序名称。使用(1)和(2)中的类,实现对学生信息和成绩的输入和管理。

(4)、创建一个单文档的MFC应用程序,读取(3)中保存的文件中的学生成绩,分别用直方图和折线方式显示所有学生某课程的成绩分布图。

——————————————————————————————

以上便是题目,很显然,题目是要求用MFC实现各种功能,我便要用Qt来实现

资料查找

众所周知,我们这是面向百度,谷歌,对象编程。而我对GUI编程可谓是一无所知,更别说Qt编程了,所以说,找到一篇别人已经完成的项目,将其读懂并“制作”成自己的项目就成为了我的课设解决方案。【滑稽】

在我没有查找到想要的项目时,我是十分崩溃的,因为我知道整个思路和实现的方法,但是我却无法将其转化为编程语言将它写出来。既然都说到这了,那就把我的思路写下来吧。

实现思路

首先我要解决怎么样存储和读取的问题,要存成什么形式?存在哪里?等等等等……经过一番查找,各种项目的方法都是用MySQL或者SQLlite以数据库的形式来存储,这个想法直接被我否决了,其一,这个项目储存的相当于只是一个二维数组,用数据库简直是杀鸡焉用牛刀,大材小用了;其二,数据库我下学期才学,我现在根本不会啊,一周的时间我也没法学啊。

我的思路是将数据储存在一个txt文件里,就想一个CSV文件一样的排列,遵循着这样的思想,我成功地找到了一个大佬的项目,项目的地址在下方:

https://github.com/ChangWenhan/StudentManagementSystem-Qt

我的这个项目就是在他的项目基础上魔改而来。

代码详解

我的项目地址在下方:

https://github.com/Yuzi19/GZHU_Program_design_2021_1

mainwindow

main.cpp的作用无须多言,就是启动mainwindow主窗口,其中的东西都是默认的,没有修改。

mainwindow是整个程序的主窗口,程序的主要功能就在这里执行,下面我详细讲讲每个部件:

按钮

第一个按钮是刷新按钮。因为程序本身设计的原因,表格中的数据不会在改变后立即变化,所以我设计了这么一个按钮来重载一遍数据,实际上要设计成实时刷新,可以把这个按钮对应的槽函数加到添加和修改删除窗口的析构函数里,应该可以得到这样的效果,如果你感兴趣,可以尝试一下。

下拉框有四个选项,分别是数学和cpp成绩的正序和倒序,点击排序按钮后,就可以依照下拉框选择的排序方式对表格进行排序。

直方图按钮点按后,会弹出一个窗口,并根据存储的数据进行直方图绘制。

增加按钮按下后,会弹出增加数据的窗口

增加按钮旁边的按钮是一个空按钮,按下并没有什么用,这是因为我把编辑和删除的功能设计成双击表格中的某一项了(下面会讲到)。

右边的确定和取消按钮,是用来确认是否保存更改的。要讲起它们的作用,就要从构造函数和析构函数讲起了。

mainwindow的构造函数的一部分:

QFile::copy("student.txt","student_old.txt");

这段话的作用是,在程序启动时,复制student.txt到student_old.txt中。

析构函数的一部分:

QFile::remove("student_old.txt");

这段话是程序退出时,删去备份student_old.txt。

确定按钮对应的槽函数:

void MainWindow::on_buttonBox_accepted()
{
    this->close();
}

直接关闭窗口,此时的student.txt就是修改后的文件,修改前的文件就随着析构函数的执行而被丢弃。

取消按钮对应的槽函数:

void MainWindow::on_buttonBox_rejected()
{
    int ret=QMessageBox::question(this,"请确认","确定要不保存数据而关闭吗?","确认","取消");
    if(ret==0)
    {
        QFile::remove("student.txt");
        QFile::rename("student_old.txt","student.txt");
        this->close();
    }
}

首先会弹出一个确认窗口:”确定要不保存数据而关闭吗?”如果选择取消,下方的if不会被执行,程序不会退出,回到主界面;如果选择确定,修改后的文件将会被废弃,修改前的文件被命名为student.txt,程序关闭,起到了不修改文件的作用。

表格

位于主窗口正中央的是一个表格,准确来说,是一个QTableView类的部件,差不多就是一个表格,我对它也没有搞得多清楚。

下面是析构函数的一部分:

this->model=new QStandardItemModel;

    this->model->setHorizontalHeaderItem(0,new QStandardItem("学号"));
    this->model->setHorizontalHeaderItem(1,new QStandardItem("姓名"));
    this->model->setHorizontalHeaderItem(2,new QStandardItem("性别"));
    this->model->setHorizontalHeaderItem(3,new QStandardItem("年龄"));
    this->model->setHorizontalHeaderItem(4,new QStandardItem("高数"));
    this->model->setHorizontalHeaderItem(5,new QStandardItem("C++"));

    this->ui->tableView->setModel(model);

    this->ui->tableView->setColumnWidth(0,140);
    this->ui->tableView->setColumnWidth(1,130);
    this->ui->tableView->setColumnWidth(2,100);
    this->ui->tableView->setColumnWidth(3,100);
    this->ui->tableView->setColumnWidth(4,100);
    this->ui->tableView->setColumnWidth(5,105);

大概可以读出来,他整了个标准的模型,然后给这模型水平第一行分别填入了学号姓名等等。然后他将这个model给了mainwindow的tableView,并设置了每一列的宽度。这些都是在析构函数中的,所以在程序一启动的时候就可以看到第一行。

主要的函数

readstudentfile()

这个函数是将student.txt中的内容读取到由QString组成的QList中,这个Qlist定义在mainwindow的头文件中,是其私有成员,mainwindow.h部分代码:

private:
    Ui::MainWindow *ui;
    QList<QString> score_line;
    QStandardItemModel *model;

readstudentfile()代码:

int MainWindow::readstudentfile()
{
    score_line.clear();
    QFile file("student.txt");
    if(!file.open(QIODevice::ReadOnly|QIODevice::Text))
    {
        return -1;
    }
    QTextStream in(&file);
    while (!in.atEnd())
    {
        QString line=in.readLine();
        score_line.append(line);
    }
    file.close();
    return 0;
}

3行:清空score_line的内容;4行:载入文件;5-8行,如果文件不存在或者无法打开则退出;9行将文字处理函数载入file;12行:定义一个字符串存储file中读出来的一行;13行:并将这一行添加到score_line的结尾;10行:直到读到file末尾为止。

display(int row, QStringList score_line)

此函数是在read函数执行完后,将score_line读取到的内容显示到tableView中。

void MainWindow::display(int row, QStringList score_line)
{
    int i=0;
    for (i=0;i<score_line.length();i++)
    {
        this->model->setItem(row,i,new QStandardItem(score_line.at(i)));
    }
}

要看它的效果,得把按钮的槽函数拿出来一起看

刷新按钮的槽函数
void MainWindow::on_pushButton_clicked()
{
    this->model->clear();
    reset();
    readstudentfile();
            int i=0,row=0;
            for (i=0;i<score_line.length();i++)
            {
                QString line=score_line.at(i);
                line=line.trimmed();
                QStringList linesplit=line.split(" ");
                        display(row++, linesplit);
            }
}

点击刷新后,首先将表格进行了清空,然后对表格进行了初始化【reset函数内容与构造函数的初始化表格代码类似,下面会提到】,然后读取了文件进了score_line,然后将内容通过display显示到表格上。

reset()

上文提到,reset函数内容与构造函数的初始化表格代码类似。

void MainWindow::reset()
{
    this->model->setHorizontalHeaderItem(0,new QStandardItem("学号"));
    this->model->setHorizontalHeaderItem(1,new QStandardItem("姓名"));
    this->model->setHorizontalHeaderItem(2,new QStandardItem("性别"));
    this->model->setHorizontalHeaderItem(3,new QStandardItem("年龄"));
    this->model->setHorizontalHeaderItem(4,new QStandardItem("高数"));
    this->model->setHorizontalHeaderItem(5,new QStandardItem("C++"));
    this->ui->tableView->setColumnWidth(0,140);
    this->ui->tableView->setColumnWidth(1,115);
    this->ui->tableView->setColumnWidth(2,100);
    this->ui->tableView->setColumnWidth(3,100);
    this->ui->tableView->setColumnWidth(4,100);
    this->ui->tableView->setColumnWidth(5,100);
}
排序
void MainWindow::on_sort_clicked()
{
    on_pushButton_clicked();
    int flag=this->ui->sortway->currentIndex();
    switch (flag) {
    case 0:
        model->sort(4,Qt::DescendingOrder);
        break;
    case 1:
        model->sort(4,Qt::AscendingOrder);
        break;
    case 2:
        model->sort(5,Qt::DescendingOrder);
        break;
    case 3:
        model->sort(5,Qt::AscendingOrder);
        break;
    default:
        break;
    }
}

首先刷新一下表格,定义flag为当前下拉框所选择的数据,使用switch对应每一种情况。

增加学生(addstu)

在mainwindow中,有一个按钮可以增加数据,它的槽函数就是启动这个窗口:

void MainWindow::on_add_student_clicked()
{
    addstu *add=new addstu;
    add->show();
}

界面如下图:

函数

取消按钮按下后就是直接关闭窗口,这里不细说了

确定按钮的槽函数:

void addstu::on_buttonBox_accepted()
{
    QString name=this->ui->add_stu_name->text();
    QString age=this->ui->add_stu_age->text();
    QString number=this->ui->add_stu_num->text();
    QString gender=this->ui->add_stu_gender->text();
    QString math=this->ui->add_stu_math->text();
    QString cpp=this->ui->add_stu_cpp->text();

    QString info=number+" "+name+" "+gender+" "+age+" "+math+" "+cpp;

    bool charge=name.length()<1||number.length()<1||gender.length()<1||
                age.length()<1||math.length()<1||cpp.length()<1;

    if(charge==1)
    {
        QMessageBox::critical(this,"错误","信息填写不完整,请检查","确定");
    }
    else
    {
        QFile mFile("student.txt");
        if(!mFile.open(QIODevice::Append|QIODevice::Text))
        {
            QMessageBox::critical(this,"错误","文件打开失败,信息没有写入","确认");
            return;
        }
        QTextStream out(&mFile);
        out<<info<<"\n";
        mFile.flush();
        mFile.close();
        return;
    }
}

首先将框中的所有数据分别赋值到对应的字符串上,再将这些字符以空格隔开,组成一个新的字符串“info”,然后在检查数据无误,文件无误后,将其写入文件的新的一行。

编辑和删除数据

触发方法

上文提到,进入删除与编辑框是双击表格中的项目触发的,在mainwindow中的槽函数是:

void MainWindow::on_tableView_doubleClicked(const QModelIndex &index)
{
    int row=this->ui->tableView->currentIndex().row();
    num1=model->data(model->index(row,0)).toString();
    name1=model->data(model->index(row,1)).toString();
    gender1=model->data(model->index(row,2)).toString();
    age1=model->data(model->index(row,3)).toString();
    math1=model->data(model->index(row,4)).toString();
    cpp1=model->data(model->index(row,5)).toString();
    
    change_and_del a;
    a.exec();
}

首先row指代当前选中的行,然后将这一行中的数据全部赋值给变量,然后启动并显示编辑删除窗口

在mainwindow.h中有如下定义:

extern QString name1;
extern QString age1;
extern QString num1;
extern QString gender1;
extern QString math1;
extern QString cpp1;

这使得这些变量在编辑删除窗口类中也可以被使用。

函数

构造函数

部分构造函数如下:

    this->ui->edit_stu_num->setText(num1);
    this->ui->edit_stu_name->setText(name1);
    this->ui->edit_stu_gender->setText(gender1);
    this->ui->edit_stu_age->setText(age1);
    this->ui->edit_stu_math->setText(math1);
    this->ui->edit_stu_cpp->setText(cpp1);

这将双击选中的学生的数据都写入了编辑框中

确认按钮槽函数(重要!!)

21-7-18的时候,我在写这一段文字,距离我写代码快过去了20天,以至于我都搞不明白这段代码的意思,在图书馆研读了半个小时我才搞清楚,代码如下:

void change_and_del::on_buttonBox_accepted()
{
    QString name=this->ui->edit_stu_name->text();
    QString age=this->ui->edit_stu_age->text();
    QString number=this->ui->edit_stu_num->text();
    QString gender=this->ui->edit_stu_gender->text();
    QString math=this->ui->edit_stu_math->text();
    QString cpp=this->ui->edit_stu_cpp->text();

    QString info=number+" "+name+" "+gender+" "+age+" "+math+" "+cpp;

    bool charge=name.length()<1||number.length()<1||gender.length()<1||
                age.length()<1||math.length()<1||cpp.length()<1;

    if(charge==1)
    {
        QMessageBox::critical(this,"错误","信息填写不完整,请检查","确定");
    }
    else
    {
        if(readstudentfile()==-1)
        {
             this->close();
             QMessageBox::critical(this,"错误","文件读取失败,信息没有删除","确认");
        }
        else
        {
           int i=0;
           for (i=0;i<score_line.length();i++)
           {
               QString line=score_line.at(i);
               line=line.trimmed();
               QStringList linesplit=line.split(" ");
               if(num1!=linesplit.at(0))
               {
                    QFile file("student_temp.txt");
                    if(!file.open(QIODevice::Append|QIODevice::Text))
                    {
                        QMessageBox::critical(this,"错误","文件打开失败,信息没有修改","确认");
                        return;
                    }
                    QTextStream out(&file);
                    out<<line+"\n";
                    file.close();
                }
               else
               {
                   QFile file("student_temp.txt");
                   if(!file.open(QIODevice::Append|QIODevice::Text))
                   {
                       QMessageBox::critical(this,"错误","文件打开失败,信息没有修改","确认");
                       return;
                   }
                   QTextStream out(&file);
                   out<<info+"\n";
                   file.flush();
                   file.close();
               }
             }
         QFile file_old("student.txt");
         QFile file_new("student_temp.txt");
         if (file_old.exists())
         {
           file_old.remove();
           file_new.rename("student.txt");
         }
         else
         {
           QMessageBox::critical(this,"错误","未有信息保存为文件,无法修改","确认");
         }
         QMessageBox::information(this,"通知","修改成功!","确认");
         this->close();
         }
    }
}

首先,3-10行,将数据从框中读取并赋值给变量。然后是常规的确认文件,无须多言,下面是重点:

29行的for循环,循环次数是文件的行数,也就是学生的个数。

然后将当前行【第i行】的内容写入line中,然后创建了一个temp的空文件,34行的if条件是学号是否对应,34-57行的内容是,若不是本次修改的行,就照抄原来的那一行(也就是将源文件复制过来),如果是本次修改的行,则把上面已经准备好的info写进去。

60-66行将temp转正同时删去旧文件。

我这样修改后的写法,就可以使修改后的学生保持在原位,原来的写法是将旧的文件除了修改的行复制进temp后,在将修改后的行加上。这样的话,一旦修改了数据,那一行就会被置于文件的末尾。

删除按钮槽函数
void change_and_del::on_del_Button_clicked()
{
    readstudentfile();
    int i=0;
    int ret=QMessageBox::question(this,"请确认","确定要删除吗?","确认","取消");
    if(ret==1)
    {
    }
    else
        {
            for (i=0;i<score_line.length();i++)
            {
                QString line=score_line.at(i);
                line=line.trimmed();
                QStringList linesplit=line.split(" ");
                if(num1!=linesplit.at(0))
                {
                    QFile file("student_temp.txt");
                    if(!file.open(QIODevice::Append|QIODevice::Text))
                    {
                       QMessageBox::critical(this,"错误","文件打开失败,信息没有写入","确认");
                       return;
                    }
                QTextStream out(&file);
                out<<line+"\n";
                file.close();
                }
            }
            QFile file_old("student.txt");
            QFile file_new("student_temp.txt");
            if (file_old.exists())
            {
               file_old.remove();
               file_new.rename("student.txt");
            }
            else
            {
               QMessageBox::critical(this,"错误","未有信息保存为文件,无法删除","确认");
            }
            QMessageBox::information(this,"通知","删除成功!","确认");
            this->close();
    }
}

有了上面编辑的经验,这个就很简单了,其实上面编辑功能已经实现了删除功能了,只是加上了else写入了新的数据而已。删除的函数就没有这样,从16行的if就可以知道原理:将原文件除了选中行都复制过来,最终效果就是那一行消失啦!

最后附上一张截图:

直方图

定义

在类私有区域定义了如下数据

int P1A[5]={0},P1B[5]={0};
QList<QString> score_line;

第一个数组是存放数学0-59,60-69,70-79,80-89,90-100的人数的,第二个数组是存放cpp的人数的。

计算人数

函数如下:

void histogram::calc()
{
    readstudentfile();
    for (int i=0;i<score_line.length();i++)
    {
        QString line=score_line.at(i);
        line=line.trimmed();
        QStringList linesplit=line.split(" ");
        int a=linesplit.at(4).toInt(); //提取数学成绩并转为int
        if(a<60)
            P1A[0]++;
        else if(a<70)
            P1A[1]++;
        else if(a<80)
            P1A[2]++;
        else if(a<90)
            P1A[3]++;
        else
            P1A[4]++;
    }
    for (int i=0;i<score_line.length();i++)
    {
        QString line=score_line.at(i);
        line=line.trimmed();
        QStringList linesplit=line.split(" ");
        int a=linesplit.at(5).toInt(); //提取cpp成绩转为int
        if(a<60)
            P1B[0]++;
        else if(a<70)
            P1B[1]++;
        else if(a<80)
            P1B[2]++;
        else if(a<90)
            P1B[3]++;
        else
            P1B[4]++;
    }

}

首先读取数据,逐行循环,读取每个人的数学/cpp成绩,并进行判断,并将其加入数组的相应位置中。注意9行和26行的赋值时,使用了QString的转为整数功能toInt(),使之可以赋给int类型的变量。

可以优化的地方:上下两个for循环完全可以合并为一个

绘图

函数如下,具体可以看注释:

void histogram::paintEvent(QPaintEvent *event)
{
    Q_UNUSED(event);

    QPainter painter(this);
    painter.setPen(QColor(0,0,0));

    //绘制直方图 远点50,400,单位高度50 单位宽度20
    painter.drawLine(50,400,750,400);     //x轴 单位长10,30个单位,总长700
    painter.drawLine(50,400,50,50);      //y轴 单位50,5个单位,总长350
    painter.drawLine(50,50,45,55);      //上箭头
    painter.drawLine(50,50,55,55);      //上箭头
    painter.drawLine(745,395,750,400);    //右箭头
    painter.drawLine(745,405,750,400);    //右箭头
    int xi = 40;            //单位长度x
    int yi = 50;            //单位长度y
    int u = 3;              //刻度的长度
    //画y轴的刻度
    for(int i=0;i<=6;i++)
    {
        painter.drawLine(50,400-yi*i,50+u,400-yi*i);        //画刻度线
        painter.drawText(QPoint(40,403-yi*i),QString::number(i));   //画刻度数字
    }
    //画x轴的刻度
    for(int i=1;i<=5;i++)
    {
        painter.drawLine(40+xi*3*i,400,40+xi*3*i,403);    //画刻度线
    }
    painter.drawText(QPoint(30+xi*3*1,420),"0-59"); //画刻度1数字
    painter.drawText(QPoint(30+xi*3*2,420),"60-69"); //画刻度2数字
    painter.drawText(QPoint(30+xi*3*3,420),"70-79"); //画刻度3数字
    painter.drawText(QPoint(30+xi*3*4,420),"80-89"); //画刻度4数字
    painter.drawText(QPoint(30+xi*3*5,420),"90-100"); //画刻度5数字

    //数学成绩直方图
    painter.setBrush(QColor(62,147,192));
    for(int i=0;i<5;i++)
    {
        painter.drawRect(40+xi*3*(i+1)-40,400-P1A[i]*yi,40,P1A[i]*yi);
    }
    //cpp成绩直方图
    painter.setBrush(QColor(62,102,149));
    for(int i=0;i<5;i++)
    {
        painter.drawRect(40+xi*3*(i+1),400-P1B[i]*yi,40,P1B[i]*yi);
    }



    //给出说明
    painter.drawText(QPoint(360,520),"说明:");
    painter.drawText(QPoint(450,520),"数学");
    painter.drawText(QPoint(450,570),"C++");
    painter.setBrush(QColor(62,147,192));
    painter.drawRect(400,500,30,30);
    painter.setBrush(QColor(62,102,149));
    painter.drawRect(400,550,30,30);


 }

简而言之,就是使用QPainter进行绘图,39行和45行是画柱子的函数,这个是函数的功能是画一个填充了颜色的矩形,后面的四个参数是左上和右下两个点的坐标,公式看起来很复杂,但是如果你稍加思考理清计算统计的思路便可知道是怎么画出来的。

最后附上一张截图。

需要改进的地方

首先呢,这是一次课设,它没有十分具体的要求,所以有些限制措施我就没有写,这就有可能导致一些问题

1.没有限制输入的内容:首先,程序的运行是基于空格来判断数据之间的间隔,如果输入的东西里面包含空格的话,就会产生意想不到的结果,比如:

这里我在年龄那里打了个空格,然后呈现在表中就是这样:

多出来了一个并不存在的第七行,至于为什么会这样,相信读到这里的你一定知道是什么原因了吧。

其次,学号位数啊什么什么的都么有加以限制,只要都写了东西就通过。也没对分数区间加以限制,1000分都可以。

2.未对学号单一性进行限制:在修改的时候都是根据学号来定位要修改的行,如果输入了两个学号一样的学生就会修改错误【比如改下面的变成了上面的】(PS:现实生活中也没有一个学校里面学号相同的学生吧)

3.没有写源文件丢失的措施:这个加上其实很简单,但是要加上去很繁琐我就没有搞,如果程序运行时student.txt没了程序会报错无法运行,但程序不会自己解决,这个可以在构造函数以及在每个判断文件是否存在的地方加上一些东西,具体的由读者来探索吧。

差不多就这些了,有疑问欢迎来GitHub发私信或者邮箱交流,O(∩_∩)O谢谢!

项目地址:https://github.com/Yuzi19/GZHU_Program_design_2021_1