ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

WinCC VBScript SQL报表查询:从变量采集到Excel打印

WinCC VBScript SQL报表查询:从变量采集到Excel打印

简介:基于VBScript与MSFlexGrid、MS DTPicker控件实现的WinCC SQL报表查询方案整理为一份doc文档,面向已具备WinCC组态基础、希望打通SQL数据库与报表展示的工控开发与运维人员,重点解决生产数据写入数据库、按日期查询和报表打印输出等问题。压缩包仅含1个doc文件,大小约1.11MB,内容围绕SQL Server 2005数据库与表字段创建、WinCC全局脚本写入和查询、日报表画面控件组态、查询与打印按钮脚本,以及附录中的可参考VBS代码展开,并提示MSFlexGrid控件注册等常见配置细节。已有1682人学习,适合作为WinCC报表功能落地时的案头参考,帮助读者理解变量记录从采集、入库到表格呈现和Excel打印的完整链路,也可用于排查脚本连接、时间范围筛选及控件使用中的问题。

1. 一张日报表背后的三个技术决定

值班的人每天下班前要做一件事:把当天几点 CPU 冲高、内存什么时候吃紧、硬盘占用怎么涨上去,整理成一张能签字的表。组态软件 WinCC 的趋势控件能看曲线,却给不出一张可打印、可归档的明细表。这套基于 VBScript 和控件实现的 WINCC SQL 报表查询功能,解决的正是从「变量」到「打印纸」之间缺失的那一段环节:全局脚本按周期把变量写进 SQL 数据库,画面上的日期控件选区间,表格控件接记录集,查询按钮出明细,打印按钮导 Excel。适合正在做组态课设、工控方向毕设,或者要给产线补一套轻量报表的人。

2. 需求拆解与技术选型:三层链路怎么切

先看现场提出的要求:按天查、看明细、带统计、能打印。四条需求看起来简单,落到组态软件上却要拆成三个独立问题:数据从哪来、存在哪、怎么给人看。这三个问题如果不分开设计,后面写脚本时就会混成一团,改一个字段名要动三处画面,这是很常见的开局错误。

2.1 三类使用者与四个动作

值班人员关心的是查询:选一个日期区间,表格里能翻到这段时间的明细,行数、时间点对得上。班组长关心的是统计:这段时间的峰值是多少、谷值是多少、平均值多少,这三个数直接决定要不要写进交接班记录。设备工程师关心的是原始数据能否导出,他要拿 Excel 去做进一步比对,比如和报警记录对齐看某个时刻发生了什么。

对应到画面动作只有四个:选日期、点查询、看表格、点打印。把这四个动作固定下来,界面上就不需要别的按钮。很多同学在课程设计里喜欢堆按钮,导出 CSV、刷新、清空、图表切换,结果每个按钮背后都是一套没写完的脚本,最后演示时全部飘红。控制住动作数量,是这套方案能顺利验收的前提。

四个动作中真正有技术含量的是查询:日期控件给出的是「日期」,数据库里存的是「日期加时间」,中间要做区间转换;记录集返回的是字节流,表格控件要的是行列坐标,中间要做映射;统计值来自第二条 SQL,不能和明细混在一起查。这三点拆清楚,脚本结构自然就出来了。

2.2 选型对比:为什么不写独立客户端

组态软件里做报表,常见三条路:外部程序、趋势控件导出、内置脚本直连数据库。把三者的实际成本摆一起,选型基本没悬念。

方案部署成本能否做区间统计与现场变量对接
独立客户端程序每台机器装运行时,版本还要对齐可以,但要多写一层数据访问需另开接口读变量
趋势控件导出低只能导曲线图片或原始点,不能算峰值均值原生支持
内置 VBS 脚本 + ADO随项目一起发布,无需额外安装一条聚合 SQL 解决直接用变量名读取

