Qt编写安防视频监控系统67-跨平台及国产系统

一、前言

得益于 Qt 的超强跨平台特性,本系统也是跨平台的,亲测的系统包括 windows 全系列、ubuntu 全系列、centeos、国产系统 UOS、国产系统银河麒麟、中标麒麟、嵌入式 linux、树莓派、香橙派等,所有的外观统一,由于默认采用的 sqlite 数据库(视频监控系统要存储的记录很少,用 Qt 内置的数据库 sqlite 是最合适的),所有数据库这块也不用担心其他平台移植的问题,毕竟 Qt 内置的 sqlite 数据库都有。

整个视频监控系统针对特定系统平台的不同处理代码不多,由于前期花了大量的精力整理了跨平台的自定义控件大全(目前超过 189 个)、各种跨平台的轮子组件(数据导入导出、数据库应用组件、多线程文件传输、地图展示等),所以只要系统直接用这些控件和组件就行,控件和组件跨平台了,整个系统也就是跨平台的,毕竟 Qt 基础控件和样式以及 painter 绘制都是完全跨平台通用的,目前为止没有发现什么不同的系统不同的区别。写程序也和造房子一样,先要把基础打牢固,基础牢固了,上面一层层的楼房蹭蹭就上去了。

二、功能特点

(一)软件模块

  1. 视频监控模块,各种停靠小窗体子模块,包括设备列表、图文警情、窗口信息、云台控制、预置位、巡航设置、设备控制、悬浮地图、网页浏览等。
  2. 视频回放模块,包括本地回放、远程回放、设备播放、图片回放、视频上传等。
  3. 电子地图模块,包括图片地图、在线地图、离线地图、路径规划等。
  4. 日志查询模块,包括本地日志、设备日志等。
  5. 系统设置模块,包括系统设置(基本设置、视频参数、数据库设置、地图配置、串口配置等)、录像机管理、摄像机管理、轮询配置、用户管理等。

(二)基础功能

  1. 支持各种视频流(rtsp、rtmp、http 等)、视频文件(mp4、rmvb、avi 等)、本地 USB 摄像机播放。
  2. 支持多画面切换,包括 1、4、6、8、9、13、16、25、36、64 画面切换。
  3. 支持全屏切换,多种切换方式包括鼠标右键菜单、工具栏按钮、快捷键(alt+enter 全屏,esc 退出全屏)。
  4. 支持视频轮询,包括 1、4、9、16 画面轮询,可设置轮询分组(轮询预案)、轮询间隔、码流类型等。
  5. 支持 onvif 协议,包括设备搜索、云台控制、设备控制(图片参数、校对时间、系统重启,抓拍图片等)。
  6. 支持权限管理,不同的用户可以对应不同的模块权限,比如删除日志、关闭系统等。
  7. 数据库支持多种,包括 sqlite、mysql、sqlserver、postgresql、oracle、人大金仓等。
  8. 本地 USB 摄像机支持设置分辨率、帧率等参数。
  9. 所有停靠模块都自动生成对应的菜单用来控制显示和隐藏,在标题栏右键可以弹出。
  10. 支持显示所有模块、隐藏所有模块、复位普通布局、复位全屏布局。
  11. 双击设备弹出实时预览视频,支持图片地图、在线地图、离线地图等。
  12. 摄像机节点拖曳到对应窗体播放视频,同时支持拖曳本地文件直接播放。
  13. 删除视频支持鼠标右键删除、悬浮条关闭删除、拖曳到视频监控面板外删除等多种方式。
  14. 图片地图上设备按钮可自由拖动,自动保存位置信息。百度地图上可以鼠标单击获取经纬度信息,用来更新设备位置。
  15. 视频监控面板窗体中任意通道支持拖曳交换,瞬间响应。
  16. 封装了百度地图,视图切换,运动轨迹,设备点位,鼠标按下获取经纬度等。
  17. 双击节点、拖曳节点、拖曳窗体交换位置等操作,均自动更新保存最后的播放地址,下次软件打开自动应用。
  18. 右下角音量条控件,失去焦点自动隐藏,音量条带静音图标。
  19. 支持视频截图,可指定单个或者对所有通道截图,底部小工具栏也有截图按钮。
  20. 支持超时自动隐藏鼠标指针、自动全屏机制。
  21. 支持 onvif 云台控制,可上下左右移动云台摄像机,包括复位和焦距调整等。
  22. 支持任意 onvif 摄像机,包括但不限于海康、大华、宇视、天地伟业、华为等。
  23. 可保存视频,可选定时存储或者单文件存储,可选存储间隔时间。
  24. 可设置视频流通信方式 tcp+udp,可设置视频解码是速度优先、质量优先、均衡等。
  25. 可设置软件中文名称、英文名称、LOGO 图标等。
  26. 存储的视频文件支持导出到指定目录,支持批量上传到服务器。

