简介:这是一份围绕SIMATIC WinCC平台实现SQL报表查询与打印的实践文档,面向工业自动化上位机开发、HMI组态及WinCC脚本学习者,帮助解决生产数据按日期检索、表格展示与报表输出的问题。内容从Microsoft SQL Server 2005新建数据库、定义时间及CPU、内存、硬盘等字段开始,延伸到WinCC全局脚本中用VBScript写入与查询数据,并讲解MSFlexGrid、MS DTPicker控件在日报表画面中的组态、查询按钮取数及打印按钮导出Excel的处理,附录给出可参考的VBS代码。资源包仅含1个doc文件,大小约1.11MB,便于集中查阅完整流程与脚本片段。已有1682人学习,适合需要理解WinCC与SQL交互、控件绑定的技术人员对照实践,积累报表画面设计与数据查询排错思路。
1. 夜班交接前的那张日报表,被我塞进了 WinCC 画面
凌晨两点交接班,操作工拿着手写表格去抄 CPU 和内存占用率,抄完再敲进 Excel 发给值班长——这套「WINCC 报表 VBS脚本 SQL」方案要解决的就是这个场面。方案把现场变量按周期写进一张 SQL 表,画面上放两个 DTPicker 选起止日期,点“查询”,MSFlexGrid 里就是整天的明细,末尾自动补上 MAX / MIN / AVG 三行,再点“打印”就落到 Excel 里预览输出。适合手里已经有 WinCC 项目、又被要求交日报周报的自动化工程师,也适合不想装第三方报表组件、只想用现有全局脚本把历史数据拉出来的同学。整套东西不用编译、不打部署包,改需求就是改几行 VBS。文档里从建库建表、控件注册到三段完整脚本都带着,抄下来能直接跑。
2. 全景拆解:一条数据从现场变量走到 Excel 表格
这套方案结构简单,但层次分得清楚:采集在 WinCC 的变量系统里,存储在 SQL Server 的一张普通表里,展示靠画面上的两个 ActiveX 控件,输出借 Excel 的 COM 接口。四段之间只靠两样东西连接——OLEDB 连接串和画面元素名。把这两样记住了,后面哪一段出问题都能一眼定位。
2.1 四层数据流:变量、SQL 表、ActiveX 控件、Excel
按数据流动方向拆开看,各层的承担者是这样的:
| 层次 | 承担者 | 关键动作 |
|---|---|---|
| 采集层 | WinCC 内部变量(cpu / ram / hd) | HMIRuntime.Tags("cpu").Read读取当前值 |
| 存储层 | SQL Server 库中的报表表 | ADO 连接 + INSERT 语句写入 |
| 展示层 | MSFlexGrid + 两个 DTPicker | 查询脚本把记录集铺进表格 |
| 输出层 | Excel COM 对象 | 单元格逐格赋值、加边框、打印预览 |
写入链路和查询链路彼此独立:全局脚本只管往表里写,画面脚本只管从表里读,两张脚本唯一的交集是“表名 + 字段名 + 连接串”这三样必须对得上。原文里恰好这三样都没统一——写入脚本把数据插进VBSReport表的time,cpu,ram,hd字段,查询脚本却从report表读shijian,cpu,ram,harddisk,连接串里的库名一个写SQL-Report、一个写Example-report。这种不一致在单机调试时很容易被“反正能跑”糊过去,换台机器就全线崩,后面避坑那一章会单独说。
理解了这个分层,调试思路也就有了:表里没数据就查全局脚本的触发条件,表里有数据但画面空就查查询脚本的 SQL 和字段名,画面有数据但打印报错就查 Excel COM 调用。玄学问题基本不存在,都是一层一层能验的。
2.2 为什么是 VBS + ADO,而不是自带归档控件
WinCC 自带的趋势和归档控件画曲线很好用,但真要做“选个日期区间、列出每一行原始记录、末尾加统计行、再导成 Excel 打印”这种报表,它就很别扭:归档控件的查询条件受限于组态,导出格式也由控件自己决定,而且归档数据落在项目目录里,别的系统想读就得额外折腾。
VBS 方案的取舍很明确。好处是脚本直接跑在 WinCC 运行时进程里,HMIRuntime.Tags读写变量零成本,ADO 走 OLEDB 连 SQL Server 不需要额外装驱动,数据落到标准数据库后,别的系统、别的报表工具、甚至直接在管理工具里跑 SQL 都能读到。代价也得认:全靠 COM 组件,控件要注册;SQL 是字符串拼出来的,没有类型检查,一个引号写错就整段执行失败;脚本没有版本管理,改错了只能靠记忆回滚。
我一般会在项目初期就把这套东西的边界划清楚:单表、单库、按天查询、数据量控制在几万行以内,这套方案很舒服。如果要按班次、按产线、按几十个变量横向对比,那就得往上堆 SQL 分组和索引,脚本也会越来越长,那时候不如换成存储过程 + 视图,脚本只负责取结果。
2.3 环境准备:SQL 实例、WinCC 项目与 OCX 注册
动手前先确认三件事都齐了,缺一样后面都会卡住。
第一步是 SQL Server 侧。打开管理工具连上实例,在 Databases 上右键 New Database,名字按项目约定取(原文用的SQL-Report,我建议直接用项目代号加_Report后缀,省得以后几个项目撞名)。然后在该库的 Tables 上右键 New Table,把列名和数据类型填进去,保存时给表起名。库名和表名一旦定下来,后面所有脚本都得照抄,别凭记忆敲。
第二步是 WinCC 侧。新建项目、建内部变量、建主画面和报表画面,这些都按常规走。真正容易卡的是 ActiveX 控件的注册——MSFlexGrid 属于 VB6 时代的组件,Windows 默认不带。
:: 32 位系统或 32 位进程:把控件拷到 System32 后注册 copy MSFLXGRD.OCX C:\Windows\System32\ regsvr32 C:\Windows\System32\MSFLXGRD.OCX :: 解除注册(控件版本冲突、画面打不开时用) regsvr32 /u C:\Windows\System32\MSFLXGRD.OCX命名的含义很直白:regsvr32后面跟 OCX 的完整路径就是注册,加/u是反注册。这里有个血泪经验——在 64 位系统上,WinCC 运行时是 32 位进程,只认 32 位控件,必须用C:\Windows\SysWOW64\regsvr32.exe去注册放在SysWOW64目录下的 OCX,用开始菜单里那个默认的 regsvr32 注册完了画面照样报“未注册类”。日期控件 DTPicker 依赖MSCOMCT2.OCX,同样要按这个流程走一遍。
第三步,确认控件能在画面里被选到。在画面的 ActiveX Controls 上右键 Add/Remove,列表里应该能看到Microsoft FlexGrid Control, Version 6.0和Microsoft Date and Time Picker Control 6.0 (SP4)。如果列表里没有,说明注册没生效或者位数不对,先回去查上一步,别急着往脚本上找原因。
提示:把 OCX 拷进系统目录这一步,等于给整台机器装了个全局组件。项目交付前记得在目标机器上重复一遍,别只在开发机上验证。
3. 写入链路:建表规范与全局脚本 action
写入这一段是整套方案的地基。表结构定歪了,后面查询脚本怎么写都别扭;触发周期定错了,一天下来几万条冗余数据,画面点查询要转半天。
3.1 建库建表:字段类型定死后面所有脚本
图形界面点出来的表,本质上就是一条 DDL。我把原文那张表还原成语句,顺手把字段名统一了——原文写入脚本里时间字段叫time,查询脚本里叫shijian,这是第一处不一致,建表时统一成shijian,后面两张脚本都照这个写。
-- 报表主表:一行对应一次采样 CREATE TABLE dbo.VBSReport ( id INT IDENTITY(1,1) PRIMARY KEY, shijian DATETIME NOT NULL, -- 采样时刻,与查询脚本字段名保持一致 cpu FLOAT NULL, -- CPU 占用率,单位 % ram FLOAT NULL, -- 内存占用率,单位 % harddisk FLOAT NULL -- 硬盘占用率,单位 % ); -- 时间是唯一的查询条件,必须建索引,否则一天的数据都要全表扫 CREATE INDEX IX_VBSReport_shijian ON dbo.VBSReport(shijian);几个参数值得停一下。DATETIME而不是VARCHAR存时间,是为了让BETWEEN能走索引、能按范围比较,如果存成字符串,'2025-3-1'和'2025-03-01'就是两个值,区间查询立刻失效。三个指标用FLOAT够用,如果现场变量本身是整型百分比,用SMALLINT更省空间。id自增主键不是必须的,但加了以后排查重复写入很方便——看id是不是连续就能判断全局脚本有没有被执行两次。
原文用图形界面建表,好处是直观,坏处是字段类型选错了没人提醒。我习惯在图形界面建完之后,右键生成一次脚本存到项目文档里,换环境时直接执行,不用再点一遍。
3.2 action 函数逐段:从读变量到写库
全局脚本里的写入函数是最短的一段,但每一行都有坑位。原文的action函数拆开看是这样:
Option Explicit Function action Dim a, b, c, d Dim strcn, cn, is_SQL ' 1. 取当前时间 + 三个现场变量 a = Now b = HMIRuntime.Tags("cpu").Read c = HMIRuntime.Tags("ram").Read d = HMIRuntime.Tags("hd").Read ' 2. OLEDB 连接串:库名、实例名、认证方式三要素 strcn = "Provider=SQLOLEDB.1;Integrated Security=SSPI;" & _ "Persist Security Info=False;" & _ "Initial Catalog=SQL-Report;Data Source=.\wincc" ' 3. 打开连接并执行插入 Set cn = CreateObject("ADODB.Connection") cn.ConnectionString = strcn cn.Open is_SQL = "insert into VBSReport(shijian,cpu,ram,harddisk) Values('" & _ a & "','" & b & "','" & c & "','" & d & "')" cn.Execute is_SQL cn.Close Set cn = Nothing End Function连接串的四个参数各有分工:Provider=SQLOLEDB.1指定用 SQL Server 的 OLEDB 提供程序;Integrated Security=SSPI表示走 Windows 集成认证,也就是用运行 WinCC 的那个系统账号登数据库,不需要在脚本里放用户名密码;Initial Catalog是数据库名,必须和建库时写的一字不差;Data Source=.\wincc里的点代表本机,wincc是实例名,如果现场是默认实例就只写.。
拼接 INSERT 那段是最脆的地方。Now返回的是区域格式的日期字符串,中文系统下大致是2025/3/1 14:22:01这种形态,SQL Server 一般能识别,但一旦服务器语言设置或区域选项不同,就会出现“从字符串转换日期和/或时间时失败”。稳妥做法是显式格式化:
' 用 ISO 格式拼时间,避开区域设置差异 Dim ts ts = Year(a) & "-" & Right("0" & Month(a), 2) & "-" & _ Right("0" & Day(a), 2) & " " & _ Right("0" & Hour(a), 2) & ":" & Right("0" & Minute(a), 2) & ":" & _ Right("0" & Second(a), 2)Right("0" & x, 2)是补零的土办法,个位数月份补成03,SQL Server 无论什么区域设置都能按yyyy-mm-dd hh:mi:ss正确解析。这种写法比调Format()可靠,因为Format()的输出受系统区域设置影响,脚本换台机器结果就变。
注意:
HMIRuntime.Tags("cpu").Read里的变量名必须和 WinCC 变量管理里完全一致,大小写不敏感但拼写不能错。读不到变量不会报错,只会返回空值,写进库就是一堆 NULL。
3.3 触发条件:周期怎么定、数据量怎么估
全局脚本写完只是躺在那儿,不配触发器它一行都不会跑。原文特意标注了“注意:全局脚本触发条件”,这确实是新手最容易漏的一步。在全局脚本编辑器里给这个动作挂一个定时器触发器,周期按需要选。
周期和数据量的关系是可以算的:1 秒一次,一天 86400 条;10 秒一次,8640 条;1 分钟一次,1440 条。报表按天查,1 分钟粒度已经足够看出趋势,一天 1440 行铺进 MSFlexGrid 也不至于卡。真按秒级采,一个月就是两百多万行,查询脚本又没做分页,点一次“查询”画面能僵在那儿十几秒,操作工以为死机了。
如果变量本身变化不频繁,用“变量触发”比定时更省:在 WinCC 里给变量配一个变化触发,值变了才写一条。缺点是采样间隔不均匀,做平均值统计时每个点的权重不一样,对日报表来说这点误差通常能接受。
还有一件事原文没提但必须做:表大了之后要清理。最简单的办法是建一个每日执行的 SQL 作业,把 N 天前的记录删掉或者转存到历史表。不清理的话,两三年后这张表能撑到几个 G,查询脚本即使有索引也会越来越慢。
4. 查询链路:DTPicker 取值、两条 SQL 与 MSFlexGrid 填充
查询这一段是整套方案里代码量最大的部分,也是最能体现设计思路的地方:用一条 SQL 拿明细、另一条 SQL 拿统计,避免在 VBS 里循环算最大最小平均;用控件的TextMatrix直接定位单元格赋值,避免一行行 Add。拆开看就四步。
4.1 两个 ActiveX 控件:插入画面与命名约定
在日报表画面里,从 ActiveX Controls 里把Microsoft FlexGrid Control, Version 6.0和Microsoft Date and Time Picker Control 6.0 (SP4)拖到画面上,各放两个日期控件(起、止),一个表格控件。放好之后第一件事是改名字——脚本里全靠ScreenItems("名字")拿对象,名字对不上就是运行时“对象不存在”。
我一般约定成Date1(起始)、Date2(结束)、MSFlexGrid1(表格),和原文保持一致。控件属性里能改的项不少,关键的几个是:MSFlexGrid 的Rows和Cols决定初始行列数(脚本里会重新赋Rows,所以初始值不重要)、FixedRows设 2 让前两行固定不滚动、FixedCols设 0。DTPicker 的Format属性设成日期格式,操作工点开就是日历,比手输靠谱。
如果插入控件时画面报错、控件显示成红叉,八成还是注册问题,回第 2 章第 3 节按位数重新注册一遍。这一步没通,后面所有脚本都白写。
4.2 日期取值与合法性校验
DTPicker 的Value属性直接就是Date类型,取年月日不需要解析字符串:
Sub OnClick(ByVal Item) Dim Date1, Date2 Dim By, Bm, Bd, Ny, Nm, Nd Dim BeginDate, EndDate Set Date1 = ScreenItems("Date1") Set Date2 = ScreenItems("Date2") ' 取出起止日期的年月日 By = Year(Date1.Value) : Bm = Month(Date1.Value) : Bd = Day(Date1.Value) Ny = Year(Date2.Value) : Nm = Month(Date2.Value) : Nd = Day(Date2.Value) ' 起点补 00:00:00,终点补 23:59:59,保证当天数据不漏 BeginDate = By & "-" & Bm & "-" & Bd & " 00:00:00" EndDate = Ny & "-" & Nm & "-" & Nd & " 23:59:59" ' 起止时间颠倒时给出提示 If By > Ny Or (By = Ny And Bm > Nm) Or (By = Ny And Bm = Nm And Bd > Nd) Then MsgBox "输入的时间不正确", vbOK, "错误的起始时间" Exit Sub ' 原文缺这一句,见第 5 章 End If End Sub校验那段的多条件判断按“年大 → 同年月大 → 同年月日大”逐级比较,短句Or连起来虽然不好看,但逻辑是对的。真正的问题在没有Exit Sub:提示弹完之后脚本继续往下跑,还是会去查一个非法区间,结果要么空结果集、要么 SQL 报错。这是我加的第一处改动。
日期拼成"2025-3-1 00:00:00"这种没有补零的格式,SQL Server 能认,因为它是按yyyy-m-d解析的。但如果哪天要把这套逻辑搬到别的数据库,就得改成补零的 ISO 格式,别指望每个数据库都这么宽容。
4.3 明细 + 聚合两条 SQL:一次把数据拿全
查询用两条 SQL,一条出明细、一条出统计,都在同一次点击里执行:
' 明细:按时间升序取出区间内所有原始记录,时间统一成 19 位字符串 Sql = "SELECT CONVERT(char(19), shijian, 20) AS shijian, cpu, ram, harddisk " & _ "FROM report WHERE shijian BETWEEN '" & BeginDate & "' AND '" & EndDate & "' " & _ "ORDER BY shijian" ' 统计:一次算出 3 个指标的 avg / min / max,共 9 个值 Sql1 = "SELECT avg(cpu) AS ylp, avg(ram) AS wdp, avg(harddisk) AS llp, " & _ "min(cpu) AS ylx, min(ram) AS wdx, min(harddisk) AS llx, " & _ "max(cpu) AS yld, max(ram) AS wdd, max(harddisk) AS lld " & _ "FROM report WHERE shijian BETWEEN '" & BeginDate & "' AND '" & EndDate & "'"CONVERT(char(19), shijian, 20)里的20是 ODBC 标准样式,输出固定为yyyy-mm-dd hh:mi:ss,长度正好 19 位。这一步很关键:明细查出来的时间如果直接按DATETIME显示,VBS 转字符串时会带上毫秒和区域格式,铺进表格里长短不一,看着很乱。转成定长字符串后,后面用Mid(t,11,16)截取时分秒也不会错位。
BETWEEN是闭区间,起点00:00:00、终点23:59:59,正好把整天圈进来。用< 次日 00:00:00的开区间写法更严谨(不会漏掉23:59:59.500这种毫秒数据),但现场变量按分钟采的话,这个差异可以忽略。
两条 SQL 共用一个连接,省掉一次握手开销。命令对象用CommandType = 1(即adCmdText)直接执行文本,Set oRs = oCom.Execute拿记录集。这里有个细节原文做对了:连接对象设了CursorLocation = 3(adUseClient,客户端游标),所以oRs.RecordCount能拿到真实行数。默认的服务端游标下RecordCount返回-1,后面MSFlexGrid1.Rows = oRs.RecordCount + 6就会算出个负数,控件直接报错。
统计那条 SQL 的结果只有一行,取出来是 9 个字段:
' 统计结果只有一行,直接按别名取 ylp = oRs1("ylp") : wdp = oRs1("wdp") : llp = oRs1("llp") ' 平均 ylx = oRs1("ylx") : wdx = oRs1("wdx") : llx = oRs1("llx") ' 最小 yld = oRs1("yld") : wdd = oRs1("wdd") : lld = oRs1("lld") ' 最大变量名用的是拼音首字母:ylp= 利用率平均、ylx= 最小、yld= 最大,对应 cpu;w开头对应内存,ll开头对应硬盘。这种命名在小脚本里能接受,一旦要让别人接手,还是改成avgCpu / minCpu / maxCpu更省事——这也是二次开发那一章要动手改的第一件事。
4.4 填充 MSFlexGrid:行数、表头与统计行
记录集拿到之后,剩下的就是往表格里铺。这一步的核心是算清楚行号:
Set MSFlexGrid1 = ScreenItems("MSFlexGrid1") MSFlexGrid1.Clear MSFlexGrid1.Rows = oRs.RecordCount + 6 ' 2 行表头 + N 行数据 + 3 行统计 + 1 行余量 ' 列宽按内容估,单位是像素 MSFlexGrid1.ColWidth(0) = 900 ' 序号 MSFlexGrid1.ColWidth(1) = 1900 ' 时间 MSFlexGrid1.ColWidth(2) = 1000 ' CPU% MSFlexGrid1.ColWidth(3) = 1000 ' RAM% MSFlexGrid1.ColWidth(4) = 1100 ' HardDisk% ' 第 0 行合并成一整条标题 For z = 0 To 4 MSFlexGrid1.Col = z MSFlexGrid1.Text = "Computer Ultilization Message(per Day)" Next MSFlexGrid1.MergeCells = 4 ' flexMergeFree,允许任意单元格合并 MSFlexGrid1.MergeRow(0) = True ' 按行方向合并 ' 第 1 行是列名 MSFlexGrid1.TextMatrix(1, 0) = "NO." MSFlexGrid1.TextMatrix(1, 1) = "Time" MSFlexGrid1.TextMatrix(1, 2) = "CPU%" MSFlexGrid1.TextMatrix(1, 3) = "RAM%" MSFlexGrid1.TextMatrix(1, 4) = "HardDisk%" ' 最后三行的行首标签 MSFlexGrid1.TextMatrix(oRs.RecordCount + 3, 0) = "MAX" MSFlexGrid1.TextMatrix(oRs.RecordCount + 4, 0) = "MIN" MSFlexGrid1.TextMatrix(oRs.RecordCount + 5, 0) = "AVG"TextMatrix(row, col)是 MSFlexGrid 最常用的属性,直接按行列坐标读写文本,不需要先定位再赋值。行号分配是:0 行合并标题、1 行列名、2 到RecordCount+1是数据、+3 / +4 / +5分别是 MAX / MIN / AVG。原文里+2那行是 SUM,被注释掉了——三个百分比相加没有业务含义,删得对。
MergeCells = 4对应flexMergeFree,配合MergeRow(0) = True,第 0 行相邻的同内容单元格才会合起来。如果不设MergeCells,MergeRow是不生效的,标题会显示成五段重复文字,这是很典型的一处顺序依赖。
数据循环里有两个细节要留意:
For i = 1 To oRs.RecordCount MSFlexGrid1.TextMatrix(i + 1, 0) = i ' 序号从 1 开始 Next这段单独跑一遍循环只为填序号,然后才是真正铺数据的那段Do While Not oRs.EOF。分成两段的原因很简单:TextMatrix的行号要先确定,而填充数据时会移动游标。原文在数据循环里还夹了一句n = n + 1,n在上面已经被赋成RecordCount用过了,循环里再自增就把它污染了——后面再用n做判断的地方全都不对。这种变量复用是脚本里最常见的隐性 bug,改的时候直接把循环里那句删掉。
时间列的显示还做了一次条件处理:起止日期是同一天时,只显示Mid(t, 11, 16)也就是hh:mm;跨天时显示完整时间戳。这样单日报表清爽,多日报表也不会混。
提示:
Rows一次赋到位,不要在循环里往AddItem。MSFlexGrid 每改一次行数都会重绘,几百行逐条加能明显看到闪烁。
4.5 打印:MSFlexGrid 转 Excel 再预览
打印这一段不碰数据库,纯粹是把表格内容搬到 Excel:
Set MSFlexGrid1 = ScreenItems("MSFlexGrid1") Set ExcelApp = CreateObject("Excel.Application") Set ExcelBook = ExcelApp.Workbooks.Add Set ExcelSheet = ExcelBook.Worksheets(1) ExcelApp.Visible = True ' 调试期开着看,交付时建议改 False ' 逐格搬运:Excel 的 Cells 行列都从 1 开始,控件从 0 开始 For irow = 0 To MSFlexGrid1.Rows - 1 For icol = 0 To MSFlexGrid1.Cols - 1 ExcelSheet.Cells(irow + 1, icol + 1) = Trim(MSFlexGrid1.TextMatrix(irow, icol)) Next Next ' 给数据区加外框:1 左 2 右 3 上 4 下 z = MSFlexGrid1.Rows ExcelSheet.Range("A1:E" & z).Borders(1).Weight = 2 ExcelSheet.Range("A1:E" & z).Borders(2).Weight = 2 ExcelSheet.Range("A1:E" & z).Borders(3).Weight = 2 ExcelSheet.Range("A1:E" & z).Borders(4).Weight = 2 ExcelSheet.Rows(1).RowHeight = 0.75 / 0.035 ' 厘米转磅 ExcelSheet.Cells.EntireColumn.AutoFit ExcelSheet.Rows(1).Font.Name = "宋体" ExcelSheet.Rows(1).Font.Bold = True ExcelSheet.Cells.HorizontalAlignment = 3 ' 居中 ExcelSheet.PageSetup.CenterHorizontally = True ExcelSheet.PrintPreview ' 换成 PrintOut 就直接打印 ExcelBook.Close ExcelApp.Quit Set ExcelApp = Nothing行列索引的错位是最容易翻车的地方:控件TextMatrix(0,0)对应 Excel 的Cells(1,1),所以取的时候要统一+1。整个搬运是双层循环,单元格数量等于行数乘列数,一天 1440 行就是七千多次 COM 调用,能明显感觉到卡顿——不追求样式的场景,直接把二维数组一次性赋给Range.Value会快一个数量级。
RowHeight那个0.75 / 0.035是把厘米换算成磅,Excel 的行高单位是磅,1 厘米约等于 28.35 磅,所以反过来除。这种魔法数字建议提出来定义成常量。
ExcelApp.Quit和Set ExcelApp = Nothing这两句千万不要省。少一句,Excel 进程就常驻在后台,操作工点十次打印就攒十个EXCEL.EXE,最后内存吃满,现场排查起来一头雾水。真出了这事,任务管理器里结束进程只是临时办法,根治还得靠脚本里成对写Quit+Nothing。
PrintPreview和PrintOut二选一:前者先给操作工看一眼再决定打不打,后者点一下就出纸。车间里我倾向用预览,省纸也省得打错了重新来。
5. 避坑与排查:控件注册、连接串、字段名最容易耗掉一整天
这套方案代码量不大,但涉及 WinCC、Windows 组件、SQL Server、Excel 四个独立系统,任何一处配置不一致,现象都很像“脚本写错了”。下面几条是我在这类项目里反复遇到的。
现象一:画面一打开就报“未注册类”,或者控件位置显示红叉。原因:MSFlexGrid / DTPicker 属于 COM 组件,开发机上装过 VB6 所以能选到,目标机器上根本没注册;或者注册了 64 位版本,而 WinCC 运行时是 32 位进程,加载不了。 解决:先用C:\Windows\SysWOW64\regsvr32.exe注册SysWOW64目录下的MSFLXGRD.OCX和MSCOMCT2.OCX,重启 WinCC 运行时再看。如果还不行,在画面里删掉控件重新插一遍——控件对象里存的是 CLSID,注册信息变了以后旧对象可能失效。
现象二:查询报“对象名 'report' 无效”,或者语句能跑但永远查出 0 行。原因:写入脚本和查询脚本用的不是同一张表、同一批字段、同一个库。原文里写入插的是VBSReport(time,cpu,ram,hd),查询读的是report(shijian,cpu,ram,harddisk),连接串的Initial Catalog一个写SQL-Report、一个写Example-report——三处全对不上。 解决:把库名、表名、字段名、连接串这四样写在一张纸上贴显示器边上,所有脚本只能出现纸上的名字。改完在管理工具里先跑一遍同样的 SELECT,确认有数据再回画面点按钮。
现象三:oRs.RecordCount得到 -1,接着Rows = -1 + 6,控件报参数错误。原因:ADO 默认用服务端游标,RecordCount不可用,返回 -1。 解决:打开连接后、执行查询前,给Connection设CursorLocation = 3(adUseClient),改用客户端游标。代价是大结果集会一次性拉到内存,几万行以内没问题,上十万行就该考虑分页了。
现象四:日期选的是当天,但只能查到前一天的记录,或者一条都查不到。原因:结束时间没补到23:59:59,BETWEEN '2025-3-1 00:00:00' AND '2025-3-1 00:00:00'只能命中零点整那一条;或者时间字段是VARCHAR类型,'2025-3-1'和'2025-03-01'字符串比较时排序结果和预期相反。 解决:时间字段一律用DATETIME,查询时起点补00:00:00、终点补23:59:59。拼接前先Exit Sub拦掉非法区间,别让脚本带着错日期往下跑。
现象五:全局脚本编译通过、运行时也不报错,但表里就是没数据。原因:触发器没配,或者配了但周期设得太长(比如 1 小时一次),调试时等了五分钟以为没生效;也可能是HMIRuntime.Tags("cpu")里的变量名拼错,读到的是空值,插进去全是NULL。 解决:先看全局脚本编辑器里那个动作有没有挂触发器,再把周期临时改成 10 秒验证。变量名直接从变量管理里复制,别手敲。验证阶段可以在脚本里加一行HMIRuntime.Trace "action fired: " & Now & vbCrLf,输出到 WinCC 的诊断窗口,比弹MsgBox温和得多——运行时弹窗会阻塞画面刷新。
现象六:打印几次之后机器变慢,任务管理器里躺着一排EXCEL.EXE。原因:脚本中途报错跳过了Quit,或者只写了Close没写Quit。 解决:把ExcelBook.Close和ExcelApp.Quit放在错误处理之后也要执行的路径上,VBS 里没有finally,至少保证正常路径上成对出现,末尾加Set ExcelApp = Nothing释放引用。
6. 二次开发:把一次性脚本改成能维护的报表模块
这套脚本能跑,但它是按“一次性交付”写的:连接串硬编码在每个 Sub 里,字段名中英混用,变量名是拼音首字母,SQL 靠字符串拼。要在这上面继续加需求(按班次统计、多变量对比、导出 PDF),不先做一轮整理,改到第三个需求就会失控。
几个改动优先级最高的地方:
| 现状 | 改法 | 收益 |
|---|---|---|
| 连接串散落在每个 Sub | 抽成一个GetConn()函数放全局脚本 | 换库只改一处 |
| 表名字段名不统一 | 建视图v_report屏蔽差异 | 查询脚本不再关心物理表 |
| 字符串拼 SQL | ADODB.Command+CreateParameter | 免去转义,防注入 |
| 拼音变量名 | 改成avgCpu / minRam这类 | 交接成本直线下降 |
最值得先做的是连接串和查询的封装。全局脚本里加一个函数,画面脚本只调它:
' 全局脚本:统一连接入口,换库只改这里 Function GetConn() Dim cn Set cn = CreateObject("ADODB.Connection") cn.ConnectionString = "Provider=SQLOLEDB.1;Integrated Security=SSPI;" & _ "Initial Catalog=SQL-Report;Data Source=.\wincc" cn.CursorLocation = 3 ' 客户端游标,保证 RecordCount 可用 cn.Open Set GetConn = cn End Function验证方法很土但有效:改完之后先不点画面按钮,在管理工具里把同一条 SQL 手工跑一遍,看结果集的行数和首末行时间对不对;对得上再回画面点查询,两边数字一致才算通过。这个顺序能帮你区分“SQL 写错了”和“脚本搬数据搬错了”,省掉大量来回猜的时间。
还有一个具体技巧:查询结果超过 2000 行时,别一次性铺满表格,加个TOP 2000或者按小时分段加载,并在画面角落显示“共 N 条,显示前 2000 条”。操作工真正关心的往往是趋势和统计行,明细只是佐证,把画面拖死反而没人用。
从那以后我每次接这类报表需求,都先把库名、表名、字段名、连接串四样东西抄在一张纸上贴在显示器边上,脚本里只允许出现纸上的名字,写完再对着纸逐字核一遍——这套笨办法救过我至少三次通宵。希望帮到你。
本文还有配套的精品资源,点击获取