ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MFC+SQLite3开发学生信息管理系统实战

MFC+SQLite3开发学生信息管理系统实战

简介:这是一套基于MFC框架开发的学生信息管理系统(含学生成绩管理功能),面向计算机专业本科生及C++初学者,适用于课程设计、大作业与毕业设计参考,帮助学习者掌握Windows桌面应用开发中用户登录注册、个人信息与成绩数据的增删改查等核心业务实现。资源为82.68MB的ZIP压缩包,包含完整VS2013工程文件、可执行程序、资源图片及UI界面资源,结构清晰,便于直接编译运行与模块化学习;其中.cpp/.h源码文件支撑MFC消息映射与数据库交互逻辑,.rc资源脚本体现对话框与菜单设计规范。已有3719人学习下载,实际交付内容涵盖可运行的完整系统、分层代码组织(界面层/业务层/数据层)、典型MFC控件(ListCtrl、Edit、ComboBox)集成用法,以及配套的登录验证与SQLite轻量级数据持久化方案,是理解传统C++桌面开发流程的优质实践样本。

1. 为什么还在用 MFC 做学生信息管理系统?不是过时,而是“稳得离谱”

你可能刚在招聘网站上刷到一条要求:“熟悉 MFC,能维护/重构学生成绩管理系统”。心里一愣:MFC?那个传说中“上古框架”?不是早该被 Qt、WPF 或 Electron 取代了吗?但现实是:某高校教务处仍在用一套 2008 年上线的 MFC 系统管理全校 3 万+学生的课程与成绩;某职业院校实训平台的学生成绩录入模块,核心界面仍是CFormView搭配CListCtrl+CEdit的经典组合;甚至某省级教育信息化服务商,其部署在 200+所中职学校的本地化系统,底层数据交互层仍基于 MFC 的CDatabase封装。这不是技术怀旧,而是真实约束下的理性选择——它不依赖 .NET 运行时、不强制升级 Windows 版本、不需管理员权限安装运行库、单 exe 可直接双击启动、对老旧机房电脑(Win7 + 2G 内存)兼容性极佳。如果你正被指派接手一个“不能重写、只能迭代”的学生成绩管理模块,或需要快速交付一个离线可用、零依赖、带打印报表和 Excel 导出的桌面端系统,MFC 不是备选,而是当前最短路径。本文就带你从零搭起一个可运行、可扩展、能过验收的学生信息管理系统骨架:含学生档案增删改查、课程绑定、成绩录入与统计、SQLite 本地存储、打印预览与导出功能——所有代码基于 Visual Studio 2019/2022 + MFC + SQLite3,不调用任何第三方 UI 库,全程可复现。


2. 搭建最小可运行骨架:MFC 对话框工程 + SQLite3 集成

2.1 创建 MFC 对话框工程并禁用 Unicode(关键第一步)

MFC 默认启用 Unicode 字符集,但 SQLite3 C 接口原生使用const char*,若不统一编码,中文字段插入后查出来是乱码,且sqlite3_exec返回SQLITE_ERROR却无明确提示。常见做法是新建工程时主动降级为多字节字符集(MBCS),而非后期修改项目属性——后者易遗漏资源文件编码、字符串常量隐式转换等黑匣子问题。

提示:VS 2019/2022 新建 MFC 应用程序向导中,“应用程序类型”选“基于对话框”,“高级功能”取消勾选“Unicode 库”,“公共语言运行时支持”选“无”。这一步省掉后续 80% 的中文乱码排查时间。

创建完成后,在StdAfx.h顶部添加 SQLite3 头文件包含路径(假设已将sqlite3.h和sqlite3.lib放入项目目录libs\sqlite3\下):

// StdAfx.h #pragma once #include "targetver.h" #include <afxwin.h> #include <afxext.h> #include <afxdisp.h> #include <afxdtctl.h> #include <afxcmn.h> // SQLite3 支持 #include "..\\libs\\sqlite3\\sqlite3.h" #pragma comment(lib, "..\\libs\\sqlite3\\sqlite3.lib")