(三)特色功能

  1. 主界面采用停靠窗体模式,各种组件以小模块的形式加入,可自定义任意模块加入。
  2. 停靠模块可拖动任意位置嵌入和悬浮,支持最大化全屏,支持多屏幕。
  3. 双重布局文件存储机制,正常模式、全屏模式都对应不同的布局方案,自动切换和保存,比如全屏模式可以突出几个模块透明显示在指定位置,更具科幻感现代化。
  4. 原创 onvif 协议机制,采用底层协议解析(udp 广播搜索 +http 请求执行命令)更轻量易懂易学习拓展,不依赖任何第三方组件比如 gsoap。
  5. 原创数据导入导出机制,跨平台不依赖任何组件,瞬间导出数据。
  6. 内置多个原创组件,宇宙超值超级牛逼,包括数据导入导出组件(导出到 xls、pdf、打印)、数据库组件(数据库管理线程、自动清理数据线程、万能分页、数据请求等)、地图组件、视频监控组件、文件多线程收发组件、onvif 通信组件、通用浏览器内核组件等。
  7. 自定义信息框 + 错误框 + 询问框 + 右下角提示框(包含多种格式)等。
  8. 精美换肤,高达 17 套皮肤样式随意更换,所有样式全部统一,包括菜单等。
  9. 视频控件悬浮条可以自行增加多个按钮,监控界面底部小工具栏也可自行增加按钮。
  10. 双击摄像机节点自动播放视频,双击节点自动依次添加视频,会自动跳到下一个,双击父节点自动添加该节点下的所有视频。可选主码流、子码流。
  11. 录像机管理、摄像机管理,可添加删除修改导入导出打印信息,立即应用新的设备信息生成树状列表,不需重启。
  12. 可选多种内核自由切换,ffmpeg、vlc、mpv 等,均可在 pro 中设置。推荐用 ffmpeg,跨平台最多,默认提供好了 linux 和 mac 平台上编译好的库。
  13. 支持硬解码,可设置硬解码类型(qsv、dxva2、d3d11va 等)。
  14. 默认采用 opengl 绘制视频,超低的 CPU 资源占用,支持 yuyv 和 nv12 两种格式绘制,很牛逼。
  15. 高度可定制化,用户可以很方便的在此基础上衍生自己的功能,比如增加自定义模块,增加运行模式、机器人监控、无人机监控、挖掘机监控等。
  16. 支持 xp、win7、win10、linux、mac、各种国产系统(UOS、中标麒麟、银河麒麟等)、嵌入式 linux 等系统。
  17. 注释完整,项目结构清晰,超级详细完整的使用开发手册,精确到每个代码文件的功能说明,不断持续迭代版本。

三、体验地址

  1. 体验地址:https://pan.baidu.com/s/1d7TH_GEYl5nOecuNlWJJ7g 提取码:01jf 文件名:bin_video_system.zip。
  2. 国内站点:https://gitee.com/feiyangqingyun
  3. 国际站点:https://github.com/feiyangqingyun
  4. 个人主页:https://blog.csdn.net/feiyangqingyun
  5. 知乎主页:https://www.zhihu.com/people/feiyangqingyun/
  6. 在线文档:https://feiyangqingyun.gitee.io/qwidgetdemo/video_system/

四、效果图






五、核心代码

#include "frmmodule.h"
#include "ui_frmmodule.h"
#include "quihelper.h"
#include "customtitlebar.h"
#include "frmvideopanel.h"

#include "frmdevicetree.h"
#include "frmdevicegps.h"
#include "frmmsglist.h"
#include "frmmsgtable.h"
#include "frmwebview.h"

#include "frmipcptz.h"
#include "frmipccontrol.h"
#include "frmipcpreset.h"

#include "frmrobotdata.h"
#include "frmrobotdebug.h"
#include "frmrobotdebug2.h"
#include "frmrobotemulate.h"