站在交付角度,第三条路最省事。组态软件本身带 VBS 引擎,全局脚本在运行态自动执行,画面上的按钮事件也可以直接写 VBS 过程,不需要额外进程、不需要跨进程通信。ADO 是三十二位的 COM 组件,从脚本里CreateObject("ADODB.Connection")就能拿到,连数据库这一步只剩一个连接串的事。

代价也有:脚本是解释执行的,调试手段少,报错信息藏得深。这决定了后面所有的设计都要为「可排查」让路——库名、表名、字段名集中写,连接串只出现一处,脚本里加注释说明每一段在做什么。带过项目的人都知道,这种环境下写脚本,可读性不是给别人看的,是给三个月后的自己留线索。

提示:选型的判断标准不是技术先不先进,而是现场那台机器上有没有。现场机器往往只装了组态软件,多一个运行时就是多一次交付风险。

2.3 数据表结构:一张明细表撑起全部查询

数据模型尽量简单。所有查询都围绕一张明细表,一天一条记录也好,一分钟一条也好,字段固定为时间加三个指标。统计值不单独建表,查询时用聚合函数现算,因为现算永远不会和明细对不上,而预统计表一旦写入失败就会长期不一致。

-- 报表明细表:一条记录对应一个采样时刻 CREATE TABLE VBSReport ( id INT IDENTITY(1,1) PRIMARY KEY, -- 自增主键,方便排查重复写入 shijian DATETIME NOT NULL, -- 采样时间 cpu FLOAT NOT NULL, -- CPU 占用百分比 ram FLOAT NOT NULL, -- 内存占用百分比 harddisk FLOAT NOT NULL -- 硬盘占用百分比 ); -- 时间字段是唯一的查询条件,索引必须建在它上面 CREATE INDEX IX_VBSReport_shijian ON VBSReport(shijian);

建表时有两个细节值得专门说一下。时间字段用DATETIME而不是字符串,字符串排序在跨月、跨年时会出问题,而且没法用BETWEEN做高效区间扫描。三个指标字段用FLOAT而不是整数,因为原始变量可能是带小数的百分比,取整会在汇总时放大误差。

索引这条容易被省略。数据量小的时候加不加索引都能跑,但采样周期设成一分钟、跑满一个月就是四万多行,没有索引的区间查询会从毫秒级掉到秒级,画面点查询按钮之后要愣一下才出结果,演示现场很尴尬。

2.4 三层职责:采集、存储、展示各管一段

整套方案按职责切成三层,每层只做一件事,层与层之间靠字段名和连接串这两个契约连接。

采集层是全局脚本里的写入过程,由触发器按固定周期调用,负责读变量、拼 INSERT、关连接。它不关心数据后面怎么用,也不需要返回任何结果。存储层是数据库实例里的那张表,负责保管和索引,不参与任何业务判断。展示层是报表画面,包含两个日期控件、一个表格控件、两个按钮,查询脚本负责把区间翻译成 SQL,打印脚本负责把表格翻译成 Excel。

层与层之间只在两个地方耦合:连接串里的库名,和 SQL 语句里的表名列名。这两处一旦前后不一致,症状是「查询没报错但一行数据都没有」,非常难查。所以实际做法是把库名和表名在脚本里只写一次,后面所有语句都用同一份拼出来,别在不同脚本里各写各的。

3. 数据落库:建库建表与全局写入脚本

数据链路的第一段是写入。这一段跑通了,后面的查询才有东西可查。很多项目卡在查询脚本上,实际根因在写入端:数据库里压根没有数据,或者时间字段存成了字符串,怎么查都查不出来。

3.1 建库与建表:从实例到表名的命名约定

组态软件安装时会自带一个数据库实例,报表数据通常就放在这个实例下新建的库里,而不是再装一个独立数据库服务。这样做的好处是备份、权限、连接串全部复用,坏处是库名容易和实例名混淆。

