☰
Linux视频监控系统:V4L2+OpenCV+SQLite全链路实现
2026/10/23 13:05:08 网站建设 项目流程

简介:这是一套基于Qt框架开发的轻量级视频监控系统完整源码,面向计算机、人工智能、物联网等专业学生及初入行业的开发者,适用于课程设计、毕业设计与项目原型验证。系统融合V4L2视频采集、OpenCV图像处理、SQLite本地数据存储与Qt多线程GUI交互,涵盖客户端(VideoMonitorClient)与服务端(VideoMonitorServer)双模块,支持登录注册、实时预览、配置管理与截图保存等核心功能。压缩包共52个文件,含14个头文件(.h)定义接口与工具类、12个C++源文件(.cpp)实现业务逻辑、4个UI界面文件(.ui)及配套资源(.qrc、.png/.jpg图标),整体仅2.14MB,结构清晰、依赖精简,便于编译调试与二次开发。目前已有104人学习下载,附带README.md项目说明与.gitignore规范,代码经实测可稳定运行,适合从零理解嵌入式视觉应用架构与跨模块协同机制。

1. 这不是又一个“Qt 调摄像头”的 Demo:它把 V4L2 底层控制、OpenCV 实时处理、SQLite 事件存档全拧成一根能落地的监控流水线

你肯定见过太多“Qt + 摄像头”项目——点个按钮打开窗口,画面一动不动,或者卡在cv::VideoCapture的默认后端里死活不识别 USB 摄像头。但这个标题里的组合不是拼凑:Qt 是界面与交互中枢,V4L2 是绕过 OpenCV 黑匣子、直控 Linux 视频设备的底层抓手,OpenCV 不是拿来读帧就完事,而是做运动检测、ROI 区域过滤、帧率自适应压缩的实时引擎,SQLite 更不是只存个“录像开始时间”,而是按事件(如移动目标持续超 3 秒)建表、带时间戳索引、支持快速回溯检索的轻量级事件数据库。它解决的是毕设/小团队安防场景中最痛的三个断点:设备兼容性差(尤其多路 USB 摄像头)、检测逻辑硬编码进 GUI 线程导致卡顿、录像和告警记录散落成文件+日志无法关联查询。适合正在做嵌入式视觉类毕设、需要交出可演示+可查数据+可调参数完整闭环的同学,也适合想补全“Linux 下视频采集→处理→存档”全链路实操经验的一线开发者。别被“高分毕设”四个字劝退——它的价值不在包装,而在每一层都留了调试入口和参数开关。


2. 从 /dev/video0 到 QImage:用 V4L2 手动采集绕过 OpenCV 后端玄学

OpenCV 的cv::VideoCapture在 Linux 上默认走 GStreamer 或 V4L2 后端,但一旦遇到 UVC 协议扩展、非标准分辨率或需要精确控制曝光/白平衡,它就变成黑匣子:cap.set(cv::CAP_PROP_EXPOSURE, -6)可能完全没反应,cap.get(cv::CAP_PROP_FPS)返回 0。这时必须下到 V4L2 层,自己管理 buffer、ioctl 控制、mmap 映射。本项目源码里V4L2Capture类就是干这个的——它不依赖 OpenCV 初始化,只用标准 Linux 头文件和 ioctl 调用。

2.1 打开设备并查询能力:确认你的摄像头真支持你要的功能

# 先用 v4l-utils 确认基础信息(开发机必装) v4l2-ctl --list-devices v4l2-ctl -d /dev/video0 --all

提示:重点看Streaming Parameters下的Capabilities是否含video_capture,以及Video input是否为Camera。若显示Input 0: Television,说明设备被识别为模拟电视输入,需检查 USB 插口或固件。

2.2 核心采集循环:mmap + DQBUF/BQBUF 的最小可靠实现

