Qt 实现软件自动更新
阅读原文时间:2021年04月23日阅读:1

一、流程

详细流程可参考本博客:https://blog.csdn.net/weixin_38739598/article/details/106571074

二、具体实现

CHttpDownLoadFile头文件

#pragma once

#include <QWidget>

//网络相关头文件
#include <QtNetwork/QNetworkAccessManager>
#include <QtNetwork/QNetworkRequest>
#include <QtNetwork/QNetworkReply>
//文件相关头文件
#include <QFile>
#include <QFileInfo>
#include <QDir>
#include <QDesktopServices>

class CHttpDownLoadFile : public QWidget
{
    Q_OBJECT

public:
    CHttpDownLoadFile(const QString &url, const QString &fileName, const QString &dir,QWidget *parent);
    ~CHttpDownLoadFile();

    void DownLoadFile();
    void DestroyData();

signals:
    //文件下载结束
    void DownloadFinishedSignal();  
    //文件下载进度
    void DownloadProcess(QString, qint64, qint64);

public slots:
    void ReplyNewDataArrived();//响应m_netReply有新的数据到达
    void ReplyFinished();//响应数据接收完成
    void ReplyDownloadProgress(qint64 bytesReceived, qint64 bytesTotal);    //响应文件下载进度

public:
    QNetworkAccessManager *m_netAccessManager;//网络参数

    QNetworkReply *m_netReply;
    QUrl m_urlAdress;//网络地址
    QString m_strFileName;//需要下载的文件名
    QString m_strDir;//文件的存储位置

    QFile *m_file;//下载的文件
    qint64 m_nReceived;//下载文件时,已经接收的文件大小和总共大小
    qint64 m_nTotal;
    bool m_bIsFinished;
};

CHttpDownLoadFile源文件

#include "CHttpDownLoadFile.h"

CHttpDownLoadFile::CHttpDownLoadFile(const QString &url, const QString &fileName, const QString &dir, QWidget *parent)
    : QWidget(parent),
    m_urlAdress(url),
    m_strFileName(fileName),
    m_strDir(dir),
    m_bIsFinished(false)
{
    m_netAccessManager = new QNetworkAccessManager(this);
}

CHttpDownLoadFile::~CHttpDownLoadFile()
{
}

void CHttpDownLoadFile::DownLoadFile()
{
    m_bIsFinished = false;
    QNetworkRequest request(m_urlAdress);
    request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
    m_netReply = m_netAccessManager->get(request);

    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 | QIODevice::Text))
    {
        m_file->close();
        delete m_file;
        m_file = NULL;

        return;
    }
}

void CHttpDownLoadFile::DestroyData()
{
    m_netAccessManager->deleteLater();
    m_netReply->deleteLater();
    m_file->close();
    m_file->deleteLater();
}

void CHttpDownLoadFile::ReplyNewDataArrived()//响应m_netReply有新的数据到达
{
    if (m_file)
    {
        // 写文件-形式为追加
        QFile file(m_strFileName);
        if (file.open(QIODevice::Append))
            file.write(m_netReply->readAll());
        file.close();
    }
    else
    {
        qDebug() << m_netReply->readAll();
    }
}

void CHttpDownLoadFile::ReplyFinished()//响应数据接收完成
{
    m_bIsFinished = true;
    m_netAccessManager->deleteLater();
    m_netReply->deleteLater();
    m_file->close();
    m_file->deleteLater();

    emit DownloadFinishedSignal();
}

void CHttpDownLoadFile::ReplyDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
{
    QFileInfo fileInfo(m_urlAdress.path());
    QString strFileName = fileInfo.fileName();
    emit DownloadProcess(strFileName, bytesReceived, bytesTotal);
}

AutoUpdate 头文件

#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"
#include "CHttpDownLoadFile.h"
#include <QList>

class AutoUpdate : public QDialog
{
    Q_OBJECT

public:
    AutoUpdate(QWidget *parent = Q_NULLPTR);

private:
    int ParseJson(const QString &str);      //解析数据函数的声明
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;  

private:
    CHttpDownLoadFile *m_httpXML;
    QList<QString> m_listFileDir;
    QList<QString> m_listFileName;
    QString m_strTip;
    QString m_strXmlName;

