软件自动更新解决放案及QT实现...1
1 文件的版本控制-XML.2
2 更新程序的实现...2
2.1 界面设置...2
2.2 程序功能...3
2.2.1 下载网络数据...3
2.2.2 XML文件的分析...6
2.2.3 下载XML文件的DownLoadXML函数...8
2.2.4 返回指定XML文件中的name版本号...8
2.2.5 返回指定XML文件中的name版本号...10
2.2.6 比较两个XML文件CheckUpdateFiles.11
2.2.7 下载文件DownLoadUpdateFiles.13
2.2.7 退出当前程序,并启动指定的程序...16
3 更新程序的启动...16
3.1 从主程序启动更新程序...16
3.2 主程序的关闭...17
3.3 更新程序关闭时启动主程序...17
4 程序调试...17
软件自动更新解决放案及QT实现
需要考虑解决的问题:
1) 需要知道哪些文件需要更新,哪些不需要;
2) 从哪里下载更新文件;
3) 将旧的文件用新的文件替换掉(包含版本控制文件);
4) 更新完毕后重新启动主程序;
第一个问题,可以为程序所使用的文件都设定一个版本号,版本号都记录在一个 XML 文件中,升级时,检查最新程序的版本控制文件和当前的版本控制文件,最新版本号较大时,表示该文件需要更新。如果一个文件不再需要了,则将该文件的版本信息从最新的版本控制文件中删除,通过对比控制文件,就知道该文件不再需要了,可以将之删除;
第二个问题,最新的版本控制文件需要放在一个可供方便下载的地方,例如FTP或者一个固定的IP;
第三个问题,通过对比新的版本控制文件和旧的版本控制文件来确定哪些需要替换或者删除;
第四个问题,更新程序运行完后,启动主程序即可,需要一个标识来说明是否更新完成;
按照以上的思路,下面对每一个步骤结合程序进行详细地阐述,软件使用QT5.1实现。
下面是使用XML文件来表示的版本控制文件:
…..
Name表示文件的名称;
dir表示所在的目录(相对目录);
version表示当前文件的版本;
更新程序使用QT5.1来实现,最终生成一个可执行文件(exe文件)。新建工程时选择QT GUI应用,其他都默认,工程名为Updater。
在构造函数中设置界面。
应用程序在屏幕中间:
QDesktopWidget *deskdop = QApplication::desktop();
this->move((deskdop->width() -this->width())/2, (deskdop->height() - this->height())/2);
无标题栏:
this->setWindowFlags(Qt::FramelessWindowHint);//没有标题栏
隐藏菜单栏和工具栏
this->ui->menuBar->hide();
this->ui->mainToolBar->hide();
固定高和宽:
this->setFixedSize(400,200);
设置背景颜色(两种方法都可以)
//this->setStyleSheet("QMainWindow{background:rgb(240,250,250)}");
QPalette pal;
pal.setColor(QPalette::Background,QColor(255,245,225) );
this->setPalette(pal);
this->setAutoFillBackground(true);
程序功能包括从网络下载数据,分析XML文件,比较当前的XML及下载的XML文件,并最终确定哪些文件需要更新或者添加
新建类CHttpDownLoadFile,类功能:从指定网络中下载指定的文件,并且存储到指定的本地文件目录中。
主要的成员函数及槽函数:
public slots:
void ReplyNewDataArrived();//响应m_netReply有新的数据到达
void ReplyFinished();//响应数据接收完成
public:
QNetworkAccessManager *m_netAccessManager;//网络参数
QNetworkReply *m_netReply;
QUrl m_urlAdress;//网络地址
QString m_strFileName;//需要下载的文件名
QString m_strDir;//文件的存储位置
QFile *m_file;//下载的文件
qint64m_nReceived;//下载文件时,已经接收的文件大小和总共大小
qint64m_nTotal;
主要函数及功能:
a.构造函数
CHttpDownloadFile(QString url,QStringfileName,QString dir,QObject *parent = 0);
url表示文件的网络地址;
filename表示文件名;
dir表示文件存储路径
如果fileName不为空,那么文件名使用fileName,否则从url提取(注:不需要加后缀)。如果dir不为空,那么将文件存储到dir指向的文件夹中,否则存储在默认路径中(即与可执行文件在同一个文件夹中),如果文件夹不存在,那么会创建,dir举例:c:/temp/,或者c:/temp,如果前面不加盘符,那么将会在默认文件夹中创建。
b. 开始下载文件的函数:void DownLoadFile()
m_netReply=m_netAccessManager->get(QNetworkRequest(m_urlAdress));
connect(m_netReply,SIGNAL(readyRead()),
this,SLOT( ReplyNewDataArrived()) );//当有新数据到达时就会触发此信号
connect(m_netReply, SIGNAL(finished()),
this,SLOT( ReplyFinished()) );//完成数据接收后发送此信号
connect(m_netReply, SIGNAL(error(QNetworkReply::NetworkError)),this, SLOT( ReplyError(QNetworkReply::NetworkError)) );//出现错误时发送此信号;
connect(m_netReply, SIGNAL(downloadProgress(qint64,qint64)), this, SLOT(ReplyDownloadProgress(qint64,qint64) ) );//用来提示文件下载进度
/*************存储文件的检测及使用************/
if( m_strFileName.isEmpty() )//文件名
{
QFileInfo fileInfo(m_urlAdress.path());
m_strFileName = fileInfo.fileName();
}
if( !m_strDir.isEmpty() )//文件夹
{
QDir directory( m_strDir );
if( !directory.exists() )//没有此文件夹,则创建
{
directory.mkpath( m_strDir );
}
m_strFileName = m_strDir + "/"+m_strFileName;//添加/是为了防止用户名没有加/,因为对于文件夹来说两个/都会当成一个/
}
if( QFile::exists(m_strFileName) )//如果文件已经存在,那么删除
{
QFile::remove(m_strFileName);
}
m_file = new QFile( m_strFileName );
if (!m_file->open(QIODevice::WriteOnly))
{
qDebug()<<"不能存储文件:"< delete m_file; m_file = NULL; return; } c.槽函数void ReplyFinished(),当下载网络数据结束时响应此函数,主要是释放资源 m_netAccessManager->deleteLater(); m_netReply->deleteLater(); m_file->close(); m_file->deleteLater(); QFile file(filename); if(!file.open(QIODevice::ReadOnly | QFile::Text)) { qDebug()<<"open for read error..." ; } QString errorStr; int errorLine; int errorColumn; QDomDocument doc; if(!doc.setContent(&file, false, &errorStr, &errorLine,&errorColumn)) { qDebug()<<"setcontent error..." ; file.close(); } file.close(); QDomElement root =doc.documentElement(); if (root.tagName() !="filelist") { qDebug()<<"root.tagname != filelist.." ; } else { QDomNodeListnodeList = root.elementsByTagName("file"); for(inti=0;i { qDebug()< < < } } } 这里只是将XML文件的内容输出,详细的代码在工程Updater的其他函数中。 函数下载XML文件,当XML文件下载完成时发送下载完成信号,提示进行下一步操作。 /**从网页下载XML版本控制文件,里面记录了最新的文件版本**/ QStringstrCurrentDir=QDir::currentPath();//当前程序运行路径 QStringstrDownLoad=strCurrentDir+"/download/";//存放下载文件的路径 QDirdirectory(strDownLoad);//如果路径不存在,则创建 if(!directory.exists()) { directory.mkpath(strDownLoad); } m_httpXML=newCHttpDownloadFile("http://www.***.com/download/**.xml","",strDownLoad,this);//调用下载文件的类 connect(m_httpXML,SIGNAL(DownloadFinishedSignal()),this,SLOT(ReplyHttpFinished()));//发生错误时一样会发送此信号 m_httpXML->DownLoadFile(); 在ReplyHttpFinished()函数中需要错误处理。 实例化m_httpXML时,需要将parent设置为this,这样当程序结束时,会自动释放。 /***********在xml中查找名字与name相同的元素,并返回版本号** *xml:xml文件的路径; *name:需要查找的元素名称; * *return:QString:版本号,如果有则返回,没有则为空 ************************************************/ QStringGetElementVersion(QStringxml,QStringname); 实现: GetElementVersion(QStringxml,QStringname) { QStringresult=""; if(xml.isEmpty()||name.isEmpty()) { qDebug()<<"名称或者xml文件路径为空"; returnresult; } if(!QFile::exists(xml)) { qDebug()<<"xml文件不存在"; returnresult; } QFilefile(xml); if(file.open(QIODevice::ReadOnly|QFile::Text))//文件打开成功 { QDomDocumentdoc; if(doc.setContent(&file)) { QDomElementroot=doc.documentElement(); if(root.tagName()=="filelist") { inti=0; QDomNodeListnodeList=root.elementsByTagName("file"); for(;i { QStringtempName =nodeList.at(i).toElement().attribute("name"); //QStringdir =nodeList.at(i).toElement().attribute("dir"); QStringversion=nodeList.at(i).toElement().attribute("version"); if(name==tempName) { qDebug()<<"find!"< result=version; break; } } if(i==nodeList.size()) { qDebug()<<"can'tfind!"< } } else { qDebug()<<"root.tagname!=filelist.."; } } else { qDebug()<<"setcontenterror..."; } file.close(); } else { qDebug()<<"openforreaderror..."; } returnresult; } /**************比较两个版本号******************* *@params: * v1,v2:两个版本号,格式:1.1.0,不能为空 * *@return: * true:如果两个版本号码相同; * false:两个版本号不同 * *说明:暂时只比较了是否相同 *******************************************/ boolCheckVersion(QStringv1,QStringv2); 实现比较简单,就不贴代码了。 通过比较两个XML文件来确定需要下载的文件。 /************比较两个XML文件**************** *@params: * name1,name2:两个XML文件,不能为空,并且文件需要存在,name1必须是最新的xml文件(刚刚下载下来的),name2是本地的xml文件; * *@return: *0-someerrorhappens,elsesuccess *将需要更新的文件名存储到m_listFileName中 *路径存储到m_listFileDir中 * *注意:m_listFileDir和m_listFileName的个数是一样的, * 如果没有元素表示所有的文件都不用更新 ******************************************/ intCheckUpdateFiles(QStringname1,QStringname2); 实现: CheckUpdateFiles(QStringname1,QStringname2) { m_listFileDir.clear(); m_listFileName.clear(); if(name1.isEmpty()||name2.isEmpty())return0; if(QFile::exists(name2)) { if(QFile::exists(name1)) { m_strTip="检查需要更新的文件..."; QFilefile(name1); if(file.open(QIODevice::ReadOnly|QFile::Text))//文件打开成功 { QStringerrorStr; interrorLine; interrorColumn; QDomDocumentdoc; if(doc.setContent(&file,false,&errorStr,&errorLine,&errorColumn)) { QDomElementroot=doc.documentElement(); if(root.tagName()=="filelist") { QDomNodeListnodeList=root.elementsByTagName("file"); for(inti=0;i { QStringname =nodeList.at(i).toElement().attribute("name"); QStringdir =nodeList.at(i).toElement().attribute("dir"); QStringversion=nodeList.at(i).toElement().attribute("version"); QStringversionDownload=GetElementVersion(name2,name);//获取本地xml文件对应文件(name)的版本信息 if(versionDownload.isEmpty())//本地XML没有此文件:下载,并放到相应的目录中 { m_listFileDir.append(dir); m_listFileName.append(name); } else { /**检查版本,如果本地版本低于下载的版本,则下载**/ if(!CheckVersion(version,versionDownload)) { m_listFileDir.append(dir); m_listFileName.append(name); } else { qDebug()< } } } return1;//此时要退出,避免关闭程序 } else { m_strTip="XML内容错误!"; return0; } } else { qDebug()<<"setcontenterror..."; return0; } file.close(); } else { m_strTip="不能打开更新文件!"; return0; } } else { m_strTip="下载更新文件错误!"; return0; } } else { m_strTip="本地的更新文件不存在!"; return0; } } 下载需要的文件,完成后启动主程序 。 /********下载最新的版本的文件,并替换或者增加********************* *旧XML中有的文件,新XML没有的,此文件不做处理。 *下载信息由m_listFileName和m_listFileDir提供 *****************************************************/ voidDownLoadUpdateFiles(); 实现: DownLoadUpdateFiles() { QStringstrServer="http://www.***.com/download/";//需要下载的文件存储位置 QStringstrCurrentDir=QDir::currentPath();//当前程序运行路径 if(m_listFileDir.isEmpty()||m_listFileDir.isEmpty()) { qDebug()<<"没有需要下载的文件1"; ExitApp(strCurrentDir+"/main.exe"); return; } m_strTip="开始下载更新文件..."; m_bIsFinished=false; for(inti=0;i<m_listFileName.size();i++) { m_strTip="正在下载文件"+m_listFileName.at(i); m_progUpdate->setValue(100*i/m_listFileName.size()); /**放置下载的文件的路径**/ QStringstrPlaceDir=strCurrentDir+"/download/"+m_listFileDir.at(i); QDirdirectory(strPlaceDir);//如果路径不存在,则创建 if(!directory.exists())directory.mkpath(strPlaceDir); QStringstrFileDirServer=strServer+m_listFileDir.at(i)+"/"+m_listFileName.at(i);//文件在服务器中的存储位置 CHttpDownloadFile*http=newCHttpDownloadFile(strFileDirServer,"",strPlaceDir,this);//调用下载文件的类 http->DownLoadFile(); while(!http->m_bIsFinished) { if(http->m_nTotal==-1) { m_progDownload->setValue(1); } else { m_progDownload->setValue(100*http->m_nReceived/http->m_nTotal); } QCoreApplication::processEvents(); } m_strTip="文件"+m_listFileName.at(i)+"下载完成"; /**将下载好的文件复制到主目录中,先删除原先的文件**/ QStringstrLocalFileName=strCurrentDir+"/"+m_listFileDir.at(i)+"/"+m_listFileName.at(i); if(QFile::exists(strLocalFileName))QFile::remove(strLocalFileName); QDirdirectory1(strCurrentDir+"/"+m_listFileDir.at(i));//如果路径不存在,则创建 if(!directory1.exists())directory1.mkpath(strCurrentDir+"/"+m_listFileDir.at(i)); QFile::copy(strPlaceDir+"/"+m_listFileName.at(i),strLocalFileName); } m_bIsFinished=true; m_strTip="更新完成!"; /**替换旧的xml文件**/ QStringstrNewXML=strCurrentDir+"/download/**.xml";//最新的XML文件 QStringstrOldXML=strCurrentDir+"/"+"**.xml";//旧的XML文件 QFile::remove(strOldXML); QFile::copy(strNewXML,strOldXML); ExitApp(strCurrentDir+"/main.exe"); } /********退出当前程序,并且启动需要的程序************************* *name:需要启动的程序,可以使用相对位置 *程序不能使用exit(0),会发生线程错误,这里使用this->close()函数 *****************************************************/ 实现: ExitApp(QStringname) { if(!name.isEmpty()) { /**运行主程序,并且退出当前更新程序(说明:主程序在上上一级目录中)**/ if(!QProcess::startDetached(name))//启动主程序,主程序在其上一级目录 { QMessageBox::warning(this,"警告信息","启动主程序错误!\n可能主程序不存在或者被破坏!\n解决办法:重新安装程序!"); } } this->close(); } 更新程序是独立的可执行文件,所以在启动主程序时,首先启动更新程序(注:网络检查放在了更新程序中),然后关闭主程序,更新完成后,再重新启动主程序。 首先需要判断是否需要启动更新程序,避免更新后再次启动更新程序; 将是否需要启动更新程序的参数放在软件的初始参数文件中,例如params.txt文件中,初始值为true,表示需要启动更新程序。主程序启动时,先获取里面的参数,并判断是否需要启动更新程序。完成更新后,将此参数设置为false,表示下次再次启动主程序时不需更新,当点击关闭主程序时再次将此参数设置为true,这样就可以保证每次启动时都会检查更新; 启动更新程序调用QProcess::startDetached()方法,启动外部程序后立即返回(也就是主程序也运行),即使主程序关闭,启动的程序也会运行,本程序使用这个方法,因为更新后。 注:如果调用QProcess::execute函数,此方法可以阻塞主程序的运行,直到启动的程序关闭,因为需要关闭主程序,还要重新启动更新后得程序,所以不能使用这个方法。 如果QProcess::startDetached方法返回true,则关闭主程序,调用exit(0),或者quit()方法; 更新程序结束时调用QProcess::startDetached("main.exe"),主程序和更新程序在同一目录。但是在关闭更新程序的时候使用exit会发送错误,错误原因:会出现夸线程发送信号的情况,这是不容许的。 当所有文件都下载完成时,使用close函数。 需要调试以下功能: (1)主程序启动时,检测是否需要启动更新程序, 检测params.cq文件里面的is_update参数的值,如果为1则启动,否则不启动(代码在CLoginBox的构造函数中); (2)如果is_update=1,那么启动更新程序,更新程序启动后,is_update设置为0; (3)主程序退出时,将is_update设置为1,即每次重新启动主程序的时候都需要检查更新; (4)正确下载xml文件,存储位置:与主程序同目录下的download文件 夹中(特别提醒:由于更新程序是从主程序中调用的,所以在更新程序中调用获取当前运行程序的目录是得到的是主程序的的目录,但是如果更新程序单独运行的话,得到的目录是和更新程序目录一样的,所以最好的方式就是讲主程序和更新程序放在同一个目录中); (5)是否正确调用了更新程序里面的CheckUpdateFiles函数,主要是检查是否输入正确的xml文件目录; (6)下载的更新文件是否正确放置; (7)下载完后旧的XML是否换成新的XML; (8)运行主程序的电脑或者服务端没有网络的情况;d.槽函数voidReplyNewDataArrived()—当数据到达时调用此函数,并存储到指定的文件中
if(m_file)
{
m_file->write(m_netReply->readAll());
m_file->flush();//注意需要刷新
}
else
{
qDebug()<<m_netReply->readAll();
}
2.2.2 XML文件的分析
2.2.3 下载XML文件的DownLoadXML函数
2.2.4 返回指定XML文件中的name版本号
2.2.5 返回指定XML文件中的name版本号
2.2.6 比较两个XML文件CheckUpdateFiles
2.2.7 下载文件DownLoadUpdateFiles
2.2.7 退出当前程序,并启动指定的程序
3 更新程序的启动
3.1 从主程序启动更新程序
3.2 主程序的关闭
3.3 更新程序关闭时启动主程序
4 程序调试