-- 新建报表库,命名保持和脚本里的 Initial Catalog 完全一致 CREATE DATABASE SQLReport; GO USE SQLReport; GO -- 建表语句与上一节一致,字段名就是后面脚本里要用的名字 CREATE TABLE VBSReport ( id INT IDENTITY(1,1) PRIMARY KEY, shijian DATETIME NOT NULL, cpu FLOAT NOT NULL, ram FLOAT NOT NULL, harddisk FLOAT NOT NULL );

命名约定要在动手前就定死:库名统一写SQLReport,表名统一写VBSReport,时间字段统一写shijian。原始资料里出现过库名写成SQL-Report、查询时又写成Example-report,表名有Report和VBSReport两种写法,字段名有hd和harddisk两种写法,这种不一致是后面最典型的翻车点。

建完表之后先手工插一条数据验证:INSERT INTO VBSReport VALUES(GETDATE(), 10, 20, 30),然后SELECT TOP 10 * FROM VBSReport。这一步能查出数据,说明库、表、权限都没问题,剩下的就是脚本的事了。

3.2 写入脚本:连接串、参数拼接与资源释放

全局脚本的写入过程是一个无参函数,靠触发器周期性调用。它做四件事:读时间、读三个变量、拼 SQL、执行并关连接。代码不长,但每一行都有讲究。

Option Explicit ' 全局脚本中的动作函数,由触发器周期性调用 Function action Dim conn, cmd, strConn, strSQL Dim tTime, vCpu, vRam, vDisk ' 1) 采样时间取脚本执行时刻,三个指标从内部变量读 tTime = Now vCpu = HMIRuntime.Tags("cpu").Read vRam = HMIRuntime.Tags("ram").Read vDisk = HMIRuntime.Tags("hd").Read ' 2) 连接串只在这里出现一次,库名和实例名写在最显眼的位置 strConn = "Provider=SQLOLEDB.1;Integrated Security=SSPI;" & _ "Persist Security Info=False;" & _ "Initial Catalog=SQLReport;Data Source=.\WINCC" ' 3) 打开连接,拼 INSERT,字段顺序和建表顺序保持一致 Set conn = CreateObject("ADODB.Connection") conn.ConnectionString = strConn conn.Open strSQL = "INSERT INTO VBSReport(shijian, cpu, ram, harddisk) " & _ "VALUES('" & tTime & "','" & vCpu & "','" & vRam & "','" & vDisk & "')" Set cmd = CreateObject("ADODB.Command") cmd.ActiveConnection = conn cmd.CommandType = 1 ' 1 表示直接执行 SQL 文本 cmd.CommandText = strSQL cmd.Execute ' 4) 连接必须显式关闭,否则长跑之后连接池会被占满 conn.Close Set cmd = Nothing Set conn = Nothing End Function

连接串里Data Source=.\WINCC指向的是组态软件自带的数据库实例,Initial Catalog指向具体库名,Integrated Security=SSPI表示用当前 Windows 账户做身份验证,不写用户名密码。这种写法在现场最省事,但前提是运行账户对目标库有写权限,权限不对时报的是「登录失败」,而不是脚本错误,容易被误判成脚本写错。

时间字段这里用了Now,拼进 SQL 时依赖服务器对日期字符串的解析。区域设置不同的机器上,Now转成字符串的格式可能不一样,稳妥做法是显式格式化:Format(Now, "yyyy-mm-dd hh:nn:ss")。这个改动看着小,但能避免在换机器部署时出现「字符串转换为日期时间失败」的错误。

Option Explicit这一行建议保留,它强制所有变量先声明,能挡掉拼写错误导致的隐式变量。代价是任何漏声明的变量都会让脚本编译不通过,而组态软件的脚本报错不会弹窗,只在诊断工具里能看到,所以写完立刻触发一次、去数据库里看有没有新数据,是必须养成的动作。

3.3 触发方式:定时器与变量变化怎么选

