开源 C++ QT QML 开发(十五)通讯--http下载

文章的目的为了记录使用QT QML开发学习的经历。开发流程和要点有些记忆模糊,赶紧记录,防止忘记。

相关链接:

开源 C++ QT QML 开发(一)基本介绍

开源 C++ QT QML 开发(二)工程结构

开源 C++ QT QML 开发(三)常用控件

开源 C++ QT QML 开发(四)复杂控件--Listview

开源 C++ QT QML 开发(五)复杂控件--Gridview

开源 C++ QT QML 开发(六)自定义控件--波形图

开源 C++ QT QML 开发(七)自定义控件--仪表盘

推荐链接:

开源 C# 快速开发(一)基础知识

开源 C# 快速开发(二)基础控件

开源 C# 快速开发(三)复杂控件

开源 C# 快速开发(四)自定义控件--波形图

开源 C# 快速开发(五)自定义控件--仪表盘

开源 C# 快速开发(六)自定义控件--圆环

开源 C# 快速开发(七)通讯--串口

开源 C# 快速开发(八)通讯--Tcp服务器端

开源 C# 快速开发(九)通讯--Tcp客户端

开源 C# 快速开发(十)通讯--http客户端

开源 C# 快速开发(十一)线程

开源 C# 快速开发(十二)进程监控

开源 C# 快速开发(十三)进程--管道通讯

开源 C# 快速开发(十四)进程--内存映射

开源 C# 快速开发(十五)进程--windows消息

开源 C# 快速开发(十六)数据库--sqlserver增删改查

本章节主要内容是:仪表盘实现了一个汽车速度表风格自定义控件,参数可设置,数据可实时更新。

1.代码分析

2.所有源码

3.效果演示

一、代码分析C++ 后端详细分析

  1. 构造函数与析构函数

    // 默认构造函数
    DownloadTool::DownloadTool(QObject* parent)
    : QObject(parent) // 调用基类构造函数
    {
    // 简单的初始化,成员变量使用默认值
    }

    // 参数化构造函数
    DownloadTool::DownloadTool(const QString& downloadUrl, const QString& savePath, QObject* parent)
    : QObject(parent)
    {
    m_downloadUrl = downloadUrl; // 直接赋值,不触发信号
    m_savePath = savePath; // 直接赋值,不触发信号
    }

    // 析构函数
    DownloadTool::~DownloadTool()
    {
    if (reply) {
    reply->deleteLater(); // 安全删除网络回复对象
    // 使用 deleteLater() 而不是 delete,避免在事件处理过程中删除
    }
    // file 智能指针会自动释放文件资源
    }

  2. 属性访问器函数

    QString DownloadTool::downloadUrl() const
    {
    return m_downloadUrl; // 返回当前下载URL
    }

    void DownloadTool::setDownloadUrl(const QString& url)
    {
    if (m_downloadUrl != url) { // 只有值改变时才更新
    m_downloadUrl = url;
    emit downloadUrlChanged(); // 通知QML属性已改变
    }
    }

    QString DownloadTool::savePath() const
    {
    return m_savePath; // 返回当前保存路径
    }

    void DownloadTool::setSavePath(const QString& path)
    {
    if (m_savePath != path) { // 值改变检查
    m_savePath = path;
    emit savePathChanged(); // 通知QML属性已改变
    }
    }

    bool DownloadTool::downloading() const
    {
    // 检查下载状态:有回复对象且正在运行
    return reply && reply->isRunning();
    }

  3. 核心下载逻辑函数

getDefaultDownloadPath()

复制代码
QString DownloadTool::getDefaultDownloadPath() const
{
    // 使用 QCoreApplication 获取应用程序目录
    // 避免依赖 QApplication (QtWidgets模块)
    return QCoreApplication::applicationDirPath() + "/downloads";
    // 示例结果: "C:/Program Files/MyApp/downloads"
}

startDownload() - 主下载入口

