忙忙碌碌又是一年,算算自己畢業四年半,一直在現在這家公司做研發外加總經理助理,研發起初用的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的規則,整理了一套通用的換膚方案。
PS:這是6年前寫的項目,新版在這,不開源 https://www.cnblogs.com/feiyangqingyun/p/10913375.html
整個系統在開始架構的時候,本人都是寫在草稿紙上的,包括布局,功能點,需要注意的處理等方面,現在要重新一一仔細寫出來,還真不容易,這里就說個大概,然后將其中的部分功能處理用代碼描述。
功能點羅列:
1:只限定一個實例處理。
視頻監管平台是一個獨占視頻通道資源的系統,不能運行多個實例在同一台電腦上運行,所以在main函數中就限制了一個實例運行。
if (!mem.create( 1)) {
myHelper::ShowMessageBoxError( " 程序已運行,軟件將自動關閉! ");
return 1;
}
其中VM為自定義的名稱,return 1表示退出程序返回1給操作系統。
如果重復運行會彈出如下提示:
2:F1鍵進入全屏模式,Esc鍵退出全屏模式。
幾乎所有的視頻監控系統,主界面都支持全屏顯示及esc退出全屏,在QT中我是這樣實現的,重寫了主界面的keyPressEvent事件,攔截按鍵消息,判斷對應按鍵,調用全屏及普通模式的方法。
{
// 空格鍵進入全屏,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,在項目中就需要增加很多對版本的判斷了。
例如頭文件的包含:
#include <QtWidgets>
#endif
例如設置UTF-8編碼:
#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語句。覺得這樣運行效率很高,而且這種方法通用任何編程語言。
{
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數據加載和雙擊處理。
{
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畫面。
{
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也實現了一個,原理很簡單,就是貼圖。
/* 說明:自定義開關按鈕控件實現文件
* 功能:用來控制配置文件的開關設置
* 作者:劉典武 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目錄下即可。
說明:公開的源碼去除了視頻處理部分及樣式部分,其余功能全部保留,並可完整編譯運行。歡迎提出建議共同學習進步!