☰
C#调用Halcon显示海康相机图像:像素转换与平移缩放实战
2026/10/15 18:24:43 网站建设 项目流程

简介:面向机器视觉与工业检测开发者的C#调用Halcon读取海康相机并显示在HsmartHwind上的完整示例工程,解决了相机采集、图像展示及平移缩放交互操作集成的问题。资源共46个文件,压缩包约22.53MB,以C#源码、Halcon依赖的dll、工程配置及编译产物为主,便于直接打开项目进行调试和二次开发。已有927人学习或下载,适合希望快速上手Halcon与工业相机对接的初中级开发者。工程内含相机设备打开、图像流异步抓取、HWindowControl窗口初始化及平移缩放事件处理等关键实现,并展示了错误处理与资源释放思路,对理解Halcon.NET接口调用、交互式显示机制以及工业相机集成流程具有直接参考价值。

1. 为什么要在 C# 里调 Halcon 读海康相机并显示在 hsmartHwind 上

做视觉上位机的人大概率都碰过这个需求:C# 写界面,Halcon 跑图像处理算法,海康工业相机出图,图像要实时显示在 Halcon 的 HSmartWindowControl(标题里的 hsmartHwind 指的就是这个控件)上,并且操作员要能用鼠标滚轮缩放、按住左键拖动,像在 HDevelop 里看图像一样方便。这个标题背后其实拆成三件事:用海康 MVS SDK 把相机的实时流拿下来,把海康的像素格式正确转成 Halcon 的 HImage,再在 WinForms 的窗口控件上自己实现一套平移缩放交互。适合两类人:一类是做上位机界面但没碰过 Halcon 显示细节的工程师,另一类是算法工程师想把调试用的图像窗口搬进自己的工具里。下面按我实际能跑通的顺序把每一步讲清楚,包括那些容易翻车的参数。

2. 环境搭建:Halcon 引用、海康 MVS SDK 与 C# 工程的匹配关系

2.1 版本与位数:先定 x64 还是 x86 再谈其它

这个组合最容易出问题的不是代码,而是运行库位数不统一。Halcon 的 .NET 接口、海康 MVS SDK 的 C# 封装、以及你的 C# 工程,三方必须同为 x64 或同为 x86。我一般建议直接上 x64,因为 Halcon 当前主流的安装包默认带 x64 的 halcondotnet.dll,海康 MVS 的运行时也优先提供 64 位版本。

在 Visual Studio 里把工程的“平台目标”改成 x64:项目属性 -> 生成 -> 平台目标 -> x64。注意不是改解决方案平台,而是改工程属性里的 PlatformTarget,否则调试时仍然会跑成 AnyCPU 或 x86,运行时加载 dll 直接报 BadImageFormatException。

.NET 版本方面,WinForms 工程选 .NET Framework 4.6.2 以上比较稳妥,也可以选 .NET 6/8,但要确认所购 Halcon 版本的 halcondotnet.dll 是否兼容对应运行时。拿不定主意时按实际安装的 Halcon 版本说明为准,别在版本兼容性上冒进。

2.2 添加引用与运行库拷贝:两个最容易漏的步骤

Halcon 的 .NET 接口在安装目录下的 bin/dotnet 文件夹里,核心是 halcondotnet.dll。海康 MVS 的 C# 封装在 MVS 安装目录的 Development\DotNet 下,核心是 MvCameraControl.dll,它依赖同目录下的 MvControlTools.dll 和若干节点配置文件。

我一般这样做:

  1. 在工程里添加两个引用:halcondotnet.dll、MvCameraControl.dll。
  2. 把海康 MVS 依赖的 MvControlTools.dll、MvCameraNode.dll、CameraParams 等文件复制到输出目录,或者直接把整个运行目录加入 PATH。最省事的方法是在工程后生成事件里加一条 xcopy,把整个依赖目录拷进 bin。
  3. Halcon 还需要 license 文件和对应的运行库,license 文件路径写到系统环境变量或安装目录下,如果没有正确加载,CreateHandle 和 ReadImage 都会在运行时报许可证错误。

把引用加好后,先做一步验证:能在代码里new HObject()并调用 HOperatorSet,再打开相机。

2.3 最小验证:不接相机,先在 HSmartWindowControl 上显示一张本地图

这个步骤能帮你把 Halcon 侧的显示链路先打通,免得后面相机、转换、显示三个环节一起排查。