复制代码
void DownloadTool::startDownload()
{
    // 1. 状态检查 - 防止重复下载
    if (downloading()) {
        return;  // 如果正在下载,直接返回
    }

    // 2. URL验证和解析
    const QUrl newUrl = QUrl::fromUserInput(m_downloadUrl);
    // QUrl::fromUserInput() 能处理各种格式的URL输入
    if (!newUrl.isValid()) {
        // URL无效,发出错误信号
#ifdef DOWNLOAD_DEBUG
        qDebug() << QString("Invalid URL: %1: %2").arg(m_downloadUrl, newUrl.errorString());
#endif
        emit sigDownloadError("Invalid URL: " + newUrl.errorString());
        return;
    }

    // 3. 文件名处理
    QString fileName = newUrl.fileName();  // 从URL提取文件名
    if (fileName.isEmpty()) {
        fileName = defaultFileName;  // 使用默认文件名 "download.file"
    }

    // 4. 保存路径处理
    if (m_savePath.isEmpty()) {
        m_savePath = getDefaultDownloadPath();  // 使用默认路径
    }

    // 5. 创建目录
    if (!QFileInfo(m_savePath).isDir()) {
        QDir dir;
        dir.mkpath(m_savePath);  // 递归创建目录
    }

    // 6. 构建完整文件路径
    fileName.prepend(m_savePath + '/');  // 在前面添加路径
    // 示例: "downloads/file.zip"

    // 7. 处理已存在文件
    if (QFile::exists(fileName)) {
        QFile::remove(fileName);  // 删除已存在的文件
    }

    // 8. 创建文件
    file = openFileForWrite(fileName);
    if (!file) {
        // 文件创建失败
        emit sigDownloadError("Unable to create file for writing");
        return;
    }

    // 9. 开始网络请求
    startRequest(newUrl);
    
    // 10. 通知状态改变
    emit downloadingChanged();  // downloading() 现在返回 true
}

openFileForWrite() - 文件创建

复制代码
std::unique_ptr<QFile> DownloadTool::openFileForWrite(const QString& fileName)
{
    // 创建 unique_ptr 管理的 QFile 对象
    std::unique_ptr<QFile> file(new QFile(fileName));
    
    // 以只写方式打开文件
    if (!file->open(QIODevice::WriteOnly)) {
        // 文件打开失败
#ifdef DOWNLOAD_DEBUG
        qDebug() << QString("Unable to save the file %1: %2.")
            .arg(QDir::toNativeSeparators(fileName), file->errorString());
#endif
        return nullptr;  // 返回空指针表示失败
    }
    
    return file;  // 返回文件对象
}

startRequest() - 网络请求发起

复制代码
void DownloadTool::startRequest(const QUrl& requestedUrl)
{
    url = requestedUrl;           // 保存当前URL
    httpRequestAborted = false;   // 重置取消标志

    // 发起GET请求
    reply = qnam.get(QNetworkRequest(url));
    
    // 连接信号槽 - 关键的网络事件处理
    connect(reply, &QNetworkReply::finished, 
            this, &DownloadTool::httpFinished);          // 请求完成
    connect(reply, &QIODevice::readyRead, 
            this, &DownloadTool::httpReadyRead);         // 数据可读
    connect(reply, &QNetworkReply::downloadProgress, 
            this, &DownloadTool::networkReplyProgress);  // 下载进度

#ifdef DOWNLOAD_DEBUG
    qDebug() << QString("Downloading %1...").arg(url.toString());
#endif
}
  1. 网络事件处理函数

httpReadyRead() - 数据接收

复制代码
void DownloadTool::httpReadyRead()
{
    // 当有数据可读时,立即写入文件
    if (file) {
        file->write(reply->readAll());  // 读取所有可用数据并写入文件
    }
    // 这种实时写入方式:
    // 优点:内存占用小,适合大文件
    // 缺点:频繁的磁盘IO操作
}

networkReplyProgress() - 进度更新

复制代码
void DownloadTool::networkReplyProgress(qint64 bytesRead, qint64 totalBytes)
{
    // 计算进度百分比,避免除零错误
    qreal progress = totalBytes > 0 ? qreal(bytesRead) / qreal(totalBytes) : 0;
    
    // 发出进度信号
    emit sigProgress(bytesRead, totalBytes, progress);

#ifdef DOWNLOAD_DEBUG
    // 调试信息:进度百分比和MB显示
    qDebug() << QString::number(progress * 100, 'f', 2) << "%    "
        << bytesRead / (1024 * 1024) << "MB" << "/" << totalBytes / (1024 * 1024) << "MB";
#endif
}

httpFinished() - 请求完成处理