2.2 初始化 SQLite3 数据库并建表:学生、课程、成绩三张主表

我们采用单数据库文件student.db,按教育业务逻辑设计三张表。注意:MFC 中所有 SQL 语句必须用CT2A转换为 ANSI 字符串传给 SQLite3,否则sqlite3_exec会静默失败。

// 在 CStudentInfoApp::InitInstance() 中末尾添加: BOOL CStudentInfoApp::InitInstance() { // ...原有代码... // 初始化数据库 InitDatabase(); return TRUE; } void CStudentInfoApp::InitDatabase() { sqlite3* db; char* zErrMsg = 0; int rc; rc = sqlite3_open("student.db", &db); if (rc != SQLITE_OK) { AfxMessageBox(_T("无法打开数据库 student.db")); return; } // 学生表:学号(主键)、姓名、性别、出生日期、班级、联系电话 const char* sql_student = "CREATE TABLE IF NOT EXISTS students (" "id TEXT PRIMARY KEY NOT NULL," "name TEXT NOT NULL," "gender TEXT," "birth_date TEXT," "class_name TEXT," "phone TEXT" ");"; // 课程表:课程编号(主键)、课程名称、学分、授课教师 const char* sql_course = "CREATE TABLE IF NOT EXISTS courses (" "course_id TEXT PRIMARY KEY NOT NULL," "course_name TEXT NOT NULL," "credits INTEGER," "teacher TEXT" ");"; // 成绩表:学号+课程号联合主键、成绩、考试日期 const char* sql_score = "CREATE TABLE IF NOT EXISTS scores (" "student_id TEXT NOT NULL," "course_id TEXT NOT NULL," "score REAL," "exam_date TEXT," "PRIMARY KEY (student_id, course_id)," "FOREIGN KEY (student_id) REFERENCES students(id)," "FOREIGN KEY (course_id) REFERENCES courses(course_id)" ");"; // 执行建表(注意:CT2A 将 CString 转为 ANSI char*) CT2A student_sql(sql_student); rc = sqlite3_exec(db, student_sql, 0, 0, &zErrMsg); if (rc != SQLITE_OK) { AfxMessageBox(CString(_T("建学生表失败:")) + zErrMsg); sqlite3_free(zErrMsg); } CT2A course_sql(sql_course); rc = sqlite3_exec(db, course_sql, 0, 0, &zErrMsg); if (rc != SQLITE_OK) { AfxMessageBox(CString(_T("建课程表失败:")) + zErrMsg); sqlite3_free(zErrMsg); } CT2A score_sql(sql_score); rc = sqlite3_exec(db, score_sql, 0, 0, &zErrMsg); if (rc != SQLITE_OK) { AfxMessageBox(CString(_T("建成绩表失败:")) + zErrMsg); sqlite3_free(zErrMsg); } sqlite3_close(db); }

参数说明与逻辑说明:

  • sqlite3_open()第二个参数为数据库文件路径,相对路径即位于.exe同目录;
  • CREATE TABLE IF NOT EXISTS是防重复建表的关键,避免多次启动程序时报错;
  • FOREIGN KEY约束虽在 SQLite3 中默认不启用(需PRAGMA foreign_keys = ON),但此处声明可提升代码可读性与未来迁移性;
  • CT2A是 MFC 提供的字符串转换宏,将CString(多字节模式下为char*)安全转为const char*,比手动LPCTSTR强制转换更可靠;
  • 错误处理中sqlite3_free(zErrMsg)必须调用,否则内存泄漏——这是新手最容易忽略的“玄学崩溃点”。

2.3 主对话框类添加控件与数据成员:用 Class Wizard 绑定控件变量

