简介:面向WinCC组态与工业自动化数据报表开发场景,这份技术文档讲解如何借助VBScript、SQL数据库及ActiveX控件实现生产数据查询与日报表输出,适合已具备WinCC基础、需要补足SQL交互与报表组态能力的工程师参考。压缩包内为1个doc文件,大小约1.11MB,内容涵盖SQL Server 2005建库建表、WINCC全局脚本写入变量、日报表画面组态、MSFlexGrid与MS DTPicker控件配置,以及“查询”“打印”按钮的脚本编写。文档还说明全局脚本触发条件、ActiveX控件添加与名称修改、按起止日期拼接SQL查询条件,以及将表格数据转存Excel后打印的实现方式;附录给出写入与查询数据库的VBS代码示例,并提示控件注册、日期范围校验等排错点,便于读者搭建从数据采集、存储到表格展示和打印输出的完整链路。已有1682人学习,可作为工控报表项目的实操参考。
1. 交接班报表还在手抄:这套 VBS + 控件的 WINCC SQL 报表能省掉什么
凌晨两点设备报警,第二天交班要的是前一天 0 点到 24 点的 CPU、内存、硬盘占用表——WinCC 自带的趋势控件能画曲线,但领导要的是一张能按日期筛选、能打印、能签字的表格。这套基于 VBScript 的 WINCC 报表方案,核心就三件事:全局脚本把内部变量周期写进 SQL Server 的报表表,MSFlexGrid 控件把查询结果铺成网格,MS DTPicker 控件选起止日期。它不引入任何报表服务器,也不装第三方插件,WinCC 装完、SQL Server 装完,剩下的就是写脚本和摆控件。适合手上跑着 WinCC 项目、需要日报/班报、又不想为几张表单独上一套报表系统的人;同样适合接手老项目、发现前任只留了一堆 .doc 说明的维护者。
2. 建库与写库:从 WinCC 变量到 SQL 表的四步链路
2.1 先把表结构定死:字段、类型与时间列
资源里的建库步骤是纯图形操作:打开 SQL Server Management Studio,连上实例,在 Databases 上右键 New Database,输入库名,再到 Tables 上右键 New Table,逐列填 Column Name 和 Data Type。这套点法在 2005 那个年代没问题,但换台机器复现时容易漏字段、漏索引,我更习惯先用 T-SQL 把结构固化成脚本,再在图形界面里核对。
-- 建库:库名与后续脚本里的 Initial Catalog 必须一字不差 CREATE DATABASE [SQL-Report]; GO USE [SQL-Report]; GO -- 报表主表:一行代表一个采集时刻的三个指标 CREATE TABLE dbo.VBSReport ( [time] DATETIME NOT NULL, -- 采集时刻,查询和排序都靠它 cpu FLOAT NULL, -- CPU 占用百分比 ram FLOAT NULL, -- 内存占用百分比 hd FLOAT NULL -- 硬盘占用百分比 ); GO -- 按时间查是唯一的高频访问路径,索引直接建在这一列 CREATE CLUSTERED INDEX IX_VBSReport_time ON dbo.VBSReport([time]); GO字段类型选 FLOAT 而不是 INT,因为HMIRuntime.Tags("cpu").Read读回来的值本身可能是带小数的浮点,落库时被截成整型,后面算平均值就会跟画面显示对不上。时间列用 DATETIME 而不是字符串,是为了让BETWEEN能走索引;如果存成varchar,'2024-1-5'和'2024-01-05'会被当成两个不同的值排序,月底对账时能让你怀疑人生。
提示:库名、表名、列名这三个东西,后面每一段脚本里都会出现。资源附带的写入脚本用的是
SQL-Report库、VBSReport表,而查询脚本里写的是Example-report库、report表,这两组名字对不上——直接抄下来跑,结果就是「写进去了但查不出来」。名字统一这件事,建表时就要定,别等到联调。
2.2 全局脚本里的 action:把三个变量写进一行记录
写库动作放在 WinCC 的全局脚本编辑器里,不是画面脚本。这一点新手经常混:画面里按钮的OnClick能用ScreenItems("控件名")拿到控件对象,全局脚本里没有画面上下文,只能通过HMIRuntime.Tags("变量名").Read读变量。全局脚本的返回值、报错都进不了画面,所以错误处理必须自己写。
' WinCC 全局脚本 - 动作,触发器设为 1 分钟定时 Option Explicit Function action Dim cn, cmd, strcn Dim a, b, c, d On Error Resume Next ' 脚本宿主不弹窗,先兜住错误 a = Now ' 采集时刻 b = HMIRuntime.Tags("cpu").Read c = HMIRuntime.Tags("ram").Read d = HMIRuntime.Tags("hd").Read ' Data Source=.\wincc 指本机的 wincc 命名实例 ' Initial Catalog 就是库名,SSPI 表示用当前 Windows 账户身份登录 strcn = "Provider=SQLOLEDB.1;Integrated Security=SSPI;" & _ "Persist Security Info=False;Initial Catalog=SQL-Report;" & _ "Data Source=.\wincc" Set cn = CreateObject("ADODB.Connection") cn.ConnectionString = strcn cn.Open If Err.Number <> 0 Then HMIRuntime.Trace "写库连接失败: " & Err.Description & vbCrLf Exit Function End If ' 用 Command + 参数,避免时间字符串里的单引号把语句拼断 Set cmd = CreateObject("ADODB.Command") Set cmd.ActiveConnection = cn cmd.CommandType = 1 ' adCmdText cmd.CommandText = "INSERT INTO VBSReport([time],cpu,ram,hd) VALUES(?,?,?,?)" cmd.Parameters.Append cmd.CreateParameter("t", 135, 1, , a) ' 135 = adDBTimeStamp cmd.Parameters.Append cmd.CreateParameter("c", 5, 1, , b) ' 5 = adDouble cmd.Parameters.Append cmd.CreateParameter("r", 5, 1, , c) cmd.Parameters.Append cmd.CreateParameter("h", 5, 1, , d) cmd.Execute If Err.Number <> 0 Then HMIRuntime.Trace "写库失败: " & Err.Description & vbCrLf End If cn.Close Set cmd = Nothing Set cn = Nothing End Function连接串里四个部分各有分工。Provider=SQLOLEDB.1是老的 OLE DB 提供程序,WinCC 这种 32 位宿主环境用它最稳;Integrated Security=SSPI表示走 Windows 集成认证,意味着 WinCC 运行时那个账户必须在 SQL Server 上有写权限,域环境换账户后脚本突然写不进去,八成就是这个原因;Data Source=.\wincc里的点代表本机,wincc是安装时创建的命名实例,不是库名,别把两者搞混。
资源原稿用的是字符串拼接:"insert into ... Values('"&a&"','"&b&"'...)"。内部变量是数值,拼接一般不出事,但只要有一个变量是字符串、里面带了单引号,整条语句立刻语法错误。参数化写法多几行,但省掉的是半夜查日志的时间。CreateParameter的第三个参数 1 表示adParamInput,第四个参数留空让 ADO 自己推长度,数值类型不填也能跑。
2.3 触发条件:周期触发和变量触发选哪个
原稿在数据库那一步特意标了一句「注意:全局脚本触发条件」,这句话是整个方案里最容易被忽略的部分。WinCC 全局脚本的触发器分两类:定时器和变量。定时器按固定周期执行,比如 60 秒一次;变量触发在指定变量值变化时执行。
选定时器的好处是数据密度均匀,做日报表时每个小时的样本数一致,画出来的平均值有可比性;坏处是不管变量变没变都插一行,一天 1440 条,一个月四万多条,两年后这张表能拖慢整个查询。选变量触发的好处是没有冗余记录,坏处是变量稳定时完全没有数据,某台设备一下午没波动,报表上就是一片空白,看起来像采集中断。
我一般用定时器加条件写入的组合:定时器 60 秒跑一次,脚本里先跟上一条记录比较,三个值都没变化就跳过。
' 在 action 开头加一段变化检测 Dim lastCpu lastCpu = HMIRuntime.Tags("lastCpu").Read ' 内部变量,记录上次写入值 If Abs(b - lastCpu) < 0.1 Then Exit Function ' 变化小于 0.1 视为没变 End If HMIRuntime.Tags("lastCpu").Write b这个 0.1 的阈值不是拍脑袋:模拟量采集本身带抖动,阈值太小等于没过滤,阈值太大又会把真实的缓慢爬升吃掉。用之前先看两周的原始数据,估算一下稳态时的波动幅度,取它的两到三倍比较靠谱。另外lastCpu这类辅助变量要设成掉电保持,否则 WinCC 一重启就丢,重启后第一条记录必然重复写入。
写入链路跑通之后,别急着做画面。先打开 Management Studio,对着表执行一次SELECT TOP 50 * FROM VBSReport ORDER BY [time] DESC,确认时间在走、三个值不是全 0、记录间隔符合预期。这三条任何一条不满足,后面画面做得再漂亮也是白搭。
3. 控件注册与画面组态:MSFlexGrid 和 DTPicker 怎么摆才不翻车
3.1 OCX 注册:为什么必须单独做这一步
MSFlexGrid 和 MS DTPicker 都是 ActiveX 控件,不是 WinCC 自带的对象。资源里的做法是在画面的 ActiveX Controls 上右键 Add/Remove,从列表里勾选Microsoft FlexGrid Control, Version 6.0和Microsoft Date and Time Picker Control 6.0 (SP4)。能在这两个名字前面打勾,前提是系统里已经注册过对应的 OCX 文件;没注册过,列表里根本看不到。
注册命令本身不复杂,麻烦的是路径。很多机器装过 VB6,安装程序顺手把MSFLXGRD.OCX注册好了,所以「直接就能选」;换一台干净的工控机,就得手动拷文件、手动注册。
# 64 位系统上,32 位 OCX 应该放在 SysWOW64 而不是 System32 copy MSFLXGRD.OCX C:\Windows\SysWOW64\ # 用 SysWOW64 目录下的 regsvr32 注册,否则会提示"模块加载失败" C:\Windows\SysWOW64\regsvr32.exe C:\Windows\SysWOW64\MSFLXGRD.OCX # 卸载时加 /u C:\Windows\SysWOW64\regsvr32.exe /u C:\Windows\SysWOW64\MSFLXGRD.OCX这里有个流传很广的错误做法:把 OCX 拷进C:\WINDOWS\system32再注册。在 32 位系统上没问题,在 64 位系统上就翻车——system32目录放的是 64 位组件,而 WinCC 是 32 位进程,它去加载 64 位的 OCX 会直接报「类未注册」。判断方法很简单:任务管理器里看 WinCC 运行进程有没有*32后缀,有就是 32 位,控件一律往SysWOW64放,注册也用SysWOW64下的 regsvr32。
注意:注册需要管理员权限。普通账户执行 regsvr32 会返回「DllRegisterServer 调用失败」,看错误码容易误判成文件损坏。用管理员身份开命令行再执行一遍。
3.2 DTPicker 的取值:日期控件返回的到底是什么
DTPicker 在画面上表现为一个下拉日历,属性里能设格式。脚本中取值的写法是ScreenItems("Date1").Value,返回的是一个日期型值,直接喂给Year()、Month()、Day()就能拆出年月日。
' 画面脚本里读两个日期控件 Dim Date1, Date2, By, Bm, Bd, Ny, Nm, Nd 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) ' 拼成 SQL 认识的字符串,月日补零,避免区域设置差异 Dim BeginDate, EndDate BeginDate = By & "-" & Right("0" & Bm, 2) & "-" & Right("0" & Bd, 2) & " 00:00:00" EndDate = Ny & "-" & Right("0" & Nm, 2) & "-" & Right("0" & Nd, 2) & " 23:59:59"补零这一步不能省。原稿是By & "-" & Bm & "-" & Bd,月份是 1 位时得到2024-1-5 00:00:00。这个字符串在部分语言环境下 SQL Server 能认,在另一些环境下会被当成yyyy-dd-MM解析,1 月 5 日直接变成 5 月 1 日,查出来的数据完全是另一个区间,而且不报错。这种错误最恶心的地方在于它是静默的,报表能出、数字有,就是不对。
DTPicker 还有一个属性值得看一眼:Format。设成dtpCustom之后配合CustomFormat可以控制显示,但脚本取值不受显示格式影响,取到的始终是完整日期。有人为了「只显示年月」把CustomFormat设成yyyy-MM,结果以为取到的值也只剩年月,这是误会。
3.3 MSFlexGrid 的属性:行列、合并与对齐
MSFlexGrid 本质是一张能设属性的二维表,脚本里通过TextMatrix(行, 列)读写单元格,行列都从 0 开始。资源里用到的关键属性如下,这张表建议直接抄进笔记:
| 属性 | 典型取值 | 作用 |
|---|---|---|
| Rows | 记录数 + 6 | 总行数,多出来的 6 行留给表头、MAX/MIN/AVG |
| Cols | 5 | 列数:序号、时间、CPU、内存、硬盘 |
| FixedRows | 2 | 前两行固定不随滚动条移动 |
| ColWidth(n) | 900 / 1900 / 1000 … | 单列宽度,单位是 twip,时间列要宽一些 |
| MergeCells | 4 | 按行列双向限制合并,配合 MergeRow 用 |
| MergeRow(0) | True | 第 0 行整行合并成一个大标题 |
| ColAlignment(n) | 4 | 垂直水平都居中,数值列看起来才整齐 |
Rows必须在填数据之前设好。先填后设会把已填内容清空,这个顺序错了,表现是「查询有结果但表格空白」。MergeCells = 4对应的是「行列都受限的合并」,只有相邻单元格内容完全一致时才会合并,所以第 0 行要先给五列都赋同一个字符串,合并才会生效。
控件拖到画面上之后,建议在属性面板里把Name改成有意义的名称,比如gridReport、dtpBegin,别用默认的MSFlexGrid1。脚本里靠名字找对象,名字一改,脚本里所有ScreenItems("...")都要同步改;反过来,先定名字再写脚本,返工量小得多。
4. 查询脚本拆解:一条 SQL 怎么变成明细表加 MAX/MIN/AVG 统计行
4.1 时间区间:为什么用 23:59:59 而不是「小于次日零点」
查询条件写成BETWEEN '起始 00:00:00' AND '结束 23:59:59',这是最常见的写法,也是最容易漏掉最后几百毫秒的写法。DATETIME 的精度是 3.33 毫秒,23:59:59.997这样的值不在区间内,如果采集周期正好落在这一带,当天的最后一条就丢了。
严格的写法是左闭右开:
-- 推荐:左闭右开,不依赖秒级精度 WHERE [time] >= '2024-01-05 00:00:00' AND [time] < '2024-01-06 00:00:00'但在 WinCC 画面脚本里,用户选的是两个日期,右边界要现算「次日」。VBScript 里DateAdd("d", 1, Date2.Value)就能得到,拼字符串时再接00:00:00。资源原稿用的是23:59:59,在采集周期为分钟级的场景下够用,因为分钟级数据不会落在 59.997 秒这种位置;但如果哪天把采集周期改成秒级或者事件触发,这个边界就必须换掉。
还有一层校验:起始日期不能晚于结束日期。原稿用了一长串If By>Ny Or By=Ny And Bm>Nm Or ...来判断,逻辑是对的,但只弹了个提示框,没有Exit Sub,用户点完确定脚本继续往下跑,照样去查一个空区间。加上退出才是完整逻辑:
If By > Ny Or (By = Ny And Bm > Nm) Or (By = Ny And Bm = Nm And Bd > Nd) Then MsgBox "起始时间不能晚于结束时间", vbOKOnly, "时间范围错误" Exit Sub End If4.2 连接与游标:RecordCount 为什么总是 -1
ADO 的 Recordset 默认用服务端游标,这种游标是「只进」的,RecordCount返回 -1,因为服务端根本没打算知道总行数。要拿到准确条数,必须把连接对象的CursorLocation设成 3(adUseClient),让结果集整个拉到客户端再统计。
Dim conn, oRs, oCom, Sql Set conn = CreateObject("ADODB.Connection") conn.ConnectionString = "Provider=SQLOLEDB.1;Integrated Security=SSPI;" & _ "Persist Security Info=False;Initial Catalog=SQL-Report;" & _ "Data Source=.\wincc" conn.CursorLocation = 3 ' adUseClient,关键的一行 conn.Open Set oCom = CreateObject("ADODB.Command") oCom.CommandType = 1 Set oCom.ActiveConnection = conn oCom.CommandText = Sql Set oRs = oCom.Execute If oRs.RecordCount = -1 Then MsgBox "结果集不支持计数,请检查 CursorLocation", vbOKOnly, "游标错误" Exit Sub End If客户端游标有代价:十万行以上会全量拉进内存,画面脚本卡住。日报表一天最多几千行,无所谓;如果哪天要做月度报表,就该改成用SELECT COUNT(*)单独查一次总数,明细照样走服务端游标分页取。
明细 SQL 和统计 SQL 是两条独立的语句,原稿用两个 Command 对象分别执行,这个思路是对的——一条查明细用于铺表格,一条用AVG/MIN/MAX聚合出统计行,不要试图在一条语句里同时拿到明细和聚合,那要靠窗口函数,老版本 SQL Server 还不支持。
-- 明细:把时间格式化成 19 位字符串,避免脚本里处理日期 SELECT CONVERT(char(19), [time], 20) AS shijian, cpu, ram, hd FROM VBSReport WHERE [time] BETWEEN '2024-01-05 00:00:00' AND '2024-01-05 23:59:59' ORDER BY [time]; -- 统计:一次算出九个值,供表格底部三行使用 SELECT AVG(cpu) AS ylp, AVG(ram) AS wdp, AVG(hd) AS llp, MIN(cpu) AS ylx, MIN(ram) AS wdx, MIN(hd) AS llx, MAX(cpu) AS yld, MAX(ram) AS wdd, MAX(hd) AS lld FROM VBSReport WHERE [time] BETWEEN '2024-01-05 00:00:00' AND '2024-01-05 23:59:59';CONVERT(char(19), [time], 20)里的 20 是 ODBC 标准格式yyyy-mm-dd hh:mi:ss,格式化在数据库侧做完,脚本里就不用管区域设置。这个转换会让时间列上的索引失效在 WHERE 里,但这里只用在 SELECT 列表,不影响查询计划。
4.3 填表:TextMatrix 的行列偏移与三个统计行
填表逻辑的核心是行列对应关系。第 0 行是合并标题,第 1 行是列名,第 2 行开始才是数据,所以第 i 条记录落在第 i+1 行。
Dim i, n, grid Set grid = ScreenItems("MSFlexGrid1") n = oRs.RecordCount grid.Clear grid.Rows = n + 6 ' 2 行表头 + n 行数据 + 3 行统计 + 1 行余量 grid.Cols = 5 ' 第 0 行整行合并做大标题 Dim z grid.Row = 0 For z = 0 To 4 grid.Col = z grid.Text = "Computer Utilization Message (per Day)" Next grid.MergeCells = 4 grid.MergeRow(0) = True ' 第 1 行列名 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%" ' 第 2 行起填明细 If n > 0 Then oRs.MoveFirst i = 0 Do While Not oRs.EOF i = i + 1 grid.TextMatrix(i + 1, 0) = CStr(i) grid.TextMatrix(i + 1, 1) = CStr(oRs.Fields(0).Value) grid.TextMatrix(i + 1, 2) = CStr(oRs.Fields(1).Value) grid.TextMatrix(i + 1, 3) = CStr(oRs.Fields(2).Value) grid.TextMatrix(i + 1, 4) = CStr(oRs.Fields(3).Value) oRs.MoveNext Loop End If ' 底部三行统计,位置跟着记录数走 grid.TextMatrix(n + 3, 0) = "MAX" grid.TextMatrix(n + 4, 0) = "MIN" grid.TextMatrix(n + 5, 0) = "AVG"资源原稿里在循环内写了n = n + 1,而n同时在循环前被赋成RecordCount、在循环后又被写成记录数用于判断If n = 0。一行变量担了三个角色,只要记录数不为零,循环跑完n就翻倍,后面TextMatrix(n+3, ...)直接越界,轻则统计行跑到表格外面,重则脚本抛错整块不刷新。这类问题在画面里没有堆栈,只能靠看现象猜——表格有数据但底部三行不见了,八成就是行号算错。
统计值的取法也有讲究。原稿直接把AVG的结果塞进单元格,显示出来是37.333333333333这种一长串。用Int(x * 1000 + 0.5) / 1000保留三位小数,视觉上干净得多,也不会因为四舍五入规则不同导致同一份数据两次查询显示不一致。
5. 排查清单:五类报错的现象、成因与处置
5.1 数据链路:写进去了,但查不出来
现象:Management Studio 里能看到表在增长,画面点查询却提示「没有找到符合条件的数据」,换几个日期都一样。
原因:最常见的是库名或表名不一致。写入脚本连的是SQL-Report库、VBSReport表,查询脚本连的是Example-report库、report表,两段代码分别从不同来源抄来,谁也没改。两个库都存在时不会报错,只是查询永远命中空表。
解决:把连接串和 SQL 语句里的库名、表名全部搜一遍,统一成实际建的那一套。更稳的做法是把库名、表名提成常量放在脚本开头,改的时候只改一处。改完先跑一次SELECT COUNT(*),确认条数不为零,再去看画面。
现象:查询范围放宽到一整年,仍然只出几行。
原因:全局脚本的触发器没配,或者配了但 WinCC 没进运行态。全局脚本只在项目激活后执行,编辑器里点「编译」不会产生数据。
解决:看脚本编辑器里触发器那一栏是不是空的;进运行态后等两个采集周期,再查一次表。如果还是没有,在action里加一句HMIRuntime.Trace,把执行时间和错误码写进诊断窗口,这是全局脚本唯一能用的排查手段。
5.2 控件链路:画面一打开就报错
现象:项目激活后画面打不开,弹出「未注册的类」或者控件位置显示成一个白框。
原因:OCX 没注册,或者注册到了错误的位置。64 位系统上把 32 位控件注册进system32,WinCC 加载时会失败;另一种情况是换了台机器,只拷了项目文件没拷控件。
解决:确认进程位数,32 位宿主就用SysWOW64目录加SysWOW64\regsvr32.exe注册。如果源文件都找不到,从装过 VB6 的机器上复制MSFLXGRD.OCX和MSCOMCT2.OCX(DTPicker 在这个文件里)。
现象:控件能显示,但点查询时脚本报「对象不支持该属性或方法」。
原因:画面里控件的Name被改过,脚本里还在用默认名MSFlexGrid1;或者控件被拖成了另一种类型,属性集不一样。
解决:在画面编辑器里选中控件,看属性面板顶部的名称,跟脚本里的ScreenItems("...")逐字比对。大小写不敏感,但拼写必须一致。
5.3 显示链路:数字对不上、表格错位
现象:明细行数和统计行的位置错开,底部三行跑到表格中间去了。
原因:Rows设小了,或者记录数和行号的换算关系写错。Rows一旦小于实际需要的行数,TextMatrix写入越界会静默失败或抛错。
解决:先用固定行数Rows = 100跑通填充逻辑,确认数据落位正确,再把行数改成动态计算。动态计算时记住公式是记录数 + 6:标题 1 行、列名 1 行、数据 n 行、统计 3 行、余量 1 行。
现象:同一天的数据,查询结果的时间列有的显示完整日期,有的只显示时分。
原因:脚本里对「起止日期相同」做了特殊处理,只截取时分显示。这个判断依赖两个日期控件的值完全相等,如果其中一个带了时间部分,判断就失效,于是同一次查询里两种格式混着出现。
解决:要么统一显示完整时间,要么在比较前把两个值都格式化成yyyy-mm-dd再比。不要直接比较日期型值,那比的是毫秒级时间戳。
5.4 边界条件:那条最容易漏的记录
现象:每天的最后一条采集记录,在日报表里时有时无。
原因:查询上界写成了23:59:59,而 DATETIME 的精度是 3.33 毫秒,落在23:59:59.5的记录被排除在外。采集周期是分钟级时这个问题不明显,改成秒级或事件触发后立刻暴露。
解决:改用左闭右开区间,上界取次日零点。日期控件只给到日,次日零点用DateAdd("d", 1, ...)算出来拼接,别手写。
提示:这套链路上任何一处「时有时无」的问题,优先怀疑时间边界和区域设置,这两个原因占了排查工作量的一大半,而且都不报错。
6. 导出 Excel 与打印:把网格变成能签字的报表纸
打印按钮的脚本逻辑是:把 MSFlexGrid 里的内容逐格搬到 Excel 工作表,加边框、调行高、设字体,然后调PrintPreview或PrintOut。搬运本身没有技巧,逐行逐列赋值即可,真正决定成败的是后面几步格式设置。
' 画面脚本 - 打印按钮 OnClick Dim grid, xl, book, sheet, i, j, rows Set grid = ScreenItems("MSFlexGrid1") Set xl = CreateObject("Excel.Application") Set book = xl.Workbooks.Add Set sheet = book.Worksheets(1) xl.Visible = True ' 调试期打开,发布时改成 False rows = grid.Rows For i = 0 To rows - 1 For j = 0 To grid.Cols - 1 ' Trim 去掉 MSFlexGrid 单元格尾部补的空格 sheet.Cells(i + 1, j + 1) = Trim(grid.TextMatrix(i, j)) Next Next ' 四条边框,1 到 4 分别是左、右、上、下 Dim k For k = 1 To 4 sheet.Range("A1:E" & rows).Borders(k).Weight = 2 Next sheet.Rows(1).RowHeight = 0.75 / 0.035 ' 行高单位是磅,原稿这个换算是把厘米转磅 sheet.Rows(1).Font.Bold = True sheet.Cells.EntireColumn.AutoFit ' 列宽自适应,时间列不会被截断 sheet.PageSetup.CenterHorizontally = True ' 水平居中,布尔值比数值稳 sheet.PrintPreview ' 先预览,确认后再换 PrintOut有三处值得说。Borders(k).Weight的 k 是固定枚举,1 左、2 右、3 上、4 下,漏掉哪个方向那一侧就没线,打印出来像是表格被裁了一刀。PageSetup.CenterHorizontally原稿写的是2/0.035,本质是拿一个换算后的数值当布尔用,改成True更直白,也不会因为 Office 版本差异被解释成别的含义。PrintPreview和PrintOut建议做成两个按钮或者加一个确认框——预览能让人发现列宽被截断、字体缺失这类问题,直接送打印机的话,纸就废了。
还有一个隐性成本:每次点打印都新开一个 Excel 进程,连点五次就是五个 EXE 在后台挂着。发布前把xl.Quit和对象释放补上,顺序是book.Close、xl.Quit、Set xl = Nothing,中间任何一步抛错,进程都会残留。
验证这套报表是否真的可用,我一般跑三个用例:选同一天,看明细行数和统计行的位置对不对;选跨月区间,看月初月末那两条边界记录在不在;选一个没有任何数据的日期,看提示框是否正常弹出、表格是否被清空而不是留着上一次的结果。第三个用例最容易被跳过,但它恰恰是现场操作员最容易触发的场景——选错日期、点了查询、画面还挂着昨天的数,拿去交接就是事故。从那以后我每次接这类老项目,都先把这三个用例跑一遍再谈画面美观。希望帮到你。
本文还有配套的精品资源,点击获取