
简介这是一份基于VC6.0使用ODBC访问Access数据库的入门学习实例适合初学C/MFC数据库编程的开发者也适合需要在桌面程序中集成数据库功能的学生与工程师。压缩包内含完整的“学生信息管理系统ODBC”示例工程共278个文件以C头文件h和源文件cpp为主体便于阅读核心逻辑另有obj、sbr等编译中间文件mdb数据库文件可直接查看表结构exe可执行文件用于对照运行ico/bmp图标与工具栏位图、dsp/dsw工程配置和资源脚本则展示了一个完整MFC程序的项目组织方式整个包约4.81MB可直接用VC6.0打开工程进行编译学习。目前已有226人学习下载。实例覆盖从ODBC数据源配置到MFC封装类的完整链路先掌握ODBC的驱动管理器、数据库驱动和DSN数据源等基础概念再通过CDatabase建立连接用CRecordset完成学生信息的增删改查并可结合Access/JET驱动理解后台数据库的访问机制。对于想快速上手C数据库开发的人这是一份能边看代码边运行验证的实用参考。1. 从一份“学生信息管理系统”源码说起VC6.0 ODBC Access 的组合还值得学吗打开ODBC.rar这个包的时候我的第一反应是“这东西居然还在传”。解压出来的文件清单很直白DATABASEView.cpp、msflexgrid.cpp、msdgridctrl.cpp外加一堆 bmp 位图和DATABASE.bsc。这其实是一个典型的 VC6.0 MFC ODBC 写成的学生信息管理系统示例数据库用的是 Accessmdb界面大概率是拿 MSFlexGrid 或 DataGrid 控件来展示学生表。对一个要维护老系统、写 VC6 课程设计或者刚从 Win32 SDK 转 MFC 的开发者来说这套代码比任何教程都直观它包含了 CDatabase 连接、CRecordset 查询、控件绑定和工具栏资源刚好是“能跑、能改、能学”的完整闭环。这个场景中最容易让人卡住的不是 C 语法而是 ODBC 这条链路本身数据源 DSN 配不对、32 位和 64 位驱动互不认、Access 的 SQL 方言奇怪、中文字段乱码。所以我不打算只写一份资源介绍而是把这份源码里能复现的关键步骤和踩坑点拆开让你拿到包之后直接能跑起来并改造成自己要用的小工具。2. ODBC 访问 Access 的原理与选型为什么绕不开 DSN 和 Jet2.1 ODBC 的三层结构和 MFC 封装到底帮你省了什么ODBC 全称 Open Database Connectivity是微软早年提出的一套访问数据库的标准。它把“应用程序”和“具体的数据库引擎”隔开中间有两个关键角色驱动管理器Driver Manager和数据库驱动。应用程序只跟 ODBC API 打交道由驱动管理器把请求转发到具体驱动比如 Access 的 Microsoft Access DriverJet。ODBC 用一个叫 DSNData Source Name的命名配置来保存数据库路径、驱动类型、用户名密码。没有这个 DSN连接时就得在连接字符串里把参数全部写清楚这也是我们经常听到的“ODBC 连接字符串”最常见的用法。在 VC6.0 里微软又包了一层 MFC 类CDatabase 负责连接CRecordset 负责查询和更新。直接用 ODBC API 写你得处理 SQLAllocHandle、SQLConnect、SQLExecDirect、SQLFetch 这一整套外加大量错误码判断。MFC 把底层句柄和结果集封装之后你只需要打开数据库、拼 SQL、遍历记录集代码量和心智负担都小一个量级。对比项原始 ODBC APIMFC CDatabase/CRecordset连接方式SQLConnect 显式句柄释放CDatabase::OpenExRAII 自动关闭查询SQLExecDirect SQLBindCol SQLFetchrecordset.Open(selectSQL)MoveNext 遍历字段读取SQLGetData 类型转换GetFieldValue 或 RFX 成员变量事务SQLTransactCDatabase::BeginTrans/CommitTrans适合人群要做跨平台或底层驱动只需要在 VC6 工程里快速完成 CRUD所以用 MFC 而不是直接调 API是“够用”的做法。这份示例包里的DATABASEView.cpp就遵循这套模式视图类里声明一个 CRecordset 对象打开后往 MSFlexGrid 控件里填行。这个方案的局限也清楚它依赖 MFC不能移植到纯 Win32 或 Linux不过对于“Access 数据库 Windows 老系统”这个组合它是省事且稳定的路线。2.2 第一步配置 ODBC 数据源 DSN 并打开第一个连接我先说最常见的做法用系统 DSN。打开 ODBC 数据源管理器选择“系统 DSN”页添加 Microsoft Access Driver.mdb填入数据库路径和 DSN 名字比如StudentDSN。很多教程让你在“用户 DSN”里配但这有个坑当前登录用户换了或者服务进程用另一个账号运行用户 DSN 就不见了。所以我一般直接建系统 DSN再配合下面的OpenEx代码。在 VC6.0 工程的按钮响应函数或初始化函数里连接代码长这样// 假设这个函数在 CRecordset 的父框架里被调用 CDatabase db; CString strConn; // 方法一使用已经配好的系统 DSN strConn _T(DSNStudentDSN;UID;PWD;); BOOL bOpen db.OpenEx(strConn, CDatabase::openReadOnly); if (!bOpen) { AfxMessageBox(_T(数据库连接失败)); return; } // 方法二不使用 DSN直接给驱动和文件路径 strConn _T(Driver{Microsoft Access Driver (*.mdb)};DBQC:\\data\\student.mdb;); bOpen db.OpenEx(strConn, CDatabase::openReadOnly);这里重点解释两行代码里的参数。OpenEx的第一个参数是完整的 ODBC 连接字符串第二个是打开模式readOnly 表示只读不传就是可读写。使用 DSN 时连接字符串里DSNStudentDSN告诉驱动管理器去读哪个数据源配置UID 和 PWD 分别传数据库用户名密码。Access 老库一般没有用户密码留空即可。使用无 DSN 方式时Driver 是驱动名称DBQ 是 Access 数据库的绝对路径。这里最容易翻车的点驱动名称里的星号和空格不能随便改如果系统里装的是 ACE 驱动Office 2007 之后的 Access驱动名要换成Microsoft Access Driver (*.mdb, *.accdb)否则会提示“找不到指定的驱动”。连接成功后下一步是打开记录集。以读取学生表为例CRecordset rs(db); CString strSQL _T(SELECT 学号, 姓名, 班级 FROM student ORDER BY 学号); // 使用 snapshot 游标允许前后滚动 BOOL bRs rs.Open(CRecordset::snapshot, strSQL); if (!bRs) { AfxMessageBox(_T(查询失败检查表名和 SQL)); return; } // 用 GetFieldValue 按列下标读出来 while (!rs.IsEOF()) { CString strNo, strName; rs.GetFieldValue((short)0, strNo); rs.GetFieldValue((short)1, strName); TRACE(_T(学号%s, 姓名%s\n), strNo, strName); rs.MoveNext(); } rs.Close();CRecordset::Open的第二个参数是 SQL 字符串也可以用SetSQL再Open但直接把 SQL 传给Open更直观。游标类型这块默认 dynaset 对 Access 也可用但 snapshot 把结果集快照放在本地遍历时不会因为别处修改而跳动。GetFieldValue按列序号取出来的值是 CString它内部完成类型转换。注意一旦 SQL 里的字段名带了中文Open 本身不会报错但用GetFieldValue取出来的可能是乱码这个我在后面避坑章会单独说。真要改造这份源码时你会发现DATABASEView.cpp里通常不会像上面这样写在函数体里而是把记录集设为视图类成员变量再在OnInitialUpdate里绑定控件。但底层逻辑完全一样先建连接再开记录集最后把记录集的行灌进 MSFlexGrid。理解了这一段后面无论原示例怎么组织你都能找到改入口。3. 常见问题与避坑VC6.0 ODBC Access 的五个经典翻车点3.1 64 位系统和 32 位进程DSN 配了却找不到现象在 Win10 或 Win11 的 64 位系统上打开控制面板里的 ODBC 数据源管理器明明创建了StudentDSN但 VC6.0 编译出来的程序一运行就报“找不到数据源名称”或者“指定的 DSN 包含驱动和应用程序之间的体系结构不匹配”。原因VC6.0 是 32 位编译器编出来的程序是 32 位进程而控制面板默认打开的 ODBC 管理器是 64 位版本它维护的数据源注册表位置在 64 位视图下。32 位程序去读的是注册表里 WOW64 映射的另一个视图于是怎么都看不到你刚配的 DSN。解决用 32 位版本的 ODBC 管理工具重新配置。在资源管理器里输入C:\Windows\SysWOW64\odbcad32.exe打开后重新创建系统 DSN指向同一个 mdb 文件。如果这台机器上还同时装了 64 位 Office Access 和 32 位 Access 驱动最好在创建 DSN 时确认“Microsoft Access Driver”的版本是 32 位的。另一个更稳妥的办法是彻底放弃 DSN改用无 DSN 连接字符串也就是第 2 章里写的Driver{Microsoft Access Driver (*.mdb)};DBQ...直接绕开 DSN 匹配问题这样不管换了哪台机器只要路径和驱动对程序就能连上。我自己后来做部署时一律不建 DSN而是把连接字符串写进 ini 配置文件。这样同样一份 exe换了数据库路径只要改配置不用去动注册表给运维省了不少事。3.2 连接字符串里的驱动名和路径空格与星号的玄学现象代码明明和教程一模一样运行到OpenEx时弹窗说“找不到数据源”或者“来自 ODBC 驱动程序的 [Microsoft][ODBC Driver Manager] 指定的 DSN 无效”。原因连接字符串里有两处最容易被抄错。一是 Access 驱动名VC6.0 时代最常见的是Microsoft Access Driver (*.mdb)这个星号和空格一个都不能少如果装了 Access 2007 以后的 ACE 驱动驱动名还多了(*.mdb, *.accdb)。二是不带 DSN 时 DBQ 后的路径如果 mdb 文件放在带中文或空格的目录下直接用\\拼接经常出问题。解决在代码里把连接字符串定义成宏先打印出来检查。我一般会这样组织#define ACCESS_DRIVER _T(Microsoft Access Driver (*.mdb)) #define ACCESS_DB _T(C:\\student_db\\info.mdb) CString strConn; strConn.Format(_T(Driver{%s};DBQ%s;), ACCESS_DRIVER, ACCESS_DB); // 用 TRACE 输出或弹窗显示给调试者看 TRACE(_T(Conn: %s\n), strConn); if (!db.OpenEx(strConn, CDatabase::openReadOnly)) { // 这里不要立刻返回先记录下来 TCHAR szErr[1024]; db.GetErrorMessage(szErr, 1024); AfxMessageBox(szErr); }注意 DBQ 后的路径反斜杠要写两个字符串里\\才代表一个反斜杠。这一类问题靠眼睛看很难看出来所以我先把连接字符串打出来再对照 ODBC 管理器里的驱动名称一字一字看。数据源配不上时错误信息里通常会带上驱动管理器的上下文比如它找不到的到底是 Driver 还是 DSN顺着那个词去查就能定位。3.3 Access 的 SQL 方言和参数写法现象程序连上了数据库但执行一句在 SQL Server 里没问题的 SQL比如SELECT TOP 10 * FROM student在 Access 里报“语法错误”或者日期字段查询时明明有数据却查不出来。原因Access 用的 Jet/ACE SQL 方言有几个明显的差异。TOP 子句的字段列表写法不同日期字面量用#2024-01-01#而不是2024-01-01字符串连接用而不是。很多从其他数据库转过来的开发都会在这上面翻车。解决写 SQL 时尽量克制只在 Access 自己的 SQL 视图里测试好再贴回 VC6 代码。参数化查询这块CRecordset 支持?占位符但用起来需要继承 CRecordset 并写 RFX 函数对初学者不够直接。日常快速查询我更建议直接用 Access 自己支持的 SQL 文本把条件拼进去时注意单引号和日期格式。例如查 2024 年 1 月 1 日报到的学生写成WHERE 报到日期 #2024-01-01#。这个写法在 Access 里是安全的也比?占位符更好猜。如果担心 SQL 注入注意 Access 的应用场景一般是单机工具注入风险相对低真要面向用户输入还是继承 CRecordset 用参数绑定别偷懒拼字符串。3.4 中文字段乱码和 CString 编码现象读取 Access 里的中文字段比如“姓名”在 TRACE 窗口显示正常但显示到 MSFlexGrid 控件或 MessageBox 里就成了乱码两个汉字变成类似“鑳″”这样的字形或者干脆丢字。原因VC6.0 默认的 CString 是 ANSI 字符集Access 的 mdb 内部用的是 Unicode。ODBC 的 Jet 驱动返回字符串时按 Unicode 给出CRecordset::GetFieldValue在内部做了转换但如果你把 CString 传给 MSFlexGrid 控件控件接口又是 ANSI这就可能经过两次转换导致丢失。更常见的情况是工程没开 Unicode 编译但 Access 表用了中文列名比如SELECT 学号 FROM student这时 ODBC 驱动返回的元数据包含中文而 VC6 的资源文件用的是 GBK两边编码对不上显示自然就乱了。解决第一优先是改用英文或拼音字段名这个方法最省心。第二如果字段名必须中文就在 View 类里把从记录集读出的 CString 用WideCharToMultiByte手动转成 GBK 再显示。代码如下CString strName; rs.GetFieldValue((short)1, strName); // 转成纯 ANSI 字节避免隐式转换出错 CStringA szAnsi strName; // 用 MessageBox 检查 AfxMessageBox(CString(szAnsi));这里的CStringA在 VC6 中是专门的 ANSI 版本拷贝构造时会把宽字符按系统代码页转换再带回 CString 时就不会多做一次。如果还不行那就调整编译选项把工程改成 Unicode但 VC6 对 Unicode 支持并不好改动面大得不偿失。我的血泪经验是Access 表里所有字段直接用英文字段名中文只作为显示标题这样彻底绕开编码问题。3.5 CRecordset 打开失败后的内存泄漏和重复打开现象程序第一次查询正常第二次再执行同一条查询时崩溃或者关闭记录集后立即释放 CDatabase 偶尔报“句柄无效”。这类问题在 DEBUG 版下还常伴一个 ASSERT。原因CRecordset 对象声明在循环里每次循环结束会自动 Close但如果你在循环内部又调用了一次rs.Open而 rs 已经处于打开状态MFC 会去重开句柄如果数据库对象 db 在 rs 之后销毁也可能先销毁了连接句柄。总之是生命周期顺序问题。解决固定一个原则记录集必须晚于数据库创建、早于数据库销毁。在同一个函数里用局部变量没问题但不要在循环里反复 Open 同一个记录集。正确做法是// 不要这样做 for (int i 0; i 3; i) { CRecordset rs(db); rs.Open(CRecordset::snapshot, strSQL); // ... rs.Close(); // 手动关掉 }改成在整个函数开始时创建记录集循环里只 MoveNext结束后统一 Close。如果确实需要在循环里对不同表查询就在每次查询前先判断是否打开若打开则 Close 再重开。另外 VC6 的 CRecordset 在异常时可能不留错误信息我通常会在这类代码外面包一层 try/catch并在 catch 里调用db.GetErrorMessage()把 ODBC 错误打出来。这个习惯帮我少调了半天的黑匣子。这四个大类覆盖了我在学这套示例时遇到的绝大多数问题把 3.1 到 3.5 过一遍基本能解决“DSN 配不上”“连接不上”“查不出来”“显示乱码”“反复崩溃”。下面再从源码角度说说怎么把这份示例包改造进自己的工程。4. 把压缩包里的示例改成你自己的工程文件结构和改连代码4.1 压缩包里的文件到底谁是谁DATABASEView.cpp、msflexgrid、bmp先看文件清单我把常见的那些文件按用途归类。DATABASE.APS和DATABASE.bsc是 VC6.0 构建过程的中间产物APS 是资源编译辅助文件bsc 是源码浏览数据库这两个文件和运行逻辑无关可以直接忽略。bkimg256blue.bmp、icon.bmp、无标题1.bmp、无标题12.bmp、Toolbar.bmp是界面资源分别用作背景、图标、按钮和工具栏位图。真正核心的是DATABASEView.cpp这是视图类实现里面大概率写了 CDatabase 和 CRecordset 的调用msflexgrid.cpp和msdgridctrl.cpp是控件包装类用来操作 MSFlexGrid 和 MSDataGrid。要看懂这套工程我建议用一个对比的思路打开DATABASEView.cpp先搜 CDatabase 和 CRecordset把这两个对象出现的位置标出来。通常结构是 App 初始化里创建 CStudentSet 等记录集派生类View 里用 Open 打开再在OnInitialUpdate里把记录集灌进网格。msflexgrid.cpp里封装的是控件的SetTextArray、SetRows这些操作你不需要把它读透只需要知道它在 View 里被使用即可。如果你不想用现成的 .dsp想从零建一个同款工程新建 MFC AppWizard 单文档工程在视图窗体上放一个 Microsoft FlexGrid Control然后引入msflexgrid.cpp和msdgridctrl.cpp再把自己的 CDatabase 连接代码放进去。注意 MSFlexGrid 控件在 VC6 里是个 ActiveX需要先包含控件头文件用 ClassWizard 生成 C 包装类压缩包里已经给了这份封装直接复制最快。4.2 把写死的数据库连接改成你自己的连接至少改两个地方第一处是连接字符串。原示例可能把 mdb 路径写死在代码里比如_T(C:\\program files\\student\\student.mdb)。这种写法在别人机器上一跑就废。我一般会把路径拆到和 exe 同级的db.ini里再用下面这段代码读取CString ConnStrFromIni(LPCTSTR lpszSection) { CString strDbPath; ::GetModuleFileName(NULL, strDbPath.GetBuffer(MAX_PATH), MAX_PATH); strDbPath.ReleaseBuffer(); int pos strDbPath.ReverseFind(_T(\\)); if (pos 0) strDbPath strDbPath.Left(pos 1); strDbPath _T(db.ini); TCHAR szBuf[256] {0}; ::GetPrivateProfileString(lpszSection, _T(DBPath), _T(data.mdb), szBuf, 255, strDbPath); CString strConn; strConn.Format(_T(Driver{Microsoft Access Driver (*.mdb)};DBQ%s%s;), (LPCTSTR)strDbPath, szBuf); return strConn; }这段代码做了三件事用GetModuleFileName拿到 exe 所在目录拼接 ini 文件名用GetPrivateProfileString读取 DBPath 值再拼成完整连接字符串。DBQ 里如果 ini 值已经带了反斜杠这里要注意别拼出双反斜杠我通常让 ini 值写相对路径比如data\student.mdb然后前面拼绝对目录。这样做的好处是换机器只需要拷贝 exe 和 ini不用重编。第二处是 SQL 语句。原示例查询的是固定的表名和字段比如SELECT * FROM student。如果你自己的表结构不同至少改两行一行是打开记录集的 SQL另一行是把记录集字段索引到网格列的代码。示例如下// 取自 DATABASEView 中 OnInitialUpdate 的常见片段 CString strSQL _T(SELECT id, name, class FROM t_student ORDER BY id); if (m_pSet-IsOpen()) m_pSet-Close(); m_pSet-Open(CRecordset::dynaset, strSQL); // 清空 MSFlexGrid然后逐行填入 m_grid.SetRows(1); m_grid.SetCols(3); m_grid.SetTextMatrix(0, 0, _T(ID)); m_grid.SetTextMatrix(0, 1, _T(姓名)); m_grid.SetTextMatrix(0, 2, _T(班级)); int row 1; while (!m_pSet-IsEOF()) { CString strID, strName, strClass; m_pSet-GetFieldValue((short)0, strID); m_pSet-GetFieldValue((short)1, strName); m_pSet-GetFieldValue((short)2, strClass); m_grid.SetRows(row 1); m_grid.SetTextMatrix(row, 0, strID); m_grid.SetTextMatrix(row, 1, strName); m_grid.SetTextMatrix(row, 2, strClass); row; m_pSet-MoveNext(); }GetFieldValue的下标是从 0 开始按 SELECT 语句里的列顺序来的。如果你把 SQL 改成SELECT name, id, class而没同步改下标网格里显示的内容就会错位。这是改示例时最容易忽略的。另外SetTextMatrix前的SetCols要和网格控件实际列数一致如果列数少了控件会拒绝SetTextMatrix甚至断言失败。经验是数清楚 SQL select 的列数再在SetCols和SetTextMatrix之间保持一致。到这里这套源码的核心链路已经通了配置文件里放路径连接字符串动态拼SQL 按自己的表结构改记录集逐行灌进网格。剩下的增删改操作原示例里AddNew、Update、Delete的写法都是标准的直接照搬即可。要注意的是Access 的 Jet 引擎默认不允许多用户并发写同一张表所以示例里的 Delete 往往直接执行 SQL而不是走CRecordset::Delete。这两种方式差别在于SQL 删除不经过当前记录集的游标状态不容易出现“记录集位置无效”的问题。我的习惯是批量操作直接用db.ExecuteSQL单条编辑用记录集中的AddNew/Update这样冲突最少。5. 上线前的验证技巧先测连接再写代码别让 ODBC 当黑匣子我踩过很多次 ODBC 的坑后养成了一个强制动作任何一次数据库操作先写一个只有“连接 单表查询”的最小函数通不过就不往下写界面逻辑。这个函数不长但能在一分钟内把驱动名、DSN、路径、权限这些变量全部验证掉。具体做法是临时在按钮响应里加一段测试代码用GetErrorMessage输出错误码。比如 0x80004005 通常是文件权限或路径问题DSN 错误会看到“ODBC 驱动管理器”字样。如果提示“无法初始化数据源”优先检查 32 位驱动是否安装。之后用_T(SELECT COUNT(*) FROM student)配合ExecuteSQL或记录集确认 SQL 方言能被 Access 接受。这样把问题范围缩小到 SQL 本身而不是整个环境连接。这个习惯救了我不止一次。最近一次维护老项目对方说“数据库能连但查不出来”我第一反应不是去读那一长串业务代码而是直接打开 ODBC 管理器看 32 位 DSN 还在不在再把连接字符串打到窗口里一看DBQ路径指向了一个已经迁移走的目录。改掉路径后一切正常。从那以后我每次接手 VC6 ODBC 的旧代码都强制走一遍“先测连接、再测 SQL、最后动界面”的顺序不在黑匣子里浪费一晚上。希望帮到你。本文还有配套的精品资源点击获取