基于Qt设计模式的多相机管理框架 海康读码相机适配
- 一、框架简介
- 二、框架核心功能
- 三、核心代码结构与解析
- 四、框架使用步骤
- 五、设计思路与为什么这么设计?
- 六、注意事项
- 七、总结
一、框架简介
代码链接:添加链接描述
本文分享一套基于 Qt 开发的多相机统一管理框架,核心适配海康读码相机(MVCodeReader 系列),同时具备良好的扩展性可兼容其他类型相机。框架设计上采用工厂模式解耦相机对象的创建逻辑,单例思想(管理器) 统一管控多设备生命周期,线程池思想(每个相机独立线程) 避免 UI 阻塞,面向抽象编程定义相机通用接口,最终实现多相机的枚举、连接、抓图、数据回调、参数配置等一体化管理
核心设计亮点
工厂模式(CameraFactory):隔离不同相机类型的实例化逻辑,新增相机类型仅需扩展工厂类和相机子类,符合 “开闭原则”;
抽象基类(Camera):定义所有相机的通用接口(Open/Close/StartGrabbing 等),子类只需实现具体逻辑,统一上层调用方式;
线程隔离:每个相机对象独立运行在子线程,避免相机回调阻塞 UI 线程,保证界面响应性;
信号槽解耦:基于 Qt 信号槽机制实现相机数据、状态、错误信息的跨线程传递,无需手动处理线程同步;
统一数据结构:通过 CameraData 基类 + MVCodeCameraData 子类封装不同相机的回调数据,保证数据传递的一致性。
二、框架核心功能
枚举子网内海康读码相机(GigE/USB 类型);
多相机独立连接 / 断开、启动 / 停止抓图;
实时接收相机回调的图像数据、条码识别结果;
图像数据解析与本地保存(支持 JPEG/Mono8 格式);
相机状态监听(连接状态、错误信息);
可扩展适配其他品牌 / 类型相机。
三、核心代码结构与解析
抽象层:定义通用接口(面向抽象编程)
(1)相机基类(Camera.h)
定义所有相机必须实现的核心接口,是框架 “通用性” 的基础,上层通过基类指针 / 引用调用,无需关注具体相机类型:
#ifndefCAMERA_H#defineCAMERA_H#include"CameraParam.h"#include<QObject>//相机基类classCamera:publicQObject{Q_OBJECTpublic:explicitCamera(QObject*parent=nullptr):QObject(parent){}virtual~Camera(){}// ch:打开设备 | en:Open DevicevirtualintOpen(void*param=nullptr)=0;// ch:关闭设备 | en:Close DevicevirtualintClose()=0;// ch:开启抓图 | en:Start GrabbingvirtualintStartGrabbing(){returntrue;};// ch:停止抓图 | en:Stop GrabbingvirtualintStopGrabbing(){returntrue;};// 获取设备连接状态virtualboolisConnected()const=0;// 获取设备 IDvirtualQStringdeviceId()const=0;// 设置设备 IDvirtualvoidsetDeviceId(constQString&deviceId)=0;signals:// 数据接收信号voiddataReceived(constQString&deviceId,constCameraData&data);// 连接状态变化信号voidconnectionStateChanged(constQString&deviceId,boolisConnected);// 错误信号voiderrorOccurred(constQString&deviceId,constQString&errorString);};#endif// CAMERA_H(2)参数 / 数据结构定义(CameraParam.h)
封装相机类型、连接参数、回调数据,通过 “基类 + 子类” 适配不同相机的差异化数据:
#ifndefCAMERAPARAM_H#defineCAMERAPARAM_H#include<QString>#include<QDebug>#include"Include/MvCodeReaderParams.h"#defineLOGDEBUGqDebug()<<__FILE__<<__LINE__//相机类型enumCameraType{MVCodeCamera=0,//海康读码相机};//相机连接参数--基类classCameraParam{public:void*ptr=nullptr;};//相机回调数据传输--基类classCameraData{public:virtual~CameraData()=default;void*ptr=nullptr;};//海康读码相机回调数据--派生类classMVCodeCameraData:publicCameraData{public:unsignedchar*pData;//图像信息MV_CODEREADER_IMAGE_OUT_INFO_EX2*pstFrameInfo;//条码信息};// 设备信息结构体structDeviceInfo{QString deviceId;// 设备唯一 IDCameraType cameraType;// 通信类型CameraParam cameraParam;// 相机连接参数};#endif// CAMERAPARAM_H- 工厂层:创建相机对象(工厂模式)
CameraFactory.h + CameraFactory.cpp
封装相机实例化逻辑,上层无需直接 new 具体相机类,降低耦合:
#ifndefCAMERAFACTORY_H#defineCAMERAFACTORY_H#include"camera.h"#include"CameraParam.h"//相机工厂classCameraFactory{public:// 创建相机staticCamera*createCamera(CameraType type);};#endif// CAMERAFACTORY_H#include"camerafactory.h"#include"cmvcamera.h"Camera*CameraFactory::createCamera(CameraType type){switch(type){caseCameraType::MVCodeCamera:returnnewCMvCamera();default:returnnullptr;}}- 实现层:海康读码相机具体实现(CMvCamera)
CMvCamera.h + CMvCamera.cpp
继承 Camera 基类,实现海康读码相机的具体逻辑,对接海康 SDK 接口:
核心功能解析:
重写 Open/Close:调用海康 SDK 的创建句柄、打开设备、销毁句柄等接口;
注册回调函数:将海康 SDK 的 C 风格回调转换为 Qt 信号,实现数据转发;
实现 StartGrabbing/StopGrabbing:调用 SDK 的抓图启停接口;
线程安全:所有 SDK 调用基于相机独立线程,避免多线程冲突。
关键代码片段(CMvCamera.cpp):
#include"cmvcamera.h"CMvCamera::CMvCamera(){m_hDevHandle=MV_NULL;// 初始化句柄}CMvCamera::~CMvCamera(){if(m_hDevHandle){MV_CODEREADER_DestroyHandle(m_hDevHandle);// 释放句柄m_hDevHandle=MV_NULL;}}// 打开设备:对接海康SDKintCMvCamera::Open(void*param){INconstMV_CODEREADER_DEVICE_INFO*pstDevInfo=(INconstMV_CODEREADER_DEVICE_INFO*)param;if(MV_NULL==pstDevInfo)returnMV_CODEREADER_E_PARAMETER;if(m_hDevHandle)returnMV_CODEREADER_E_CALLORDER;// 创建句柄intnRet=MV_CODEREADER_CreateHandle(&m_hDevHandle,pstDevInfo);if(MV_CODEREADER_OK!=nRet){emiterrorOccurred(m_deviceId,QString("创建句柄失败.错误码:%1").arg(nRet));returnnRet;}// 打开设备nRet=MV_CODEREADER_OpenDevice(m_hDevHandle);if(MV_CODEREADER_OK!=nRet){MV_CODEREADER_DestroyHandle(m_hDevHandle);m_hDevHandle=MV_NULL;emiterrorOccurred(m_deviceId,QString("打开设备失败.错误码:%1").arg(nRet));returnnRet;}// 注册图像回调:将SDK回调转发为Qt信号nRet=RegisterImageCallBack(0,ImageCallback,this);if(MV_CODEREADER_OK!=nRet){emiterrorOccurred(m_deviceId,QString("注册回调失败.错误码:%1").arg(nRet));}emitconnectionStateChanged(m_deviceId,true);// 发送连接成功信号returnnRet;}// 图像回调函数(SDK回调):转换为Qt信号voidCMvCamera::ImageCallback(unsignedchar*pData,MV_CODEREADER_IMAGE_OUT_INFO_EX2*pstFrameInfo,void*pUser){CMvCamera*instance=static_cast<CMvCamera*>(pUser);MVCodeCameraData CodeData;CodeData.pData=pData;CodeData.pstFrameInfo=pstFrameInfo;emit instance->dataReceived(instance->m_deviceId,CodeData);// 发送数据信号}// 其他核心接口实现(Close/StartGrabbing/StopGrabbing等)...- 管理层:相机管理器(Cameramanager)
Cameramanager.h + Cameramanager.cpp
统一管理多相机的生命周期、线程、连接状态,是上层 UI 与底层相机的中间层:
核心功能:
新增 / 移除设备:创建相机对象并分配独立线程;
连接 / 断开设备:调用相机 Open/Close 接口;
启停抓图:调用相机 StartGrabbing/StopGrabbing 接口;
信号转发:将相机的信号统一转发给 UI 层,简化 UI 层对接。
关键代码片段(Cameramanager.cpp):
#include"cameramanager.h"#include"camerafactory.h"Cameramanager::Cameramanager(QObject*parent):QObject(parent){}Cameramanager::~Cameramanager(){// 析构时清理所有设备QStringList deviceIds=m_devices.keys();for(constQString&deviceId:deviceIds){removeDevice(deviceId);}}// 添加设备:创建相机+独立线程boolCameramanager::addDevice(constDeviceInfo&deviceInfo){if(m_devices.contains(deviceInfo.deviceId))returnfalse;// 工厂模式创建相机Camera*camera=CameraFactory::createCamera(deviceInfo.cameraType);if(!camera)returnfalse;camera->setDeviceId(deviceInfo.deviceId);// 为相机创建独立线程QThread*thread=newQThread();camera->moveToThread(thread);// 连接信号:转发相机的所有信号connect(camera,&Camera::dataReceived,this,&Cameramanager::onDataReceived);connect(camera,&Camera::connectionStateChanged,this,&Cameramanager::onConnectionStateChanged);connect(camera,&Camera::errorOccurred,this,&Cameramanager::onErrorOccurred);thread->start();// 启动线程// 保存设备信息Device device;device.camera=camera;device.thread=thread;device.deviceInfo=deviceInfo;m_devices.insert(deviceInfo.deviceId,device);returntrue;}// 连接设备:调用相机Open接口boolCameramanager::connectDevice(constQString&deviceId){if(!m_devices.contains(deviceId))returnfalse;Device device=m_devices.value(deviceId);returndevice.camera->Open(&device.deviceInfo.cameraParam);}// 转发数据信号给UI层voidCameramanager::onDataReceived(constQString&deviceId,constCameraData&data){emitdataReceived(deviceId,data);}// 其他管理接口(removeDevice/StartGrabbing/StopGrabbing等)...- 应用层:UI 界面(Widget)
Widget.h + Widget.cpp
上层业务逻辑实现,包括:
枚举相机设备并注册到管理器;
按钮控制(初始化 / 连接 / 启停抓图);
接收相机数据并解析(图像显示、条码提取、图片保存)。
关键代码片段(Widget.cpp):
#include"widget.h"#include"ui_widget.h"#include<QDir>Widget::Widget(QWidget*parent):QWidget(parent),ui(newUi::Widget){ui->setupUi(this);// 初始化图片保存路径m_savePath=QCoreApplication::applicationDirPath()+"/images";QDirdir(m_savePath);if(!dir.exists())dir.mkpath(".");// 绑定按钮事件connect(ui->pushButton,&QPushButton::clicked,this,&Widget::pb_Init);// 接收管理器的相机数据connect(&manager,&Cameramanager::dataReceived,this,&Widget::slot_recv_camereData);}// 初始化:枚举相机并注册到管理器voidWidget::pb_Init(){MV_CODEREADER_DEVICE_INFO_LIST m_stDevList;memset(&m_stDevList,0,sizeof(MV_CODEREADER_DEVICE_INFO_LIST));// 枚举海康读码相机intnRet=CMvCamera::EnumDevices(&m_stDevList,MV_CODEREADER_GIGE_DEVICE);if(MV_CODEREADER_OK!=nRet)return;intdeviceNum=m_stDevList.nDeviceNum;if(deviceNum>CAMERA_NUM)deviceNum=CAMERA_NUM;// 限制最大相机数for(inti=0;i<deviceNum;i++){MV_CODEREADER_DEVICE_INFO*pDeviceInfo=m_stDevList.pDeviceInfo[i];QString strSerialNumber="";// 提取设备序列号(唯一ID)if(pDeviceInfo->nTLayerType==MV_CODEREADER_GIGE_DEVICE){strSerialNumber=(char*)pDeviceInfo->SpecialInfo.stGigEInfo.chSerialNumber;}elseif(pDeviceInfo->nTLayerType==MV_CODEREADER_USB_DEVICE){strSerialNumber=(char*)pDeviceInfo->SpecialInfo.stUsb3VInfo.chSerialNumber;}// 注册设备到管理器DeviceInfo HKCameraDevice;HKCameraDevice.deviceId=strSerialNumber;HKCameraDevice.cameraType=MVCodeCamera;HKCameraDevice.cameraParam.ptr=pDeviceInfo;// 传递海康设备信息manager.addDevice(HKCameraDevice);// 显示设备ID到UIif(i==0)ui->c1->setText(strSerialNumber);elseif(i==1)ui->c2->setText(strSerialNumber);elseif(i==2)ui->c3->setText(strSerialNumber);elseif(i==3)ui->c4->setText(strSerialNumber);}}// 接收相机数据:解析图像+条码+保存图片voidWidget::slot_recv_camereData(constQString&deviceId,constCameraData&data){try{// 转换为海康读码相机数据constMVCodeCameraData&subData=dynamic_cast<constMVCodeCameraData&>(data);if(!subData.pData||!subData.pstFrameInfo)return;// 解析条码信息MV_CODEREADER_RESULT_BCR_EX2*stBcrResult=(MV_CODEREADER_RESULT_BCR_EX2*)subData.pstFrameInfo->UnparsedBcrList.pstCodeListEx2;if(stBcrResult){for(unsignedinti=0;i<stBcrResult->nCodeNum;i++){qDebug()<<"条码"<<i<<":"<<QString::fromLocal8Bit(stBcrResult->stBcrInfoEx2[i].chCode);}}// 解析图像数据(支持JPEG/Mono8)QImage image;if(subData.pstFrameInfo->enPixelType==PixelType_CodeReader_Gvsp_Jpeg){image.loadFromData(subData.pData,subData.pstFrameInfo->nFrameLen,"JPG");}elseif(subData.pstFrameInfo->enPixelType==PixelType_CodeReader_Gvsp_Mono8){QByteArraybufferCopy((constchar*)subData.pData,subData.pstFrameInfo->nWidth*subData.pstFrameInfo->nHeight);image=QImage((constuchar*)bufferCopy.constData(),subData.pstFrameInfo->nWidth,subData.pstFrameInfo->nHeight,QImage::Format_Grayscale8).copy();}// 显示图像到UIQPixmap showPixmap=QPixmap::fromImage(image).scaled(ui->label->size(),Qt::IgnoreAspectRatio,Qt::SmoothTransformation);if(deviceId==ui->c1->text())ui->label->setPixmap(showPixmap);// ... 其他相机图像显示逻辑// 保存图片到本地if(!image.isNull()){QString timeStr=QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss_zzz");QString fileName=QString("camera_%1_%2.png").arg(deviceId).arg(timeStr);image.save(m_savePath+"/"+fileName,"PNG");}}catch(conststd::bad_cast&e){qDebug()<<"类型转换失败:"<<e.what();}}// 批量启动/停止抓图、连接/断开设备等按钮逻辑...四、框架使用步骤
- 环境准备
安装 Qt(5.x/6.x 均可),配置 Qt 环境;
集成海康读码相机 SDK(MvCodeReaderCtrl),将 SDK 的头文件、库文件加入项目;
项目中添加上述所有代码文件,配置库链接(如海康的 MvCodeReaderCtrl.lib)。 - 快速使用
(1)UI 布局
在 Qt Designer 中添加:
4 个标签(label/label_2/label_3/label_4):显示相机图像;
4 个文本框(c1/c2/c3/c4):显示相机设备 ID;
按钮:初始化、连接所有相机、启动抓图、停止抓图、断开所有相机。
(2)核心调用流程
// 1. 初始化:枚举相机并注册到管理器widget->pb_Init();// 2. 连接所有相机widget->on_pushButton_4_clicked();// 3. 启动所有相机抓图widget->on_pushButton_2_clicked();// 4. 停止抓图widget->on_pushButton_3_clicked();// 5. 断开所有相机widget->on_pushButton_5_clicked();- 扩展新相机类型
如果需要适配其他品牌 / 类型相机,只需:
在CameraType枚举中新增相机类型(如OtherCamera);
继承Camera基类,实现新相机的Open/Close/StartGrabbing等接口;
在CameraFactory中新增 case 分支,返回新相机实例;
(可选)继承CameraData基类,封装新相机的回调数据。
五、设计思路与为什么这么设计?
- 为什么用工厂模式?
问题:直接 new 具体相机类会导致上层代码与底层相机强耦合,新增相机需修改上层代码;
解决:工厂模式将 “对象创建” 与 “业务逻辑” 分离,上层只需通过CameraFactory::createCamera获取相机实例,新增相机仅需扩展工厂类,符合 “开闭原则”。 - 为什么每个相机独立线程?
问题:相机回调函数运行在 SDK 线程,如果直接在回调中处理 UI / 耗时操作,会导致 UI 阻塞、回调丢失;
解决:每个相机对象移到独立线程,所有 SDK 调用、回调处理都在子线程,UI 线程仅负责显示和用户交互,保证界面流畅。 - 为什么用抽象基类?
问题:不同相机的接口、数据结构差异大,上层代码需适配不同相机的调用方式;
解决:定义通用抽象接口,上层通过基类指针调用,无需关注具体相机类型,实现 “多态调用”,降低上层代码复杂度。 - 为什么用信号槽转发数据?
问题:子线程的相机数据无法直接操作 UI(Qt 线程安全限制);
解决:通过 Qt 信号槽的跨线程机制,将相机数据从子线程转发到 UI 线程,无需手动加锁,简化线程同步。
六、注意事项
内存管理:析构管理器时需停止所有相机、销毁线程,避免内存泄漏;
异常处理:相机回调中需捕获类型转换、空指针等异常,避免程序崩溃;
SDK 兼容性:不同版本海康 SDK 的接口可能略有差异,需根据 SDK 文档调整;
线程安全:相机参数配置需在相机线程中执行,避免跨线程直接调用 SDK 接口;
图片保存:JPEG 格式需使用nFrameLen作为数据长度(压缩数据长度≠宽 × 高)。
七、总结
这套框架通过设计模式 + Qt 特性,实现了多相机的统一管理,既保证了代码的可扩展性(新增相机类型低成本),又保证了运行的稳定性(线程隔离、异添加链接描述常处理)。核心思想是 “面向抽象编程”+“解耦”,适合需要对接多类型相机、追求代码可维护性的工业视觉项目。