Qt/C++/QML HTTP Multipart/Form-Data 上传下载示例

下面我将为您提供一个完整的示例,展示如何使用Qt+C++和QML实现HTTP multipart/form-data格式的文件上传和下载功能。

项目结构

HttpDemo/
  ├── HttpDemo.pro
  ├── main.cpp
  ├── FileUploadDownloader.h
  ├── FileUploadDownloader.cpp
  └── qml/
      └── main.qml

项目文件

首先创建项目文件:

QT += quick network

CONFIG += c++11

SOURCES += \
    main.cpp \
    FileUploadDownloader.cpp

HEADERS += \
    FileUploadDownloader.h

RESOURCES += qml.qrc

# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target

# 添加资源文件
qml.files = qml/main.qml
qml.prefix = /
RESOURCES += qml

C++ 头文件

#ifndef FILEUPLOADDOWNLOADER_H
#define FILEUPLOADDOWNLOADER_H

#include <QObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QFile>
#include <QUrl>
#include <QHttpMultiPart>

class FileUploadDownloader : public QObject
{
    Q_OBJECT
    
    Q_PROPERTY(bool uploading READ isUploading NOTIFY uploadingChanged)
    Q_PROPERTY(bool downloading READ isDownloading NOTIFY downloadingChanged)
    Q_PROPERTY(double uploadProgress READ uploadProgress NOTIFY uploadProgressChanged)
    Q_PROPERTY(double downloadProgress READ downloadProgress NOTIFY downloadProgressChanged)
    
public:
    explicit FileUploadDownloader(QObject *parent = nullptr);
    ~FileUploadDownloader();
    
    Q_INVOKABLE bool uploadFile(const QString &filePath, const QString &url, const QString &fieldName = "file");
    Q_INVOKABLE bool downloadFile(const QString &url, const QString &savePath);
    Q_INVOKABLE void cancelUpload();
    Q_INVOKABLE void cancelDownload();
    
    bool isUploading() const;
    bool isDownloading() const;
    double uploadProgress() const;
    double downloadProgress() const;
    
signals:
    void uploadingChanged();
    void downloadingChanged();
    void uploadProgressChanged();
    void downloadProgressChanged();
    
    void uploadFinished(bool success, const QString &message);
    void downloadFinished(bool success, const QString &message, const QString &filePath);
    
private slots:
    void onUploadProgress(qint64 bytesSent, qint64 bytesTotal);
    void onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal);
    void onUploadFinished();
    void onDownloadFinished();
    
private:
    QNetworkAccessManager *m_networkManager;
    QNetworkReply *m_uploadReply;
    QNetworkReply *m_downloadReply;
    QFile *m_downloadFile;
    QHttpMultiPart *m_multiPart;
    
    bool m_uploading;
    bool m_downloading;
    double m_uploadProgress;
    double m_downloadProgress;
    QString m_downloadPath;
};

#endif // FILEUPLOADDOWNLOADER_H

C++ 实现文件

#include "FileUploadDownloader.h"
#include <QFileInfo>
#include <QHttpPart>
#include <QMimeDatabase>

FileUploadDownloader::FileUploadDownloader(QObject *parent)
    : QObject(parent)
    , m_networkManager(new QNetworkAccessManager(this))
    , m_uploadReply(nullptr)
    , m_downloadReply(nullptr)
    , m_downloadFile(nullptr)
    , m_multiPart(nullptr)
    , m_uploading(false)
    , m_downloading(false)
    , m_uploadProgress(0.0)
    , m_downloadProgress(0.0)
{
}

FileUploadDownloader::~FileUploadDownloader()
{
    cancelUpload();
    cancelDownload();
}

