Skip to content

Latest commit

 

History

History
1099 lines (975 loc) · 39.2 KB

File metadata and controls

1099 lines (975 loc) · 39.2 KB

八、网络请求

这一章将带我们到世界各地,因为我们从应用到互联网的道路上走得更远。从为我们编写一些帮助类来管理 web 请求开始,我们将从一个实时 RSS 源中提取数据,并通过一些 XML 处理来解释它。有了手头的解析数据,我们就可以使用我们的 QML 技巧,并在新视图中显示这些项目。点击其中一个 RSS 项目将启动一个网络浏览器窗口,以便更详细地查看相关文章。我们将涵盖以下主题:

  • 网络存取
  • 网络请求
  • RSS 视图
  • 简易资讯聚合

网络存取

底层的组网协议协商全部由 Qt 内部处理,我们可以通过QNetworkAccessManager类轻松的与外界取得联系。为了能够访问该功能,我们需要将network模块添加到cm-lib.pro:

QT += sql network

Qt 的弱点之一是缺少接口,这使得单元测试在某些情况下很困难。如果我们只是直接使用QNetworkAccessManager,那么如果不对网络进行真正的调用,我们将无法测试我们的代码,这是不可取的。然而,这个问题的一个快速简单的解决方案是将 Qt 实现隐藏在我们自己的接口后面,我们将在这里这样做。

就本章而言,我们需要对网络做的就是检查我们是否有连接,并发送一个 HTTP GET 请求。考虑到这一点,在一个新的文件夹cm-lib/source/networking中创建一个头文件i-network-access-manager.h,并实现接口:

#ifndef INETWORKACCESSMANAGER_H
#define INETWORKACCESSMANAGER_H
#include <QNetworkReply>
#include <QNetworkRequest>

namespace cm {
namespace networking {
class INetworkAccessManager
{
public:
    INetworkAccessManager(){}
    virtual ~INetworkAccessManager(){}
    virtual QNetworkReply* get(const QNetworkRequest& request) = 0;
    virtual bool isNetworkAccessible() const = 0;
};
}}
#endif

QNetworkRequest是另一个 Qt 类,表示通过网络发送的请求,QNetworkReply表示通过网络接收的响应。理想情况下,我们也将把这些实现隐藏在接口后面,但是现在让我们来处理网络访问接口。准备好之后,在同一个文件夹中创建一个具体的实现类NetworkAccessManager:

network-access-manager.h:

#ifndef NETWORKACCESSMANAGER_H
#define NETWORKACCESSMANAGER_H
#include <QObject>
#include <QScopedPointer>
#include <networking/i-network-access-manager.h>
namespace cm {
namespace networking {
class NetworkAccessManager : public QObject, public INetworkAccessManager
{
    Q_OBJECT
public:
    explicit NetworkAccessManager(QObject* parent = nullptr);
    ~NetworkAccessManager();
    QNetworkReply* get(const QNetworkRequest& request) override;
    bool isNetworkAccessible() const override;
private:
    class Implementation;
    QScopedPointer<Implementation> implementation;
};
}}
#endif

network-access-manager.cpp:

#include "network-access-manager.h"
#include <QNetworkAccessManager>
namespace cm {
namespace networking {
class NetworkAccessManager::Implementation
{
public:
    Implementation()
    {}
    QNetworkAccessManager networkAccessManager;
};
NetworkAccessManager::NetworkAccessManager(QObject *parent)
    : QObject(parent)
    , INetworkAccessManager()
{
    implementation.reset(new Implementation());
}
NetworkAccessManager::~NetworkAccessManager()
{
}
QNetworkReply* NetworkAccessManager::get(const QNetworkRequest& request)
{
    return implementation->networkAccessManager.get(request);
}
bool NetworkAccessManager::isNetworkAccessible() const
{
    return implementation->networkAccessManager.networkAccessible() == QNetworkAccessManager::Accessible;
}
}}

我们所做的就是持有一个私有的QNetworkAccessManager实例,并通过它传递对我们接口的调用。该接口可以很容易地扩展到包括额外的功能,如相同方法的 HTTP POST 请求。

网络请求

如果您以前没有使用过 HTTP 协议,那么它可以归结为客户端和服务器之间由请求和响应组成的对话。比如我们可以在自己喜欢的网页浏览器中向www.bbc.co.uk提出请求,就会收到包含各种新闻条目和文章的回复。在我们的NetworkAccessManager包装器的get()方法中,我们引用了一个QNetworkRequest(我们对服务器的请求)和一个QNetworkReply(服务器对我们的响应)。虽然我们不会直接将QNetworkRequestQNetworkReply隐藏在它们自己独立的接口后面,但是我们将采用 web 请求和相应响应的概念,并为该交互创建一个接口和实现。仍然在cm-lib/source/networking中,创建一个接口头文件i-web-request.h:

#ifndef IWEBREQUEST_H
#define IWEBREQUEST_H
#include <QUrl>
namespace cm {
namespace networking {
class IWebRequest
{
public:
    IWebRequest(){}
    virtual ~IWebRequest(){}
    virtual void execute() = 0;
    virtual bool isBusy() const = 0;
    virtual void setUrl(const QUrl& url) = 0;
    virtual QUrl url() const = 0;
};
}}
#endif

HTTP 请求的关键信息是请求要发送到的网址,由QUrl Qt 类表示。我们为属性提供了一个url()取值器和setUrl()变异器。另外两种方法是检查isBusy() web 请求对象是发出请求还是接收响应,以及是发送到execute()还是发送请求到网络。同样,有了接口,让我们直接进入实现,在同一个文件夹中有一个新的WebRequest类。

web-request.h:

#ifndef WEBREQUEST_H
#define WEBREQUEST_H
#include <QList>
#include <QObject>
#include <QSslError>
#include <networking/i-network-access-manager.h>
#include <networking/i-web-request.h>
namespace cm {
namespace networking {
class WebRequest : public QObject, public IWebRequest
{
    Q_OBJECT
public:
    WebRequest(QObject* parent, INetworkAccessManager* networkAccessManager, const QUrl& url);
    WebRequest(QObject* parent = nullptr) = delete;
    ~WebRequest();
public:
    void execute() override;
    bool isBusy() const override;
    void setUrl(const QUrl& url) override;
    QUrl url() const override;
signals:
    void error(QString message);
    void isBusyChanged();
    void requestComplete(int statusCode, QByteArray body);
    void urlChanged();
private slots:
    void replyDelegate();
    void sslErrorsDelegate( const QList<QSslError>& _errors );
private:
    class Implementation;
    QScopedPointer<Implementation> implementation;
};
}}
#endif

web-request.cpp:

#include "web-request.h"

#include <QMap>
#include <QNetworkReply>
#include <QNetworkRequest>
namespace cm {
namespace networking { // Private Implementation
static const QMap<QNetworkReply::NetworkError, QString> networkErrorMapper = {
    {QNetworkReply::ConnectionRefusedError, "The remote server refused the connection (the server is not accepting requests)."},
    /* ...section shortened in print for brevity...*/
    {QNetworkReply::UnknownServerError, "An unknown error related to the server response was detected."}
};
class WebRequest::Implementation
{
public:
    Implementation(WebRequest* _webRequest, INetworkAccessManager* _networkAccessManager, const QUrl& _url)
        : webRequest(_webRequest)
        , networkAccessManager(_networkAccessManager)
        , url(_url)
    {
    }
    WebRequest* webRequest{nullptr};
    INetworkAccessManager* networkAccessManager{nullptr};
    QUrl url {};
    QNetworkReply* reply {nullptr};
public: 
    bool isBusy() const
    {
        return isBusy_;
    }
    void setIsBusy(bool value)
    {
        if (value != isBusy_) {
            isBusy_ = value;
            emit webRequest->isBusyChanged();
        }
    }
private:
    bool isBusy_{false};
};
}
namespace networking {  // Structors
WebRequest::WebRequest(QObject* parent, INetworkAccessManager* networkAccessManager, const QUrl& url)
    : QObject(parent)
    , IWebRequest()
{
    implementation.reset(new WebRequest::Implementation(this, networkAccessManager, url));
}
WebRequest::~WebRequest()
{
}
}
namespace networking { // Methods
void WebRequest::execute()
{
    if(implementation->isBusy()) {
        return;
    }

    if(!implementation->networkAccessManager->isNetworkAccessible()) {
        emit error("Network not accessible");
        return;
    }
    implementation->setIsBusy(true);
    QNetworkRequest request;
    request.setUrl(implementation->url);
    implementation->reply = implementation->networkAccessManager->get(request);
    if(implementation->reply != nullptr) {
        connect(implementation->reply, &QNetworkReply::finished, this, &WebRequest::replyDelegate);
        connect(implementation->reply, &QNetworkReply::sslErrors, this, &WebRequest::sslErrorsDelegate);
    }
}
bool WebRequest::isBusy() const
{
    return implementation->isBusy();
}
void WebRequest::setUrl(const QUrl& url)
{
    if(url != implementation->url) {
        implementation->url = url;
        emit urlChanged();
    }
}
QUrl WebRequest::url() const
{
    return implementation->url;
}
}
namespace networking { // Private Slots
void WebRequest::replyDelegate()
{
    implementation->setIsBusy(false);
    if (implementation->reply == nullptr) {
        emit error("Unexpected error - reply object is null");
        return;
    }
    disconnect(implementation->reply, &QNetworkReply::finished, this, &WebRequest::replyDelegate);
    disconnect(implementation->reply, &QNetworkReply::sslErrors, this, &WebRequest::sslErrorsDelegate);
    auto statusCode = implementation->reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
    auto responseBody = implementation->reply->readAll();
    auto replyStatus = implementation->reply->error();
    implementation->reply->deleteLater();
    if (replyStatus != QNetworkReply::NoError) {
        emit error(networkErrorMapper[implementation->reply->error()]);
    }
    emit requestComplete(statusCode, responseBody);
}
void WebRequest::sslErrorsDelegate(const QList<QSslError>& errors)
{
    QString sslError;
    for (const auto& error : errors) {
        sslError += error.errorString() + "\n";
    }
    emit error(sslError);
}
}}

