十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MFC课程设计:超市管理系统模块划分与收银事务实现

MFC课程设计:超市管理系统模块划分与收银事务实现 简介这是一份基于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 MFCx86 和 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 驱动连接串要设计成可切换的形态。提示如果连接报“未找到提供程序”把连接串里的ProviderMicrosoft.Jet.OLEDB.4.0换成Microsoft.ACE.OLEDB.12.0装上 Access Database Engine 即可。连接串不要散落在每个对话框里集中到一个函数里管// App 工具函数返回一个完整可用的 ADO 连接串 CString GetConnectionString() { CString strConn; // 数据库文件与 exe 放在同一目录避免写死绝对路径 strConn.Format( _T(ProviderMicrosoft.Jet.OLEDB.4.0;) _T(Data Source%s\\supermarket.mdb), (LPCTSTR)GetExeDir()); // GetExeDir 的实现见最后一章 return strConn; }这里把数据库路径和连接串拆成两件事连接串指定 ProviderData 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_qtystock_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 的 UpdateadCmdText告诉 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 把结果发给主对话框再由主对话框刷新界面。这里的关键是不能在线程里直接调用SetDlgItemTextMFC 控件不是线程安全的跨线程操作 UI 轻则显示错乱、重则崩溃。正确姿势是 PostMessage 一个自定义消息回到主线程的消息处理函数里再更新界面。CREATE_SUSPENDED和m_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_FULLROWSELECT和LVS_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 的 TEXTOLE 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点结算界面显示单价和金额商品库存减 1UC-02收银员输入不存在的条码 999点结算提示“商品不存在”不扣库存不写流水UC-03收银员商品库存 2输入数量 3点结算提示“库存不足”事务回滚流水表无增加这张表直接抄进软件工程课程设计文档的需求分析章节。答辩时老师往往会顺着用例往下问“UC-03 怎么保证库存不足时不写流水”这时候就回答“UPDATE 影响行数为 0 就 RollbackTrans”代码和文档当场对齐。5.2 软件工程流程图怎么画才不被追问流程图不是画得越复杂越好而是要把数据流向画清楚。超市系统只画两条主链路就够了一条销售、一条入库。销售流程可以简化成这样的步骤序列收银员输入条码和数量程序按条码查 Product 表查不到则提示查得到则取出单价和库存判断库存是否足够不足则提示并终止在事务中执行 UPDATE 扣减库存在同一个事务中 INSERT 销售流水事务提交界面显示本单金额画数据流图时把“商品”“库存”“销售流水”作为数据存储把“扫码输入”“结算”“库存扣减”作为加工处理。两张图里各画一个“检查库存”判断框逻辑就完整了。不要画那种三十多个节点的全流程大图答辩时讲不完老师也不会逐节点看完。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。课设代码里这个函数只写一次全工程所有数据库操作就都安全了。本文还有配套的精品资源点击获取
返回列表