复制代码
void DownloadTool::httpFinished()
{
    // 1. 通知状态改变
    emit downloadingChanged();  // downloading() 现在返回 false

    // 2. 文件信息保存和清理
    QFileInfo fi;
    if (file) {
        fi.setFile(file->fileName());  // 保存文件信息
        file->close();                 // 关闭文件
        file.reset();                  // 释放文件指针
    }

    // 3. 检查是否用户取消
    if (httpRequestAborted) {
        return;  // 如果是用户取消,直接返回
    }

    // 4. 检查网络错误
    if (reply->error()) {
        // 删除不完整的文件
        QFile::remove(fi.absoluteFilePath());
        
        QString errorString = QString("Download failed: %1.").arg(reply->errorString());
#ifdef DOWNLOAD_DEBUG
        qDebug() << errorString;
#endif
        emit sigDownloadError(errorString);
        return;
    }

    // 5. 处理HTTP重定向
    const QVariant redirectionTarget = reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
    if (!redirectionTarget.isNull()) {
        // 构建重定向URL
        const QUrl redirectedUrl = url.resolved(redirectionTarget.toUrl());
        
        // 重新打开文件(同一个文件)
        file = openFileForWrite(fi.absoluteFilePath());
        if (!file) {
            emit sigDownloadError("Unable to create file for redirection");
            return;
        }
        
        // 重新发起请求到重定向URL
        startRequest(redirectedUrl);
        return;  // 注意这里的return,重定向时不会发出完成信号
    }

    // 6. 清理网络回复对象
    reply->deleteLater();  // 安全删除
    reply = nullptr;       // 置空指针

    // 7. 发出下载完成信号
    emit sigDownloadFinished();

#ifdef DOWNLOAD_DEBUG
    qDebug() << QString("Downloaded %1 bytes to %2 in %3")
        .arg(fi.size()).arg(fi.fileName(), QDir::toNativeSeparators(fi.absolutePath()));
    qDebug() << "Finished";
#endif
}

cancelDownload() - 取消下载

复制代码
void DownloadTool::cancelDownload()
{
    // 1. 状态检查
    if (!downloading()) {
        return;  // 如果不在下载状态,直接返回
    }

    // 2. 设置取消标志
    httpRequestAborted = true;
    
    // 3. 中止网络请求
    reply->abort();  // 这会触发 reply->error() 和 httpFinished()
    
    // 4. 通知状态改变
    emit downloadingChanged();
}

QML 前端函数详细分析

  1. 工具函数

formatBytes() - 字节格式化

复制代码
function formatBytes(bytes) {
    if (bytes === 0) return "0 B";  // 处理零值
    
    const k = 1024;  // 使用1024进制
    const sizes = ['B', 'KB', 'MB', 'GB'];  // 单位数组
    
    // 计算单位索引:log1024(bytes)
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    
    // 格式化显示:值保留2位小数 + 单位
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}

// 示例:

// formatBytes(1024) → "1.00 KB"

// formatBytes(1536) → "1.50 KB"

// formatBytes(1048576) → "1.00 MB"

updateButtonStates() - 按钮状态管理

qml

function updateButtonStates() {

// 下载按钮:有URL且不在下载状态时可用

downloadButton.enabled = urlTextField.text !== "" && !downloadTool.downloading

// 取消按钮:只在下载状态时可用

cancelButton.enabled = downloadTool.downloading

// 暂停按钮:预留功能

pauseResumeButton.enabled = downloadTool.downloading

}

  1. 信号处理函数

进度信号处理

复制代码
onSigProgress: {
    // 1. 更新进度条
    progressBar.value = progress
    
    // 2. 更新进度文本
    progressText.text = formatBytes(bytesRead) + " / " + formatBytes(totalBytes) +
                       " (" + (progress * 100).toFixed(1) + "%)"

    // 3. 计算下载速度(每秒)
    if (speedTimer.running) {
        var currentTime = new Date().getTime()  // 当前时间戳(毫秒)
        var timeDiff = (currentTime - speedTimer.lastTime) / 1000  // 转换为秒
        
        if (timeDiff > 0) {
            var bytesDiff = bytesRead - speedTimer.lastBytes  // 字节差
            var speed = bytesDiff / timeDiff  // 速度(字节/秒)
            speedText.text = "Speed: " + formatBytes(speed) + "/s"
        }
        
        // 更新计时器状态
        speedTimer.lastBytes = bytesRead
        speedTimer.lastTime = currentTime
    }
}

状态信号处理

