MFC课程设计:超市管理系统模块划分与收银事务实现
2026/9/14 9:17:38 网站建设 项目流程

简介:这是一份基于MFC框架实现的超市管理系统完整源码,面向软件工程课程设计及Windows桌面应用开发学习者,项目覆盖商品管理、库存控制、销售记录、会员管理等日常运营功能,并通过数据库交互完成数据存取,体现了从需求分析到编码测试的完整流程。压缩包共118个文件、约27.87MB,以cpp/h源码、bmp与ico图形资源、rc界面定义及sln/dsp工程文件为主,可直接打开工程查看界面设计、类结构和业务逻辑。目前已有191人学习下载。通过研究该源码,可掌握MFC消息映射、对话框设计、商品类与库存类等业务类构建、数据库连接等关键技巧,理解完整项目的组织方式。这份完整项目既是课程设计的参考模板,也能帮助开发者深入理解Windows下管理信息系统的开发思路,对提升实际编码与调试能力有明显价值。

1. mfc.zip 里的超市系统,课设里最该做厚的是数据流

mfc.zip 这种命名,基本等于在说:一个 MFC 工程、一个 Access 数据库、一份课程设计报告,打包在一起。超市管理系统是软件工程课程设计里的常驻题目,但大多数人的第一版都做成了“两个按钮加上一个 List Control 的增删改查”,界面勉强能看,答辩时一问业务流程就露馅。真正值钱的不是窗口画得多好,而是商品从入库、上架、销售到库存扣减这条数据链能不能闭环,以及收银这种核心动作在代码里有没有事务保护。这篇文章就从技术选型、模块划分、MFC 控件和课程设计文档四个方向,把一套能在答辩现场自圆其说的做法讲清楚。

2. MFC 课设技术选型:对话框程序、Access 文件与 ADO 连接串

MFC 在课程设计里不算过时,它最大的优势是零额外安装成本。新装 Visual Studio 时勾选“使用 C++ 的桌面开发”工作负载,再勾选“适用于最新 v142 生成工具的 C++ MFC(x86 和 x64)”,应用向导就能直接生成框架。相比之下 C#/WinForms 更简单,但题目指定了 MFC,硬换语言容易在答辩时被老师按课程要求追问。我一般会直接选对话框程序而不是文档视图,配合 Access 的 .mdb 文件和 ADO 访问数据库,整套方案对运行环境的要求低到“拷个文件夹就能演示”。

2.1 选对话框而不是文档视图,省掉序列化包袱

MFC 应用向导里有单文档、多文档和对话框三种模板。超市系统的本质是“多个操作窗口共同操作一个数据库”,不是文本编辑器。单文档的 CDocument/CView 机制里,OnNewDocument、DeleteContents、Serialize 这一整套都围绕“打开文件、编辑、保存文件”设计,放到超市系统里几乎用不上,反而要在多视图之间纠结数据刷新时机。对话框程序更直白:每个业务功能一个 CDialogEx,主界面用 DoModal 拉起子窗口,控件变量和消息映射都在一个类里,答辩时顺着按钮讲代码路径很顺。

入口代码我一般长这样:

BOOL CShopApp::InitInstance() { // 初始化一次 ADO,整个程序共用一个连接对象 m_pConn.CreateInstance(__uuidof(Connection)); HRESULT hr = m_pConn->Open( _bstr_t(GetConnectionString()), // 连接串集中管理 _T(""), _T(""), adModeUnknown); if (FAILED(hr)) { AfxMessageBox(_T("数据库连接失败,请检查 supermarket.mdb")); return FALSE; } CSaleDlg dlg; m_pMainWnd = &dlg; dlg.DoModal(); // 主对话框阻塞在这里 return FALSE; // 退出后结束进程 }

这段代码里m_pConn是 App 类的_ConnectionPtr成员,所有对话框共享一个连接,比每个窗口都重新 Open 一次干净得多。adModeUnknown表示连接模式由 OLE DB Provider 决定;CreateInstance(__uuidof(Connection))创建的是 ADO 的 Connection 对象。如果 Open 失败,弹窗提示后直接返回 FALSE,避免后面所有数据库操作都挂在空指针上。

这里顺便说一个很多人问过的点:想让控制台程序支持 MFC,其实不需要新建 MFC 工程。在 VS 工程属性里找到“常规 -> 使用 MFC”,把它从“使用标准 Windows 库”改成“在共享 DLL 中使用 MFC”,就能在 main 函数里用 CString、CArray 这些类。但做课设界面不要走这条路,控制台没有消息循环,按钮、列表控件全要从 WinMain 手工搭,工作量直接翻倍。

程序框架适用场景MFC 课程设计里的问题
对话框程序 CDialogEx表单 + 列表的数据库操作界面最适合,代码路径清晰
单文档/多文档 CDocument文档编辑、文件序列化超市系统用不上 Serialize
控制台 + MFC 类库纯逻辑验证、批量数据处理没有界面框架,演示效果差

2.2 Access + ADO:最稳定的课设数据库组合

数据库方案上,我通常不考虑 SQL Server Express。答辩机器不固定,一个 .mdf 文件要求目标机器装了 SQL Server 实例,没装就是现场翻车。纯文件方案更不推荐,商品、销售、库存全用 C++ 自己读写,连个带条件的 SELECT 都要手写循环,统计模块做完基本能写一本书。Access 的 .mdb 是单文件,拷贝就能带走,配合 ADO 的 OLE DB Provider,增删改查和事务全都是标准 SQL,代码量小很多。唯一要留意的是较新的 Windows 机器可能没有 Jet 驱动,连接串要设计成可切换的形态。

提示:如果连接报“未找到提供程序”,把连接串里的Provider=Microsoft.Jet.OLEDB.4.0换成Microsoft.ACE.OLEDB.12.0,装上 Access Database Engine 即可。

连接串不要散落在每个对话框里,集中到一个函数里管:

// App 工具函数,返回一个完整可用的 ADO 连接串 CString GetConnectionString() { CString strConn; // 数据库文件与 exe 放在同一目录,避免写死绝对路径 strConn.Format( _T("Provider=Microsoft.Jet.OLEDB.4.0;") _T("Data Source=%s\\supermarket.mdb"), (LPCTSTR)GetExeDir()); // GetExeDir 的实现见最后一章 return strConn; }

这里把数据库路径和连接串拆成两件事:连接串指定 Provider,Data Source 只负责提供 .mdb 文件的完整路径。写死C:\Users\xxx\...这种绝对路径是课设最常见的翻车点,换一台机器演示就找不到数据库文件。GetExeDir 用 GetModuleFileName 获取 exe 所在目录,保证从任何目录启动程序都能定位到数据库,具体实现在后面调试部分给出。

3. 超市系统模块划分与收银主线的 MFC 代码实现

拿到题目先别急着拖控件,把模块边界画出来,后面写代码和写文档都会轻松很多。一个合格的超市 MFC 系统,至少要覆盖四个模块:收银、商品维护、入库、统计。收银是主链路,商品维护是基础数据,入库影响库存,统计依赖销售流水。模块之间通过数据库表产生关系,而不是通过窗口之间互相传指针。

3.1 模块边界:哪些该做、哪些不该做

收银模块负责输入条码、输入数量、计算金额、扣库存、写销售记录;商品维护模块是商品表的增删改查,注意这里只管商品资料,不管库存数量;入库模块单独做一张进货记录表,入库时同时增加商品库存;统计模块按天汇总销售额。很多课设把“查询”当模块来写,做一个大对话框把商品表所有字段列出来,没有任何业务动作,这种模块在答辩时老师很难找到提问点。

模块核心界面控件操作的数据表验收时演示的动作
收银Edit + Button + List ControlProduct、Sale扫码结算后库存自动减少
商品维护List Control + 增删改按钮Product新增商品后收银台能查到
入库Edit + DataGrid 或 ListStockIn入库后商品库存增加
统计List Control + 日期范围选择Sale按日汇总销售金额正确

表里的验收动作,每个都能对应到软件工程需求分析里的一个用例。这些用例不是写文档时现场编的,而是你代码里真正实现了的路径。

3.2 建表 SQL:商品表、销售表、进货表的最小字段集

我一般建三张表,字段只保留最能说明问题的几个。Access 的 SQL 和 T-SQL 略有差异,字段类型用 TEXT、LONG、CURRENCY、DATETIME,自增主键叫 AUTOINCREMENT。

CREATE TABLE Product ( code TEXT PRIMARY KEY, -- 条码,可作为扫码枪输入 name TEXT NOT NULL, -- 商品名称 category TEXT, -- 分类,统计时可按类分组 price CURRENCY NOT NULL, -- 单价 stock_qty LONG NOT NULL -- 当前库存数量 ); CREATE TABLE Sale ( sale_id AUTOINCREMENT PRIMARY KEY, code TEXT NOT NULL, -- 卖出去的商品条码 qty LONG NOT NULL, -- 数量 amount CURRENCY NOT NULL, -- 本行金额,单价 x 数量 sale_time DATETIME NOT NULL -- 销售时间,统计按它分组 ); CREATE TABLE StockIn ( in_id AUTOINCREMENT PRIMARY KEY, code TEXT NOT NULL, qty LONG NOT NULL, in_time DATETIME NOT NULL );

三个表的结构说明:Product 的 code 是主键,它同时被 Sale 和 StockIn 引用,但我不建议在课设里做外键约束——Access 的外键在复制文件、删除记录时会带来一堆“无法删除”的提示,课设阶段用应用层逻辑保证一致性就够了。SALE 表不存商品名称,只存条码,需要显示名称时再去 Product 表 JOIN。这样设计有个好处:商品改名时销售历史不受影响,也让程序里必须写至少一条 JOIN 语句,答辩讲数据关系时更经得起追问。

3.3 收银结算的 MFC 代码:查库存-扣库存-写流水

收银是整个系统的核心,不要把它写成“界面上点一下插入一条记录”的水平。完整顺序应该是:查商品 -> 检查库存 -> 事务里更新库存并写销售流水。下面这段代码可以直接放在收银对话框的结算按钮事件里:

void CSaleDlg::OnBnClickedBtnCommit() { // 1. 从两个编辑框读入条码和数量 CString strCode, strQty; GetDlgItemText(IDC_EDIT_CODE, strCode); GetDlgItemText(IDC_EDIT_QTY, strQty); int nQty = _ttoi(strQty); if (strCode.IsEmpty() || nQty <= 0) { AfxMessageBox(_T("条码或数量无效")); return; } // 条码里的单引号要转义,防止拼 SQL 时语句被截断 strCode.Replace(_T("'"), _T("''")); // 2. 查询商品单价和库存 _RecordsetPtr pRs; pRs.CreateInstance(__uuidof(Recordset)); CString strSql; strSql.Format( _T("SELECT price, stock_qty FROM Product WHERE code='%s'"), (LPCTSTR)strCode); pRs->Open(_bstr_t(strSql), theApp.m_pConn.GetInterfacePtr(), adOpenKeyset, adLockReadOnly, adCmdText); if (pRs->adoEOF) { AfxMessageBox(_T("商品不存在")); pRs->Close(); return; } double dPrice = pRs->Fields->GetItem("price")->Value; long lStock = pRs->Fields->GetItem("stock_qty")->Value; pRs->Close(); // 3. 库存检查放在事务外做快速判断 if (lStock < nQty) { AfxMessageBox(_T("库存不足")); GetDlgItem(IDC_EDIT_QTY)->SetFocus(); return; } // 4. 事务内扣库存、写销售流水 double dAmount = dPrice * nQty; CString strUpdate, strInsert; strUpdate.Format( _T("UPDATE Product SET stock_qty=stock_qty-%d WHERE code='%s'"), nQty, (LPCTSTR)strCode); strInsert.Format( _T("INSERT INTO Sale(code, qty, amount, sale_time) ") _T("VALUES('%s', %d, %.2f, NOW())"), (LPCTSTR)strCode, nQty, dAmount); theApp.m_pConn->BeginTrans(); try { long lAffected = 0; theApp.m_pConn->Execute(_bstr_t(strUpdate), &lAffected, adCmdText); if (lAffected == 0) { // 影响行数为0,说明商品被并发删掉了,整体回滚 theApp.m_pConn->RollbackTrans(); AfxMessageBox(_T("商品状态已变化,请重试")); return; } theApp.m_pConn->Execute(_bstr_t(strInsert), NULL, adCmdText); theApp.m_pConn->CommitTrans(); } catch (_com_error& e) { theApp.m_pConn->RollbackTrans(); AfxMessageBox(e.Description()); return; } CString strAmount; strAmount.Format(_T("%.2f"), dAmount); SetDlgItemText(IDC_EDIT_AMOUNT, strAmount); SetDlgItemText(IDC_EDIT_CODE, _T("")); SetDlgItemText(IDC_EDIT_QTY, _T("1")); GetDlgItem(IDC_EDIT_CODE)->SetFocus(); }

这段代码有几个参数需要注意。adOpenKeyset表示键集游标,在 Access 单表查询时性能稳定;adLockReadOnly说明这里只读不写,写操作走 Execute 而不是 Recordset 的 Update;adCmdText告诉 ADO 第一个参数是 SQL 文本。_ttoi把字符串转成 int,注意它不做合法性检查,所以前面先判空、判小于等于 0。Fields->GetItem("price")->Value返回_variant_t,可以直接赋值给 double 和 long,这是 ADO 的隐式转换。

3.3.1 事务与 UPDATE 影响行数的关系

把 UPDATE 和 INSERT 放进 BeginTrans/CommitTrans 是课程设计里少有的“工业级”细节。库存扣减成功了但销售流水没写进去,账目就会对不上;用了事务后,两条语句要么都成功,要么都回滚。lAffected接收的是 Execute 执行后影响的行数,这里是判断 UPDATE 是否真正改到行的关键。库存不足的判断虽然前面已经做过,但事务里再查一次影响行数,防的是两个收银台并发卖同一件商品时,检查完库存那一瞬间货被另一个窗口买走。这个地方在答辩时被问到的概率极高,能说出“用影响行数防止超卖”这句话,基本就把并发卖空的问题讲清楚了。

3.4 统计报表放工作线程:MFC 里别在主线程跑大循环

课设的统计功能一般是“按日期范围汇总销售额”。如果直接在按钮事件里写一个 while 循环遍历 Recordset,数据量到几千条时界面就会卡住不动,拖动窗口都没反应。这不是死机,是主线程被查询占住了。MFC 里给这类活开一个线程很简单:

// 用 AfxBeginThread 启动一个工作线程做汇总 CWinThread* pThread = AfxBeginThread( StatThreadProc, // 线程函数 this, // 参数,传 this 方便稍后 PostMessage THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED); // 先挂起,设置好状态再启动 pThread->m_bAutoDelete = FALSE; // 手工管理线程对象,防止崩溃 pThread->ResumeThread();

线程函数返回 UINT,内部跑完统计后用 PostMessage 把结果发给主对话框,再由主对话框刷新界面。这里的关键是不能在线程里直接调用SetDlgItemText,MFC 控件不是线程安全的,跨线程操作 UI 轻则显示错乱、重则崩溃。正确姿势是 PostMessage 一个自定义消息,回到主线程的消息处理函数里再更新界面。CREATE_SUSPENDEDm_bAutoDelete = FALSE这两个参数是 MFC 线程最容易踩的坑:前者保证线程在设置完必要状态后才开始跑,后者防止线程结束的瞬间对象被自动 delete 掉,导致其他代码还握着悬空指针。统计量小的时候主线程跑也没什么大问题,但答辩时被问“为什么列表数据多了界面会卡”,用“我把统计放到工作线程了”来回答,比临时解释“数据量其实不大”要硬气得多。

4. MFC 控件调试:List Control 表头、字符集与消息映射的坑

MFC 课设里八成的报错和乱码,集中在控件样式、字符集和消息映射三件事上。这些问题本身不难,但症状很有迷惑性:有的不报错只是不显示,有的显示出来全是问号,还有的按钮就是进不去事件。下面三个坑是每个超市系统几乎都会踩的。

4.1 List Control 设了 LVS_REPORT 却不显示表头

最常见的情况是:对话框资源里拖了一个 List Control,属性里 View 还是默认的 Icon,代码里写了 InsertColumn,运行起来却只看得到图标或什么都不显示,表头完全没有。原因是 List Control 的样式没有切成报表模式。在 OnInitDialog 里补一段修正:

// 强制切成报表模式,再用扩展样式加网格线和整行选中 m_list.ModifyStyle(LVS_TYPEMASK, LVS_REPORT); m_list.SetExtendedStyle( m_list.GetExtendedStyle() | LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES); // 三列:条码左对齐、名称左对齐、价格右对齐 m_list.InsertColumn(0, _T("条码"), LVCFMT_LEFT, 100); m_list.InsertColumn(1, _T("品名"), LVCFMT_LEFT, 140); m_list.InsertColumn(2, _T("价格"), LVCFMT_RIGHT, 80);

LVS_TYPEMASK是个位掩码,它把 View 风格里的 Icon / Small Icon / List / Report 四类盖住,ModifyStyle用按位替换的方式把类型改成LVS_REPORT。之后 SetExtendedStyle 加的LVS_EX_FULLROWSELECTLVS_EX_GRIDLINES分别是整行选中和网格线,这两个都是 Report 之外额外的界面效果。InsertColumn 的第三个参数LVCFMT_LEFT/LVCFMT_RIGHT控制列对齐;如果对不齐,先检查是不是 List Control 的 View 还停在 Icon,或者插入列时根本没给列宽——列宽为 0 的列是看不见的,这两个原因比对齐参数本身更容易导致“没有效果”。

4.2 中文乱码:Unicode 字符集下 CString 与 _bstr_t 互转

网上能找到的 MFC 课设源码大多来自 VS2008 之前,工程字符集是 ANSI,代码里到处都是char[]CString的混用。新版 Visual Studio 默认是 Unicode 字符集,直接把旧代码拖进来,从数据库读出来的中文字段就会变成一片问号。标准做法是新建工程保持默认的 Unicode,所有字符串统一用 CString、TCHAR、_T() 宏,不要出现裸的char*拼接。ADO 读字段时的转换写法是这样:

// 从 Recordset 取商品名称字段 _variant_t vName = pRs->Fields->GetItem(_T("name"))->Value; CString strName; strName = (LPCTSTR)(_bstr_t)vName; // BSTR(宽字符) -> CString

_bstr_t是一个 RAII 风格的 BSTR 封装类,它从_variant_t构造时会把变体里的值转成宽字符的 BSTR,再通过(LPCTSTR)转成 CString,整个过程不会出现问号。如果工程还是 ANSI,这里就要多绕一步 GBK 编码转换,所以我一般建议直接把工程属性里“字符集”设为“使用 Unicode 字符集”,全项目只认这一条规则,乱码问题就能按一个标准排查。写成这样配合数据库字段时,注意数据库里的文本类型是 Access 的 TEXT,OLE DB 返回的就是 BSTR,和这条链路是完全对齐的。

4.3 消息映射与 UpdateData:按钮事件为什么进不去

MFC 对话框的按钮事件依赖消息映射表。类向导生成后,类声明里有DECLARE_MESSAGE_MAP(),类实现文件里有BEGIN_MESSAGE_MAP(...)ON_BN_CLICKED宏,缺一个按钮事件都不触发。手写时最容易犯的错是把ON_BN_CLICKED写成ON_COMMAND,前者响应的是按下去这个动作,后者响应的是菜单和加速键的关联命令,按钮用错了宏事件根本进不来。

UpdateData的坑则相反,它不会不触发,而是“触发得很错位”。UpdateData(TRUE)是把界面控件内容刷到变量,UpdateData(FALSE)是把变量刷到界面。我见过不少代码在按钮事件里先UpdateData(TRUE)读取输入,中间又调了一次UpdateData(FALSE),把用户刚输入的条码又用旧变量覆盖回界面。其实在这个场景里更稳妥的做法是像我第三节那样直接GetDlgItemText/SetDlgItemText:读取输入用 Get,回显结果用 Set,不依赖变量绑定顺序,事件代码从上往下读就是一条清晰的流程。

5. 软件工程课程设计的文档:需求用例、流程图与测试记录

课程设计报告里最容易被看穿的地方,是需求分析写成“系统能够实现收银功能”这种一句废话。软件工程文档的价值在颗粒度:需求描述要落到“谁、做了什么操作、系统返回什么结果”,流程图要覆盖数据流向,测试记录要有明确的输入输出对照。照着下面的格式写,文档和代码才能对得上。

5.1 需求分析:用表格列“能演示的用例”

需求分析不用写“本系统采用 C/S 架构”之类的空话,重点是用例要能对应到演示路径。下表是从收银模块抽出的三个用例,每个都能在界面上操作一遍:

用例编号参与者操作步骤预期结果
UC-01收银员输入条码 001,数量 1,点结算界面显示单价和金额,商品库存减 1
UC-02收银员输入不存在的条码 999,点结算提示“商品不存在”,不扣库存不写流水
UC-03收银员商品库存 2,输入数量 3,点结算提示“库存不足”,事务回滚,流水表无增加

这张表直接抄进软件工程课程设计文档的需求分析章节。答辩时老师往往会顺着用例往下问:“UC-03 怎么保证库存不足时不写流水?”这时候就回答“UPDATE 影响行数为 0 就 RollbackTrans”,代码和文档当场对齐。

5.2 软件工程流程图怎么画才不被追问

流程图不是画得越复杂越好,而是要把数据流向画清楚。超市系统只画两条主链路就够了,一条销售、一条入库。销售流程可以简化成这样的步骤序列:

  1. 收银员输入条码和数量
  2. 程序按条码查 Product 表
  3. 查不到则提示,查得到则取出单价和库存
  4. 判断库存是否足够,不足则提示并终止
  5. 在事务中执行 UPDATE 扣减库存
  6. 在同一个事务中 INSERT 销售流水
  7. 事务提交,界面显示本单金额

画数据流图时,把“商品”“库存”“销售流水”作为数据存储,把“扫码输入”“结算”“库存扣减”作为加工处理。两张图里各画一个“检查库存”判断框,逻辑就完整了。不要画那种三十多个节点的全流程大图,答辩时讲不完,老师也不会逐节点看完。

5.3 测试记录和验收演示数据

测试记录直接复用上面的用例格式,加两列“实际结果”和“是否通过”。每个用例一行,能证明代码和需求一致。演示数据要提前造好:准备 5 个以上的商品,条码和名称最好贴近真实超市场景,比如“6901234567890 农夫山泉”,数量设置成有零有整,这样演示完库存变化一眼就能看出来。别用“商品1、商品2”这种名字,老师会觉得连基础数据都没用心准备。

6. 调试技巧:用 OutputDebugString 和数据库路径定位问题

课设代码出问题时,AfxMessageBox 弹窗虽然直观,但每弹一次要手动关一次,在循环里调试会崩溃。更实用的办法是用 OutputDebugString 往调试输出窗口打日志,配合 Microsoft 的 DebugView 工具,不阻塞界面就能看到程序内部执行到哪一步。

6.1 OutputDebugString 看收银过程

在收银按钮事件里插入日志输出,比到处设断点更省时间:

OutputDebugString(_T("== OnBnClickedBtnCommit ==\n")); CString strInfo; strInfo.Format(_T("code=%s qty=%d"), (LPCTSTR)strCode, nQty); OutputDebugString(strInfo);

这段代码把当前输入的条码和数量打到输出窗口,执行完一行再看下一步。OutputDebugString在 Release 配置下也能用,而TRACE宏只在 Debug 下输出,这是两者最大的差别。如果程序运行起来界面没反应,重点看有没有打进== OnBnClickedBtnCommit ==;如果打进了但库里没数据,就往 BeginTrans 前后各打一条,很快能定位是卡在查询、更新还是事务回滚。

6.2 经典坑:双击 exe 报“找不到数据库文件”,VS 里跑却正常

这是我的第三个坑,也是课设验收前最容易被放倒的地方:在 Visual Studio 里按 F5 一切正常,把整个 Debug 文件夹拷到别的机器双击 exe,立刻弹“数据库连接失败”。原因不是文件没拷过去,而是代码里用了相对路径连接数据库。GetCurrentDirectory返回的是“当前工作目录”,VS 调试时默认为工程目录,所以能找到;双击 exe 时工作目录可能是 system32 或桌面,相对路径自然失效。

用 exe 所在的目录拼连接串,才是稳定写法:

#include <shlwapi.h> #pragma comment(lib, "shlwapi.lib") // 获取当前进程 exe 所在目录,末尾不带反斜杠 CString GetExeDir() { TCHAR szPath[MAX_PATH] = {0}; GetModuleFileName(NULL, szPath, MAX_PATH); // 拿 exe 绝对路径 PathRemoveFileSpec(szPath); // 去掉文件名部分 return CString(szPath); }

GetModuleFileName的第一个参数传 NULL,取到的是当前进程主模块的完整路径;PathRemoveFileSpec来自 shlwapi.h,把一个带文件名的路径截掉文件名,留下目录。连接串里拼上它,无论程序从哪个目录启动,都能稳定找到同一份 supermarket.mdb。课设代码里这个函数只写一次,全工程所有数据库操作就都安全了。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询