    void DownLoadXML();
    QString GetElementVersion(const QString &xml, const QString &name);
    bool CheckVersion(const QString &v1, const QString &v2);
    int CheckUpdateFiles(const QString &xml1, const QString &xml2);
    void DownLoadUpdateFiles();
    void ExitApp(const QString &name);

private slots:
    void slotUpdateNow();
    void slotClose();
    void replyFinished(QNetworkReply *reply);   //json 文件下载结束
    void onDownloadProgress(QString fileName, qint64 bytesReceived, qint64 bytesTotal); //下载进度
    void ReplyHttpFinished();
private:
    Ui::AutoUpdate ui;
};

AutoUpdate源文件

# include <stdio.h>
# include <inttypes.h>
# include <Windows.h>
# include <WinInet.h>
#include "AutoUpdate.h"
#include "configer.h"
#include "Format.h"
#include <QDomDocument>

AutoUpdate::AutoUpdate(QWidget *parent)
    : QDialog(parent),
    m_NetManager(NULL),
    m_NetManagerDown(NULL),
    m_nTime(0)
{
    ui.setupUi(this);
    ui.progressBar->setValue(0);
    m_strFileName = QString(QString::fromLocal8Bit(Configer::GetInstance()->FilePath().c_str())) + "\\Test.zip";

    m_CurVerison = QString::fromStdString(Configer::GetInstance()->GetVersion());
    setWindowIcon(QIcon(QString::fromStdString(Configer::GetInstance()->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::GetInstance()->GetCfgQurl().c_str())); //包含最新版本软件的下载地址
    //quest.setHeader(QNetworkRequest::UserAgentHeader, "RT-Thread ART");
    QNetworkReply *reply = m_NetManager->get(quest);    //发送get网络请求
    connect(reply, SIGNAL(error), this, SLOT(slotError));
}
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
        {
            QString strCurrentDir = QString(QString::fromLocal8Bit(Configer::GetInstance()->FilePath().c_str()));//当前程序运行路径
            ExitApp(strCurrentDir + QString::fromStdString(Configer::GetInstance()->GetLocation()) + Configer::GetInstance()->GetRunExe().c_str());
        }
    }
    return 0;
}
void AutoUpdate::replyFinished(QNetworkReply *reply)
{
    QVariant statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
    if (statusCode != 200)
    {
        //QMessageBox::critical(this, QString::fromLocal8Bit("Json文件下载失败 状态码:%1").arg(statusCode.toString()), reply->errorString());
        return;
    }

    ParseJson(reply->readAll());
    reply->deleteLater();
}
void AutoUpdate::slotUpdateNow()
{
    //杀掉正在运行的进程
    QProcess taskkill;
    taskkill.execute("taskkill", QStringList() << "-im" << Configer::GetInstance()->GetRunExe().c_str() << "-f");

    ui.stackedWidget->setCurrentWidget(ui.pageInfo);
    ui.butNow->hide();
    DownLoadXML();
}
void AutoUpdate::slotClose()
{
    reject();
    qApp->exit(0);
    m_httpXML->DestroyData();
    /*
    this->close();*/

}
void AutoUpdate::onDownloadProgress(QString fileName,qint64 bytesReceived, qint64 bytesTotal)
{
    ui.lblFileInfo->setText(fileName);
    // 总时间
    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;
}

void AutoUpdate::DownLoadXML()
{
    /**从网页下载XML版本控制文件,里面记录了最新的文件版本**/
    string str = Configer::GetInstance()->FilePath();
    QString strDownLoad = QString::fromLocal8Bit(Configer::GetInstance()->FilePath().c_str()) + QString::fromStdString(Configer::GetInstance()->GetLocation());//存放下载文件的路径

    QDir directory(strDownLoad);//如果路径不存在,则创建
    if (!directory.exists())
    {
        directory.mkpath(strDownLoad);
    }
    QUrl url(QString::fromStdString(Configer::GetInstance()->GetXmlQurl()));
    QFileInfo fileInfo(url.path());
    m_strXmlName = fileInfo.fileName();

    m_httpXML = new CHttpDownLoadFile(QString::fromStdString(Configer::GetInstance()->GetXmlQurl()), "", strDownLoad, this);//调用下载文件的类
    connect(m_httpXML, SIGNAL(DownloadFinishedSignal()), this, SLOT(ReplyHttpFinished()));//发生错误时一样会发送此信号
    m_httpXML->DownLoadFile();
}