复制代码
// 下载完成
onSigDownloadFinished: {
    statusText.text = "✓ 下载完成!"
    statusText.color = successColor  // 绿色
    updateButtonStates()
    progressBar.Material.accent = successColor  // 进度条变绿色
    speedText.text = ""  // 清空速度显示
}

// 下载错误
onSigDownloadError: {
    statusText.text = "✗ Error: " + errorMessage
    statusText.color = errorColor  // 红色
    updateButtonStates()
    progressBar.value = 0  // 重置进度条
    speedText.text = ""  // 清空速度显示
}

// 下载状态改变
onDownloadingChanged: {
    if (downloading) {
        // 开始下载
        statusText.text = "⏳ 下载中..."
        statusText.color = primaryColor  // 蓝色
        progressBar.Material.accent = primaryColor
        speedTimer.start()  // 启动速度计时器
    } else {
        // 下载停止
        speedTimer.stop()  // 停止速度计时器
    }
    updateButtonStates()
}
  1. 用户交互函数

下载按钮点击

复制代码
onClicked: {
    progressBar.value = 0  // 重置进度条
    progressText.text = "开始下载..."  // 更新进度文本
    
    // 重置速度计时器状态
    speedTimer.lastBytes = 0
    speedTimer.lastTime = new Date().getTime()
    
    // 调用C++下载方法
    downloadTool.startDownload()
}

取消按钮点击

复制代码
onClicked: {
    // 调用C++取消方法
    downloadTool.cancelDownload()
    
    // 更新UI状态
    statusText.text = "下载取消"
    statusText.color = warningColor  // 橙色
}

二、所有源码

DownloadTool.h文件源码

复制代码
#ifndef DOWNLOADTOOL_H
#define DOWNLOADTOOL_H

#include <QObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QFile>
#include <QDir>
#include <QUrl>
#include <QFileInfo>
#include <memory>

// 可以取消注释下面的宏来启用调试输出
// #define DOWNLOAD_DEBUG

class DownloadTool : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString downloadUrl READ downloadUrl WRITE setDownloadUrl NOTIFY downloadUrlChanged)
    Q_PROPERTY(QString savePath READ savePath WRITE setSavePath NOTIFY savePathChanged)
    Q_PROPERTY(bool downloading READ downloading NOTIFY downloadingChanged)

public:
    explicit DownloadTool(QObject* parent = nullptr);
    DownloadTool(const QString& downloadUrl, const QString& savePath, QObject* parent = nullptr);
    ~DownloadTool();

    QString downloadUrl() const;
    void setDownloadUrl(const QString& url);

    QString savePath() const;
    void setSavePath(const QString& path);

    bool downloading() const;

    Q_INVOKABLE void startDownload();
    Q_INVOKABLE void cancelDownload();

signals:
    void downloadUrlChanged();
    void savePathChanged();
    void downloadingChanged();
    void sigProgress(qint64 bytesRead, qint64 totalBytes, qreal progress);
    void sigDownloadFinished();
    void sigDownloadError(const QString& errorMessage);

private slots:
    void httpFinished();
    void httpReadyRead();
    void networkReplyProgress(qint64 bytesRead, qint64 totalBytes);

private:
    void startRequest(const QUrl& requestedUrl);
    std::unique_ptr<QFile> openFileForWrite(const QString& fileName);
    QString getDefaultDownloadPath() const;

private:
    QString m_downloadUrl;
    QString m_savePath;
    QNetworkAccessManager qnam;
    QNetworkReply* reply = nullptr;
    std::unique_ptr<QFile> file;
    QUrl url;
    bool httpRequestAborted = false;
    const QString defaultFileName = "download.file";
};

#endif // DOWNLOADTOOL_H

DownloadTool.cpp文件源码

复制代码
#include "DownloadTool.h"
#include <QCoreApplication>
#include <QDebug>

DownloadTool::DownloadTool(QObject* parent)
    : QObject(parent)
{
}

DownloadTool::DownloadTool(const QString& downloadUrl, const QString& savePath, QObject* parent)
    : QObject(parent)
{
    m_downloadUrl = downloadUrl;
    m_savePath = savePath;
}

DownloadTool::~DownloadTool()
{
    if (reply) {
        reply->deleteLater();
    }
}

QString DownloadTool::downloadUrl() const
{
    return m_downloadUrl;
}

