MFC超市仓库管理系统开发:从数据库设计到增删改查实战

发布时间:2026/10/1 18:23:17
MFC超市仓库管理系统开发:从数据库设计到增删改查实战 简介基于MFC实现的简易超市仓库管理系统是一份面向高校C课程设计和期末大作业的完整源码包适合需要完成类似管理系统项目的学生参考。项目围绕超市仓库日常管理包含入库、出库、销售、用户管理等业务模块代码经过严格调试并达到95分以上评审成绩附有可运行的exe和SQL数据库脚本下载后能直接运行比对。压缩包共113个文件以C源文件、头文件和图标资源为主同时包含工程配置文件、编译中间文件、调试信息及数据库脚本等整体约104.47MB结构清晰。从内容预览可见主程序、添加对话框、销售对话框、用户管理对话框等类分别对应系统的初始化、信息录入、销售出库和用户权限管理能帮助学习者快速理解项目组织方式。目前已有222人学习使用适合作为课程设计模板或MFC入门后的综合练手项目。1. 用MFC做超市仓库管理系统先搞清楚这套源码能给你什么看到“基于mfc实现的简易超市仓库管理系统源码数据库95分以上期末大作业.zip”这个标题第一反应是这又是一份典型的数据库课程设计。它面向的场景很具体——大学里MFC课设或者数据库课设的收尾阶段要求你做一个能连数据库、能增删改查、能演示的桌面程序。MFC负责界面数据库负责数据落地而“95分以上”意味着它不只是能跑还要有报表、有统计、有容错、有可演示的操作闭环。这套方案适合两类人一是还没定题目、想直接复用完整源码和数据库脚本的同学二是已经有自己的代码但界面和数据库连接部分撑不起答辩想找一份成熟参考来补位的同学。它不涉及任何高深的算法核心是把MFC窗口程序和关系型数据库之间的数据流打通。接下来我按自己做课设和帮人改代码的实际顺序把整个方案的骨架、建表、增删改查、坑位和高分点拆开讲。2. 先定程序形态再连数据库MFC项目的地基2.1 对话框还是单文档超市管理系统的主窗口选型MFC程序有几种基础形态基于CDialog的对话框程序、基于单文档视图SDI的程序、基于多文档视图MDI的程序还有基于CFormView的表单视图程序。对“超市仓库管理系统”这种业务99%的课设不需要做到多文档也不需要复杂的文档/视图架构。我一般建议首选对话框程序。理由很直接对话框程序的InitInstance里只有两三行关键代码DoModal()一弹出来就是主窗口逻辑直观好答辩。而单文档程序要维护CSingleDocTemplate、CFrameWnd、CView、CDocument四者的关系MFC四大类里只要一个关联写错程序启动就白屏或者直接崩新手上手成本高出一截。如果老师要求界面看起来像个“系统”有左侧菜单、右侧内容区的效果可以在对话框左侧放一个CTreeCtrl做导航右侧放几个CDialogEx页面切换一样能做出多页面的感觉而且不用碰文档/视图那一套。下面是对话框程序入口的标准写法很多向导生成的工程代码比这个复杂核心差异就在InitInstance// MyApp.cpp —— MFC对话框程序的入口 BOOL CMyApp::InitInstance() { // CWinApp基类初始化内部会设置进程和资源句柄 CWinApp::InitInstance(); // 创建主对话框对象IDD_MAIN_DIALOG 是资源编辑器里的对话框ID CMyMainDlg dlg; m_pMainWnd dlg; // 模态启动DoModal 返回前消息循环一直由对话框内部驱动 INT_PTR nRet dlg.DoModal(); if (nRet -1) { // 对话框创建失败比如资源ID不存在 TRACE(_T(对话框创建失败\n)); return FALSE; } // 对话框关闭后退出程序 return FALSE; }这段代码里m_pMainWnd是CWinThread的成员把主窗口指针交给框架后MFC才能正确处理窗口消息和退出流程。DoModal()整个程序只会执行一次它内部跑的是模态消息循环所有的按钮点击、列表刷新、菜单响应都发生在这个循环里。如果DoModal返回-1通常是资源文件里找不到IDD_MAIN_DIALOG或者是对话框类没有关联这个资源ID。从MFC工程创建的细节来说无论用Visual Studio哪个版本新建项目时要勾选“MFC”组件否则连afxwin.h都编不过。装VS时如果没装MFC组件用Visual Studio Installer里“单个组件”搜索“MFC”补上即可这是离线安装MFC最常见的场景。选完工程模板后在“应用程序类型”里选“基于对话框”这样向导生成的代码就是上面这个结构不需要自己从零删改。2.2 数据库访问方案选型ADO是课设最省事的路线超市仓库管理系统要落库MFC层面常见做法有三种ODBC API、DAO、ADO。我只会推荐用ADO配合MySQL或者用CDatabase配合ODBC连Access。这里有个很容易被忽略的点MFC本身并不知道MySQL的存在所有数据库访问都要经过驱动。ADO是对COM的封装#import引入msado15.dll后可以直接用_ConnectionPtr、_RecordsetPtr这些智能指针代码量比ODBC API小一个量级也不用手动管理HSTMT。MySQL这边要额外装一个ODBC驱动官方叫Connector/ODBCMySQL 8.x对应的是“MySQL ODBC 8.0 Driver”MySQL 5.x用“MySQL ODBC 5.3 Driver”。驱动名字差一个版本号连不上时第一步就检查这里。连接字符串格式如下#import C:\Program Files\Common Files\System\ado\msado15.dll no_namespace rename(EOF, adoEOF) BOOL CDbHelper::Open(const CString host, const CString user, const CString pwd, const CString db) { // COM初始化ADO是COM组件每个线程使用前必须先初始化 ::CoInitialize(NULL); try { // connStr的内容就是连接数据库的凭据全部参数都在这里 CString connStr; connStr.Format( _T(DRIVER{MySQL ODBC 8.0 Driver};) _T(SERVER%s;PORT3306;DATABASE%s;) _T(USER%s;PASSWORD%s;charsetutf8;OPTION3;), host, db, user, pwd); // 创建ADO连接对象 m_pConn.CreateInstance(__uuidof(Connection)); // 5秒连不上就直接失败不要傻等 m_pConn-ConnectionTimeout 5; m_pConn-Open(_bstr_t(connStr), _T(), _T(), adModeUnknown); return TRUE; } catch (_com_error e) { // 连接失败ErrorMessage里带有驱动层给出的原因 AfxMessageBox(e.ErrorMessage()); return FALSE; } }这段代码的四个关键参数分别说明DRIVER决定走哪个ODBC驱动名字写错会直接报“找不到数据源”SERVER填localhost还是局域网的IP决定了数据库是否允许远程连接charsetutf8解决的就是中文乱码问题如果你前面建库用了utf8mb4这里应该写charsetutf8mb4OPTION3是ODBC连接选项的常用值表示启用多语句支持对同时绑定参数执行的场景有帮助。另外注意#import这行要放在stdafx.h或者预编译头里且整个工程只能写一次。如果编译时提示找不到msado15.dll就去安装MDAC或者拷贝一份msado15.tlb但实际上Visual Studio自带这个文件不必额外找。这个过程中最容易出现的翻车点是驱动版本和连接串不匹配现象是“找不到指定的驱动程序”解决办法就是去控制面板查看本机已安装的ODBC驱动名把DRIVER里的名字改一致。2.3 封装数据库操作类连接、查询、释放一次管好课设代码最忌讳把_ConnectionPtr到处裸奔。每个对话框里都写一遍CreateInstance、Open最后关闭时顺序一乱就是崩溃。实际操作中我一般会把数据库操作收拢成一个CDbHelper类把连接、执行、查询、关闭全部封装成方法对话框里只留着成员指针调用时统一从helper拿数据。// DbHelper.h —— 数据库访问封装类 class CDbHelper { public: CDbHelper(); virtual ~CDbHelper(); // 建立连接参数含义见 2.2 中的连接字符串 BOOL Open(const CString host, const CString user, const CString pwd, const CString db); void Close(); // 执行不带结果集的SQL比如 INSERT、UPDATE、DELETE BOOL Execute(const CString sql); // 执行带结果集的SQL返回记录集指针 _RecordsetPtr Query(const CString sql); private: _ConnectionPtr m_pConn; };// DbHelper.cpp —— 核心方法实现 CDbHelper::~CDbHelper() { Close(); } void CDbHelper::Close() { // 先关记录集再关连接这个顺序不要反过来 if (m_pConn ! NULL m_pConn-State adStateOpen) { m_pConn-Close(); } m_pConn NULL; // 每个Open调用对应的CoInitialize在程序最后统一释放 ::CoUninitialize(); } BOOL CDbHelper::Execute(const CString sql) { try { // 打开状态下才能执行命令 if (m_pConn NULL || m_pConn-State ! adStateOpen) { AfxMessageBox(_T(数据库未连接)); return FALSE; } // 将CString转成_bstr_t是ADO的标准参数类型 m_pConn-Execute(_bstr_t(sql), NULL, adCmdText); return TRUE; } catch (_com_error e) { TRACE(_T(Execute failed: %s\n), e.ErrorMessage()); return FALSE; } } _RecordsetPtr CDbHelper::Query(const CString sql) { _RecordsetPtr pRs; pRs.CreateInstance(__uuidof(Recordset)); // adOpenDynamic 表示可滚动的动态游标课设里最常用 pRs-Open(_bstr_t(sql), _variant_t((IDispatch*)m_pConn, true), adOpenDynamic, adLockOptimistic, adCmdText); return pRs; }封装类里有个细节很多人会忽略Close()里要先判断State已经关闭的连接再调一次Close()会触发异常_ConnectionPtr是智能指针把m_pConn置NULL会释放内部COM引用但前提是之前创建它时调过CreateInstance。在课设这种短生命周期程序里全局只保留一个CDbHelper实例程序退出时自然析构是性价比最高的连接管理方式。说到连接池那是在高频访问场景下才需要的优化期末大作业里完全没有必要实现连接池。反而如果你在答辩时主动提一句“连接池适用于频繁短连接的服务器端场景本系统采用单长连接满足桌面端低并发需求”这是个加分表达说明你理解了连接生命周期而不是背了一个名词。3. 建表与增删改查把超市仓库的数据流转做完整3.1 数据表设计四张表别把库存塞进商品表超市仓库管理系统的核心数据是商品资料、库存数量、出入库流水和商品分类。很多新手图省事只建一张tbl_product表把库存数量做成一列出入库只记当前数量结果就是最后没法统计“这个月进了多少货、出了多少货”。要拿95分流水账必须有。我常用的建表结构是四张表分类表、商品表、库存表、出入库流水表。SQL脚本如下-- 创建数据库注意字符集要和连接字符串里的charset保持一致 CREATE DATABASE IF NOT EXISTS supermarket_db DEFAULT CHARSET utf8mb4; USE supermarket_db; -- 商品分类表一个分类对应多个商品 CREATE TABLE tbl_category ( cat_id INT AUTO_INCREMENT PRIMARY KEY, cat_name VARCHAR(50) NOT NULL UNIQUE, remark VARCHAR(200) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 商品信息表只存静态属性和价格不存数量 CREATE TABLE tbl_product ( product_id INT AUTO_INCREMENT PRIMARY KEY, product_code VARCHAR(20) NOT NULL UNIQUE, product_name VARCHAR(100) NOT NULL, cat_id INT NOT NULL, spec VARCHAR(50), unit VARCHAR(10) DEFAULT 件, purchase_price DECIMAL(10,2), sale_price DECIMAL(10,2), min_stock INT DEFAULT 0 COMMENT 库存预警下限, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (cat_id) REFERENCES tbl_category(cat_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 库存表一个商品只有一条库存记录数量单独维护 CREATE TABLE tbl_stock ( product_id INT PRIMARY KEY, quantity INT NOT NULL DEFAULT 0, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (product_id) REFERENCES tbl_product(product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 出入库流水表只追加记录不修改已写入的数据 CREATE TABLE tbl_io_log ( log_id INT AUTO_INCREMENT PRIMARY KEY, product_id INT NOT NULL, io_type TINYINT NOT NULL COMMENT 1入库, 2出库, quantity INT NOT NULL, operator VARCHAR(50), log_time DATETIME DEFAULT CURRENT_TIMESTAMP, remark VARCHAR(200), FOREIGN KEY (product_id) REFERENCES tbl_product(product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;表拆分的逻辑是tbl_product保存商品“是什么”价格、规格、分类属于静态属性tbl_stock保存“现在有多少”这属于动态状态tbl_io_log保存“什么时候进了多少、出了多少”这是历史行为。三个关注点分开存查询才不互相干扰。建表注释在课设里非常重要。COMMENT 1入库, 2出库这种写法答辩时打开MySQL命令行一展示老师对你的专业度立刻加分。字段类型上商品编码用VARCHAR(20)而不是INT因为编码允许有前缀字母价格用DECIMAL(10,2)绝不用FLOAT浮点算金额会出精度错误这是数据库课设里一个常被忽略的细节。主键统一用自增INT商品编码只做唯一键不做主键避免后续改编码时连串外键都要改。3.2 增删改查用参数化SQL替代字符串拼接商品管理的增删改查标准做法是把每条SQL封装成一个函数。这里有一个关键原则不要用CString::Format拼SQL而是用ADO的参数对象绑定值。原因有两点一是字符串拼接时商品名里如果带单引号SQL会被截断或者报语法错误二是某次选中“插入”操作时输入了中文内容拼接出来的SQL里中文字符集不对写进库里就变成乱码再查出来只能看到问号。下面是商品新增的参数化写法其余更新、删除、查询都是同一套路BOOL CProductDB::AddProduct(const CProduct info) { // 注意参数名用 开头? 占位符也可以二者选一种 CString sql _T(INSERT INTO tbl_product ) _T((product_code, product_name, cat_id, spec, unit, ) _T(purchase_price, sale_price, min_stock) ) _T(VALUES(?, ?, ?, ?, ?, ?, ?, ?)); try { _CommandPtr pCmd; pCmd.CreateInstance(__uuidof(Command)); // 绑定当前连接 pCmd-ActiveConnection m_pDb-GetConnection(); pCmd-CommandText _bstr_t(sql); pCmd-CommandType adCmdText; // 依次追加参数类型、方向、长度、值 pCmd-Parameters-Append( pCmd-CreateParameter(_T(code), adVarChar, adParamInput, 20, _bstr_t(info.strCode))); pCmd-Parameters-Append( pCmd-CreateParameter(_T(name), adVarChar, adParamInput, 100, _bstr_t(info.strName))); pCmd-Parameters-Append( pCmd-CreateParameter(_T(cat), adInteger, adParamInput, 0, _variant_t(info.nCatId))); // spec, unit, price, sale_price, min_stock 依次追加写法相同 pCmd-Execute(NULL, NULL, adCmdText); return TRUE; } catch (_com_error e) { TRACE(_T(AddProduct error: %s\n), e.ErrorMessage()); return FALSE; } }参数绑定有几个点必须说明CreateParameter的第一个参数是参数名第二个是数据类型第三个是方向第四个是数据长度第五个是值。adVarChar对应字符串adInteger对应整型adDouble或adCurrency对应金额。长度要跟表结构里的字段长度匹配比如VARCHAR(20)就写20写大了影响不大写小了会截断。_variant_t是COM里万能的值包装类型传数字时用它最省事。更新和删除的区别只在于SQL语句不同更新用UPDATE tbl_product SET ... WHERE product_id?删除用DELETE FROM tbl_product WHERE product_id?。删除时要注意外键约束如果这个商品已经有出入库流水或者库存记录直接删除会报外键冲突所以通常做逻辑删除加一个is_deleted字段删除时改成1查询时默认WHERE is_deleted0这也是答辩中可以提的“逻辑删除 vs 物理删除”的加分点。3.3 显示到界面把查询结果填充到CListCtrl数据库端的结果集拿到之后要在界面上显示MFC里最常见的控件就是CListCtrl也就是列表视图。这里有一个很多人第一次写会翻车的点列表控件要先设列再插行而且插入的“行”用的是InsertItem填充每一行的其余列用的是SetItemText两者的索引含义不一样。另外MFC的CListCtrl没有直接的“获取总列数”方法你需要用GetHeaderCtrl()-GetItemCount()来拿这在动态设置列宽时会用到。// 把查询到的商品记录集填充进 m_list 列表控件 void CProductPage::RefreshList(_RecordsetPtr pRs) { // 关闭重绘数据量大时能避免界面闪烁 m_list.SetRedraw(FALSE); // 先清空旧数据避免重复显示 m_list.DeleteAllItems(); // 表头在OnInitDialog里SetColumn已设置这里不再重复 int nRow 0; while (!pRs-adoEOF) { // InsertItem只负责创建行返回值是行号 CString strCode (LPCTSTR)(_bstr_t)pRs-Fields-GetItem(_T(product_code))-Value; m_list.InsertItem(nRow, strCode); // SetItemText从第1列开始填第0列已经由InsertItem写入 CString strName (LPCTSTR)(_bstr_t)pRs-Fields-GetItem(_T(product_name))-Value; m_list.SetItemText(nRow, 1, strName); CString strQty (LPCTSTR)(_bstr_t)pRs-Fields-GetItem(_T(quantity))-Value; m_list.SetItemText(nRow, 2, strQty); // 移动到下一条记录 pRs-MoveNext(); nRow; } // 恢复重绘并立即刷新一次 m_list.SetRedraw(TRUE); m_list.Invalidate(FALSE); }这段代码里最关键的两个细节第一SetRedraw(FALSE)必须在DeleteAllItems之前调用否则清空动作本身就会触发一次重绘第二(LPCTSTR)(_bstr_t)这个转换在Unicode工程下是安全的因为_bstr_t提供了operator LPCTSTR但如果你把工程切到多字节字符集则要验证转换结果。pRs-Fields-GetItem(_T(product_code))这种写法是按字段名取值字段名必须和SQL里的别名或表字段一致写错一个字符运行时就抛异常。在商品录入页面下拉框绑定分类数据也是同样的套路先查SELECT cat_id, cat_name FROM tbl_category再循环m_cboCat.AddString(strCatName)同时用SetItemData把cat_id存到每一项的附加数据里选择时用GetItemData拿回ID。这样做的好处是界面上显示中文名称数据库存的是数字外键不会出现“分类名拼写不一致”的数据脏问题。4. 避坑与排查MFC数据库课设最常翻车的6个位置4.1 中文乱码Unicode字符集和数据库代码页打架现象往MySQL里插入中文商品名刷新后显示问号或者数据库里看是正常中文但程序读出来变成乱码。原因MFC工程默认使用Unicode字符集而ODBC驱动和MySQL连接层面默认的代码页可能是gbk或latin1三方编码不一致中间一次转换就丢数据。解决建库时指定DEFAULT CHARSETutf8mb4连接字符串里加上charsetutf8mb4两处保持一致。如果项目里还有历史代码用CString::Format拼接SQL还要把CString先转成std::string再拼到SQL里不能直接(LPCTSTR)裸转。调字符集属于那种“做对了静悄悄做错了全是问号”的玄学问题建议在CDbHelper::Open里连接成功后执行一句SET NAMES utf8mb4作为保险Multi-Layer字符转换的报错能少一大半。4.2 字符串内存泄漏CString和BSTR的释放时机现象程序关闭后Visual Studio输出窗口打印Detected memory leaks!泄漏点定位到某个循环里反复执行的查询函数。原因_bstr_t和CString混用时循环体内创建的COM字符串对象没有立即释放最常见的是把Fields-GetItem(...)-Value赋给_variant_t后忘记调用Clear()另一个常见原因是CoInitialize和CoUninitialize不配对在一个线程里初始化了两次只释放了一次。解决循环内不要长时间持有_variant_t取完值立刻转成CString让临时对象在循环体内析构在CDbHelper里维护一个m_bCoInit标志只在第一次Open时调用CoInitialize最后一次Close时才调用CoUninitialize。如果还想定位具体泄漏行可以在InitInstance开头加_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);调试模式下退出就会打印带文件行号的泄漏块。4.3 程序退出时崩溃COM释放顺序不对现象点击主窗口关闭按钮程序没有正常退出而是弹“0xC0000005 访问违例”堆栈停在_com_error或~_ConnectionPtr附近。原因CDbHelper是某对话框的成员变量对话框销毁时会先析构helper如果helper里还在使用COM对象而对应的CoUninitialize已经在其它地方调用过就会造成“先释放COM库后释放COM对象”的顺序错误。另一个常见场景是全局CDbHelper实例在CWinApp析构之后才触发释放。解决把数据库相关的释放全部放到CMyApp::ExitInstance()里显式调用确保在MFC框架清理COM之前执行同时Close()方法里先关闭记录集、再关闭连接、最后才CoUninitialize这个顺序写死在封装类里外部不要手动干预。关程序时如果还崩就检查每个窗口的OnDestroy里是否又调了一次CDbHelper::Close重复释放比不释放更容易炸。4.4 列表刷新闪烁一行一行插入导致重绘风暴现象商品列表几百条数据时每次刷新界面像波浪一样从上往下闪眼睛明显不舒服。原因InsertItem每执行一次控件就会触发一次局部重绘在SetRedraw(FALSE)没有开启的情况下几百次重绘叠加就变成闪烁。解决刷新前调用m_list.SetRedraw(FALSE)全部插完再SetRedraw(TRUE)并Invalidate(FALSE)。如果数据上千行还可以在OnInitDialog时给列表控件设置LVS_OWNERDRAWFIXED风格配合虚列表LVS_OWNERDATA只填充可见行的数据这属于进阶优化。课设里先做到SetRedraw两行代码就已经能解决90%的闪烁问题。4.5 换电脑连不上数据库连接配置写死在代码里现象在自己电脑演示正常压缩包发给老师或同学运行后弹“数据库连接失败”但程序本身启动正常。原因CDbHelper::Open的参数里写死了localhost或某个IP接收方电脑上的MySQL服务没启动或者没有导入.sql脚本建库或者用户密码不一致。解决连接参数不要硬编码放到主程序目录下的config.ini里用GetPrivateProfileString读取。下面是读取配置的最小实现// 程序启动时读取 config.ini 里的数据库参数 CString strHost, strUser, strPwd, strDb; TCHAR szBuf[256] { 0 }; // 读取失败时给出默认值保证本地开发不受影响 GetPrivateProfileString(_T(Database), _T(Host), _T(localhost), szBuf, 256, _T(./config.ini)); strHost szBuf; GetPrivateProfileString(_T(Database), _T(User), _T(root), szBuf, 256, _T(./config.ini)); strUser szBuf; // Password 和 Database 同理不再重复同时压缩包里要附带一个database.sql并在说明文档里写清楚“先启动MySQL再执行source database.sql最后修改config.ini”。这既是工程习惯也直接影响老师验收时能不能跑起来——跑不起来的项目内容再好也是白搭。4.6 获取列表总列数CListCtrl的隐藏坑现象想动态设置所有列的宽度用了GetColumnCount编译直接报错因为CListCtrl没有这个方法。原因MFC的CListCtrl封装里列信息存放在头控件Header Control中需要拿GetHeaderCtrl()再取项目数。解决使用GetHeaderCtrl()-GetItemCount()获取总列数然后循环SetColumnWidth。注意GetHeaderCtrl()在LVS_NOCOLUMNHEADER样式下返回NULL所以要保证列表控件有列头。这个细节经常被忽略但答辩现场当着老师的面查MSDN是很尴尬的提前记住这个API能省一分钟的临场翻车。5. 冲95分的加分项报表统计、预警与演示节奏5.1 用一条GROUP BY语句做出分类库存金额报表期末大作业想从85分拉到95分光有增删改查不够必须有“数据统计”的模块。最省力的做法是做一个“库存报表”页面用一条GROUP BY把分类维度上的商品数量、库存总量、库存金额一次性算出来填充到CListCtrl里。SQL如下SELECT c.cat_name AS 分类名称, COUNT(DISTINCT p.product_id) AS 商品种类数, SUM(s.quantity) AS 库存总量, SUM(p.purchase_price * s.quantity) AS 库存总成本 FROM tbl_product p JOIN tbl_category c ON p.cat_id c.cat_id JOIN tbl_stock s ON p.product_id s.product_id GROUP BY c.cat_id, c.cat_name ORDER BY 库存总成本 DESC;这段SQL里SUM(p.purchase_price * s.quantity)就是库存金额的聚合ORDER BY 库存总成本 DESC可以直接用别名排序这在MySQL里是合法的。将结果集填充到报表页的CListCtrl时直接复用第三章的SetRedraw和InsertItem套路没必要另写一套。展示形式上可以把报表放在一个单独的CDialogEx里从主界面“统计报表”按钮打开给老师的演示路径就变成了商品管理 → 入出库登记 → 分类报表三个页面形成闭环。5.2 库存预警低于下限就染红三分钟演示的设计思路第二个加分点是库存预警。逻辑很简单查询商品列表时把quantity min_stock的记录用红色文字标出来让“缺货商品”一目了然。MFC里给CListCtrl某一行文字上色需要自己处理NM_CUSTOMDRAW通知核心代码是在OnNMCustomdrawList里判断行号对应的数据是否命中预警条件然后返回CDRF_NEWFONT并设置文字颜色。// 列表控件的 NM_CUSTOMDRAW 通知处理 void CProductPage::OnNMCustomdrawList(NMHDR* pNMHDR, LRESULT* pResult) { NMLVCUSTOMDRAW* pLVCD (NMLVCUSTOMDRAW*)pNMHDR; *pResult CDRF_DODEFAULT; if (pLVCD-nmcd.dwDrawStage CDDS_PREPAINT) { // 请求每个条目单独绘制 *pResult CDRF_NOTIFYITEMDRAW; } else if (pLVCD-nmcd.dwDrawStage CDDS_ITEMPREPAINT) { // 根据行号拿到对应的商品库存状态 int nRow (int)pLVCD-nmcd.dwItemSpec; if (m_bLowStock[nRow]) { // 缺货行文字染成红色 pLVCD-clrText RGB(220, 50, 50); } *pResult CDRF_NEWFONT; } }这段代码的前提是维护一个m_bLowStock数组在刷新列表时根据quantity和min_stock填充。红色文字不需要额外的图片或控件一个NM_CUSTOMDRAW就解决了。这个功能在答辩演示时非常出效果因为它不只是一个“功能”而是一个“判断逻辑”能引出你关于库存临界值的思考。最后说说演示节奏。课设定分的关键除了代码质量还看你能不能在三分钟内说清楚数据库有哪些表、表之间什么关系、程序连库用的什么方案、入库和出库操作如何影响库存和流水。我当年做这个题曾经因为不清空CListCtrl的旧数据导致列表越翻越长也曾经因为连接串里漏了charset被中文乱码折磨到半夜最后在Close()里加了状态判断才彻底安稳。这些坑不算深但每个都要亲手踩一次才记得住。把一个系统做成“能演示、能讲清、能应对追问”比堆二十个页面更接近95分的目标。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询