用QT陆续写过几个商业项目以及公司内部硬件通信需要的一些辅助设置工具,其中以报警视频联动管理平台为主,从2010年的C#版本开始编写,陆续完善积累到2014年的稳定版,花了相当多的精力去完善,不断查看同行的相关软件功能,吸取优秀的功能,摒弃无用的功能,用易用简洁全完善每一个细节。
今天要分享的是给某区安全管理中心编写的视频监管平台,稍后会放出QT4-QT5各版本可完整编译运行的源码。
按照习惯,首先上一张主界面截图:
XP下运行截图:
Win7下运行截图:
Ubuntu下运行截图:
整个系统在开始架构的时候,本人都是写在草稿纸上的,包括布局,功能点,需要注意的处理等方面,现在要重新一一仔细写出来,还真不容易,这里就说个大概,然后将其中的部分功能处理用代码描述。
项目需求:某区下面有几百所学校,每个学校都有若干台NVR或者DVR,每台NVR和DVR都挂接着N个IPC(摄像机)(包括网络摄像机和模拟摄像机),现在需要对所有学校的监控进行查看以及回放和轮询,能够对指定学校进行视频监控,对所有学校的视重点部位视频进行查看轮询,可自定义轮询时间等。
开发过程:本着尽量追求简洁的要求,最终编写了如上图的主界面。没有采用QT自带的界面,而是重写了界面,自定义无边框拖动,自由换肤,全部采用QSS控制,本人从官网http://qt-project.org/doc/qt-4.8/stylesheet-examples.html彻底学习了下QSS的规则,整理了一套通用的换肤方案。
功能点罗列:
1:只限定一个实例处理。
视频监管平台是一个独占视频通道资源的系统,不能运行多个实例在同一台电脑上运行,所以在main函数中就限制了一个实例运行。
//创建共享内存,判断是否已经运行程序
QSharedMemory mem("VM");
if (!mem.create(1)) {
myHelper::ShowMessageBoxError("程序已运行,软件将自动关闭!");
return 1;
}
其中VM为自定义的名称,return 1表示退出程序返回1给操作系统。
2:F1键进入全屏模式,Esc键退出全屏模式。
几乎所有的视频监控系统,主界面都支持全屏显示及esc退出全屏,在QT中我是这样实现的,重写了主界面的keyPressEvent事件,拦截按键消息,判断对应按键,调用全屏及普通模式的方法。
void frmMain::keyPressEvent(QKeyEvent *event)
{
//空格键进入全屏,esc键退出全屏
switch(event->key()) {
case Qt::Key_F1:
screen_full();
break;
case Qt::Key_Escape:
screen_normal();
break;
default:
QDialog::keyPressEvent(event);
break;
}
}
void frmMain::screen_full()
{
this->setGeometry(qApp->desktop()->geometry());
this->layout()->setContentsMargins(0, 0, 0, 0);
ui->widget_main->layout()->setContentsMargins(0, 0, 0, 0);
ui->widget_title->setVisible(false);
ui->treeMain->setVisible(false);
}
void frmMain::screen_normal()
{
this->setGeometry(qApp->desktop()->availableGeometry());
this->layout()->setContentsMargins(1, 1, 1, 1);
ui->widget_main->layout()->setContentsMargins(5, 5, 5, 5);
ui->widget_title->setVisible(true);
ui->treeMain->setVisible(true);
}
3:支持QT4到QT5各个版本编译运行。
之前放出过N个工具及一个安防报警管理系统的源码,有兴趣的可以点击这里查看:http://www.qtcn.org/bbs/read-htm-tid-56372.html,很多兄弟反应不能在QT5上编译通过,甚是操蛋。本人一直喜欢用QT4.7,两个原因,一个是项目需要,程序主要在ARM上面跑,觉得部分公司提供的硬件开发板核心板都是自带的QT4.7版本的运行库,其他版本编译的程序无法在4.7运行库下运行。一个是QT4.7版本很经典,在windows上打包只需要附带几个DLL即可,打包压缩下一般才七八兆,很小,方便。到现在的QT5.4,我擦,光插件库就需要130MB,我勒个去,这还让用户的电脑怎么活啊!
本项目并木有牵扯到windows.h头文件,连修改系统时间的方法都改用了通用的系统命令,所以只要稍微更改部分地方即可,我代码中用的是:
#if (QT_VERSION > QT_VERSION_CHECK(5,0,0))
#include <QtWidgets>
#endif
QT5以上的版本需要引入头文件QtWidgets,设置UTF8编码在QT5中已经取消了。
//设置编码为UTF8
static void SetUTF8Code() {
#if (QT_VERSION <= QT_VERSION_CHECK(5,0,0))
QTextCodec *codec = QTextCodec::codecForName("UTF-8");
QTextCodec::setCodecForLocale(codec);
QTextCodec::setCodecForCStrings(codec);
QTextCodec::setCodecForTr(codec);
#endif
}
将所有的toAscii()方法全部替换成了toLatin1(),本来QT4就支持toLatin1()方法,作用几乎一样,索性就所有的替换成了toLatin1()方法。
4:精心编写的通用的excelhelper类,独创的不依赖任何office组件的导出数据到表格的处理,通用任何excel版本。
方法:
void ToExcel(QString fileName, QString sheetName,
QString title, QString columnNames[],
int columnWidths[], int columnCount, QStringList content);
具体实现可以下载源码下来看。
5:7套精美样式换肤,整体界面换肤。
这个花了本人N多精力去一个个调颜色,一个个控件写对应样式,所以在源码中并不包含,
在主界面的右上角做了个小箭头用来更换样式。
void frmMain::change_style()
{
QAction *action = (QAction *)sender();
QString style = action->text();
if (style == "淡蓝色") {
myApp::AppStyle = ":/image/blue.css";
} else if (style == "蓝色") {
myApp::AppStyle = ":/image/dev.css";
} else if (style == "灰色") {
myApp::AppStyle = ":/image/gray.css";
} else if (style == "黑色") {
myApp::AppStyle = ":/image/black.css";
} else if (style == "灰黑色") {
myApp::AppStyle = ":/image/brown.css";
} else if (style == "白色") {
myApp::AppStyle = ":/image/white.css";
} else if (style == "银色") {
myApp::AppStyle = ":/image/silvery.css";
}
myHelper::SetStyle(myApp::AppStyle);
myApp::WriteConfig();
}
6:自带中文翻译文件。
鼠标右键菜单,默认QT是英文的,客户看了很恼火,需要自己将中文翻译文件加入资源文件编译到可执行文件中。
//设置为中文字符
static void SetChinese() {
QTranslator *translator = new QTranslator(qApp);
translator->load(":/image/qt_zh_CN.qm");
qApp->installTranslator(translator);
}
7:基本常用的数据库处理,添加删除修改操作,表格显示。
本人一直喜欢采用拼接sql字符串来执行SQL语句。觉得这样运行效率很高,而且这种方法通用任何编程语言。
void frmIPC::on_btnAdd_clicked()
{
QString IPCID = ui->cboxIPCID->currentText();
QString IPCName = ui->txtIPCName->text();
QString NVRID = ui->cboxNVRID->currentText();
QString NVRName = ui->cboxNVRName->currentText();
QString IPCType = ui->cboxIPCType->currentText();
QString IPCRtspAddrMain = ui->txtIPCRtspAddrMain->text();
QString IPCRtspAddrSub = ui->txtIPCRtspAddrSub->text();
QString IPCUserName = ui->txtIPCUserName->text();
QString IPCUserPwd = ui->txtIPCUserPwd->text();
QString IPCUse = ui->cboxIPCUse->currentText();
if (IPCName == "") {
myHelper::ShowMessageBoxError("名称不能为空,请重新填写!");
ui->txtIPCName->setFocus();
return;
}
if (NVRName == "") {
myHelper::ShowMessageBoxError("NVR名称不能为空,请先添加好NVR!");
return;
}
if (IPCRtspAddrMain == "") {
myHelper::ShowMessageBoxError("主码流地址不能为空,请重新填写!");
ui->txtIPCRtspAddrMain->setFocus();
return;
}
if (IPCRtspAddrSub == "") {
myHelper::ShowMessageBoxError("子码流地址不能为空,请重新填写!");
ui->txtIPCRtspAddrSub->setFocus();
return;
}
//检测编号是否唯一
if (IsExistIPCID(NVRID, IPCID)) {
myHelper::ShowMessageBoxError("编号已经存在,请重新选择!");
return;
}
QSqlQuery query;
QString sql = "insert into [IPCInfo](";
sql += "[IPCID],[IPCName],[NVRID],[NVRName],";
sql += "[IPCType],[IPCRtspAddrMain],[IPCRtspAddrSub],";
sql += "[IPCUserName],[IPCUserPwd],[IPCUse])";
sql += "values('";
sql += IPCID + "','";
sql += IPCName + "','";
sql += NVRID + "','";
sql += NVRName + "','";
sql += IPCType + "','";
sql += IPCRtspAddrMain + "','";
sql += IPCRtspAddrSub + "','";
sql += IPCUserName + "','";
sql += IPCUserPwd + "','";
sql += IPCUse + "')";
query.exec(sql);
LoadIPCInfo();
ui->cboxIPCID->setCurrentIndex(ui->cboxIPCID->currentIndex() + 1);
ui->txtIPCName->setText(QString("摄像机%1").arg(ui->cboxIPCID->currentText()));
}
void frmIPC::on_btnDelete_clicked()
{
if (ui->tableMain->currentIndex().row() < 0) {
myHelper::ShowMessageBoxError("请选择要删除的摄像机!");
return;
}
QString tempIPCID = queryModule->record(
ui->tableMain->currentIndex().row())
.value(0).toString();
if (myHelper::ShowMessageBoxQuesion("确定要删除摄像机吗?") == 1) {
QSqlQuery query;
QString sql = "delete from [IPCInfo] where [IPCID]='" + tempIPCID + "'";
query.exec(sql);
myHelper::Sleep(100);
//同步删除轮询表中的摄像机信息
sql = "delete from [PollInfo] where [IPCID]='" + tempIPCID + "'";
query.exec(sql);
myHelper::Sleep(100);
LoadIPCInfo();
}
}
void frmIPC::on_btnUpdate_clicked()
{
if (ui->tableMain->currentIndex().row() < 0) {
myHelper::ShowMessageBoxError("请选择要修改的摄像机!");
return;
}
QString tempIPCID = queryModule->record(
ui->tableMain->currentIndex().row())
.value(0).toString();
QString IPCID = ui->cboxIPCID->currentText();
QString IPCName = ui->txtIPCName->text();
QString NVRID = ui->cboxNVRID->currentText();
QString NVRName = ui->cboxNVRName->currentText();
QString IPCType = ui->cboxIPCType->currentText();
QString IPCRtspAddrMain = ui->txtIPCRtspAddrMain->text();
QString IPCRtspAddrSub = ui->txtIPCRtspAddrSub->text();
QString IPCUserName = ui->txtIPCUserName->text();
QString IPCUserPwd = ui->txtIPCUserPwd->text();
QString IPCUse = ui->cboxIPCUse->currentText();
if (IPCID != tempIPCID) {
//检测编号是否和已经存在的除自己之外的编号相同
if (IsExistIPCID(NVRID, IPCID)) {
myHelper::ShowMessageBoxError("编号已经存在,请重新选择!");
return;
}
}
QSqlQuery query;
QString sql = "update [IPCInfo] set";
sql += " [IPCID]='" + IPCID;
sql += "',[IPCName]='" + IPCName;
sql += "',[NVRID]='" + NVRID;
sql += "',[NVRName]='" + NVRName;
sql += "',[IPCType]='" + IPCType;
sql += "',[IPCRtspAddrMain]='" + IPCRtspAddrMain;
sql += "',[IPCRtspAddrSub]='" + IPCRtspAddrSub;
sql += "',[IPCUserName]='" + IPCUserName;
sql += "',[IPCUserPwd]='" + IPCUserPwd;
sql += "',[IPCUse]='" + IPCUse;
sql += "' where [IPCID]='" + tempIPCID + "'";
query.exec(sql);
myHelper::Sleep(100);
//同步修改轮询表的信息
sql = "update [PollInfo] set";
sql += " [IPCID]='" + IPCID;
sql += "',[IPCName]='" + IPCName;
sql += "',[NVRID]='" + NVRID;
sql += "',[NVRName]='" + NVRName;
sql += "',[IPCRtspAddrMain]='" + IPCRtspAddrMain;
sql += "',[IPCRtspAddrSub]='" + IPCRtspAddrSub;
sql += "' where [IPCID]='" + tempIPCID + "'";
query.exec(sql);
myHelper::Sleep(100);
LoadIPCInfo();
}
8:QTreeView及QTableView数据加载和双击处理。
如下图:
void frmPollConfig::LoadNVRIPC()
{
ui->treeMain->clear();
QSqlQuery queryNVR;
QString sqlNVR = "select [NVRID],[NVRName],[NVRIP] from [NVRInfo] where [NVRUse]='启用'";
queryNVR.exec(sqlNVR);
while (queryNVR.next()) {
QString tempNVRID = queryNVR.value(0).toString();
QString tempNVRName = queryNVR.value(1).toString();
QString tempNVRIP = queryNVR.value(2).toString();
QTreeWidgetItem *itemNVR = new QTreeWidgetItem
(ui->treeMain, QStringList(tempNVRName + "[" + tempNVRIP + "]"));
itemNVR->setIcon(0, QIcon(":/image/nvr.png"));
//查询没有添加在轮询表中的摄像机信息
QSqlQuery queryIPC;
QString sqlIPC = "select [IPCID],[IPCName],[IPCRtspAddrMain] from [IPCInfo]";
sqlIPC += " where [NVRID]='" + tempNVRID;
sqlIPC += "' and [IPCUse]='启用'";
sqlIPC += " order by [IPCID] asc";
queryIPC.exec(sqlIPC);
while (queryIPC.next()) {
QString tempIPCID = queryIPC.value(0).toString();
//如果该摄像机已经存在轮询表,则跳过
if (IsExistIPCID(tempIPCID)) {
continue;
}
QString tempIPCName = queryIPC.value(1).toString();
QString rtspAddr = queryIPC.value(2).toString();
QStringList temp = rtspAddr.split("/");
QString ip = temp[2].split(":")[0];
temp = QStringList(QString(tempIPCName + "[" + ip + "](" + tempIPCID + ")"));
QTreeWidgetItem *itemIPC = new QTreeWidgetItem(itemNVR, temp);
itemIPC->setIcon(0, QIcon(":/image/ipc_normal.png"));
itemNVR->addChild(itemIPC);
}
}
ui->treeMain->expandAll();
}
9:16通道画面展示区域处理,自由切换1画面4画面9画面16画面。
这个功能看似简单,确花费我一个星期的时间完善。
void frmMain::show_video_4()
{
removelayout();
video_max = false;
int index = 0;
QAction *action = (QAction *)sender();
QString name = action->text();
if (name == "通道1-通道4") {
index = 0;
myApp::VideoType = "1_4";
} else if (name == "通道5-通道8") {
index = 4;
myApp::VideoType = "5_8";
} else if (name == "通道9-通道12") {
index = 8;
myApp::VideoType = "9_12";
} else if (name == "通道13-通道16") {
index = 12;
myApp::VideoType = "13_16";
}
change_video_4(index);
myApp::WriteConfig();
}
void frmMain::change_video_4(int index)
{
for (int i = (index + 0); i < (index + 2); i++) {
VideoLay[0]->addWidget(VideoLab);