void DownloadTool::setDownloadUrl(const QString& url)
{
    if (m_downloadUrl != url) {
        m_downloadUrl = url;
        emit downloadUrlChanged();
    }
}

QString DownloadTool::savePath() const
{
    return m_savePath;
}

void DownloadTool::setSavePath(const QString& path)
{
    if (m_savePath != path) {
        m_savePath = path;
        emit savePathChanged();
    }
}

bool DownloadTool::downloading() const
{
    return reply && reply->isRunning();
}

QString DownloadTool::getDefaultDownloadPath() const
{
    // 使用 QCoreApplication 而不是 QApplication
    return QCoreApplication::applicationDirPath() + "/downloads";
}

void DownloadTool::startDownload()
{
    if (downloading()) {
        return;
    }

    const QUrl newUrl = QUrl::fromUserInput(m_downloadUrl);

    if (!newUrl.isValid()) {
#ifdef DOWNLOAD_DEBUG
        qDebug() << QString("Invalid URL: %1: %2").arg(m_downloadUrl, newUrl.errorString());
#endif // DOWNLOAD_DEBUG
        emit sigDownloadError("Invalid URL: " + newUrl.errorString());
        return;
    }

    QString fileName = newUrl.fileName();
    if (fileName.isEmpty()) fileName = defaultFileName;

    if (m_savePath.isEmpty()) {
        m_savePath = getDefaultDownloadPath();
    }

    if (!QFileInfo(m_savePath).isDir()) {
        QDir dir;
        dir.mkpath(m_savePath);
    }

    fileName.prepend(m_savePath + '/');
    if (QFile::exists(fileName)) {
        QFile::remove(fileName);
    }

    file = openFileForWrite(fileName);
    if (!file) {
        emit sigDownloadError("Unable to create file for writing");
        return;
    }

    startRequest(newUrl);
    emit downloadingChanged();
}

void DownloadTool::cancelDownload()
{
    if (!downloading()) {
        return;
    }

    httpRequestAborted = true;
    reply->abort();
    emit downloadingChanged();
}

void DownloadTool::httpFinished()
{
    emit downloadingChanged();

    QFileInfo fi;
    if (file) {
        fi.setFile(file->fileName());
        file->close();
        file.reset();
    }

    if (httpRequestAborted) {
        return;
    }

    if (reply->error()) {
        QFile::remove(fi.absoluteFilePath());
        QString errorString = QString("Download failed: %1.").arg(reply->errorString());
#ifdef DOWNLOAD_DEBUG
        qDebug() << errorString;
#endif // DOWNLOAD_DEBUG
        emit sigDownloadError(errorString);
        return;
    }

    const QVariant redirectionTarget = reply->attribute(QNetworkRequest::RedirectionTargetAttribute);

    if (!redirectionTarget.isNull()) {
        const QUrl redirectedUrl = url.resolved(redirectionTarget.toUrl());
        file = openFileForWrite(fi.absoluteFilePath());
        if (!file) {
            emit sigDownloadError("Unable to create file for redirection");
            return;
        }
        startRequest(redirectedUrl);
        return;
    }

    reply->deleteLater();
    reply = nullptr;

    emit sigDownloadFinished();

#ifdef DOWNLOAD_DEBUG
    qDebug() << QString("Downloaded %1 bytes to %2 in %3")
        .arg(fi.size()).arg(fi.fileName(), QDir::toNativeSeparators(fi.absolutePath()));
    qDebug() << "Finished";
#endif // DOWNLOAD_DEBUG
}

void DownloadTool::httpReadyRead()
{
    if (file) file->write(reply->readAll());
}

void DownloadTool::networkReplyProgress(qint64 bytesRead, qint64 totalBytes)
{
    qreal progress = totalBytes > 0 ? qreal(bytesRead) / qreal(totalBytes) : 0;
    emit sigProgress(bytesRead, totalBytes, progress);

#ifdef DOWNLOAD_DEBUG
    qDebug() << QString::number(progress * 100, 'f', 2) << "%    "
        << bytesRead / (1024 * 1024) << "MB" << "/" << totalBytes / (1024 * 1024) << "MB";
#endif // DOWNLOAD_DEBUG
}

