视频监控实现画面缩放功能

文章目录

概要

在视频监控系统中,经常需要查看视频画面中的细节。通过实现区域放大、滚轮缩放和拖拽平移等功能,可以让用户更方便地观察视频细节。本文介绍如何在 Windows 系统下实现这些交互功能。

一、功能说明

框选缩放功能:按住鼠标左键,移动鼠标画出矩形框,松开左键,会将框选区域放大至整个窗口显示。点击鼠标右键,即恢复显示原画面。

鼠标滚轮缩放功能:通过鼠标滚轮控制缩放比例,以鼠标所在位置为中心进行缩放。

鼠标拖拽功能:在放大状态下,按住左键可以拖动视频画面,查看不同区域。

二、核心实现代码

cpp 复制代码
// 检查区域
bool IsValidZoomRc(const RECT *pRC) {
    return pRC != nullptr && abs(pRC->left - pRC->right) > 4 && abs(pRC->top - pRC->bottom) > 4;
}

// 左上右下位置相反时对调
inline void fmtRect(RECT &rc) {
    if (rc.left > rc.right) std::swap(rc.left, rc.right);
    if (rc.top > rc.bottom) std::swap(rc.top, rc.bottom);
}

// 播放类
class CPlayer {
   // ...省略其他代码
   private:
    bool m_bZoom;  // 是否启用缩放功能
    HANDLE m_hPlayer; // 调用播放库的句柄
    POINT m_ptDragStart;  // 拖动起始点
    RECT m_rcDragStart;  // 拖动开始时的显示区域
    AV_STREAM_INFO m_stStreamInfo;  // 存放流信息的结构体
    RECT m_rcZoom;  // 记录当前显示的画面区域
    RECT m_rcDrawRect;  // 用于画框

    // 在播放库回调出来的句柄上画框选的方框
    void OnVideoDraw(HANDLE handle, HDC hdc) {
        if (m_bZoom) {
            auto oldPen = SelectObject(hdc, m_hDrawPen);
            auto oldBrush = SelectObject(hdc, m_hDrawBrush);
            Rectangle(hdc, m_rcDrawRect.left, m_rcDrawRect.top, m_rcDrawRect.right, m_rcDrawRect.bottom);
            SelectObject(hdc, oldPen);
            SelectObject(hdc, oldBrush);
        }
    }

    // 画框缩放接口
    void DoZoom(int wndWidth, int wndHeight, const RECT *pRc, bool bLRButtonUp) {
        if (!m_bZoom && m_hPlayer != (void*)INFINITE) {
            HS_ShowRect(m_hPlayer, nullptr);  // 调用播放库接口,还原画面
            return;
        }
        if (pRc != nullptr) {
            // 如果画面已经处于放大状态,不再支持画框
            if (((m_rcZoom.right - m_rcZoom.left) != m_stStreamInfo.nWidth) ||
                ((m_rcZoom.bottom - m_rcZoom.top) != m_stStreamInfo.nHeight)) {
                return;
            }
        }
        if (bLRButtonUp || pRc == nullptr) {
            ZeroMemory(&m_rcDrawRect, sizeof(m_rcDrawRect));
        } else {
            m_rcDrawRect = *pRc;
        }

        double fWndWidth = wndWidth;
        double fWndHeight = wndHeight;
        if (bLRButtonUp) {
            if (pRc == nullptr) {
                if (m_hPlayer != (void*)INFINITE) {
                    HS_ShowRect(m_hPlayer, nullptr);
                }
                SetRect(&m_rcZoom, 0, 0, m_stStreamInfo.nWidth, m_stStreamInfo.nHeight);
            } else {
                RECT rc = m_rcZoom, rcPrm = *pRc;
                double fWidth = (double)fWndWidth / abs(m_rcZoom.left - m_rcZoom.right),
                fHeight = (double)fWndHeight / abs(m_rcZoom.top - m_rcZoom.bottom);
                fmtRect(rcPrm);
                rc.left += (LONG)floor((double)rcPrm.left / fWidth);
                rc.right = rc.left + (LONG)floor((double)abs(rcPrm.left - rcPrm.right) / fWidth);
                rc.top += (LONG)floor((double)rcPrm.top / fHeight);
                rc.bottom = rc.top + (LONG)floor((double)abs(rcPrm.top - rcPrm.bottom) / fHeight);
                if (m_hPlayer != (void*)INFINITE) {
                    bool bRet = IsValidZoomRc(&rc) && 0 == HS_ShowRect(m_hPlayer, &rc);  // 将要显示的区域传给播放库
                    if (bRet) {
                        m_rcZoom = rc;
                    }
                }
            }    
        }
    }

