WinCC画面图层隐藏显示:状态字+VBS脚本与权限控制
简介:这份资源围绕西门子WinCC画面图层控制展开,面向工业自动化领域从事SCADA组态开发的初、中级工程师与项目调试人员,重点解决画面中图层可见性无法灵活切换、动态交互实现困难的问题。资源包共收录298个文件,压缩后约10.1MB,以rpl、pdl、dcf、dum等WinCC项目与运行文件为主体,辅以xml、ini、cfg等配置文件及mdf、ldf数据库文件,构成一套可直接打开研究的完整组态工程。其中LayerVisible相关文件与VBS脚本示例,演示了通过Layer.Visible属性在按钮点击等事件中控制图层显示与隐藏的写法,便于理解事件驱动编程的组织方式。已有2288人学习该案例,读者可借此掌握多图层画面分层管理、隐藏图层承载数据加载或错误提示等实用技巧,并参考工程目录结构搭建自己的调试环境。
1. 一个误触引出的图层需求:画面越做越大之后,隐藏显示成了刚需
某包装线项目的操作画面上,一个手动阀按钮被压在趋势曲线底下,换型时操作工误触,整线停了十几分钟。复盘下来不是操作工的责任,而是画面把“常用”和“偶尔用”的东西全堆在同一层,靠位置和透明度互相遮挡。这类问题在 WinCC 项目里很常见:工艺流程图、报警面板、参数表、维护按钮挤在一屏上,删不掉也放不下。
这套“WinCC 画面图层隐藏显示”方案要解决的就是这件事——用图层号把画面对象分组,运行期按需显示或隐藏,并按用户权限区分谁能打开哪一层。适合正在做 WinCC 课程设计、毕设,或者手里有一个越改越乱的现场画面、想把它重新收拢的读者。
2. 需求拆解与选型:四种隐藏显示手段各自的适用边界
动手写脚本之前,先要把“隐藏”这件事拆细。很多人一上来就在按钮里写十几行SetVisible,画面能跑,但改一次工艺就要重写一遍脚本,返工量比画画面本身还大。合理的顺序是先定图层职责,再定用什么手段去控制它。
2.1 图层要解决的不是“看不见”,而是遮挡顺序和权限边界
WinCC 的画面编辑器里,对象本身就有前后叠放的顺序,图层(Layer)在这一层之上又给了一个逻辑分组。同样是“把维护按钮藏起来”,两种做法的后果完全不同:靠位置藏,对象还在画面上,鼠标点过去依然可能命中;靠图层藏,把整组对象的可见性关掉,交互和显示一起消失,才是真正的隐藏。
所以图层的第一个职责是收拾遮挡。工艺流程图放底层,管道、设备、流向属于“永远要看”的内容;报警横幅、参数面板、维护工具栏放上层,属于“需要时再看”的内容。第二个职责是切权限。现场操作工只该看到操作层,维护层、标定层、诊断层应该由更高权限的用户打开。把这两件事想清楚,图层编号才有依据,否则编到后面自己都记不住第 17 层放的是什么。
2.2 四种手段的对比:对象属性、图层脚本、画面窗口、弹出画面
WinCC 里能做“显示/隐藏”的路径不止一条,选错了不是不能用,是后面维护成本会不断放大。我一般按下面这张表来定:
| 手段 | 做法 | 优点 | 代价 | 适用场景 |
|---|---|---|---|---|
| 对象可见性 | 给单个对象的“显示”属性绑定 BOOL 变量 | 不用脚本,改起来直观 | 对象多了要一个个绑,容易漏 | 少量固定对象,如某个铭牌、某段说明文字 |
| 图层脚本 | 按对象名前缀批量设置Visible | 一次控制一整层,扩展性好 | 依赖命名规范,需要脚本功底 | 主体方案,工艺层/报警层/维护层切换 |
| 画面窗口切换 | 用画面窗口控件切换PictureName | 各画面独立,互不干扰 | 状态不共享,跨窗口同步麻烦 | 差异很大的整屏内容,如手动/自动两套界面 |
| 弹出画面 | 按钮触发弹出画面或对话框 | 不占主画面,聚焦单一任务 | 弹出位置和层级要额外调 | 参数设定、确认提示这类短交互 |
我的结论是:主体内容用图层脚本,零星对象用属性绑定,整屏切换才用画面窗口。弹出画面只用于短交互,不要拿它当图层用,否则一个画面上弹出三四个窗口,操作工反而更乱。三种手段混用没问题,但要约定清楚边界:哪一层归脚本管,哪几个对象归变量管,写进设计说明,别让下一个人靠猜。
2.3 图层编号规划表:从 L00 到 L31 的职责分配
WinCC 的对象图层号通常可用 0~31,正好能和 32 位状态字一一对应。编号不要随手定,按“越底层越基础、越上层越专用”排,并且留出空位:
| 图层 | 前缀 | 内容 | 默认可见 | 最低权限 |
|---|---|---|---|---|
| L00 | L00_ | 背景、边框、标题栏 | 是 | 操作员 |
| L01 | L01_ | 工艺流程图与管道 | 是 | 操作员 |
| L02 | L02_ | 设备状态与数值显示 | 是 | 操作员 |
| L03 | L03_ | 操作按钮(启动、停止、复位) | 是 | 操作员 |
| L04 | L04_ | 报警横幅与提示条 | 是 | 操作员 |
| L05 | L05_ | 参数设定面板 | 否 | 班组长 |
| L06 | L06_ | 趋势与历史曲线 | 否 | 班组长 |
| L07 | L07_ | 维护与手动点动 | 否 | 维护工程师 |
| L08 | L08_ | 标定与诊断 | 否 | 维护工程师 |
| L09~L31 | Lxx_ | 预留 | 否 | 按需 |
前缀固定四位,L+ 两位编号 + 下划线,后面直接接英文功能名,例如L05_ParaPanel、L07_JogValve。这套前缀不是为了好看,是让脚本能靠字符串比较批量找对象——名字规范了,脚本就只有一份;名字乱了,每加一个对象都要回去改代码。
提示:编号表要作为项目文档的一部分随画面一起交付,画面改了多少次,这张表就跟着更新到哪一版。现场调试时最怕的就是“文档上一套、画面上另一套”。
3. 总体设计:状态字 + 图层管理器 + 画面脚本的三段式结构
图层方案真正的难点不在“怎么把对象设成不可见”,而在“关掉之后,下次打开画面它还记不记得”。我见过不少课设,按钮点一下能隐藏,切到别的画面再回来就全亮了,答辩时被问一句就卡住。解决办法是把状态从画面里拎出来,存到一个独立的地方。
3.1 数据层:用一个 32 位状态字承载全部图层开关
状态字用一个 32 位无符号变量,建议建在内部变量里,命名LayerState,位 0 对应 L00,位 1 对应 L01,依此类推。值为 1 表示该层可见,为 0 表示隐藏。这样整个项目的图层状态只占一个变量,读写一次就够,不用维护几十个 BOOL。
为什么不用几十个 BOOL 变量?一是脚本里读 32 次和读 1 次的差别在低配工控机上体现得很明显;二是状态字方便做整体操作,比如“恢复默认视图”就是往变量里写一个常量;三是备份和复位简单,清 0 就是全部隐藏,写0x1F就是前五层可见。如果项目里还要记录操作日志,一次写变量也好过连写几十条。
' 状态字约定:bit0~bit31 依次对应 L00~L31 ' 常量集中定义,改约定只改这里 Const LAYER_BASE_VISIBLE = 31 ' 前五层默认可见,二进制 0001 1111默认视图的初值一般在画面启动时写入,或者干脆在 PLC 侧或全局脚本里初始化一次。我倾向于在启动画面里初始化,理由很实际:现场调试时要能一个人把默认值改掉重看效果,不用去动全局脚本。
3.2 逻辑层:图层管理器的函数划分与调用时机
图层管理器不用做成多复杂的东西,三个函数就够:
ApplyLayer(sPrefix, bShow)负责把当前画面里匹配前缀的对象统一设成显示或隐藏;WriteLayerBit(iBit, bShow)负责把状态写回状态字;ReplayLayers()负责在画面打开时按状态字把本画面的图层重放一遍。前两个被按钮调用,第三个挂在画面的事件上。
调用时机很关键,我一般固定三处:画面打开事件(OnOpen)重放状态;图层按钮点击(OnClick)切换并写状态;权限切换或用户注销之后强制回到默认视图。第三处最容易被漏掉,结果是高权限用户开着维护层走了,换操作工上来,维护按钮还亮着,这就是个实实在在的隐患。
还有一个设计取舍要提前定:状态字是全局的还是每画面一份?全局的实现简单,但不同画面对“第 5 层”的定义必须一致;每画面一份灵活,代价是要多维护一张对照表。中小项目我一般用全局状态字 + 严格统一的图层编号表,够用且不容易乱。画面特别多、图层含义差异大的项目,才拆成每画面独立。
3.3 交互层:按钮、权限与状态回显怎么串起来
按钮本身要做三件事:判断当前状态、执行切换、把结果反馈给操作工。第三件经常被忽略,导致按钮按下去没反应,操作工以为死机,其实是他没有权限。
我一般把图层按钮做成两种形态。工具栏上的小按钮用图标或文字切换,比如“参数”按钮在面板打开时高亮;画面角落的锁形按钮只负责切换整组维护层,按下后弹出确认。权限判断一定要写在脚本内部,而不是只依赖按钮的“权限”属性——按钮的权限属性拦得住鼠标,拦不住别的地方调用这个脚本。
' 从内部变量读当前登录用户,做图层权限校验 Function CanOpenLayer(ByVal iBit) Dim sUser sUser = HMIRuntime.Tags("@CurrentUser").Read Select Case iBit Case 7, 8 ' L07、L08 属维护层 CanOpenLayer = (sUser <> "" And sUser <> "operator") Case 5, 6 ' L05、L06 属班组长层 CanOpenLayer = (sUser <> "" And sUser <> "operator") Case Else CanOpenLayer = True End Select End Function@CurrentUser是用户管理里的当前用户名,没登录时读到空串。不同版本对这个内部变量的支持略有差别,上线前先在测试画面上打一行日志确认能读到值,别直接照抄。用户组名按项目实际的用户管理配置来改,我这里用operator只是举例。
4. 关键实现:命名约定、批量脚本与动态化配置
设计定完,落地就三件事:对象名字要能被脚本找到、脚本要能正确处理组对象、不写脚本的部分要配到位。这三件事任何一件出问题,表现都是“按钮点了没反应”,排查起来很折磨人。
4.1 命名约定与对象清单:让脚本找得到对象
WinCC 的 VBS 里,画面对象通过名称访问,脚本能拿到的判断依据主要是ObjectName。所以命名不是风格问题,是接口问题。约定四条:前缀固定四位且大写;前缀后紧跟英文功能名,用下划线或驼峰都行,但全项目统一;同一个逻辑元素拆成多个图元时,全部带同一前缀,例如L04_AlarmBar_Text、L04_AlarmBar_Bg;组对象本身也要带前缀,否则递归进去的入口就没了。
画面上对象一多,光靠肉眼核对前缀很不靠谱。我一般让脚本自己数一遍,做一张对象清单,跟设计文档对照:
' 图层自检:统计每个图层前缀下的对象数量 Sub LayerAudit() Dim d, n, o, k Set d = CreateObject("Scripting.Dictionary") For n = 0 To Screen.Items.Count - 1 Set o = Screen.Items(n) k = Left(o.ObjectName, 4) ' 取前四位当前缀 If Left(k, 1) = "L" Then If d.Exists(k) Then d(k) = d(k) + 1 Else d.Add k, 1 End If End If Next For Each k In d.Keys HMIRuntime.Trace "图层 " & k & " 对象数 " & d(k) & vbCrLf Next End Sub这段只统计顶层对象,跑一次就能发现两类问题:某个图层一个对象都没有(编号写错了或者漏了前缀),某个图层对象数比文档多(后来加的对象没登记)。我在项目里会把它挂在一个隐藏的诊断按钮上,交付前跑一遍,比人工翻画面快得多。
4.2 VBS 批量切换的实现与递归处理
核心难点是组对象。画面上的管道、阀门经常被编成一个组,脚本只遍历顶层就只能改到组本身,组里面的图元仍然按原样显示。所以必须往下递归。
' 把当前画面里带指定前缀的对象统一设成显示或隐藏 Sub ApplyLayer(ByVal sPrefix, ByVal bShow) Dim n, o For n = 0 To Screen.Items.Count - 1 Set o = Screen.Items(n) If Left(o.ObjectName, Len(sPrefix)) = sPrefix Then o.Visible = bShow End If WalkGroup o, sPrefix, bShow ' 组对象继续向下钻 Next End Sub ' 递归处理组对象内部成员;普通对象没有 Items,按错误跳过 Sub WalkGroup(ByVal oParent, ByVal sPrefix, ByVal bShow) Dim c On Error Resume Next For Each c In oParent.Items If Err.Number <> 0 Then Exit For If Left(c.ObjectName, Len(sPrefix)) = sPrefix Then c.Visible = bShow End If WalkGroup c, sPrefix, bShow Next Err.Clear End SubScreen指当前画面,Screen.Items.Count是顶层成员数,ObjectName是对象名,Visible就是要改的属性。On Error Resume Next在这里是权宜之计:普通图元没有Items集合,访问就报错,靠错误跳过比判断类型更省事。但正式项目里我建议先判断对象是否支持子集合,再用On Error兜底,否则一旦有别的错误也被一并吃掉,排查时连线索都没有。
按钮事件把三层串起来:
Sub OnClick(ByVal Item) Dim dw, bShow dw = CLng(HMIRuntime.Tags("LayerState").Read) bShow = ((dw And 32) = 0) ' 第 5 位为 0 表示当前隐藏,点击后显示 If Not CanOpenLayer(5) Then HMIRuntime.Trace "无权限打开 L05" & vbCrLf Exit Sub End If ApplyLayer "L05_", bShow If bShow Then dw = dw Or 32 Else dw = dw And (Not 32) End If HMIRuntime.Tags("LayerState").Write dw End Sub这里32就是2^5,对应第 5 位。CLng不能省,VBS 里位运算碰到浮点会出意外结果。权限判断放在切换之前,避免先改了画面再判断,出现“闪一下又弹回来”的诡异现象。
画面打开时重放状态:
Sub OnOpen() Dim dw, i dw = CLng(HMIRuntime.Tags("LayerState").Read) For i = 0 To 8 ' 只重放本画面用到的图层 ApplyLayer "L" & Right("0" & CStr(i), 2) & "_", ((dw And CLng(2 ^ i)) <> 0) Next End Sub注意循环上界写的是 8,不是 31。每调用一次ApplyLayer都要遍历一遍画面对象,循环 32 次就是 32 遍全遍历,画面对象多的时候打开会明显迟钝。只重放本画面实际用到的图层,是这里最有效的一步优化。
4.3 C 脚本版本:什么时候必须换语言
VBS 写起来舒服,但在两种情况下我建议换 C 脚本:一是老项目或者低配工控机,逻辑一多就感觉按钮有延迟;二是需要调用 WinCC 里只有 C 接口才提供的函数。C 版本的思路完全一样,只是改成逐个对象调用SetVisible:
#include "apdefap.h" void OnClick(char* lpszPictureName, char* lpszObjectName, char* lpszPropertyName) { /* 一次处理一个图层的成员,对象名已按前缀约定命名 */ SetVisible(lpszPictureName, "L05_ParaPanel", FALSE); SetVisible(lpszPictureName, "L05_ParaTable", FALSE); SetVisible(lpszPictureName, "L05_ParaFooter", FALSE); /* 状态写回状态字,供画面打开时重放 */ DWORD dw = GetTagDWord("LayerState"); dw &= ~(1u << 5); /* 清第 5 位 */ SetTagDWord("LayerState", dw); }lpszPictureName是当前画面名,SetVisible的第三个参数是目标状态,GetTagDWord/SetTagDWord读写 32 位变量。C 版本没法像 VBS 那样遍历集合,只能一个对象一行,这是它的短板——图层里对象一多,代码就长得没法看,而且改画面就得改代码。
折中的办法是用 C 脚本按位写状态字,把批量设置可见性的活儿留给 VBS,各干各擅长的事。真要追求速度,可以用 C 脚本配合对象名数组循环,把对象名集中在一张表里,但维护成本会上升,中小项目没必要。
4.4 不写脚本的做法:动态化对话框绑定变量
不是所有隐藏都要写脚本。对象少、关系固定的时候,直接在动态化对话框里给“显示”属性绑一个 BOOL 变量最省事,改起来连代码都不用看。典型用法是给画面窗口控件的“显示”属性绑定变量,或者给某段说明文字绑定LayerState的某一位。
这里有个坑要提前说:变量绑定是“跟随”关系,变量一变属性就变;脚本是“一次性”动作,改完就结束。两者混用的时候,同一层既绑了变量又被脚本改,就会出现“按下去又弹回来”的现象。我的做法是一层只用一种方式,脚本管理的图层,对象上就不要绑可见性变量。
顺便说一句,图层可见性和对象“使能”属性是两回事。隐藏一个按钮,按钮看不见也点不到;把按钮设成禁用,它还在那儿,只是变灰。维护层的按钮我倾向用隐藏,因为它压根不该出现在操作工的视野里;工艺条件不满足的操作,才用禁用加提示文字。
5. 避坑与排查:图层方案最容易翻车的五个点
5.1 隐藏不生效:先查组对象、前缀和事件挂载
现象:按钮点下去,画面上大部分元素隐藏了,但管道里几个阀门还在。
原因:这些阀门被编成了组,脚本只遍历到组本身,组内成员没跟着变;或者组对象的名称没带前缀,递归的入口就断了。
解决:确认组对象自身带前缀,并且WalkGroup真的被调用到。排查时在递归里加一行HMIRuntime.Trace c.ObjectName & vbCrLf,把钻进去的对象全打出来,一眼就能看出断在哪一层。
现象:某个对象无论怎么点都不隐藏。
原因:前缀拼错,L05写成L5或者大小写不一致,字符串比较不匹配;也可能这个对象是后来加的,压根没起名前缀。
解决:跑一遍 4.1 里的自检脚本,对比对象数量;命名统一大写,前缀长度用Len(sPrefix)动态取,不要写死。
现象:画面打开时图层状态是乱的,像“回放旧状态”。
原因:OnOpen没挂上,或者挂在画面的事件里但函数名写错(OnOpen大小写敏感)。
解决:在OnOpen里先Trace一行,确认事件确实进得来;再看状态字的值是不是上一次留下的。
5.2 隐藏生效但行为异常:点击穿透与闪烁
现象:图层隐藏后,原位置还能点到东西,或者点到下面的按钮。
原因:用改背景色、改透明度的方式“假隐藏”,对象仍然响应鼠标;也有的是用了画面窗口叠加,窗口本身还占着位置。
解决:一律用Visible属性,不要靠颜色伪装。如果确实需要“看得见但不能点”,那要用使能属性并配灰显,别和隐藏混为一谈。
现象:切换时画面明显闪一下,或者面板先消失再出现。
原因:先改了某个对象、又改回来,中间状态被刷出来了;常见于脚本里写了两次相反的赋值,或者按钮事件和变量绑定同时生效。
解决:一层只保留一条控制路径。脚本里先把目标状态算完,再统一赋一遍,不要边算边改。
5.3 状态丢失与性能拖慢
现象:切到别的画面再切回来,之前隐藏的图层全亮了。
原因:状态只存在画面对象上,没写回状态字;或者写回了但OnOpen没做重放。
解决:每次切换必须同时做两件事——改画面、写状态字。这两步写在一个函数里,别拆开,拆开就一定有人漏。
现象:对象多的画面,点图层按钮要等一两秒才有反应,操作工以为卡死,连点好几下。
原因:每次点击都全画面遍历,甚至嵌套递归里反复访问集合;或者OnOpen里循环了全部 32 层。
解决:只处理目标前缀,只重放本画面用到的图层;必要时把对象集合先缓存到数组里复用。按钮点击后立刻改按钮自身的高亮状态做即时反馈,操作工就不会连点。
注意:连点导致的状态错乱很难复现,但在现场确实会发生。按钮事件开头加一个简单的防抖标志位,能省掉很多排查时间。
6. 验收与答辩:用一张检查表和一段自检脚本证明图层是可靠的
图层这种东西,演示时点两下看着都对,一验收就露馅。我一般按下面这张表逐项过,一项不过就不算完成:
| 检查项 | 验证方法 | 通过标准 |
|---|---|---|
| 图层前缀完整 | 跑LayerAudit,与编号表逐行对比 | 无空层、无未登记对象 |
| 组对象递归生效 | 隐藏含组对象的图层,逐一点击组内图元原位置 | 组内成员全部不可见、不可点 |
| 状态持久性 | 切换图层后进出其他画面三次 | 返回后状态与离开时一致 |
| 权限边界 | 用操作员账号点维护层按钮 | 无变化,且有日志记录 |
| 默认视图复位 | 注销后重新登录 | 回到 L00~L04 可见 |
| 响应速度 | 在对象最多的画面上连续切换五层 | 无明显迟滞、无闪烁 |
答辩演示有个小技巧:别只演“点一下藏起来”,要演“藏起来之后状态还在”。顺序是先打开参数层,切到工艺画面再切回来,让评委看到状态被记住;再用低权限账号点维护层,让评委看到被拦住。这两个动作一做完,方案的说服力比讲十分钟设计思路强。
现场留个后手也重要。我习惯在画面角落放一个隐藏的诊断入口,点开就跑一次自检脚本,把各图层的对象数、当前状态字的值打出来。调试期靠它定位问题,交付前把它连同状态字一起复位。
这套东西我第一次做的时候没当回事,觉得按钮能隐藏就算完事,结果现场换型时操作工把维护层打开了关不掉,电话打过来我只能在电话里指挥他切画面。从那以后我每次做图层都强制走一遍:状态字先定、编号表先写、自检脚本先挂,三项齐了才动手画对象。希望帮到你。
本文还有配套的精品资源,点击获取