下面是一个完整的 WinForms 示例,涵盖从设备枚举、下拉列表绑定、打开设备、连续采集显示到关闭设备的完整流程。
一、环境准备
在 Visual Studio 项目中添加以下引用:
| DLL | 路径 | 用途 |
|---|---|---|
MvCameraControl.Net.dll | MVS\MVS\Development\DotNet\x64 | 海康相机 SDK 托管封装 |
halcondotnet.dll | Halcon\bin\dotnet35或dotnet64 | Halcon .NET 接口 |
项目平台目标必须设为x64。
相机设备为MVS虚拟设备。
usingSystem;usingSystem.Collections.Generic;usingSystem.Runtime.InteropServices;usingSystem.Threading;usingSystem.Windows.Forms;usingMvCamCtrl.NET;usingHalconDotNet;二、界面控件
在 Form 上放置:
ComboBox— 命名为cmbDevices,用于显示设备列表Button— 命名为btnEnum,文本“枚举设备”Button— 命名为btnOpen,文本“打开设备”Button— 命名为btnStart,文本“开始采集”Button— 命名为btnStop,文本“停止采集”Button— 命名为btnClose,文本“关闭设备”HSmartWindowControl— 命名为hSmartWindowControl1,Dock 设为 Fill
将halcondotnet.dll添加到工具箱后,HSmartWindowControl控件会出现在工具箱中,拖入窗体即可。
三、完整代码
publicpartialclassForm1:Form{privateMyCamera_camera=null;privateMyCamera.MV_CC_DEVICE_INFO_LIST_deviceList;privateList<MyCamera.MV_CC_DEVICE_INFO>_deviceInfos=newList<MyCamera.MV_CC_DEVICE_INFO>();privateMyCamera.MV_CC_DEVICE_INFO_selectedDeviceInfo;// 采集控制privatebool_isGrabbing=false;privateThread_grabThread;privatereadonlyobject_displayLock=newobject();// 回调委托必须声明为成员变量,防止被 GC 回收privateMyCamera.cbOutputExdelegate_imgCallback;privateGCHandle_gch;publicForm1(){InitializeComponent();}// ============================================================// 1. 枚举设备 → 绑定到下拉列表// ============================================================privatevoidbtnEnum_Click(objectsender,EventArgse){cmbDevices.Items.Clear();_deviceInfos.Clear();_deviceList=newMyCamera.MV_CC_DEVICE_INFO_LIST();intnRet=MyCamera.MV_CC_EnumDevices_NET(MyCamera.MV_GIGE_DEVICE|MyCamera.MV_USB_DEVICE,ref_deviceList);if(nRet!=MyCamera.MV_OK||_deviceList.nDeviceNum==0){MessageBox.Show("未发现设备,错误码: "+nRet.ToString("X8"));return;}// 遍历非托管设备列表,转换为托管结构体for(inti=0;i<_deviceList.nDeviceNum;i++){vardevInfo=(MyCamera.MV_CC_DEVICE_INFO)Marshal.PtrToStructure(_deviceList.pDeviceInfo[i],typeof(MyCamera.MV_CC_DEVICE_INFO));_deviceInfos.Add(devInfo);// 根据设备类型提取序列号/名称作为下拉列表显示文本stringdisplayName="";if(devInfo.nTLayerType==MyCamera.MV_GIGE_DEVICE){vargigeInfo=(MyCamera.MV_GIGE_DEVICE_INFO)MyCamera.ByteToStruct(devInfo.SpecialInfo.stGigEInfo,typeof(MyCamera.MV_GIGE_DEVICE_INFO));displayName=gigeInfo.chUserDefinedName!=""?gigeInfo.chUserDefinedName:gigeInfo.chSerialNumber;}elseif(devInfo.nTLayerType==MyCamera.MV_USB_DEVICE){varusbInfo=(MyCamera.MV_USB3_DEVICE_INFO)MyCamera.ByteToStruct(devInfo.SpecialInfo.stUsb3VInfo,typeof(MyCamera.MV_USB3_DEVICE_INFO));displayName=usbInfo.chUserDefinedName!=""?usbInfo.chUserDefinedName:usbInfo.chSerialNumber;}cmbDevices.Items.Add($"[{i}]{displayName}");}cmbDevices.SelectedIndex=0;}// ============================================================// 2. 打开设备// ============================================================privatevoidbtnOpen_Click(objectsender,EventArgse){if(cmbDevices.SelectedIndex<0){MessageBox.Show("请先枚举并选择设备");return;}if(_camera!=null){MessageBox.Show("设备已打开,请先关闭");return;}_selectedDeviceInfo=_deviceInfos[cmbDevices.SelectedIndex];_camera=newMyCamera();intnRet=_camera.MV_CC_CreateDevice_NET(ref_selectedDeviceInfo);if(nRet!=MyCamera.MV_OK){MessageBox.Show("创建句柄失败: "+nRet.ToString("X8"));_camera=null;return;}nRet=_camera.MV_CC_OpenDevice_NET();if(nRet!=MyCamera.MV_OK){MessageBox.Show("打开设备失败: "+nRet.ToString("X8"));_camera.MV_CC_DestroyDevice_NET();_camera=null;return;}// 设置连续采集模式(关闭触发)_camera.MV_CC_SetEnumValue_NET("TriggerMode",(uint)MyCamera.MV_CAM_TRIGGER_MODE.MV_TRIGGER_MODE_OFF);MessageBox.Show("设备已打开");}// ============================================================// 3. 连续采集到 HSmartWindowControl(回调方式)// ============================================================privatevoidbtnStart_Click(objectsender,EventArgse){if(_camera==null){MessageBox.Show("请先打开设备");return;}// 注册回调(必须声明为成员变量防止 GC)_imgCallback=newMyCamera.cbOutputExdelegate(OnImageGrabbed);_gch=GCHandle.Alloc(_imgCallback);_camera.MV_CC_RegisterImageCallBackEx_NET(_imgCallback,IntPtr.Zero);intnRet=_camera.MV_CC_StartGrabbing_NET();if(nRet!=MyCamera.MV_OK){MessageBox.Show("开始采集失败: "+nRet.ToString("X8"));return;}_isGrabbing=true;MessageBox.Show("已开始连续采集");}/// <summary>/// 回调函数:运行在 SDK 非托管线程上/// 原则:只做深拷贝 + 入队,不做耗时操作/// </summary>privatevoidOnImageGrabbed(IntPtrpData,refMyCamera.MV_FRAME_OUT_INFO_EXpFrameInfo,IntPtrpUser){if(!_isGrabbing)return;if(pFrameInfo.nFrameLen==0)return;// 过滤空帧intwidth=pFrameInfo.nWidth;intheight=pFrameInfo.nHeight;intframeLen=(int)pFrameInfo.nFrameLen;varstFrameInfo=pFrameInfo;// ① 深拷贝到托管内存(回调返回后 SDK 缓冲区可能被覆写)byte[]buffer=newbyte[frameLen];Marshal.Copy(pData,buffer,0,frameLen);// ② 交给独立线程处理,不阻塞回调ThreadPool.QueueUserWorkItem(_=>{try{DisplayImage(buffer,width,height,stFrameInfo.enPixelType);}catch{/* 忽略单帧异常 */}});}/// <summary>/// 将原始图像数据转换为 HObject 并显示到 HSmartWindowControl/// </summary>privatevoidDisplayImage(byte[]buffer,intwidth,intheight,uintpixelType){HObjectho_Image=null;try{// 获取非托管指针GCHandlehandle=GCHandle.Alloc(buffer,GCHandleType.Pinned);IntPtrptr=handle.AddrOfPinnedObject();if(pixelType==MyCamera.MvGvspPixelType.PixelType_Gvsp_Mono8){// 灰度图HOperatorSet.GenImage1(outho_Image,"byte",newHTuple(width),newHTuple(height),newHTuple(ptr));}else{// 彩色图HOperatorSet.GenImageInterleaved(outho_Image,newHTuple(ptr),"rgb",newHTuple(width),newHTuple(height),-1,"byte",0,0,0,0,-1,0);}handle.Free();// 显示到 HSmartWindowControl(必须在 UI 线程上调用)hSmartWindowControl1.Invoke(newAction(()=>{lock(_displayLock){HOperatorSet.DispObj(ho_Image,hSmartWindowControl1.HalconWindow);}}));}finally{if(ho_Image!=null)ho_Image.Dispose();}}// ============================================================// 4. 停止采集// ============================================================privatevoidbtnStop_Click(objectsender,EventArgse){if(_camera==null)return;_isGrabbing=false;intnRet=_camera.MV_CC_StopGrabbing_NET();if(nRet!=MyCamera.MV_OK){MessageBox.Show("停止采集失败: "+nRet.ToString("X8"));}// 等待最后一次回调执行完毕Thread.Sleep(500);// 释放回调委托的 GCHandleif(_gch.IsAllocated)_gch.Free();_imgCallback=null;MessageBox.Show("已停止采集");}// ============================================================// 5. 关闭设备// ============================================================privatevoidbtnClose_Click(objectsender,EventArgse){if(_camera==null)return;// 如果正在采集,先停止if(_isGrabbing){_isGrabbing=false;_camera.MV_CC_StopGrabbing_NET();Thread.Sleep(500);if(_gch.IsAllocated)_gch.Free();}_camera.MV_CC_CloseDevice_NET();_camera.MV_CC_DestroyDevice_NET();_camera=null;MessageBox.Show("设备已关闭");}// ============================================================// 窗体关闭时统一释放// ============================================================privatevoidForm1_FormClosing(objectsender,FormClosingEventArgse){if(_camera!=null){if(_isGrabbing){_isGrabbing=false;_camera.MV_CC_StopGrabbing_NET();Thread.Sleep(500);}if(_gch.IsAllocated)_gch.Free();_camera.MV_CC_CloseDevice_NET();_camera.MV_CC_DestroyDevice_NET();_camera=null;}}}四、调用顺序总结
btnEnum_Click → MV_CC_EnumDevices_NET → 绑定 ComboBox btnOpen_Click → MV_CC_CreateDevice_NET → MV_CC_OpenDevice_NET btnStart_Click → MV_CC_RegisterImageCallBackEx_NET → MV_CC_StartGrabbing_NET [回调自动触发] → Marshal.Copy(深拷贝) → 后台线程 → GenImage1/GenImageInterleaved → HSmartWindowControl 显示 btnStop_Click → MV_CC_StopGrabbing_NET → Thread.Sleep(500) → 释放 GCHandle btnClose_Click → MV_CC_CloseDevice_NET → MV_CC_DestroyDevice_NET FormClosing → 统一释放相机 + GCHandle + HObject五、关键注意事项
回调委托必须声明为成员变量:
_imgCallback如果是局部变量,会被 GC 回收,导致回调失效甚至程序崩溃。回调中必须深拷贝:
pData指向 SDK 环形缓冲区,回调返回后可能被覆写。用Marshal.Copy拷贝到byte[]后再处理。UI 更新必须切回主线程:回调运行在 SDK 线程,不能直接操作
HSmartWindowControl。用Invoke或BeginInvoke切换。停止采集后等待再释放:
MV_CC_StopGrabbing_NET后建议Thread.Sleep(500),确保 SDK 采集线程已完成最后一次回调,否则可能在回调访问已释放对象时崩溃。HObject 必须 Dispose:Halcon 使用非托管内存,C# GC 不感知。每帧转换后的
ho_Image必须在finally中释放。枚举后释放非托管内存:如果使用
Marshal.AllocHGlobal分配设备列表内存,枚举完成后必须Marshal.FreeHGlobal释放。