    // 鼠标滚轮缩放接口
    void DoWheelZoom(POINT ptMouse, int wndWidth, int wndHeight, short zDelta) {
        if (!m_bZoom || m_hPlayer == (void*)INFINITE) {
            return;
        }

        // 计算缩放比例 - 每次放大20%或缩小25%
        double scale = (zDelta > 0) ? 0.8 : 1.25;

        // 当前视频显示区域的宽高
        int currentWidth = m_rcZoom.right - m_rcZoom.left;
        int currentHeight = m_rcZoom.bottom - m_rcZoom.top;

        // 新的视频显示区域宽高
        int newWidth = (int)(currentWidth * scale);
        int newHeight = (int)(currentHeight * scale);
        if (zDelta > 0) {
            // 这里控制最多放大11次
            double minWidth = m_stStreamInfo.nWidth * pow(scale, 12);
            if (newWidth < minWidth) {
                return;
            }
        } else {
            // 这里控制只能缩小到视频原宽高
            if (newWidth > m_stStreamInfo.nWidth ||
                newHeight > m_stStreamInfo.nHeight) {
                newWidth = m_stStreamInfo.nWidth;
                newHeight = m_stStreamInfo.nHeight;
            }
        }

        // 计算鼠标在窗口坐标系中的相对位置(0-1范围)
        double mouseXRatio = ptMouse.x / (double)wndWidth;
        double mouseYRatio = ptMouse.y / (double)wndHeight;
        // 计算新的视频缩放区域,以鼠标位置为中心点
        RECT rcNew = {};
        int widthDiff = currentWidth - newWidth;
        int heightDiff = currentHeight - newHeight;
        rcNew.left = m_rcZoom.left + (int)(widthDiff * mouseXRatio);
        rcNew.top = m_rcZoom.top + (int)(heightDiff * mouseYRatio);
        rcNew.right = rcNew.left + newWidth;
        rcNew.bottom = rcNew.top + newHeight;

        // 确保不会超出视频边界
        if (rcNew.left < 0) {
            rcNew.right -= rcNew.left;
            rcNew.left = 0;
        }
        if (rcNew.top < 0) {
            rcNew.bottom -= rcNew.top;
            rcNew.top = 0;
        }
        if (rcNew.right > m_stStreamInfo.nWidth) {
            rcNew.left -= (rcNew.right - m_stStreamInfo.nWidth);
            rcNew.right = m_stStreamInfo.nWidth;
        }
        if (rcNew.bottom > m_stStreamInfo.nHeight) {
            rcNew.top -= (rcNew.bottom - m_stStreamInfo.nHeight);
            rcNew.bottom = m_stStreamInfo.nHeight;
        }

        // 应用新的缩放区域
        if (IsValidZoomRc(&rcNew)) {
            HS_ShowRect(m_hPlayer, &rcNew);
            m_rcZoom = rcNew;
        }
    }