frmModule::frmModule(QWidget *parent) : QMainWindow(parent), ui(new Ui::frmModule)
{
ui->setupUi(this);
this->initForm();
this->initWidget();
this->addWidget();
}

frmModule::~frmModule()
{
delete ui;
}

void frmModule::showEvent(QShowEvent *)
{
// 首次显示的时候加载布局
static bool isLoad = false;
if (!isLoad) {
isLoad = true;
// 先设置下默认尺寸
this->initSize();
// 采用 invokeMethod 异步执行
//QMetaObject::invokeMethod(this, "initLayout", Qt::QueuedConnection);
QTimer::singleShot(300, this, SLOT(initLayout()));
return;
}

<span class="hljs-comment">//再次显示的时候将刚才暂时隐藏的浮动窗体显示</span>
foreach (QDockWidget *dockWidget, hideWidgets) {
    dockWidget-&gt;setVisible(<span class="hljs-literal">true</span>);
}

}

void frmModule::hideEvent(QHideEvent *)
{
// 记住所有可见浮动的窗体有哪些, 切换到其他页面需要暂时隐藏
hideWidgets.clear();
foreach (QDockWidget *dockWidget, dockWidgets) {
if (dockWidget->isVisible() && dockWidget->isFloating()) {
dockWidget->setVisible(false);
hideWidgets << dockWidget;
}
}
}

void frmModule::initForm()
{
// 程序退出的时候保存布局到配置文件
connect(AppEvent::Instance(), SIGNAL(exitAll()), this, SLOT(saveLayout()));
// 系统设置中透明度值改变立即应用
connect(AppEvent::Instance(), SIGNAL(changeWindowOpacity()), this, SLOT(changeWindowOpacity()));
// 全屏切换自动切换对应的布局
connect(AppEvent::Instance(), SIGNAL(fullScreen(bool)), this, SLOT(fullScreen(bool)));
}

void frmModule::initSize()
{
if (dockWidths.count() == 0 || dockHeights.count() == 0) {
return;
}
if (dockWidths.count() != dockHeights.count()) {
return;
}
if (dockWidths.count() != dockWidgets.count()) {
return;
}
#if (QT_VERSION >= QT_VERSION_CHECK(5,6,0))
this->resizeDocks(dockWidgets, dockWidths, Qt::Horizontal);
this->resizeDocks(dockWidgets, dockHeights, Qt::Vertical);
#endif
}

void frmModule::initMenu()
{
// 将子模块的名称集合以及可见状态发给主界面生成右键菜单
QList<QString> titles;
QList<bool> visibles;
foreach (QDockWidget *dockWidget, dockWidgets) {
titles << dockWidget->windowTitle();
visibles << dockWidget->isVisible();
}
emit loadModuleFinshed(titles, visibles);
}