void DownloadTool::startRequest(const QUrl& requestedUrl)
{
    url = requestedUrl;
    httpRequestAborted = false;

    reply = qnam.get(QNetworkRequest(url));
    connect(reply, &QNetworkReply::finished, this, &DownloadTool::httpFinished);
    connect(reply, &QIODevice::readyRead, this, &DownloadTool::httpReadyRead);
    connect(reply, &QNetworkReply::downloadProgress, this, &DownloadTool::networkReplyProgress);

#ifdef DOWNLOAD_DEBUG
    qDebug() << QString("Downloading %1...").arg(url.toString());
#endif // DOWNLOAD_DEBUG
}

std::unique_ptr<QFile> DownloadTool::openFileForWrite(const QString& fileName)
{
    std::unique_ptr<QFile> file(new QFile(fileName));
    if (!file->open(QIODevice::WriteOnly)) {
#ifdef DOWNLOAD_DEBUG
        qDebug() << QString("Unable to save the file %1: %2.")
            .arg(QDir::toNativeSeparators(fileName), file->errorString());
#endif // DOWNLOAD_DEBUG
        return nullptr;
    }
    return file;
}

main.qml文件源码

复制代码
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
import QtQuick.Controls.Material 2.12
import DownloadTool 1.0

ApplicationWindow {
    id: window
    width: 700
    height: 550
    minimumWidth: 600
    minimumHeight: 450
    title: "HTTP 文件下载器"
    visible: true

    // 颜色定义
    property color primaryColor: "#2196F3"
    property color successColor: "#4CAF50"
    property color errorColor: "#F44336"
    property color warningColor: "#FF9800"
    property color textColor: "#333333"
    property color lightGray: "#F5F5F5"
    property color borderColor: "#E0E0E0"

    Material.theme: Material.Light
    Material.accent: primaryColor

    DownloadTool {
        id: downloadTool
        downloadUrl: urlTextField.text
        savePath: pathTextField.text

        onSigProgress: {
            progressBar.value = progress
            progressText.text = formatBytes(bytesRead) + " / " + formatBytes(totalBytes) +
                               " (" + (progress * 100).toFixed(1) + "%)"

            // 计算下载速度
            if (speedTimer.running) {
                var currentTime = new Date().getTime()
                var timeDiff = (currentTime - speedTimer.lastTime) / 1000 // 转换为秒
                if (timeDiff > 0) {
                    var bytesDiff = bytesRead - speedTimer.lastBytes
                    var speed = bytesDiff / timeDiff
                    speedText.text = "Speed: " + formatBytes(speed) + "/s"
                }
                speedTimer.lastBytes = bytesRead
                speedTimer.lastTime = currentTime
            }
        }

        onSigDownloadFinished: {
            statusText.text = "✓ 下载完成!"
            statusText.color = successColor
            updateButtonStates()
            progressBar.Material.accent = successColor
            speedText.text = ""
        }

        onSigDownloadError: {
            statusText.text = "✗ Error: " + errorMessage
            statusText.color = errorColor
            updateButtonStates()
            progressBar.value = 0
            speedText.text = ""
        }

        onDownloadingChanged: {
            if (downloading) {
                statusText.text = "⏳ 下载中..."
                statusText.color = primaryColor
                progressBar.Material.accent = primaryColor
                speedTimer.start()
            } else {
                speedTimer.stop()
            }
            updateButtonStates()
        }
    }

    Timer {
        id: speedTimer
        interval: 1000
        repeat: true
        running: false
        property real lastBytes: 0
        property real lastTime: new Date().getTime()

        onTriggered: {
            // 定时更新速度显示
        }
    }

    function formatBytes(bytes) {
        if (bytes === 0) return "0 B";
        const k = 1024;
        const sizes = ['B', 'KB', 'MB', 'GB'];
        const i = Math.floor(Math.log(bytes) / Math.log(k));
        return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
    }

    function updateButtonStates() {
        downloadButton.enabled = urlTextField.text !== "" && !downloadTool.downloading
        cancelButton.enabled = downloadTool.downloading
        pauseResumeButton.enabled = downloadTool.downloading
    }

    Rectangle {
        anchors.fill: parent
        color: lightGray
    }

    ColumnLayout {
        anchors.fill: parent
        anchors.margins: 25
        spacing: 20

        // 标题区域
        Rectangle {
            Layout.fillWidth: true
            Layout.preferredHeight: 80
            color: primaryColor
            radius: 12

            RowLayout {
                anchors.fill: parent
                anchors.margins: 20

                Label {
                    text: "📥"
                    font.pixelSize: 32
                    Layout.alignment: Qt.AlignVCenter
                }

                ColumnLayout {
                    Layout.fillWidth: true
                    spacing: 4

                    Label {
                        text: "http 文件下载器"
                        font.pixelSize: 24
                        font.bold: true
                        color: "white"
                        Layout.fillWidth: true
                    }

                    Label {
                        text: "下载文件"
                        font.pixelSize: 14
                        color: "white"
                        opacity: 0.9
                        Layout.fillWidth: true
                    }
                }
            }
        }

        // 下载设置卡片
        Rectangle {
            Layout.fillWidth: true
            Layout.preferredHeight: 140
            color: "white"
            radius: 12
            border.color: borderColor
            border.width: 1

            ColumnLayout {
                anchors.fill: parent
                anchors.margins: 20
                spacing: 15

                Label {
                    text: "下载设置"
                    font.pixelSize: 16
                    font.bold: true
                    color: textColor
                    Layout.fillWidth: true
                }

                ColumnLayout {
                    Layout.fillWidth: true
                    spacing: 12

                    RowLayout {
                        Layout.fillWidth: true
                        spacing: 10

                        Label {
                            text: "🔗"
                            font.pixelSize: 16
                            Layout.preferredWidth: 30
                        }

                        TextField {
                            id: urlTextField
                            Layout.fillWidth: true
                            placeholderText: "https://dl.360safe.com/pclianmeng/n/3__3112523__3f7372633d6c6d266c733d6e33366163663466393961__68616f2e3336302e636e__0cd2.exe"
                            font.pixelSize: 14
                            selectByMouse: true
                            onTextChanged: updateButtonStates()

                            background: Rectangle {
                                radius: 6
                                border.color: urlTextField.activeFocus ? primaryColor : borderColor
                                border.width: 1
                                color: "transparent"
                            }
                        }
                    }

                    RowLayout {
                        Layout.fillWidth: true
                        spacing: 10

                        Label {
                            text: "📁"
                            font.pixelSize: 16
                            Layout.preferredWidth: 30
                        }

                        TextField {
                            id: pathTextField
                            Layout.fillWidth: true
                            placeholderText: "Enter save directory path"
                            font.pixelSize: 14
                            selectByMouse: true

                            background: Rectangle {
                                radius: 6
                                border.color: pathTextField.activeFocus ? primaryColor : borderColor
                                border.width: 1
                                color: "transparent"
                            }
                        }
                    }
                }
            }
        }

        // 进度卡片
        Rectangle {
            Layout.fillWidth: true
            Layout.preferredHeight: 180
            color: "white"
            radius: 12
            border.color: borderColor
            border.width: 1

            ColumnLayout {
                anchors.fill: parent
                anchors.margins: 20
                spacing: 15

                Label {
                    text: "下载进度"
                    font.pixelSize: 16
                    font.bold: true
                    color: textColor
                    Layout.fillWidth: true
                }

                ColumnLayout {
                    Layout.fillWidth: true
                    spacing: 12

                    ProgressBar {
                        id: progressBar
                        Layout.fillWidth: true
                        from: 0
                        to: 1
                        value: 0

                        background: Rectangle {
                            implicitHeight: 8
                            color: "#e0e0e0"
                            radius: 4
                        }

                        contentItem: Item {
                            implicitHeight: 8

                            Rectangle {
                                width: progressBar.visualPosition * parent.width
                                height: parent.height
                                radius: 4
                                color: progressBar.Material.accent
                            }
                        }
                    }

                    RowLayout {
                        Layout.fillWidth: true

                        Label {
                            id: progressText
                            text: "准备下载"
                            font.pixelSize: 13
                            color: textColor
                            opacity: 0.8
                            Layout.fillWidth: true
                        }

                        Label {
                            id: speedText
                            text: ""
                            font.pixelSize: 13
                            color: primaryColor
                            font.bold: true
                        }
                    }

                    // 状态显示
                    Rectangle {
                        Layout.fillWidth: true
                        Layout.preferredHeight: 50
                        color: "transparent"
                        border.color: borderColor
                        border.width: 1
                        radius: 8

                        Label {
                            id: statusText
                            anchors.centerIn: parent
                            text: "输入url 点击下载"
                            font.pixelSize: 14
                            color: textColor
                            opacity: 0.7
                        }
                    }
                }
            }
        }

        // 按钮区域
        RowLayout {
            Layout.alignment: Qt.AlignHCenter
            spacing: 15

            Button {
                id: downloadButton
                text: "🚀 下载"
                Material.background: primaryColor
                Material.foreground: "white"
                font.pixelSize: 14
                font.bold: true
                enabled: urlTextField.text !== "" && !downloadTool.downloading
                onClicked: {
                    progressBar.value = 0
                    progressText.text = "开始下载..."
                    speedTimer.lastBytes = 0
                    speedTimer.lastTime = new Date().getTime()
                    downloadTool.startDownload()
                }

                contentItem: Label {
                    text: downloadButton.text
                    font: downloadButton.font
                    color: downloadButton.enabled ? "white" : "#999"
                    horizontalAlignment: Text.AlignHCenter
                    verticalAlignment: Text.AlignVCenter
                }

                background: Rectangle {
                    radius: 8
                    color: downloadButton.enabled ? primaryColor : "#E0E0E0"
                }
            }

            Button {
                id: pauseResumeButton
                text: "⏸️ 暂停"
                Material.background: warningColor
                Material.foreground: "white"
                font.pixelSize: 14
                enabled: false // 暂停功能需要额外实现
                visible: false // 暂时隐藏,需要额外实现暂停功能

                background: Rectangle {
                    radius: 8
                    color: pauseResumeButton.enabled ? warningColor : "#E0E0E0"
                }
            }

            Button {
                id: cancelButton
                text: "❌ 取消"
                Material.background: errorColor
                Material.foreground: "white"
                font.pixelSize: 14
                font.bold: true
                enabled: downloadTool.downloading
                onClicked: {
                    downloadTool.cancelDownload()
                    statusText.text = "下载取消"
                    statusText.color = warningColor
                }

                background: Rectangle {
                    radius: 8
                    color: cancelButton.enabled ? errorColor : "#E0E0E0"
                }
            }
        }

        // 底部信息
        Rectangle {
            Layout.fillWidth: true
            Layout.preferredHeight: 40
            color: "transparent"

            Label {
                anchors.centerIn: parent
                text: "v1.0"
                font.pixelSize: 12
                color: textColor
                opacity: 0.5
            }
        }
    }

    Component.onCompleted: {
        // 设置默认下载路径
        if (pathTextField.text === "") {
            pathTextField.text = "downloads"
        }
        updateButtonStates()

        // 设置示例URL(可选)
        // urlTextField.text = "https://www.example.com/sample-file.zip"
    }
}