QString AutoUpdate::GetElementVersion(const QString &xml, const QString &name)
{
    QString result = "";
    if (xml.isEmpty() || name.isEmpty())
    {
        qDebug() << "名称或者xml文件路径为空";
        return result;
    }
    if (!QFile::exists(xml))
    {
        qDebug() << "xml文件不存在";
        return result;
    }

    QFile file(xml);
    if (file.open(QIODevice::ReadOnly | QFile::Text))//文件打开成功
    {
        QDomDocument doc;
        QString errorStr;
        int errorLine;
        int errorColumn;

        if (doc.setContent(&file, false, &errorStr, &errorLine, &errorColumn))
        {
            QDomElement root = doc.documentElement();
            if (root.tagName() == "filelist")
            {
                int i = 0;
                QDomNodeList nodeList = root.elementsByTagName("file");
                for (; i<nodeList.size(); i++)
                {
                    QString tempName = nodeList.at(i).toElement().attribute("name");
                    //QStringdir    =nodeList.at(i).toElement().attribute("dir");

                    QString version = nodeList.at(i).toElement().attribute("version");
                    if (name == tempName)
                    {
                        qDebug() << "find!" << name;
                        result = version;
                        break;
                    }

                }
                if (i == nodeList.size())
                {
                    qDebug() << "can'tfind!" << name;
                }
            }
            else
            {
                qDebug() << "root.tagname!=filelist..";
            }

        }
        else
        {
            qDebug() << "setcontenterror...";
        }

        file.close();
    }
    else
    {
        qDebug() << "openforreaderror...";
    }
    return result;
}

bool AutoUpdate::CheckVersion(const QString &v1, const QString &v2)
{
    return v1 == v2;
}

int AutoUpdate::CheckUpdateFiles(const QString &xml1, const QString &xml2)
{
    m_listFileDir.clear();
    m_listFileName.clear();

    if (xml1.isEmpty() || xml2.isEmpty())
        return 0;

    if (QFile::exists(xml2))
    {
        if (QFile::exists(xml1))
        {
            m_strTip = QString::fromLocal8Bit("检查需要更新的文件...");
            QFile file(xml1);
            if (file.open(QIODevice::ReadOnly | QFile::Text))//文件打开成功
            {

                QString errorStr;
                int errorLine;
                int errorColumn;

                QDomDocument doc;

                if (doc.setContent(&file, false, &errorStr, &errorLine, &errorColumn))
                {
                    QDomElement root = doc.documentElement();
                    if (root.tagName() == "filelist")
                    {
                        QDomNodeList nodeList = root.elementsByTagName("file");
                        for (int i = 0; i<nodeList.size(); i++)
                        {
                            QString name = nodeList.at(i).toElement().attribute("name");
                            QString dir = nodeList.at(i).toElement().attribute("dir");
                            QString version = nodeList.at(i).toElement().attribute("version");
                            QString versionDownload = GetElementVersion(xml2, 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() << name << QString::fromLocal8Bit("文件是最新版本,不需要更新");
                                }
                            }
                        }
                        return 1;//此时要退出,避免关闭程序
                    }
                    else
                    {
                        m_strTip = "XML内容错误!";
                        return 0;

                    }
                }
                else
                {
                    qDebug() << "setcontenterror...";
                    return 0;
                }
             file.close();

            }
            else
            {
                m_strTip = "不能打开更新文件!";
                return 0;

            }

        }
        else
        {

            m_strTip = "下载更新文件错误!";
            return 0;
        }
    }

    else
    {
        m_strTip = "本地的更新文件不存在!";
        return 0;
    }
}