void frmModule::initWidget()
{
// 清空列表
this->dockWidgets.clear();
this->dockWidths.clear();
this->dockHeights.clear();

<span class="hljs-comment">//停靠窗体默认尺寸 绝大部分模块都按照这个尺寸来</span>
<span class="hljs-type">int</span> width = <span class="hljs-number">220</span>;
<span class="hljs-type">int</span> height = <span class="hljs-number">500</span>;

<span class="hljs-comment">//根据不同的工作模式加载不同的模块</span>
<span class="hljs-comment">//可以自行更改对应的名称</span>
<span class="hljs-keyword">if</span> (AppConfig::WorkMode == <span class="hljs-number">1</span>) {
    frmDeviceGps *deviceGps = new frmDeviceGps;
    frmRobotData *robotData = new frmRobotData;
    connect(robotData, SIGNAL(moveDevice(<span class="hljs-type">int</span>, QString, QString)),
            deviceGps, SLOT(moveDevice(<span class="hljs-type">int</span>, QString, QString)));

    newWidget(new frmDeviceTree, <span class="hljs-string">"设备列表"</span>, width, height);
    newWidget(new frmMsgTable, <span class="hljs-string">"窗口信息"</span>, width, <span class="hljs-number">200</span>);

    newWidget(deviceGps, <span class="hljs-string">"设备轨迹"</span>, width, <span class="hljs-number">200</span>);
    newWidget(robotData, <span class="hljs-string">"仿真数据"</span>, width, height);
    newWidget(new frmRobotDebug2, <span class="hljs-string">"数据调试"</span>, width, height);
    newWidget(new frmRobotEmulate, <span class="hljs-string">"运动仿真"</span>, width, height);
} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (AppConfig::WorkMode == <span class="hljs-number">2</span>) {
    newWidget(new frmMsgList, <span class="hljs-string">"图文警情"</span>, width, height);
    newWidget(new frmMsgTable, <span class="hljs-string">"窗口信息"</span>, width, <span class="hljs-number">200</span>);

    newWidget(new frmDeviceGps, <span class="hljs-string">"飞行轨迹"</span>, width, <span class="hljs-number">200</span>);
    newWidget(new frmIpcPtz, <span class="hljs-string">"云台控制"</span>, width, height);
    newWidget(new frmDeviceTree, <span class="hljs-string">"设备列表"</span>, width, height);
} <span class="hljs-keyword">else</span> {
    newWidget(new frmMsgList, <span class="hljs-string">"图文警情"</span>, width, height);
    newWidget(new frmMsgTable, <span class="hljs-string">"窗口信息"</span>, width, <span class="hljs-number">200</span>);
    newWidget(new frmDeviceGps, <span class="hljs-string">"悬浮地图"</span>, width, <span class="hljs-number">250</span>);
    newWidget(new frmDeviceTree, <span class="hljs-string">"设备列表"</span>, width, height);

    newWidget(new frmIpcPtz, <span class="hljs-string">"云台控制"</span>, width, height);
    newWidget(new frmIpcControl, <span class="hljs-string">"设备控制"</span>, width, height);
    newWidget(new frmIpcPreset, <span class="hljs-string">"预置巡航"</span>, width, height);
}

#if 0
// 演示加载网页浏览模块
frmWebView *webView = new frmWebView;
webView->resize(400, 300);
//webView->setUrl("http://www.qtcn.org/");
webView->setUrl("http://data.fengmap.cn:1024/yz/connect/DEMO/%E6%A8%A1%E5%9E%8B3D/DEMO%E4%B8%89%E7%BB%B41/20180611_%E5%94%90%E5%B1%B1%E5%B7%A5%E4%BA%BA%E5%8C%BB%E9%99%A2_241902");
QDockWidget *dockWebView = newWidget(webView, "网页浏览", width, height);
// 设置当前子模块固定尺寸
dockWebView->setFixedSize(800, 600);
dockWebView->setProperty("FixedSize", true);
// 加入到停靠布局中
addWidget(dockWebView, 0);
#endif

#if 0
// 演示独立的视频监控窗体, 多屏幕
frmVideoPanel *videoPanel2 = new frmVideoPanel;
videoPanel2->resize(400, 300);
QDockWidget *dockVideoPanel = newWidget(videoPanel2, "图像监控", width, height);
// 加入到停靠布局中
addWidget(dockVideoPanel, 0);
#endif

<span class="hljs-comment">//实例化视频监控通道画面</span>
frmVideoPanel *videoPanel = new frmVideoPanel;
videoPanel-&gt;setObjectName(<span class="hljs-string">"centralWidget_frmVideoPanel"</span>);

<span class="hljs-comment">//设置中心窗体</span>
this-&gt;setCentralWidget(videoPanel);
<span class="hljs-comment">//设置停靠参数,不允许重叠,只允许拖动</span>
<span class="hljs-comment">//this-&gt;setDockOptions(QMainWindow::AnimatedDocks);</span>

}

QDockWidget *frmModule::newWidget(QWidget *widget, const QString &title, int width, int height)
{
// 设置拉伸策略
//widget->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);

<span class="hljs-comment">//实例化停靠窗体</span>
QString objName = widget-&gt;objectName();
QDockWidget *dockWidget = new QDockWidget;
dockWidget-&gt;setObjectName(<span class="hljs-string">"dockWidget_"</span> + objName);
dockWidget-&gt;setWindowTitle(title);
dockWidget-&gt;setWidget(widget);
<span class="hljs-comment">//这里控制停靠窗体的透明度,在剥离出来悬浮的时候透明效果可见</span>
dockWidget-&gt;setWindowOpacity((qreal)AppConfig::WindowOpacity / <span class="hljs-number">100</span>);

<span class="hljs-comment">//自定义停靠窗体标题栏</span>
CustomTitleBar *titleBar = new CustomTitleBar;
titleBar-&gt;setObjectName(<span class="hljs-string">"titleBar_"</span> + objName);
titleBar-&gt;setFull(<span class="hljs-literal">false</span>);
titleBar-&gt;setTitle(title);
dockWidget-&gt;setTitleBarWidget(titleBar);
connect(dockWidget, SIGNAL(visibilityChanged(<span class="hljs-type">bool</span>)), this, SLOT(visibilityChanged(<span class="hljs-type">bool</span>)));

<span class="hljs-comment">//操作员不能对停靠窗体做任何动作</span>
<span class="hljs-keyword">if</span> (AppData::CurrentUserType == <span class="hljs-string">"操作员"</span>) {
    dockWidget-&gt;setFeatures(QDockWidget::NoDockWidgetFeatures);
}

<span class="hljs-comment">//设置只允许左侧和右侧停靠</span>
dockWidget-&gt;setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);