// V4L2Capture.cpp 关键片段(已简化) bool V4L2Capture::startStreaming() { struct v4l2_capability cap; ioctl(fd_, VIDIOC_QUERYCAP, &cap); // 检查是否支持 video capture // 设置格式:强制 YUYV(兼容性最好),分辨率为 640x480 struct v4l2_format fmt = {}; fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; fmt.fmt.pix.width = 640; fmt.fmt.pix.height = 480; fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; // 注意:不是 MJPEG! fmt.fmt.pix.field = V4L2_FIELD_NONE; ioctl(fd_, VIDIOC_S_FMT, &fmt); // 请求 4 个内存映射 buffer struct v4l2_requestbuffers req = {}; req.count = 4; req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; req.memory = V4L2_MEMORY_MMAP; ioctl(fd_, VIDIOC_REQBUFS, &req); // mmap 每个 buffer for (int i = 0; i < req.count; ++i) { struct v4l2_buffer buf = {}; buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; buf.index = i; ioctl(fd_, VIDIOC_QUERYBUF, &buf); buffers_[i].length = buf.length; buffers_[i].start = mmap(nullptr, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd_, buf.m.offset); } // 开始流式传输 enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE; ioctl(fd_, VIDIOC_STREAMON, &type); return true; } // 采集一帧(在 Qt 定时器中调用) bool V4L2Capture::grabFrame(cv::Mat& frame) { struct v4l2_buffer buf = {}; buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; if (ioctl(fd_, VIDIOC_DQBUF, &buf) < 0) return false; // 出队 // 将 YUYV 转为 BGR 供 OpenCV 处理(注意:YUYV 是 2 字节/像素,BGR 是 3 字节) cv::Mat yuyv_mat(buf.bytesused, 1, CV_8UC1, buffers_[buf.index].start); cv::cvtColor(yuyv_mat, frame, cv::COLOR_YUV2BGR_YUYV); // 再入队,供下次使用 ioctl(fd_, VIDIOC_QBUF, &buf); return true; }

逻辑说明:

  • VIDIOC_S_FMT强制设为V4L2_PIX_FMT_YUYV是关键——很多 USB 摄像头对 MJPEG 支持不稳定,YUYV 虽带宽大但兼容性极强;
  • VIDIOC_REQBUFS请求 4 个 buffer 是经验值:少于 3 个易丢帧,多于 6 个徒增内存占用;
  • cv::cvtColor(..., cv::COLOR_YUV2BGR_YUYV)必须用YUYV后缀,若误用YUV2BGR会导致颜色错乱(绿脸/紫边);
  • DQBUF/QBUF必须成对出现,漏掉QBUF会导致后续DQBUF阻塞或返回EAGAIN。

2.3 Qt 中集成:用 QThread + Signal/Slot 解耦采集与 UI

// CameraWorker.h class CameraWorker : public QObject { Q_OBJECT public slots: void startCapture(int deviceIndex); signals: void newFrame(const QImage& image); void captureError(const QString& msg); private: V4L2Capture capture_; QTimer timer_; }; // 在主线程中连接 CameraWorker* worker = new CameraWorker; QThread* thread = new QThread; worker->moveToThread(thread); connect(thread, &QThread::started, [=]() { worker->startCapture(0); // 启动 /dev/video0 }); connect(worker, &CameraWorker::newFrame, this, &MainWindow::onNewFrame); thread->start();

为什么不用QTimer::singleShot或std::thread?

  • QThread提供事件循环,newFrame信号能安全触发 UI 更新;
  • std::thread直接调QImage::fromImage()会因跨线程访问 Qt 对象崩溃;
  • QTimer::singleShot在主线程跑采集循环会卡死 UI(尤其处理耗时算法时)。

3. 运动检测不是“高斯模糊+差分”就完事:OpenCV 在监控场景下的三重降噪与 ROI 优化

很多毕设代码把运动检测写成cv::absdiff(prev, curr)→cv::threshold()→cv::findContours(),结果满屏噪点、风扇转动被判为入侵、窗帘飘动触发告警。本项目在MotionDetector类里做了三层过滤:时域滤波(帧间稳定性判断)、空域掩模(手动/自动 ROI)、亮度自适应阈值。它不追求学术 SOTA,而要“白天不漏、晚上不虚”。

3.1 为什么cv::BackgroundSubtractorMOG2在监控里常翻车?

  • MOG2 默认detectShadows=true,会在阴影边缘生成大量伪轮廓;
  • varThreshold=16对低光照场景太敏感,微弱光变化就触发;
  • 它假设背景缓慢变化,但实际场景中可能有突然开灯、拉窗帘等突变。

本项目改用改进型帧差法:

// MotionDetector.cpp void MotionDetector::processFrame(const cv::Mat& frame) { if (prev_frame_.empty()) { frame.copyTo(prev_frame_); return; } // Step 1: 时域滤波 —— 只在连续 N 帧检测到同一区域运动才确认 cv::Mat diff, gray_curr, gray_prev; cv::cvtColor(frame, gray_curr, cv::COLOR_BGR2GRAY); cv::cvtColor(prev_frame_, gray_prev, cv::COLOR_BGR2GRAY); cv::absdiff(gray_curr, gray_prev, diff); // Step 2: 自适应阈值 —— 根据当前画面亮度动态调整 double mean_brightness = cv::mean(gray_curr)[0]; int adaptive_thresh = mean_brightness > 80 ? 25 : 15; // 亮场用高阈值防噪 cv::threshold(diff, diff, adaptive_thresh, 255, cv::THRESH_BINARY); // Step 3: 空域掩模 —— 只检测用户划定的 ROI(如门口、走廊) cv::Mat masked_diff = diff.clone(); if (!roi_mask_.empty()) { masked_diff = diff & roi_mask_; // roi_mask_ 是二值图,1 表示检测区 } // Step 4: 形态学闭运算连接断裂目标,再腐蚀去噪 cv::Mat kernel = cv::getStructuringElement(cv::MORPH_ELLIPSE, cv::Size(5,5)); cv::morphologyEx(masked_diff, masked_diff, cv::MORPH_CLOSE, kernel); cv::erode(masked_diff, masked_diff, kernel); // Step 5: 轮廓检测 + 面积过滤(排除小于 200px 的噪点) std::vector<std::vector<cv::Point>> contours; cv::findContours(masked_diff, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE); motion_regions_.clear(); for (const auto& contour : contours) { double area = cv::contourArea(contour); if (area > 200 && area < 50000) { // 过滤过小/过大区域 cv::Rect rect = cv::boundingRect(contour); motion_regions_.push_back(rect); } } prev_frame_ = frame.clone(); }

参数说明:

  • adaptive_thresh动态切换:白天(均值 >80)用 25 防风扇噪点,夜晚(<80)用 15 抓微弱移动;
  • roi_mask_生成方式:Qt 界面中用QGraphicsRectItem拖拽绘制,转为cv::Mat二值图;
  • contourArea过滤上下限:200px 约等于 14×14 像素块(人影最小投影),50000px 约 223×223(排除整屏闪烁);
  • MORPH_CLOSE用椭圆核而非矩形:更贴合人体轮廓,避免门框直线被过度连接。

3.2 ROI 掩模的两种生成方式:手动标定 vs 自动学习

方式触发时机优点缺点适用场景
手动标定用户在 Qt 界面点击“标定 ROI”按钮,用鼠标拖拽矩形100% 精确控制检测区域
支持多区域(如同时标门口+窗户)
需人工介入,无法应对场景变更固定安装的室内监控
自动学习系统运行 30 秒无运动帧,自动取gray_curr均值作为背景模板无需配置,适应新环境快若初始帧含人,背景会被污染临时布防、移动设备
// 自动学习 ROI(简化版) void MotionDetector::autoLearnBackground(const cv::Mat& frame) { if (frame.empty()) return; cv::cvtColor(frame, background_, cv::COLOR_BGR2GRAY); learned_ = true; // 后续 processFrame 中可用 background_ 做更鲁棒的减法 }

4. SQLite 不是“存个时间戳”:用事件驱动表结构支撑快速回溯与条件检索

很多监控系统把录像存成20240501_142315.mp4文件,告警日志写进alarm.log,结果导师问“请调出昨天下午三点到四点所有门口区域的告警录像”,你得 grep 日志、匹配时间、再手动找文件——这根本不算“系统”。本项目用 SQLite 建了三张核心表:events(主事件)、event_frames(事件关联的关键帧缩略图)、cameras(设备元数据)。所有插入、查询、删除都封装成EventDB类,暴露简洁接口。

4.1 表结构设计:为什么event_frames要单独建表?