全局脚本写好了,还得挂触发器。组态软件通常提供两类触发:定时器和变量变化。定时器按固定周期跑,比如每六十秒一次;变量变化在指定变量数值改变时跑。

变量变化触发看起来更「智能」,实际坑不少。数据平稳的时候变量不变,脚本不执行,报表里就出现大段空白;变量抖动的时候脚本狂执行,一分钟写进去几十条,统计结果全被拉偏。除非有明确的「事件型数据」需求,报表采样一律用定时器。

周期怎么定取决于两件事:数据要保留多久、表格一屏能显示多少行。一分钟一条,一天一千四百四十条,查询一天的数据表格要翻很多屏,所以实际项目里常用五分钟或十五分钟一条,一天两三百条,正好一两页。保留周期则交给数据库的定时任务去做,超过半年的明细归档到历史表,主表保持轻量。

3.4 写入链路的自检:三处确认

写完脚本别急着做画面,先确认数据真的落库了。第一步在数据库里执行SELECT TOP 20 * FROM VBSReport ORDER BY shijian DESC,看最新几条的时间是不是当前时间、数值是不是在合理区间。第二步看组态软件的全局脚本诊断,里面会记录每次执行的错误信息,权限问题、语法问题都在这里露出。第三步把触发器停掉再启动,确认周期符合预期。

数值异常也是常见情况。如果三个指标读出来都是零,多半是变量名写错了,HMIRuntime.Tags("cpu").Read在变量不存在时不一定抛错,可能返回空值,插进数据库就成了零。所以第一次跑通后,刻意改一下现场值,看数据库里的数字跟着变,这一步花不了两分钟,能省掉后面一整天。

4. 画面组态与脚本落地:从日期选择到 Excel 打印的完整交互

写入链路稳定之后,剩下的全在画面上。这一章把控件注册、布局、属性设置、查询脚本和打印脚本一次讲完,因为这五件事在实际项目里是连着做的,拆开讲反而容易漏。

4.1 控件注册与画面布局

表格控件和日期控件都是 ActiveX 控件,需要在画面编辑器里先加进控件库再拖到画面上。表格控件通常来自一个独立的 OCX 文件,机器上如果装过 VB6 之类的开发环境,控件库里直接就有;没装过就要先注册。

:: 注册表格控件,必须在管理员权限的命令行里执行 regsvr32 C:\Windows\SysWOW64\MSFLXGRD.OCX :: 卸载时加 /u 参数,用于换版本或排查冲突 regsvr32 /u C:\Windows\SysWOW64\MSFLXGRD.OCX

注册这一步的坑集中在位数上。组态软件是三十二位进程,就必须注册三十二位版本的控件,放在SysWOW64目录;如果错放到System32,注册会提示成功,但画面打开时报「未注册的类」,两头都对不上。执行注册命令要用管理员身份,普通权限下 UMA 提示一闪而过,很多人以为成功了其实没有。

画面布局按「上选区、下表格」来分。顶部左侧放两个日期控件,分别命名Date1和Date2,代表起始和结束;顶部右侧放两个按钮,「查询」和「打印」;下方整块留给表格控件,命名MSFlexGrid1。控件命名必须和脚本里ScreenItems("...")的字符串完全一致,大小写也要一致,这是运行时找不到控件的头号原因。

4.2 表格控件的属性设置清单

表格控件默认长得像一张白纸,行列数、列宽、对齐方式全部要在脚本里设。把这些属性和它们的含义列成一张表,照着重设一遍,画面基本就成型了。

属性设置值作用
Rows记录数 + 6标题行、列头行、明细行、空行、三行汇总
Cols5序号、时间、CPU、内存、硬盘
ColWidth(n)900 至 1900序号列窄,时间列宽,其余居中
MergeCells4允许按行合并,用于顶部大标题
MergeRow(0)True第一行整行合并成一张跨列表头
ColAlignment(n)4居中对齐,4 是居中常量
TextMatrix(r, c)文本按行列坐标写值,比逐格移动光标快

