简介:这是一份以电子文档形式整理的西门子WINCC SQL报表查询实现教程,面向工业自动化工程师与组态软件开发者,重点解决在WINCC人机界面中借助VBS脚本和ActiveX控件完成生产数据入库、查询与报表展示的问题。文档从SQL Server 2005数据库创建讲起,内容依次覆盖新建数据库、定义表格字段、编写WINCC全局脚本写入数据,再到报表画面组态中使用表格显示控件和日期选择控件实现按日期查询、按钮触发显示以及导出为电子表格后打印,整体流程清晰可循。压缩包仅含1个文档文件,大小1.11MB,累计已有1680人学习下载。附录给出了数据库写入与查询的完整VBS脚本示例,可直接参考改造;针对表格显示控件无法正常注册使用的常见环境问题,也给出了控件注册命令的详细解决方法,帮助读者避开实操配置坑点。
1. 为什么非要用 VBScript 在 WINCC 里写 SQL 报表:先绕开三个最常见的弯路
做过程控制的工程师迟早会撞上同一个需求:现场几十台设备跑了一天,报警、产量、温度曲线全躺在数据库里,领导第二天早上要报表,而你手里的 WINCC 画面除了趋势控件和归档表格,拿不出能自动汇总、筛选、导出 Excel 的东西。于是有人去翻 WinCC Online Help,有人去装 Report Generator,有人用 WinCC 自带的 OLEDB Provider 在外部写程序——结果不是授权没到位,就是报表格式对不上现场习惯,最后所有人都回到同一个解法:在 WINCC 画面里用 VBScript 写 SQL 查询,把结果丢给表格控件。
这套方案的真正价值,不是它比 Report Generator 功能多,而是它不依赖额外授权、不要求现场装报表软件、不会因为画面发布之后就改不了脚本。你只需要三样东西:一个能跑脚本的 WINCC 画面、一个能放数据的表格控件(常见的是微软的 MSFlexGrid 或 MSHFLexGrid,也有不少人用 Siemens 自己的 Control Tool 里的控件)、一段能连上 SQL Server 的 VBScript。看起来简单,但实际做的时候,坑几乎都埋在“控件拿不到数据”“脚本报错却查不到原因”“SQL 语句里中文乱码”这三件事上。这篇文章我把整个实现过程、参数设置和踩过的坑一次说清楚。
适合看这个方向的人:已经在用 WINCC 7.x、维护或开发过程画面,手头有 SQL Server 或 Windows 认证的数据库,需要做日报/周报/历史报警查询。如果你刚碰 WINCC,连 C 脚本和 VBScript 都分不清,建议先跑通一个最小例子再往下读。
2. 先把架构和原理捋清楚:WINCC 里的 VBScript 凭什么能查 SQL Server
2.1 WINCC 脚本体系里,为什么选 VBScript 而不是 C 脚本
WINCC 从 6.0 开始就内置了两套脚本环境:C 脚本(C Script)和 VBScript。C 脚本更适合做画面对象的消息触发、变量读写和简单数学计算,但一旦涉及字符串拼接、ADO 对象调用、动态创建控件实例,写起来就特别别扭。VBScript 的优势恰好在这里体现:它天生就是面向对象的脚本语言,创建 ADODB.Connection、ADODB.Recordset、调用控件方法这些操作,语法直接,调试时还能用 MsgBox 弹中间结果,而 C 脚本里弹窗要调用 WinCC 的 MessageBox 函数,麻烦得多。
还有一个关键区别:VBScript 可以直接访问 Windows 脚本宿主提供的对象模型,而 C 脚本做不到。这一点直接决定了 SQL 查询功能的实现路径——你在 WINCC 画面里写的 VBScript 本质上和 Windows 下任意一个 .vbs 文件没有太大区别,只是运行在 WINCC 的进程里、能同时读取 WINCC 的变量归档和画面对象。所以你可以把这段脚本理解成:一个跑在 WINCC 内部的“小工具”,通过 ADO 去连数据库,再把查询结果填到画面上的表格控件里。
在实际的项目里,我通常的做法是:把所有 SQL 查询逻辑集中写在一个全局脚本或画面脚本里,从按钮事件调用。不要在每一个按钮事件里都写一遍连接字符串和 SQL 语句,否则后期维护字段映射时会疯掉。这算是第一个原则:脚本分层,连接与查询分离。
2.2 ADO + SQL Server:这套查询链路的工作流程
整个查询链路一共四步,和你在任何开发环境里写数据库程序没有本质区别,只是运行环境变成了 WINCC 画面:
第一步,创建 ADODB.Connection 对象,指定连接字符串,包括数据库服务器地址、数据库名称、认证方式(Windows 认证或 SQL 认证)。 第二步,打开连接。 第三步,创建 ADODB.Recordset 或直接执行 SQL 语句,把结果集拿到内存里。 第四步,把 Recordset 的内容逐行写入表格控件对应的单元格。
这套链路里有三个最容易被忽视的细节。第一个是连接字符串里的 Provider,WINCC 项目常用的是 SQLOLEDB,但如果你用 SQL Server 2008 以上版本,SQLNCLI 或 MSOLEDBSQL 更合适,这直接关系到能否读到 VAR 表的归档数据。第二个是 Recordset 的游标类型,如果只用默认的只读游标,在某些控件绑定场景下会导致无法获取行数,建议显式指定 adOpenKeyset。第三个是表格控件的填充方式——很多人试图用数据源绑定(DataSource 属性)直接显示查询结果,但对 MSFlexGrid 这类控件,数据源绑定经常因为列类型不匹配崩溃,老老实实循环写单元格反而最稳定。
2.3 关键选型:表格控件用哪个,数据怎么走
WINCC 画面里能放哪些控件,取决于你的 WINCC 版本和安装时选了哪些选件。常见的选择有三个:MSFlexGrid、MSHFlexGrid(微软的)、Siemens WinCC Dashboard Control(西门子的)。MSFlexGrid 是最老牌的,虽然界面丑了点,但胜在稳定、方法齐全、网上资料多。MSHFlexGrid 是 MSFlexGrid 的升级版,支持 ADO 数据绑定和分层显示,但绑定模式在 WINCC 里兼容性不如老版本。西门子自己的控件和其他选件绑定更紧密,但需要额外授权,而且上手难度高。
我个人的建议是:别折腾控件选型,画面里放一个 MSFlexGrid(通常 ActiveX 控件注册后名为 MSFlexGridLib.MSFlexGrid 或类似名称),然后所有数据填充都用脚本循环写。具体做法是:先设置固定行数(FixedRows)和总行数(Rows)、列数(Cols),然后按行按列写入 TextMatrix 或设置 Text 属性。这样做的性能虽然不如数据源绑定,但查询结果最多几千行,报表场景完全够用,而且调试起来清清楚楚。
3. 做一张能查 SQL 报表的画面:数据库准备与第一个最小脚本
3.1 建表和测试数据:先把 SQL Server 这边搞定
不管 WINCC 那边脚本怎么写,数据库这边得先有个能查的表。大多数场景是查 WINCC 的变量归档日志,即 RT_xxx 表,但为了不受归档表结构和权限影响,建议先在 SQL Server 里建一张自己的业务表,插几行测试数据,把脚本链路跑通后再换回归档表。
下面这段 SQL 是典型的简化示例,实际项目中你会新建一个作业记录表来存产量和报警计数。
-- 建一张简单的作业记录表,用来做报表查询测试 CREATE TABLE dbo.Report_Test ( RecordID INT IDENTITY(1,1) PRIMARY KEY, ProductName NVARCHAR(50), DeviceName NVARCHAR(50), OutputQty INT, AlarmCount INT, RecordTime DATETIME ); -- 插入几行测试数据,日期可以用 GETDATE() 生成当天时间 INSERT INTO dbo.Report_Test (ProductName, DeviceName, OutputQty, AlarmCount, RecordTime) VALUES (N'产品A', N'设备1', 120, 3, GETDATE()), (N'产品B', N'设备2', 80, 0, GETDATE()), (N'产品A', N'设备2', 95, 1, GETDATE());这里有个很容易忽略的事情:字段类型尽量用 NCHAR、NVARCHAR 而不是 CHAR、VARCHAR,因为在 WINCC/VBScript 环境下,中文字符串的编码常用 UTF-16,如果你在数据库连接字符串里没有指定字符集,字段类型又是 VARCHAR,查询结果里中文就会变成问号或乱码。后面避坑章节会专门说。
3.2 在 WINCC 画面里放控件并注册名称
数据库表建好了,接下来回到 WINCC 图形编辑器。新建一个画面,从“对象选项板”的“控件”里拖一个 MSFlexGrid 到画面中央,把它拉到你想要的表格大小。然后右键控件,在属性对话框里把控件名称改成一个有意义的标识符,比如 FrxReportGrid:
修改控件名称这个动作极其重要,因为后面 VBScript 里访问控件全靠这个名字。很多人直接在脚本里写 ScreenItems("MSFlexGrid1"),结果发布到别的机器上控件实例名不一致,脚本立刻报错。建议从一开始就统一命名规则:报表相关控件一律用 Frx 开头,后面跟用途。
3.3 第一个查询脚本:简单查询并填充表格
现在写第一个能跑的脚本。在画面里放一个按钮,按钮的鼠标点击事件里写 VBScript。下面是一个最简版本,只做一件事:查询 Report_Test 表的所有数据并填进表格。
' 获取画面中的表格控件 Dim oFrx Set oFrx = ScreenItems("FrxReportGrid") ' 创建 ADO 连接和记录集对象 Dim oCn Dim oRs Set oCn = CreateObject("ADODB.Connection") Set oRs = CreateObject("ADODB.Recordset") ' 连接字符串:注意服务器名、库名要改成实际的 ' 这里用 Windows 集成认证,所以不需要 UID/PWD Dim sConn sConn = "Provider=SQLOLEDB;Data Source=localhost;Initial Catalog=TestDB;Integrated Security=SSPI" ' SQL 查询语句:先只查全表验证链路 Dim sSQL sSQL = "SELECT ProductName, DeviceName, OutputQty, AlarmCount, RecordTime FROM dbo.Report_Test" ' 打开连接并执行查询 oCn.Open sConn oRs.CursorLocation = 3 ' 3 = adUseClient,客户端游标,方便统计行数 oRs.Open sSQL, oCn, 3, 1 ' 3=adOpenKeyset, 1=adLockReadOnly ' 先把表格清空,设置行列数 ' 行数 = 记录集行数 + 固定行(表头) + 1 Dim nRowCount, nColCount nColCount = oRs.Fields.Count nRowCount = oRs.RecordCount + 2 ' 1 行表头 + 1 行空余 oFrx.Rows = nRowCount oFrx.Cols = nColCount oFrx.FixedRows = 1 ' 写表头 Dim i For i = 0 To nColCount - 1 oFrx.TextMatrix(0, i) = oRs.Fields(i).Name Next ' 写数据 Dim r r = 1 Do While Not oRs.EOF For i = 0 To nColCount - 1 oFrx.TextMatrix(r, i) = CStr(oRs.Fields(i).Value) Next r = r + 1 oRs.MoveNext Loop ' 清理对象 oRs.Close oCn.Close Set oRs = Nothing Set oCn = Nothing注意代码里有两个小地方是我故意写出来的:oCn.Open 和 oRs.Open 的参数。很多网上流传的脚本版本在打开记录集时只写 SQL 和连接对象两个参数,导致后续读 RecordCount 得到 -1,或者光标定位出错。这里显式指定 CursorLocation、CursorType、LockType 是为了绕开这个老坑。
代码写完后,运行 WINCC 画面,点按钮,如果一切正常,表格里应该出现三行测试数据。如果不正常,先检查连接字符串里的 Data Source 和 Initial Catalog 是否写对,再检查 SQL Server 是否允许 Windows 集成认证的远程连接。WinCC 运行时用的登录账户必须是 SQL Server 里有权访问该数据库的账户,这个细节经常被漏掉——你本机调试没问题,一部署到工控机上就报“无法登录”,十有八九是这个问题。
3.4 参数化查询:怎么把画面上的输入条件传给 SQL
上面的例子只解决了“查全表”,但报表功能的核心是条件查询——按时间范围、按产品名、按设备名筛选。这时就要把画面上的输入框内容拼进 SQL 语句。常见的做法是用输入输出域或组合框,然后在脚本里读它们的 Text 属性。
下面的代码演示带时间范围的查询。这里有一个大坑:WINCC 里的日期时间格式和 SQL Server 的格式不一定一致。你从输入域里拿到的是一个字符串,直接拼到 SQL 里,经常因为格式对不上导致查询返回空。最稳妥的做法是在脚本里把字符串先转成标准格式,再用参数化方式传给 SQL。
' 从画面上的两个输入域取开始时间和结束时间 Dim dtStart, dtEnd dtStart = ScreenItems("IOStartTime").Text dtEnd = ScreenItems("IOEndTime").Text ' 拼 SQL:注意时间条件用的是 >= 和 <,而不是 BETWEEN Dim sSQL sSQL = "SELECT ProductName, DeviceName, OutputQty, AlarmCount, RecordTime " & _ "FROM dbo.Report_Test " & _ "WHERE RecordTime >= '" & dtStart & "' AND RecordTime < '" & dtEnd & "'"这种方式虽然简单,但有一个安全性隐患——SQL 注入,以及一个实用性隐患——日期格式。比如你从输入域拿到的是 2024-05-20 08:00:00,看起来没问题,但如果你在输入域里填了 2024-05-20 8:00 这种短格式,SQL 语句可能就会报转换失败。更复杂的场景是 WINCC 输入域内部对日期的处理还可能自动加上毫秒或时区信息,结果查出来的数据和你预想的不一致。
我的建议是:如果条件只涉及日期时间,就不要从输入域拿字符串,而是在脚本里用 Now 或 DateAdd 函数生成标准时间,然后 FormatDateTime 处理后再拼进去。如果条件涉及文本(比如设备名),则一定要用参数化查询。ADO 的 Command 对象支持参数化,虽然 VBScript 里写起来稍微繁琐,但能避免乱码和注入问题。后面避坑章节会展示一种折中方案。
4. 从固定报表到灵活查询:把表格控件和筛选条件联动起来
4.1 设计一个实用的报表查询界面布局
画面上只有一个查询按钮和一张表格,用起来还是太裸。实际项目里,查询界面通常长这样:顶部一行条件区——开始日期、结束日期、产品下拉框、设备下拉框、查询按钮、导出按钮;中部是表格区域;底部是本页数据统计。
WinCC 的输入输出域直接支持日期时间类型的显示和输入,下拉框可以用组合框控件(ComboBox)或 WINCC 自带的对象。这里有几个布局细节值得注意:第一,不要把开始日期和结束日期放在同一行且没有分隔,操作工容易点混,最好中间加一个带箭头的静态文本。第二,下拉框的选项不要写死在属性里,建议在画面打开事件里用脚本从数据库读取一次,这样以后设备列表变了不用动画面。第三,表格控件默认显示的行列数都偏小,一定要在画面属性或打开事件里设置好列宽——VBScript 里设置列宽是 oFrx.ColWidth(i),单位是缇(Twip),1 厘米约等于 567 缇。
设置列宽的代码可以放在画面的“打开画面”事件(OnOpen)里,这样每次打开报表画面,表格就自动格式化好。
' 画面打开事件:设置表格列数和列宽 Dim oFrx Set oFrx = ScreenItems("FrxReportGrid") ' 假设要显示5列,列宽依次设置 oFrx.Cols = 5 oFrx.ColWidth(0) = 2000 ' 产品名称 oFrx.ColWidth(1) = 2000 ' 设备名称 oFrx.ColWidth(2) = 1500 ' 产量 oFrx.ColWidth(3) = 1200 ' 报警次数 oFrx.ColWidth(4) = 3500 ' 记录时间 ' 表头背景色和字体可以一起设置 oFrx.FixedRows = 1 oFrx.Row = 0 oFrx.CellBackColor = 8421504 ' 灰色背景,RGB 值转十进制关于 CellBackColor 的 RGB 值,VBScript 里使用 Long 整型表示颜色,公式是 R + G256 + B65536,比如浅灰色 RGB(192,192,192) 对应 12632256。如果你直接填 VB 里那种 &H00C0C0C0&,在 WINCC 脚本里可能会解析异常,所以建议直接用十进制数。
4.2 把查询脚本拆成函数:让按钮事件只负责调用
当查询参数变多,SQL 语句变长,把所有代码都塞进按钮事件里会非常难看,而且排错极其痛苦。我习惯的做法是写一个独立函数 FillReportGrid(sWhere),接收一个 WHERE 条件的字符串,内部完成连接、查询、填充。然后按钮事件里只负责拼条件。
下面给出一个更完整的函数版本,说明如何把上一章的脚本重构成可维护的结构。这里我把连接字符串做成一个函数,方便统一修改数据库实例名。
' 全局脚本里写一个获取连接字符串的函数 Function GetSQLConnStr() ' 生产环境服务器名和库名集中在这里改 GetSQLConnStr = "Provider=SQLOLEDB;Data Source=192.168.1.10;Initial Catalog=FactoryDB;Integrated Security=SSPI" End Function ' 填充报表函数:参数是拼好的 WHERE 条件(不含 WHERE 关键字) Sub FillReportGrid(sWhere) Dim oFrx, oCn, oRs, sSQL, i, r, nCols Set oFrx = ScreenItems("FrxReportGrid") Set oCn = CreateObject("ADODB.Connection") Set oRs = CreateObject("ADODB.Recordset") ' 基础 SQL,后面按情况接 WHERE sSQL = "SELECT ProductName, DeviceName, OutputQty, AlarmCount, RecordTime FROM Report_Test" If Len(sWhere) > 0 Then sSQL = sSQL & " WHERE " & sWhere End If oCn.Open GetSQLConnStr() oRs.CursorLocation = 3 ' adUseClient oRs.Open sSQL, oCn, 3, 1 ' 表头 nCols = oRs.Fields.Count oFrx.Rows = oRs.RecordCount + 2 oFrx.Cols = nCols oFrx.FixedRows = 1 For i = 0 To nCols - 1 oFrx.TextMatrix(0, i) = oRs.Fields(i).Name Next ' 数据行 r = 1 Do While Not oRs.EOF For i = 0 To nCols - 1 ' 注意:日期字段要格式化后再显示 If oRs.Fields(i).Type = 135 Then ' 135 = adDBTimeStamp oFrx.TextMatrix(r, i) = FormatDateTime(oRs.Fields(i).Value, 0) Else oFrx.TextMatrix(r, i) = CStr(oRs.Fields(i).Value) End If Next r = r + 1 oRs.MoveNext Loop oRs.Close oCn.Close Set oRs = Nothing Set oCn = Nothing End Sub这段代码里有一个细节值得重点说:oRs.Fields(i).Type = 135 是 ADODB 里的时间戳类型。如果你直接把 datetime 字段的 Value 赋给 TextMatrix,显示出来的可能是类似 2024-05-20 08:00:00 的原始值,但有些环境下 SQL Server 返回的是带有小数秒的格式,还有时区转换问题。通过 FormatDateTime 格式化后,显示就稳定了。
重构后按钮事件里只需要这么做:
' 查询按钮点击事件 Dim sWhere sWhere = "" ' 如果开始时间不为空,就附加条件 If Len(ScreenItems("IOStartTime").Text) > 0 Then sWhere = sWhere & "RecordTime >= '" & ScreenItems("IOStartTime").Text & "' " End If ' 如果结束时间不为空,继续附加条件 If Len(ScreenItems("IOEndTime").Text) > 0 Then If Len(sWhere) > 0 Then sWhere = sWhere & " AND " sWhere = sWhere & "RecordTime < '" & ScreenItems("IOEndTime").Text & "' " End If ' 调用填充函数 FillReportGrid sWhere看起来清爽很多。但注意上面拼接 WHERE 的代码仍然没有处理 SQL 注入和格式问题。如果现场操作员水平参差不齐,输入的日期格式五花八门,查询就会经常翻车。下一章专门说这些坑。
4.3 下拉框选项从数据库动态加载
前面提到,下拉框不要写死。用组合框控件,在画面打开事件里查一次设备表,把设备名逐条 AddItem 进去。这样的好处是设备增减后报表界面不用改。
' 画面打开事件:加载设备下拉框 Dim oCmb, oCn, oRs, sSQL Set oCmb = ScreenItems("CmbDevice") Set oCn = CreateObject("ADODB.Connection") Set oRs = CreateObject("ADODB.Recordset") oCn.Open GetSQLConnStr() sSQL = "SELECT DISTINCT DeviceName FROM Report_Test ORDER BY DeviceName" oRs.Open sSQL, oCn, 3, 1 oCmb.Clear Do While Not oRs.EOF oCmb.AddItem oRs.Fields(0).Value oRs.MoveNext Loop oRs.Close oCn.Close Set oRs = Nothing Set oCn = Nothing值得注意的是,组合框的 AddItem 方法在 WINCC 的控件里,第一个选项默认索引是 0,如果你之后要用 ListIndex 属性作为条件值,注意别读错。
5. WINCC 做 SQL 报表查询的避坑指南:五个我翻过车的细节
5.1 连接字符串的 Provider 版本不对,导致查不到归档表
现象: 脚本在开发环境跑得好好的,部署到现场工控机上,查询 HISTORIAN 数据时返回空记录集,但直接连业务表又没问题。 原因: 现场机器的 SQL Server 2008 或 2012 环境不支持你开发机上用的 SQLOLEDB 老驱动,或者 ODBC 数据源指向了错误的实例名。我遇到过一次,开发机用的 Provider=SQLOLEDB,而现场是 SQL Server 2014 的默认实例改成命名实例,Data Source 里只写了机器名,导致找不到实例。 解决: 在 GetSQLConnStr 里显式写清实例名,比如 Data Source=192.168.1.10\SQLEXPRESS,或者干脆用逗号写端口,例如 Data Source=192.168.1.10,1433。另外,如果现场没有 OLEDB 驱动,改用 Provider=SQLNCLI11 或 MSOLEDBSQL,但在老 Windows 上注意驱动是否真的注册了。
5.2 中文乱码:SQL 语句里带中文条件查不到数据
现象: 按下查询按钮,表格是空的,但把所有条件去掉却能看到中文数据。把条件里的设备名在 SQL Server Management Studio 里手动执行没问题,在 WINCC 里查就空。 原因: 这是最典型的字符集冲突。VBScript 字符串是 Unicode,拼接成 SQL 后传给 OLEDB 驱动时,默认代码页可能不是中文代码页,导致 SQL 里的中文字面量在 SQL Server 端和表里的 NVARCHAR 不匹配。 解决: 两个办法一起上。第一,把字段类型和 SQL 里的字符串常量都加上 N 前缀,比如 sSQL = sSQL & "WHERE DeviceName = N'" & strDevice & "'"。第二,在 ADODB.Connection 对象的连接字符串里加 Character Set 参数,或者最简单的方式是用参数化查询。我用参数化解决了这个坑,代码会稍微长一点,但再没出现过乱码。
5.3 表格行数不刷新:上一次查询数据多,下一次查询数据少
现象: 第一次查询返回 100 行,表格显示 100 行;第二次查询条件更严格只返回 5 行,但表格底部还留着原来 95 行的旧数据,看起来像没查对。 原因: MSFlexGrid 的 Rows 属性一旦被设置成 100,如果 RecordCount + 2 小于 100,控件不会自动删掉多余行。我只在第一次填表时设置了 Rows,后面没有在每次填充前重新设置。 解决: 在 FillReportGrid 函数开头就要无条件设置 Rows 和 Cols,并且先全部清空。最彻底的方法是 oFrx.Clear。然后重置行列数,这样能保证控件完全重画。
5.4 运行时提示 ActiveX 控件未注册
现象: WINCC 开发环境里按住 Ctrl 键运行时画面正常,一旦发布到另一台操作员站上,画面里的表格控件显示成一个红叉,脚本按钮点了没反应。 原因: 这台操作员站上没注册 MSFlexGrid 这个 ActiveX 控件,或者 WINCC 安装时没有勾选“ActiveX 控件”组件。注意,WinCC 运行版装完后,MSFlexGrid 默认不一定被注册。 解决: 在操作员站上以管理员身份命令行运行 regsvr32 MSFLXGRD.OCX,或者更省事的方式是重新安装 WINCC/补充安装选件时把 ActiveX 支持勾上。还有,开发机和运行机的 WINCC 版本必须一致,否则即使注册了,版本不同也可能出现兼容性错误。
5.5 WINCC 脚本里执行耗时 SQL 卡死画面
现象: 数据量一大,按下查询按钮整个画面卡住十几秒,鼠标转圈,操作工以为死机了,反复点按钮,最后结果出来前界面早已失去响应。 原因: VBScript 是单线程的,脚本执行期间 WINCC 的画面刷新被阻塞。查询一个几十万行的归档表,尤其还带排序和聚合,耗时自然很长。 解决: 给 SQL 加上限制返回行数和索引优化。常用手段是 SELECT TOP 1000,或者在业务表上按 RecordTime 建索引。如果产品要求大量数据,就把查询拆成“先查汇总”和“点明细再查”两步。实在要在运行时优化体验,可以在脚本开头设置 ScreenItems("FrxReportGrid").Redraw = False,填充完再设 True,同时用 Screen 的修改消息提升响应。但说实话,报表查询十几秒的大查询,最好的办法就是限制范围。
这五个坑解决了,报表功能基本就能稳定运行了。但如果你想让它更贴近日常使用,还有两个小进阶可以做——导出 Excel 和加一个查询进度提示。
6. 把报表做得更好用:导出 Excel 和查询性能的实战技巧
画面里的表格只能在屏幕上看着,领导往往要 Excel 文件。在 VBScript 里导出 Excel 有几种办法,其中一种是用 Excel.Application 对象,那需要现场装了 Office;另一种是把表格内容直接写成 CSV 文件,然后用户用 Excel 打开。工业现场很多时候操作员站上压根没装 Office,所以 CSV 方式反而是最通用的。
下面是一个把表格内容存成 CSV 文件的子过程。这样导出的文件既可以用记事本打开,也可以用 Excel 双击打开,不会因为 Office 版本问题报错。
' 导出表格到 CSV 文件 Sub ExportGridToCSV(sFilePath) Dim oFrx Dim i, r Dim sLine Set oFrx = ScreenItems("FrxReportGrid") ' 用 FileSystemObject 创建文件 Dim fso, f Set fso = CreateObject("Scripting.FileSystemObject") Set f = fso.CreateTextFile(sFilePath, True) ' 遍历表格所有行,每行拼接成 CSV 格式 For r = 0 To oFrx.Rows - 1 sLine = "" For i = 0 To oFrx.Cols - 1 ' 转义:如果内容里有逗号,需要用引号包起来 Dim sCell sCell = oFrx.TextMatrix(r, i) If InStr(sCell, ",") > 0 Or InStr(sCell, """") > 0 Then sCell = """" & Replace(sCell, """", """""") & """" End If sLine = sLine & sCell If i < oFrx.Cols - 1 Then sLine = sLine & "," End If Next f.WriteLine sLine Next f.Close Set f = Nothing Set fso = Nothing End Sub这段代码有三个值得注意的点:第一,CSV 的换行符默认是 CRLF;第二,如果单元格内容里有英文逗号,必须用双引号包裹,否则 Excel 打开会错列;第三,WinCC 运行时脚本的文件操作权限要够,建议导出到 C 盘的某个固定目录,避免写到受保护的文件夹时失败。
调用方式如下:
' 导出按钮事件:生成带时间戳的文件名 Dim sFile sFile = "C:\ReportExport\Output_" & FormatDateTime(Now, 0) & ".csv" ' 注意文件名里不能有冒号,需要替换 sFile = Replace(sFile, ":", "") sFile = Replace(sFile, "/", "") ExportGridToCSV sFile MsgBox "导出完成:" & sFile这里有一个细节:FormatDateTime(Now, 0) 返回的日期时间字符串里带斜杠和冒号,这些字符在 Windows 文件名里是非法的,所以必须先 Replace 掉。这也是一个常见的导出坑,我最初写的时候直接生成文件名,结果 fso.CreateTextFile 报“路径中文件名非法”。
另一个实用技巧是给耗时查询加一个简单的进度提示,不过 VBScript 里没有多线程能力,所以只能退而求其次,在查询开始前用静态文本显示“正在查询,请稍候……”,查询结束后改成“共返回 N 条记录”。虽然不能实时刷新,但至少能拦住 90% 的重复点击。
' 查询按钮事件改造 ScreenItems("TxtStatus").Text = "正在查询,请稍候..." FillReportGrid sWhere Dim nRowCount nRowCount = ScreenItems("FrxReportGrid").Rows - ScreenItems("FrxReportGrid").FixedRows - 1 ScreenItems("TxtStatus").Text = "查询完成,共 " & CStr(nRowCount) & " 条记录"关于性能优化,我最后补充一点:如果数据表已经很大,比如几十万行,无论怎么优化脚本,查询都会慢。更合理的做法是在 SQL 层做聚合,例如把产量按小时汇总后在表格里只显示汇总结果。明细数据留给单独的“查明细”按钮。这样报表界面操作流畅,领导要的数字也能快速看到。
这套方案我在多个项目里都落地过,从最初踩中文乱码的坑,到后来把 CSV 导出和参数化查询收拾利索,大概花了两天时间打磨。现在每当现场要“再加一个报表”,我只需要新建一个画面、复制脚本、改一下 SQL 和字段映射就行,半小时交活。有些细节如果你照着本文一次跑通,自然明白哪些步骤可以省;如果中途报错,回看避坑那一章,基本都能找到答案。希望帮到你。
本文还有配套的精品资源,点击获取