实现看起来比实际更复杂,这完全是因为冗长的错误代码映射。如果出现某种问题,Qt 将使用枚举器报告错误。映射的目的只是将枚举器与人类可读的错误描述相匹配,我们可以将该描述呈现给用户,或者写入控制台或日志文件。

除了接口方法,我们还有一些信号可以用来告诉任何感兴趣的观察者已经发生的事件:

  • error()将在出现问题时发出,并将错误描述作为参数传递
  • 当请求开始或结束,并且请求变得繁忙或空闲时,触发isBusyChanged()
  • requestComplete()在响应被接收和处理后发出,将包含 HTTP 状态代码和代表响应主体的字节数组
  • 当网址更新时,将触发urlChanged()

我们还有几个私有槽,它们将是处理回复和处理任何 SSL 错误的代理。当我们执行新的请求时,它们连接到QNetworkReply对象上的信号,当我们收到回复时,它们再次断开连接。

实现的核心其实是两种方法——发送请求的execute()和处理响应的replyDelegate()

在执行时,我们首先确保我们没有忙于执行另一个请求,然后与网络访问管理器一起检查我们是否有可用的连接。假设我们这样做了,然后我们设置忙碌标志,并使用当前设置的网址构建一个QNetworkRequest。然后,我们将请求传递给我们的网络访问管理器(作为接口注入,这样我们就可以改变它的行为),最后,我们连接我们的代理插槽并等待响应。

当我们收到回复时,我们会在读取我们感兴趣的响应详细信息(主要是 HTTP 状态代码和响应正文)之前,取消繁忙标志并断开插槽。我们检查回复是否成功完成(请注意,4xx 或 5xx 范围内的“否定”HTTP 响应代码在此上下文中仍算作成功完成的请求),并发出详细信息供任何感兴趣的方捕获和处理。

RSS 视图

让我们给我们的应用添加一个新的视图,在那里我们可以使用我们的新类显示来自 web 服务的一些信息。

这里没有什么新的或复杂的东西,所以我不会展示所有的代码,但有几个步骤需要记住:

  1. cm-ui/views中创建新的RssView.qml视图,并从SplashView复制 QML,用“Rss 视图”替换“闪屏视图”文本

  2. 将视图添加到/views前缀块的views.qrc中,并使用别名RssView.qml

  3. goRssView()信号加到NavigationController

  4. MasterView中,将onGoRssView槽添加到连接元素,并使用它导航到RssView

  5. NavigationBar中,添加一个新的NavigationButton,其中iconCharacter\uf09e,描述为RSS Feed,而hoverColour#8acece,使用onNavigationButtonClicked槽在NavigationController上调用goRssView()

只需几个简单的步骤,我们现在就有了一个全新的视图,我们可以使用导航栏访问它:

接下来,我们将通过以下步骤向视图添加上下文命令栏:

  1. CommandController中,添加新的私人成员列表rssViewContextCommands

  2. 添加访问器方法ui_rssViewContextCommands()

  3. 添加一个名为ui_rssViewContextCommandsQ_PROPERTY

  4. 增加一个新的插槽onRssRefreshExecuted(),只需向控制台写入一条调试消息;表示它已经被调用

  5. 将名为rssRefreshCommand的新命令附加到带有0xf021图标字符和“刷新”标签的rssViewContextCommands上,并将其连接到onRssRefreshExecuted()插槽

  6. RssView中,添加一个CommandBar组件,将commandList连接到命令控制器上的ui_rssViewContextCommands

前几章的辛苦付出,现在真的有回报了;我们的新视图有自己的命令栏和功能齐全的刷新按钮。当您单击它时,它应该会写出您添加到控制台的调试消息:

接下来,我们需要创建我们的NetworkAccessManagerWebRequest类的实例。像往常一样,我们将这些添加到MasterController中,并为CommandController注入依赖。

MasterController中,添加两个新的私人成员:

NetworkAccessManager* networkAccessManager{nullptr};
WebRequest* rssWebRequest{nullptr};

请记住包含相关的标题。在Implementation构造函数中实例化这些新成员,确保它们是在commandController之前创建的:

networkAccessManager = new NetworkAccessManager(masterController);
rssWebRequest = new WebRequest(masterController, networkAccessManager, QUrl("http://feeds.bbci.co.uk/news/rss.xml?edition=uk"));

