news 2026/9/12 12:25:38

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从VC++6到现代实践:拆解50万行MFC电子档案系统的架构与部署经验

简介:电子档案编制系统全套VC++6源码是一份面向软件开发人员与建筑工程资料管理岗位的完整工程资源,覆盖档案编制、施工日志编制、文档管理三大核心功能,并内置重庆建筑工程全部资料模板库,支持所见即所得的编辑模式,用户可在编辑视图中任意修改表格、文本、图像,也能自定义资料模板,支持整页复制粘贴,嵌入AutoCAD和Word等多种格式文档。资源包共两千个文件,大小约一百四十四点一八兆字节,核心为一千四百一十九个头文件与一千二百六十三个源程序文件,辅以位图图标、光标文件、动态链接库、静态库、模板文件以及数据库文件,并附有帮助文档,目录结构清晰,便于二次开发与学习。资源内包含详细开发文档和简易SQL2000数据库,可快速还原项目环境。当前已有一百七十人浏览学习,适合需要深入理解电子档案编制业务和界面框架的读者。

1. 一套 50 万行 VC++6 电子档案系统,到底能拆出什么

建筑工程资料组卷是“表格多、改动频繁、签字不能错”的典型场景。这套电子档案编制系统用 VC++6.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 库表设计与数据访问层

数据层很直接:VC++6 时代最稳的是 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 state=1"); 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 表,state=1 表示启用。CreateParameter 的四个参数分别是名称、类型、方向、长度,最后一个是具体值。业务层每次打开“签字”工具条前调 CheckRole,只返回真假,不把角色列表拉到界面里,权限判断点集中。要留意 adInteger 参数的长度写成 0,SQL 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 全局内存,获得 BITMAPINFOHEADER;CreateDIBitmap 的关键参数 CBM_INIT 表示用像素数据和颜色表初始化位图。biClrUsed 非零时用它的真值,否则按 256 色计算颜色表偏移。实际业务里很多扫描图是 24 位真彩,此时 biClrUsed 为 0,但颜色表并不存在,如果仍按 256 色算偏移就会读错数据。可靠的做法是先解析 biBitCount,24 位以上直接跳过颜色表。

在编辑区绘制的正确顺序是:先由模板解析出若干矩形元素,再对每个矩形调用 StretchDIBits 绘制背景图,表格线用 GDI 的 Rectangle/LineTo,不把表格做成一个个控件。这样缩放时不会出现控件重叠,打印时也能用同一套坐标走 Printer DC。

常见的自绘函数选择对比如下:

函数特点当前系统用途
SetDIBitsToDevice1:1 快速显示,不做缩放扫描图原始校验
StretchDIBits支持缩放,适合预览和缩放打印编辑区主要绘制
BitBlt位图块复制,最快但需要先转换双缓冲贴图
AlphaBlend半透明混色,VC6 需 msimg32.lib电子印章覆盖

3.2 表格自绘与字典拖拽的交互设计

模板元素不全是位图,还要让用户修改。系统提供文本字典、图像字典,用户从侧边栏把“主筋直径”“监理单位意见”等字段拖到编辑框,其实是在模板元素表里插入一条 elem_type=1 的文本元素并改变 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 = N'A4竖向' THEN 1 ELSE 0 END) AS a4v, SUM(CASE WHEN page_size = N'A4横向' 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 检查注册码和系统绑定信息,校验不通过时直接退出,不进入主窗口。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 12:24:50

企业微信API主动调用技术实战与优化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 12:23:33

Codex Token统计Skill实战:一句话打开每日用量看板

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 12:21:25

如何将 Graphiti MCP 服务器以 stdio 方式接入 Claude Desktop?

如何将 Graphiti MCP 服务器以 stdio 方式接入 Claude Desktop&#xff1f; 【免费下载链接】graphiti Build Real-Time Knowledge Graphs for AI Agents 项目地址: https://gitcode.com/GitHub_Trending/grap/graphiti Claude Desktop 只支持 stdio 传输&#xff0c;而…

作者头像 李华