bool FileUploadDownloader::uploadFile(const QString &filePath, const QString &url, const QString &fieldName)
{
    // 检查文件是否存在
    QFileInfo fileInfo(filePath);
    if (!fileInfo.exists() || !fileInfo.isFile()) {
        emit uploadFinished(false, "文件不存在");
        return false;
    }
    
    // 检查是否已经在上传
    if (m_uploading) {
        emit uploadFinished(false, "已有上传任务正在进行");
        return false;
    }
    
    // 打开文件
    QFile *file = new QFile(filePath);
    if (!file->open(QIODevice::ReadOnly)) {
        delete file;
        emit uploadFinished(false, "无法打开文件: " + file->errorString());
        return false;
    }
    
    // 设置HTTP multipart
    m_multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
    
    // 添加文件部分
    QHttpPart filePart;
    
    // 设置Content-Disposition头
    QString contentDisposition = QString("form-data; name=\"%1\"; filename=\"%2\"")
                                .arg(fieldName)
                                .arg(fileInfo.fileName());
    filePart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant(contentDisposition));
    
    // 设置Content-Type头
    QMimeDatabase db;
    QMimeType mime = db.mimeTypeForFile(filePath);
    filePart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant(mime.name()));
    
    // 设置文件内容
    filePart.setBodyDevice(file);
    file->setParent(m_multiPart); // 让multiPart来管理文件的生命周期
    
    m_multiPart->append(filePart);
    
    // 创建请求
    QNetworkRequest request(QUrl(url));
    
    // 发送POST请求
    m_uploadReply = m_networkManager->post(request, m_multiPart);
    m_multiPart->setParent(m_uploadReply); // 让reply来管理multiPart的生命周期
    
    // 连接信号
    connect(m_uploadReply, &QNetworkReply::uploadProgress, this, &FileUploadDownloader::onUploadProgress);
    connect(m_uploadReply, &QNetworkReply::finished, this, &FileUploadDownloader::onUploadFinished);
    
    // 更新状态
    m_uploading = true;
    emit uploadingChanged();
    
    return true;
}

bool FileUploadDownloader::downloadFile(const QString &url, const QString &savePath)
{
    // 检查是否已经在下载
    if (m_downloading) {
        emit downloadFinished(false, "已有下载任务正在进行", "");
        return false;
    }
    
    // 创建文件
    m_downloadFile = new QFile(savePath);
    if (!m_downloadFile->open(QIODevice::WriteOnly)) {
        QString errorMsg = m_downloadFile->errorString();
        delete m_downloadFile;
        m_downloadFile = nullptr;
        emit downloadFinished(false, "无法创建文件: " + errorMsg, "");
        return false;
    }
    
    // 创建请求
    QNetworkRequest request(QUrl(url));
    
    // 发送GET请求
    m_downloadReply = m_networkManager->get(request);
    
    // 连接信号
    connect(m_downloadReply, &QNetworkReply::downloadProgress, this, &FileUploadDownloader::onDownloadProgress);
    connect(m_downloadReply, &QNetworkReply::finished, this, &FileUploadDownloader::onDownloadFinished);
    connect(m_downloadReply, &QNetworkReply::readyRead, this, [this]() {
        if (m_downloadFile && m_downloadReply) {
            m_downloadFile->write(m_downloadReply->readAll());
        }
    });
    
    // 保存下载路径
    m_downloadPath = savePath;
    
    // 更新状态
    m_downloading = true;
    emit downloadingChanged();
    
    return true;
}

void FileUploadDownloader::cancelUpload()
{
    if (m_uploadReply) {
        m_uploadReply->abort();
        m_uploadReply->deleteLater();
        m_uploadReply = nullptr;
    }
    
    if (m_multiPart) {
        m_multiPart->deleteLater();
        m_multiPart = nullptr;
    }
    
    if (m_uploading) {
        m_uploading = false;
        emit uploadingChanged();
        emit uploadFinished(false, "上传已取消");
    }
    
    m_uploadProgress = 0.0;
    emit uploadProgressChanged();
}

void FileUploadDownloader::cancelDownload()
{
    if (m_downloadReply) {
        m_downloadReply->abort();
        m_downloadReply->deleteLater();
        m_downloadReply = nullptr;
    }
    
    if (m_downloadFile) {
        m_downloadFile->close();
        m_downloadFile->remove(); // 删除未完成的文件
        delete m_downloadFile;
        m_downloadFile = nullptr;
    }
    
    if (m_downloading) {
        m_downloading = false;
        emit downloadingChanged();
        emit downloadFinished(false, "下载已取消", "");
    }
    
    m_downloadProgress = 0.0;
    emit downloadProgressChanged();
}

