QT界面设计
设计一个基于QT的光电设备控制管理界面,按照配置、协议指令和视频流三大模块进行组织。
一、整体布局设计
text
yaml
┌─────────────────────────────────────────────────────────────────────────┐
│ ■ 光电设备控制系统 v1.0 --- □ × │
├─────────────────────────────────────────────────────────────────────────┤
│ 设备连接状态: ● 已连接 | 设备型号: ZL-Z50-FS2375 | 硬件版本: ATC │
├─────────────────────────────────────────────────────────────────────────┤
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ Tab1: 设备配置 │ Tab2: 协议指令 │ Tab3: 视频监控 │ │
│ ├───────────────────────────────────────────────────────────────────┤ │
│ │ │ │
│ │ (内容区域) │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────────────────┤
│ 日志: [2026-08-18 14:32:15] 连接成功 | 状态栏信息 │
└─────────────────────────────────────────────────────────────────────────┘
二、Tab1:设备配置模块
2.1 界面布局
text
less
┌─────────────────────────────────────────────────────────────────────────┐
│ ■ 设备配置 [连接] [断开] │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─ 通信设置 ────────────────────────────────────────────────────────┐ │
│ │ 协议类型: [TCP ▼] (TCP/UDP/HTTP) │ │
│ │ IP地址: [192.168.1.64 ] │ │
│ │ 端口: [39020 ] (TCP:39020/UDP:39030/HTTP:80) │ │
│ │ 用户名: [admin ] │ │
│ │ 密码: [●●●●●●●●● ] [显示] │ │
│ │ 超时时间: [3000] ms │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ UDP服务状态 ─────────────────────────────────────────────────────┐ │
│ │ UDP服务: [● 已开启] (需在设备Web端手动启用) │ │
│ │ 提示: UDP模式无需登录认证,但需设备端开启UDP开关 │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ 设备信息 ────────────────────────────────────────────────────────┐ │
│ │ 设备型号: [ZL-Z50-FS2375 ] [获取信息] │ │
│ │ 硬件版本: [ATC59 ] │ │
│ │ 软件版本: [V2.3.1 ] │ │
│ │ 设备IP: [192.168.1.64 ] AI服务IP: [192.168.1.249 ] │ │
│ │ 支持功能: ☑ 可见光 ☑ 热像 ☑ 激光测距 ☑ 自动跟踪 │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ 配置管理 ────────────────────────────────────────────────────────┐ │
│ │ [保存配置] [加载配置] [导出配置] [恢复默认] │ │
│ │ 配置文件: [config_20260818.json] │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
2.2 核心逻辑说明
| 功能 | 操作 | 协议指令 |
|---|---|---|
| TCP连接 | 点击"连接" | TCP握手 → userSaltGet → userLogin → 启动心跳定时器(15s) |
| UDP连接 | 选择UDP协议后连接 | 直接发送UDP数据包,无需登录 |
| HTTP连接 | 选择HTTP协议 | POST请求 /cgi-bin/proc.cgi |
| 获取设备信息 | 点击"获取信息" | 实际需通过 ptzControl 状态回传获取 |
| 心跳保活 | 自动后台运行 | 每15秒发送 userOnlineHeart |
| 配置持久化 | 保存/加载 | QSettings 或 JSON 文件存储 |
2.3 关键数据结构 (C++)
cpp
ini
struct DeviceConfig {
// 通信参数
enum Protocol { TCP, UDP, HTTP } protocol;
QString ipAddress;
quint16 port;
QString username;
QString password;
int timeoutMs;
// 设备信息
QString deviceModel;
QString hardwareVersion;
QString softwareVersion;
QString aiServerIp;
bool hasVisibleCam;
bool hasThermalCam;
bool hasLaserRanging;
bool hasAutoTrack;
};
三、Tab2:协议指令模块
3.1 整体布局(按功能分组)
text
less
┌─────────────────────────────────────────────────────────────────────────┐
│ ■ 协议指令控制 │
├─────────────────────────────────────────────────────────────────────────┤
│ ┌─ 云台控制 ──────────────────── ┌─ 角度/视场定位 ────────────────┐ │
│ │ [↑] [↓] [←] [→] [■停止] │ 水平角: [ 45.00 ]° │ │
│ │ 速度: [████████░░] 80% │ 俯仰角: [ -15.00 ]° │ │
│ │ │ 视场角: [ 6.00 ]° │ │
│ │ [预置位1] [预置位2] [预置位3] │ [角度定位] [视场定位] [引导] │ │
│ │ 预置位管理: [设置] [调用] [删除]│ 引导频率: ≤1Hz 提示 │ │
│ └─────────────────────────────────┴─────────────────────────────────┘ │
│ │
│ ┌─ 镜头控制 ──────────────────── ┌─ 跟踪控制 ──────────────────────┐ │
│ │ 变倍: [─] [████████░░] [+] │ 跟踪使能: [● 开启] │ │
│ │ 聚焦: [─] [██████░░░░] [+] │ 跟踪模式: [识别自适应 ▼] │ │
│ │ [自动聚焦] [视场定位到] [2.0°] │ 自动跟踪: [● 开启] │ │
│ │ 通道切换: [可见光●] [热像○] │ [手动框选] [停止跟踪] │ │
│ │ 镜头电源: [可见光●] [热像●] │ 跟踪状态: ● 锁定 | 目标: 无人机 │
│ └─────────────────────────────────┴─────────────────────────────────┘ │
│ │
│ ┌─ 激光测距 ──────────────────── ┌─ 校北/标校 ─────────────────────┐ │
│ │ [单次测距] [多次测距(5s)] │ 水平标校: [ 0.00 ]° │ │
│ │ 测距结果: 1245.6 m │ 俯仰标校: [ 0.00 ]° │ │
│ │ 状态: 成功 | 主波: 有 │ [执行标校] [重置零点] │ │
│ └─────────────────────────────────┴─────────────────────────────────┘ │
│ │
│ ┌─ 指令控制台 ──────────────────────────────────────────────────────┐ │
│ │ [原始指令:] [{"cmd":"ptzControl","param":{...}}] [发送] [清空] │ │
│ │ 响应: {"cmd":"ptzControl","param":{"ackvalue":100}} │ │
│ │ 快捷指令: [角度定位] [视场定位] [跟踪使能] [抓图] [▼更多] │ │
│ └───────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
3.2 协议指令分组设计
| 分组 | 包含指令 | 对应ActionID/Cmd |
|---|---|---|
| 云台运动 | 上/下/左/右/停止 | actionid: 0,1,2,3,8 |
| 变倍聚焦 | 变倍大/小/停止, 聚焦近/远/停止 | actionid: 9,10,29,11,12,30 |
| 预置位 | 设置/调用/删除 | actionid: 13,14,15 |
| 角度定位 | 水平/俯仰角度定位 | actionid: 51 |
| 视场定位 | 视场角定位 | actionid: 42 |
| 跟踪控制 | 使能/模式/自动/手动/停止 | ivpSet/ivpTrackingCtrl |
| 镜头电源 | 可见光/热像开关 | ptzSetPowerCamEnable/ptzSetPowerIrEnable |
| 激光测距 | 单次/多次/停止 | laserRanging |
| 校北标校 | 水平/俯仰标校 | ptzAngleHorCalib/ptzAngleVerCalib |
3.3 核心交互控件设计
3.3.1 云台方向控制(自定义控件)
cpp
csharp
// 十字方向控制 + 停止按钮
class PTZDirectionControl : public QWidget {
Q_OBJECT
signals:
void directionPressed(int actionId); // 0上,1下,2左,3右
void directionReleased();
void stopClicked();
public:
void setSpeed(int speed); // 0-63
};
3.3.2 角度定位控件(支持雷达引导联动)
cpp
arduino
class AnglePositionWidget : public QWidget {
Q_OBJECT
// 输入:水平角 0-360°, 俯仰角 -90-+90°, 视场角 0.58-33.4°
// 输出:ptzControl actionid=51 指令
public slots:
void onPosition(double pan, double tilt, double view);
void onRadarGuide(double distance, double azimuth, double elevation);
};
3.3.3 跟踪控制控件
cpp
kotlin
class TrackingControlWidget : public QWidget {
Q_OBJECT
// 跟踪使能开关 (ivpSet type=4)
// 跟踪模式选择 (对空/识别自适应/对地)
// 自动跟踪开关 (bAlarmTracking)
// 手动框选 (在视频上绘制矩形)
// 停止跟踪 (ivpTrackingCtrl bTracking=false)
};
3.4 快捷功能设计
| 快捷按钮 | 功能 | 组合指令 |
|---|---|---|
| 雷达引导 | 模拟雷达目标输入 | 自动执行:角度定位 + 视场定位 + 开启自动跟踪 |
| 一键归位 | 回到初始位置 | 水平0° + 俯仰0° + 视场最大 |
| 无人机跟踪 | 快速切换无人机模式 | 模式=识别自适应 + 自动跟踪开 |
| 人车跟踪 | 快速切换人车模式 | 模式=对地 + 自动跟踪开 |
| 抓图 | 抓拍当前画面 | vencSnap (建议返回URL) |
四、Tab3:视频监控模块
4.1 界面布局
text
ini
┌─────────────────────────────────────────────────────────────────────────┐
│ ■ 视频监控 │
├─────────────────────────────────────────────────────────────────────────┤
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ 视频显示区域 (QOpenGLWidget) │ │
│ │ │ │
│ │ ┌──────────────────────┐ │ │
│ │ │ 跟踪目标鹰眼显示 │ │ │
│ │ │ [目标框 2x放大] │ │ │
│ │ └──────────────────────┘ │ │
│ │ │ │
│ │ [跟踪框] ← 目标位置 │ │
│ │ │ │
│ │ OSD信息: 雷达目标 方位:45.0° 距离:1245m 无人机 │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ 播放控制 ─────────────────────────────────────────────────────────┐ │
│ │ [▶ 播放] [■ 停止] [⏸ 暂停] [📷 截图] [⏺ 录像] │ │
│ │ 通道: [可见光 ●] [热像 ○] │ │
│ │ 分辨率: [1920x1080 ▼] 帧率: [25fps] 码率: [4Mbps] │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ RTSP设置 ─────────────────────────────────────────────────────────┐ │
│ │ RTSP URL: [rtsp://admin:****@192.168.1.64:554/ch0/stream0] │ │
│ │ [生成URL] [自动连接] 延迟: 156ms │ │
│ │ FFmpeg参数: [-rtsp_transport tcp -analyzeduration 1000000] │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ 叠加信息控制 ────────────────────────────────────────────────────┐ │
│ │ ☑ 显示跟踪框 ☑ 显示目标类型 ☑ 显示距离 ☑ 显示十字准心 │ │
│ │ ☑ 雷达信息OSD (刷新率 ≤1Hz) 自定义OSD: [无人机 45.0° 1245m] │ │
│ └───────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
4.2 FFmpeg拉流实现方案
4.2.1 RTSP URL生成
cpp
perl
QString generateRtspUrl(const DeviceConfig& cfg, int channel, int stream) {
// 可见光: rtsp://admin:Abc.12345@192.168.1.64:554/ch0/stream0
// 热像: rtsp://admin:Abc.12345@192.168.1.64:554/ch1/stream0
QString url = QString("rtsp://%1:%2@%3:554/ch%4/stream%5")
.arg(cfg.username)
.arg(cfg.password)
.arg(cfg.ipAddress)
.arg(channel) // 0=可见光, 1=热像
.arg(stream); // 0=主码流
return url;
}
注意 :密码需要通过
userSaltGet获取salt后加密,与登录密码一致。
4.2.2 FFmpeg解码 + OpenGL渲染 (核心代码框架)
cpp
ini
class VideoPlayer : public QOpenGLWidget {
Q_OBJECT
private:
AVFormatContext* pFormatCtx = nullptr;
AVCodecContext* pCodecCtx = nullptr;
AVFrame* pFrame = nullptr;
AVPacket packet;
std::thread decodeThread;
QMutex mutex;
QImage currentFrame;
public:
bool startStream(const QString& url) {
// 1. 初始化FFmpeg
avformat_network_init();
// 2. 打开RTSP流
AVDictionary* opts = nullptr;
av_dict_set(&opts, "rtsp_transport", "tcp", 0); // TCP传输
av_dict_set(&opts, "analyzeduration", "1000000", 0);
av_dict_set(&opts, "max_delay", "500000", 0);
av_dict_set(&opts, "reorder_queue_size", "0", 0);
if (avformat_open_input(&pFormatCtx, url.toStdString().c_str(),
nullptr, &opts) < 0) {
return false;
}
// 3. 查找视频流
int videoStreamIndex = -1;
for (int i = 0; i < pFormatCtx->nb_streams; i++) {
if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
videoStreamIndex = i;
break;
}
}
// 4. 初始化解码器
AVCodec* pCodec = avcodec_find_decoder(
pFormatCtx->streams[videoStreamIndex]->codecpar->codec_id);
pCodecCtx = avcodec_alloc_context3(pCodec);
avcodec_parameters_to_context(pCodecCtx,
pFormatCtx->streams[videoStreamIndex]->codecpar);
avcodec_open2(pCodecCtx, pCodec, nullptr);
// 5. 启动解码线程
decodeThread = std::thread(&VideoPlayer::decodeLoop, this);
return true;
}
void decodeLoop() {
while (isRunning) {
if (av_read_frame(pFormatCtx, &packet) >= 0) {
if (packet.stream_index == videoStreamIndex) {
avcodec_send_packet(pCodecCtx, &packet);
while (avcodec_receive_frame(pCodecCtx, pFrame) == 0) {
// 转换为RGB并更新显示
QImage frame = convertToQImage(pFrame);
QMetaObject::invokeMethod(this, "updateFrame",
Q_ARG(QImage, frame));
}
}
av_packet_unref(&packet);
}
}
}
protected:
void paintGL() override {
QMutexLocker locker(&mutex);
if (!currentFrame.isNull()) {
// OpenGL渲染当前帧
renderTexture(currentFrame);
}
}
};
4.3 视频叠加OSD设计
cpp
scss
class VideoOverlay {
public:
struct OverlayInfo {
bool showTrackBox;
bool showTargetType;
bool showDistance;
bool showCrosshair;
bool showRadarInfo;
QString radarInfo; // 频率 ≤ 1Hz
QRect trackRect; // 跟踪框位置 (640x512坐标系需缩放)
int targetType; // 1:车, 2:人, 4:无人机
float distance;
float offsetH; // 脱靶量
float offsetV;
};
void paintOverlay(QPainter* painter, const QSize& viewSize,
const OverlayInfo& info) {
// 1. 绘制跟踪框 (需要坐标转换 640x512 → viewSize)
QRect rect = scaleRect(info.trackRect, viewSize);
painter->setPen(QPen(Qt::green, 2));
painter->drawRect(rect);
// 2. 绘制目标类型标签
QString targetName = getTargetName(info.targetType);
painter->drawText(rect.topLeft() + QPoint(0, -10), targetName);
// 3. 绘制十字准心
if (info.showCrosshair) {
QPoint center(viewSize.width()/2, viewSize.height()/2);
painter->drawLine(center.x() - 20, center.y(),
center.x() + 20, center.y());
painter->drawLine(center.x(), center.y() - 20,
center.x(), center.y() + 20);
}
// 4. 绘制雷达OSD信息 (频率≤1Hz)
if (info.showRadarInfo) {
painter->drawText(QPoint(10, 30), info.radarInfo);
}
}
};
五、完整UI设计架构(QT类结构)
text
scss
├── MainWindow
│ ├── QTabWidget
│ │ ├── ConfigTab (设备配置)
│ │ │ ├── ConnectionWidget (IP/端口/账号/密码)
│ │ │ ├── DeviceInfoWidget (型号/版本/能力)
│ │ │ └── ConfigManageWidget (保存/加载/导出)
│ │ ├── CommandTab (协议指令)
│ │ │ ├── PTZControlWidget (方向控制 + 预置位)
│ │ │ ├── LensControlWidget (变倍/聚焦/视场)
│ │ │ ├── AnglePositionWidget (角度/视场定位)
│ │ │ ├── TrackingControlWidget (跟踪使能/模式/触发)
│ │ │ ├── LaserRangingWidget (激光测距)
│ │ │ ├── CalibrationWidget (校北标校)
│ │ │ └── CommandConsoleWidget (原始指令/响应)
│ │ └── VideoTab (视频监控)
│ │ ├── VideoDisplayWidget (QOpenGLWidget + FFmpeg)
│ │ ├── PlaybackControlWidget (播放/停止/截图/录像)
│ │ ├── RtspConfigWidget (URL生成/参数设置)
│ │ └── OverlayControlWidget (OSD叠加控制)
│ ├── StatusBar (连接状态/日志/错误信息)
│ └── CommunicationManager (TCP/UDP/HTTP通信封装)
│ ├── login() / logout() / heartbeat()
│ ├── sendCommand()
│ ├── onResponse() / onReport()
│ └── fragmentReassembler (分包重组)
└── Utils
├── AngleConverter (俯仰角映射转换)
├── CoordinateConverter (640x512 ↔ 显示分辨率)
└── ConfigSerializer (JSON配置序列化)
六、关键技术要点
6.1 通信层封装
cpp
arduino
class CommunicationManager : public QObject {
Q_OBJECT
public:
enum ProtocolType { TCP, UDP, HTTP };
bool connectDevice(const DeviceConfig& config);
void disconnectDevice();
bool sendCommand(const QJsonObject& cmd,
std::function<void(QJsonObject)> callback = nullptr);
signals:
void connected();
void disconnected();
void responseReceived(QJsonObject response);
void reportReceived(QJsonObject report); // 主动回传
void errorOccurred(QString error);
private:
QTcpSocket* tcpSocket;
QUdpSocket* udpSocket;
QTimer* heartbeatTimer;
QString token;
FragmentReassembler fragmentReassembler;
};
6.2 分包重组处理(放在通信层透明处理)
cpp
ini
class FragmentReassembler {
private:
struct FragmentBuffer {
QByteArray data;
int totalPackets;
QMap<int, QByteArray> fragments; // seq → data
};
QMap<quint32, FragmentBuffer> buffers; // msgId → buffer
public:
QByteArray processPacket(quint32 msgId, quint8 fragFlag,
const QByteArray& payload) {
bool isFragmented = (fragFlag & 0x80) != 0;
bool isEndPacket = (fragFlag & 0x40) != 0;
if (!isFragmented) {
return payload; // 单包,直接返回
}
// 解析分包头
quint32 seq = qFromLittleEndian<quint32>(payload.mid(0, 4));
quint32 total = qFromLittleEndian<quint32>(payload.mid(4, 4));
QByteArray data = payload.mid(8);
// 存储分包
buffers[msgId].fragments[seq] = data;
buffers[msgId].totalPackets = total;
// 收齐后合并
if (isEndPacket && buffers[msgId].fragments.size() == total) {
QByteArray complete;
for (int i = 0; i < total; i++) {
complete += buffers[msgId].fragments[i];
}
buffers.remove(msgId);
return complete;
}
return QByteArray(); // 未收齐,返回空
}
};
七、完整UI示例(设计效果)
text
less
╔═══════════════════════════════════════════════════════════════════════════╗
║ █ 光电设备控制系统 v1.0 --- □ × ║
╠═══════════════════════════════════════════════════════════════════════════╣
║ 状态: ● 已连接 设备: ZL-Z50-FS2375 版本: ATC59 ║
╠═══════════════════════════════════════════════════════════════════════════╣
║ ┌──────┐ ┌──────┐ ┌──────────────────────────────────────────────────┐ ║
║ │ 配置 │ │ 指令 │ │ 视频监控 │ ║
║ ├──────┤ ├──────┤ │ ┌────────────────────────────────────────────┐ │ ║
║ │ │ │ │ │ │ │ │ ║
║ │ IP │ │ [↑] │ │ │ 视频画面区域 │ │ ║
║ │ 端口 │ │[←][■][→]│ │ [跟踪框] [鹰眼] │ │ ║
║ │ 账号 │ │ [↓] │ │ │ │ │ ║
║ │ 密码 │ │ │ │ │ OSD: 无人机 方位45° 距离1245m │ │ ║
║ │ │ │ 预置位│ │ └────────────────────────────────────────────┘ │ ║
║ │[连接] │ │ 角度 │ │ [▶] [■] [⏸] [📷] [⏺] 通道: [可见光●] │ ║
║ │ │ │ 视场 │ │ RTSP: rtsp://admin:***@192.168.1.64:554/... │ ║
║ │ │ │ 跟踪 │ │ 延迟: 156ms 帧率: 25fps │ ║
║ │ │ │ 测距 │ │ ☑跟踪框 ☑类型 ☑距离 ☑十字 ☑雷达OSD │ ║
║ │ │ │ 校北 │ └──────────────────────────────────────────────────┘ ║
║ │ │ │ 控制台│ ║
║ │ │ │ │ ║
║ └──────┘ └──────┘ ║
╠═══════════════════════════════════════════════════════════════════════════╣
║ 日志: [14:32:15] 连接成功 [14:32:20] 角度定位完成 ║
╚═══════════════════════════════════════════════════════════════════════════╝
总结
| 模块 | 核心功能 | 关键技术 |
|---|---|---|
| 配置模块 | IP/端口/账号/密码管理,连接/断开 | TCP/UDP/HTTP封装,登录认证,心跳保活 |
| 协议指令 | 云台控制、角度定位、跟踪控制、镜头控制 | ptzControl + ivpSet + ivpTrackingCtrl |
| 视频监控 | RTSP拉流、解码、显示、OSD叠加 | FFmpeg + OpenGL/QPainter |
这个设计涵盖了您提到的三大模块,同时兼顾了协议指令的分组管理和底层通信的透明封装(包括登录认证、分包重组等)。您可以根据实际需求调整布局和功能细节。
模块化设计
先独立开发,然后移植到上位机。
完全可行,而且这是一个非常明智的工程决策。 将软件先独立开发,后续再移植到上位机(其他平台或集成环境),这种策略在实际工程中非常常见,是模块化和渐进式开发的最佳实践。
一、为什么这个策略可行?
1.1 技术层面验证
| 维度 | 分析 | 结论 |
|---|---|---|
| QT跨平台性 | QT原生支持 Windows/Linux/macOS/嵌入式Linux(ARM) | ✅ 无缝移植 |
| 依赖库独立性 | FFmpeg、OpenGL、网络库均跨平台 | ✅ 各平台均有对应版本 |
| 协议层无平台依赖 | TCP/UDP/HTTP + JSON,纯网络通信 | ✅ 与操作系统无关 |
| UI代码可复用 | QML/QWidgets 在不同平台表现一致 | ✅ 仅需调整布局适配屏幕 |
1.2 工程层面验证
text
markdown
独立开发阶段 移植阶段
┌─────────────────────┐ ┌─────────────────────┐
│ Windows/Linux桌面 │ ──────▶ │ 上位机集成环境 │
│ + 模拟设备环境 │ │ + 真实光电设备 │
│ + 快速迭代调试 │ │ + 完整功能验证 │
│ + 单元测试覆盖 │ │ + 性能优化 │
└─────────────────────┘ └─────────────────────┘
│ │
▼ ▼
独立可执行程序 动态库/插件/子进程
二、移植策略建议
2.1 架构分层设计(核心关键)
cpp
arduino
// ============================================================
// 第一层:核心业务层 (纯C++,无UI,无平台依赖)
// ============================================================
namespace PhotoelectricCore {
class DeviceController { ... }; // 设备控制
class ProtocolParser { ... }; // 协议解析
class VideoDecoder { ... }; // 视频解码 (FFmpeg封装)
class CommunicationManager { ... }; // 通信管理
class ConfigManager { ... }; // 配置管理
}
// ============================================================
// 第二层:UI表示层 (QT Widgets/QML,可替换)
// ============================================================
namespace PhotoelectricUI {
class MainWindow : public QMainWindow { ... };
class ConfigTab : public QWidget { ... };
class CommandTab : public QWidget { ... };
class VideoTab : public QWidget { ... };
}
// ============================================================
// 第三层:适配器层 (桥接核心与UI,为移植预留接口)
// ============================================================
class DeviceAdapter : public QObject {
Q_OBJECT
signals:
void deviceStatusChanged(Status status);
void trackingReportReceived(TrackingData data);
void videoFrameReceived(QImage frame);
public slots:
void sendCommand(QJsonObject cmd);
void startStream(QString url);
void stopStream();
};
2.2 移植到上位机的三种方式
| 方式 | 适用场景 | 工作量 | 优缺点 |
|---|---|---|---|
| 独立子进程 | 上位机通过进程间通信调用 | 小 | ✅ 改动最小,隔离性好 ❌ 进程间通信开销 |
| 动态库(.so/.dll) | 上位机以插件方式加载 | 中 | ✅ 性能好,集成度高 ❌ 需要调整编译配置 |
| 源码级整合 | 上位机直接包含源码编译 | 大 | ✅ 无额外开销 ❌ 代码耦合,维护困难 |
推荐 :先做独立子进程 方式,通过 本地Socket或共享内存 与上位机通信,上位机无需关心内部实现细节,只需发送指令和接收状态/视频流。
三、需要考虑的移植细节
3.1 设备模拟器(独立开发阶段必需)
cpp
ini
// 在没有真实设备时,用模拟器进行开发和测试
class DeviceSimulator {
public:
// 模拟设备响应
QJsonObject handleCommand(const QJsonObject& cmd) {
QString cmdName = cmd["cmd"].toString();
if (cmdName == "userLogin") {
return createLoginResponse();
}
if (cmdName == "ptzControl") {
int actionId = cmd["param"]["actionid"].toInt();
if (actionId == 51) {
// 模拟角度定位响应
return createAckResponse(100);
}
}
if (cmdName == "ivpSet") {
// 模拟跟踪状态回传
emit trackingReportGenerated(createMockTrackingData());
}
return createAckResponse(100);
}
signals:
void trackingReportGenerated(TrackingData data);
void ptzStatusGenerated(PTZStatus status);
};
3.2 移植时需要调整的地方
| 项目 | 独立开发时 | 移植到上位机时 |
|---|---|---|
| 窗口管理 | 独立QApplication | 可能嵌入到父窗口(QWidget::setParent()) |
| 日志输出 | 控制台 + 界面 | 需对接上位机日志系统 |
| 配置存储 | 本地配置文件 | 可能改为注册表或上位机统一配置 |
| 视频渲染 | 独立OpenGL窗口 | 可能需输出到上位机指定的渲染区域 |
| 热键 | 全局热键 | 需避免与上位机冲突 |
| 插件加载 | 静态链接 | 可改造为动态库(Q_PLUGIN_METADATA) |
3.3 上位机集成时的接口设计
cpp
csharp
// 抽象接口,供上位机调用
class PhotoelectricController {
public:
virtual ~PhotoelectricController() = default;
// 连接管理
virtual bool connectDevice(const QString& ip, int port,
const QString& user, const QString& pass) = 0;
virtual void disconnectDevice() = 0;
// 控制指令
virtual bool sendPtzCommand(int actionId, const QJsonObject& params) = 0;
virtual bool setTrackingMode(int mode, bool adaptive) = 0;
virtual bool startAutoTrack() = 0;
virtual bool stopTrack() = 0;
// 视频流
virtual bool startVideoStream(int channel, QWidget* container = nullptr) = 0;
virtual void stopVideoStream() = 0;
// 回调注册
virtual void setStatusCallback(std::function<void(Status)> cb) = 0;
virtual void setTrackingCallback(std::function<void(TrackingData)> cb) = 0;
virtual void setVideoFrameCallback(std::function<void(QImage)> cb) = 0;
signals:
// 纯虚信号(或回调函数方式)
};
四、移植路径规划
阶段一:独立桌面应用开发(当前阶段)
text
目标:功能完整、界面美观、可演示
产出:独立的 .exe / .app / 可执行文件
测试:通过设备模拟器 + 真实设备验证
阶段二:核心库抽取
text
swift
目标:将业务逻辑从UI中剥离,形成独立动态库
动作:
① 识别并分离"核心层"代码
② 设计清晰的API边界
③ 编译为动态库 (.dll / .so)
④ 编写接口文档和示例
产出:libPhotoelectricCore.so / PhotoelectricCore.dll
阶段三:上位机集成
text
目标:嵌入到实际项目的上位机系统中
动作:
① 上位机加载动态库
② 通过API调用控制设备
③ 处理回调数据(跟踪状态、视频帧等)
④ 界面适配(如需要)
产出:集成后的完整系统
五、注意事项与建议
| 事项 | 建议 |
|---|---|
| 依赖版本锁定 | 记录所有依赖库版本号(FFmpeg、OpenCV、QT版本),移植时保持一致 |
| 编译配置分离 | 使用 CMake 管理,区分 Desktop/Embedded/上位机 等不同构建配置 |
| 避免硬编码路径 | 所有路径通过配置文件或注册表获取 |
| 日志分级 | Debug/Info/Warning/Error 级别,上位机可根据需要过滤 |
| 资源文件 | 图片、字体等资源放入 .qrc 文件,随库一起分发 |
| 线程模型 | 解码线程、网络线程、UI线程严格分离,避免移植时死锁 |
| 内存管理 | 使用智能指针,避免因平台差异导致的内存泄漏 |
六、结论
| 问题 | 回答 |
|---|---|
| 是否可行? | ✅ 完全可行,这是标准工程实践 |
| 风险大吗? | 低。QT + FFmpeg 都是高度跨平台的技术栈 |
| 需要提前做什么? | 做好分层架构设计,核心逻辑与UI分离 |
| 移植工作量预估 | 约占总开发量的 15%~25%(主要是接口适配和测试) |
核心原则 :写代码时永远假设UI是可替换的,所有业务逻辑、网络通信、视频处理都放在非UI层,UI层只负责展示和接收用户操作事件。这样无论后续是独立运行、嵌入上位机,还是迁移到其他界面框架,改动都最小。