private void btnLoad_Click(object sender, EventArgs e) { HObject img = null; HOperatorSet.ReadImage(out img, @"D:\demo.png"); img.GetImageSize(out HTuple width, out HTuple height); HWindow win = hSmartWindowControl.HalconWindow; win.SetPart(0, 0, height.I - 1, width.I - 1); img.DispObj(win); }

这段代码做了三件事:ReadImage 读取本地图片到 HObject,GetImageSize 拿到图像宽高,SetPart 把显示区域设置成整幅图像,DispObj 将 HObject 输出到 HSmartWindowControl 内部的 Halcon 窗口句柄。

这里要强调 SetPart 的参数顺序:第一个是 row1,第二个是 col1,第三个是 row2,第四个是 col2,也就是先 y 后 x。很多人在这一步写反了,导致图像显示区域裁剪错乱。如果这段能正常显示图片,说明 Halcon 的 dll 引用、license、控件本身都没问题,可以进相机环节了。

3. 从海康相机取流并转换成 Halcon HImage:像素格式是第一个坎

3.1 初始化相机与启动连续取流的四步流程

海康相机用 MVS SDK 打开设备的固定流程是:枚举设备 -> 创建句柄 -> 打开设备 -> 设置像素格式 -> 注册回调 -> 开始抓取。下面是一段我用过的标准打开流程。