Rows一定要在Clear之后、填数据之前设,顺序反了会出现「索引超出范围」。行数算错是另一个高频问题:明细占n行,加上标题、列头、空行和三行汇总,一共n + 6行,写n + 5就会在填最后一行汇总时越界。

4.3 查询脚本:两条 SQL 与记录集填充

查询按钮的事件脚本是整个方案里最长的一段。它要读日期、校验、执行两条 SQL、填表、写汇总。按顺序拆开就清楚了。

Sub OnClick(ByVal Item) Dim oConn, oCmd1, oCmd2, oRs1, oRs2, grid Dim strConn, strSQL1, strSQL2 Dim d1, d2, sBegin, sEnd, n, i ' 1) 读两个日期控件,格式化成数据库认的字符串 Set d1 = ScreenItems("Date1") Set d2 = ScreenItems("Date2") If d1.Value > d2.Value Then MsgBox "起始时间晚于结束时间", vbOKOnly, "时间输入有误" Exit Sub End If sBegin = Format(d1.Value, "yyyy-mm-dd") & " 00:00:00" sEnd = Format(d2.Value, "yyyy-mm-dd") & " 23:59:59" ' 2) 明细 SQL 与聚合 SQL 分开写,别混成一条 strSQL1 = "SELECT CONVERT(char(19), shijian, 20) AS shijian, cpu, ram, harddisk " & _ "FROM VBSReport WHERE shijian BETWEEN '" & sBegin & "' AND '" & sEnd & "' " & _ "ORDER BY shijian" strSQL2 = "SELECT AVG(cpu) AS avg_cpu, MIN(cpu) AS min_cpu, MAX(cpu) AS max_cpu, " & _ "AVG(ram) AS avg_ram, MIN(ram) AS min_ram, MAX(ram) AS max_ram, " & _ "AVG(harddisk) AS avg_disk, MIN(harddisk) AS min_disk, MAX(harddisk) AS max_disk " & _ "FROM VBSReport WHERE shijian BETWEEN '" & sBegin & "' AND '" & sEnd & "'" ' 3) 客户端游标,RecordCount 才会返回真实行数 strConn = "Provider=SQLOLEDB.1;Integrated Security=SSPI;" & _ "Persist Security Info=False;" & _ "Initial Catalog=SQLReport;Data Source=.\WINCC" Set oConn = CreateObject("ADODB.Connection") oConn.CursorLocation = 3 oConn.ConnectionString = strConn oConn.Open Set oCmd1 = CreateObject("ADODB.Command") oCmd1.ActiveConnection = oConn oCmd1.CommandType = 1 oCmd1.CommandText = strSQL1 Set oRs1 = oCmd1.Execute Set oCmd2 = CreateObject("ADODB.Command") oCmd2.ActiveConnection = oConn oCmd2.CommandType = 1 oCmd2.CommandText = strSQL2 Set oRs2 = oCmd2.Execute n = oRs1.RecordCount If n = 0 Then MsgBox "该时间段没有数据,请调整日期后重试", vbOKOnly, "无记录" Exit Sub End If ' 后续填表逻辑见下一段 End Sub

CursorLocation = 3这一行是必须的。ADO 默认用服务器端游标,RecordCount会返回 -1,表格行数就变成负数,直接报错。设成客户端游标之后,记录集会一次性取回本地,行数准确,代价是数据量特别大时占内存,对一天几百到几千条的报表完全够用。

两条 SQL 各自的作用要分清。第一条只取明细,时间字段用CONVERT截成十九位字符串,避免带毫秒的小数点影响显示;第二条只取九个数,AVG、MIN、MAX各三列。别名里的下划线不能省,AS avg_cpu写成ASavg_cpu会被数据库当成一个完整的别名,取值时按avg_cpu找不到列,报的是「未找到该项目」。