这里我们使用的是英国广播公司与英国相关的 RSS 源的网址;只需替换超链接文本,就可以随意将此内容替换为您选择的另一个提要。

接下来,将rssWebRequest作为新参数传递给commandController构造器:

commandController = new CommandController(masterController, databaseController, navigationController, newClient, clientSearch, rssWebRequest);

接下来,编辑CommandController将这个新参数作为界面的指针:

explicit CommandController(QObject* _parent = nullptr, IDatabaseController* databaseController = nullptr, NavigationController* navigationController = nullptr, models::Client* newClient = nullptr, models::ClientSearch* clientSearch = nullptr, networking::IWebRequest* rssWebRequest = nullptr);

通过Implementation构造函数传递该指针,并将其存储在私有成员变量中,就像我们对所有其他依赖项所做的那样:

IWebRequest* rssWebRequest{nullptr};

我们现在可以更新onRssRefreshExecuted()槽来执行 web 请求:

void CommandController::onRssRefreshExecuted()
{
    qDebug() << "You executed the Rss Refresh command!";

    implementation->rssWebRequest->execute();
}

命令控制器现在对用户按下刷新按钮做出反应,并执行 web 请求。然而,当我们收到回复时,我们目前没有做任何事情。让我们在公共插槽部分向MasterController添加一个委托:

void MasterController::onRssReplyReceived(int statusCode, QByteArray body)
{
    qDebug() << "Received RSS request response code " << statusCode << ":";
    qDebug() << body;
}

现在,在Implementation中实例化rssWebRequest之后,我们可以将requestComplete信号连接到我们的新代表:

QObject::connect(rssWebRequest, &WebRequest::requestComplete, masterController, &MasterController::onRssReplyReceived);

现在构建并运行应用,导航到 RSS 视图,然后单击刷新。短暂延迟后,在执行请求时,您会看到各种各样的废话被打印到应用输出控制台:

Received RSS request response code 200 :
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<?xml-stylesheet title=...”

恭喜你!你有一个 RSS 源!现在,它是什么?

简易资讯聚合

丰富的网站摘要 ( RSS )是一种用于传递定期变化的网络内容的格式,本质上是一个完整的网站、新闻广播、博客或类似的浓缩到要点的内容。每一个条目都包含一些基本的信息,比如日期和描述性的标题,并且提供了一个指向包含完整文章的网页的超链接。

数据是从 XML 扩展而来的,必须遵守在http://www.rssboard.org/rss-specification描述的定义标准。

就本例而言,将它归结为基础,XML 如下所示:

<rss>
    <channel>
        <title></title>
        <description></description>
        <link></link>
        <image>
            <url></url>
            <title></title>
            <link></link>
            <width></width>
            <height></height>
        </image>
        <item>
            <title></title>
            <description></description>
            <link></link>
            <pubDate></pubDate>
        </item>
        <item>
                …
          </item>
    </channel>
</rss>

在根<rss>节点内部,我们有一个<channel>节点,它又包含一个<image>节点和一个或多个<item>节点的集合。

我们将把这些节点建模为类,但是首先我们需要引入 XML 模块并编写一个小的助手类来为我们做一些解析。在cm-lib.procm-ui.pro中,将xml模块添加到QT变量中的模块;考虑这个例子:

QT += sql network xml

接下来,在新文件夹cm-lib/source/utilities中创建新的XmlHelper类。

xml-helper.h:

#ifndef XMLHELPER_H
#define XMLHELPER_H
#include <QDomNode>
#include <QString>
namespace cm {
namespace utilities {
class XmlHelper
{
public:
    static QString toString(const QDomNode& domNode);
private:
    XmlHelper(){}
    static void appendNode(const QDomNode& domNode, QString& output);
};
}}
#endif

xml-helper.cpp:

#include "xml-helper.h"

namespace cm {
namespace utilities {
QString XmlHelper::toString(const QDomNode& domNode)
{
    QString returnValue;
    for(auto i = 0; i < domNode.childNodes().size(); ++ i) {
        QDomNode subNode = domNode.childNodes().at(i);
        appendNode(subNode, returnValue);
    }
    return returnValue;
}
void XmlHelper::appendNode(const QDomNode& domNode, QString& output)
{
    if(domNode.nodeType() == QDomNode::TextNode) {
        output.append(domNode.nodeValue());
        return;
    }
    if(domNode.nodeType() == QDomNode::AttributeNode) {
        output.append(" ");
        output.append(domNode.nodeName());
        output.append("=\"");
        output.append(domNode.nodeValue());
        output.append("\"");
        return;
    }
    if(domNode.nodeType() == QDomNode::ElementNode) {
        output.append("<");
        output.append(domNode.nodeName());
        // Add attributes
        for(auto i = 0; i < domNode.attributes().size(); ++ i) {
            QDomNode subNode = domNode.attributes().item(i);
            appendNode(subNode, output);
        }
        output.append(">");
        for(auto i = 0; i < domNode.childNodes().size(); ++ i) {
            QDomNode subNode = domNode.childNodes().at(i);
            appendNode(subNode, output);
        }
        output.append("</" + domNode.nodeName() + ">");
    }
}
}}