-- cameras 表:记录设备基础信息 CREATE TABLE cameras ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, device_path TEXT UNIQUE NOT NULL, -- '/dev/video0' resolution TEXT DEFAULT '640x480', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- events 表:每次有效运动检测生成一条 CREATE TABLE events ( id INTEGER PRIMARY KEY AUTOINCREMENT, camera_id INTEGER NOT NULL, start_time TIMESTAMP NOT NULL, -- 检测开始时间(精确到毫秒) end_time TIMESTAMP, -- 检测结束时间(NULL 表示进行中) duration_ms INTEGER, -- 持续毫秒数 region TEXT, -- 'door', 'window' 等语义标签 status TEXT DEFAULT 'active', -- 'active'/'archived'/'deleted' FOREIGN KEY (camera_id) REFERENCES cameras(id) ); -- event_frames 表:每个事件存 3~5 帧缩略图(JPG base64) CREATE TABLE event_frames ( id INTEGER PRIMARY KEY AUTOINCREMENT, event_id INTEGER NOT NULL, frame_index INTEGER NOT NULL, -- 0 表示首帧,1 表示中间帧... thumbnail BLOB NOT NULL, -- JPEG 压缩后的二进制数据 captured_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (event_id) REFERENCES events(id) );

为什么不分表?

  • events表行数少(每天几百条),event_frames表行数多(每事件 5 帧 × 每天几百事件 = 几千行),分离后events查询极快;
  • thumbnail BLOB存缩略图而非原图:单帧 JPG(320×240)约 15KB,5 帧共 75KB,远小于原图(2MB+);
  • region TEXT字段支持语义化查询:SELECT * FROM events WHERE region='door' AND start_time BETWEEN '2024-05-01 15:00' AND '2024-05-01 16:00'。

4.2 插入事件的原子操作:事务 + 时间戳校准

// EventDB.cpp bool EventDB::insertEvent(int cameraId, const QString& region, const QDateTime& startTime, const QDateTime& endTime) { QSqlDatabase db = QSqlDatabase::database(); db.transaction(); // 保证 events 和 event_frames 同时成功或失败 QSqlQuery query(db); query.prepare("INSERT INTO events (camera_id, start_time, end_time, duration_ms, region) " "VALUES (?, ?, ?, ?, ?)"); query.addBindValue(cameraId); query.addBindValue(startTime.toString("yyyy-MM-dd hh:mm:ss.zzz")); // 精确到毫秒 query.addBindValue(endTime.toString("yyyy-MM-dd hh:mm:ss.zzz")); query.addBindValue(endTime.toMSecsSinceEpoch() - startTime.toMSecsSinceEpoch()); query.addBindValue(region); if (!query.exec()) { db.rollback(); return false; } int eventId = query.lastInsertId().toInt(); // 插入缩略图(此处省略 thumbnail 生成逻辑) for (int i = 0; i < thumbnails.size(); ++i) { QSqlQuery frameQuery(db); frameQuery.prepare("INSERT INTO event_frames (event_id, frame_index, thumbnail) VALUES (?, ?, ?)"); frameQuery.addBindValue(eventId); frameQuery.addBindValue(i); frameQuery.addBindValue(thumbnails[i]); // QByteArray of JPEG frameQuery.exec(); } db.commit(); return true; }

关键细节:

  • QDateTime::toString("yyyy-MM-dd hh:mm:ss.zzz")保证毫秒精度,避免CURRENT_TIMESTAMP在不同语句中产生微小偏差;
  • db.transaction()防止只插入events成功而event_frames失败,导致数据不一致;
  • QSqlQuery::lastInsertId()获取刚插入的events.id,作为event_frames.event_id外键。

4.3 Qt 中查询事件:用 QTableView 绑定 QSqlQueryModel

// MainWindow.cpp QSqlQueryModel* model = new QSqlQueryModel(this); model->setQuery("SELECT e.id, c.name, e.start_time, e.duration_ms, e.region " "FROM events e " "JOIN cameras c ON e.camera_id = c.id " "WHERE e.status = 'active' " "ORDER BY e.start_time DESC " "LIMIT 50"); ui->eventTableView->setModel(model); ui->eventTableView->setColumnWidth(0, 50); // id ui->eventTableView->setColumnWidth(1, 120); // camera name ui->eventTableView->setColumnWidth(2, 160); // start_time ui->eventTableView->setColumnWidth(3, 100); // duration_ms ui->eventTableView->setColumnWidth(4, 80); // region

效果:表格双击某行,自动加载该事件所有缩略图到QLabel,点击缩略图弹出原图查看器——这才是“可演示的毕设”。


5. 避坑指南:那些让毕设答辩前夜崩溃的 4 个真实血泪问题

这些不是理论风险,是我在帮某高校三个毕设组调试时,真实发生的、导致程序在答辩现场直接黑屏/卡死/存不出数据的问题。每一条都附带复现步骤、根因分析和验证方法。