填表部分是纯坐标操作,逻辑很直白:第一行合并成标题,第二行写列头,从第三行开始按顺序写明细,最后三行写最大值、最小值、平均值。

' 4) 表格尺寸和列宽,Rows 必须在 Clear 之后设置 Set grid = ScreenItems("MSFlexGrid1") grid.Clear grid.Rows = n + 6 grid.Cols = 5 grid.ColWidth(0) = 900 grid.ColWidth(1) = 1900 grid.ColWidth(2) = 1000 grid.ColWidth(3) = 1000 grid.ColWidth(4) = 1100 ' 5) 首行合并成跨列标题 Dim z For z = 0 To 4 grid.Row = 0 grid.Col = z grid.Text = "机组运行日报表" Next grid.MergeCells = 4 grid.MergeRow(0) = True ' 6) 第二行列头 grid.TextMatrix(1, 0) = "NO." grid.TextMatrix(1, 1) = "Time" grid.TextMatrix(1, 2) = "CPU%" grid.TextMatrix(1, 3) = "RAM%" grid.TextMatrix(1, 4) = "HardDisk%" ' 7) 逐行写明细,行号从 1 开始,写在第 3 行起 oRs1.MoveFirst i = 0 Do While Not oRs1.EOF i = i + 1 grid.TextMatrix(i + 1, 0) = CStr(i) grid.TextMatrix(i + 1, 1) = oRs1.Fields("shijian").Value grid.TextMatrix(i + 1, 2) = CStr(oRs1.Fields("cpu").Value) grid.TextMatrix(i + 1, 3) = CStr(oRs1.Fields("ram").Value) grid.TextMatrix(i + 1, 4) = CStr(oRs1.Fields("harddisk").Value) oRs1.MoveNext Loop ' 8) 尾部三行汇总,行号分别是 n+3、n+4、n+5 grid.TextMatrix(n + 3, 0) = "MAX" grid.TextMatrix(n + 4, 0) = "MIN" grid.TextMatrix(n + 5, 0) = "AVG" grid.TextMatrix(n + 3, 2) = CStr(oRs2.Fields("max_cpu").Value) grid.TextMatrix(n + 4, 2) = CStr(oRs2.Fields("min_cpu").Value) grid.TextMatrix(n + 5, 2) = CStr(oRs2.Fields("avg_cpu").Value) grid.TextMatrix(n + 3, 3) = CStr(oRs2.Fields("max_ram").Value) grid.TextMatrix(n + 4, 3) = CStr(oRs2.Fields("min_ram").Value) grid.TextMatrix(n + 5, 3) = CStr(oRs2.Fields("avg_ram").Value) grid.TextMatrix(n + 3, 4) = CStr(oRs2.Fields("max_disk").Value) grid.TextMatrix(n + 4, 4) = CStr(oRs2.Fields("min_disk").Value) grid.TextMatrix(n + 5, 4) = CStr(oRs2.Fields("avg_disk").Value) oConn.Close Set oRs1 = Nothing Set oRs2 = Nothing Set oConn = Nothing

明细行的起始位置是第 3 行,也就是索引 2。循环变量i从 1 加到n,写入位置是i + 1,正好落在 2 到n + 1,与尾部汇总的起始行n + 3之间留了一行空行作为分隔。这个空行不是多余,它让打印出来的表格在汇总区和明细区之间有视觉区隔,评审时看起来干净很多。

平均值直接用 SQL 算,不在脚本里累加。原因是浮点累加在长区间上会出现精度漂移,而且脚本要额外维护三个累加变量,代码立刻长一倍。数据库的AVG在FLOAT字段上有明确的舍入行为,结果稳定可复现。

4.4 打印脚本:把表格搬进 Excel 并套上格式

