ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

WinCC弹窗跟随鼠标的C脚本实现与画面边界定位优化

2026/9/17 13:29:04 拓冰建站 浏览量
WinCC弹窗跟随鼠标的C脚本实现与画面边界定位优化 简介人机界面HMI组态设计中的弹窗交互直接影响操作员查看工艺参数的效率。鼠标跟随弹窗是工业监控画面中常见且实用的交互范式其底层依赖鼠标事件回调机制通过获取鼠标坐标并动态计算窗口位置。在西门子WinCC等组态软件中C脚本是实现这一功能的核心扩展手段常与SetLeft、SetTop、GetTagWord等API配合完成坐标换算、标签读取与屏幕边缘溢出判断。合理运用画面设计坐标与屏幕尺寸变量可以在弹窗贴近鼠标的同时保证窗口完整落入可视区域从而避免遮挡相邻设备、降低误操作率。除了设备详情展示该方案还可推广到报警提示、参数快显等多种工控画面场景提升HMI整体操作流畅度与信息可读性。本文围绕WinCC C脚本的弹窗跟随机制拆解坐标定位与边界保护的工程实现细节。1. 弹窗跟随鼠标不是省事而是WINCC画面交互的定位问题在WINCC V7.x的现场项目里鼠标悬停或者点击设备图元从旁边弹出一块显示工艺参数的小窗口是特别常见、也特别容易做砸的需求。很多组态工程师的第一反应是给对象加“可见性”动画让窗口显示在固定位置结果弹窗落在画面角落操作员的视线要在设备和角落之间来回跑现场操作习惯是盯着鼠标附近看固定弹窗反而会遮住相邻设备误点率反而上升。更好的处理方式是把弹窗左上角移动到鼠标按下点附近同时做屏幕边界校验避免窗口跑出可见区域。这篇文章从WINCC的C脚本事件回调机制讲起拆解坐标计算与对齐分支的细节最后封装成可跨画面复用的函数适合正在做WINCC画面交互优化的工程师参考。2. 事件回调与坐标来源WINCC的C脚本如何拿到鼠标位置2.1 鼠标事件的绑定入口WINCC画面编辑器中的每个对象都有一组可绑定脚本的事件。选中图元后右键进入对象属性在“事件”页签下找到“鼠标”可以看到“鼠标左键按下”“鼠标左键释放”“鼠标右键按下”等多个事件项这个弹窗跟随功能通常绑定在“鼠标左键按下”上。绑定方式有两种一种是在事件项里直接编写C脚本另一种是选择已有的脚本函数。无论哪种方式WINCC运行系统在鼠标按下时都会自动调用对应的回调函数。// 在画面编辑器中为图元“鼠标左键按下”事件添加的脚本关联 // 事件触发后运行系统调用下述函数参数由WINCC自动填充 void OnLButtonDown(char* lpszPictureName, char* lpszObjectName, char* lpszPropertyName, UINT nFlags, int x, int y) { // 该函数在左键按下瞬间被调用x、y即当前鼠标坐标 }这段代码没有业务逻辑但能说明回调函数的参数来源。lpszPictureName是当前画面的名称lpszObjectName是触发事件的那个图元对象名nFlags是功能键状态真正离散的是最后两个整型参数x、y。它们的数值由运行系统在事件产生时填充不需要脚本自己调用GetCursorPos之类的系统函数。2.2 坐标原点的理解与常见误解x、y的单位是像素坐标原点是画面左上角不是屏幕左上角也不是图元自身的左上角。以WINCC V7.5为例画面设计分辨率设置为1920×1080运行后实际显示窗口也是1920×1080那么鼠标点击点坐标和画面坐标一一对应。但若设计分辨率与显示器实际分辨率不一致运行系统会做缩放x、y仍以设计分辨率为基准返回。也就是说设计画面是1280×1024显示器是1920×1080鼠标实际点击在距屏幕左边600像素处回调拿到的可能是按画面比例换算后的数值。这个差异在弹窗定位时必须考虑因为SetLeft和SetTop传入的数值也是基于画面设计坐标的。若混用屏幕实际像素和画面设计像素弹窗位置会出现固定偏差。现场排查这类偏差时我一般先确认画面分辨率设置为“适应窗口大小”还是“保持原尺寸”再把鼠标落点坐标和画面中的标尺对照能快速确认是不是坐标系理解出了问题。2.3 屏幕尺寸用标签而不是写死常量原文示例通过GetTagWord(ScreenHeigh)和GetTagWord(ScreenWidth)获取屏幕画面宽高。这里的ScreenHeigh和ScreenWidth必须在WINCC变量管理器中提前建立类型为无符号16位字WORD。它们可以是内部变量也可以来自PLC或上位机数据常见做法是做成内部变量在启动画面的C脚本里用ScreenHeight()和ScreenWidth()系统函数赋值一次。变量名推荐类型连接初始值ScreenWidthWORD无符号16位内部变量0ScreenHeighWORD无符号16位内部变量0给初始值0有讲究若WinCC运行系统尚未执行初始化脚本而鼠标事件先触发宽高读到0时弹窗会被定位到画面左上角附近不会出现负数坐标导致的乱跳。实际使用中可以把变量做成DWORD或者浮点型但GetTagWord函数要求标签是WORD类型类型不匹配脚本会在运行时报错并中止具体排错思路在第5章展开。3. SetVisible与SetLeft组合弹窗跟随鼠标核心C脚本的实现3.1 完整脚本与逐段注释以下是经过整理可用的完整脚本函数体逻辑和原文示例保持一致补充了关键步骤的中文注释。#include apdefap.h // 鼠标左键按下事件处理函数 void OnLButtonDown(char* lpszPictureName, char* lpszObjectName, char* lpszPropertyName, UINT nFlags, int x, int y) { int WindowsHeight, WindowsWidth, PictureHeight, PictureWidth; // 1. 显示弹窗画面窗口 SetVisible(lpszPictureName, PopupWindow, 1); // 2. 修改弹窗标题栏文字 SetPropChar(lpszPictureName, PopupWindow, CaptionText, 设备详细信息); // 3. 读取弹窗自身的宽高取自画面窗口对象属性 WindowsHeight GetHeight(lpszPictureName, PopupWindow); WindowsWidth GetWidth(lpszPictureName, PopupWindow); // 4. 读取当前画面尺寸变量来自WINCC内部标签 PictureHeight GetTagWord(ScreenHeigh); PictureWidth GetTagWord(ScreenWidth); // 5. 横向位置计算 if (WindowsWidth * 2 PictureWidth) { // 弹窗宽度超过画面一半采取右对齐策略 SetLeft(lpszPictureName, PopupWindow, PictureWidth - WindowsWidth); } else if (x WindowsWidth PictureWidth) { // 鼠标位置在画面右半部分弹窗向左平移避免超出画面 SetLeft(lpszPictureName, PopupWindow, x - WindowsWidth); } else { // 常规情况弹窗左上角直接放在鼠标点击处 SetLeft(lpszPictureName, PopupWindow, x); } // 6. 纵向位置计算 if (WindowsHeight * 2 PictureHeight) { // 弹窗高度超过画面一半采取下对齐策略 SetTop(lpszPictureName, PopupWindow, PictureHeight - WindowsHeight); } else if (y WindowsHeight PictureHeight) { // 鼠标位置在画面下半部分弹窗向上平移 SetTop(lpszPictureName, PopupWindow, y - WindowsHeight); } else { // 常规情况弹窗左上角直接放在鼠标点击处 SetTop(lpszPictureName, PopupWindow, y); } }这段脚本把弹窗定位拆成横向和纵向两个互不干扰的计算链先处理最极端的整屏对齐再处理鼠标附近的边缘溢出最后落到常规的鼠标跟随。横向用SetLeft控制窗口左边界纵向用SetTop控制窗口上边界两者组合后弹窗左上角坐标就是(x, y)。3.2 关键API的用途与参数说明脚本里真正起作用的API并不多但每个参数的来源都容易写错。下表把6个核心函数的用途和参数含义列清楚。函数名作用关键参数说明SetVisible(pic, obj, flag)控制指定画面窗口是否可见pic为当前画面名obj为画面窗口对象名flag为1显示、0隐藏SetPropChar(pic, obj, prop, value)修改对象的字符串属性这里用于改CaptionText标题栏文本GetHeight(pic, obj)获取对象当前高度像素值返回对象在画面设计坐标系下的高度GetWidth(pic, obj)获取对象当前宽度像素值返回对象在画面设计坐标系下的宽度GetTagWord(tagName)读取WORD类型内部/外部变量参数是变量名字符串不带符号SetLeft(pic, obj, value)设置对象左边界坐标value可为负值但弹窗会部分移出画面SetTop(pic, obj, value)设置对象上边界坐标与SetLeft配合完成二维定位这里最需要注意的是SetVisible的第二个参数。它接收的是画面窗口对象的对象名在画面编辑器中选中弹窗控件在“对象属性”里看到的“对象名”是什么就填什么。原文示例写的XXXXXL.PDL更像一个画面文件名直接填写会导致运行系统找不到对象但脚本不报错弹窗无任何反应。我项目里的习惯是把弹窗对象命名为PopupWindow或者TipWindow脚本里写实际对象名。3.3 弹窗对象在画面编辑器里的对应配置要让脚本里所有这些调用生效弹窗本身必须是一个嵌入在当前画面中的“画面窗口”对象而不是独立运行的一个顶级窗口。画面窗口可以嵌套任意画面文件这样弹窗的内容维护在单独的PDL文件中逻辑清晰且便于复用。具体配置时先在画面编辑器里放置一个画面窗口控件属性中设置“画面名称”指向真正想弹出的PDL文件比如DeviceDetail.PDL再把画面窗口的“对象名”改成与脚本一致。显示状态默认置为隐藏这样初始进入画面看不到弹窗鼠标点击后才显示出来。弹窗尺寸既可以在属性中写死也会在运行时通过GetHeight、GetWidth读回来参与边界判断。标题栏文本使用SetPropChar动态设置比在弹窗画面里放一个静态文本更灵活同一个弹窗文件可以服务不同设备类型。4. 弹窗不出屏幕宽高判断、对齐分支与像素偏移的边界逻辑4.1 分支判定顺序的数学意义边界处理逻辑表面上是三层if-else实际上反映了两条设计原则窗口在任何情况下都必须完整落在画面内在完整可见的前提下窗口优先贴近鼠标点击点。先看横向的三个分支。第一层WindowsWidth * 2 PictureWidth判定弹窗宽度超过画面宽度的一半。此时弹窗无论左对齐还是右对齐都会在另一侧超出画面唯一可行方案是贴右边缘让弹窗右边界与画面右边界重合左边界坐标为PictureWidth - WindowsWidth。第二层x WindowsWidth PictureWidth说明弹窗宽度虽未超过半屏但鼠标点击点太靠右弹窗保持左上角在x的话右边界会超出画面所以将左边界改为x - WindowsWidth让弹窗右边界恰好位于鼠标点击处左侧。第三层是常规情况直接令左边界等于x。纵向逻辑与横向完全对称SetTop和SetLeft各自独立计算互不影响。这种横纵分离的好处是不需要考虑二维联合越界的复杂情况每一维只有0到画面边界一个约束数学上更容易验证。4.2 弹窗遮挡鼠标位置的偏移量设计按原逻辑弹窗左上角直接落在鼠标点击点意味着弹窗会覆盖点击处下方的图元。某些场景下这不是问题点击后弹窗本来就挡住下面的设备。但若弹窗内容较多、面积较大操作员希望点击的设备处于可见状态或者弹窗本身需要展示鼠标所指位置的信息就必须加入偏移量。#define POPUP_OFFSET_X 24 #define POPUP_OFFSET_Y 24 // 在原有分支基础上将常规情况的定位坐标增加偏移量 SetLeft(lpszPictureName, PopupWindow, x POPUP_OFFSET_X); SetTop(lpszPictureName, PopupWindow, y POPUP_OFFSET_Y); // 同时把边缘溢出判断考虑偏移量防止窗口被挤出画面 else if (x POPUP_OFFSET_X WindowsWidth PictureWidth) { SetLeft(lpszPictureName, PopupWindow, x - WindowsWidth - POPUP_OFFSET_X); }偏移量取24像素是比较中庸的做法既避免鼠标指针被弹窗边缘压住又不会让弹窗离得太远视觉上仍保持“跟随”的直觉。实际项目中如果现场操作员反馈弹窗总挡到数据读取区域可以把偏移量继续调大到32或40像素。需要特别注意偏移量加入后边缘溢出判断中的不等式也要同步带上偏移量否则弹窗在画面边界附近时会先算到正常位置、再被偏移量推出屏幕出现边界弹跳现象。4.3 分支条件与实际表现对照表判定条件坐标计算公式弹窗视觉表现弹窗宽度超过画面一半左边界 画面宽 - 弹窗宽弹窗紧贴画面右边缘弹窗未过半屏但点击点偏右左边界 x - 弹窗宽弹窗位于鼠标左侧右边缘与鼠标重合其余情况左边界 x弹窗正常跟随鼠标弹窗高度超过画面一半上边界 画面高 - 弹窗高弹窗紧贴画面底边缘弹窗未过半屏但点击点偏下上边界 y - 弹窗高弹窗位于鼠标上方下边缘与鼠标重合其余情况上边界 y弹窗正常跟随鼠标当弹窗尺寸超过画面一半时分支把窗口强行贴到右边缘或底边缘这时弹窗会完全覆盖画面一侧的图元。若项目里弹窗内容确实很大我通常会把弹窗拆成两个画面一个只显示概要数据的小弹窗跟随鼠标另一个通过按钮打开的大弹窗固定在画面中央避免大弹窗频繁遮挡全部内容。5. 标签类型与对象名不一致WINCC脚本最常见的三个现场问题5.1 GetTagWord读取类型不匹配导致脚本中断GetTagWord要求读取的目标变量必须是WORD无符号16位类型。实际项目里很多工程师在变量管理器里新建的是INT有符号16位类型或者为了省事直接用了FLOAT鼠标一点弹窗没出来运行日志里能看到脚本执行错误。WINCC的C脚本在运行时遇到类型不匹配会中断当前脚本的执行已经执行过的语句生效错误点之后的语句全部跳过所以有时弹窗出现了但位置不对有时干脆不显示。// 错误写法ScreenWidth是INT类型用GetTagWord读取会出错 // PictureWidth GetTagWord(ScreenWidth); // 推荐写法一变量类型保持WORD脚本不变 // 推荐写法二脚本改用GetTagFloat读取浮点类型标签 PictureWidth GetTagFloat(ScreenWidth); PictureHeight GetTagFloat(ScreenHeigh);如果是内部变量建议直接修改变量类型为WORD与GetTagWord匹配如果标签来自PLC通信区不便修改类型就把脚本换成GetTagFloatSetLeft和SetTop对接收值的类型没有额外要求整数、浮点数都可以传入。修复后要重启WINCC运行系统变量类型的修改在运行状态下不生效。5.2 对象名拼写与中英文字符差异SetVisible找不到对象时不会报错这是弹窗跟随功能最隐蔽的坑。画面窗口对象的名称在画面编辑器右侧“对象属性”面板里可以看到注意对象名和画面文件名是两码事一个画面文件可以被多个画面窗口对象引用。脚本中的字符串必须与对象名完全一致包括大小写、空格、中英文括号。常见失误是WinCC复制画面文件时对象名自动带上_1后缀脚本没同步修改还有项目在多语言环境下编辑对象属性里的名称显示被翻译成中文实际底层名称仍是英文。我调试时会先在脚本里加一句输出把所有用到的名称打出来。printf(pic%s, objPopupWindow, left%d, top%d\n, lpszPictureName, x, y);WINCC的printf输出会进入运行系统的诊断日志在WinCC项目路径下的Diagnosis目录中或通过运行系统自带的“诊断窗口”查看。确认输出里lpszPictureName与画面名一致、对象名确实为PopupWindow后基本能定位问题在名称匹配还是坐标计算。5.3 分辨率切换后标签值未刷新前文提过ScreenWidth和ScreenHeigh用WORD类型存初始值0。现场常见的另一种情况是画面在开发机上分辨率1920×1080部署到现场触屏上分辨率1280×1024开发阶段缓存了正确的标签值但切换分辨率后初始化脚本没有重新执行弹窗仍按1920宽度判断边界导致在1280宽度的画面里弹窗被判定为“未过半屏”实际却移出可显示区域。// 建议在弹窗事件脚本中每次实时读取画面尺寸避免依赖初始化时序 int w, h; w GetTagWord(ScreenWidth); h GetTagWord(ScreenHeigh); // 若标签读取不可靠可直接用系统函数获取画面尺寸 PictureWidth GetPictureWidth(lpszPictureName); PictureHeight GetPictureHeight(lpszPictureName);GetPictureWidth和GetPictureHeight在部分WINCC版本中可用返回的是当前画面设计分辨率下的宽度和高度省去变量管理和初始化时序的麻烦。优先使用系统函数标签法作为变量来源的备选方案能同时规避分辨率刷新和标签类型两个坑。6. 从脚本到函数库把弹窗定位逻辑抽成GSC全局函数用于跨画面复用6.1 在全局脚本中定义参数化弹窗函数单画面脚本写得再好复制到几十个画面里就成了维护负担。WINCC的全局脚本可以写在“全局脚本编辑器”中的GSC文件里编译后供所有画面的事件调用。把弹窗定位逻辑封装成一个函数输入弹窗画面名、对象名、鼠标坐标和显示标题四个参数任何画面里的鼠标事件都能复用同一套逻辑。// 全局脚本文件 GlobalFunctions.GSC #include apdefap.h // 通用弹窗定位函数供所有画面调用 void ShowPopupAtCursor(char* lpszPictureName, char* lpszObjectName, char* lpszCaption, int nX, int nY) { int WindowsHeight, WindowsWidth, PictureHeight, PictureWidth; SetVisible(lpszPictureName, lpszObjectName, 1); SetPropChar(lpszPictureName, lpszObjectName, CaptionText, lpszCaption); WindowsHeight GetHeight(lpszPictureName, lpszObjectName); WindowsWidth GetWidth(lpszPictureName, lpszObjectName); PictureHeight GetTagWord(ScreenHeigh); PictureWidth GetTagWord(ScreenWidth); if (WindowsWidth * 2 PictureWidth) { SetLeft(lpszPictureName, lpszObjectName, PictureWidth - WindowsWidth); } else if (nX WindowsWidth PictureWidth) { SetLeft(lpszPictureName, lpszObjectName, nX - WindowsWidth); } else { SetLeft(lpszPictureName, lpszObjectName, nX); } if (WindowsHeight * 2 PictureHeight) { SetTop(lpszPictureName, lpszObjectName, PictureHeight - WindowsHeight); } else if (nY WindowsHeight PictureHeight) { SetTop(lpszPictureName, lpszObjectName, nY - WindowsHeight); } else { SetTop(lpszPictureName, lpszObjectName, nY); } }全局函数里将对象名和标题都参数化之后弹窗跟随脚本已经从“这段代码只服务于某个画面”升级为“项目中所有弹出窗口的标准行为”。每个图元只需要在鼠标左键按下事件里写一行调用弹窗命名规范和边界策略完全由全局函数统一管理后续想调整偏移量或者增加动画过渡只改一个地方。6.2 画面事件中的一行调用与关闭按钮在实际画面中设备图元的“鼠标左键按下”事件脚本简化为直接传参调用。// 画面内图元的鼠标左键按下事件脚本 ShowPopupAtCursor(lpszPictureName, PopupWindow, 设备详情, x, y);弹窗关闭可以在弹窗PDL画面里放一个关闭按钮按钮的鼠标事件脚本调用SetVisible传入弹窗所在画面的名称和对象名将可见性置0。这里要注意弹窗PDL画面中获取不到宿主画面的lpszPictureName需要借助GetParentPicture这类函数获取父画面名称或者通过对象属性的“隐藏”逻辑联动。6.3 扩展用偏移量参数让不同调用点有不同弹窗位置全局函数再加一个偏移量参数就能让鼠标点击点与弹窗左上角保持灵活距离。void ShowPopupAtCursorEx(char* lpszPictureName, char* lpszObjectName, char* lpszCaption, int nX, int nY, int nOffsetX, int nOffsetY) { // 在nX、nY基础上叠加偏移量后再执行原有边界判断 // 边界判断中同步使用 nX nOffsetX WindowsWidth // 常规情况 SetLeft(..., nX nOffsetX) // 溢出情况 SetLeft(..., nX - WindowsWidth - nOffsetX) }实际调用时传不同的偏移量就能实现不同设备弹出的提示窗口位置风格一致、参数独立的工程效果这也是把单次脚本沉淀为项目级工具后最直接的收益。本文还有配套的精品资源点击获取