关于这个类做什么,我就不赘述了,因为这不是本章的重点,但本质上,如果我们收到一个包含 HTML 标记的 XML 节点(这在 RSS 中很常见),XML 解析器会有点混乱,也会把 HTML 分解成 XML 节点,这不是我们想要的。考虑这个例子:

<xmlNode>
    Here is something from a website that has a <a href=”http://www.bbc.co.uk”>hyperlink</a> in it.
</xmlNode>

在这种情况下,XML 解析器会将<a>视为 XML,并将内容分解为三个子节点,如下所示:

<xmlNode>
    <textNode1>Here is something from a website that has a </textNode1>
    <a href=”http://www.bbc.co.uk”>hyperlink</a>
    <textNode2>in it.</textNode2>
</xmlNode>

这使得很难在用户界面上向用户显示 xmlNode 的内容。相反,我们使用 XmlHelper 来手动解析内容,并构造一个字符串,这要容易得多。

现在,让我们继续讨论 RSS 类。在新的cm-lib/source/rss folder中,创建新的RssChannelRssImageRssItem类。

rss-image.h:

#ifndef RSSIMAGE_H
#define RSSIMAGE_H
#include <QObject>
#include <QScopedPointer>
#include <QtXml/QDomNode>
#include <cm-lib_global.h>
namespace cm {
namespace rss {
class CMLIBSHARED_EXPORT RssImage : public QObject
{
    Q_OBJECT
    Q_PROPERTY(quint16 ui_height READ height CONSTANT)
    Q_PROPERTY(QString ui_link READ link CONSTANT)
    Q_PROPERTY(QString ui_title READ title CONSTANT)
    Q_PROPERTY(QString ui_url READ url CONSTANT)
    Q_PROPERTY(quint16 ui_width READ width CONSTANT)
public:
    explicit RssImage(QObject* parent = nullptr, const QDomNode& domNode = QDomNode());
    ~RssImage();
    quint16 height() const;
    const QString& link() const;
    const QString& title() const;
    const QString& url() const;
    quint16 width() const;
private:
    class Implementation;
    QScopedPointer<Implementation> implementation;
};
}}

#endif

rss-image.cpp:

#include "rss-image.h"

namespace cm {
namespace rss {
class RssImage::Implementation
{
public:
    QString url;    // Mandatory. URL of GIF, JPEG or PNG that represents the channel.
    QString title;  // Mandatory.  Describes the image.
    QString link;   // Mandatory.  URL of the site.
    quint16 width;  // Optional.  Width in pixels.  Max 144, default 
                                                                    88.
    quint16 height; // Optional.  Height in pixels.  Max 400, default 
                                                                    31
    void update(const QDomNode& domNode)
    {
        QDomElement imageUrl = domNode.firstChildElement("url");
        if(!imageUrl.isNull()) {
            url = imageUrl.text();
        }
        QDomElement imageTitle = domNode.firstChildElement("title");
        if(!imageTitle.isNull()) {
            title = imageTitle.text();
        }
        QDomElement imageLink = domNode.firstChildElement("link");
        if(!imageLink.isNull()) {
            link = imageLink.text();
        }
        QDomElement imageWidth = domNode.firstChildElement("width");
        if(!imageWidth.isNull()) {
            width = static_cast<quint16>(imageWidth.text().toShort());
        } else {
            width = 88;
        }
        QDomElement imageHeight = domNode.firstChildElement("height");
        if(!imageHeight.isNull()) {
            height = static_cast<quint16>
                                  (imageHeight.text().toShort());
        } else {
            height = 31;
        }
    }
};
RssImage::RssImage(QObject* parent, const QDomNode& domNode)
    : QObject(parent)
{
    implementation.reset(new Implementation());
    implementation->update(domNode);
}
RssImage::~RssImage()
{
}
quint16 RssImage::height() const
{
    return implementation->height;
}
const QString& RssImage::link() const
{
    return implementation->link;
}
const QString& RssImage::title() const
{
    return implementation->title;
}
const QString& RssImage::url() const
{
    return implementation->url;
}
quint16 RssImage::width() const
{
    return implementation->width;
}
}}

这个类只是一个普通的数据模型,除了它将由 Qt 的QDomNode类表示的一个 XML <image>节点构建。我们使用firstChildElement()方法定位<url><title><link>强制子节点,然后通过text()方法访问每个节点的值。<width><height>节点是可选的,如果它们不存在,我们使用 88 x 31 像素的默认图像大小。