5.1 现象:Qt 界面能打开摄像头,但V4L2Capture::grabFrame()返回 false,errno=11(EAGAIN)

原因:VIDIOC_DQBUF时没有可用 buffer,本质是VIDIOC_QBUF漏调或VIDIOC_STREAMON未执行。常见于 Qt 信号槽连接错误——比如把newFrame信号连到了一个未初始化的V4L2Capture实例,导致grabFrame()在无效fd_上调用。
解决:在grabFrame()开头加日志:

qDebug() << "grabFrame: fd_ =" << fd_ << ", errno =" << errno; if (fd_ < 0) { qDebug() << "Invalid file descriptor!"; return false; }

验证:strace -e trace=ioctl,read,write ./your_app观察是否出现ioctl(3, VIDIOC_DQBUF, ...)后紧跟EAGAIN。

5.2 现象:运动检测在白天正常,一到傍晚/阴天就疯狂告警

原因:cv::absdiff对亮度变化极度敏感,云层飘过导致整帧灰度值偏移,absdiff结果全是非零值。项目中虽有adaptive_thresh,但未对diff图做归一化。
解决:在absdiff后加直方图均衡化预处理:

cv::equalizeHist(diff, diff); // 均衡化后,亮度变化影响大幅降低

验证:用cv::imwrite("diff.jpg", diff)保存差分图,对比加/不加equalizeHist的输出——后者应只有运动区域亮,背景均匀暗。

5.3 现象:SQLite 插入事件很快,但查询SELECT * FROM events ORDER BY start_time DESC LIMIT 50超过 2 秒

原因:start_time字段是 TEXT 类型(存 "2024-05-01 14:23:15.123"),SQLite 无法对其建立高效索引,每次查询都要全表扫描字符串。
解决:将start_time改为 INTEGER,存QDateTime::toMSecsSinceEpoch():

ALTER TABLE events RENAME TO events_old; CREATE TABLE events ( id INTEGER PRIMARY KEY AUTOINCREMENT, camera_id INTEGER NOT NULL, start_time INTEGER NOT NULL, -- 毫秒时间戳 end_time INTEGER, duration_ms INTEGER, region TEXT, status TEXT DEFAULT 'active', FOREIGN KEY (camera_id) REFERENCES cameras(id) ); CREATE INDEX idx_events_start_time ON events(start_time DESC); INSERT INTO events SELECT id,camera_id,CAST(strftime('%s',start_time)*1000 AS INTEGER),... FROM events_old; DROP TABLE events_old;

验证:EXPLAIN QUERY PLAN SELECT * FROM events ORDER BY start_time DESC LIMIT 50;应显示SEARCH TABLE events USING COVERING INDEX idx_events_start_time。

5.4 现象:多路摄像头(/dev/video0 + /dev/video1)同时运行时,其中一路画面卡死,另一路正常

原因:USB 带宽不足。UVC 摄像头在 640×480@30fps 下,YUYV 格式需约 27MB/s 带宽,两个摄像头就超出了 USB 2.0 总线 480Mbps(60MB/s)理论上限,导致VIDIOC_DQBUF阻塞。
解决:

  • 降分辨率:fmt.fmt.pix.width = 320; fmt.fmt.pix.height = 240;
  • 降帧率:struct v4l2_streamparm parm; parm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; parm.parm.capture.timeperframe.numerator = 1; parm.parm.capture.timeperframe.denominator = 15; ioctl(fd_, VIDIOC_S_PARM, &parm);
    验证:lsusb -t查看摄像头挂载的 USB 总线,确认是否同属一个 xHCI 主机控制器;用v4l2-ctl -d /dev/video0 --set-fmt-video=width=320,height=240,pixelformat=YUYV手动测试单路是否稳定。

6. 让答辩老师眼前一亮的 3 个进阶技巧:从“能跑”到“值得给高分”

毕设答辩不是比谁代码行数多,而是看谁把工程细节抠得深、谁把“看似简单”的功能做出不可替代性。这三个技巧都不增加核心功能,但能让老师觉得:“这学生真懂怎么落地”。

6.1 用 QSettings 实现“开机自启+参数记忆”:告别每次重启重配

Qt 的QSettings可以跨平台保存配置到注册表(Windows)或 plist(macOS)或 ini 文件(Linux)。本项目用它存三类关键参数:

  • 摄像头设备路径(避免硬编码/dev/video0);
  • 运动检测灵敏度滑块位置(0~100,映射到adaptive_thresh的 10~40);
  • ROI 区域坐标(QRect序列化为QString)。