dockWidgets &lt;&lt; dockWidget;
dockWidths &lt;&lt; width;
dockHeights &lt;&lt; height;
<span class="hljs-keyword">return</span> dockWidget;

}

void frmModule::addWidget()
{
// 根据不同的工作模式调整模块的位置
// 下面的居然有顺序要求才能应用透明度, 貌似要从右到左, 妹的不知道怎么回事
if (AppConfig::WorkMode == 1) {
// 添加右侧窗体
addWidget(2, 1);
addWidget(3, 1);
addWidget(4, 1);
addWidget(5, 1);

    <span class="hljs-comment">//添加左侧窗体</span>
    addWidget(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>);
    addWidget(<span class="hljs-number">1</span>, <span class="hljs-number">0</span>);

    <span class="hljs-comment">//合并窗体形成选项卡</span>
    this-&gt;tabifyDockWidget(dockWidgets.at(<span class="hljs-number">3</span>), dockWidgets.at(<span class="hljs-number">4</span>));
    this-&gt;tabifyDockWidget(dockWidgets.at(<span class="hljs-number">4</span>), dockWidgets.at(<span class="hljs-number">5</span>));
} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (AppConfig::WorkMode == <span class="hljs-number">2</span>) {
    <span class="hljs-comment">//添加右侧窗体</span>
    addWidget(<span class="hljs-number">2</span>, <span class="hljs-number">1</span>);
    addWidget(<span class="hljs-number">3</span>, <span class="hljs-number">1</span>);
    addWidget(<span class="hljs-number">4</span>, <span class="hljs-number">1</span>);

    <span class="hljs-comment">//添加左侧窗体</span>
    addWidget(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>);
    addWidget(<span class="hljs-number">1</span>, <span class="hljs-number">0</span>);
} <span class="hljs-keyword">else</span> {
    <span class="hljs-comment">//添加右侧窗体</span>
    addWidget(<span class="hljs-number">3</span>, <span class="hljs-number">1</span>);
    addWidget(<span class="hljs-number">4</span>, <span class="hljs-number">1</span>);
    addWidget(<span class="hljs-number">6</span>, <span class="hljs-number">1</span>);

    <span class="hljs-comment">//添加左侧窗体</span>
    addWidget(<span class="hljs-number">2</span>, <span class="hljs-number">0</span>);
    addWidget(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>);
    addWidget(<span class="hljs-number">1</span>, <span class="hljs-number">0</span>);
    addWidget(<span class="hljs-number">5</span>, <span class="hljs-number">0</span>);

    <span class="hljs-comment">//合并窗体形成选项卡</span>
    this-&gt;tabifyDockWidget(dockWidgets.at(<span class="hljs-number">1</span>), dockWidgets.at(<span class="hljs-number">5</span>));
    this-&gt;tabifyDockWidget(dockWidgets.at(<span class="hljs-number">4</span>), dockWidgets.at(<span class="hljs-number">6</span>));
}

}

void frmModule::addWidget(int index, int position)
{
if (index < dockWidgets.count()) {
addWidget(dockWidgets.at(index), position);
}
}

void frmModule::addWidget(QDockWidget *widget, int position)
{
// 设置停靠位置
Qt::DockWidgetArea area;
if (position == 0) {
area = Qt::LeftDockWidgetArea;
} else if (position == 1) {
area = Qt::RightDockWidgetArea;
} else if (position == 2) {
area = Qt::TopDockWidgetArea;
} else if (position == 3) {
area = Qt::BottomDockWidgetArea;
} else {
area = Qt::AllDockWidgetAreas;
}

this-&gt;addDockWidget(area, widget);

}