rss-item.h:

#ifndef RSSITEM_H
#define RSSITEM_H
#include <QDateTime>
#include <QObject>
#include <QscopedPointer>
#include <QtXml/QDomNode>
#include <cm-lib_global.h>
namespace cm {
namespace rss {
class CMLIBSHARED_EXPORT RssItem : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString ui_description READ description CONSTANT)
    Q_PROPERTY(QString ui_link READ link CONSTANT)
    Q_PROPERTY(QDateTime ui_pubDate READ pubDate CONSTANT)
    Q_PROPERTY(QString ui_title READ title CONSTANT)
public:
    RssItem(QObject* parent = nullptr, const QDomNode& domNode = QDomNode());
    ~RssItem();
    const QString& description() const;
    const QString& link() const;
    const QDateTime& pubDate() const;
    const QString& title() const;
private:
    class Implementation;
    QScopedPointer<Implementation> implementation;
};
}}
#endif

rss-item.cpp:

#include "rss-item.h"
#include <QTextStream>
#include <utilities/xml-helper.h>
using namespace cm::utilities;
namespace cm {
namespace rss {
class RssItem::Implementation
{
public:
    Implementation(RssItem* _rssItem)
        : rssItem(_rssItem)
    {
    }
    RssItem* rssItem{nullptr};
    QString description;    // This or Title mandatory.  Either the 
                            synopsis or full story.  HTML is allowed.
    QString link;           // Optional. Link to full story.  Populated 
                                  if Description is only the synopsis.
    QDateTime pubDate;      // Optional. When the item was published. 
                     RFC 822 format e.g. Sun, 19 May 2002 15:21:36 GMT.
    QString title;          // This or Description mandatory.
    void update(const QDomNode& domNode)
    {
        for(auto i = 0; i < domNode.childNodes().size(); ++ i) {
            QDomNode childNode = domNode.childNodes().at(i);
            if(childNode.nodeName() == "description") {
                description = XmlHelper::toString(childNode);
            }
        }
        QDomElement itemLink = domNode.firstChildElement("link");
        if(!itemLink.isNull()) {
            link = itemLink.text();
        }
        QDomElement itemPubDate = domNode.firstChildElement("pubDate");
        if(!itemPubDate.isNull()) {
            pubDate = QDateTime::fromString(itemPubDate.text(), 
                                                     Qt::RFC2822Date);
        }
        QDomElement itemTitle = domNode.firstChildElement("title");
        if(!itemTitle.isNull()) {
            title = itemTitle.text();
        }
    }
};
RssItem::RssItem(QObject* parent, const QDomNode& domNode)
{
    implementation.reset(new Implementation(this));
    implementation->update(domNode);
}
RssItem::~RssItem()
{
}
const QString& RssItem::description() const
{
    return implementation->description;
}
const QString& RssItem::link() const
{
    return implementation->link;
}
const QDateTime& RssItem::pubDate() const
{
    return implementation->pubDate;
}
const QString& RssItem::title() const
{
    return implementation->title;
}
}}

这个班和上一个班差不多。这次我们在解析<description>节点时使用我们的 XMLHelper 类,因为它很有可能包含 HTML 标签。还要注意,当使用静态QDateTime::fromString()方法将字符串转换为QDateTime对象时,Qt 还包含Qt::RFC2822Date格式说明符。这是 RSS 规范中使用的格式,使我们不必自己手动解析日期。

rss-channel.h:

#ifndef RSSCHANNEL_H
#define RSSCHANNEL_H
#include <QDateTime>
#include <QtXml/QDomElement>
#include <QtXml/QDomNode>
#include <QList>
#include <QObject>
#include <QtQml/QQmlListProperty>
#include <QString>
#include <cm-lib_global.h>
#include <rss/rss-image.h>
#include <rss/rss-item.h>
namespace cm {
namespace rss {
class CMLIBSHARED_EXPORT RssChannel : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString ui_description READ description CONSTANT)
    Q_PROPERTY(cm::rss::RssImage* ui_image READ image CONSTANT)
    Q_PROPERTY(QQmlListProperty<cm::rss::RssItem> ui_items READ 
                                                ui_items CONSTANT)
    Q_PROPERTY(QString ui_link READ link CONSTANT)
    Q_PROPERTY(QString ui_title READ title CONSTANT)
public:
    RssChannel(QObject* parent = nullptr, const QDomNode& domNode = QDomNode());
    ~RssChannel();
    void addItem(RssItem* item);
    const QString& description() const;
    RssImage* image() const;
    const QList<RssItem*>& items() const;
    const QString& link() const;
    void setImage(RssImage* image);
    const QString& title() const;
    QQmlListProperty<RssItem> ui_items();
    static RssChannel* fromXml(const QByteArray& xmlData, QObject* 
                                            parent = nullptr);
private:
    class Implementation;
    QScopedPointer<Implementation> implementation;
};
}}
#endif