bool FileUploadDownloader::isUploading() const
{
    return m_uploading;
}

bool FileUploadDownloader::isDownloading() const
{
    return m_downloading;
}

double FileUploadDownloader::uploadProgress() const
{
    return m_uploadProgress;
}

double FileUploadDownloader::downloadProgress() const
{
    return m_downloadProgress;
}

void FileUploadDownloader::onUploadProgress(qint64 bytesSent, qint64 bytesTotal)
{
    if (bytesTotal > 0) {
        m_uploadProgress = static_cast<double>(bytesSent) / bytesTotal;
    } else {
        m_uploadProgress = 0;
    }
    emit uploadProgressChanged();
}

void FileUploadDownloader::onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
{
    if (bytesTotal > 0) {
        m_downloadProgress = static_cast<double>(bytesReceived) / bytesTotal;
    } else {
        m_downloadProgress = 0;
    }
    emit downloadProgressChanged();
}

void FileUploadDownloader::onUploadFinished()
{
    bool success = false;
    QString message;
    
    if (m_uploadReply->error() == QNetworkReply::NoError) {
        success = true;
        message = "上传成功";
    } else {
        message = "上传失败: " + m_uploadReply->errorString();
    }
    
    // 清理资源
    m_uploadReply->deleteLater();
    m_uploadReply = nullptr;
    
    // 更新状态
    m_uploading = false;
    emit uploadingChanged();
    
    // 通知结果
    emit uploadFinished(success, message);
}

void FileUploadDownloader::onDownloadFinished()
{
    bool success = false;
    QString message;
    QString filePath = "";
    
    if (m_downloadReply->error() == QNetworkReply::NoError) {
        if (m_downloadFile) {
            // 写入剩余的数据
            m_downloadFile->write(m_downloadReply->readAll());
            m_downloadFile->flush();
            m_downloadFile->close();
            
            success = true;
            message = "下载成功";
            filePath = m_downloadPath;
        } else {
            message = "下载失败: 文件句柄无效";
        }
    } else {
        message = "下载失败: " + m_downloadReply->errorString();
    }
    
    // 清理资源
    if (m_downloadFile) {
        delete m_downloadFile;
        m_downloadFile = nullptr;
    }
    
    m_downloadReply->deleteLater();
    m_downloadReply = nullptr;
    
    // 更新状态
    m_downloading = false;
    emit downloadingChanged();
    
    // 通知结果
    emit downloadFinished(success, message, filePath);
}

主函数

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include "FileUploadDownloader.h"

int main(int argc, char *argv[])
{
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
    
    QGuiApplication app(argc, argv);
    
    // 创建上传下载处理器
    FileUploadDownloader uploader;
    
    QQmlApplicationEngine engine;
    
    // 将C++对象暴露给QML
    engine.rootContext()->setContextProperty("uploader", &uploader);
    
    // 加载QML
    engine.load(QUrl(QStringLiteral("qrc:/qml/main.qml")));
    
    if (engine.rootObjects().isEmpty())
        return -1;
    
    return app.exec();
}

QML用户界面

import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Controls 2.12
import QtQuick.Dialogs 1.3
import QtQuick.Layouts 1.12