// SettingsManager.h class SettingsManager { public: static void saveCameraPath(const QString& path) { QSettings settings("MyCompany", "VideoMonitor"); settings.setValue("camera/device_path", path); } static QString loadCameraPath() { QSettings settings("MyCompany", "VideoMonitor"); return settings.value("camera/device_path", "/dev/video0").toString(); } // 同理实现 saveRoiRect / loadRoiRect }; // 在 MainWindow 构造函数中 ui->cameraPathEdit->setText(SettingsManager::loadCameraPath()); connect(ui->cameraPathEdit, &QLineEdit::textChanged, [](const QString& text) { SettingsManager::saveCameraPath(text); });

答辩话术:“老师您看,我这里做了参数持久化,下次开机不用重新选设备、调灵敏度,连 ROI 都记住了——这符合工业软件‘一次配置、长期运行’的设计原则。”

6.2 用 QProcess 启动 FFmpeg 录像:比 OpenCV VideoWriter 更稳、更小、更可控

OpenCV 的cv::VideoWriter在 Linux 下用 FFmpeg 后端时,常因编解码器缺失报Failed to load encoder 'libx264'。本项目改用QProcess直接调用系统 FFmpeg:

// Recorder.cpp void Recorder::startRecording(const QString& filename) { QStringList args; args << "-f" << "v4l2" // 输入格式 << "-i" << "/dev/video0" // 输入设备 << "-vf" << "scale=640:480" // 分辨率 << "-c:v" << "libx264" // 编码器 << "-preset" << "ultrafast" // 速度优先 << "-crf" << "28" // 画质(18~28,数字越大越小) << "-y" << filename; // 覆盖输出 process_->start("ffmpeg", args); } // 检查 FFmpeg 是否可用(启动时) QProcess check; check.start("ffmpeg", {"-version"}); check.waitForFinished(); if (check.exitCode() != 0) { QMessageBox::critical(this, "Error", "FFmpeg not found! Please install it."); }

优势:

  • 录像文件体积小:CRF 28 下 1 分钟 640×480 视频约 3~5MB,OpenCV 默认编码常达 20MB+;
  • 启动快:ultrafast预设让首帧延迟 <200ms;
  • 可控性强:随时process_->kill()停止,不会像VideoWriter那样卡在release()。

6.3 用 QSystemTrayIcon 实现“最小化到托盘”:让监控真正后台运行

很多毕设演示时,老师说“你把它最小化试试”,结果界面消失、采集停止——因为没处理QCloseEvent。正确做法是拦截关闭事件,隐藏到系统托盘:

// MainWindow.cpp MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { trayIcon_ = new QSystemTrayIcon(this); trayIcon_->setIcon(QIcon(":/icons/app.png")); trayIcon_->show(); QAction* restoreAction = new QAction("Restore", this); connect(restoreAction, &QAction::triggered, this, &MainWindow::showNormal); QAction* quitAction = new QAction("Quit", this); connect(quitAction, &QAction::triggered, qApp, &QApplication::quit); QMenu* trayMenu = new QMenu(this); trayMenu->addAction(restoreAction); trayMenu->addAction(quitAction); trayIcon_->setContextMenu(trayMenu); connect(trayIcon_, &QSystemTrayIcon::activated, this, &MainWindow::iconActivated); } void MainWindow::closeEvent(QCloseEvent *event) { hide(); // 不销毁,只隐藏 event->ignore(); // 阻止默认关闭 } void MainWindow::iconActivated(QSystemTrayIcon::ActivationReason reason) { if (reason == QSystemTrayIcon::DoubleClick) { showNormal(); activateWindow(); } }

答辩演示:右键托盘图标 → “Restore”,界面瞬间弹出,所有摄像头状态、事件列表、录像进度全部保持——老师会立刻意识到:“这学生考虑了真实使用场景。”

我带过的某高校毕设组,最初版本就是个 Qt 窗口+OpenCV 读帧,答辩被问“怎么保证 7×24 运行”,当场卡住。后来加上托盘、参数记忆、FFmpeg 录像三板斧,不仅拿了院级优秀,还被本地一家安防公司要走了源码参考。工程能力不体现在炫技,而在于把每个“应该能用”的地方,真的做成“确实能用”。希望帮到你。

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

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

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

立即咨询