也可以参照本博主另外一篇:Qt 实现软件自动更新
一、自动更新程序流程
- 加载本地配置文件获取获取保存的版本号
- 每次启动程序,首先从服务器请求最新的版本信息文件(包括版本号,压缩包下载地址,更新时间,更新说明)
- 解压下载的文件获取服务器版本号,与本地版本号进行比较,如果高于本地,就下载压缩文件
- 解压缩程序压缩包
- 更新本地版本号,启动压缩包程序,退出当前自动更新程序
二、代码实现
1. 加载配置文件(采用TinyXml解析xml文件)
TinyXml详细用法可参考:C++库(TinyXml)的安装和使用
XMl 文件内容
<?xml version="1.0" encoding="utf-8" ?>
<AutoUpdate>
<Version>V4.0.0.2</Version>
<Url>https://eval.zhihuiyunji.com/riskclient_update/</Url>
<WindowIcon>:/AutoUpdate/Resources/RiskControlClient_24.png</WindowIcon>
<RunExe>\RiskClient\RiskClient.exe</RunExe>
</AutoUpdate>
Configer 头文件
#pragma once
#include "tinyxml/tinyxml.h"
#include <iostream>
#include <string>
#include <memory>
#include <vector>
using namespace std;
class StaticClass
{
private:
StaticClass();
StaticClass(const StaticClass&);
StaticClass& operator = (const StaticClass&);
};
class Configer : StaticClass
{
public:
~Configer() {};
static bool Load();
static string FilePath();
static string FilePath_Cfg();
static bool LoadConfigFile();
static bool SaveVersion(const char *version);
static const string &GetVersion();
static const string &GetWindowIcon();
static const string &GetQurl();
static const string &GetRunExe();
private:
static string m_strVersion;
static string m_strIcon;
static string m_ptrQurl;
static string m_ptrRunExe;
};
cpp 文件
#include "configer.h"
#include "windows.h"
string Configer::m_strVersion;
string Configer::m_strIcon;
string Configer::m_ptrQurl;
string Configer::m_ptrRunExe;
string Configer::FilePath()
{
string strReturn;
HMODULE hModule = GetModuleHandle(NULL);
char pFileName[MAX_PATH] = { 0 };
::GetModuleFileNameA(hModule, pFileName, MAX_PATH);
string strModuleFullPath = pFileName;
size_t npos = strModuleFullPath.rfind("\\", strModuleFullPath.length());
if (npos != string::npos)
{
strReturn = strModuleFullPath.substr(0, npos);
}
return strReturn;
}
string Configer::FilePath_Cfg()
{
return FilePath() + "\\cfg.xml";
}
bool Configer::Load()
{
return LoadConfigFile();
}
bool Configer::LoadConfigFile()
{
std::shared_ptr<TiXmlDocument> doc_(new TiXmlDocument);
if (!doc_->LoadFile(FilePath_Cfg().c_str()))
{
std::cout << doc_->ErrorDesc() << std::endl;
return false;
}
TiXmlElement *root = doc_->FirstChildElement(); //获取根节点元素
if (root == NULL)
{
std::cerr << "Failed to load file:No curversion element." << std::endl;
doc_->Clear();
return false;
}
TiXmlElement *ptrVersion = root->FirstChildElement("Version");
if (ptrVersion != nullptr)
{
m_strVersion = ptrVersion->GetText();
}
else
{
std::cerr << "Load version error" << endl;
return false;
}
TiXmlElement *ptrUrl = root->FirstChildElement("Url");
if (ptrUrl != nullptr)
{
m_ptrQurl = ptrUrl->GetText();
}
else
{
std::cerr << "Load Qurl error" << endl;
return false;
}
TiXmlElement *ptrIcon = root->FirstChildElement("WindowIcon");
if (ptrIcon != nullptr)
{
m_strIcon = ptrIcon->GetText();
}
else
{
std::cerr << "Load WindowIcon error" << endl;
return false;
}
TiXmlElement *ptrRunExe = root->FirstChildElement("RunExe");
if (ptrRunExe != nullptr)
{
m_ptrRunExe = ptrRunExe->GetText();
}
else
{
std::cerr << "Load RunExe error" << endl;
return false;
}
return true;
}
bool Configer::SaveVersion(const char *version)
{
std::shared_ptr<TiXmlDocument> doc_(new TiXmlDocument);
if (!doc_->LoadFile(FilePath_Cfg().c_str()))
{
std::cout << doc_->ErrorDesc() << std::endl;
return false;
}
TiXmlElement *root = doc_->FirstChildElement(); //获取根节点元素
if (root == NULL)
{
std::cerr << "Failed to load file:No curversion element." << std::endl;
doc_->Clear();
return false;
}
TiXmlElement *ptrVersion = root->FirstChildElement("Version");
if (ptrVersion != nullptr)
{
TiXmlNode *ptrText = ptrVersion->FirstChild();
if (ptrText != nullptr)
{
ptrText->SetValue(version); //设置节点文本值
}
}
else
{
std::cerr << "Set Value false" << endl;
return false;
}
doc_->SaveFile(FilePath_Cfg().c_str());
return true;
}
const string &Configer::GetVersion()
{
return m_strVersion;
}
const string &Configer::GetWindowIcon()
{
return m_strIcon;
}
const string &Configer::GetQurl()
{
return m_ptrQurl;
}
const string &Configer::GetRunExe()
{
return m_ptrRunExe;
}
2.计算下载文件速度,剩余时间相关
参考文件:Qt 之 HTTP 上传/下载
#include "Format.h"
#include <QString>
// 字节转KB、MB、GB
QString formatSize(qint64 bytes)
{
QString strUnit;
double dSize = bytes * 1.0;
if (dSize <= 0)
{
dSize = 0.0;
}
else if (dSize < 1024)
{
strUnit = "Bytes";
}
else if (dSize < 1024 * 1024)
{
dSize /= 1024;
strUnit = "KB";
}
else if (dSize < 1024 * 1024 * 1024)
{
dSize /= (1024 * 1024);
strUnit = "MB";
}
else
{
dSize /= (1024 * 1024 * 1024);
strUnit = "GB";
}
return QString("%1 %2").arg(QString::number(dSize, 'f', 2)).arg(strUnit);
}
// 速度转KB/S、MB/S、GB/S
QString speed(double speed)
{
QString strUnit;
if (speed <= 0)
{
speed = 0;
strUnit = "Bytes/s";
}
else if (speed < 1024)
{
strUnit = "Bytes/s";
}
else if (speed < 1024 * 1024)
{
speed /= 1024;
strUnit = "KB/s";
}
else if (speed < 1024 * 1024 * 1024)
{
speed /= (1024 * 1024);
strUnit = "MB/s";
}
else
{
speed /= (1024 * 1024 * 1024);
strUnit = "GB/s";
}
QString strSpeed = QString::number(speed, 'f', 2);
return QString("%1 %2").arg(strSpeed).arg(strUnit);
}
// 秒转*d *h *m *s
QString timeFormat(int seconds)
{
QString strValue;
QString strSpacing(" ");
if (seconds <= 0)
{
strValue = QString("%1s").arg(0);
}
else if (seconds < 60)
{
strValue = QString("%1s").arg(seconds);
}
else if (seconds < 60 * 60)
{
int nMinute = seconds / 60;
int nSecond = seconds - nMinute * 60;
strValue = QString("%1m").arg(nMinute);
if (nSecond > 0)
strValue += strSpacing + QString("%1s").arg(nSecond);
}
else if (seconds < 60 * 60 * 24)
{
int nHour = seconds / (60 * 60);
int nMinute = (seconds - nHour * 60 * 60) / 60;
int nSecond = seconds - nHour * 60 * 60 - nMinute * 60;
strValue = QString("%1h").arg(nHour);
if (nMinute > 0)
strValue += strSpacing + QString("%1m").arg(nMinute);
if (nSecond > 0)
strValue += strSpacing + QString("%1s").arg(nSecond);
}
else
{
int nDay = seconds / (60 * 60 * 24);
int nHour = (seconds - nDay * 60 * 60 * 24) / (60 * 60);
int nMinute = (seconds - nDay * 60 * 60 * 24 - nHour * 60 * 60) / 60;
int nSecond = seconds - nDay * 60 * 60 * 24 - nHour * 60 * 60 - nMinute * 60;
strValue = QString("%1d").arg(nDay);
if (nHour > 0)
strValue += strSpacing + QString("%1h").arg(nHour);
if (nMinute > 0)
strValue += strSpacing + QString("%1m").arg(nMinute);
if (nSecond > 0)
strValue += strSpacing + QString("%1s").arg(nSecond);
}
return strValue;
}
3.网络下载解压相关
头文件
#pragma once
#include <QtWidgets/QDialog>
#include <QtWidgets/QMainWindow>
#include <QUrl>
#include <QDesktopServices>
//网络相关头文件
#include <QtNetwork/QNetworkAccessManager>
#include <QtNetwork/QNetworkRequest>
#include <QtNetwork/QNetworkReply>
//JSON相关头文件
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QMessageBox>
#include <QFile>
#include <QFileInfo>
#include <QDir>
#include <QTime>
#include <QProgressBar>
#include <QTextCodec>
#include <QProcess>
#include "zlib.h"
#include "zconf.h"
#include "unzip.h"
#include <direct.h>
#include "ui_AutoUpdate.h"
class AutoUpdate : public QDialog
{
Q_OBJECT
public:
AutoUpdate(QWidget *parent = Q_NULLPTR);
private:
int ParseJson(const QString &str); //解析数据函数的声明
bool UnzipFile(const char *zipFileName, const char *goalPath);
bool UnzipCurrentFile(unzFile &uf, const char *destFolder);
private:
QNetworkAccessManager * m_NetManager; //定义网络请求对象
QNetworkAccessManager * m_NetManagerDown; //定义网络请求对象
QNetworkReply * m_pReply;
QString m_CurVerison; //定义当前软件的版本号
QUrl m_url;
QString m_strUrl;
QString m_strFileName;
QTime m_downloadTime;
int m_nTime;
QString m_lastVerison;
std::string strFileName;
signals:
void replyFinished_over(int);
private slots:
void slotUpdateNow();
void slotClose();
void replyFinished(QNetworkReply *reply); //json 文件下载结束
void replyFinished_zip(QNetworkReply *reply); //zip文件下载结束
void onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal); //下载进度
void onReplyFinished(int statusCode);
void readyRead();
private:
Ui::AutoUpdate ui;
};
cpp 文件
# include <stdio.h>
# include <inttypes.h>
# include <Windows.h>
# include <WinInet.h>
#include "AutoUpdate.h"
#include "configer.h"
#include "Format.h"
//#include "zlib.h"
//#include "zconf.h"
# define DOWNLOAD_BUFFER_SIZE 512
AutoUpdate::AutoUpdate(QWidget *parent)
: QDialog(parent),
m_NetManager(NULL),
m_NetManagerDown(NULL),
m_nTime(0)
{
ui.setupUi(this);
m_strFileName = QString(QString::fromLocal8Bit(Configer::FilePath().c_str())) + "\\Test.zip";
m_CurVerison = QString::fromStdString(Configer::GetVersion());
setWindowIcon(QIcon(QString::fromStdString(Configer::GetWindowIcon())));
setWindowFlags(Qt::FramelessWindowHint | Qt::WindowMinimizeButtonHint | Qt::WindowStaysOnTopHint);
m_NetManager = new QNetworkAccessManager(this); //新建QNetworkAccessManager对象
connect(m_NetManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(replyFinished(QNetworkReply*)));//关联信号和槽
QNetworkRequest quest;
quest.setUrl(QUrl(Configer::GetQurl().c_str())); //包含最新版本软件的下载地址
quest.setHeader(QNetworkRequest::UserAgentHeader, "RT-Thread ART");
m_NetManager->get(quest); //发送get网络请求
}
int AutoUpdate::ParseJson(const QString &str)
{
QJsonParseError err_rpt;
QJsonDocument root_Doc = QJsonDocument::fromJson(str.toUtf8(), &err_rpt);//字符串格式化为JSON
if (err_rpt.error != QJsonParseError::NoError)
{
QMessageBox::critical(this, QString::fromLocal8Bit("检查失败"), QString::fromLocal8Bit("服务器地址错误或JSON格式错误!"));
return -1;
}
if (root_Doc.isObject())
{
QJsonObject root_Obj = root_Doc.object(); //创建JSON对象,不是字符串
m_lastVerison = root_Obj.value("LatestVerison").toString(); //V1.0
m_strUrl = root_Obj.value("Url").toString();
QString UpdateTime = root_Obj.value("UpdateTime").toString();
QString ReleaseNote = root_Obj.value("ReleaseNote").toString();
if (m_lastVerison > m_CurVerison)
{
this->show();
QString warningStr = QString::fromLocal8Bit("检测到新版本!\n版本号:") + m_lastVerison + "\n" + QString::fromLocal8Bit("更新时间:") + UpdateTime + "\n" + QString::fromLocal8Bit("更新说明:") + ReleaseNote;
ui.textBrowser->append(warningStr);
}
else
{
QProcess::startDetached(QString(QString::fromLocal8Bit(Configer::FilePath().c_str())) + Configer::GetRunExe().c_str(), QStringList());
qApp->quit();
}
}
return 0;
}
void AutoUpdate::replyFinished(QNetworkReply *reply)
{
QVariant statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
if (statusCode != 200)
{
return;
}
QString str = reply->readAll();//读取接收到的数据
ParseJson(str);
reply->deleteLater();
}
void AutoUpdate::replyFinished_zip(QNetworkReply *reply)
{
// 获取响应的信息,状态码为200表示正常
QVariant statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
// 无错误返回
if (reply->error() == QNetworkReply::NoError)
{
// 重命名临时文件
QFileInfo fileInfo(m_strFileName);
QFileInfo newFileInfo = fileInfo.absolutePath() + "/"+ m_url.fileName();
QDir dir;
if (dir.exists(fileInfo.absolutePath()))
{
if (newFileInfo.exists())
newFileInfo.dir().remove(newFileInfo.fileName());
QFile::rename(m_strFileName, newFileInfo.absoluteFilePath());
}
}
else
{
QString strError = reply->errorString();
qDebug() << "Error:" << strError;
}
emit replyFinished_over(statusCode.toInt());
}
void AutoUpdate::slotUpdateNow()
{
ui.stackedWidget->setCurrentWidget(ui.pageInfo);
if(m_NetManagerDown == NULL)
{
m_NetManagerDown = new QNetworkAccessManager(this);
connect(this, SIGNAL(downloadProgress(qint64, qint64)), this, SLOT(onDownloadProgress(qint64, qint64)), Qt::QueuedConnection);
connect(this, SIGNAL(replyFinished_over(int)), this, SLOT(onReplyFinished(int)), Qt::QueuedConnection);
}
m_url = QUrl(m_strUrl); //包含最新版本软件的下载地址
connect(m_NetManagerDown, SIGNAL(finished(QNetworkReply*)), this, SLOT(replyFinished_zip(QNetworkReply*)));//关联信号和槽
QNetworkRequest quest;
quest.setUrl(m_url); //包含最新版本软件的下载地址
quest.setHeader(QNetworkRequest::ContentTypeHeader, "application/zip");
m_pReply = m_NetManagerDown->get(quest); //发送get网络请求
connect(m_pReply, SIGNAL(downloadProgress(qint64, qint64)), this, SLOT(onDownloadProgress(qint64, qint64)), Qt::QueuedConnection);
connect(m_pReply, SIGNAL(readyRead()), this, SLOT(readyRead()));
m_downloadTime.start();
}
void AutoUpdate::slotClose()
{
reject();
}
void AutoUpdate::onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
{
// 总时间
int nTime = m_downloadTime.elapsed();
// 本次下载所用时间
nTime -= m_nTime;
// 下载速度
double dBytesSpeed = (bytesReceived * 1000.0) / nTime;
double dSpeed = dBytesSpeed;
//剩余时间
qint64 leftBytes = (bytesTotal - bytesReceived);
double dLeftTime = (leftBytes * 1.0) / dBytesSpeed;
ui.lblSpeedInfo->setText(speed(dSpeed));
ui.lblLeftTime->setText(timeFormat(dLeftTime));
ui.lblFileInfoSize->setText(formatSize(bytesTotal));
ui.lblDownLoadInfo->setText(formatSize(bytesReceived));
ui.progressBar->setMaximum(bytesTotal);
ui.progressBar->setValue(bytesReceived);
// 获取上一次的时间
m_nTime = nTime;
}
bool AutoUpdate::UnzipCurrentFile(unzFile &uf, const char *destFolder)
{
char szFilePath[512] = {0};
unz_file_info64 FileInfo;
FILE * fp4= NULL; // 创建解压文件的文件指针
errno_t err = 0;
if (unzGetCurrentFileInfo64(uf, &FileInfo, szFilePath, sizeof(szFilePath), NULL, 0, NULL, 0) != UNZ_OK)
return false;
std::string str(szFilePath);
size_t len = strlen(str.c_str());
if (len <= 0)
{
return false;
}
if (str == "RiskClient/cfg.xml") //保证xml文件不更新
{
return true;
}
std::string fullFileName = destFolder;
fullFileName = fullFileName+ "\\" +szFilePath;
if (szFilePath[len - 1] == '\\' || szFilePath[len - 1] == '/' || str == "")
{
_mkdir(fullFileName.c_str());
return true;
}
err = fopen_s(&fp4, fullFileName.c_str(), "wb");
if (err)
{
return false;
}
const int BUFFER_SIZE = 4096;
unsigned char byBuffer[BUFFER_SIZE];
if (unzOpenCurrentFile(uf) != UNZ_OK)
{
fclose(fp4);
return false;
}
while (true)
{
int nSize = unzReadCurrentFile(uf, byBuffer, BUFFER_SIZE);
if (nSize < 0)
{
unzCloseCurrentFile(uf);
fclose(fp4);
return false;
}
else if (nSize == 0)
{
break;
}
else
{
size_t wSize = fwrite(byBuffer, 1, nSize, fp4);
if (wSize != nSize)
{
unzCloseCurrentFile(uf);
fclose(fp4);
return false;
}
}
}
unzCloseCurrentFile(uf);
fclose(fp4);
return true;
}
bool AutoUpdate::UnzipFile(const char *zipFileName, const char *goalPath)
{
unzFile uf = unzOpen64(zipFileName);
if (uf == NULL)
return false;
unz_global_info64 gi;
if (unzGetGlobalInfo64(uf, &gi) != UNZ_OK)
{
unzClose(uf);
return false;
}
std::string path = zipFileName;
auto pos = path.find_last_of("/\\");
if (pos != std::string::npos)
path.erase(path.begin() + pos, path.end());
for (int i = 0; i < gi.number_entry; ++i)
{
if (!UnzipCurrentFile(uf, goalPath))
{
unzClose(uf);
return false;
}
if (i < gi.number_entry - 1)
{
if (unzGoToNextFile(uf) != UNZ_OK)
{
unzClose(uf);
return false;
}
}
}
unzClose(uf);
return true;
}
void AutoUpdate::onReplyFinished(int statusCode)
{
QString strStatus = (statusCode == 200) ? QStringLiteral("下载成功") : QStringLiteral("下载失败");
ui.lblStatus->setText(strStatus);
if (statusCode == 200)
{
bool flag = UnzipFile(strFileName.c_str(), Configer::FilePath().c_str());
if (flag)
{
//更新配置文件版本号
Configer::SaveVersion(m_lastVerison.toStdString().c_str());
//启动解压好的可执行程序
QProcess::startDetached(QString(QString::fromLocal8Bit(Configer::FilePath().c_str())) + Configer::GetRunExe().c_str(), QStringList());
//退出当前进程
qApp->quit();
}
}
}
void AutoUpdate::readyRead()
{
QFileInfo fileInfo(m_strFileName);
QString fileName = m_url.fileName();
QFileInfo newFileInfo = fileInfo.absolutePath() + "/"+ m_url.fileName();
QTextCodec *code = QTextCodec::codecForName("GB2312");//解决中文路径问题
strFileName = code->fromUnicode(newFileInfo.absoluteFilePath()).data();
ui.lblFileInfo->setText(fileName);
// 写文件-形式为追加
QFile file(m_strFileName);
if (file.open(QIODevice::Append))
file.write(m_pReply->readAll());
file.close();
}
本文章没有实现文件比较的功能,是解压缩替换掉原来所有的文件
源码下载地址:Qt 实现软件升级
参考文章:Qt 实现自动更新的一种简单方法