FEATURED · 精选文章

WinCC弹窗跟随鼠标的C脚本实现:坐标换算与多屏适配

发布时间 / 2026/9/17 16:04:27
来源 / 创域科博编辑部
栏目 / 资讯中心
WinCC弹窗跟随鼠标的C脚本实现:坐标换算与多屏适配 简介一份聚焦西门子SIMATIC WINCC人机界面开发的实用技术文档面向工业自动化HMI工程师、组态软件二次开发人员。文档针对操作画面中弹窗需要跟随鼠标位置实时显示这一交互需求给出了基于C语言脚本的完整实现方法内容涵盖鼠标左键事件回调、弹窗显示与标题设置、窗口和屏幕尺寸获取、越界判断及自适应对齐策略等关键环节并解释了屏幕宽度、高度等系统变量在弹窗定位中的作用。资源包仅1个docx文件约14KB正文包含可直接参考的C脚本代码和逐段注释并对GetVisible、SetLeft、SetTop、GetHeight等WINCC常用画面函数做了用法说明方便读者理解每个语句的执行逻辑结合自身项目灵活替换弹窗名称和相关变量。该资源已有1711人学习浏览特别适合正在开发WINCC复杂交互界面、希望提升操作友好度的工控技术人员下载学习。1. 当鼠标悬停在设备上SIMATIC WINCC 弹窗跟随鼠标要解决的三件事操作员在 SIMATIC WINCC 画面上巡检时鼠标移到哪台泵旁边就浮出一张小卡片显示这台泵的电流、频率和运行状态鼠标移开卡片消失。这个动作听上去简单但如果挨个给图元挂鼠标事件画面脚本会很快膨胀弹窗也会像拉锯一样来回抖动。用 C 语言脚本做弹窗跟随鼠标是多年项目里被反复验证的稳定路线C 脚本负责取光标位置、换算坐标、控制可见性画面窗口负责当弹窗的容器。适合正在维护老项目、想减少画面事件脚本数量的工程师。下面这条链路可以在运行时直接搬进你的画面里前提是先搞清屏幕坐标、客户区坐标和画面坐标三者之间的换算关系。2. 从鼠标位置到画面坐标C 语言脚本里的坐标换算闭环2.1 为什么 C 脚本不能直接把 GetCursorPos 的结果给画面窗口WINCC 画面窗口对象的 X、Y 属性按画面坐标定位画面坐标原点在画面左上角GetCursorPos拿到的却是屏幕坐标原点在屏幕左上角。二者只有在运行窗口恰好铺满主显示器、没有标题栏、没有滚动条时才一致现场几乎都满足不了。还有一个容易忽视的坑当运行系统窗口带标题栏和菜单栏时客户区原点和屏幕原点之间有一段固定偏移。就算运行窗口在左上角ScreenToClient 之前的原始屏幕坐标也会多出这几十个像素。多显示器操作台上更麻烦副屏在主屏左侧时光标的 X 坐标是负值直接把屏幕坐标赋给画面窗口弹窗会飞出可视区。所以在 C 脚本里要形成一个闭环先取屏幕坐标再定位鼠标所在的窗口最后换算成该窗口客户区坐标才能作为画面窗口的 X、Y 基准。WINCC 的画面窗口如果有滚动条或画面放大缩小还需要再叠加滚动偏移这一层下文会说到。2.2 在 WINCC 的 GSC 环境里声明并调用 GetCursorPosWINCC 的 C 脚本环境GSC支持调用 user32.dll 里的 WinAPI但不能像 Visual Studio 那样直接包含 windows.h。常见做法是在脚本动作头部做外部函数声明#pragma code(user32.dll) BOOL GetCursorPos(POINT* lpPoint); #pragma code()如果脚本编译器提示 POINT 未定义通常是 GSC 头文件不完整在声明上方补一个结构体即可typedef struct { long x; long y; } MYPOINT;GetCursorPos的lpPoint是输出指针函数把鼠标在屏幕坐标系中的坐标写进去返回非零表示成功返回零表示取不到坐标常见于远程桌面会话断开、鼠标驱动异常等场景。脚本里要判返回值取不到坐标时直接把弹窗隐藏而不是沿用上一次的位置否则弹窗会像“幽灵”一样停在现场画面上。提示POINT里的 x、y 用 long 声明不要用 int。C 脚本里的 int 在不同版本上可能是 16 位多显示器虚拟屏幕坐标可能超过 32767 或小于 -32768。一个可用的最小调用片段MYPOINT pt; if (GetCursorPos(pt) 0) { SetTagBit(FLOAT_VISIBLE, 0); return; }这段代码先取光标位置失败就隐藏弹窗。后续所有换算都基于 pt 里的屏幕坐标进行。2.3 把屏幕坐标换算成画面坐标WindowFromPoint 与 ScreenToClient 的配合屏幕坐标不能直接使用要把它换算到运行系统窗口的客户区。常见步骤如下GetCursorPos取得屏幕坐标 ptWindowFromPoint找到该点下方的窗口句柄再用GetAncestor(..., GA_ROOT)取根窗口ScreenToClient把屏幕坐标转换成该根窗口的客户区坐标如果运行系统有工具栏或状态栏再叠加一个校准偏移。MYPOINT pt, cl; HWND hRoot; if (GetCursorPos(pt) 0) { SetTagBit(FLOAT_VISIBLE, 0); return; } hRoot GetAncestor(WindowFromPoint(pt), GA_ROOT); if (hRoot NULL) { SetTagBit(FLOAT_VISIBLE, 0); return; } cl pt; ScreenToClient(hRoot, cl);注意WindowFromPoint返回的不一定是根窗口可能是画面里的某个子控件所以必须用GetAncestor向上取根否则ScreenToClient的坐标系基准会跟 WINCC 画面窗口不一致。cl拿到的是客户区坐标与画面坐标在“无滚动条、无缩放”的前提下可直接对应。下面是这段换算链路涉及的 API 和容易踩的坑API作用典型坑GetCursorPos读光标屏幕坐标返回值要判断远程断开后会拿到旧值WindowFromPoint定位鼠标下方的窗口可能返回子控件句柄不能直接用GetAncestor(..., GA_ROOT)取根窗口需要额外声明 user32 外部函数ScreenToClient屏幕坐标转客户区坐标目标窗口必须是根窗口否则原点错位3. 用 C 语言脚本驱动画面窗口让弹窗内容随悬停对象切换3.1 弹窗载体用画面窗口放一张“空卡片”子画面弹窗本体建议用 WINCC 的画面窗口对象承载而不是用 VBS 动态创建窗口。原因是画面窗口天然跟随 WINCC 的画面生命周期项目关闭时自动释放不存在窗口句柄泄漏缺点是画面窗口本身没有圆角、阴影这些现代 UI 效果但工业监控画面里可读性比好看更重要。具体做法是新建一个子画面 FloatCard.pdl尺寸设成 220×120 左右里面放两个 I/O 域和一个静态文本I/O 域连接要显示的变量在主画面里拖入一个“画面窗口”对象关联到 FloatCard.pdl把画面窗口的 X 属性连接到内部变量FLOAT_XY 属性连接到FLOAT_YVisible 属性连接到FLOAT_VISIBLE画面窗口的“缩放”属性设为“否”避免子画面被拉伸变形。把 X、Y、Visible 直接连到内部变量比在 C 脚本里用对象导航函数改属性更省事。调试时打开 WINCC 变量管理器的“在线表”或变量监视窗口能直接看到 FLOAT_X、FLOAT_Y 的实时变化坐标有没有算错一眼就能判断。3.2 坐标变量类型与 SetTag 调用方式FLOAT_X、FLOAT_Y在变量管理里建议设成“带符号 32 位数DINT”。理由在前面提过副屏在主屏左侧时坐标是负数如果选无符号 DWORD 或 16 位 INT会出现溢出或符号错误。FLOAT_VISIBLE用二进制变量即可。C 脚本里赋值用对应的SetTagDInt和SetTagBitSetTagDInt(FLOAT_X, cl.x 16); SetTagDInt(FLOAT_Y, cl.y 24); SetTagBit(FLOAT_VISIBLE, 1);这里的16、24是弹窗相对鼠标的偏移量目的是让弹窗出现在光标的右下方而不是正好压在光标上。偏移量可自行调整操作画面时不要遮挡正在看的点位。3.3 悬停判定用区域数组代替在每个图元上挂事件很多初版实现是在每个图元的“鼠标移动”事件里挂一段 C 脚本图元一多脚本数量爆炸而且鼠标快速扫过整个画面时事件脚本排队执行弹窗跟不上光标移动。更稳的做法是把设备图元在画面上的矩形区域预先定义成数组由一个周期任务统一判断鼠标落在哪个区域。typedef struct { long left; long top; long right; long bottom; int deviceId; } AREA; AREA g_areas[] { { 10, 10, 120, 80, 1 }, { 140, 10, 260, 80, 2 }, { 10, 120, 120, 200, 3 } };g_areas数组里每一项代表一个可悬停区域deviceId用于区分弹窗内容。配合坐标换算在周期脚本里做一次遍历判断void MoveFloatWnd() { MYPOINT pt, cl; HWND hRoot; int i; int hitArea 0; if (GetCursorPos(pt) 0) { SetTagBit(FLOAT_VISIBLE, 0); return; } hRoot GetAncestor(WindowFromPoint(pt), GA_ROOT); if (hRoot NULL) { SetTagBit(FLOAT_VISIBLE, 0); return; } cl pt; ScreenToClient(hRoot, cl); for (i 0; i 3; i) { if (cl.x g_areas[i].left cl.x g_areas[i].right cl.y g_areas[i].top cl.y g_areas[i].bottom) { hitArea g_areas[i].deviceId; break; } } if (hitArea ! 0) { SetTagDInt(FLOAT_X, cl.x 16); SetTagDInt(FLOAT_Y, cl.y 24); SetTagDInt(FLOAT_DEVICE, hitArea); SetTagBit(FLOAT_VISIBLE, 1); } else { SetTagBit(FLOAT_VISIBLE, 0); } }这段脚本把两件事合并了一是持续移动弹窗位置二是根据光标所在区域决定弹窗是否显示。弹窗内的具体数据可以在子画面里通过变量前缀或直接连接变量来区分FLOAT_DEVICE这个变量可以在画面窗口的动态属性中做“变量前缀”切换不同设备显示不同值。与逐图元挂事件相比区域数组方式把判定逻辑收敛到一个函数里删减设备时只需调整数组不用动画面上的脚本分布。缺点是需要手工维护区域坐标设备位置在编辑器里调整后要同步改数组。4. 弹窗跟随鼠标的节奏控制与多显示器参数边界4.1 刷新周期设成多少从 100ms 起步MoveFloatWnd不要挂在鼠标事件上而是放到 WINCC 全局脚本的“周期执行”动作里。经验值是 100ms 刷新一次。50ms 时肉眼感觉不到更“跟手”CPU 占用率反而明显上升500ms 时弹窗拖动感明显变钝光标已经进入下一个设备区域弹窗内容还没切过来。在全局脚本里选择周期执行后会有两个时间参数起始时间和周期。起始时间可以设一个小于周期的随机数比如 30ms避免多个周期动作在同一时刻挤占脚本引擎周期填 100ms。如果现场画面对象很多还要观察 WINCC 运行系统的 CPU 占用特别是在老工控机上必要时退回 200ms弹窗移动会有一点延迟但不会影响操作判断。4.2 边缘翻转弹窗不超出屏幕的坐标修正光标靠近屏幕右下角时按固定偏移显示的弹窗会超出屏幕边界。修正逻辑一般放在赋值之前先计算弹窗目标位置如果超出虚拟屏幕边界就反向偏移。long winW 220; long winH 120; long xPos cl.x 16; long yPos cl.y 24; if (xPos winW SM_CX_VIRTUAL_SCREEN) xPos cl.x - winW - 8; if (yPos winH SM_CY_VIRTUAL_SCREEN) yPos cl.y - winH - 8;SM_CX_VIRTUAL_SCREEN和SM_CY_VIRTUAL_SCREEN是 Windows 虚拟屏幕的宽高通过GetSystemMetrics读取。翻转后的弹窗会出现在光标左上侧保证完整可见。偏移量 8 是弹窗与光标的间距防止弹窗边框贴住光标影响点击。4.3 高 DPI 与多显示器物理坐标和逻辑坐标的比值换算高分屏是这套方案最容易翻车的地方。Windows 在 150% 缩放时GetCursorPos返回的是物理像素坐标而 WINCC 运行系统内部使用的可能是逻辑像素两者比例是 1.5。表现是光标越往右移动弹窗离光标越远。常见做法是用GetDpiForWindow读取根窗口的 DPI 值再按scale dpi / 96.0对客户区坐标做修正UINT dpi GetDpiForWindow(hRoot); double scale ((double)dpi) / 96.0; cl.x (long)(cl.x / scale); cl.y (long)(cl.y / scale);这个修正只有在 WINCC 运行系统按系统 DPI 感知模式启动时才有效。如果运行系统在兼容模式下被系统强制缩放GetDpiForWindow返回 96画面图像被拉伸这种情况下用脚本修坐标已经没有意义通常要调整快捷方式的“高 DPI 缩放替代”设置让系统代为缩放。多显示器布局中还有一个隐藏问题副屏缩放率和主屏不一致时Windows 会为每个显示器分配不同的 DPI光标跨越显示器时坐标换算结果会跳变。调试时要把两台显示器设成相同缩放率至少弹窗跟随逻辑在大多数现场可以正常工作。现场情况现象处理方式副屏在主屏左侧坐标出现负值变量类型用 DINT边界按虚拟屏幕计算系统缩放 150%光圈越近偏得越多用 GetDpiForWindow 按比例修正远程桌面断开重连光标停在断开位置判断 GetCursorPos 失败后隐藏弹窗不同显示器缩放率不同跨越屏幕时弹窗跳动现场统一各屏缩放率后再部署5. 验证 C 脚本坐标正确性的几个调试办法和防闪烁技巧5.1 先把坐标写到在线变量再决定要不要动画面窗口调试时不要一上来就盯着弹窗是否跟随先把坐标链路拆开验证。在MoveFloatWnd里临时加几个调试变量SetTagDInt(DBG_CURSOR_X, pt.x); SetTagDInt(DBG_CURSOR_Y, pt.y); SetTagDInt(DBG_CLIENT_X, cl.x); SetTagDInt(DBG_CLIENT_Y, cl.y); SetTagDInt(DBG_ROOT, (long)hRoot);然后在画面上放一个临时 I/O 域连接这四个变量分别在画面四个角落移动鼠标核对读数是否符合预期。检查要点如下检查项预期异常时查哪里光标在画面左上角DBG_CLIENT_X/Y 接近 0GetAncestor 是否取到根窗口光标移到主屏右侧DBG_CLIENT_X 随鼠标增大ScreenToClient 是否用 hRoot 调用光标移到左侧副屏DBG_CLIENT_X 出现负值变量类型是否为 DINT光标离开运行窗口DBG_ROOT 是否为当前画面主窗WindowFromPoint 取到了其他应用窗口坐标链路确认无误后再打开画面窗口的跟随开关问题范围一下子缩小到显示侧而不是坐标侧。5.2 用位移阈值和窗口判断减少画面刷新次数弹窗抖动通常不是脚本逻辑错而是每 100ms 都在给画面窗口赋新坐标哪怕光标只移动了 1 个像素。WINCC 图形引擎每次坐标变化都会触发重绘这种毫秒级抖动在画面上表现为弹窗边缘闪烁。解决办法只更新超过阈值的坐标。在脚本里保存上次坐标static long lastX -9999; static long lastY -9999; if (abs(cl.x - lastX) 2 abs(cl.y - lastY) 2) { return; } lastX cl.x; lastY cl.y;2 像素的阈值人眼几乎感知不到但画面窗口的重绘次数会大幅减少。另一个实用技巧是判断光标是否仍在运行窗口内如果GetAncestor(WindowFromPoint(pt), GA_ROOT)返回的句柄不是 WINCC 运行系统主窗口直接隐藏弹窗并返回避免鼠标移到其他软件窗口上时弹窗还悬在画面里。注意static变量在 WINCC 周期执行脚本中也会保留值但脚本被手动停止后static 值会重置为初始值所以阈值判断的初值要设成 -9999避免第一次移动被阈值逻辑吞掉。把位移阈值和窗口判定合进MoveFloatWnd后弹窗在视觉上稳定脚本调用量也明显降低。后续如果现场出现“弹窗偶尔不跟随”的情况优先怀疑的不是脚本而是画面窗口的 X/Y 变量连接是否被其他画面对象重复写入。本文还有配套的精品资源点击获取
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