Qt5 进阶【7】网络请求与 REST API 实战:QNetworkAccessManager 深度应用

目标读者:已经看完前 1--6 期、具备一定 C++ 和 Qt 开发经验,希望在桌面项目中把"调用 HTTP/REST 接口"这一块设计规范、稳定好用的工程师。

开发环境示例:Qt 5.12 / 5.15 + Qt Creator + CMake(文中工程均以 CMake 为例,可自行改为 qmake)。

目录

[一、问题背景:从「能发一个 GET」到「能撑住线上场景」](#一、问题背景:从「能发一个 GET」到「能撑住线上场景」)

[1. UI 卡顿:网络一慢,整个界面跟着「休克」](#1. UI 卡顿:网络一慢,整个界面跟着「休克」)

[2. QNetworkAccessManager 到处 new,一会儿就「不知道谁在管网络」](#2. QNetworkAccessManager 到处 new,一会儿就「不知道谁在管网络」)

[3. 没有统一的超时和错误处理:接口挂住,全应用跟着「半死不活」](#3. 没有统一的超时和错误处理:接口挂住,全应用跟着「半死不活」)

[4. HTTPS / 证书问题:开发阶段一把 ignoreSslErrors(),上线埋雷](#4. HTTPS / 证书问题:开发阶段一把 ignoreSslErrors(),上线埋雷)

[5. 请求一多,就分不清「哪个响应对应哪个请求」](#5. 请求一多,就分不清「哪个响应对应哪个请求」)

[二、核心知识点:把 QNetworkAccessManager 真正「用对」](#二、核心知识点:把 QNetworkAccessManager 真正「用对」)

[1. Manager:少而精,长寿命](#1. Manager:少而精,长寿命)

[2. QNetworkRequest:配置一次,少填坑](#2. QNetworkRequest:配置一次,少填坑)

[3. QNetworkReply:记得收尾和错误分流](#3. QNetworkReply:记得收尾和错误分流)

[4. 超时管理:QNetworkReply 自身没有「超时」的概念](#4. 超时管理:QNetworkReply 自身没有「超时」的概念)

[5. HTTPS 与 SSL 错误:别一刀切全部 ignore](#5. HTTPS 与 SSL 错误:别一刀切全部 ignore)

[6. 请求队列与并发控制:让 QNetworkAccessManager「不乱」](#6. 请求队列与并发控制:让 QNetworkAccessManager「不乱」)

[三、完整工程实战:GitHub REST API 小客户端](#三、完整工程实战:GitHub REST API 小客户端)

[1. 工程结构](#1. 工程结构)

[2. CMakeLists.txt(可直接使用)](#2. CMakeLists.txt(可直接使用))

[3. RequestInfo:封装一次 HTTP 调用的参数与结果](#3. RequestInfo:封装一次 HTTP 调用的参数与结果)

[4. NetworkManager:统一管理 QNetworkAccessManager](#4. NetworkManager:统一管理 QNetworkAccessManager)

[5. ApiClient:面向业务的 REST 客户端](#5. ApiClient:面向业务的 REST 客户端)

[6. MainWindow:一个简单的 GitHub 客户端界面](#6. MainWindow:一个简单的 GitHub 客户端界面)

[7. main.cpp:程序入口](#7. main.cpp:程序入口)

[四、实战中的坑与优化:结合这个 Demo 再看几条经验](#四、实战中的坑与优化:结合这个 Demo 再看几条经验)

[1. 把「请求发起」和「结果处理」分开](#1. 把「请求发起」和「结果处理」分开)

[2. 不要在 UI 层直接处理 QNetworkReply](#2. 不要在 UI 层直接处理 QNetworkReply)

[3. 超时不是「可选项」,而是「必需项」](#3. 超时不是「可选项」,而是「必需项」)

[4. 日志与调试](#4. 日志与调试)

[五、小结:项目中可落地的 Qt 网络访问实践清单](#五、小结:项目中可落地的 Qt 网络访问实践清单)


一、问题背景:从「能发一个 GET」到「能撑住线上场景」

很多人第一次在 Qt 里发 HTTP 请求,大概都是下面这种写法:

cpp 复制代码
QNetworkAccessManager mgr;
QNetworkRequest req(QUrl("https://api.example.com/data"));
QNetworkReply *reply = mgr.get(req);
QByteArray data = reply->readAll(); // 甚至直接这样同步用

能不能跑?能。但一旦放进真实项目,就会暴露出一堆问题。

1. UI 卡顿:网络一慢,整个界面跟着「休克」

常见现象:

  • 点一个「同步」按钮后,窗口直接假死几十秒;
  • 鼠标拖动窗口边框不跟手,甚至无法关闭窗口;
  • 明明只是请求个接口,用户感觉像程序崩溃了一样。

本质上还是把网络 I/O 写成了「同步逻辑」:

cpp 复制代码
// 各种 waitFor... / 循环 + processEvents 之类的同步逻辑
reply->waitForReadyRead();  // 或者 while(!reply->isFinished()) { ... }

这在 Demo 里或内网接口上看不出问题,一到弱网或偶发抖动的环境,体验就会崩。

2. QNetworkAccessManager 到处 new,一会儿就「不知道谁在管网络」

我见过不少类似的代码:

cpp 复制代码
void SomeDialog::doRequest()
{
    auto *mgr = new QNetworkAccessManager; // 没有 parent
    connect(mgr, &QNetworkAccessManager::finished, this, &SomeDialog::handleReply);
    mgr->get(QNetworkRequest(QUrl("https://...")));
}

跑起来之后:

  • 请求确实发出去了,也有响应;
  • mgr 一直没人 delete,进程内存慢慢爬;
  • 多个模块各自 new 一个 Manager,连接复用不好、Cookie 管理混乱。

QNetworkAccessManager 本身设计的思路是「少量长寿命实例」,而不是每发一次请求就 new 一次。

3. 没有统一的超时和错误处理:接口挂住,全应用跟着「半死不活」

典型写法是:

  • 只连了 finished(),没处理 errorOccurred()
  • 没有专门的超时逻辑,只能等网络栈自己放弃(可能是几十秒甚至更久);
  • 业务代码里只知道「没回调」,很难区分是超时、DNS 失败,还是 TLS 出问题。

一旦后台某个接口出现问题,前台就会出现长时间「转圈圈」,用户完全不知道发生了什么。

4. HTTPS / 证书问题:开发阶段一把 ignoreSslErrors(),上线埋雷

不少人遇到自签名证书 / 企业代理抓 HTTPS 时,第一反应是:

cpp 复制代码
connect(reply, &QNetworkReply::sslErrors,
        this, [](const QList<QSslError> &errors){
    Q_UNUSED(errors);
    reply->ignoreSslErrors();
});

开发阶段看起来很「爽」,什么都不挡。但真实发布时:

  • 很可能绕过了本该生效的安全校验;
  • 遇到环境里 TLS 配置不一致时,又会变成「某些机器老是报错」的玄学问题。

5. 请求一多,就分不清「哪个响应对应哪个请求」

例如:

cpp 复制代码
connect(manager, &QNetworkAccessManager::finished,
        this, &MainWindow::onReplyFinished);

在槽函数里,只能通过 qobject_cast<QNetworkReply*>(sender()) 判断是谁,但:

  • 如果你同时请求了「用户信息、仓库列表、Issue 列表」等多个接口;
  • 不做额外封装,槽函数就会变成一个大号 switch-case,极不利于维护。

这一期,我们的目标不是「教你发一个 GET/POST」,而是搭一整套可以直接迁入项目的网络访问小框架,解决:

  • QNetworkAccessManager 的生命周期和复用;
  • 请求的封装、队列和并发控制;
  • 超时与错误处理;
  • 简单的 REST API 封装(以 GitHub API 为例);
  • 在 UI 中清晰展示请求状态和错误信息。

二、核心知识点:把 QNetworkAccessManager 真正「用对」

1. Manager:少而精,长寿命

建议:整个应用中,按「业务域」划分 QNetworkAccessManager,而不是「一个请求一个 Manager」。

典型做法:

  • 应用层提供一个 NetworkManager 单例;
  • 内部持有一个或少量 QNetworkAccessManager;
  • 所有请求都通过这个单例调度,方便统一配置(代理、Cookie、SSL 策略等)。
cpp 复制代码
class NetworkManager : public QObject
{
    Q_OBJECT
public:
    static NetworkManager *instance();
    QNetworkAccessManager *manager() { return &m_mgr; }

private:
    explicit NetworkManager(QObject *parent = nullptr);
    QNetworkAccessManager m_mgr;
};

这样:

  • Manager 的生命周期和应用一致;
  • 可以很容易在一个地方挂统一的日志 / 代理 / 身份认证逻辑。

2. QNetworkRequest:配置一次,少填坑

常用要点:

  • URLsetUrl(QUrl("https://api.github.com/user"))
  • 通用头部:User-Agent、Accept、Authorization 等;
  • 缓存策略setAttribute(QNetworkRequest::CacheLoadControlAttribute, ...)
  • 超时提示:可以通过 Attribute 或自定义属性配合 QTimer 使用。

示例:

cpp 复制代码
QNetworkRequest req(QUrl("https://api.github.com/user/repos"));
req.setHeader(QNetworkRequest::UserAgentHeader, "QtRestDemo/1.0");
req.setRawHeader("Accept", "application/json");
// 自定义属性(超时毫秒)
req.setAttribute(QNetworkRequest::User, 15000);

3. QNetworkReply:记得收尾和错误分流

核心模式(推荐写法):

cpp 复制代码
QNetworkReply *reply = mgr->get(req);
connect(reply, &QNetworkReply::finished, this, [this, reply]{
    reply->deleteLater();          // 无论成功失败,最后一定要释放

    if (reply->error() != QNetworkReply::NoError) {
        // 统一错误处理
        handleNetworkError(reply);
        return;
    }

    QByteArray body = reply->readAll();
    // 解析 JSON / 文本
});

特别注意:

  • 不要在 finished 之前读数据(除非你明确知道在 readyRead 阶段就拿数据);
  • 成功与失败都要有日志,方便排查;
  • 如果你准备封装成一个「请求对象」,最好在里面统一管理 deleteLater()

4. 超时管理:QNetworkReply 自身没有「超时」的概念

典型的模式是给每个请求配一个 QTimer:

cpp 复制代码
QNetworkReply *reply = mgr->get(req);
QTimer *timer = new QTimer(reply);  // 以 reply 作为 parent,方便自动销毁
timer->setSingleShot(true);
timer->setInterval(timeoutMs);

connect(timer, &QTimer::timeout, reply, [reply]{
    if (reply->isRunning()) {
        reply->abort(); // 触发 error() 和 finished()
    }
});

connect(reply, &QNetworkReply::finished, timer, &QTimer::stop);

timer->start();

这样:

  • Timeout 的资源和 reply 绑定,不用担心泄漏;
  • abort() 后会触发 error 回调,那里可以统一判断 TimeoutError。

5. HTTPS 与 SSL 错误:别一刀切全部 ignore

对于自己控制的测试环境,可以针对性地忽略某些 SSL 错误;而在正式环境中,应该以「尽量不忽略」为原则。

基础处理模式:

cpp 复制代码
connect(reply, &QNetworkReply::sslErrors,
        this, [this, reply](const QList<QSslError> &errors){
    bool canIgnore = true;
    for (const QSslError &e : errors) {
        // 对可以接受的错误单独判断(比如某些测试环境)
        if (e.error() == QSslError::SelfSignedCertificate) {
            continue;
        }
        canIgnore = false;
        qWarning() << "SSL error:" << e.errorString();
    }
    if (canIgnore) {
        reply->ignoreSslErrors();
    } else {
        reply->abort();
    }
});

6. 请求队列与并发控制:让 QNetworkAccessManager「不乱」

QNetworkAccessManager 本身有连接复用与并发控制,但在业务层我们还是希望:

  • 控制「同时在飞」的请求数量(例如最多 4 个);
  • 超出部分进队列,按 FIFO 或优先级执行;
  • 提供取消当前所有请求的能力。

实现思路:

  • 自己维护一个 QQueue<RequestInfo*>
  • QSemaphore 控制并发;
  • 每个 RequestInfo 封装 URL、方法、头、body、超时和回调。

三、完整工程实战:GitHub REST API 小客户端

下面给出一个可以直接在 Qt Creator 中编译运行的 Demo 工程,名字叫 RestApiClient,主要功能:

  • 使用 GitHub API 获取当前用户信息;
  • 拉取用户的仓库列表;
  • 查看选中仓库的 Issues 列表;
  • 支持请求队列、基本错误处理和简单 UI 展示。

说明:为了方便演示,这里使用 GitHub 的公开 API,你需要自己准备一个 Personal Access Token(只勾选最小权限即可),填进界面中的 Token 输入框。

1. 工程结构

cpp 复制代码
RestApiClient/
├── CMakeLists.txt
├── include/
│   ├── networkmanager.h
│   ├── requestinfo.h
│   ├── apiclient.h
│   └── mainwindow.h
└── src/
    ├── main.cpp
    ├── networkmanager.cpp
    ├── requestinfo.cpp
    ├── apiclient.cpp
    └── mainwindow.cpp

为避免一次性太复杂,我这里不再写独立队列类,而是把关键能力收敛在 ApiClient + NetworkManager 里,方便你先看懂整体流程,再按需扩展。


2. CMakeLists.txt(可直接使用)

QtCreator可以自动生成。


3. RequestInfo:封装一次 HTTP 调用的参数与结果

include/requestinfo.h

cpp 复制代码
#pragma once

#include <QObject>
#include <QUrl>
#include <QVariantMap>
#include <QNetworkReply>

/**
 * @brief RequestInfo 封装一次 HTTP 请求的元数据与结果
 */
class RequestInfo : public QObject
{
    Q_OBJECT
public:
    enum Method {
        Get,
        Post,
        Put,
        Delete
    };
    Q_ENUM(Method)

    explicit RequestInfo(const QUrl &url,
                         Method method,
                         const QVariantMap &headers = {},
                         const QByteArray &body = {},
                         QObject *parent = nullptr);

    // 便于 ApiClient 访问的字段(这里只做演示,不做过度封装)
    QUrl url;
    Method method;
    QVariantMap headers;
    QByteArray body;

    // 结果
    QByteArray responseData;
    int statusCode = 0;
    QNetworkReply::NetworkError error = QNetworkReply::NoError;
    QString errorString;

    // 配置
    int timeoutMs = 30000;
};

src/requestinfo.cpp

cpp 复制代码
#include "requestinfo.h"

RequestInfo::RequestInfo(const QUrl &u,
                         Method m,
                         const QVariantMap &h,
                         const QByteArray &b,
                         QObject *parent)
    : QObject(parent),
      url(u),
      method(m),
      headers(h),
      body(b)
{
}

4. NetworkManager:统一管理 QNetworkAccessManager

include/networkmanager.h

cpp 复制代码
#pragma once

#include <QObject>
#include <QNetworkAccessManager>
#include <QPointer>

class RequestInfo;

/**
 * @brief NetworkManager 全局网络管理器
 *
 * - 持有一个 QNetworkAccessManager 实例
 * - 提供发送请求的统一入口
 */
class NetworkManager : public QObject
{
    Q_OBJECT
public:
    static NetworkManager *instance();

    // 发送请求(异步),完成后发射 finished(RequestInfo*)
    void send(RequestInfo *info);

signals:
    void finished(RequestInfo *info);

private:
    explicit NetworkManager(QObject *parent = nullptr);
    static QPointer<NetworkManager> s_instance;

    QNetworkAccessManager m_mgr;
};

src/networkmanager.cpp

cpp 复制代码
#include "networkmanager.h"
#include "requestinfo.h"

#include <QNetworkRequest>
#include <QTimer>
#include <QDebug>

QPointer<NetworkManager> NetworkManager::s_instance = nullptr;

NetworkManager *NetworkManager::instance()
{
    if (!s_instance) {
        s_instance = new NetworkManager();
    }
    return s_instance;
}

NetworkManager::NetworkManager(QObject *parent)
    : QObject(parent)
{
    // 可以在这里设置代理、网络配置等
    qDebug() << "NetworkManager created";
}

void NetworkManager::send(RequestInfo *info)
{
    if (!info) return;

    QNetworkRequest req(info->url);

    // 设置常用头部
    req.setHeader(QNetworkRequest::UserAgentHeader, "QtRestApiDemo/1.0");
    req.setRawHeader("Accept", "application/json");

    // 追加自定义头
    for (auto it = info->headers.constBegin(); it != info->headers.constEnd(); ++it) {
        req.setRawHeader(it.key().toUtf8(), it.value().toByteArray());
    }

    // 发起请求
    QNetworkReply *reply = nullptr;
    switch (info->method) {
    case RequestInfo::Get:
        reply = m_mgr.get(req);
        break;
    case RequestInfo::Post:
        reply = m_mgr.post(req, info->body);
        break;
    case RequestInfo::Put:
        reply = m_mgr.put(req, info->body);
        break;
    case RequestInfo::Delete:
        reply = m_mgr.deleteResource(req);
        break;
    }

    if (!reply) {
        info->error = QNetworkReply::UnknownNetworkError;
        info->errorString = "无法创建网络请求";
        emit finished(info);
        return;
    }

    // 超时处理
    int timeoutMs = info->timeoutMs > 0 ? info->timeoutMs : 30000;
    QTimer *timer = new QTimer(reply);
    timer->setSingleShot(true);
    timer->setInterval(timeoutMs);
    QObject::connect(timer, &QTimer::timeout, reply, [reply](){
        if (reply->isRunning()) {
            qWarning() << "Request timeout, aborting" << reply->url();
            reply->abort();
        }
    });
    timer->start();

    // finished
    QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, info, timer](){
        timer->stop();
        timer->deleteLater();

        info->error = reply->error();
        info->errorString = reply->errorString();
        info->responseData = reply->readAll();
        info->statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();

        reply->deleteLater();

        emit finished(info);
    });

    // sslErrors(示例中只打印)
    QObject::connect(reply, &QNetworkReply::sslErrors, this,
                     [reply](const QList<QSslError> &errors){
        for (const QSslError &e : errors) {
            qWarning() << "SSL error:" << e.errorString();
        }
        // 根据需要决定是否 ignore,这里演示不忽略
        Q_UNUSED(reply);
    });
}

5. ApiClient:面向业务的 REST 客户端

include/apiclient.h

cpp 复制代码
#pragma once

#include <QObject>
#include <functional>
#include <QVariantMap>
#include <QVariantList>

class RequestInfo;

/**
 * @brief ApiClient 简单 GitHub API 客户端
 *
 * 只演示几个典型接口:
 * - GET /user
 * - GET /user/repos
 * - GET /repos/{owner}/{repo}/issues
 */
class ApiClient : public QObject
{
    Q_OBJECT
public:
    explicit ApiClient(QObject *parent = nullptr);

    void setToken(const QString &token);

    // 回调签名:result, success, message
    using ApiCallback = std::function<void(const QVariant &result, bool ok, const QString &msg)>;

    void getCurrentUser(const ApiCallback &cb);
    void getUserRepos(const ApiCallback &cb);
    void getRepoIssues(const QString &owner,
                       const QString &repo,
                       const ApiCallback &cb);

private slots:
    void onRequestFinished(RequestInfo *info);

private:
    RequestInfo *createRequest(const QString &path,
                               const QString &method = "GET",
                               const QVariantMap &body = {});

    QVariant parseJson(const QByteArray &data, bool &ok, QString &msg);

private:
    QString m_baseUrl = "https://api.github.com";
    QString m_token;
    QHash<RequestInfo*, ApiCallback> m_callbacks;
};

src/apiclient.cpp

cpp 复制代码
#include "apiclient.h"
#include "networkmanager.h"
#include "requestinfo.h"

#include <QUrl>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QDebug>

ApiClient::ApiClient(QObject *parent)
    : QObject(parent)
{
    connect(NetworkManager::instance(), &NetworkManager::finished,
            this, &ApiClient::onRequestFinished);
}

void ApiClient::setToken(const QString &token)
{
    m_token = token;
}

RequestInfo *ApiClient::createRequest(const QString &path,
                                      const QString &method,
                                      const QVariantMap &body)
{
    QUrl url(m_baseUrl + path);
    RequestInfo::Method m = RequestInfo::Get;

    if (method.compare("POST", Qt::CaseInsensitive) == 0) {
        m = RequestInfo::Post;
    } else if (method.compare("PUT", Qt::CaseInsensitive) == 0) {
        m = RequestInfo::Put;
    } else if (method.compare("DELETE", Qt::CaseInsensitive) == 0) {
        m = RequestInfo::Delete;
    }

    QVariantMap headers;
    headers["Accept"] = "application/vnd.github+json";
    if (!m_token.isEmpty()) {
        headers["Authorization"] = "token " + m_token;
    }

    QByteArray bodyBytes;
    if (!body.isEmpty()) {
        QJsonDocument doc = QJsonDocument::fromVariant(body);
        bodyBytes = doc.toJson(QJsonDocument::Compact);
        headers["Content-Type"] = "application/json";
    }

    auto *info = new RequestInfo(url, m, headers, bodyBytes, this);
    info->timeoutMs = 20000; // 20 秒超时
    return info;
}

void ApiClient::getCurrentUser(const ApiCallback &cb)
{
    RequestInfo *info = createRequest("/user", "GET");
    m_callbacks.insert(info, cb);
    NetworkManager::instance()->send(info);
}

void ApiClient::getUserRepos(const ApiCallback &cb)
{
    // GitHub 支持 /user/repos 获取当前用户所有仓库
    RequestInfo *info = createRequest("/user/repos", "GET");
    m_callbacks.insert(info, cb);
    NetworkManager::instance()->send(info);
}

void ApiClient::getRepoIssues(const QString &owner,
                              const QString &repo,
                              const ApiCallback &cb)
{
    QString path = QString("/repos/%1/%2/issues").arg(owner, repo);
    RequestInfo *info = createRequest(path, "GET");
    m_callbacks.insert(info, cb);
    NetworkManager::instance()->send(info);
}

QVariant ApiClient::parseJson(const QByteArray &data, bool &ok, QString &msg)
{
    ok = false;
    msg.clear();

    QJsonParseError err;
    QJsonDocument doc = QJsonDocument::fromJson(data, &err);
    if (err.error != QJsonParseError::NoError) {
        msg = QString("JSON 解析错误: %1").arg(err.errorString());
        return {};
    }

    if (doc.isObject()) {
        ok = true;
        return doc.object().toVariantMap();
    } else if (doc.isArray()) {
        ok = true;
        return doc.array().toVariantList();
    } else {
        msg = "未知 JSON 根类型";
        return {};
    }
}

void ApiClient::onRequestFinished(RequestInfo *info)
{
    if (!m_callbacks.contains(info)) {
        // 不是这个客户端发起的请求
        return;
    }

    ApiCallback cb = m_callbacks.take(info);

    bool ok = false;
    QString msg;

    if (info->error != QNetworkReply::NoError) {
        msg = QString("网络错误(%1): %2")
                .arg(static_cast<int>(info->error))
                .arg(info->errorString);
    } else if (info->statusCode >= 400) {
        msg = QString("HTTP 错误 %1").arg(info->statusCode);
    } else {
        QVariant parsed = parseJson(info->responseData, ok, msg);
        if (cb) {
            cb(parsed, ok, msg);
        }
        info->deleteLater();
        return;
    }

    // 走到这里说明有错误
    if (cb) {
        cb(QVariant(), false, msg);
    }
    info->deleteLater();
}

6. MainWindow:一个简单的 GitHub 客户端界面

include/mainwindow.h

cpp 复制代码
#pragma once

#include <QMainWindow>

class QLineEdit;
class QPushButton;
class QLabel;
class QListWidget;
class QComboBox;
class QTextEdit;
class QProgressBar;

class ApiClient;

class MainWindow : public QMainWindow
{
    Q_OBJECT
public:
    explicit MainWindow(QWidget *parent = nullptr);

private slots:
    void onLoginClicked();
    void onFetchReposClicked();
    void onFetchIssuesClicked();
    void onClearIssuesClicked();

private:
    void setupUi();
    void setLoggedIn(bool loggedIn);
    void logStatus(const QString &text);

private:
    ApiClient   *m_client = nullptr;

    QLineEdit   *m_tokenEdit   = nullptr;
    QPushButton *m_loginBtn    = nullptr;
    QLabel      *m_userLabel   = nullptr;

    QPushButton *m_fetchReposBtn  = nullptr;
    QListWidget *m_repoList       = nullptr;
    QComboBox   *m_repoCombo      = nullptr;

    QPushButton *m_fetchIssuesBtn = nullptr;
    QPushButton *m_clearIssuesBtn = nullptr;
    QTextEdit   *m_issuesText     = nullptr;

    QProgressBar *m_progress      = nullptr;
    QLabel       *m_statusLabel   = nullptr;

    QString m_currentUser;
};

src/mainwindow.cpp

cpp 复制代码
#include "mainwindow.h"
#include "apiclient.h"

#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGroupBox>
#include <QLineEdit>
#include <QPushButton>
#include <QLabel>
#include <QListWidget>
#include <QComboBox>
#include <QTextEdit>
#include <QProgressBar>
#include <QMessageBox>
#include <QDebug>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    setWindowTitle(tr("Qt5 GitHub REST API 示例"));
    resize(960, 640);

    setupUi();

    m_client = new ApiClient(this);

    setLoggedIn(false);
}

void MainWindow::setupUi()
{
    QWidget *central = new QWidget(this);
    QVBoxLayout *mainLayout = new QVBoxLayout(central);

    // 登录区
    auto *loginGroup = new QGroupBox(tr("GitHub 登录"), this);
    auto *loginLayout = new QHBoxLayout(loginGroup);

    auto *tokenLabel = new QLabel(tr("Token:"), this);
    m_tokenEdit = new QLineEdit(this);
    m_tokenEdit->setEchoMode(QLineEdit::Password);
    m_tokenEdit->setPlaceholderText(tr("请输入 GitHub Personal Access Token"));
    m_loginBtn = new QPushButton(tr("登录"), this);

    loginLayout->addWidget(tokenLabel);
    loginLayout->addWidget(m_tokenEdit, 1);
    loginLayout->addWidget(m_loginBtn);
    loginGroup->setLayout(loginLayout);
    mainLayout->addWidget(loginGroup);

    // 用户信息
    auto *userLayout = new QHBoxLayout();
    m_userLabel = new QLabel(tr("未登录"), this);
    m_userLabel->setStyleSheet("font-weight:bold;");
    userLayout->addWidget(m_userLabel);
    userLayout->addStretch();
    mainLayout->addLayout(userLayout);

    // 控制按钮
    auto *ctrlLayout = new QHBoxLayout();
    m_fetchReposBtn = new QPushButton(tr("获取仓库列表"), this);
    m_fetchIssuesBtn = new QPushButton(tr("获取 Issues"), this);
    m_clearIssuesBtn = new QPushButton(tr("清空 Issues"), this);

    ctrlLayout->addWidget(m_fetchReposBtn);
    ctrlLayout->addWidget(m_fetchIssuesBtn);
    ctrlLayout->addWidget(m_clearIssuesBtn);
    ctrlLayout->addStretch();
    mainLayout->addLayout(ctrlLayout);

    // 主内容
    auto *contentLayout = new QHBoxLayout();

    // 左侧:仓库列表
    auto *leftLayout = new QVBoxLayout();
    auto *repoListLabel = new QLabel(tr("仓库列表:"), this);
    m_repoList = new QListWidget(this);

    leftLayout->addWidget(repoListLabel);
    leftLayout->addWidget(m_repoList);
    contentLayout->addLayout(leftLayout, 1);

    // 右侧:选择仓库 + Issues
    auto *rightLayout = new QVBoxLayout();
    auto *repoComboLabel = new QLabel(tr("当前仓库:"), this);
    m_repoCombo = new QComboBox(this);
    m_issuesText = new QTextEdit(this);
    m_issuesText->setReadOnly(true);

    rightLayout->addWidget(repoComboLabel);
    rightLayout->addWidget(m_repoCombo);
    rightLayout->addWidget(m_issuesText, 1);
    contentLayout->addLayout(rightLayout, 2);

    mainLayout->addLayout(contentLayout, 1);

    // 状态栏
    auto *bottomLayout = new QHBoxLayout();
    m_progress = new QProgressBar(this);
    m_progress->setRange(0, 0);
    m_progress->setVisible(false);
    m_statusLabel = new QLabel(tr("就绪"), this);

    bottomLayout->addWidget(m_progress);
    bottomLayout->addWidget(m_statusLabel);
    bottomLayout->addStretch();
    mainLayout->addLayout(bottomLayout);

    setCentralWidget(central);

    // 连接信号槽
    connect(m_loginBtn, &QPushButton::clicked,
            this, &MainWindow::onLoginClicked);
    connect(m_fetchReposBtn, &QPushButton::clicked,
            this, &MainWindow::onFetchReposClicked);
    connect(m_fetchIssuesBtn, &QPushButton::clicked,
            this, &MainWindow::onFetchIssuesClicked);
    connect(m_clearIssuesBtn, &QPushButton::clicked,
            this, &MainWindow::onClearIssuesClicked);
}

void MainWindow::setLoggedIn(bool loggedIn)
{
    m_tokenEdit->setEnabled(!loggedIn);
    m_loginBtn->setEnabled(!loggedIn);
    m_fetchReposBtn->setEnabled(loggedIn);
    m_fetchIssuesBtn->setEnabled(loggedIn && !m_repoCombo->currentText().isEmpty());

    if (!loggedIn) {
        m_userLabel->setText(tr("未登录"));
        m_currentUser.clear();
        m_repoList->clear();
        m_repoCombo->clear();
        m_issuesText->clear();
    }
}

void MainWindow::logStatus(const QString &text)
{
    m_statusLabel->setText(text);
    qDebug() << text;
}

void MainWindow::onLoginClicked()
{
    QString token = m_tokenEdit->text().trimmed();
    if (token.isEmpty()) {
        QMessageBox::warning(this, tr("提示"), tr("请先输入 Token"));
        return;
    }

    m_client->setToken(token);
    m_progress->setVisible(true);
    logStatus(tr("正在验证 Token..."));

    m_client->getCurrentUser([this](const QVariant &result, bool ok, const QString &msg){
        m_progress->setVisible(false);

        if (!ok) {
            QMessageBox::warning(this, tr("登录失败"), msg);
            setLoggedIn(false);
            return;
        }

        QVariantMap obj = result.toMap();
        m_currentUser = obj.value("login").toString();
        if (m_currentUser.isEmpty()) {
            QMessageBox::warning(this, tr("登录失败"), tr("未获取到 login 字段"));
            setLoggedIn(false);
            return;
        }

        m_userLabel->setText(tr("已登录:%1").arg(m_currentUser));
        setLoggedIn(true);
        logStatus(tr("登录成功,欢迎 %1").arg(m_currentUser));
    });
}

void MainWindow::onFetchReposClicked()
{
    if (m_currentUser.isEmpty()) {
        QMessageBox::warning(this, tr("提示"), tr("请先登录"));
        return;
    }

    m_progress->setVisible(true);
    m_repoList->clear();
    m_repoCombo->clear();
    m_issuesText->clear();
    logStatus(tr("正在获取仓库列表..."));

    m_client->getUserRepos([this](const QVariant &result, bool ok, const QString &msg){
        m_progress->setVisible(false);

        if (!ok) {
            QMessageBox::warning(this, tr("获取仓库失败"), msg);
            logStatus(tr("获取仓库失败:%1").arg(msg));
            return;
        }

        QVariantList list = result.toList();
        for (const QVariant &v : list) {
            QVariantMap repo = v.toMap();
            QString name = repo.value("name").toString();
            if (!name.isEmpty()) {
                m_repoList->addItem(name);
                m_repoCombo->addItem(name);
            }
        }

        logStatus(tr("仓库获取完成,共 %1 个").arg(list.size()));
    });
}

void MainWindow::onFetchIssuesClicked()
{
    if (m_currentUser.isEmpty()) {
        QMessageBox::warning(this, tr("提示"), tr("请先登录"));
        return;
    }
    if (m_repoCombo->currentText().isEmpty()) {
        QMessageBox::warning(this, tr("提示"), tr("请先选择一个仓库"));
        return;
    }

    QString repoName = m_repoCombo->currentText();
    m_progress->setVisible(true);
    m_issuesText->clear();
    logStatus(tr("正在获取 %1/%2 的 Issues...").arg(m_currentUser, repoName));

    m_client->getRepoIssues(m_currentUser, repoName,
        [this, repoName](const QVariant &result, bool ok, const QString &msg){
            m_progress->setVisible(false);

            if (!ok) {
                QMessageBox::warning(this, tr("获取 Issues 失败"), msg);
                logStatus(tr("获取 Issues 失败:%1").arg(msg));
                return;
            }

            QVariantList issues = result.toList();

            QString html;
            for (const QVariant &v : issues) {
                QVariantMap issue = v.toMap();
                int number = issue.value("number").toInt();
                QString title = issue.value("title").toString();
                QString state = issue.value("state").toString();

                html += QString("<h3>#%1 [%2] %3</h3>")
                        .arg(number)
                        .arg(state)
                        .arg(title);
                html += "<hr/>";
            }
            if (html.isEmpty()) {
                html = tr("没有获取到 Issue。");
            }

            m_issuesText->setHtml(html);
            logStatus(tr("仓库 %1 的 Issues 获取完成,共 %2 个")
                      .arg(repoName)
                      .arg(issues.size()));
        });
}

void MainWindow::onClearIssuesClicked()
{
    m_issuesText->clear();
    logStatus(tr("Issues 已清空"));
}

7. main.cpp:程序入口

cpp 复制代码
#include "mainwindow.h"
#include <QApplication>
#include <QSslSocket>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    QApplication::setApplicationName("RestApiClient");
    QApplication::setOrganizationName("QtAdvancedSeries");
    a.setStyle("Fusion");

    // 设置 OpenSSL 路径
    QCoreApplication::addLibraryPath(QCoreApplication::applicationDirPath());

    qDebug() << "SSL支持:" << QSslSocket::supportsSsl();
    qDebug() << "SSL库版本:" << QSslSocket::sslLibraryVersionString();
    qDebug() << "SSL构建版本:" << QSslSocket::sslLibraryBuildVersionString();

    MainWindow w;
    w.show();

    return a.exec();
}

四、实战中的坑与优化:结合这个 Demo 再看几条经验

1. 把「请求发起」和「结果处理」分开

在这个 Demo 里:

  • NetworkManager::send() 只负责:组装 QNetworkRequest、创建 QNetworkReply、处理超时、收集结果;
  • ApiClient 负责:构建 RequestInfo、解析 JSON、把结果转成 QVariantMap / QVariantList,再以 callback 的方式抛给 UI。

这种分层的好处是:如果将来你要换成别的 HTTP 库(例如 libcurl、Boost.Beast),只需要改 NetworkManager 和 RequestInfo,不动 UI 和业务层。

2. 不要在 UI 层直接处理 QNetworkReply

UI 层只关心「成功还是失败、结果是什么」,不关心:

  • 返回头里有什么;
  • TLS 是什么版本;
  • 是否重试、是否走代理等。

这些都应该在网络层或 ApiClient 层统一封装。

这样 UI 层的函数签名就很干净:

cpp 复制代码
void MainWindow::onFetchReposClicked()
{
    m_client->getUserRepos([this](const QVariant &result, bool ok, const QString &msg){
        // ...
    });
}

3. 超时不是「可选项」,而是「必需项」

所有实际项目中用到的 HTTP 请求,都应该有明确的超时逻辑:

  • 网络层统一设置默认超时;
  • 某些接口可以单独提高 / 降低;
  • 超时后给出明确提示,而不是让用户干等。

4. 日志与调试

建议在 NetworkManager 里增加:

  • 请求 URL、方法、头部与 body 的调试日志;
  • 响应状态码、错误码、错误字符串;
  • 必要时可以把 body 也打印到单独日志里(注意脱敏)。

这样在出现线上问题时,只需远程看一眼日志,就能大致判断是「网络问题、服务端问题还是客户端逻辑问题」。


五、小结:项目中可落地的 Qt 网络访问实践清单

结合本期内容,把关键点整理成一份「可以直接写进团队开发规范」的条目:

  1. 统一入口

    • 全局只维护一个或少量 QNetworkAccessManager,由 NetworkManager 封装;
    • 所有 HTTP 请求都通过统一入口发起,方便挂公共逻辑。
  2. 请求封装

    • 用 RequestInfo(或类似结构)记录 URL、方法、头部、body、超时等信息;
    • 网络层返回时只负责填充 error/statusCode/body 等基础字段。
  3. 响应解析

    • 专门的 ApiClient 层负责把 QByteArrayQJsonDocument → QVariantMap/VariantList;
    • ApiClient 只暴露「业务相关」的函数,例如 getUserRepos()getRepoIssues()
  4. 错误与超时处理

    • 超时一律由网络层配合 QTimer 实现;
    • 所有请求都必须在失败时给出明确的错误信息(包括错误码和简要提示)。
  5. 线程与 UI 安全

    • 不在子线程直接访问 QWidget;
    • 全部网络回调通过 Qt 的信号槽在主线程处理,然后再更新 UI。
  6. 安全性

    • HTTPS 和 SSL 错误要按类型区分处理,不要无脑 ignoreSslErrors()
    • 访问令牌、密码等敏感信息不要写死在代码中,而是从配置或 UI 注入。

如果你把这一套 Demo 跑通,再按上面的规范梳理一下自己项目中的网络访问代码,通常可以明显减少:

  • 难以复现的网络崩溃;
  • 无法解释的卡顿;
  • 到处散落的「一次性」网络逻辑。

到这里,Qt5 进阶专题从基础机制(前 1--5 期)走到文件(第 6 期)和网络(第 7 期),已经基本覆盖了绝大多数桌面应用的「底座能力」。

后续再配合数据库、架构与性能优化,你就可以比较自信地搭建起一个完整的 Qt 桌面应用。

相关推荐
走粥2 小时前
选项式API与组合式API的区别
开发语言·前端·javascript·vue.js·前端框架
试剂小课堂 Pro2 小时前
mPEG-Silane:mPEG链单端接三乙氧基硅的亲水性硅烷偶联剂
java·c语言·网络·c++·python·tomcat
mjhcsp2 小时前
P14992 取模(题解)
c++
郑州光合科技余经理2 小时前
源码部署同城O2O系统:中台架构开发指南
java·开发语言·后端·架构·系统架构·uni-app·php
阿波罗尼亚2 小时前
Java框架中的分层架构
java·开发语言·架构
踏歌~2 小时前
终极指南:在 Windows 上配置 KDB+, JupyterQ 与 Python (embedPy)
开发语言·windows·python
Henry Zhu1232 小时前
Qt Model/View架构详解(三):自定义模型
开发语言·qt
野生技术架构师2 小时前
【面试题】为什么 Java 8 移除了永久代(PermGen)并引入了元空间(Metaspace)?
java·开发语言
Leo July2 小时前
【Java】Java设计模式实战指南:从原理到框架应用
java·开发语言·设计模式