main.cpp文件源码

复制代码
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QIcon>
#include "DownloadTool.h"

int main(int argc, char *argv[])
{
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
    QGuiApplication app(argc, argv);

    app.setOrganizationName("YourCompany");
    app.setApplicationName("FileDownloader");

    // 注册 C++ 类型到 QML
    qmlRegisterType<DownloadTool>("DownloadTool", 1, 0, "DownloadTool");

    QQmlApplicationEngine engine;
    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));

    if (engine.rootObjects().isEmpty())
        return -1;

    return app.exec();
}

三、效果演示

输入下载文件的url,点击下载,下载开始同时显示进度条。

相关推荐
我梦之64 小时前
libevent输出缓存区的数据
服务器·网络·c++·缓存
磨十三4 小时前
C++ 单例模式(Singleton)详解
c++·单例模式
fakaifa4 小时前
【全开源】企业微信SCRM社群营销高级版系统+uniapp前端
uni-app·开源·企业微信·scrm·源码下载·企业微信scrm
气氛小组长4 小时前
继原生ACME支持后:我们让NGINX的自动证书变得更简单
开源
CoderJia程序员甲4 小时前
GitHub 热榜项目 - 日榜(2025-10-10)
ai·开源·大模型·github·ai教程
Nuyoah11klay5 小时前
华清远见25072班C++学习day7
c++
bkspiderx5 小时前
C++设计模式之行为型模式:迭代器模式(Iterator)
c++·设计模式·迭代器模式
ajassi20005 小时前
开源 java android app 开发(十八)最新编译器Android Studio 2025.1.3.7
android·java·开源
DolphinScheduler社区6 小时前
# 3.1.8<3.2.0<3.3.1,Apache DolphinScheduler集群升级避坑指南
java·大数据·开源·apache·任务调度·海豚调度