    // 拖拽画面接口
    void DragMoveZoom(POINT ptMouse, int wndWidth, int wndHeight, bool bLBtnDown) {
        if (!m_bZoom || m_hPlayer == (void*)INFINITE) {
            return;
        }
        if (bLBtnDown) {  // 鼠标左键按下事件
            m_ptZoomDragStart = ptMouse;
            m_rcZoomDragStart = m_rcZoom;
        }

        // 画面未处于放大状态,不支持拖拽
        if (((m_rcZoom.right - m_rcZoom.left) == m_stStreamInfo.nWidth) &&
            ((m_rcZoom.bottom - m_rcZoom.top) == m_stStreamInfo.nHeight)) {
            return;
        }

        // 计算鼠标移动的距离
        int deltaX = m_ptZoomDragStart.x - ptMouse.x;
        int deltaY = m_ptZoomDragStart.y - ptMouse.y;

        // 根据窗口和实际视频的比例计算实际需要移动的距离
        double scaleX = (double)(m_rcZoom.right - m_rcZoom.left) / wndWidth;
        double scaleY = (double)(m_rcZoom.bottom - m_rcZoom.top) / wndHeight;

        // 计算实际移动距离
        int actualDeltaX = (int)(deltaX * scaleX);
        int actualDeltaY = (int)(deltaY * scaleY);

        // 计算新的显示区域
        RECT rcNew = m_rcZoomDragStart;
        rcNew.left += actualDeltaX;
        rcNew.right += actualDeltaX;
        rcNew.top += actualDeltaY;
        rcNew.bottom += actualDeltaY;

        // 边界检查
        if (rcNew.left < 0) {
            rcNew.right -= rcNew.left;
            rcNew.left = 0;
        }
        if (rcNew.top < 0) {
            rcNew.bottom -= rcNew.top;
            rcNew.top = 0;
        }
        if (rcNew.right > m_stStreamInfo.nWidth) {
            rcNew.left -= (rcNew.right - m_stStreamInfo.nWidth);
            rcNew.right = m_stStreamInfo.nWidth;
        }
        if (rcNew.bottom > m_stStreamInfo.nHeight) {
            rcNew.top -= (rcNew.bottom - m_stStreamInfo.nHeight);
            rcNew.bottom = m_stStreamInfo.nHeight;
        }

        // 应用新的显示区域
        if (IsValidZoomRc(&rcNew)) {
            HS_ShowRect(m_hPlayer, &rcNew);
            m_rcZoom = rcNew;
        }
    }
    // ...省略其他代码
};

// 窗口类
class CPlayWnd {
   // ...省略其他代码
   private:
    RECT m_arrBlock;  // 记录框选的区域
    bool m_bLBottonDowned;  // 记录鼠标左键是否处于按下状态
    int m_nWndWidth;
    int m_nWndHeight;
 
