简介本资源是一份面向工业自动化领域初学者与组态王开发人员的实用技术文档聚焦于解决电机运行时间精准记录这一典型设备运维需求。通过组态王报表功能结合脚本二次开发完整实现了双电机启动时间、停止时间及运行时长自动换算为天/时/分/秒的结构化记录与报表填充适用于设备状态监控、预防性维护与运行数据分析等实际工程场景。资源为1个423KB的Word文档.docx内含详细实现思路、变量定义规范、日期格式化处理逻辑、启停事件触发脚本含HTConvertTime时间转换、字符串截取与拼接、报表行列定位等关键代码段以及完整的操作步骤说明。目前已有2950人学习下载读者可直接复用该方案中的内存变量设计、自定义函数框架与报表数据联动逻辑快速构建同类设备运行日志系统。1. 组态王电机运行时间记录不是“加个按钮就能出报表”而是用内存变量脚本报表三线协同的闭环逻辑你有没有遇到过这种现场客户指着组态王画面问“这台电机今天一共开了多久”——你点开历史曲线手动拖拽、估算、心算最后报个“大概7小时23分”对方眉头一皱“能导出精确到秒的明细吗”别急着翻帮助文档。组态王原生确实不带“设备运行时长自动归档”功能但它留了一条极其实用的缝报表控件可编程写入 内存变量持久暂存 系统时间变量实时可读。这篇笔记讲的就是怎么把这三条线拧成一股绳让两台电机的每一次启停都自动生成带序号、名称、启动时间、停止时间、精确到秒的运行时长格式为“X天Y时Z分W秒”的结构化记录。它不依赖OPC服务器、不调用外部数据库、不改组态王安装目录纯靠组态王6.55及以上版本自带的脚本引擎KingView Script和报表控件实现。适合做设备维保台账、能耗初筛、故障频次统计的中小型自动化项目。如果你正被“领导要日报”“审计要原始记录”“运维要追溯依据”三座大山压着又没预算上MES那这个方案就是你今晚该抄的第一份作业。2. 报表结构设计与内存变量规划为什么必须用3个整形变量少一个就断链要让“启动→填行→停止→回填”这个动作闭环光有报表是空架子。组态王报表本身不记状态它只负责显示而设备启停是瞬时事件脚本执行完就结束。中间缺的“记忆体”就是内存变量。这里不是随便建几个变量就行而是有明确分工的三人小组2.1 报表10五列固定结构拒绝自由发挥新建报表命名为“报表10”注意原文写“报表1”但后文调用函数时传参是报表 1存在笔误实际工程中建议统一为“报表10”避免空格引发的ReportSetCellString调用失败。其列定义必须严格如下列号列名数据类型说明1序号字符串自增编号从1开始2设备名称字符串如“冰水循环泵2”3启动时间字符串格式2024-06-15 14:23:084停止时间字符串同上格式5运行时长字符串格式0天2时15分33秒提示列宽需手动设为足够容纳最长字符串如“999天23时59分59秒”共15字符否则显示截断。右键报表→“列属性”→“宽度”设为120像素起。2.2 三个内存整形变量各司其职一个都不能少在组态王开发系统中进入“数据库”→“内存实型/整型变量”新建以下3个内存整形变量注意类型必须是“内存整形”非“IO整形”或“内存实型”变量名类型初始值作用说明Motor1Row内存整形0记录“设备1”当前运行周期在报表中的行号启动时写入停止时读取Motor2Row内存整形0记录“设备2”当前运行周期在报表中的行号同上LastRecordRow内存整形1记录报表中下一条新记录应写入的行号即当前最大行号1启动时赋值为什么LastRecordRow初始值设为1因为报表行号从1开始计数第一行就是序号“1”。当设备1首次启动脚本会将LastRecordRow的值1写入第1行然后LastRecordRow变为2下次启动自动写第2行。这个变量是保证记录严格顺序追加的核心漏建或类型设错所有后续逻辑都会错位。2.3 关键约束报表行号与变量值的同步铁律组态王报表的报表当前行是一个全局只读变量表示当前光标所在行。但我们的逻辑不需要光标需要的是逻辑行号。因此所有写入操作必须显式指定行号而非依赖报表当前行。原文脚本中出现报表当前行报表当前行1是危险操作——它会改变全局状态干扰其他报表操作。正确做法是用LastRecordRow作为写入目标行号写完立即自增。例如// 启动设备时向报表10第LastRecordRow行写入 ReportSetCellString(报表10, LastRecordRow, 1, StrFromInt(LastRecordRow, 10)); // 序号行号 ReportSetCellString(报表10, LastRecordRow, 2, name); // 设备名称 ReportSetCellString(报表10, LastRecordRow, 3, str); // 启动时间 // 保存此设备的行号到对应变量 Motor1Row LastRecordRow; // 设备1启动记下这一行 // 更新总行号为下一次记录准备 LastRecordRow LastRecordRow 1;这段代码里Motor1Row存的是“设备1本次启动所在的物理行号”LastRecordRow存的是“报表下一个空闲行号”。二者解耦互不干扰。这是整个方案能稳定运行的底层契约。3. 自定义函数SetReport把时间拼接、秒级计算、格式转换全封装进一个函数把原文中零散的脚本揉成一个可复用、易调试的自定义函数是降低维护成本的关键。我们命名为SetReport它接收3个参数motorState设备当前运行状态1启动0停止、motorName设备名称字符串、rowVarAddr指向内存变量的地址用于读写该设备的行号。注意组态王脚本不支持指针传参所以实际实现中我们用switch分支区分设备rowVarAddr改为设备ID如1代表Motor1Row2代表Motor2Row。3.1 函数完整代码含关键注释// 自定义函数SetReport // 参数说明 // state: 整型1设备启动0设备停止 // name: 字符串设备名称如冰水循环泵2 // motorID: 整型1设备12设备2 function SetReport(state, name, motorID) { // 步骤1构建标准时间字符串YYYY-MM-DD HH:MM:SS string strY StrFromInt(\\本站点\$年, 10); string strM StrFromInt(\\本站点\$月, 10); string strD StrFromInt(\\本站点\$日, 10); string strH StrFromInt(\\本站点\$时, 10); string strMi StrFromInt(\\本站点\$分, 10); string strS StrFromInt(\\本站点\$秒, 10); // 补零逻辑确保月份、日期、小时、分钟、秒均为2位 if (\\本站点\$月 10) strM 0 strM; if (\\本站点\$日 10) strD 0 strD; if (\\本站点\$时 10) strH 0 strH; if (\\本站点\$分 10) strMi 0 strMi; if (\\本站点\$秒 10) strS 0 strS; string timeStr strY - strM - strD strH : strMi : strS; // 步骤2根据motorID获取/设置对应设备的行号变量 long varRow; switch(motorID) { case 1: varRow \\本站点\Motor1Row; break; case 2: varRow \\本站点\Motor2Row; break; default: return; // 无效ID退出 } // 步骤3设备启动逻辑state 1 if (state 1) { // 写入序号等于当前LastRecordRow ReportSetCellString(报表10, \\本站点\LastRecordRow, 1, StrFromInt(\\本站点\LastRecordRow, 10)); // 写入设备名称 ReportSetCellString(报表10, \\本站点\LastRecordRow, 2, name); // 写入启动时间 ReportSetCellString(报表10, \\本站点\LastRecordRow, 3, timeStr); // 更新该设备的行号变量 if (motorID 1) \\本站点\Motor1Row \\本站点\LastRecordRow; else \\本站点\Motor2Row \\本站点\LastRecordRow; // 更新总行号为下一次启动准备 \\本站点\LastRecordRow \\本站点\LastRecordRow 1; } // 步骤4设备停止逻辑state 0 else if (state 0) { // 从报表中读取该设备启动时间第varRow行第3列 string startStr ReportGetCellString(报表10, varRow, 3); if (startStr ) return; // 防御性检查若为空说明未启动过不处理 // 步骤4.1解析启动时间字符串提取年月日时分秒 // 格式固定为2024-06-15 14:23:08位置索引从1开始组态王字符串索引 string midY StrMid(startStr, 1, 4); // 年位置1取4位 string midM StrMid(startStr, 6, 2); // 月位置6取2位跳过- string midD StrMid(startStr, 9, 2); // 日位置9取2位 string midH StrMid(startStr, 12, 2); // 时位置12取2位跳过 和: string midMi StrMid(startStr, 15, 2); // 分位置15取2位 string midS StrMid(startStr, 18, 2); // 秒位置18取2位 long lY StrToInt(midY); long lM StrToInt(midM); long lD StrToInt(midD); long lH StrToInt(midH); long lMi StrToInt(midMi); long lS StrToInt(midS); // 步骤4.2调用HTConvertTime将启动/停止时间转为秒级时间戳 long startTime HTConvertTime(lY, lM, lD, lH, lMi, lS); long endTime HTConvertTime(\\本站点\$年, \\本站点\$月, \\本站点\$日, \\本站点\$时, \\本站点\$分, \\本站点\$秒); long timeLength endTime - startTime; // 单位秒 // 步骤4.3将秒数转换为X天Y时Z分W秒格式 string durationStr; long days timeLength / 86400; long remainingAfterDays timeLength % 86400; long hours remainingAfterDays / 3600; long remainingAfterHours remainingAfterDays % 3600; long minutes remainingAfterHours / 60; long seconds remainingAfterHours % 60; // 拼接字符串避免出现0天0时0分5秒只显示有效单位 durationStr ; if (days 0) durationStr durationStr StrFromInt(days, 10) 天; if (hours 0 || days 0) durationStr durationStr StrFromInt(hours, 10) 时; if (minutes 0 || hours 0 || days 0) durationStr durationStr StrFromInt(minutes, 10) 分; durationStr durationStr StrFromInt(seconds, 10) 秒; // 步骤4.4将停止时间和运行时长写入报表对应行 ReportSetCellString(报表10, varRow, 4, timeStr); // 停止时间写入第4列 ReportSetCellString(报表10, varRow, 5, durationStr); // 运行时长写入第5列 } }参数说明与调用示例state必须是1或0由设备运行状态位如PLC的M100.0触发。不要传-1、2等非法值。name字符串常量必须用双引号包裹如冰水循环泵2。motorID整数1或2对应前文定义的Motor1Row/Motor2Row。典型调用场景写在设备“启动”按钮的“按下”事件中SetReport(1, 冰水循环泵1, 1); // 设备1启动典型调用场景写在设备“停止”按钮的“按下”事件中SetReport(0, 冰水循环泵1, 1); // 设备1停止这个函数把所有时间处理、计算、写入逻辑打包以后新增第三台设备只需在switch里加case 3并新建Motor3Row变量调用时传3即可无需动核心逻辑。4. 运行事件配置与启停联动让PLC信号真正驱动报表记录函数写好了但不会自己跑。必须把它挂到真实的设备状态变化上。组态王提供“命令语言”功能可在IO变量的“值发生变化”事件中触发脚本。这才是工业现场的真实工作流PLC告诉组态王“电机1启动了”组态王立刻执行SetReport(1, 电机1, 1)。4.1 创建IO变量映射PLC地址假设PLC中电机1运行状态位地址为DB1.DBX0.0西门子S7-300电机2运行状态位地址为DB1.DBX0.1在组态王“数据库”中新建两个IO离散量变量变量名PLC_Motor1_Run类型IO离散量连接设备已配置的PLC驱动寄存器DB1.DBX0.0变量名PLC_Motor2_Run类型IO离散量连接设备同上寄存器DB1.DBX0.1注意确保PLC驱动已正确配置且通讯正常PLC_Motor1_Run的值能实时反映PLC中该位的状态0或1。4.2 配置“值发生变化”事件命令右键PLC_Motor1_Run变量 → “事件” → “值发生变化” → 勾选“启用”在下方编辑框中输入// 当PLC_Motor1_Run从0变1上升沿视为启动 if (\\本站点\PLC_Motor1_Run 1 GetLastValue(\\本站点\PLC_Motor1_Run) 0) { SetReport(1, 冰水循环泵1, 1); } // 当PLC_Motor1_Run从1变0下降沿视为停止 if (\\本站点\PLC_Motor1_Run 0 GetLastValue(\\本站点\PLC_Motor1_Run) 1) { SetReport(0, 冰水循环泵1, 1); }同样为PLC_Motor2_Run配置事件命令if (\\本站点\PLC_Motor2_Run 1 GetLastValue(\\本站点\PLC_Motor2_Run) 0) { SetReport(1, 冰水循环泵2, 2); } if (\\本站点\PLC_Motor2_Run 0 GetLastValue(\\本站点\PLC_Motor2_Run) 1) { SetReport(0, 冰水循环泵2, 2); }关键函数说明GetLastValue(var)返回变量var在上一个扫描周期的值。这是检测边沿上升沿/下降沿的唯一可靠方法。直接用if (old0 new1)是错的因为old变量不存在。为什么不用“按下/弹起”按钮事件因为现场按钮可能失灵、被短接而PLC状态是真实设备反馈更可信。按钮事件仅作调试备用。4.3 调试技巧用“变量浏览器”实时监控链条运行组态王运行系统后打开“工具”→“变量浏览器”添加以下变量观察PLC_Motor1_Run看是否随PLC变化Motor1Row看启动时是否被赋值为当前LastRecordRowLastRecordRow看每次启动后是否1报表10在画面中拖动查看确认数据实时出现如果Motor1Row始终为0检查①PLC_Motor1_Run事件是否启用②SetReport函数中motorID1分支是否执行可在函数开头加MessageBox(Debug: Motor1 Start);临时弹窗③LastRecordRow初始值是否为1不是0。5. 避坑 / 常见问题 / 排查血泪经验总结的5个致命细节这个方案看似简单但在真实项目中90%的问题都出在细节。以下是我在某高校实验室、某食品厂DCS改造中踩过的坑按发生频率排序5.1 现象报表里只显示启动时间停止时间空白运行时长为“0天0时0分0秒”原因ReportGetCellString(报表10, varRow, 3)读取启动时间失败。常见于①varRow值为0设备从未启动过或Motor1Row变量初始值没设为0②varRow值超出报表当前行数如报表只有5行varRow10③ 启动时间列第3列被意外清空或格式错误如写入了空格、乱码。解决在SetReport函数停止逻辑开头加防御性检查if (varRow 0 || varRow \\本站点\LastRecordRow - 1) { MessageBox(错误设备 StrFromInt(motorID, 10) 未启动或行号越界); return; } string startStr ReportGetCellString(报表10, varRow, 3); if (startStr || StrLen(startStr) 19) { // 标准时间字符串长度至少19 MessageBox(错误启动时间为空或格式错误行号 StrFromInt(varRow, 10)); return; }5.2 现象运行时长计算为负数如“-1天23时59分59秒”原因HTConvertTime函数对日期合法性校验极严。如果启动时间字符串中月份为13、日期为32、或年份为0000HTConvertTime会返回0导致endTime - 0 endTime而endTime是当前时间戳远大于0但若startTime解析失败返回0则timeLength为正大数再经除法后显示为负因long类型溢出。更常见的是启动时间字符串格式不对如2024-6-15 14:23:08月/日未补零StrMid取值错位midM取到6 带空格StrToInt失败返回0。解决严格校验startStr格式。在解析前加if (StrLen(startStr) ! 19 || StrMid(startStr, 5, 1) ! - || StrMid(startStr, 8, 1) ! - || StrMid(startStr, 11, 1) ! || StrMid(startStr, 14, 1) ! : || StrMid(startStr, 17, 1) ! :) { MessageBox(错误启动时间格式非法应为YYYY-MM-DD HH:MM:SS当前 startStr); return; }5.3 现象同一台设备多次启停报表中只有一行记录且“停止时间”覆盖了“启动时间”原因Motor1Row变量被重复写入。典型场景设备启动后PLC状态位因干扰抖动在100ms内反复0→1→0导致SetReport(1,...)和SetReport(0,...)在极短时间内连续执行Motor1Row被覆盖。解决增加防抖逻辑。在SetReport启动分支中加入“如果Motor1Row已非0且该行第4列停止时间为空则不执行启动写入”if (motorID 1 \\本站点\Motor1Row 0) { string stopTime ReportGetCellString(报表10, \\本站点\Motor1Row, 4); if (stopTime ! ) { // 说明上一周期已停止可安全写新周期 // 执行启动写入... } else { MessageBox(警告设备1上一周期未停止忽略本次启动); return; } }5.4 现象报表显示正常但导出Excel后时间列变成数字如44234.592时长列丢失原因组态王报表导出时对字符串列默认按数值处理。2024-06-15 14:23:08被Excel识别为日期0天2时15分33秒被识别为文本但格式错乱。解决导出前在报表设计界面右键每列 → “列属性” → “格式” → 设置第3、4列时间格式设为“文本”第5列时长格式设为“文本”这样导出的CSV/Excel中内容会自动加英文双引号Excel打开时即为纯文本。5.5 现象组态王运行一段时间后报表卡死点击无响应原因ReportSetCellString和ReportGetCellString是重量级操作频繁调用如每秒多次会阻塞UI线程。原文方案在设备启停瞬间调用本无问题但如果误将此函数放在“循环脚本”或“定时器”中就会雪崩。解决确认SetReport只在IO变量“值发生变化”事件中调用绝对禁止放在① 全局脚本的while(1)循环中② 画面“定时刷新”事件中哪怕设为10秒③ 其他任何周期性触发的命令中。这是性能红线。6. 进阶技巧让记录具备断电续写与跨班次统计能力做到上面五章你已经能稳定记录电机启停了。但工业现场还有两个刚需一是组态王意外重启后不能丢掉正在运行的周期二是夜班开的电机白班停的时长要跨天计算。这两个需求原文没提但一线项目天天碰。6.1 断电续写用文件存储代替纯内存变量内存变量Motor1Row、Motor2Row、LastRecordRow在组态王关闭后会清零。如果电机正在运行时断电重启后Motor1Row0再停机就找不到启动时间了。解决方案用组态王的FileWrite/FileRead函数将关键变量值存到本地文件。步骤在组态王“系统配置”→“启动命令”中添加一行// 启动时读取上次保存的变量值 string data FileRead(C:\\KingView\\Runtime\\MotorRecord.dat); if (data ! ) { \\本站点\Motor1Row StrToInt(StrMid(data, 1, 10)); // 前10位存Motor1Row \\本站点\Motor2Row StrToInt(StrMid(data, 11, 10)); // 中间10位 \\本站点\LastRecordRow StrToInt(StrMid(data, 21, 10)); // 后10位 }在SetReport函数每次成功写入后启动/停止逻辑末尾添加// 将三个变量值格式化为30位字符串各10位不足左补0写入文件 string saveData StrFromInt(\\本站点\Motor1Row, 10) StrFromInt(\\本站点\Motor2Row, 10) StrFromInt(\\本站点\LastRecordRow, 10); FileWrite(C:\\KingView\\Runtime\\MotorRecord.dat, saveData);注意FileWrite路径必须是组态王运行账户有写入权限的目录如C:\KingView\Runtime避免写到Program Files下因UAC被拒。6.2 跨班次时长统计用SQL报表替代普通报表普通报表只能显示不能聚合。要统计“今日总运行时长”得手动加。升级方案将记录存入Access数据库用SQL报表查询。实施要点新建Access数据库MotorLog.mdb建表RunLog字段ID(AutoNumber),MotorName(Text),StartTime(DateTime),StopTime(DateTime),DurationSec(Long)修改SetReport停止逻辑不再用ReportSetCellString改用SQLInsertstring sql INSERT INTO RunLog (MotorName, StartTime, StopTime, DurationSec) VALUES ( name , # startStr #, # timeStr #, StrFromInt(timeLength, 10) ); SQLExecute(MotorLogDSN, sql); // MotorLogDSN为已配置的ODBC数据源在组态王中新建“SQL报表”数据源选MotorLogDSNSQL语句SELECT MotorName, SUM(DurationSec)/3600.0 AS TotalHours FROM RunLog WHERE StartTime #2024-06-15 00:00:00# GROUP BY MotorName这样就能实时看到每台电机当日累计运行小时数。6.3 我的习惯每次部署必做的三件事从那以后我每次给客户部署这个功能都强制走一遍这三步先关掉所有PLC输出用组态王“变量仿真器”手动置位PLC_Motor1_Run单步执行SetReport用MessageBox逐行打印varRow、startStr、timeLength确认数值流无异常在报表设计界面把第3、4、5列的“格式”全部设为“文本”并右键列头→“自动调整列宽”避免导出时错位在组态王“系统配置”→“退出命令”中加入FileWrite保存当前变量值确保关机前最后一刻也落盘。这三步多花10分钟能省去后续80%的现场返工。希望帮到你。本文还有配套的精品资源点击获取