void frmModule::changeWindowOpacity()
{
foreach (QDockWidget *dockWidget, dockWidgets) {
dockWidget->setWindowOpacity((qreal)AppConfig::WindowOpacity / 100);
}
}

void frmModule::fullScreen(bool full)
{
// 先保存原来的布局再加载新的布局
this->saveLayout(!AppConfig::FormFull);
QTimer::singleShot(200, this, SLOT(fullScreen()));
}

void frmModule::fullScreen()
{
this->initLayout(AppConfig::FormFull);
}

QString frmModule::getLayoutIni(bool full)
{
QString flag = full ? "full" : "normal";
QString file = QString("%1/layout/video_workmode%2_%3.ini").arg(QUIHelper::appPath()).arg(AppConfig::WorkMode).arg(flag);
return file;
}

void frmModule::initLayout(bool full)
{
// 调用 Qt 自己的函数 restoreState 来加载布局
// 不同的工作模式对应不同的布局文件, 区分全屏和正常状态
QString file = getLayoutIni(full);
QByteArray data = AppConfig::readLayout(file);
this->restoreState(data);

<span class="hljs-comment">//生成停靠窗体菜单</span>
this-&gt;initMenu();

}

void frmModule::saveLayout(bool full)
{
// 当前页面不可见不用处理
if (!this->isVisible()) {
return;
}

<span class="hljs-comment">//调用Qt自己的函数 saveState 来保存布局</span>
<span class="hljs-comment">//不同的工作模式对应不同的布局文件,区分全屏和正常状态</span>
QString file = getLayoutIni(full);
QByteArray data = this-&gt;saveState();
AppConfig::writeLayout(file, data);

}

void frmModule::resetLayout(bool full)
{
// 删除布局配置文件
QFile(getLayoutIni(full)).remove();

<span class="hljs-comment">//复位所有模块</span>
foreach (QDockWidget *dockWidget, dockWidgets) {
    dockWidget-&gt;setVisible(<span class="hljs-literal">true</span>);
    dockWidget-&gt;setFloating(<span class="hljs-literal">false</span>);
}

<span class="hljs-comment">//设置所有停靠窗体的默认宽高</span>
this-&gt;initSize();
<span class="hljs-comment">//立即保存布局</span>
this-&gt;saveLayout(full);
<span class="hljs-comment">//this-&gt;initLayout(full);</span>

}

void frmModule::visibilityChanged(bool visible)
{
// 参数中的 visible 没有用如果该模块在 tab 中并且不是当前 tab
QDockWidget *dockWidget = (QDockWidget *)sender();
emit visibilityChangedFromModule(dockWidget->windowTitle(), dockWidget->isVisible());
}

void frmModule::visibilityChangedFromMain(const QString &title, bool visible)
{
// 当前模块不可见则不用处理
if (!this->isVisible()) {
return;
}

<span class="hljs-comment">//根据传过来的不同菜单标题进行处理</span>
<span class="hljs-keyword">if</span> (title.endsWith(<span class="hljs-string">"当前布局"</span>)) {
    <span class="hljs-comment">//保存当前布局</span>
    this-&gt;saveLayout();
} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (title.endsWith(<span class="hljs-string">"所有模块"</span>)) {
    <span class="hljs-comment">//隐藏所有模块</span>
    foreach (QDockWidget *dockWidget, dockWidgets) {
        dockWidget-&gt;setVisible(visible);
    }
} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (title.endsWith(<span class="hljs-string">"普通布局"</span>)) {
    <span class="hljs-comment">//复位普通布局</span>
    resetLayout(<span class="hljs-literal">false</span>);
} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (title.endsWith(<span class="hljs-string">"全屏布局"</span>)) {
    <span class="hljs-comment">//复位全屏布局</span>
    resetLayout(<span class="hljs-literal">true</span>);
} <span class="hljs-keyword">else</span> {
    <span class="hljs-comment">//找到对应的子模块切换显示隐藏</span>
    foreach (QDockWidget *dockWidget, dockWidgets) {
        <span class="hljs-keyword">if</span> (dockWidget-&gt;windowTitle() == title) {
            dockWidget-&gt;setVisible(visible);
            <span class="hljs-keyword">break</span>;
        }
    }
}

}