Window {
    visible: true
    width: 600
    height: 400
    title: "HTTP上传下载示例"
    
    FileDialog {
        id: uploadFileDialog
        title: "选择要上传的文件"
        folder: shortcuts.home
        nameFilters: ["所有文件 (*.*)"]
        onAccepted: {
            selectedFileTextField.text = fileUrl.toString().replace("file:///", "")
        }
    }
    
    FileDialog {
        id: saveFileDialog
        title: "选择保存位置"
        folder: shortcuts.home
        selectExisting: false
        onAccepted: {
            let path = fileUrl.toString().replace("file:///", "")
            uploader.downloadFile(downloadUrlTextField.text, path)
        }
    }
    
    ColumnLayout {
        anchors.fill: parent
        anchors.margins: 20
        spacing: 10
        
        GroupBox {
            title: "文件上传"
            Layout.fillWidth: true
            
            GridLayout {
                anchors.fill: parent
                columns: 3
                rowSpacing: 10
                columnSpacing: 10
                
                Label { text: "服务器URL:" }
                TextField {
                    id: uploadUrlTextField
                    Layout.fillWidth: true
                    placeholderText: "例如:http://example.com/upload"
                    text: "http://httpbin.org/post"
                }
                Button {
                    text: "上传"
                    enabled: !uploader.uploading && selectedFileTextField.text != ""
                    onClicked: {
                        uploader.uploadFile(selectedFileTextField.text, uploadUrlTextField.text, "file")
                    }
                }
                
                Label { text: "选择文件:" }
                TextField {
                    id: selectedFileTextField
                    Layout.fillWidth: true
                    readOnly: true
                    placeholderText: "点击右侧按钮选择文件"
                }
                Button {
                    text: "浏览..."
                    onClicked: uploadFileDialog.open()
                }
                
                Label { text: "上传进度:" }
                ProgressBar {
                    id: uploadProgressBar
                    Layout.fillWidth: true
                    Layout.columnSpan: 2
                    from: 0
                    to: 1
                    value: uploader.uploadProgress
                }
                
                Label {
                    id: uploadStatusLabel
                    Layout.columnSpan: 3
                    Layout.fillWidth: true
                    wrapMode: Text.WordWrap
                }
            }
        }
        
        GroupBox {
            title: "文件下载"
            Layout.fillWidth: true
            
            GridLayout {
                anchors.fill: parent
                columns: 3
                rowSpacing: 10
                columnSpacing: 10
                
                Label { text: "文件URL:" }
                TextField {
                    id: downloadUrlTextField
                    Layout.fillWidth: true
                    placeholderText: "例如:http://example.com/file.zip"
                    text: "https://speed.hetzner.de/100MB.bin"
                }
                Button {
                    text: "下载"
                    enabled: !uploader.downloading && downloadUrlTextField.text != ""
                    onClicked: saveFileDialog.open()
                }
                
                Label { text: "下载进度:" }
                ProgressBar {
                    id: downloadProgressBar
                    Layout.fillWidth: true
                    Layout.columnSpan: 2
                    from: 0
                    to: 1
                    value: uploader.downloadProgress
                }
                
                Label {
                    id: downloadStatusLabel
                    Layout.columnSpan: 3
                    Layout.fillWidth: true
                    wrapMode: Text.WordWrap
                }
            }
        }
        
        Item { Layout.fillHeight: true }
        
        RowLayout {
            Layout.fillWidth: true
            
            Button {
                text: "取消上传"
                enabled: uploader.uploading
                onClicked: uploader.cancelUpload()
            }
            
            Button {
                text: "取消下载"
                enabled: uploader.downloading
                onClicked: uploader.cancelDownload()
            }
            
            Item { Layout.fillWidth: true }
            
            Button {
                text: "退出程序"
                onClicked: Qt.quit()
            }
        }
    }
    
    // 连接信号
    Connections {
        target: uploader
        
        onUploadFinished: {
            uploadStatusLabel.text = message
        }
        
        onDownloadFinished: {
            downloadStatusLabel.text = message + (success ? ": " + filePath : "")
        }
    }
}

资源文件

创建qml.qrc文件:

<!DOCTYPE RCC>
<RCC version="1.0">
    <qresource prefix="/">
        <file>qml/main.qml</file>
    </qresource>
</RCC>

使用说明

  1. 这个示例程序提供了文件上传和下载的功能:

    • 上传部分使用multipart/form-data格式发送文件
    • 下载部分进行普通的HTTP GET请求
  2. 上传示例中使用了http://httpbin.org/post作为测试服务器,这是一个测试HTTP请求的服务,会将你上传的数据以JSON格式返回

  3. 下载示例使用了https://speed.hetzner.de/100MB.bin作为测试文件

  4. 界面中提供了进度条显示上传/下载进度,以及取消按钮

  5. 程序会显示上传/下载的状态信息

备注

  1. 实际使用时,你需要将上传URL替换为你自己的服务器地址
  2. 如果需要添加额外的表单字段,可以在uploadFile方法中添加更多的QHttpPart
  3. 错误处理可以根据实际需求进一步完善
  4. 对于大文件,你可能需要实现断点续传功能

更多推荐