打印这条路上有两种做法:直接用表格控件的打印方法,或者把数据搬到 Excel 再打。前者格式难控,页眉页脚、边框、行高都要在控件里一点点调;后者借用 Excel 的排版能力,边框合并都是现成的。实际项目里基本都走第二条路。

Sub OnClick(ByVal Item) Dim xlApp, xlBook, xlSheet, grid, r, c Set grid = ScreenItems("MSFlexGrid1") ' 1) 没查询过就别打印,挡掉空表 If grid.Rows <= 2 Then MsgBox "请先执行查询再打印", vbOKOnly, "提示" Exit Sub End If ' 2) 起一个隐藏的 Excel 实例,逐格搬运数据 Set xlApp = CreateObject("Excel.Application") xlApp.Visible = False Set xlBook = xlApp.Workbooks.Add Set xlSheet = xlBook.Worksheets(1) For r = 0 To grid.Rows - 1 For c = 0 To grid.Cols - 1 xlSheet.Cells(r + 1, c + 1) = Trim(grid.TextMatrix(r, c)) Next Next ' 3) 套边框、合并首行、自动列宽、居中 With xlSheet.Range("A1:E" & grid.Rows) .Borders(1).Weight = 2 .Borders(2).Weight = 2 .Borders(3).Weight = 2 .Borders(4).Weight = 2 End With xlSheet.Range("A1:E1").Merge xlSheet.Rows(1).RowHeight = 22 xlSheet.Columns("A:E").AutoFit xlSheet.Cells.HorizontalAlignment = -4108 ' -4108 表示居中 ' 4) 显示出来让用户确认,预览关闭后由用户自行处理 xlApp.Visible = True xlSheet.PrintPreview End Sub

Borders(1)到Borders(4)分别对应左、右、上、下四条边,Weight = 2是中等线宽。整段套在With里,避免四行重复写范围字符串,范围末尾用grid.Rows动态拼接,行数变了边框也跟着变。

结尾这里有个取舍要说清。预览之后马上Close和Quit,预览窗口会跟着消失,用户根本来不及看;不关,任务管理器里就会攒下一堆 Excel 进程。我通常的做法是保留 Excel 可见、把预览交给用户,同时约定「打印完手动关掉 Excel」,如果确实要自动关,就把PrintPreview换成PrintOut直接送到打印机,然后再关。两种都行,但必须选一种写死在脚本里,不能一半一半。

5. 避坑与排查:五个让报表跑不起来的地方

这套方案里绝大多数的「跑不起来」,都不是技术难题,是几处细节没对齐。下面五条是按出现频率排的。

第一条:查询不报错,但表格一行都没有。现象是点了查询按钮,弹出「没有数据」,可数据库里明明有记录。原因几乎都是库名、表名、字段名前后不一致:写入脚本连的是SQLReport,查询脚本连的是另一个库;写入时字段叫harddisk,查询时写成了hd;表名一处写VBSReport一处写Report。解决办法是把库名、表名、字段名在每段脚本顶部集中定义成常量,所有 SQL 用常量拼接,改一处就全改。排查时先在数据库客户端里手工执行查询脚本拼出来的那条 SQL,能查出数据说明数据库没问题,问题在脚本。

第二条:画面打开时报「未注册的类」或控件显示成灰色方块。现象是画面编辑器里控件是正常的,运行态一打开就报错。原因是控件没注册,或者注册了位数不对的版本。解决:确认组态软件是三十二位进程,把三十二位 OCX 放到SysWOW64,用管理员身份执行regsvr32,成功后重新打开画面。如果机器上装过 VB6 之类的开发环境,控件库直接可用,就不需要手工注册。判断是否注册成功,可以再执行一次注册命令,弹「已加载但未找到卸载入口」之类提示一般说明注册过了。

