程序设计
课程设计实验报告
学院: 计算机科学与网络工程学院
专业班级: XXX
姓名: XXX
学号: 190XXXXXXX
指导老师:张艳玲
2020.6.29
目录
一、课程设计题目及内容
题目:学生成绩管理系统
设计要求及提示如下:
(1)、设计一个学生类Student,包括数据成员:姓名、学号、二门课程(面向对象程序设计、高等数学)的成绩。
(2)、创建一个管理学生的类Management,包括实现学生的数据的增加、删除、修改、按课程成绩排序、保存学生数据到文件及加载文件中的数据等功能。
(3)、创建一个基于对话框的MFC应用程序,程序窗口的标题上有你姓名、学号和应用程序名称。使用(1)和(2)中的类,实现对学生信息和成绩的输入和管理。
(4)、创建一个单文档的MFC应用程序,读取(3)中保存的文件中的学生成绩,分别用直方图和折线方式显示所有学生某课程的成绩分布图。
二、每个功能模块的设计分析及算法描述
各个功能模块的核心功能在Management类完成:
- 在Management类里声明了一个动态数组all,默认初始化容量为10个,在添加数据时,首先进行数据合法性检查,如果检查不通过直接添加失败,通过则进行添加。如果数组容量满了,将进行自动扩容,扩容的方法是:再开辟一个更大容量的动态数组temp,然后逐一将all中的数据拷贝到temp,拷贝完成之后释放掉all的内存空间,再让all指向temp。如果数组容量再次满了,重复上述操作。
- 删除数据时,需要传入学号,是先逐一遍历all数组中的元素,当发现某个Student与传入的学号相同时,就将这这后面的所有Student向前挪一位,覆盖掉要被删除的Student。
- 修改数据,分为修改姓名、学号、程序课成绩、高数课成绩四个函数,这四个函数都是需要传入学号和修改后的数据,方法和删除数据一样,先根据id找到要修改的Student,然后修改。
- 排序功能:采用冒泡排序。
- 加载文件中的数据:在构造函数里初始化数组all,使用ifstream读取和程序相同目录下的“data.txt”文件,逐个读取后封装成Student对象,添加到数组all。
- 保存数据到文件:在析构函数里进行,先用ofstream打开“data.txt”文件,然后遍历all数组,分别文件输出姓名、学号、程序课成绩、高数课成绩,每个信息之间用空格分隔,每保存完一个Student后回车。
data.txt文件如图:
画统计图在View类完成:
- 排序之前,从Management类里获取要统计的信息(高数课成绩或者程序课成绩),vector类型,然后再使用这些数据来统计。
- 绘制条形图,先创建一个矩形,把vector中的数据分为5个区间,根据矩形的宽度、高度和区间的最高人数计算每个人应该占多少高度、每个区间占多少宽度,然后根据这些信息来绘制。
- 绘制折线图,也是先创建一个矩形,然后在这个矩形里画线(10行,n列,n为数据的数量),用矩形的宽度/数据的数量可以得到每个点相差的距离,用矩形的高度*数据的值/100得到高度,然后每个点的信息都记录在数组里,最后交给Polyline函数画线。
查找功能:首先要检查输入内容的合法性(分数区间不能为空,最大值不能大于100,最小值不能小于0,且最小值不能比最大值大),借助vector容器,遍历Management的all数组,如果满足条件,则将改Student添加到vector容器中,最后遍历完之后输入vector容器保存的结果。模糊查询的方法是用CString的Find()函数,如果返回结果不为-1,则代表这个CString里包含这个字符串。
三、程序中使用的数据及主要符号说明
Student 类:
CString m_name;//姓名
CString m_id;//学号(唯一)
int score1;//程序课成绩
int score2;//高数课成绩
Management 类:
Student* all; //存放学生数据的数组
int capacity; //数组当前容量
int size; //数组当前大小
(extern) Management m; //Management对象,在View.cpp和ADD.cpp都有出现,为了在两个cpp共用一个对象,所以在其中一个地方加了extern
数据管理的对话框:
ADD; //添加数据的对话框,类名为ADD
CString m_name; //输入姓名的编辑框
CString m_id;//输入学号的编辑框
int score1;//输入程序课成绩的编辑框
int score2;//输入高数课成绩的编辑框
int Search_Score1_Min; //数据查找时,需要输入的程序课成绩最小值
int Search_Score1_Max; //数据查找时,需要输入的程序课成绩最大值
int Search_Score2_Min; //数据查找时,需要输入的高数课成绩最小值
int Search_Score2_Max; //数据查找时,需要输入的高数课成绩最大值
CString Search_Name; //数据查找时,需要输入的姓名
CString Search_Id; //数据查找时,需要输入的程序课成绩学号
四、带有详细注释的自己编写的源程序
Student.h
#pragma once
class Student {
public:
CString m_name;//姓名
CString m_id;//学号(唯一)
int score1;//程序课
int score2;//高数课
//全参构造
Student(const CString& m_name, const CString& m_id, int score1, int score2);
//默认构造
Student();
};
Student.cpp
#include "pch.h"
#include "Student.h"
Student::Student(const CString& m_name, const CString& m_id, int score1, int score2)
: m_name(m_name),
m_id(m_id),
score1(score1),
score2(score2) {
}
Student::Student() {
}
Management.h
#pragma once
#include<fstream>
#include "Student.h"
#include <string>
#include "ADD.h"
#include <vector>
using namespace std;
constexpr auto INIT_SIZE = 10; //默认大小是10个;
constexpr auto INCREASE_SIZE = 10; //数组满后扩充的容量;
class Management {
public:
Student* all = NULL; //存放学生数据的数组,在构造函数中对其初始化
int capacity = 0; //数组当前容量
int size = 0; //当前大小
//--------------------------------------------------------
//基本功能
void add(Student stu); //添加数据
bool del(CString id); //根据ID删除数据
bool hasId(CString id); //判断id是否存在
void sortByScore1(bool = false); //按程序课成绩排序(false:降序,true:升序,采用了默认参数,默认降序)
void sortByScore2(bool = false); //按高数课成绩排序(false:降序,true:升序,默认降序)
void sortById(bool = false); //按学号排序(false:降序,true:升序,默认降序)
//---------------------------------------------------------
//根据id获取名字、分数
CString getName(CString id);
int getS1(CString id); //获取程序课成绩
int getS2(CString id); //获取高数课成绩
//---------------------------------------------------------
//根据id来修改信息
bool changeData(CString oldId, CString newName, CString newId, int S1, int S2) const;
//---------------------------------------------------------
//构造和析构函数
Management();
~Management();
//--------------------------------------------------------
//获取高数和程序课的vector集合(用于画统计图)
vector<int> getMathScores() const;
vector<int> getProgrammingScores() const;
};
Management.cpp
#include "pch.h"
#include "Management.h"
//添加数据
void Management::add(Student stu) {
if (size >= capacity) {
//如果当前数组存满了
capacity += INCREASE_SIZE;
Student* temp = new Student[capacity]; //重新创建一个容量更大的数组
for (int i = 0; i < size; i++) {
temp[i] = all[i]; //把旧数组的数据迁移到新数组
}
delete[] all; //把旧数组的空间释放掉
all = temp; //让all指针指向新数组
}
all[size++] = stu; //插入数据
}
//删除数据
bool Management::del(CString id) {
for (int i = 0; i < size; i++) {
//寻找指定id
if (id == all[i].m_id) {
//让后面的元素往前移,填补中间被删除后空缺的位置
for (int j = i; j < size - 1; ++j) {
all[j] = all[j + 1];
}
--size;
return true;
}
}
//如果找不到指定的id,删除失败
return false;
}
//判断是否存在指定的id
bool Management::hasId(CString id) {
for (int i = 0; i < size; i++) {
if (id == all[i].m_id)
return true;
}
return false;
}
//按照程序课成绩排序(false:降序,true:升序,采用默认参数,默认降序)
void Management::sortByScore1(bool tag) {
//采用冒泡排序算法
if (tag) {
//升序
for (int i = 0; i < size; i++) {
for (int j = 0; j < size - i - 1; ++j) {
if (all[j].score1 > all[j + 1].score1) {
Student temp = all[j];
all[j] = all[j + 1];
all[j + 1] = temp;
}
}
}
}else {
//降序
for (int i = 0; i < size; i++) {
for (int j = 0; j < size - i - 1; ++j) {
if (all[j].score1 < all[j + 1].score1) {//和上面的不同是这里< ,上面 >
Student temp = all[j];
all[j] = all[j + 1];
all[j + 1] = temp;
}
}
}
}
}
//按照高数课成绩排序(false:降序,true:升序,默认降序)
void Management::sortByScore2(bool tag) {
//采用冒泡排序算法(代码和上面的函数一样,只是把score1改成score2而已)
if (tag) {
//升序
for (int i = 0; i < size; i++) {
for (int j = 0; j < size - i - 1; ++j) {
if (all[j].score2 > all[j + 1].score2) {
Student temp = all[j];
all[j] = all[j + 1];
all[j + 1] = temp;
}
}
}
}
else {
//降序
for (int i = 0; i < size; i++) {
for (int j = 0; j < size - i - 1; ++j) {
if (all[j].score2 < all[j + 1].score2) {//和上面的不同是这里< ,上面 >
Student temp = all[j];
all[j] = all[j + 1];
all[j + 1] = temp;
}
}
}
}
}
//按学号排序(false:降序,true:升序,默认降序)
void Management::sortById(bool tag)
{
//采用冒泡排序算法(代码和按程序课排序的函数一样,只是把score1改成m_id而已)
if (tag) {
//升序
for (int i = 0; i < size; i++) {
for (int j = 0; j < size - i - 1; ++j) {
if (all[j].m_id > all[j + 1].m_id) {
Student temp = all[j];
all[j] = all[j + 1];
all[j + 1] = temp;
}
}
}
}
else {
//降序
for (int i = 0; i < size; i++) {
for (int j = 0; j < size - i - 1; ++j) {
if (all[j].m_id < all[j + 1].m_id) {//和上面的不同是这里< ,上面 >
Student temp = all[j];
all[j] = all[j + 1];
all[j + 1] = temp;
}
}
}
}
}
CString Management::getName(CString id) {
for (int i = 0; i < size; i++) {
if (id == all[i].m_id)
return all[i].m_name;
}
return TEXT("");
}
//获取程序课成绩
int Management::getS1(CString id) {
for (int i = 0; i < size; i++) {
if (id == all[i].m_id)
return all[i].score1;
}
return 0;
}
//获取高数课成绩
int Management::getS2(CString id) {
for (int i = 0; i < size; i++) {
if (id == all[i].m_id)
return all[i].score2;
}
return 0;
}
//修改信息(学号不存在返回false)
bool Management::changeData(CString oldId, CString newName, CString newId, int S1, int S2) const {
for (int i = 0; i < size; i++) {
if (oldId == all[i].m_id) {
all[i].m_name = newName;
all[i].m_id = newId;
all[i].score1 = S1;
all[i].score2 = S2;
return true;
}
}
return false;
}
//构造函数
Management::Management() {
all = new Student[INIT_SIZE]; //初始化数组
capacity = INIT_SIZE; //更新容量
//读取文件
ifstream ifs("data.txt", ios::in);
if (ifs.is_open() && ifs.peek() != EOF) {
//确保文件已经打开,并且不是空
while (ifs.peek() != EOF) {
//一直读到结尾为止
string name, id;
int score1, score2;
ifs >> name >> id >> score1 >> score2; //逐个读取数据
CString m_name(name.c_str());
CString m_id(id.c_str());
Student stu(m_name, m_id, score1, score2); //新建一个Student
add(stu); //将Student添加到数组
}
}
ifs.close(); //释放资源
}
//析构
Management::~Management() {
//保存文件
ofstream ofs("data.txt", ios::out);
if (ofs.is_open()) {
for (int i = 0; i < size; i++) {
Student stu = all[i];
string name(CT2A(all[i].m_name.GetBuffer())); //将CString转为string
string id(CT2A(all[i].m_id.GetBuffer()));
ofs << name << " " << id << " " << stu.score1 << " " << stu.score2;
if (i != size - 1) ofs << endl;
//最后一个数据之后不要换行,否则下次读取的时候会读取多一条空数据
}
}
ofs.close();
delete[] all;
}
vector<int> Management::getMathScores() const {
vector<int> datas;
for (int i = 0; i < size; i++) {
datas.push_back(all[i].score2);
}
return datas;
}
vector<int> Management::getProgrammingScores() const {
vector<int> datas;
for (int i = 0; i < size; i++) {
datas.push_back(all[i].score1);
}
return datas;
}
ADD.h(对话框类)
#pragma once
// ADD 对话框
class ADD : public CDialogEx
{
DECLARE_DYNAMIC(ADD)
public:
ADD(CWnd* pParent = nullptr); // 标准构造函数
virtual ~ADD();
// 对话框数据
#ifdef AFX_DESIGN_TIME
enum { IDD = ADD_STUDENT };
#endif
protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV 支持
DECLARE_MESSAGE_MAP()
public:
CString m_name;
CString m_id;
int score1;
int score2;
afx_msg void OnBnClickedButton1();
CListBox DATA;
afx_msg void OnBnClickedButton6();
afx_msg void OnBnClickedButton2();
afx_msg void OnLbnSelchangeList1();
afx_msg void OnBnClickedButton3();
afx_msg void OnBnClickedButton4();
afx_msg void OnBnClickedButton5();
afx_msg void OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized);
afx_msg void OnBnClickedButton7();
afx_msg void OnBnClickedButton8();
afx_msg void OnBnClickedButton9();
afx_msg void OnBnClickedButton10();
afx_msg void OnBnClickedButton11();
afx_msg void OnBnClickedButton12();
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
int Search_Score1_Min;
int Search_Score1_Max;
int Search_Score2_Min;
int Search_Score2_Max;
CString Search_Name;
CString Search_Id;
afx_msg void OnBnClickedButton13();
};
ADD.cpp
// ADD.cpp: 实现文件
//
#include "pch.h"
#include "StudentManagerSystem.h"
#include "afxdialogex.h"
#include "ADD.h"
#include "ChildView.h"
#include<ctime>
#include <vector>
Management m; //创建Management对象
// ADD 对话框
IMPLEMENT_DYNAMIC(ADD, CDialogEx)
ADD::ADD(CWnd* pParent /*=nullptr*/)
: CDialogEx(ADD_STUDENT, pParent)
, m_name(_T(""))
, m_id(_T(""))
, score1(60)
, score2(60), Search_Score1_Min(0)
, Search_Score1_Max(100)
, Search_Score2_Min(0)
, Search_Score2_Max(100)
, Search_Name(_T(""))
, Search_Id(_T("")) {
}
ADD::~ADD() = default;
void ADD::DoDataExchange(CDataExchange* pDX) {
CDialogEx::DoDataExchange(pDX);
DDX_Text(pDX, IDC_EDIT1, m_name);
DDX_Text(pDX, IDC_EDIT2, m_id);
DDX_Text(pDX, IDC_EDIT3, score1);
DDX_Text(pDX, IDC_EDIT4, score2);
DDX_Control(pDX, IDC_LIST1, DATA);
DDX_Text(pDX, IDC_EDIT7, Search_Score1_Min);
DDX_Text(pDX, IDC_EDIT8, Search_Score1_Max);
DDX_Text(pDX, IDC_EDIT9, Search_Score2_Min);
DDX_Text(pDX, IDC_EDIT10, Search_Score2_Max);
DDX_Text(pDX, IDC_EDIT5, Search_Name);
DDX_Text(pDX, IDC_EDIT6, Search_Id);
}
BEGIN_MESSAGE_MAP(ADD, CDialogEx)
ON_BN_CLICKED(IDC_BUTTON1, &ADD::OnBnClickedButton1)
ON_BN_CLICKED(IDC_BUTTON6, &ADD::OnBnClickedButton6)
ON_BN_CLICKED(IDC_BUTTON2, &ADD::OnBnClickedButton2)
ON_LBN_SELCHANGE(IDC_LIST1, &ADD::OnLbnSelchangeList1)
ON_BN_CLICKED(IDC_BUTTON3, &ADD::OnBnClickedButton3)
ON_BN_CLICKED(IDC_BUTTON4, &ADD::OnBnClickedButton4)
ON_BN_CLICKED(IDC_BUTTON5, &ADD::OnBnClickedButton5)
ON_WM_ACTIVATE()
ON_BN_CLICKED(IDC_BUTTON7, &ADD::OnBnClickedButton7)
ON_BN_CLICKED(IDC_BUTTON8, &ADD::OnBnClickedButton8)
ON_BN_CLICKED(IDC_BUTTON9, &ADD::OnBnClickedButton9)
ON_BN_CLICKED(IDC_BUTTON10, &ADD::OnBnClickedButton10)
ON_BN_CLICKED(IDC_BUTTON11, &ADD::OnBnClickedButton11)
ON_BN_CLICKED(IDC_BUTTON12, &ADD::OnBnClickedButton12)
ON_WM_CREATE()
ON_BN_CLICKED(IDC_BUTTON13, &ADD::OnBnClickedButton13)
END_MESSAGE_MAP()
// ADD 消息处理程序
// 添加按钮
void ADD::OnBnClickedButton1() {
//判断输入的内容是否为空
//我这样做的原因是:如果先UpdateData,那数字输入框如果为空,会弹窗
CString cstr1;
CString cstr2;
CString cstr3;
CString cstr4;
GetDlgItem(IDC_EDIT1)->GetWindowText(cstr1);
GetDlgItem(IDC_EDIT2)->GetWindowText(cstr2);
GetDlgItem(IDC_EDIT3)->GetWindowText(cstr3);
GetDlgItem(IDC_EDIT4)->GetWindowText(cstr4);
if (cstr1 == _T("") || cstr2 == _T("") || cstr3 == _T("") || cstr4 == _T("")) {
MessageBox(TEXT("输入的内容不能为空!"));
return;
}
//==================================================================
UpdateData(TRUE); //将控件中的数据更新到变量
//==================================================================
//判断输入的内容是否包含空格
int a = m_name.Find(TEXT(" ")); //m_name空格所在的索引
int b = m_id.Find(TEXT(" ")); //查找空格所在的索引
if (a != -1 || b != -1) {
//如果索引不是-1,代表输入的东西一定包含空格
MessageBox(TEXT("输入的内容请不要包含空格!"));
return;
}
//==================================================================
//判断学号是否全为数字,并且长度为10
if (m_id.GetLength() == 10) {
//如果长度为10,再判断是否每一位都是数字
for (int i = 0; i < 10; ++i) {
if (!(m_id.GetAt(i) >= '0' && m_id.GetAt(i) <= '9')) {
//只要碰到一个字符不是数字,添加失败
MessageBox(TEXT("学号必须由数字组成"));
return;
}
}
}
else {
//如果长度不为10,添加失败
MessageBox(TEXT("学号的长度必须为10"));
return;
}
//==================================================================
//判断成绩是否符合范围
if (score1 < 0 || score1 > 100 || score2 < 0 || score2 > 100) {
MessageBox(TEXT("分数必须在0到100分之间"));
return;
}
//==================================================================
//判断要添加的学号是否存在
if (m.hasId(m_id)) {
MessageBox(TEXT("该学号已重复"));
return;
}
//==================================================================
//以上条件都满足后,添加成功
Student stu(m_name, m_id, score1, score2);
m.add(stu);
DATA.AddString(m_id + "==" + m_name); //让ListBox添加一条数据
MessageBox(TEXT("添加成功"));
DATA.SetCurSel(DATA.GetCount() - 1); //添加之后让ListBox选中刚刚添加的数据
OnLbnSelchangeList1(); //手动触发ListBox的Change事件
}
// 删除按钮
void ADD::OnBnClickedButton2() {
CString id;
int i = DATA.GetCurSel(); //获取ListBox正在选中的索引
if (i != -1) {
DATA.GetText(i, id);
const int index = id.Find(_T("==")); //‘==’在字符串里的位置
id = id.Mid(0, index); //列表里显示的格式为 学号==姓名,读取==前面的,就是学号
if (m.del(id) /*删除数据,如果学号不存在将返回false*/) {
DATA.DeleteString(i); //列表删除对应的项
MessageBox(TEXT("删除成功"));
DATA.SetCurSel(i == 0 ? 0 : i - 1); //手动让ListBox选中被删除的前面一项
OnLbnSelchangeList1(); //手动触发ListBox的Change事件
}
else {
MessageBox(TEXT("该学号不存在!可能是由于数据未更新所致,请刷新列表!"));
}
}
else {
//-1代表没选中
MessageBox(TEXT("当前没有选中任何学生,请先在左边列表中选择!"));
}
}
/**
* ListBox的Change事件
* 功能:当列表框选中某些项的时候,右边显示这个学生的信息
*/
void ADD::OnLbnSelchangeList1() {
// TODO: 在此添加控件通知处理程序代码
int i = DATA.GetCurSel(); //获取当前ListBox选中的索引
if (i != -1) {
CString id;
DATA.GetText(i, id);
i = id.Find(_T("=="));
id = id.Mid(0, i); //列表里显示的格式为 学号==姓名,读取==前面的,就是学号
if (m.hasId(id)) {
m_name = m.getName(id);
m_id = id;
score1 = m.getS1(id);
score2 = m.getS2(id);
UpdateData(FALSE); //将变量的值同步到控件
}
else {
MessageBox(TEXT("该学号不存在!可能是由于数据未更新所致,请刷新列表!"));
}
}
}
//编辑按钮
void ADD::OnBnClickedButton3() {
//获取ListBox当前选中的内容,以获取学号信息
CString id; //学号
int i = DATA.GetCurSel(); //当前列表框选中的索引
if (i == -1) {
MessageBox(TEXT("当前没有选中任何学生,请先在左边列表中选择!"));
return;
}
DATA.GetText(i, id);
const int index = id.Find(_T("==")); //‘==’在字符串里的位置
id = id.Mid(0, index); //列表里显示的格式为 学号==姓名,读取==前面的,就是学号
//==================================================================
//检查空格和学号格式是否正确,这部分和添加数据里面的代码是一样的。
//==================================================================
//判断输入的内容是否为空
//我这样做的原因是:如果先UpdateData,那数字输入框如果为空,会弹窗
CString cstr1;
CString cstr2;
CString cstr3;
CString cstr4;
GetDlgItem(IDC_EDIT1)->GetWindowText(cstr1);
GetDlgItem(IDC_EDIT2)->GetWindowText(cstr2);
GetDlgItem(IDC_EDIT3)->GetWindowText(cstr3);
GetDlgItem(IDC_EDIT4)->GetWindowText(cstr4);
if (cstr1 == _T("") || cstr2 == _T("") || cstr3 == _T("") || cstr4 == _T("")) {
MessageBox(TEXT("输入的内容不能为空!"));
return;
}
//==================================================================
UpdateData(TRUE); //将控件中的数据更新到变量
//==================================================================
//判断输入的内容是否包含空格
int a = m_name.Find(TEXT(" ")); //m_name空格所在的索引
int b = m_id.Find(TEXT(" ")); //查找空格所在的索引
if (a != -1 || b != -1) {
//如果索引不是-1,代表输入的东西一定包含空格
MessageBox(TEXT("输入的内容请不要包含空格!"));
return;
}
//==================================================================
//判断学号是否全为数字,并且长度为10
if (m_id.GetLength() == 10) {
//如果长度为10,再判断是否每一位都是数字
for (int i = 0; i < 10; ++i) {
if (!(m_id.GetAt(i) >= '0' && m_id.GetAt(i) <= '9')) {
//只要碰到一个字符不是数字,添加失败
MessageBox(TEXT("学号必须由数字组成"));
return;
}
}
}
else {
//如果长度不为10,添加失败
MessageBox(TEXT("学号的长度必须为10"));
return;
}
//==================================================================
//判断成绩是否符合范围
if (score1 < 0 || score1 > 100 || score2 < 0 || score2 > 100) {
MessageBox(TEXT("分数必须在0到100分之间"));
return;
}
//==================================================================
//判断是否需要修改学号
//不需要:m_id和当前的学号一致,不作修改
//需要:m_id和当前的学号不一致,下一步还需判断修改后的学号是否重复
if (m_id != id && m.hasId(m_id)) {
//如果m_id != id,代表希望修改学号,但是修改学号需要判断学号有没有重复
MessageBox(TEXT("该学号已重复"));
return;
}
//===================================================================
//以上条件都满足之后,
//1.原来的学号存在:修改成功
//2.原来的学号不存在:修改失败
if (m.changeData(id, m_name, m_id, score1, score2)) {
//修改数据,如果学号不存在将返回false
OnBnClickedButton6(); //手动触发“刷新列表”的单击事件
MessageBox(TEXT("修改成功"));
//刷新列表之后,ListBox又会变为不选中状态
//自动选回刚刚选中的那个
for (int j = 0; j < DATA.GetCount(); j++) {
CString cs;
DATA.GetText(j, cs);
if (cs.Mid(0, cs.Find(_T("=="))) == m_id) {
DATA.SetCurSel(j);
break;
}
}
}
else {
MessageBox(TEXT("该学号不存在!可能是由于数据未更新所致,请刷新列表!"));
}
}
//按程序课降序按钮
void ADD::OnBnClickedButton4() {
m.sortByScore1(); //排序
CString id;
int i = DATA.GetCurSel(); //记住排序之前选中的索引
OnBnClickedButton6(); //刷新列表
//刷新列表之后,ListBox又会变为不选中状态
//需要自动选回刚刚选中的那个(如果i==-1,代表原来并没有选择)
if (i != -1) {
DATA.GetText(i, id);
for (int j = 0; j < DATA.GetCount(); j++) {
CString cs;
DATA.GetText(j, cs);
if (cs.Mid(0, cs.Find(_T("=="))) == m_id) {
DATA.SetCurSel(j);
break;
}
}
}
OnLbnSelchangeList1(); //触发ListBox的事件,让右边能显示选中的学生的个人信息
}
//按高数课降序按钮(这部分代码和按程序课降序的一样,唯一不同的地方是第一行的排序函数不同)
void ADD::OnBnClickedButton5() {
m.sortByScore2();
CString id;
int i = DATA.GetCurSel();
OnBnClickedButton6();
if (i != -1) {
DATA.GetText(i, id);
for (int j = 0; j < DATA.GetCount(); j++) {
CString cs;
DATA.GetText(j, cs);
if (cs.Mid(0, cs.Find(_T("=="))) == m_id) {
DATA.SetCurSel(j);
break;
}
}
}
OnLbnSelchangeList1();
}
//刷新列表按钮
void ADD::OnBnClickedButton6() {
DATA.ResetContent(); //清空列表
for (int i = 0; i < m.size; i++) {
CString str;
str = m.all[i].m_id;
str += "==";
str += m.all[i].m_name;
DATA.AddString(str); //读取数组,逐个插入数据
}
}
//窗口激活消息(显示窗口后刷新列表)
void ADD::OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized) {
CDialogEx::OnActivate(nState, pWndOther, bMinimized);
// TODO: 在此处添加消息处理程序代码
srand((unsigned long)time(NULL)); //随机数种子
OnBnClickedButton6();
}
//按程序课升序(这部分代码和降序的一样,唯一不同的地方是第一行的排序函数给了true参数)
void ADD::OnBnClickedButton7() {
m.sortByScore1(true); //排序
CString id;
int i = DATA.GetCurSel(); //记住排序之前选中的索引
OnBnClickedButton6(); //刷新列表
//刷新列表之后,ListBox又会变为不选中状态
//自动选回刚刚选中的那个(如果i==-1,代表原来并没有选择)
if (i != -1) {
DATA.GetText(i, id);
for (int j = 0; j < DATA.GetCount(); j++) {
CString cs;
DATA.GetText(j, cs);
if (cs.Mid(0, cs.Find(_T("=="))) == m_id) {
DATA.SetCurSel(j);
break;
}
}
}
OnLbnSelchangeList1(); //触发ListBox的事件,让右边能显示选中的学生的个人信息
}
//按高数课降序(这部分代码和降序的一样,唯一不同的地方是第一行的排序函数给了true参数)
void ADD::OnBnClickedButton8() {
m.sortByScore2(true);
CString id;
int i = DATA.GetCurSel();
OnBnClickedButton6();
if (i != -1) {
DATA.GetText(i, id);
for (int j = 0; j < DATA.GetCount(); j++) {
CString cs;
DATA.GetText(j, cs);
if (cs.Mid(0, cs.Find(_T("=="))) == m_id) {
DATA.SetCurSel(j);
break;
}
}
}
OnLbnSelchangeList1();
}
//按学号降序(和上面的排序是一样的)
void ADD::OnBnClickedButton9() {
m.sortById(); //排序
CString id;
int i = DATA.GetCurSel(); //记住排序之前选中的索引
OnBnClickedButton6(); //刷新列表
//刷新列表之后,ListBox又会变为不选中状态
//自动选回刚刚选中的那个(如果i==-1,代表原来并没有选择)
if (i != -1) {
DATA.GetText(i, id);
for (int j = 0; j < DATA.GetCount(); j++) {
CString cs;
DATA.GetText(j, cs);
if (cs.Mid(0, cs.Find(_T("=="))) == m_id) {
DATA.SetCurSel(j);
break;
}
}
}
OnLbnSelchangeList1(); //触发ListBox的事件,让右边能显示选中的学生的个人信息
}
//按学号升序
void ADD::OnBnClickedButton10() {
m.sortById(true); //排序
CString id;
int i = DATA.GetCurSel(); //记住排序之前选中的索引
OnBnClickedButton6(); //刷新列表
//刷新列表之后,ListBox又会变为不选中状态
//自动选回刚刚选中的那个(如果i==-1,代表原来并没有选择)
if (i != -1) {
DATA.GetText(i, id);
for (int j = 0; j < DATA.GetCount(); j++) {
CString cs;
DATA.GetText(j, cs);
if (cs.Mid(0, cs.Find(_T("=="))) == m_id) {
DATA.SetCurSel(j);
break;
}
}
}
OnLbnSelchangeList1(); //触发ListBox的事件,让右边能显示选中的学生的个人信息
}
//面向对象分数随机
void ADD::OnBnClickedButton11() {
UpdateData(TRUE);
score1 = rand() % 101; //0-100之间的随机数
UpdateData(FALSE); //数据更新到控件
}
//高数分数随机
void ADD::OnBnClickedButton12() {
UpdateData(TRUE);
score2 = rand() % 101; //0-100之间的随机数
UpdateData(FALSE); //数据更新到控件
}
int ADD::OnCreate(LPCREATESTRUCT lpCreateStruct) {
if (CDialogEx::OnCreate(lpCreateStruct) == -1)
return -1;
// TODO: 在此添加您专用的创建代码
return 0;
}
//查询按钮
void ADD::OnBnClickedButton13() {
//判断输入的成绩范围是不是为空的
CString cstr1;
CString cstr2;
CString cstr3;
CString cstr4;
GetDlgItem(IDC_EDIT7)->GetWindowText(cstr1);
GetDlgItem(IDC_EDIT8)->GetWindowText(cstr2);
GetDlgItem(IDC_EDIT9)->GetWindowText(cstr3);
GetDlgItem(IDC_EDIT10)->GetWindowText(cstr4);
if (cstr1 == _T("") || cstr2 == _T("") || cstr3 == _T("") || cstr4 == _T("")) {
MessageBox(TEXT("输入的成绩的范围不能为空!"));
return;
}
//==============================================================
UpdateData(TRUE);
//进行数据的合法性判断
if (Search_Score1_Max > 100 || Search_Score1_Max < 0 || Search_Score1_Min > 100 || Search_Score1_Min < 0 ||
Search_Score2_Max > 100 || Search_Score2_Max < 0 || Search_Score2_Min > 100 || Search_Score2_Min < 0) {
MessageBox(TEXT("输入的成绩的范围必须在区间[0,100]内!"));
return;
}
if (Search_Score1_Min > Search_Score1_Max || Search_Score2_Min > Search_Score2_Max) {
MessageBox(TEXT("输入的成绩最小值不能比最大值大!"));
return;
}
//===============================================================
//开始查询
std::vector<Student> result;
for (int i = 0; i < m.size; ++i) {
Student stu = m.all[i];
if (stu.m_name.Find(Search_Name) != -1 &&
stu.m_id.Find(Search_Id) != -1 &&
stu.score1 >= Search_Score1_Min &&
stu.score1 <= Search_Score1_Max &&
stu.score2 >= Search_Score2_Min &&
stu.score2 <= Search_Score2_Max) {
//满足条件
result.push_back(stu);
}
}
CString str;
for (auto stu : result) {
CString cScore1;
CString cScore2;
cScore1.Format(_T("%d"), stu.score1);
cScore2.Format(_T("%d"), stu.score2);
str += _T("学生姓名:") + stu.m_name + _T("\t学号:") + stu.m_id + _T("\t程序课成绩:") + cScore1 + _T("\t高数课成绩:") + cScore2 + _T("\n");
}
CString cLength;
cLength.Format(_T("%d"), result.size());
MessageBox(str,_T("共查询到") + cLength + _T("条结果"));
}
ChildView.h
// ChildView.h: CChildView 类的接口
//
#pragma once
#include "ADD.h"
#include "Management.h"
#include<vector>
using namespace std;
// CChildView 窗口
class CChildView : public CWnd
{
// 构造
public:
CChildView();
ADD add;
// 特性
public:
// 操作
public:
// 重写
protected:
virtual BOOL PreCreateWindow(CREATESTRUCT& cs);
// 实现
public:
virtual ~CChildView();
// 生成的消息映射函数
protected:
afx_msg void OnPaint();
DECLARE_MESSAGE_MAP()
public:
afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
void DrawBar(CDC *dc, std::vector<int> db,bool left = true) const;
void DrawLine(CDC *dc, std::vector<int> db,bool left = true) const;
void OnDraw(CDC* pDC);
};
ChildView.cpp
// ChildView.cpp: CChildView 类的实现
//
#include "pch.h"
#include "framework.h"
#include "ChildView.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
extern Management m; //需要和ADD.cpp共用一个Management对象,所以这里要声明extern
// CChildView
CChildView::CChildView() {
}
CChildView::~CChildView() {
}
BEGIN_MESSAGE_MAP(CChildView, CWnd)
ON_WM_PAINT()
ON_WM_LBUTTONDOWN()
ON_WM_DRAWITEM()
ON_WM_DRAWITEM()
END_MESSAGE_MAP()
// CChildView 消息处理程序
BOOL CChildView::PreCreateWindow(CREATESTRUCT& cs) {
if (!CWnd::PreCreateWindow(cs))
return FALSE;
cs.dwExStyle |= WS_EX_CLIENTEDGE;
cs.style &= ~WS_BORDER;
cs.lpszClass = AfxRegisterWndClass(CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS,
::LoadCursor(nullptr, IDC_ARROW), reinterpret_cast<HBRUSH>(COLOR_WINDOW + 1),
nullptr);
return TRUE;
}
void CChildView::OnPaint() {
CPaintDC dc(this); // 用于绘制的设备上下文
// TODO: 在此处添加消息处理程序代码
// 不要为绘制消息而调用 CWnd::OnPaint()
OnDraw(&dc);
}
//画条状图,db:vector类的集合,里面存放所有要统计的数据
void CChildView::DrawBar(CDC* dc, std::vector<int> db,bool left) const {//left:是否在左边
CRect rc; //外面的矩形
GetClientRect(rc); //获取CWnd客户区的尺寸。(让rc占满窗口)
rc.DeflateRect(30, 15, 10, rc.Height() / 2+15); //x1,y1,x2,y2
if(left) {
//如果是左边,相应修改一下x2的值
rc.right = rc.Width() / 2 - 15;
dc->TextOutW(rc.left, 0, TEXT("高数课成绩统计"));
}else {
rc.left = rc.Width() / 2 + 15;
dc->TextOutW(rc.left, 0, TEXT("程序课成绩统计"));
}
CBrush brush1(HS_BDIAGONAL, RGB(96, 192, 255)); //向下阴影(从左到右)在45度(右斜),蓝色
CBrush brush2(HS_FDIAGONAL, RGB(96, 192, 255)); //向上阴影(从左到右)在45度(左斜),蓝色
CPen pen(PS_INSIDEFRAME, 2, RGB(96, 192, 255)); //
int n = 5; //定义有五个区间
int width = rc.Width() / n; //一个区间的宽度
int s[] = {0, 0, 0, 0, 0}; //各区间人数
//统计各区间人数
for (int i = 0; i < db.size(); i++) {
if (db[i] < 60) s[0] ++;
else if (db[i] >= 60 && db[i] < 70) s[1]++;
else if (db[i] >= 70 && db[i] < 80) s[2]++;
else if (db[i] >= 80 && db[i] < 90) s[3]++;
else s[4]++;
}
//找出人数最大的区间
int max_s = s[0];
for (int i = 0; i < n; i++)
if (max_s < s[i]) max_s = s[i];
int per_Height = rc.Height() / max_s; //算出一个人代表的矩形高度
CRect ps_rect(rc); //直方图的矩形
ps_rect.right = ps_rect.left + width;
dc->SelectObject(&pen); //选择画笔
CString str[5] = {_T("<60"),_T("60-70"),_T("70-80"),_T("80-90"),_T(">=90")}; //将每个区间的注释先存储
for (int i = 0; i < n; i++) {
ps_rect.top = ps_rect.bottom - per_Height * s[i]; //该区间的高度
if (i % 2) dc->SelectObject(&brush2);
else dc->SelectObject(&brush1); //每隔一个区间用不同的斜线,左斜或右斜
dc->Rectangle(ps_rect); //绘制矩形
if (s[i] > 0) {
CString str1;
str1.Format(_T("%d人"), s[i]); //设置str1的内容
dc->DrawText(str1, ps_rect, DT_CENTER | DT_VCENTER | DT_SINGLELINE); //在矩形中间显示人数
}
//输出注释在矩形下面
dc->TextOutW((ps_rect.left + ps_rect.right) / 2 - 15, ps_rect.bottom+5 , str[i]);
ps_rect.OffsetRect(width, 0); //移动矩形框的位置,水平方向移动width
}
}
//折线图
void CChildView::DrawLine(CDC* dc, std::vector<int> db,bool left) const {
CRect rc; //最外面的矩形
GetClientRect(rc); //让矩形占满窗口
rc.DeflateRect(30, rc.Height() / 2 + 15, 10, 15); x1,y1,x2,y2
if (left) {
//如果是左边,相应修改一下x2的值
rc.right = rc.Width() / 2 - 15;
}
else {
rc.left = rc.Width() / 2 + 15;
}
int gridXnums = db.size(); //列数
int gridYnums = 10; //行数
int dx = rc.Width() / gridXnums; //每一个格子的宽度
int dy = rc.Height() / gridYnums; //每一个格子的高度
CRect gridRect(rc.left, rc.top, rc.left + dx * gridXnums, rc.top + dy * gridYnums);
CPen gridPen(PS_DASHDOTDOT, 1, RGB(96, 192, 255)); //获取蓝色的点状画笔
dc->SelectObject(&gridPen); //选择画笔
//-------------------------------------------------------------------
//画格子
for (int i = 0; i <= gridXnums; i++) {
dc->MoveTo(gridRect.left + i * dx, gridRect.bottom);
dc->LineTo(gridRect.left + i * dx, gridRect.top);
}
for (int j = 0; j <= gridYnums; j++) {
dc->MoveTo(gridRect.left, gridRect.top + j * dy);
dc->LineTo(gridRect.right, gridRect.top + j * dy);
CString str;
str.Format(_T("%d"), 100 - 10 * j);
dc->TextOutW(rc.left - 25, gridRect.top + j * dy - 10, str); //标注纵坐标的值
}
//-------------------------------------------------------------------
gridPen.Detach(); //官方解释:从CGdiObject对象分离Windows GDI对象,并将句柄返回给Windows GDI对象。
//但是不加这一行会报异常
CPen linePen(PS_SOLID, 2, RGB(255, 0, 0)); //创建一个红色实线画笔,用于画线
dc->SelectObject(&linePen); //选择画笔
int nCount = db.size(); //一共多少个数据
int deta = gridRect.Width() / nCount; //每一个数据所占的宽度
POINT* pt = new POINT[nCount];
for (int i = 0; i < nCount; i++) {
pt[i].x = gridRect.left + i * deta;
pt[i].y = gridRect.bottom - (int)(db[i] / 100.0 * gridRect.Height());
CString score;
score.Format(_T("%d"), db[i]); //设置score的内容
// dc->TextOutW(pt[i].x - 5, gridRect.bottom + 5, score); //显示分数(在下面)
}
dc->Polyline(pt, nCount); //画线
delete[] pt;
pt = nullptr;
}
void CChildView::OnDraw(CDC* pDC) {
vector<int> programmingScores = m.getProgrammingScores();
vector<int> mathScores = m.getMathScores();
if (!mathScores.empty() && !programmingScores.empty()) {
//只有数据不为空了才去画图
DrawBar(pDC, mathScores,true); //左边条形图画高数
DrawBar(pDC, programmingScores,false); //右边条形图画程序
DrawLine(pDC, mathScores,true); //左边折线图画高数
DrawLine(pDC, programmingScores,false); //右边折线图画程序
}
}
//左
void CChildView::OnLButtonDown(UINT nFlags, CPoint point) {
CWnd::OnLButtonDown(nFlags, point);
//弹出对话框
add.DoModal();
//重绘统计图
Invalidate(); // 重画窗口。
OnPaint();
}
五、程序运行时的效果图
- 统计图
-
数据管理
-
添加学生时,姓名、学号或成绩为空
-
添加学生时,学号不是数字或者长度不为10
-
如果姓名或学号输入了空格
-
输入的成绩越界
-
成功添加数据
-
成功删除数据
-
成功修改数据(容错条件和添加数据一样,不再演示)
-
按成绩排序(从折线图看效果)
-
数据查询,查询全部
-
数据查询,查询高数课和程序课成绩都大于80分的
-
数据查询,查询姓名里带有“刘”字的
-
查询学号里带有2的
六、实验结果分析
我在实验中遇到的问题和解决方法
- 对话框初始化时程序崩溃,如下图:
本来我是打算在对话框一打开就自动加载数据,而不需要手动加载数据,所以我在对话框的OnCreate()函数里进行列表的初始化操作,才会发生这样子的程序奔溃,经过我后面的尝试,把初始化代码放到OnActivate()函数里才解决问题。问题产生的原因我不太清楚,我个人猜测是OnCreate函数在窗口还没完全显示的时候执行,而OnActivate会在窗口显示完成之后才执行,应该是因为窗口还没显示的时候把数据添加到列表导致的问题。
-
Management对象的问题
因为我所有的学生数据都存放在Management里的all数组里,所以我需要在对话框ADD类和单文档绘图的View类里共用一个Management对象,一开始我是把对象创建在.h文件中,结果造成了一些错误(无法解析的外部符号),于是我按照网上的办法,把对象放在.cpp文件中。但是新的问题出现了,我本以为多个cpp文件如果共用一个变量,只需在一个.cpp中包含另一个.cpp就行了,结果我在一个debug中,发现Management对象竟然创建了5次,这时我才意识到多个cpp中的Management并不是共用的,后来我的解决办法是在View.cpp中的Management加extern修饰符。 -
数据文件保存和读取问题
我在Management类的构造和析构函数中对数据文件进行读取和保存,我每向文件输出完一条学生信息,我都会换行,但是在读取的时候会莫名地多出一条空白数据,经过我的研究,是因为输出最后一行数据再回车导致的问题。我的解决办法是在向文件输出时加个if判断,如果这条数据不是最后一条数据,则再输出回车。 -
关于成绩输入框的弹窗问题
我成绩输入框创建变量的时候,类型设置为int,当执行UpdataData函数的时候,如果里面不是数字或者为空,会弹出这样的一个框
本来这样的提示是没什么问题的,但是弹出框了之后,如果是添加数据,则照样会添加成功,只是成绩都是默认值0,但这样子就不符合逻辑了!我的解决办法是:先在把编辑框的Number属性改成True,保证了输入框无法输入数字,然后就还剩输入为空的情况,我就通过以下代码,在UpdateData之前检查输入是否为空:
收获
- 更加熟悉MFC编程,学会了在View类的绘图方法,学会了基于对话框的很多内容。
- 重新复习了上个学期所学的冒泡排序、动态数组等知识。
- 领悟了CString与string的区别,并学会了CString与string的互相转换、其他基本数据类型转换成CString。
- 掌握了面向对象的编程思想。