忙忙碌碌又是一年,算算自己毕业四年半,一直在现在这家公司做研发外加总经理助理,研发起初用的VB.NET,而后全面转为C#,最后又全面转为QT,都是由于项目需要,算下来自己搞QT编程也已经四年了,2010年开始接触QT并编写一些公司需要的辅助工具,其实搞程序的,我感觉绝大部分都是出于本身兴趣爱好,然后持之以恒的钻研,不断成长和进步。
项目需求:某区下面有几百所学校,每个学校都有若干台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各个版本编译运行。
QT5与QT4的区别还是让很多搞QT开发的同学着实生气了一把,好端端的把一些方法去除掉了,而且有些头文件重新移到了其他地方,为了兼容QT4与QT5,在项目中就需要增加很多对版本的判断了。
例如头文件的包含:
#if (QT_VERSION > QT_VERSION_CHECK(5,0,0))
#include <QtWidgets>
#endif 例如设置UTF-8编码:
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 }
4:基本常用的数据库处理,添加删除修改操作,表格显示。
本人一直喜欢采用拼接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();
}
5: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();
}
6: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[i]);
VideoLab[i]->setVisible(
true);
}
for (
int i = (index +
2); i < (index +
4); i++) {
VideoLay[
1]->addWidget(VideoLab[i]);
VideoLab[i]->setVisible(
true);
}
}
7:精美开关按钮。
现在流行APP,各种APP上面都带有很精美的开关,参考了360安全卫士以及金山毒霸的开关按钮,用QT也实现了一个,原理很简单,就是贴图。
#include
" switchbutton.h " /* 说明:自定义开关按钮控件实现文件 * 功能:用来控制配置文件的开关设置 * 作者:刘典武 QQ:517216493 * 时间:2013-12-19 检查:2014-1-10 */ SwitchButton::SwitchButton(QWidget *parent): QPushButton(parent)
{
setCursor(QCursor(Qt::PointingHandCursor));
isCheck =
false;
styleOn =
" background-image: url(:/image/btncheckon.png); border: 0px; ";
styleOff =
" background-image: url(:/image/btncheckoff.png); border: 0px; ";
setFocusPolicy(Qt::NoFocus);
setFixedSize(
87,
28);
// 不允许变化大小 setStyleSheet(styleOff);
// 设置当前样式 connect(
this, SIGNAL(clicked()),
this, SLOT(ChangeOnOff()));
}
void SwitchButton::ChangeOnOff()
{
if (isCheck) {
setStyleSheet(styleOff);
isCheck =
false;
}
else {
setStyleSheet(styleOn);
isCheck =
true;
}
}
// 设置当前选中状态 void SwitchButton::SetCheck(
bool isCheck)
{
if (
this->isCheck != isCheck) {
this->isCheck = !isCheck;
ChangeOnOff();
}
}
8:重写过的消息框,错误框,询问框及输入框。
本人不喜欢系统的MessageBox,用QDialog重新布局自定义了一个。只需一句话调用即可。
在win7下运行截图如下:
在XP下运行截图如下:
在ubuntu上运行截图:
可执行文件下载:http://pan.baidu.com/s/1hqxhtbA
源码下载:http://pan.baidu.com/s/1mgFWeDU
编译运行后如果提示缺少数据库。将源码下的file文件夹下的配置文件config.txt及VM.db数据库文件复制到bin目录下即可。
说明:公开的源码去除了视频处理部分及样式部分,其余功能全部保留,并可完整编译运行。欢迎提出建议共同学习进步!