void AutoUpdate::DownLoadUpdateFiles()
{
    QString strServer = QString::fromStdString(Configer::GetInstance()->GetSerQurl());//需要下载的文件存储位置
    QString strCurrentDir = QString(QString::fromLocal8Bit(Configer::GetInstance()->FilePath().c_str()));//当前程序运行路径

    if (m_listFileDir.isEmpty() || m_listFileDir.isEmpty())
    {
        ExitApp(strCurrentDir + QString::fromLocal8Bit(Configer::GetInstance()->GetLocation().c_str()) + Configer::GetInstance()->GetRunExe().c_str());
        return;
    }
    m_strTip = QString::fromLocal8Bit("开始下载更新文件...");

    for (int i = 0; i<m_listFileName.size(); i++)
    {
        m_strTip = QString::fromLocal8Bit("正在下载文件") + m_listFileName.at(i);
        ui.progressBar->setValue(100 * i / m_listFileName.size());

        /**放置下载的文件的路径**/
        QString temp = m_listFileDir.at(i);
        QString tempdir = m_listFileDir.at(i);
        if (!m_listFileDir.at(i).isEmpty())
        {
            temp = temp + "/";
            tempdir = tempdir + "\\";
        }
        QString strPlaceDir = strCurrentDir + QString::fromStdString(Configer::GetInstance()->GetLocation()) + tempdir;
        QDir directory(strPlaceDir);//如果路径不存在,则创建
        if (!directory.exists())
            directory.mkpath(strPlaceDir);

        QString strFileDirServer = strServer + temp + m_listFileName.at(i);//文件在服务器中的存储位置

        CHttpDownLoadFile *http = new CHttpDownLoadFile(strFileDirServer, "", strPlaceDir, this);//调用下载文件的类
        http->DownLoadFile();
        connect(http, SIGNAL(DownloadProcess(QString, qint64, qint64)), this, SLOT(onDownloadProgress(QString, qint64, qint64)));
        m_downloadTime.start();
        while (!http->m_bIsFinished)
        {
            if (http->m_nTotal == -1)
            {
                ui.progressBar->setValue(1);
            }
            else
            {
                ui.progressBar->setValue(100 * http->m_nReceived / http->m_nTotal);
            }

            QCoreApplication::processEvents();
        }
        m_strTip = QString::fromLocal8Bit("文件") + m_listFileName.at(i) + QString::fromLocal8Bit("下载完成");

        ///**将下载好的文件复制到主目录中,先删除原先的文件**/
        //QString strLocalFileName = strCurrentDir + "\\" + m_listFileDir.at(i) + "\\" + m_listFileName.at(i);

        //if (QFile::exists(strLocalFileName))QFile::remove(strLocalFileName);

        //QDir directory1(strCurrentDir + "\\" + m_listFileDir.at(i));//如果路径不存在,则创建

        //if (!directory1.exists())directory1.mkpath(strCurrentDir + "\\" + m_listFileDir.at(i));

        //QFile::copy(strPlaceDir + "\\" + m_listFileName.at(i), strLocalFileName);

    }

    m_strTip = QString::fromLocal8Bit("更新完成!");

    /**替换旧的xml文件**/
    QString strNewXML = strCurrentDir + QString::fromStdString(Configer::GetInstance()->GetLocation()) + m_strXmlName;//最新的XML文件
    QString strOldXML = strCurrentDir + "\\" + m_strXmlName;//旧的XML文件

    QFile::remove(strOldXML);
    QFile::copy(strNewXML, strOldXML);

    ExitApp(strCurrentDir + QString::fromStdString(Configer::GetInstance()->GetLocation()) + Configer::GetInstance()->GetRunExe().c_str());
}

void AutoUpdate::ExitApp(const QString &name)
{
    if (!name.isEmpty())
    {
        /**运行主程序,并且退出当前更新程序(说明:主程序在上上一级目录中)**/
        //更新配置文件版本号
        Configer::GetInstance()->SaveVersion(m_lastVerison.toStdString().c_str());
        if (!QProcess::startDetached(name, QStringList()))//启动主程序,主程序在其上一级目录
        {
            QMessageBox::warning(this, QString::fromLocal8Bit("警告信息"), QString::fromLocal8Bit("启动主程序错误!\n可能主程序不存在或者被破坏!\n解决办法:重新安装程序!"));
        }
    }
    this->close();
}

void AutoUpdate::ReplyHttpFinished()
{
    CheckUpdateFiles(QString::fromLocal8Bit(Configer::GetInstance()->FilePath().c_str()) + QString::fromStdString(Configer::GetInstance()->GetLocation()) + m_strXmlName, QString::fromLocal8Bit(Configer::GetInstance()->FilePath().c_str()) + "\\" + m_strXmlName);
    DownLoadUpdateFiles();
}

三、遇到的问题

  • 程序支持中文路径,不需要中间进行转换,设置编码。
  • 如果程序安装到C盘,需要以管理员的方式启动程序。
  • 如果要更新的程序已经在运行中,那么就需要杀掉正在运行的进程,再进行更新,不然会更新失败。
  • 相对上https://blog.csdn.net/weixin_38739598/article/details/106571074,避免了下载整个压缩包,没必要更新的文件也下载了。

四、实现效果



参考文章:https://blog.csdn.net/hulinhulin/article/details/46839107
由于博主只写了部分,所以自己实现了部分。

源码下载地址:Qt 实现软件自动升级