第三条:时间段选得对,查出来的记录总是少几条。现象是明明全天都有数据,起止日期选同一天,却少了开头或结尾的记录。原因在上界写成了23:59:59,而数据库里的时间带毫秒,最后半秒的数据落在区间之外;另一种情况是拼接日期时没有补零,2024-3-5这种格式在部分区域设置下解析异常。解决:上界改成「次日零点」并用小于号,即shijian >= 起点 AND shijian < 次日零点;日期统一用Format(x, "yyyy-mm-dd")生成,绝不手工拼月日。

第四条:点打印没反应,或者任务管理器里残留一堆 Excel 进程。现象分两种:一种是预览窗口一闪而过,一种是反复点几次之后机器变卡。前者是因为PrintPreview之后立刻Quit,预览被关掉;后者是因为对象没有释放,每次点击都新起一个 Excel 实例。解决:把「预览」和「自动关闭」当成互斥的两种模式,只选一种;如果选了自动关闭,就按xlSheet、xlBook、xlApp的顺序逐个置Nothing,并在Quit之前先Close工作簿,顺序反了会留下僵尸进程。

第五条:全局脚本偶尔不写数据,或者同一时刻写进去两条。前者的典型原因是触发器设成了变量变化,数据平稳时脚本不执行;后者的原因是同一个触发器被重复挂载,或者有人手工点了「执行一次」又赶上周期触发。解决:报表采样一律用定时触发器,周期按需设五分钟或十五分钟;排查时打开脚本诊断看执行记录,如果时间戳有规律地缺失,就是触发条件的问题,如果同一秒出现两条,就是重复触发。另外Option Explicit打开的脚本里,任何一个未声明变量都会让整段编译失败,而失败信息只在诊断里可见,所以每次改完脚本都要触发一次并回库里看结果。

6. 验收与答辩:怎么证明这套报表是真的能用

课程设计或毕设的验收,评的人不会去看你脚本写得多优雅,他只看三件事:数据是不是真的在动、查询是不是真的按区间、打印出来的东西能不能看。所以演示顺序要按数据流走,不要按代码结构走。

先开数据库客户端,把明细表的最新十条记录摆出来,指着时间戳说这是刚才现场改值之后写进去的。然后在画面上把某个变量手动改一下,等一个采样周期,刷新数据库,让评委看到新记录进来。这一步证明写入链路是活的。

接着切到报表画面,日期控件选当天,点查询,表格出数据,滚到底部指出 MAX、MIN、AVG 三行。再把起始日期改成前一天,同一个按钮再点一次,表格内容变了,证明查询真的是按区间走,不是写死的。最后点打印,Excel 里能看到带边框、有合并标题、列宽合适的表,按一次打印预览,翻两页。

答辩提问基本围绕四个方向:为什么用脚本不用外部程序、时间区间边界怎么处理、数据量大了怎么办、断电或者数据库断开时脚本会怎样。前两个在正文里已经有答案;第三个的回答是索引加归档,主表只留近期数据;第四个要诚实说清,写入脚本没有做重试,断连时那一个周期的数据会丢,如果需要补偿就在脚本里加错误捕获并把失败记录写到本地文本,下次启动补写。

验收前我自己会跑一张检查表,逐项打勾,少一项都不上台:

检查项通过标准
写入链路改一次现场值,一个周期后数据库出现新记录
区间边界起止选同一天,条数与数据库 COUNT 一致
空数据选一个没有数据的日期,弹出提示而不是报错
时间倒置起始晚于结束,弹出提示且表格不清空
表格汇总MAX、MIN、AVG 与手工执行 SQL 的结果一致
打印导出 Excel 有边框、有标题、列宽自适应
重复点击连点五次查询,画面不卡死、不叠加数据

从那以后我每次做完这种报表,都会先不写界面,直接在数据库里手工跑一遍 SQL,确认数据在、区间对、聚合值合理,再回去接画面。脚本这种东西出问题时不会告诉你哪一行错了,能提前在数据库这一层排掉的,就别留给运行态。希望这套结构能帮到你手上那份方案。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进