rss-channel.cpp:

#include "rss-channel.h"
#include <QtXml/QDomDocument>
namespace cm {
namespace rss {
class RssChannel::Implementation
{
public:
    QString description;            // Mandatory.  Phrase or sentence describing the channel.
    RssImage* image{nullptr};       // Optional.  Image representing the channel.
    QList<RssItem*> items;          // Optional.  Collection representing stories.
    QString link;                   // Mandatory.  URL to the corresponding HTML website.
    QString title;                  // Mandatory.  THe name of the Channel.
    void update(const QDomNode& domNode)
    {
        QDomElement channelDescription = domNode.firstChildElement("description");
        if(!channelDescription.isNull()) {
            description = channelDescription.text();
        }
        QDomElement channelLink = domNode.firstChildElement("link");
        if(!channelLink.isNull()) {
            link = channelLink.text();
        }
        QDomElement channelTitle = domNode.firstChildElement("title");
        if(!channelTitle.isNull()) {
            title = channelTitle.text();
        }
    }
};
RssChannel::RssChannel(QObject* parent, const QDomNode& domNode)
    : QObject(parent)
{
    implementation.reset(new Implementation());
    implementation->update(domNode);
}
RssChannel::~RssChannel()
{
}
void RssChannel::addItem(RssItem* item)
{
    if(!implementation->items.contains(item)) {
        item->setParent(this);
        implementation->items.push_back(item);
    }
}
const QString&  RssChannel::description() const
{
    return implementation->description;
}
RssImage* RssChannel::image() const
{
    return implementation->image;
}
const QList<RssItem*>&  RssChannel::items() const
{
    return implementation->items;
}
const QString&  RssChannel::link() const
{
    return implementation->link;
}
void RssChannel::setImage(RssImage* image)
{
    if(implementation->image) {
        implementation->image->deleteLater();
        implementation->image = nullptr;
    }
    image->setParent(this);
    implementation->image = image;
}
const QString& RssChannel::title() const
{
    return implementation->title;
}
QQmlListProperty<RssItem> RssChannel::ui_items()
{
    return QQmlListProperty<RssItem>(this, implementation->items);
}
RssChannel* RssChannel::fromXml(const QByteArray& xmlData, QObject* parent)
{
    QDomDocument doc;
    doc.setContent(xmlData);
    auto channelNodes = doc.elementsByTagName("channel");
    // Rss must have 1 channel
    if(channelNodes.size() != 1) return nullptr;
    RssChannel* channel = new RssChannel(parent, channelNodes.at(0));
    auto imageNodes = doc.elementsByTagName("image");
    if(imageNodes.size() > 0) {
        channel->setImage(new RssImage(channel, imageNodes.at(0)));
    }
    auto itemNodes = doc.elementsByTagName("item");
    for (auto i = 0; i < itemNodes.size(); ++ i) {
        channel->addItem(new RssItem(channel, itemNodes.item(i)));
    }
    return channel;
}
}}

这个类与前面的类大致相同,但是因为这是我们的 XML 树的根对象,所以我们还有一个静态的fromXml()方法。这里的目标是从包含 RSS 提要 XML 的 RSS web 请求响应中获取字节数组,并让该方法为我们创建一个 RSS 频道、图像和项目层次结构。

我们将 XML 字节数组传递给 Qt QDomDocument类,就像我们之前对 JSON 和QJsonDocument类所做的那样。我们使用elementsByTagName()方法找到<channel>标签,然后使用该标签作为构造器的QDomNode参数构造一个新的RssChannel对象。感谢update()方法,RssChannel填充自己的属性。然后,我们定位<image><item>子节点,并创建新的RssImageRssItem实例,将其添加到根RssChannel对象中。同样,这些类能够从提供的QDomNode中填充它们自己的属性。

在我们忘记之前,让我们也在main()中注册课程:

qmlRegisterType<cm::rss::RssChannel>("CM", 1, 0, "RssChannel");
qmlRegisterType<cm::rss::RssImage>("CM", 1, 0, "RssImage");
qmlRegisterType<cm::rss::RssItem>("CM", 1, 0, "RssItem");

我们现在可以在MasterController中添加一个RssChannel来绑定用户界面:

  1. MasterController中,添加一个新的RssChannel*类型的rssChannel私有成员变量
  2. 添加一个rssChannel()访问器方法
  3. 增加一个rssChannelChanged()信号
  4. 使用READ的访问器和NOTIFY的信号添加名为ui_rssChannelQ_PROPERTY

我们不会在没有任何 RSS 数据的情况下创建一个构造,而是在 RSS 回复委托中进行:

void MasterController::onRssReplyReceived(int statusCode, QByteArray body)
{
    qDebug() << "Received RSS request response code " << statusCode << ":";
    qDebug() << body;
    if(implementation->rssChannel) {
        implementation->rssChannel->deleteLater();
        implementation->rssChannel = nullptr;
        emit rssChannelChanged();
    }
    implementation->rssChannel = RssChannel::fromXml(body, this);
    emit rssChannelChanged();
}

我们执行一些内务处理,检查内存中是否已经有一个旧的通道对象,如果有,它会使用QObjectdeleteLater()方法安全地删除它。然后,我们继续使用来自 web 请求的 XML 数据构建一个新的通道。

Always use deleteLater() on QObject derived classes rather than the standard C++ delete keyword as the destruction will be synchronized with the event loop and you will minimize the risk of unexpected exceptions.

我们将以类似于管理搜索结果的方式在响应中显示 RSS 项目,带有ListView和相关委托。将RssItemDelegate.qml添加到cm-ui/components并执行编辑components.qrcqmldir文件的常规步骤:

import QtQuick 2.9
import assets 1.0
import CM 1.0
Item {
    property RssItem rssItem
    implicitWidth: parent.width
    implicitHeight: background.height
    Rectangle {
        id: background
        width: parent.width
        height: textPubDate.implicitHeight + textTitle.implicitHeight + 
                       borderBottom.height + (Style.sizeItemMargin * 3)
        color: Style.colourPanelBackground
        Text {
            id: textPubDate
            anchors {
                top: parent.top
                left: parent.left
                right: parent.right
                margins: Style.sizeItemMargin
            }
            text: Qt.formatDateTime(rssItem.ui_pubDate, "ddd, d MMM 
                                                    yyyy @ h:mm ap")
            font {
                pixelSize: Style.pixelSizeDataControls
                italic: true
                weight: Font.Light
            }
            color: Style.colorItemDateFont
        }
        Text {
            id: textTitle
            anchors {
                top: textPubDate.bottom
                left: parent.left
                right: parent.right
                margins: Style.sizeItemMargin
            }
            text: rssItem.ui_title
            font {
                pixelSize: Style.pixelSizeDataControls
            }
            color: Style.colorItemTitleFont
            wrapMode: Text.Wrap
        }
        Rectangle {
            id: borderBottom
            anchors {
                top: textTitle.bottom
                left: parent.left
                right: parent.right
                topMargin: Style.sizeItemMargin
            }
            height: 1
            color: Style.colorItemBorder
        }
        MouseArea {
            anchors.fill: parent
            cursorShape: Qt.PointingHandCursor
            hoverEnabled: true
            onEntered: background.state = "hover"
            onExited: background.state = ""
            onClicked: if(rssItem.ui_link !== "") {
                           Qt.openUrlExternally(rssItem.ui_link);
                       }
        }
        states: [
            State {
                name: "hover"
                PropertyChanges {
                    target: background
                    color: Style.colourPanelBackgroundHover
                }
            }
        ]
    }
}

为了支持这个组件,我们需要添加一些样式属性:

readonly property color colourItemBackground: "#fefefe"
readonly property color colourItemBackgroundHover: "#efefef"
readonly property color colorItemBorder: "#efefef"
readonly property color colorItemDateFont: "#636363"
readonly property color colorItemTitleFont: "#131313"
readonly property real sizeItemMargin: 5

我们现在可以在RssView中利用这个委托:

import QtQuick 2.9
import assets 1.0
import components 1.0
Item {
    Rectangle {
        anchors.fill: parent
        color: Style.colourBackground
    }
    ListView {
        id: itemsView
        anchors {
            top: parent.top
            left: parent.left
            right: parent.right
            bottom: commandBar.top
            margins: Style.sizeHeaderMargin
        }
        clip: true
        model: masterController.ui_rssChannel ? masterController.ui_rssChannel.ui_items : 0
        delegate: RssItemDelegate {
            rssItem: modelData
        }
    }
    CommandBar {
        id: commandBar
        commandList: masterController.ui_commandController.ui_rssViewContextCommands
    }
}

构建并运行,导航到 RSS 视图,然后单击刷新按钮发出 web 请求并显示响应:

将鼠标悬停在项目上以查看光标效果,然后单击项目以在默认网络浏览器中打开它。Qt 在Qt.openUrlExternally()方法中为我们处理这个动作,我们将 RSS Item link属性传递给它。

摘要

在这一章中,我们扩展了应用之外的范围,并开始使用互联网上的 HTTP 请求与外部 API 进行交互。我们使用自己的接口抽象了 Qt 功能,以改善解耦,并使我们的组件更加测试友好。我们快速了解了 RSS 及其结构,以及如何使用 Qt 的 XML 模块处理 XML 节点树。最后,我们加强了我们一直在做的出色的用户界面工作,并添加了一个交互式视图来显示一个 RSS 提要,并为给定的网址启动默认的网络浏览器。

第 9 章总结中,我们将了解打包我们的应用以部署到其他计算机所需的步骤。