最小限のRSSリスト・アプリケーション

ネットワークリソースを取得して表示する方法のデモ。

この例では、ユーザーが要求したリソースをフェッチし、レスポンスに含まれるデータを表示する方法を、RSSリスト・アプリケーションで説明します。(RDFサイトサマリー、またはReally Simple Syndicationは、Webサイトの更新を伝達するための標準フォーマットです。詳しくはhttps://www.rssboard.org/rss-specification を参照)。この例ではネットワークの使い方に重点を置いているため、図のユーザーインターフェイスはシンプルですが、本格的なRSSリーダーには当然もっと洗練されたインターフェイスが必要です。

また、この例では、受信したデータを非同期で解析し、メンバ変数に状態を保存して、インクリメンタル・パーサがネットワーク経由で到着したデータのチャンクを消費できるようにする方法を示している。構文解析されるコンテンツの構成要素は、あるデータのチャンクで始まっても、後のチャンクまで完了しないことがあるため、パーサーが呼び出しの間に状態を保持する必要がある。

メイン・プログラムはかなり最小限のものである。QApplicationRSSListing ウィジェットをインスタンス化し、後者を表示し、前者に制御を渡すだけである。説明のために、チェックするリソースのデフォルト値としてQtブログのURLをウィジェットに与えています。

int main(int argc, char **argv)
{
    QApplication app(argc, argv);
    RSSListing rsslisting(u"https://www.qt.io/blog/rss.xml"_s);
    rsslisting.show();
    return app.exec();
}

RSSListing クラス

class RSSListing : public QWidget
{
    Q_OBJECT
public:
    explicit RSSListing(const QString &url = QString(), QWidget *widget = nullptr);

public slots:
    void fetch();
    void finished(QNetworkReply *reply);
    void consumeData();
    void error(QNetworkReply::NetworkError);

private:
    void parseXml();
    void get(const QUrl &url);

    // Parser state:
    QXmlStreamReader xml;
    QString currentTag;
    QString linkString;
    QString titleString;

    // Network state:
    QNetworkAccessManager manager;
    QNetworkReply *currentReply;

    // UI elements:
    QLineEdit *lineEdit;
    QTreeWidget *treeWidget;
    QPushButton *fetchButton;
};

ウィジェット自体は、取得する URL を指定するためのシンプルなユーザーインターフェイスを提供し、利用可能な更新が表示されたら、更新されたアイテムのダウンロードを制御します。QLineEdit はURLの入力を提供し、QTreeWidget はフェッチされた結果の表示を提供します。

ウィジェットはRSS(XMLの一種)を非同期でダウンロード・解析し、到着したデータをXMLリーダーに供給する。これにより、非常に大きなデータソースの読み込みをサポートします。データはネットワークからXMLリーダーを通してストリーミングされるため、XMLの全文をメモリに保持する必要はない。他のコンテキストでは、同様のアプローチにより、ユーザーがこのようなインクリメンタルな読み込みを中断することができる。

構築
RSSListing::RSSListing(const QString &url, QWidget *parent)
    : QWidget(parent), currentReply(0)
{
    connect(&manager, &QNetworkAccessManager::finished, this, &RSSListing::finished);

    lineEdit = new QLineEdit(this);
    lineEdit->setText(url);
    connect(lineEdit, &QLineEdit::returnPressed, this, &RSSListing::fetch);

    fetchButton = new QPushButton(tr("Fetch"), this);
    connect(fetchButton, &QPushButton::clicked, this, &RSSListing::fetch);

    treeWidget = new QTreeWidget(this);
    connect(treeWidget, &QTreeWidget::itemActivated,
            // Open the link in the browser:
            this, [](QTreeWidgetItem *item) { QDesktopServices::openUrl(QUrl(item->text(1))); });
    treeWidget->setHeaderLabels(QStringList { tr("Title"), tr("Link") });
    treeWidget->header()->setSectionResizeMode(QHeaderView::ResizeToContents);

    QHBoxLayout *hboxLayout = new QHBoxLayout;
    hboxLayout->addWidget(lineEdit);
    hboxLayout->addWidget(fetchButton);

    QVBoxLayout *layout = new QVBoxLayout(this);
    layout->addLayout(hboxLayout);
    layout->addWidget(treeWidget);

    setWindowTitle(tr("RSS listing example"));
    resize(640, 480);
}

コンストラクタは、ウィジェットのさまざまなコンポーネントをセットアップし、それらのシグナルを処理するために使用するスロットに接続する。

ユーザーインターフェースは、ラインエディット、プッシュボタン、リストビューウィジェットで構成される。ラインエディットはフェッチするURLを入力するために使われ、プッシュボタンは更新のフェッチプロセスを開始します。ラインエディットはデフォルトでは空ですが、今回のmain() のように、コンストラクタの呼び出し側でオーバーライドできます。いずれにせよ、ユーザーはデフォルトを別のRSSフィードのURLに置き換えることができます。

リストビューには、RSSフィードで更新された項目が表示されます。そのうちの1つをダブルクリックすると、QDesktopServices::openUrl ()を使って、ユーザーのブラウザやその他のユーザーエージェントにURLが送信されます。

スロット
void RSSListing::fetch()
{
    lineEdit->setReadOnly(true);
    fetchButton->setEnabled(false);
    treeWidget->clear();

    get(QUrl(lineEdit->text()));
}

void RSSListing::consumeData()
{
    int statusCode = currentReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
    if (statusCode >= 200 && statusCode < 300)
        parseXml();
}

void RSSListing::error(QNetworkReply::NetworkError)
{
    qWarning("error retrieving RSS feed");
    xml.clear();
    currentReply->disconnect(this);
    currentReply->deleteLater();
    currentReply = nullptr;
}

void RSSListing::finished(QNetworkReply *reply)
{
    Q_UNUSED(reply);
    lineEdit->setReadOnly(false);
    fetchButton->setEnabled(true);
}

すべてのスロットは、難しい処理をプライベートメソッドに委譲することでシンプルに保たれています。

ユーザが "Fetch "ボタンをクリックするか、ライン編集でリターンキーを押すかしてURLの入力を完了すると、fetch() スロットは "Fetch "ボタンを無効にし、ライン編集のさらなる編集を無効にします。利用可能な更新の表示を消去し、HTTP GETリクエストの開始をget()

データが受信されると、ネットワーク・リプライはreadyRead() シグナルをトリガーし、get()consumeData() スロットに接続する。このシグナルは、レスポンスのステータスコードが正常であるかどうかをチェックし、正常であれば、parseXml() を呼び出してデータを消費する。

ネットワーク・リプライにエラーが発生した場合、このエラーはerror() スロットに送られる。 スロットはエラーを報告し、XMLストリーム・リーダーをクリアしてから、リプライとの接続を切断して削除する。

ネットワーク・リプライが完了すると(成功したかどうかにかかわらず)、finished() スロットは UI を復元し、行の編集と "Fetch" ボタンを再度有効にして、新しい URL をフェッチできるようにします。

get()メソッド
void RSSListing::get(const QUrl &url)
{
    if (currentReply) {
        currentReply->disconnect(this);
        currentReply->deleteLater();
    }
    currentReply = url.isValid() ? manager.get(QNetworkRequest(url)) : nullptr;
    if (currentReply) {
        connect(currentReply, &QNetworkReply::readyRead, this, &RSSListing::consumeData);
        connect(currentReply, &QNetworkReply::errorOccurred, this, &RSSListing::error);

    }
    xml.setDevice(currentReply); // Equivalent to clear() if currentReply is null.
}

get() メソッドは、fetch() スロットが HTTP GET リクエストを開始するために使用します。最初にXMLストリーム・リーダーをクリアし、もしリプライが現在アクティブであれば、それを切断して削除します。渡されたURLが有効であれば、ネットワークアクセスマネージャにGETを要求する。ネットワーク・リプライ・オブジェクトはQIODevice でもあり、ここからデータを読み取ることができます。

parseXml() メソッド
void RSSListing::parseXml()
{
    while (!xml.atEnd()) {
        xml.readNext();
        if (xml.isStartElement()) {
            if (xml.name() == u"item") {
                linkString = xml.attributes().value("rss:about").toString();
                titleString.clear();
            }
            currentTag = xml.name().toString();
        } else if (xml.isEndElement()) {
            if (xml.name() == u"item") {

                QTreeWidgetItem *item = new QTreeWidgetItem;
                item->setText(0, titleString);
                item->setText(1, linkString);
                treeWidget->addTopLevelItem(item);
            }
        } else if (xml.isCharacters() && !xml.isWhitespace()) {
            if (currentTag == "title")
                titleString += xml.text();
            else if (currentTag == "link")
                linkString += xml.text();
        }
    }
    if (xml.error() && xml.error() != QXmlStreamReader::PrematureEndOfDocumentError)
        qWarning() << "XML ERROR:" << xml.lineNumber() << ": " << xml.errorString();
}

データが受信され、XML ストリーム・リーダーが使用できるようになると、parseXml() は XML ストリームから読み込み、item 要素と、その中にあるtitle 要素およびlink 要素をチェックします。itemrss:about 属性をツリービューの Link 列の URL として使用し、link 要素の内容が使用されない場合は、title 要素の内容をツリービューの Title 列に使用します。各item 要素が閉じると、その詳細がツリーウィジェットの新しい行になり、抽出されたタイトルと URL が Title 列と Link 列に表示されます。

linkString titleString currentTag RSSListing 新しいデータが到着すると、このメソッドが繰り返し呼び出され、受信したデータの 1 つのチャンクが、後のチャンクが到着するまで完了しない要素を開始することがあるからです。これにより、パーサーはすべてのデータが到着するまで待つ必要がなく、データが到着すると非同期で操作できるようになります。

プロジェクト例 @ code.qt.io

QNetworkReplyQXmlStreamReaderも参照して ください。

©2024 The Qt Company Ltd. 本書に含まれるドキュメントの著作権は、それぞれの所有者に帰属します。 本書で提供されるドキュメントは、Free Software Foundation が発行したGNU Free Documentation License version 1.3に基づいてライセンスされています。 Qtおよびそれぞれのロゴは、フィンランドおよびその他の国におけるThe Qt Company Ltd.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。