ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

从VC++6到现代实践:拆解50万行MFC电子档案系统的架构与部署经验

2026/9/12 12:25:44 拓冰建站 浏览量
从VC++6到现代实践:拆解50万行MFC电子档案系统的架构与部署经验 简介电子档案编制系统全套VC6源码是一份面向软件开发人员与建筑工程资料管理岗位的完整工程资源覆盖档案编制、施工日志编制、文档管理三大核心功能并内置重庆建筑工程全部资料模板库支持所见即所得的编辑模式用户可在编辑视图中任意修改表格、文本、图像也能自定义资料模板支持整页复制粘贴嵌入AutoCAD和Word等多种格式文档。资源包共两千个文件大小约一百四十四点一八兆字节核心为一千四百一十九个头文件与一千二百六十三个源程序文件辅以位图图标、光标文件、动态链接库、静态库、模板文件以及数据库文件并附有帮助文档目录结构清晰便于二次开发与学习。资源内包含详细开发文档和简易SQL2000数据库可快速还原项目环境。当前已有一百七十人浏览学习适合需要深入理解电子档案编制业务和界面框架的读者。1. 一套 50 万行 VC6 电子档案系统到底能拆出什么建筑工程资料组卷是“表格多、改动频繁、签字不能错”的典型场景。这套电子档案编制系统用 VC6.0 SQL2000 实现了档案编制、施工日志、文档管理三大功能源码约 50 万行压缩包 120M附带详细的开发文档和一个可直接挂接的简易 SQL2000。核心价值不仅在于能跑更在于它的即见即所得编辑模式和重庆建筑工程资料模板库用户可以在编辑视图里直接改表格、文字、图像不用反复“预览再回编辑”。对维护过 MFC 大项目的人SetUpKey.cpp、dibapi.cpp、CPHDlg.cpp 这些文件里藏着权限校验、DIB 位图处理、业务对话框的典型写法。下文按“模块边界 → 编辑区 → 模板库 → 部署签名”的顺序拆。2. 从 MFC 框架看电子档案编制系统的模块边界与数据流2.1 文档-视图架构下的三大核心模块先说模块边界。摘要里的“档案编制、施工日志编制、文档管理”不是三个独立 EXE而是在同一个 MFC MDI 进程里由 CWinApp 派生的应用对象统一管理。档案编制模块处理检验批、分项工程验收表的录入与组卷施工日志按日期生成卷内目录文档管理管附件、扫描图、AutoCAD 图纸等外部文件。三者共用同一个 CDocument/CView 数据对差别在初始化时装入的模板类型和工具栏状态。这样设计的好处是编辑引擎只有一套维护成本集中在视图绘制和模板解析上。从源码文件名也能印证模块划分CPHDlg.cpp 这类文件以 CPH 为前缀对应某个业务模块的对话框SetUpKey.cpp 管系统登录、功能授权与注册信息不属于业务编辑链dibapi.cpp 把 DIB 位图的读写、调色板转换、缩放显示集中在工具层。一个初学者最容易犯的错是拿到源码后从 App 类的 InitInstance 一路读下去结果被 50 万行困住。我一般先把文件列表按前缀分组再对照开发文档里的模块清单建立映射表先把“哪个文件属于哪个模块”固定下来再读调用链效率会高很多。2.2 SQL2000 库表设计与数据访问层数据层很直接VC6 时代最稳的是 ADO 或 ODBC 直连 SQL2000。该项目带的是简易 SQL2000意味着发布包里没有全套系统库和联机丛书只保留服务端必须的 sqlservr.exe、客户端连接库和核心系统库。关键点在库表设计模板表、实例表、资源表三层结构。模板表保存重庆建筑工程全部资料模板实例表保存用户编辑后的当前表单资源表存文本、图像字典以及签字日志。下面是常见的主从表结构实际字段比这里多但骨架如此-- 模板主表 CREATE TABLE tpl_main ( tpl_id int PRIMARY KEY, tpl_name nvarchar(128), spec_code nvarchar(32), -- 所属专业/分部代码如渝建竣-20 page_size nvarchar(16), -- A4竖向、A4横向 create_by int, create_dt datetime ); -- 模板元素表把表格拆成矩形区域 CREATE TABLE tpl_elem ( elem_id int PRIMARY KEY, tpl_id int REFERENCES tpl_main(tpl_id), elem_type tinyint, -- 1文字 2表格线 3图像 4OLE rect_left int, rect_top int, rect_right int, rect_bottom int, font_size int, content ntext );这个建表语句说明了几件事。tpl_id 作为模板主键spec_code 用来按专业过滤页面tpl_elem 里的 rect 四个坐标决定即见即得编辑区里元素的位置content 不拆成多个字段是因为模板种类杂、字段多变用 EAV 式结构反而好维护。数据访问层必须注意SQL2000 取多结果集时用 ADO 要把游标类型设为 adUseClient否则编辑框频繁刷新时容易产生锁死。连接字符串写在独立配置文件中发布时让资料员自己改服务器 IP 和数据库名不要写死在注册表。2.3 权限模块 SetUpKey.cpp 的角色控制摘要列出了系统管理员、资料编辑人、签字人、授权修改资料人四种角色。SetUpKey.cpp 里实现的不只是登录框而是“密钥 账户”的双因子入口SetUpKey 读取用户手里的注册码校验本机特征后再进入账户密码登录。这样签字人能确认是本人操作也避免把 SQL2000 的 sa 密码直接暴露给操作者。角色与权限的对应关系可以直接落到一张表角色新建模板编辑实例签字授权他人修改系统管理员允许允许不允许允许资料编辑人按模板分配允许不允许不允许签字人不允许只读允许不允许授权修改资料人不允许允许不允许只限被授权页典型角色校验代码用 ADO 实现示意如下BOOL CheckRole(const CString strUser, int nRoleID) { _ConnectionPtr pConn NULL; pConn.CreateInstance(__uuidof(Connection)); pConn-ConnectionString _bstr_t(m_strDSN); pConn-Open(, , , adConnectUnspecified); _CommandPtr pCmd pConn-CreateCommand(); pCmd-CommandText _bstr_t( SELECT COUNT(*) FROM sec_user_role WHERE user_no? AND role_id? AND state1); pCmd-Parameters-Append( pCmd-CreateParameter(u, adVarChar, adParamInput, 20, _bstr_t(strUser))); pCmd-Parameters-Append( pCmd-CreateParameter(r, adInteger, adParamInput, 0, nRoleID)); _variant_t vCount pCmd-Execute(NULL, NULL, adCmdText); return (vCount.intVal 0); }这段代码的逻辑很直接按用户号和角色 ID 查 sec_user_role 表state1 表示启用。CreateParameter 的四个参数分别是名称、类型、方向、长度最后一个是具体值。业务层每次打开“签字”工具条前调 CheckRole只返回真假不把角色列表拉到界面里权限判断点集中。要留意 adInteger 参数的长度写成 0SQL Server 端按 int 处理写成 4 反而容易在 OLEDB 下报类型不匹配。SetUpKey.cpp 对“授权修改资料人”发放临时口令时最好同时记录操作日志否则审计时缺责任人。3. 即见即得编辑区视图、位图与模板渲染3.1 DIB 位图与所见即所得渲染原理“即见即得”是这个系统最重的功能。用户看到的是表格线、文字、印章按实际打印位置摆好拖动或输入后光标不跳页。老式做法是用 EMF/位图作为背景再叠加透明控件dibapi.cpp 在这里承担的是把扫描图、历史图纸转成 DIB并按当前缩放率重采样。DIB 的好处是设备无关保存到数据库里用 BYTE 数组就能存打印时按实际 DPI 映射。dibapi.cpp 里常见函数包括读取 BMP、设置 DIB 位图信息、从 DIB 生成 CBitmap。一个通用封装如下BOOL DIBToBitmap(HDIB hDIB, HBITMAP hBitmap) { LPBITMAPINFOHEADER lpBMIH (LPBITMAPINFOHEADER)GlobalLock(hDIB); if (lpBMIH NULL) return FALSE; HDC hDC ::GetDC(NULL); hBitmap CreateDIBitmap(hDC, lpBMIH, CBM_INIT, (LPBYTE)lpBMIH lpBMIH-biSize (lpBMIH-biClrUsed ? lpBMIH-biClrUsed : 256) * sizeof(RGBQUAD), (LPBITMAPINFO)lpBMIH, DIB_RGB_COLORS); ::ReleaseDC(NULL, hDC); GlobalUnlock(hDIB); return hBitmap ! NULL; }这里先用 GlobalLock 锁住 DIB 全局内存获得 BITMAPINFOHEADERCreateDIBitmap 的关键参数 CBM_INIT 表示用像素数据和颜色表初始化位图。biClrUsed 非零时用它的真值否则按 256 色计算颜色表偏移。实际业务里很多扫描图是 24 位真彩此时 biClrUsed 为 0但颜色表并不存在如果仍按 256 色算偏移就会读错数据。可靠的做法是先解析 biBitCount24 位以上直接跳过颜色表。在编辑区绘制的正确顺序是先由模板解析出若干矩形元素再对每个矩形调用 StretchDIBits 绘制背景图表格线用 GDI 的 Rectangle/LineTo不把表格做成一个个控件。这样缩放时不会出现控件重叠打印时也能用同一套坐标走 Printer DC。常见的自绘函数选择对比如下函数特点当前系统用途SetDIBitsToDevice1:1 快速显示不做缩放扫描图原始校验StretchDIBits支持缩放适合预览和缩放打印编辑区主要绘制BitBlt位图块复制最快但需要先转换双缓冲贴图AlphaBlend半透明混色VC6 需 msimg32.lib电子印章覆盖3.2 表格自绘与字典拖拽的交互设计模板元素不全是位图还要让用户修改。系统提供文本字典、图像字典用户从侧边栏把“主筋直径”“监理单位意见”等字段拖到编辑框其实是在模板元素表里插入一条 elem_type1 的文本元素并改变 content。这里的难点是文本换行与边线同步自绘时用 DrawText 的 DT_WORDBREAK | DT_EDITCONTROL 标志元素多行时自动撑高而不是修改模板矩形区域这样表格横线不会被撑破。拖拽命中测试要区分编辑模式和预览模式。预览模式只绘制不响应编辑模式下对 tpl_elem 里的每个矩形做 PtInRect。因为视图滚动坐标要先 ScreenToClient 再减掉滚动偏移。我通常会在 OnMouseMove 里维护当前悬停元素 ID显示边框提示鼠标样式变成 IBARAM让资料员能明确知道放下的位置。拖动结束时要判断元素类型文本元素重新解析换行图像元素则重新加载 DIB 并刷新区域OLE 元素需要激活容器避免用定时器全页重绘。3.3 整页复制粘贴的实现边界摘要里专门提到“整页复制、整页粘贴”这比文本流粘贴复杂。它复制的不只是 content而是把整组 tpl_elem 按源页面的相对位置复制过来。常见实现是把选中区域的元素序列化为临时结构粘贴时按目标页的主题偏移重算坐标。代码层面可以这样组织void CPastePage(CDocument* pDoc, int nTargetPage, const CByteArray arrPageData, int nOffsetX, int nOffsetY) { CMemFile f((BYTE*)arrPageData.GetData(), arrPageData.GetSize()); CArchive ar(f, CArchive::load); int nCount 0; ar nCount; for (int i 0; i nCount; i) { int nLeft, nTop, nRight, nBottom, nType; CString strContent; ar nLeft nTop nRight nBottom nType strContent; // 页眉、页脚元素不随粘贴平移 if (nTop PAGE_HEADER_HEIGHT) continue; nLeft nOffsetX; nRight nOffsetX; nTop nOffsetY; nBottom nOffsetY; pDoc-InsertTemplateElem(nTargetPage, nType, nLeft, nTop, nRight, nBottom, strContent); } ar.Close(); }这段逻辑说明三种边界情况。页眉区元素被强制跳过避免把工程名称贴得满页都是偏移量由用户在粘贴时选择“平移到同类位置”反序列化用 CArchive 把整页数据打成一个字节流正好存进数据库的 image/varbinary 字段。如果用户复制的是 AutoCAD 或 Word 嵌入对象元素的 content 存的是 OLE 对象存储路径或二进制块粘贴时重新激活 OLE 容器这部分建议直接复用 MFC 的 COleDocument 基础类不要自己实现二进制解析。4. 资料模板库的二次开发从内置模板到自定义模板4.1 模板库的数据模型与覆盖范围重庆建筑工程资料模板库为什么值得讲因为模板是按《建设工程文件归档整理规范》和重庆市地方表格体系做的分成施工管理、施工技术、进度造价、物资出厂、施工记录等若干卷。这套系统能直接生产“渝建竣”表。拿到源码后第一要看的是 tpl_main 表和 tpl_elem 表有多少条数据这决定你能否把它平移到其他省市而不是先纠结界面好不好看。我一般用下面这个查询统计模板覆盖和表格尺寸偏差SELECT spec_code, COUNT(*) AS tpl_cnt, SUM(CASE WHEN page_size NA4竖向 THEN 1 ELSE 0 END) AS a4v, SUM(CASE WHEN page_size NA4横向 THEN 1 ELSE 0 END) AS a4h FROM tpl_main GROUP BY spec_code ORDER BY spec_code;这段 SQL 会按专业代码输出模板数量与页面方向分布。如果某个 spec_code 的出现次数明显低于行政上应有的表数量说明该阶段模板缺失或目录编号不一致。做迁移的开发者会用这个结果比对地方标准目录而不是逐个打开模板编辑器看。模板编辑器本身是一个对话框工程核心界面和业务编辑页共用一个视图类只是初始化参数不同。4.2 从扫描图快速生成模板的流程摘要第 8 条支持“使用资料的扫描图快速生成”模板这是同类系统少有的能力。扫描图生成模板的做法分四步扫描、校正、识别表格线、矢量化。扫描图进入系统后先转成灰度 DIB用阈值分割把背景去掉然后用 Hough 变换检测水平线和竖直线交点就是表格元胞的角点最后按“表格线向内收缩 2 像素”生成 tpl_elem 的矩形记录。对老代码库来说不要期望它实现了智能 OCR它做的是“贴底图 叠线条”把扫描图作为模板背景层用户用鼠标沿着表格线描边双击闭合后自动生成矩形。这样做门槛低、准确率高因为表格线不一定笔直可能有污渍。生成后保留原始扫描图作为模板预览图避免矢量化结果和原稿有偏差时无从查证。这里的开发文档如果描述模糊就直接在 TplEdit 对话框里打断点看坐标换算比读文档准确。4.3 用开发文档快速梳理定制点的技巧这套源码带了详细开发文档120M 压缩包里有相当大的比例是设计文档和接口说明。老项目文档有个特点模块图画得很大但函数级说明很少。要接二次开发建议先把文档目录复制成可勾选清单然后按“启动初始化 → 登录 → 选择工程 → 打开模板 → 编辑 → 保存 → 组卷”这条主链路去读源码对应文件不要按文档目录顺序读。结合现在常用的 AI 提效方式可以把 SQL2000 里 tpl_main/tpl_elem 的字段导出成 CSV再把开发文档中相应的段落贴给模型让模型输出字段之间的映射和可能遗漏的默认值。注意这个办法只适合梳理文档、代码、库表三者的对应关系不适合直接让 AI 修改 50 万行的代码逻辑。定制点通常落在四个边界定制边界涉及文件/表常见风险模板入库脚本tpl_main/tpl_elemspec_code 编号与本地目录不一致打印分页算法CPrintView 派生类元素坐标未按打印 DPI 缩放字典资源dict_text/dict_image资源 ID 冲突权限表sec_user_role角色频率与用户表不一致改这几个边界一般只动 SQL 和参数不用动编辑视图。5. 编译部署与权限签名老项目最容易被卡住的三个细节5.1 精简 SQL2000 的正确安装姿势压缩包里的“简易 SQL2000”不是标准安装盘通常是指精简安装的桌面版。必须在 VC6 工程里把数据访问配成“使用 SQL Server 身份验证”否则 Windows 凭据不对时会一直连接超时。先把 MDAC 驱动装好再手工创建数据库exec xp_cmdshell mkdir D:\EArchiveData sp_attach_db EArchive, D:\EArchiveData\EArchive.mdf, D:\EArchiveData\EArchive_log.ldf;这段 SQL 用 sp_attach_db 把源码包中的主数据文件和日志文件挂接到实例上。xp_cmdshell 在 SQL2000 默认开启但容易成为安全口子生产环境建议用完就关。如果客户端报“未找到提供程序”检查 VC6 程序目录下是否注册了 msado15.dll。5.2 签名坐标与权限校验的联动电子签名不是图片盖章而是把签名人的用户 ID、签名时间、页面上签名的矩形区域写进一个单独 sign_log 表。SetUpKey.cpp 里的签名动作会在按下鼠标时记录坐标松开时生成不可见的校验记录。这样后续打印时如果某个字段在签字后被修改程序可以根据 sign_log 里的坐标判断修改是否发生在签名区域之内。实用技巧是在视图类的 OnPrint 里先校验签名数据完整性再调用基类打印BOOL CEditView::OnPrint(CDC* pDC, CPrintInfo* pInfo) { if (!VerifySignPositions(GetDocument()-GetCurrentPage())) AfxMessageBox(_T(该页有签名区域数据被改动), MB_ICONWARNING); return CRichEditView::OnPrint(pDC, pInfo); }这段代码先对当前页的 sign_log 记录做遍历比对元素表的最后修改时间是否晚于签名时间。不要试图把签名绑定到编辑器内部否则别人直接用数据库工具改库时校验就失效了。最后一步是在程序启动时用 SetUpKey 检查注册码和系统绑定信息校验不通过时直接退出不进入主窗口。本文还有配套的精品资源点击获取