using MvCamCtrl.NET; private MyCamera _cam; private bool OpenCamera() { var deviceList = new MyCamera.MV_CC_DEVICE_INFO_LIST(); uint nRet = MyCamera.MV_CC_EnumDevices( MyCamera.MV_GIGE_DEVICE | MyCamera.MV_USB_DEVICE, ref deviceList); if (nRet != 0 || deviceList.nDeviceNum == 0) { MessageBox.Show("未找到相机"); return false; } var stDevInfo = new MyCamera.MV_CC_DEVICE_INFO(); IntPtr pDevInfo = Marshal.AllocHGlobal(Marshal.SizeOf(stDevInfo)); Marshal.Copy(deviceList.pDeviceInfo[0], pDevInfo, 0, Marshal.SizeOf(stDevInfo)); // 仅示意:实际应使用 StructureToPtr 整体拷贝,避免只拷贝首字节 _cam = new MyCamera(); nRet = _cam.MV_CC_CreateHandle(ref stDevInfo); if (nRet != 0) return false; nRet = _cam.MV_CC_OpenDevice(MyCamera.MV_ACCESS_Exclusive, 0); if (nRet != 0) return false; nRet = _cam.MV_CC_SetEnumValue("PixelFormat", (uint)MyCamera.PixelType_Gvsp_RGB8_Packed); if (nRet != 0) return false; nRet = _cam.MV_CC_RegisterImageCallBackEx(CameraCallback, IntPtr.Zero); if (nRet != 0) return false; nRet = _cam.MV_CC_StartGrabbing(); return nRet == 0; }

对枚举到的设备信息,特别注意deviceList.pDeviceInfo[0]在你的工程里类型是 IntPtr,直接拿它创建句柄前必须把数据复制到 MV_CC_DEVICE_INFO 结构体。我见过不少新人在这一步直接用 IntPtr 传给 CreateHandle,编译能过但运行时句柄无效。稳妥方式是先 Marshal.PtrToStructure 到结构,再在 CreateHandle 前把结构体信息保留住。

注册回调后调用 StartGrabbing,相机就开始持续把图像帧送到回调函数里。海康 SDK 的 C# 封装里,回调签名是个 delegate,第一个参数是图像数据指针,第二个参数是帧信息结构体。

3.2 回调线程与 UI 线程:为什么不能直接在回调里创建 HImage

海康相机回调是后台线程执行的,而 HWindow 控件的显示操作必须在创建它的主线程里做。如果直接在回调里构造 HImage 再调用 DispObj,界面大概率会随机卡死或闪退,这是整个链路里最典型的翻车点。

我的处理方式是把回调里的像素数据先拷到托管数组,然后通过 BeginInvoke 投递到 UI 线程,在 UI 线程上做 HImage 转换和显示。

private void CameraCallback(IntPtr pData, ref MyCamera.MV_FRAME_OUT_INFO pFrameInfo, IntPtr pUser) { if (pData == IntPtr.Zero || pFrameInfo.nFrameLen == 0) return; byte[] frameBuffer = new byte[pFrameInfo.nFrameLen]; Marshal.Copy(pData, frameBuffer, 0, (int)pFrameInfo.nFrameLen); ImageFrameArgs args = new ImageFrameArgs { Data = frameBuffer, Width = (int)pFrameInfo.nWidth, Height = (int)pFrameInfo.nHeight, PixelType = pFrameInfo.enPixelType }; BeginInvoke(new Action<ImageFrameArgs>(OnCameraFrameReceived), args); }

每次回调都新建 byte[] 听起来很浪费,但实际效果稳定,而且 UI 侧只保留最新一帧。只要 UI 线程处理速度跟不上采集帧率,就把旧的未处理帧丢弃,保证界面始终显示最新图像。

调用 BeginInvoke 前要判断窗体是否已关闭,否则窗体和句柄都销毁后回调仍然投递,会引发 ObjectDisposedException。这个判断我放在回调函数开头,用IsDisposed || Disposing提前 return。

3.3 Mono8、RGB8、Bayer:三种像素格式转 HImage 的代码模板

海康相机常见输出格式是 Mono8、RGB8_Packed、BayerRG8。不同格式转 HImage 的方式完全不同,我封装了一个函数,按像素类型分支处理。

private HObject ConvertToHImage(byte[] data, int width, int height, uint pixelType) { HObject img = null; unsafe { fixed (byte* p = data) { IntPtr ptr = new IntPtr(p); if (pixelType == (uint)MyCamera.PixelType_Gvsp_Mono8) { HOperatorSet.GenImage1(out img, "byte", width, height, ptr); } else if (pixelType == (uint)MyCamera.PixelType_Gvsp_BayerRG8) { HOperatorSet.GenImage1(out img, "bayer_rg", width, height, ptr); HObject rgb = null; HOperatorSet.CfaToRgb(img, out rgb, "bilinear", "rgb"); img.Dispose(); img = rgb; } else { HOperatorSet.GenImageInterleaved( out img, ptr, "rgb", width, height, 0, "byte", 24, 0, width, height, 0, 0); } } } return img; }

GenImage1 用于单通道数据,第二个参数固定传 "byte",表示每个像素一个字节。Bayer 格式必须先做 CFA 插值,Halcon 里用 CfaToRgb 完成。这里的 "bayer_rg" 要和相机传感器排列严格对应,海康相机若输出 BayerRG8 就选 "bayer_rg",若你改成了 BayerGB8 就换 "bayer_gb",选错的话图像会有大量彩色噪点。

GenImageInterleaved 的参数看着吓人,实际要理解的关键就三个:type 是 "rgb" 还是 "bgr",bitsPerPixel 是 24,imageWidth/imageHeight 与前面 width/height 保持一致。如果图像颜色红蓝互换,把 "rgb" 改成 "bgr" 就行,这个在本文第 5 章还会专门讲。

4. 在 HSmartWindowControl 上实现平移缩放:坐标映射、SetPart 重算与拖拽逻辑

4.1 窗口坐标与图像坐标的差异是交互实现的地基

HSmartWindowControl 显示时,HALCON 内部通过 SetPart 决定把图像哪一块映射到窗口。鼠标事件里拿到的 e.X、e.Y 是控件窗口坐标,左上角是原点,向右是 x,向下是 y。图像坐标用行列表示,row 是行(y 方向),col 是列(x 方向)。二者方向一致,但刻度不同:窗口坐标是像素位置比例,图像坐标是当前显示区域里的实际坐标。

要把鼠标位置换算到图像坐标,必须知道当前 SetPart 的区域。假设当前显示区域是_row1, _col1, _row2, _col2,窗口宽高是 winW、winH,鼠标点对应的图像坐标就是:

imageCol = _col1 + (e.X / winW) * (_col2 - _col1) imageRow = _row1 + (e.Y / winH) * (_row2 - _row1)

这个换算在缩放里是锚点基准,在平移里是增量基准,所有后续公式都从这里展开。

4.2 滚轮缩放:保持鼠标位置不变的关键算法

缩放最自然的交互是:鼠标在哪个点,图像就围绕那个点放大或缩小。直接改 SetPart 宽度高度而不调整左上角坐标,图像会从左上角缩放,鼠标下方的内容会跑掉,手感很差。实现时要以鼠标所在的图像坐标作为锚点,重新计算显示区域。

private void hSmartWindowControl_MouseWheel(object sender, MouseEventArgs e) { if (_currentImage == null || e.Delta == 0) return; int winW = hSmartWindowControl.Width; int winH = hSmartWindowControl.Height; if (winW <= 0 || winH <= 0) return; double rX = (double)e.X / winW; double rY = (double)e.Y / winH; double mouseCol = _col1 + rX * (_col2 - _col1); double mouseRow = _row1 + rY * (_row2 - _row1); double factor = e.Delta > 0 ? 1.25 : 0.8; double newSpanCol = (_col2 - _col1) * (1.0 / factor); double newSpanRow = (_row2 - _row1) * (1.0 / factor); if (newSpanCol < 64 || newSpanRow < 64) return; double newCol1 = mouseCol - rX * newSpanCol; double newRow1 = mouseRow - rY * newSpanRow; double newCol2 = newCol1 + newSpanCol; double newRow2 = newRow1 + newSpanRow; ApplyDisplayPart(newRow1, newCol1, newRow2, newCol2); _row1 = newRow1; _col1 = newCol1; _row2 = newRow2; _col2 = newCol2; }

关键是 rX、rY 这两个比例值。缩放前后,鼠标在窗口中的位置比例必须一致,新区域左上角就是用“鼠标图像坐标减去鼠标位置比例乘以新区跨度”算出来的。factor 取值 1.25 和 0.8,也就是每格缩放 25%,手感适中,不会一下缩得太快。

最小缩放限制是一定要加的,否则用户不断放大,显示区域会变成小数甚至负跨度,SetPart 会报错或显示异常。我通常会加最小跨度限制,同时区域不允许越过图像边界,防止缩放时视图跑丢。

4.3 左键拖拽平移:增量映射与反向移动

平移的直觉是图像跟着鼠标走,鼠标往右拖,图像内容往右移。这要求显示区域相对图像往左移,也就是显示区域的 row、col 值要减去鼠标位移映射到图像坐标的增量。

private Point _mouseDownPos; private bool _dragging; private double _dragStartRow1, _dragStartCol1, _dragStartRow2, _dragStartCol2; private void hSmartWindowControl_MouseDown(object sender, MouseEventArgs e) { if (e.Button != MouseButtons.Left || _currentImage == null) return; _dragging = true; _mouseDownPos = e.Location; _dragStartRow1 = _row1; _dragStartCol1 = _col1; _dragStartRow2 = _row2; _dragStartCol2 = _col2; } private void hSmartWindowControl_MouseMove(object sender, MouseEventArgs e) { if (!_dragging) return; int winW = hSmartWindowControl.Width; int winH = hSmartWindowControl.Height; if (winW <= 0 || winH <= 0) return; double dx = e.X - _mouseDownPos.X; double dy = e.Y - _mouseDownPos.Y; double spanCol = _dragStartCol2 - _dragStartCol1; double spanRow = _dragStartRow2 - _dragStartRow1; double nCol1 = _dragStartCol1 - spanCol / winW * dx; double nRow1 = _dragStartRow1 - spanRow / winH * dy; double nCol2 = nCol1 + spanCol; double nRow2 = nRow1 + spanRow; ApplyDisplayPart(nRow1, nCol1, nRow2, nCol2); _row1 = nRow1; _col1 = nCol1; _row2 = nRow2; _col2 = nCol2; } private void hSmartWindowControl_MouseUp(object sender, MouseEventArgs e) { _dragging = false; }

拖动过程中每次都重新 SetPart 并 DispObj,帧率高的时候会频繁重绘。实际做下来,只要当前机器不是太老,拖动 500 万像素左右的图不会卡。如果图像很大比如 2500 万像素以上,建议在拖动过程中降低 DispObj 频率,比如只在鼠标移动超过 5 个像素时才刷新,否则 UI 线程会被重绘占满。

4.4 ApplyDisplayPart 与边界约束:这一步决定交互是否“稳”

上面几段代码反复调用了 ApplyDisplayPart,这个函数负责把新的显示区域裁剪到图像范围内,再写入 HWindow。边界限制不是可选项,没有它,缩放平移一会儿就把图像拖出视野,再也找不回来。

private void ApplyDisplayPart(double row1, double col1, double row2, double col2) { if (_imageWidth <= 0 || _imageHeight <= 0) return; double spanCol = col2 - col1; double spanRow = row2 - row1; col1 = Math.Max(0, Math.Min(col1, _imageWidth - spanCol)); row1 = Math.Max(0, Math.Min(row1, _imageHeight - spanRow)); col2 = col1 + spanCol; row2 = row1 + spanRow; HWindow win = hSmartWindowControl.HalconWindow; win.SetPart((int)Math.Round(row1), (int)Math.Round(col1), (int)Math.Round(row2), (int)Math.Round(col2)); win.DispObj(_currentImage); }

这里做了两个限制:第一,显示区域不能完全超出图像范围,左上角坐标不小于 0,且不大于“图像尺寸减跨度”,保证画面上始终有内容;第二,SetPart 参数是 int 类型,double 计算后必须取整,否则窗口句柄的显示区域会闪烁抖动。

5. 平移缩放高频踩坑与常见问题排查

5.1 缩放时图像围绕左上角而不是鼠标周围缩放

现象:鼠标滚轮滚动后,图像确实变大了,但放大区域始终是左上角那一块,鼠标位置的内容不是变大而是跑掉了。

原因:没有把鼠标窗口坐标换算成图像坐标,而是直接拿 e.X、e.Y 当锚点,或者缩放时只改了显示区域宽高,没改左上角坐标。

解决:按照第 4.2 的公式来,先计算 rX、rY 比例,再以鼠标所在图像坐标为锚点反推新区域左上角。关键点在于锚点必须是“当前帧鼠标位置对应的图像坐标”,而不是鼠标所在窗口坐标。这个计算看起来只多两步,但恰恰是手感从“能用”变“好用”的分水岭。

5.2 海康图像颜色整体错乱,红蓝互换或偏紫

现象:相机的彩色图显示出来颜色不对劲,红色物体变成蓝色,绿色还在,整体像伪彩色。

原因:海康 SDK 输出的像素字节顺序与 Halcon GenImageInterleaved 的 type 参数不匹配。Halcon 的 "rgb" 表示内存里按红绿蓝排列,而海康某些型号或某些像素格式下实际输出的是 BGR 顺序,或者相反。颜色错乱本质上是通道序错误。

解决:把 GenImageInterleaved 的 type 从 "rgb" 改为 "bgr",或者反过来。如果你用的是 GenImageInterleaved 以外的路径,比如先把海康数据转成 Bitmap 再转 HImage,也会在通道序上出同样问题。我的排查方法是:拍一张红蓝物体分明的图,如果红蓝互换就翻转 type 值,一次就能判断。这个参数没有绝对对错,取决于相机型号和 SDK 版本,代码里建议把 type 做成配置项,换相机不用改代码。

5.3 相机回调里直接调用 DispObj,窗体偶尔卡死或闪退

现象:程序运行时图像能显示,但过一段时间界面无响应,或直接抛异常退出,异常信息指向 HWindow 相关的句柄操作。

原因:海康相机回调线程不是 UI 线程,HWindow 控件的 DispObj、SetPart 必须在创建该控件的线程里调用。跨线程调用 HALCON 窗口句柄,轻则绘制错乱,重则句柄冲突导致进程崩溃。

解决:回调里只做数据拷贝,通过 BeginInvoke 把图像数据投递到 UI 线程再执行转换和显示。具体代码在 3.2 已经给出,这里补充一个细节:回调函数开头要判断IsDisposed,否则窗体关闭时回调仍触发 BeginInvoke,会再次抛异常。窗体关闭流程也要先停止抓取,再释放句柄,顺序反了会在退出时报错。

5.4 长时间运行内存持续增长,几十小时后图像显示一顿一顿

现象:程序开机跑一晚上,内存占用从几百兆涨到几个 G,界面操作明显变卡。任务管理器里能看到进程内存曲线一直往上走。

原因:回调每帧创建 byte[],UI 线程每帧创建 HObject,旧的 HObject 没有被释放。HALCON 的 HObject 是底层对象引用,托管 GC 不一定及时回收,必须显式 Dispose。

解决:在新建 HObject 前把上一个 HObject Dispose 掉,并在窗体关闭时统一清理。代码里建议这样:

private void ShowNewFrame(HObject newImage, int width, int height) { lock (_lockObj) { if (_currentImage != null) { _currentImage.Dispose(); } _currentImage = newImage; } }

这里加 lock 是因为回调线程与 UI 线程都会访问 _currentImage,防止 Dispose 与 DispObj 并发执行产生句柄冲突。内存问题的排查就是看任务管理器面板,确认内存稳定在固定值附近,不再单调上涨,就算处理干净了。

5.5 调整窗口大小后图像被拉伸变形

现象:把控件拖大或拖小,图像看起来明显横向或纵向拉伸,圆都变成椭圆。

原因:SetPart 设定的显示区域宽高比和控件窗口宽高比不一致。HSmartWindowControl 默认按 SetPart 区域拉伸到整个控件,不会自动保持图像比例。窗口尺寸一变,原来的 SetPart 区域比例就和新窗口比例对不上,自然变形。

解决:在控件的 Resize 事件里重新计算显示区域,按窗口比例调整区域跨度,再 SetPart。计算思路是保持显示区域的中心点,以及其中一个跨度方向不变,另一个跨度按窗口宽高比重新计算。

private void hSmartWindowControl_Resize(object sender, EventArgs e) { if (_imageWidth <= 0 || hSmartWindowControl.Height <= 0) return; double winRatio = (double)hSmartWindowControl.Width / hSmartWindowControl.Height; double centerCol = (_col1 + _col2) / 2.0; double centerRow = (_row1 + _row2) / 2.0; double spanRow = _row2 - _row1; double spanCol = spanRow * winRatio; _row1 = centerRow - spanRow / 2.0; _row2 = centerRow + spanRow / 2.0; _col1 = centerCol - spanCol / 2.0; _col2 = centerCol + spanCol / 2.0; ApplyDisplayPart(_row1, _col1, _row2, _col2); }

注意 Resize 事件在窗体初始化阶段就会触发,所以函数开头必须判断 _imageWidth 是否为 0,否则第一次 Resize 时所有区域值都是默认值,会把显示区域清乱。

6. 把显示、取流、交互封装成独立控件:最小可复用 CameraView 控件的做法

前面几章解决了三个“怎么跑通”的问题。最后一个有价值的进阶是把这套逻辑封装成一个独立的 WinForms 控件:外面只暴露一个DisplayFrame(byte[] data, int w, int h, uint pixelType)方法,内部完成 HImage 转换、显示、平移缩放、边界约束和资源释放。

封装思路是新建一个用户控件,里面放一个 HSmartWindowControl,把鼠标事件全部写在这个控件内部。外部窗体只要拿到一帧数据调用 DisplayFrame,不需要关心坐标计算和 SetPart。我在做多相机项目时,就是同一套控件实例化两三个,切换相机时只替换数据源,界面侧逻辑完全复用。

public class CameraViewControl : UserControl { private HSmartWindowControl _hWin; private HObject _currentImage; private int _imageWidth; private int _imageHeight; public CameraViewControl() { _hWin = new HSmartWindowControl(); _hWin.Dock = DockStyle.Fill; Controls.Add(_hWin); _hWin.MouseWheel += OnMouseWheel; _hWin.MouseDown += OnMouseDown; _hWin.MouseMove += OnMouseMove; _hWin.MouseUp += OnMouseUp; _hWin.Resize += OnViewResize; } public void DisplayFrame(byte[] data, int width, int height, uint pixelType) { if (_currentImage != null) { _currentImage.Dispose(); } _currentImage = ConvertToHImage(data, width, height, pixelType); _imageWidth = width; _imageHeight = height; ApplyDisplayPart(0, 0, height - 1, width - 1); } }

这个封装里仍然需要第 3.3 的 ConvertToHImage、第 4 章的整套鼠标事件和边界约束代码。DisplayFrame 每帧来的时候先 Dispose 旧图像再转新图,可以一并解决内存泄漏。这里有一个习惯我吃了不少亏:所有子事件处理函数的开头都要判断 _currentImage 是否为 null 以及控件是否 Disposed,否则设计器拖控件时也会触发 MouseDown,报空引用。

封装好后做一个简单的验证清单:看看缩放手感是否平滑,拖动时图像有没有抖动,连续跑 30 分钟以上内存是否稳定。我自己的经验是,最容易被忽略的并不是缩放公式本身,而是 HSmartWindowControl 在拖拽过程中不断重绘,导致 UI 线程响应变慢。如果拖拽不跟手,优先检查是 HImage 转换占了时间还是 DispObj 占了时间,前者考虑降低帧率,后者考虑在拖动时只做 SetPart 不做全量 DispObj。

这个方向做到位,一套控件就能同时服务几个视觉工位,后续加算法、加记录、加相机切换,都是在外面包逻辑,不用再回到窗口交互这些底层细节里折腾。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询