   public:
    // 窗口事件响应函数
    LRESULT ChildWinMsgProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) {
        switch (msg) {
            // ...省略其他代码
            case WM_LBUTTONDOWN: {
                m_bLBottonDowned = true;
                // SetCapture(hWnd);  // 捕获鼠标
                POINT ptMouse = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
                auto &rc = m_arrBlock;
                rc.left = rc.right = LOWORD(lParam);
                rc.top = rc.bottom = HIWORD(lParam);
                
                /* 这里通知播放类CPlayer,调用到CPlayer的缩放和拖拽接口 */
                /* OnDoZoom(m_nWndWidth, m_nWndHeight, &rc, false); */
                /* OnDragZoom(m_nWndWidth, m_nWndHeight, ptMouse, true); */
            } break;
            case WM_MOUSELEAVE: {
                if (m_bLBottonDowned) {
                    m_bLBottonDowned = false;
                    auto &rc = m_arrBlock;
                    /* 这里通知播放类CPlayer,调用到CPlayer的缩放接口 */
                    /* OnDoZoom(m_nWndWidth, m_nWndHeight, &rc, true); */
                }
            } break;
            case WM_MOUSEMOVE: {
                // 设置捕获鼠标离开窗口事件
                TRACKMOUSEEVENT trackEvent;
                trackEvent.cbSize = sizeof(TRACKMOUSEEVENT);
                trackEvent.dwFlags = TME_LEAVE;
                trackEvent.hwndTrack = hWnd;
                TrackMouseEvent(&trackEvent);

                if (m_bLBottonDowned) {
                    POINT ptMouse = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
                    auto &rc = m_arrBlock;
                    rc.right = LOWORD(lParam);
                    rc.bottom = HIWORD(lParam);
                    /* 这里通知播放类CPlayer,调用到CPlayer的缩放和拖拽接口 */
                    /* OnDoZoom(m_nWndWidth, m_nWndHeight, &rc, false); */
                    /* OnDragZoom(m_nWndWidth, m_nWndHeight, ptMouse, false); */
                }
            } break;
            case WM_MOUSEWHEEL: {
                POINT ptMouse;
                // 鼠标在屏幕的坐标
                ptMouse.x = GET_X_LPARAM(lParam);
                ptMouse.y = GET_Y_LPARAM(lParam);
                // 转换为相对于窗口的坐标
                ScreenToClient(hWnd, &ptMouse);
                // 获取滚轮delta值
                short zDelta = GET_WHEEL_DELTA_WPARAM(wParam);

                /* 这里通知播放类CPlayer,调用到CPlayer的滚轮缩放接口 */
                /* OnWheelZoom(ptMouse, m_nWndWidth, m_nWndHeight, zDelta); */
            } break;
            case WM_LBUTTONUP: {
                if (m_bLBottonDowned) {
                    // ReleaseCapture();  // 释放鼠标捕获
                    m_bLBottonDowned = false;
                    auto &rc = m_arrBlock;
                    rc.right = LOWORD(lParam);
                    rc.bottom = HIWORD(lParam);
                    /* 这里通知播放类CPlayer,调用到CPlayer的缩放接口 */
                    /* OnDoZoom(m_nWndWidth, m_nWndHeight, &rc, true); */
                }
            } break;
            case WM_RBUTTONUP: {
                /* 这里通知播放类CPlayer,调用到CPlayer的缩放接口 */
                /* OnDoZoom(0, 0, nullptr, true); */

            } break;
            case WM_LBUTTONDBLCLK: {
                /* 这里通知播放类CPlayer,调用到CPlayer的缩放接口 */
                /* OnDoZoom(0, 0, nullptr, true); */
            } break;
            // ...省略其他代码
        }
    }
   // ...省略其他代码
};

三、技术细节

  • 本文主要介绍的是如何计算出显示区域,计算出来后将结果传给播放库去显示,播放库代码不在本文展示
  • 使用GET_X_LPARAMGET_Y_LPARAM替代LOWORDHIWORD处理坐标
  • 正确处理屏幕坐标到客户区坐标的转换
  • 不使用SetCapture时,鼠标移出窗口后消息中断,拖动操作可能无法正常完成
  • 使用SetCapture后,可以跟踪整个拖动过程,即使鼠标移出窗口也能继续操作
相关推荐
爱穿西装的C先生11 分钟前
C++学习日记---第13天(类和对象---封装)
c++·学习·程序人生·蓝桥杯
AKA 埼玉12 分钟前
第一周周总结
c++
legend_jz17 分钟前
【linux】手搓线程池
linux·运维·服务器·c++·笔记·学习·学习方法
嗨信奥1 小时前
GESP C++等级考试 二级真题(2024年9月)
c++
绵绵细雨中的乡音2 小时前
功能强大的stringstream类
c++·算法
JXH_1232 小时前
ms-hot29 解码方法
c++·算法·leetcode
LinuxST2 小时前
30、Firefly-rk3399定时器
linux·windows·stm32·嵌入式硬件·ubuntu
万物复苏1013 小时前
c++-用c++解决简单数学问题
开发语言·c++·笔记·青少年编程
咔咔咔的3 小时前
3239. 最少翻转次数使二进制矩阵回文 I
c++