在IDD_STUDENTINFO_DIALOG对话框资源中,拖入以下控件(布局略,重点在变量绑定):

控件类型ID名称绑定变量名类型说明
编辑框IDC_EDIT_STU_ID学号m_strStuIDCString输入学号,用于增删改查
编辑框IDC_EDIT_STU_NAME姓名m_strStuNameCString支持中文输入
组合框IDC_COMBO_GENDER性别m_comboGenderCComboBox下拉选项:男 / 女
日期控件IDC_DATETIMEPICKER_BIRTH出生日期m_dtBirthCDateTimeCtrl自动格式化为 YYYY-MM-DD
列表控件IDC_LIST_STUDENTS学生列表m_listStudentsCListCtrl显示学生档案,设置 Report 风格
按钮IDC_BTN_ADD_STU添加学生——消息映射BN_CLICKED
按钮IDC_BTN_DEL_STU删除学生——消息映射BN_CLICKED

注意:CDateTimeCtrl默认显示为“年月日”格式,但其GetTime()返回COleDateTime,需手动转为CString存入数据库。CListCtrl必须在OnInitDialog()中调用InsertColumn()设置列标题,否则显示为空白。

在CStudentInfoDlg::OnInitDialog()中初始化列表控件列:

BOOL CStudentInfoDlg::OnInitDialog() { CDialogEx::OnInitDialog(); // 设置学生列表控件列 m_listStudents.InsertColumn(0, _T("学号"), LVCFMT_LEFT, 100); m_listStudents.InsertColumn(1, _T("姓名"), LVCFMT_LEFT, 100); m_listStudents.InsertColumn(2, _T("性别"), LVCFMT_LEFT, 60); m_listStudents.InsertColumn(3, _T("出生日期"), LVCFMT_LEFT, 100); m_listStudents.InsertColumn(4, _T("班级"), LVCFMT_LEFT, 100); m_listStudents.InsertColumn(5, _T("电话"), LVCFMT_LEFT, 120); // 初始化性别下拉框 m_comboGender.AddString(_T("男")); m_comboGender.AddString(_T("女")); m_comboGender.SetCurSel(0); // 加载学生数据 LoadStudentsFromDB(); return TRUE; }

LoadStudentsFromDB()是后续章节的核心函数,此处先留空占位。至此,最小骨架已可编译运行——界面能弹出、控件可交互、数据库文件自动生成,下一步就是让数据真正流动起来。


3. 实现学生 CRUD:从界面输入到 SQLite 插入/查询/删除

3.1 添加学生:校验 + 插入 + 刷新列表

点击“添加学生”按钮,需完成三项动作:前端校验(学号非空、姓名非空)、SQL 插入、列表刷新。关键在于sqlite3_bind_*参数化查询,避免 SQL 注入与引号转义问题。

void CStudentInfoDlg::OnBnClickedBtnAddStu() { UpdateData(TRUE); // 将控件值读入成员变量 // 校验 if (m_strStuID.IsEmpty() || m_strStuName.IsEmpty()) { AfxMessageBox(_T("学号和姓名不能为空!")); return; } sqlite3* db; int rc = sqlite3_open("student.db", &db); if (rc != SQLITE_OK) { AfxMessageBox(_T("数据库打开失败")); return; } // 使用参数化 INSERT,避免 SQL 注入与引号问题 const char* sql = "INSERT INTO students (id, name, gender, birth_date, class_name, phone) VALUES (?, ?, ?, ?, ?, ?);"; sqlite3_stmt* stmt; rc = sqlite3_prepare_v2(db, sql, -1, &stmt, 0); if (rc != SQLITE_OK) { AfxMessageBox(_T("SQL 编译失败")); sqlite3_close(db); return; } // 绑定参数(索引从 1 开始) CT2A idA(m_strStuID), nameA(m_strStuName), phoneA(m_strPhone); CString strGender = (m_comboGender.GetCurSel() == 0) ? _T("男") : _T("女"); CT2A genderA(strGender); // 获取出生日期字符串(格式:YYYY-MM-DD) COleDateTime dt; m_dtBirth.GetTime(dt); CString strBirth = dt.Format(_T("%Y-%m-%d")); CT2A birthA(strBirth); // 班级从编辑框 IDC_EDIT_CLASS 获取(需提前在对话框中添加该控件并绑定 m_strClass) CT2A classA(m_strClass); sqlite3_bind_text(stmt, 1, idA, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, nameA, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 3, genderA, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 4, birthA, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 5, classA, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 6, phoneA, -1, SQLITE_STATIC); rc = sqlite3_step(stmt); if (rc != SQLITE_DONE) { AfxMessageBox(_T("添加学生失败:") + CString(sqlite3_errmsg(db))); } else { AfxMessageBox(_T("添加成功!")); // 清空输入框 m_strStuID.Empty(); m_strStuName.Empty(); m_strPhone.Empty(); m_strClass.Empty(); m_comboGender.SetCurSel(0); m_dtBirth.SetTime(COleDateTime::GetCurrentTime()); UpdateData(FALSE); // 将空值刷回控件 // 刷新列表 LoadStudentsFromDB(); } sqlite3_finalize(stmt); sqlite3_close(db); }

参数说明与逻辑说明:

  • sqlite3_prepare_v2()+sqlite3_bind_*是 SQLite3 安全插入的标准范式,比拼接字符串sprintf安全十倍;
  • CT2A转换必须在sqlite3_bind_text调用前完成,且sqlite3_bind_text第五个参数SQLITE_STATIC表示字符串内存由调用者管理(即CT2A对象生命周期覆盖 bind 全程),这是避免崩溃的关键;
  • COleDateTime::Format(_T("%Y-%m-%d"))确保日期字符串符合 SQLite3 的TEXT存储规范,避免2024/05/20这类格式导致查询失效;
  • sqlite3_finalize()必须调用,否则预编译语句句柄泄漏,多次点击后程序卡死——这是血泪经验。

3.2 查询与显示学生:用sqlite3_exec回调填充CListCtrl

LoadStudentsFromDB()函数负责从数据库读取全部学生,并填入CListCtrl。难点在于回调函数如何将每行结果映射到列表项。

struct ListCtrlData { CListCtrl* pList; int nItem; }; int callback_for_list(void* data, int argc, char** argv, char** azColName) { struct ListCtrlData* p = (struct ListCtrlData*)data; int nItem = p->nItem; // 插入新行 p->pList->InsertItem(nItem, argv[0] ? argv[0] : _T("")); p->pList->SetItemText(nItem, 1, argv[1] ? argv[1] : _T("")); p->pList->SetItemText(nItem, 2, argv[2] ? argv[2] : _T("")); p->pList->SetItemText(nItem, 3, argv[3] ? argv[3] : _T("")); p->pList->SetItemText(nItem, 4, argv[4] ? argv[4] : _T("")); p->pList->SetItemText(nItem, 5, argv[5] ? argv[5] : _T("")); p->nItem++; return 0; } void CStudentInfoDlg::LoadStudentsFromDB() { m_listStudents.DeleteAllItems(); // 清空旧数据 sqlite3* db; char* zErrMsg = 0; int rc = sqlite3_open("student.db", &db); if (rc != SQLITE_OK) return; const char* sql = "SELECT id, name, gender, birth_date, class_name, phone FROM students ORDER BY id;"; CT2A sqlA(sql); struct ListCtrlData listData; listData.pList = &m_listStudents; listData.nItem = 0; rc = sqlite3_exec(db, sqlA, callback_for_list, &listData, &zErrMsg); if (rc != SQLITE_OK) { AfxMessageBox(CString(_T("查询学生失败:")) + zErrMsg); sqlite3_free(zErrMsg); } sqlite3_close(db); }

为什么不用sqlite3_get_table?
sqlite3_get_table返回二维字符数组,需手动解析内存布局,而sqlite3_exec回调天然按行处理,与CListCtrl::InsertItem逻辑完全匹配,代码更直白、不易越界。

3.3 删除学生:基于选中行 + 外键级联检查

删除操作需谨慎:若该学生有成绩记录,直接删students表会违反外键约束(SQLite3 默认不启用 FK,但显式声明后应尊重业务逻辑)。我们采用“软删除”策略:先查scores表是否存在关联记录,有则提示,无则执行删除。

void CStudentInfoDlg::OnBnClickedBtnDelStu() { // 获取当前选中行 POSITION pos = m_listStudents.GetFirstSelectedItemPosition(); if (!pos) { AfxMessageBox(_T("请先选择要删除的学生!")); return; } int nItem = m_listStudents.GetNextSelectedItem(pos); CString strID = m_listStudents.GetItemText(nItem, 0); // 学号在第0列 sqlite3* db; int rc = sqlite3_open("student.db", &db); if (rc != SQLITE_OK) { AfxMessageBox(_T("数据库打开失败")); return; } // 检查该学生是否有成绩记录 CString sqlCheck = _T("SELECT COUNT(*) FROM scores WHERE student_id = '") + strID + _T("';"); CT2A sqlCheckA(sqlCheck); char* zErrMsg = 0; char** result; int nRow, nCol; rc = sqlite3_get_table(db, sqlCheckA, &result, &nRow, &nCol, &zErrMsg); if (rc != SQLITE_OK) { AfxMessageBox(CString(_T("检查成绩失败:")) + zErrMsg); sqlite3_free(zErrMsg); sqlite3_close(db); return; } int nScoreCount = atoi(result[1]); // result[0]是列名,result[1]是COUNT值 sqlite3_free_table(result); if (nScoreCount > 0) { if (AfxMessageBox(_T("该学生已有成绩记录,删除将导致成绩数据孤立。\n确定要删除?"), MB_YESNO) != IDYES) { sqlite3_close(db); return; } } // 执行删除 CString sqlDel = _T("DELETE FROM students WHERE id = '") + strID + _T("';"); CT2A sqlDelA(sqlDel); rc = sqlite3_exec(db, sqlDelA, 0, 0, &zErrMsg); if (rc != SQLITE_OK) { AfxMessageBox(CString(_T("删除失败:")) + zErrMsg); sqlite3_free(zErrMsg); } else { AfxMessageBox(_T("删除成功!")); LoadStudentsFromDB(); // 刷新列表 } sqlite3_close(db); }

注意:此处sqlite3_get_table用于简单 COUNT 查询,因其返回结构固定(1行1列),比回调更简洁;但生产环境建议统一用sqlite3_prepare_v2+sqlite3_step,避免sqlite3_get_table的内存分配风险。


4. 成绩管理模块:课程绑定、成绩录入与统计报表

4.1 课程下拉框动态加载:从courses表读取并绑定到CComboBox

在主对话框中添加第二个CComboBox(IDC_COMBO_COURSE),用于选择课程。需在OnInitDialog()中加载课程列表:

// 在 OnInitDialog() 末尾添加: CComboBox* pComboCourse = (CComboBox*)GetDlgItem(IDC_COMBO_COURSE); pComboCourse->ResetContent(); sqlite3* db; int rc = sqlite3_open("student.db", &db); if (rc == SQLITE_OK) { const char* sql = "SELECT course_id, course_name FROM courses ORDER BY course_id;"; CT2A sqlA(sql); char* zErrMsg = 0; char** result; int nRow, nCol; rc = sqlite3_get_table(db, sqlA, &result, &nRow, &nCol, &zErrMsg); if (rc == SQLITE_OK && nRow > 0) { for (int i = 0; i < nRow; i++) { int idx = i * nCol + 1; // course_name 在第1列(0是course_id) if (result[idx]) { pComboCourse->AddString(result[idx]); } } } sqlite3_free_table(result); sqlite3_close(db); }

4.2 成绩录入:学号自动带出 + 成绩范围校验

在对话框中添加“成绩录入”分组框,内含:

  • 学号编辑框(只读,从列表选中自动填入)
  • 课程下拉框(IDC_COMBO_COURSE)
  • 成绩编辑框(IDC_EDIT_SCORE,类型设为 Number)
  • “录入成绩”按钮(IDC_BTN_INPUT_SCORE)

关键逻辑:当用户在CListCtrl中选中学生时,自动将学号填入成绩录入区的学号框:

void CStudentInfoDlg::OnLvnItemchangedListStudents(NMHDR *pNMHDR, LRESULT *pResult) { LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR); if (pNMLV->uChanged & LVIF_STATE && (pNMLV->uNewState & LVIS_SELECTED)) { int nItem = pNMLV->iItem; CString strID = m_listStudents.GetItemText(nItem, 0); CEdit* pEditID = (CEdit*)GetDlgItem(IDC_EDIT_SCORE_STUID); pEditID->SetWindowText(strID); } *pResult = 0; }

成绩录入按钮处理:

void CStudentInfoDlg::OnBnClickedBtnInputScore() { CEdit* pEditID = (CEdit*)GetDlgItem(IDC_EDIT_SCORE_STUID); CEdit* pEditScore = (CEdit*)GetDlgItem(IDC_EDIT_SCORE); CComboBox* pComboCourse = (CComboBox*)GetDlgItem(IDC_COMBO_COURSE); CString strStuID, strScoreStr; pEditID->GetWindowText(strStuID); pEditScore->GetWindowText(strScoreStr); int nCourseSel = pComboCourse->GetCurSel(); if (nCourseSel == CB_ERR || strStuID.IsEmpty() || strScoreStr.IsEmpty()) { AfxMessageBox(_T("请先选择学生和课程,并输入成绩!")); return; } double dScore = _tstof(strScoreStr); if (dScore < 0 || dScore > 100) { AfxMessageBox(_T("成绩必须在 0~100 之间!")); return; } CString strCourseID = GetCourseIDByIndex(nCourseSel); // 自定义函数,根据下拉索引查 course_id if (strCourseID.IsEmpty()) return; // 插入成绩 sqlite3* db; int rc = sqlite3_open("student.db", &db); if (rc != SQLITE_OK) return; CString sql = _T("INSERT OR REPLACE INTO scores (student_id, course_id, score, exam_date) VALUES ('") + strStuID + _T("', '") + strCourseID + _T("', ") + strScoreStr + _T(", date('now'));"); CT2A sqlA(sql); char* zErrMsg = 0; rc = sqlite3_exec(db, sqlA, 0, 0, &zErrMsg); if (rc != SQLITE_OK) { AfxMessageBox(CString(_T("录入失败:")) + zErrMsg); sqlite3_free(zErrMsg); } else { AfxMessageBox(_T("成绩录入成功!")); pEditScore->SetWindowText(_T("")); } sqlite3_close(db); }

INSERT OR REPLACE是关键:若该学生该课程已有成绩,则更新;否则插入新记录,避免主键冲突。

4.3 成绩统计报表:按班级/课程/学生维度聚合查询

添加“统计报表”按钮(IDC_BTN_REPORT),点击后弹出模态对话框显示统计结果。我们用CListCtrl复用显示,但查询 SQL 更复杂:

void CStudentInfoDlg::OnBnClickedBtnReport() { CReportDlg dlg; // 自定义报表对话框类 dlg.DoModal(); }

CReportDlg中实现按班级统计平均分(示例):

// CReportDlg.cpp void CReportDlg::OnBnClickedBtnClassAvg() { m_listReport.DeleteAllItems(); m_listReport.InsertColumn(0, _T("班级"), LVCFMT_LEFT, 120); m_listReport.InsertColumn(1, _T("学生人数"), LVCFMT_RIGHT, 100); m_listReport.InsertColumn(2, _T("平均分"), LVCFMT_RIGHT, 100); m_listReport.InsertColumn(3, _T("最高分"), LVCFMT_RIGHT, 100); m_listReport.InsertColumn(4, _T("最低分"), LVCFMT_RIGHT, 100); sqlite3* db; int rc = sqlite3_open("student.db", &db); if (rc != SQLITE_OK) return; const char* sql = "SELECT s.class_name, COUNT(*), AVG(sc.score), MAX(sc.score), MIN(sc.score) " "FROM students s " "JOIN scores sc ON s.id = sc.student_id " "GROUP BY s.class_name " "ORDER BY AVG(sc.score) DESC;"; CT2A sqlA(sql); struct ListCtrlData listData; listData.pList = &m_listReport; listData.nItem = 0; rc = sqlite3_exec(db, sqlA, callback_for_list, &listData, 0); sqlite3_close(db); }

此 SQL 利用JOIN关联学生与成绩表,GROUP BY按班级聚合,AVG/MAX/MIN计算统计值,结果直接填入报表列表——无需中间容器,内存效率高。


5. 避坑指南:MFC + SQLite3 开发中 5 个真实翻车现场

5.1 现象:程序启动时弹出“找不到 VCRUNTIME140.dll”

原因:VS 2019/2022 默认生成动态链接的 C++ 运行时(/MD),而目标机器未安装对应 VC Redistributable。MFC 程序本身还依赖MFC140u.dll等。
解决:项目属性 → C/C++ → 代码生成 → 运行时库,改为/MT(静态链接);同时在“常规”页 → 使用 MFC → “在静态库中使用 MFC”。重新编译后,单 exe 即可运行,无需额外安装包。

5.2 现象:CListCtrl显示中文为方块或乱码

原因:对话框字体未设为支持中文的字体(如“微软雅黑”),或控件未启用 Unicode(但本方案已设为 MBCS,故更可能是字体问题)。
解决:在资源视图中右键对话框 → 属性 → 字体,设为“微软雅黑”,字号 9;对每个CEdit、CStatic、CListCtrl控件单独设置字体(右键控件 → 属性 → 字体),确保继承链完整。

5.3 现象:sqlite3_exec执行INSERT成功,但SELECT查不到数据

原因:未调用sqlite3_close()关闭数据库,或多个线程同时操作同一sqlite3*句柄导致事务未提交。SQLite3 默认开启 auto-commit,但若之前执行过BEGIN TRANSACTION未COMMIT,后续INSERT会处于挂起状态。
解决:确保每次数据库操作后调用sqlite3_close();或统一用sqlite3_prepare_v2+sqlite3_step,避免混合使用exec与prepare;调试时在 SQL 语句末尾加;并用 DB Browser for SQLite 直接执行验证。

5.4 现象:CDateTimeCtrl获取的日期COleDateTime转字符串后为“3010-01-01”

原因:未初始化控件时间,默认值为COleDateTime::null,Format()时返回异常值。
解决:在OnInitDialog()中调用m_dtBirth.SetTime(COleDateTime::GetCurrentTime())初始化;或在DoDataExchange()中为DDX_DateTimeCtrl绑定变量,由 MFC 自动管理。

5.5 现象:导出 Excel 时中文列名显示为“???”

原因:使用CFile写 CSV 文件时未写 BOM(Byte Order Mark),Excel 默认用 ANSI 打开 UTF-8 编码文件。
解决:导出前写入 UTF-8 BOM(0xEF, 0xBB, 0xBF):

CFile file(_T("report.csv"), CFile::modeCreate | CFile::modeWrite); BYTE bom[3] = {0xEF, 0xBB, 0xBF}; file.Write(bom, 3); // 后续写入 UTF-8 字符串(用 CT2UTF8 转换) CT2UTF8 utf8Str(_T("班级,姓名,成绩\n")); file.Write(utf8Str, strlen(utf8Str));

6. 进阶技巧:打印预览与 PDF 导出(不依赖 Office)

6.1 用CPrintDialog+CDC实现所见即所得打印

MFC 原生打印框架成熟稳定,无需第三方库。核心是重载OnPrint()并在OnBeginPrinting()中计算页数与内容布局。

void CStudentInfoDlg::OnFilePrint() { CPrintDialog printDlg(FALSE); printDlg.m_pd.nMinPage = printDlg.m_pd.nMaxPage = printDlg.m_pd.nFromPage = 1; printDlg.m_pd.nToPage = 1; printDlg.m_pd.hDevMode = NULL; printDlg.m_pd.hDevNames = NULL; if (printDlg.DoModal() == IDOK) { CDC dc; dc.Attach(printDlg.GetPrinterDC()); DOCINFO di = { sizeof(DOCINFO) }; di.lpszDocName = _T("学生信息打印"); dc.StartDoc(&di); dc.StartPage(); // 打印标题 CFont fontTitle; fontTitle.CreateFont(24, 0, 0, 0, FW_BOLD, 0, 0, 0, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH | FF_SWISS, _T("微软雅黑")); CFont* pOldFont = dc.SelectObject(&fontTitle); dc.TextOut(100, 100, _T("学生信息汇总表")); dc.SelectObject(pOldFont); // 打印列表内容(简化版,实际需分页) int y = 300; for (int i = 0; i < m_listStudents.GetItemCount(); i++) { CString line; for (int j = 0; j < m_listStudents.GetHeaderCtrl()->GetItemCount(); j++) { line += m_listStudents.GetItemText(i, j) + _T("\t"); } dc.TextOut(100, y, line); y += 20; } dc.EndPage(); dc.EndDoc(); dc.Detach(); } }

6.2 导出 PDF:嵌入轻量级libharu(HPDF)

libharu是纯 C 的开源 PDF 库,编译后仅 300KB,无依赖,完美适配 MFC。下载源码编译为hpdf.lib,添加头文件hpdf.h。

#include "hpdf.h" void CStudentInfoDlg::OnFileExportPdf() { HPDF_Doc pdf = HPDF_New(NULL, NULL); if (!pdf) { AfxMessageBox(_T("PDF 创建失败")); return; } HPDF_Page page = HPDF_AddPage(pdf); HPDF_Page_SetSize(page, HPDF_PAGE_SIZE_A4, HPDF_PAGE_PORTRAIT); // 设置中文字体(需提供 simsun.ttc 文件) HPDF_Font font = HPDF_GetFont(pdf, "Helvetica", NULL); HPDF_Page_SetFontAndSize(page, font, 12); // 写入标题 HPDF_Page_BeginText(page); HPDF_Page_MoveTextPos(page, 100, 800); HPDF_Page_ShowText(page, "学生信息汇总表"); HPDF_Page_EndText(page); // 写入表格数据(简化) int y = 750; for (int i = 0; i < m_listStudents.GetItemCount(); i++) { CString line; for (int j = 0; j < 6; j++) { line += m_listStudents.GetItemText(i, j) + _T(" "); } HPDF_Page_BeginText(page); HPDF_Page_MoveTextPos(page, 100, y); HPDF_Page_ShowText(page, CT2CA(line)); HPDF_Page_EndText(page); y -= 20; } // 保存 if (HPDF_SaveToFile(pdf, "student_report.pdf") != HPDF_OK) { AfxMessageBox(_T("PDF 保存失败")); } else { AfxMessageBox(_T("PDF 导出成功 <p> <a href="https://download.csdn.net/download/ambiguous__/21403389" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进