Qt Quick 演示 - RSS 新闻

一个 QML RSS 新闻阅读器,使用XmlListModelXmlListModelRole 自定义 QML 类型下载 XML 数据,使用ListModelListElement 创建分类列表,并使用ListView 显示数据。

RSS News演示了以下 Qt Quick功能:

  • 使用自定义 QML 类型。
  • 使用列表模型和列表元素表示数据。
  • 使用 XML 列表模型下载 XML 数据。
  • 使用列表视图显示数据。
  • 使用Component 类型为新闻条目列表视图创建页脚。
  • 使用Image 类型创建关闭应用程序的按钮。

运行示例

运行示例 Qt Creator,打开Welcome 模式,并从Examples 中选择示例。更多信息,请参阅Qt Creator: 教程:构建并运行

使用自定义类型

在 RSS 新闻应用程序中,我们使用了以下自定义类型,每个类型都在单独的 .qml 文件中定义:

  • BusyIndicator.qml
  • CategoryDelegate.qml
  • NewsDelegate.qml
  • RssFeeds.qml
  • ScrollBar.qml

创建主窗口

Main.qml 中,我们使用带有自定义属性的Rectangle 类型创建应用程序主窗口:

Rectangle {
    id: window

    width: 800
    height: 480

    property string currentFeed: rssFeeds.get(0).feed
    property bool loading: feedModel.status === XmlListModel.Loading
    property bool isPortrait: Screen.primaryOrientation === Qt.PortraitOrientation

稍后我们将使用自定义属性加载 XML 数据,并根据屏幕方向调整屏幕布局。

创建分类列表

Main.qml 中,我们使用在RssFeeds.qml 中指定的RssFeeds 自定义类型来创建 feed 类别列表:

    RssFeeds { id: rssFeeds }

RssFeeds.qml 中,我们使用ListModel 类型和ListElement 类型来创建类别列表,其中列表元素代表 feed 类别:

ListModel {
    ListElement { name: "Top Stories"; feed: "news.yahoo.com/rss/topstories"; image: "images/TopStories.jpg" }
    ListElement { name: "World"; feed: "news.yahoo.com/rss/world"; image: "images/World.jpg" }
    ListElement { name: "Europe"; feed: "news.yahoo.com/rss/europe"; image: "images/Europe.jpg" }
    ListElement { name: "Asia"; feed: "news.yahoo.com/rss/asia"; image: "images/Asia.jpg" }
    ListElement { name: "U.S. National"; feed: "news.yahoo.com/rss/us"; image: "images/USNational.jpg"  }
    ListElement { name: "Politics"; feed: "news.yahoo.com/rss/politics"; image: "images/Politics.jpg" }
    ListElement { name: "Business"; feed: "news.yahoo.com/rss/business"; image: "images/Business.jpg" }
    ListElement { name: "Technology"; feed: "news.yahoo.com/rss/tech"; image: "images/Technology.jpg" }
    ListElement { name: "Entertainment"; feed: "news.yahoo.com/rss/entertainment"; image: "images/Entertainment.jpg" }
    ListElement { name: "Health"; feed: "news.yahoo.com/rss/health"; image: "images/Health.jpg" }
    ListElement { name: "Science"; feed: "news.yahoo.com/rss/science"; image: "images/Science.jpg" }
    ListElement { name: "Sports"; feed: "news.yahoo.com/rss/sports"; image: "images/Sports.jpg" }
}

列表元素的定义与其他 QML 类型相似,除了它们包含角色定义集合,而不是属性。角色既定义数据的访问方式,也包括数据本身。

对于每个列表元素,我们使用name 角色来指定类别名称,使用feed 角色来指定加载数据的 URL,使用image 角色来显示类别的图片。

Main.qml 中,我们使用ListView 类型来显示类别列表:

    ListView {
        id: categories
        property int itemWidth: 190

        width: window.isPortrait ? parent.width : itemWidth
        height: window.isPortrait ? itemWidth : parent.height
        orientation: window.isPortrait ? ListView.Horizontal : ListView.Vertical
        anchors.top: parent.top
        model: rssFeeds
        delegate: CategoryDelegate {
            itemSize: categories.itemWidth
            isLoading: window.loading
            onClicked: function () {
                if (window.currentFeed == feed)
                    feedModel.reload()
                else
                    window.currentFeed = feed

            }
        }

在纵向方向上,我们使用orientation 属性在窗口顶部水平布局类别列表;在横向方向上,我们使用 属性在窗口左侧垂直布局类别列表。根据方向,我们将列表的宽度或高度绑定为一个固定值 (itemWidth)。

在两种方向上,我们都使用anchors.top 属性将列表视图定位在屏幕顶部。

我们使用model 属性从rssFeeds 模型加载 XML 数据,并使用CategoryDelegate 作为委托来实例化列表中的每个项目。

创建列表元素

CategoryDelegate.qml 中,我们使用带有自定义属性的Rectangle 类型来创建列表元素:

Rectangle {
    id: delegate

    required property real itemSize
    required property string name
    required property string feed
    required property string image
    required property int index
    required property bool isLoading

    property bool selected: ListView.isCurrentItem

我们使用required 声明我们期望从模型获得的属性。

我们将selected 属性设置为ListView.isCurrentItem 附加属性,以指定如果delegate 是当前项目,则selected 就是true

我们使用Image 类型source 属性来显示图片,图片位于委托中心,由rssFeeds 列表模型中的image 角色为列表元素指定:

    Image {
        anchors.centerIn: parent
        source: delegate.image
    }

我们使用Text 类型为列表元素添加标题:

    Text {
        id: titleText

        anchors {
            left: parent.left; leftMargin: 20
            right: parent.right; rightMargin: 20
            top: parent.top; topMargin: 20
        }

        font { pixelSize: 18; bold: true }
        text: delegate.name
        color: delegate.selected ? "#ffffff" : "#ebebdd"
        scale: delegate.selected ? 1.15 : 1.0
        Behavior on color { ColorAnimation { duration: 150 } }
        Behavior on scale { PropertyAnimation { duration: 300 } }

我们使用anchors 属性将标题置于列表元素的顶部,并留出 20 像素的边距。我们使用font 属性来调整字体大小和文本格式。

我们使用colorscale 属性来增亮文本,并在列表项为当前项时将其缩放得稍大一些。通过应用Behavior 属性,我们可以将选择和取消选择列表项的操作动画化。

当用户点击类别列表元素时,我们使用MouseArea 类型下载 XML 数据:

    MouseArea {
        anchors.fill: delegate
        onClicked: {
            delegate.ListView.view.currentIndex = delegate.index
            delegate.clicked()
        }
    }

anchors.fill 属性被设置为delegate ,以便用户可以点击列表元素中的任意位置。

为了在clicked 事件中加载类别列表的 XML 数据,我们设置了ListView 附加属性的currentIndex ,并在onClicked 中发出clicked signal() 信号。

下载 XML 数据

Main.qml 中,我们使用XmlListModel 类型作为ListView 元素的数据源,以显示所选类别中的新闻条目:

    XmlListModel {
        id: feedModel

        source: "https://" + window.currentFeed
        query: "/rss/channel/item"

我们使用source 属性和window.currentFeed 自定义属性来获取所选类别的新闻条目。

query 属性指定XmlListModel 为 XML 文档中的每个<item> 生成一个模型项。

我们使用XmlListModelRole 类型来指定模型项属性。每个模型项都有titlecontentlinkpubDate 属性,这些属性与 XML 文档中相应<item> 的值相匹配:

        XmlListModelRole { name: "title"; elementName: "title"; attributeName: ""}
        XmlListModelRole { name: "content"; elementName: "content"; attributeName: "url" }
        XmlListModelRole { name: "link"; elementName: "link"; attributeName: "" }
        XmlListModelRole { name: "pubDate"; elementName: "pubDate"; attributeName: "" }
    }

我们在ListView 类型中使用feedModel 模型来显示数据:

    ListView {
        id: list

        anchors.left: window.isPortrait ? window.left : categories.right
        anchors.right: closeButton.left
        anchors.top: window.isPortrait ? categories.bottom : window.top
        anchors.bottom: window.bottom
        anchors.leftMargin: 30
        anchors.rightMargin: 4
        clip: window.isPortrait
        model: feedModel
        footer: footerText
        delegate: NewsDelegate {}
    }

为了在纵向方向上将新闻条目列在类别列表的下方,在横向方向上将新闻条目列在类别列表的右侧,我们使用isPortrait 自定义属性将新闻条目列表的顶部锚定在纵向方向上window 的左侧和categories 的底部,以及横向方向上categories 的右侧和window 的底部。

在两种方向上,我们都使用anchors.bottom 属性将列表视图的底部锚定到窗口底部。

在纵向方向上,我们将新闻条目的画图剪切到列表视图的边界矩形上,以避免新闻条目滚动到其他条目上时出现图形假象。而在横向模式下,由于列表在垂直方向上横跨整个屏幕,因此不需要这样做。

我们使用model 属性从feedModel 模型加载 XML 数据,并使用NewsDelegate 作为委托来实例化列表中的每个项目。

NewsDelegate.qml 中,我们使用Column 类型来布局 XML 数据:

Column {
    id: delegate

    required property string title
    required property string content
    required property string link
    required property string pubDate

    width: delegate.ListView.view.width
    spacing: 8

在列中,我们使用和另一列来定位图片和标题文本:

    Row {
        width: parent.width
        spacing: 8

        Column {
            Item {
                width: 4
                height: titleText.font.pixelSize / 4
            }

            Image {
                id: titleImage
                source: delegate.content
                width: Math.min(delegate.width / 2, sourceSize.width)
                fillMode: Image.PreserveAspectFit
            }
        }

        Text {
            id: titleText

            text: delegate.title.replace(/&#39;/g, "'")
            width: delegate.width - titleImage.width
            wrapMode: Text.WordWrap
            font.pixelSize: 26
            font.bold: true
        }
    }

我们使用timeSinceEvent() JavaScript 函数生成项目发布时间的文本表示:

    Text {
        width: delegate.width
        font.pixelSize: 12
        textFormat: Text.RichText
        font.italic: true
        text: delegate.timeSinceEvent(delegate.pubDate) + " (<a href=\"" + delegate.link + "\">Link</a>)"
        onLinkActivated: function(link) {
            Qt.openUrlExternally(link)
        }
    }

当用户选择链接时,我们使用onLinkActivated 信号处理器在外部浏览器中打开 URL。

向用户提供反馈

CategoryDelegate.qml 中,我们使用BusyIndicator 自定义类型来表示加载 XML 数据时的活动:

    BusyIndicator {
        scale: 0.8
        visible: delegate.ListView.isCurrentItem && delegate.isLoading
        anchors.centerIn: parent
    }

我们使用scale 属性将指示器大小减小到0.8 。我们将visible 属性与delegate 列表视图的isCurrentItem 附加属性和主窗口的loading 属性绑定,以便在当前项目为分类列表项目且正在加载 XML 数据时显示指示器图像。

我们在BusyIndicator.qml 中定义了BusyIndicator 类型。我们使用Image 类型来显示图像,并对其rotation 属性应用NumberAnimation 来无限循环旋转图像:

Image {
    id: container

    source: "images/busy.png";

    NumberAnimation on rotation {
        running: container.visible
        from: 0; to: 360;
        loops: Animation.Infinite;
        duration: 1200
    }
}

在应用程序中,您也可以使用BusyIndicator 类型的 Qt Quick Controls模块中的类型。

创建滚动条

Main.qml 中,我们使用自己定制的ScrollBar 类型在类别和新闻条目列表视图中创建滚动条。在您的应用程序中,您也可以使用该模块中的ScrollView 类型。 Qt Quick Controls模块中的类型。

首先,我们在类别列表视图中创建一个滚动条。我们将orientation 属性绑定到isPortrait 属性和Qt::Orientation 枚举类型的Horizontal 值,以纵向显示水平滚动条,并将Vertical 值绑定到横向显示垂直滚动条:

    ScrollBar {
        id: listScrollBar

        orientation: window.isPortrait ? Qt.Horizontal : Qt.Vertical
        height: window.isPortrait ? 8 : categories.height;
        width: window.isPortrait ? categories.width : 8
        scrollArea: categories;
        anchors.right: categories.right
    }

categories 列表视图一样,我们根据isPortrait 属性调整滚动条的宽度和高度。

我们使用scrollArea 属性在categories 列表视图中显示滚动条。

我们使用anchors.right 属性将滚动条锚定到类别列表的右侧。

    ScrollBar {
        scrollArea: list
        width: 8
        anchors.right: window.right
        anchors.top: window.isPortrait ? categories.bottom : window.top
        anchors.bottom: window.bottom
    }

其次,我们在新闻条目列表视图中创建另一个滚动条。我们希望无论屏幕方向如何,垂直滚动条都显示在视图的右侧,因此我们可以将width 属性设置为8 ,并将anchors.right 属性绑定到window.right 属性。我们使用anchors.top 属性将滚动条锚定到纵向类别列表的顶部和横向新闻条目列表的顶部。我们使用anchors.bottom 属性将滚动条底部锚定到两种方向的列表视图底部。

我们在ScrollBar.qml 中定义了ScrollBar 类型。我们使用带有自定义属性的Item 类型为滚动条创建一个容器:

Item {
    id: container

    property variant scrollArea
    property int orientation: Qt.Vertical

    opacity: 0

我们使用BorderImage 类型在通过position() 函数计算出的 x 和 y 位置显示滚动条缩略图:

    BorderImage {
        source: "images/scrollbar.png"
        border { left: 1; right: 1; top: 1; bottom: 1 }
        x: container.orientation == Qt.Vertical ? 2 : container.position()
        y: container.orientation == Qt.Vertical ? container.position() : 2
        width: container.orientation == Qt.Vertical ? container.width - 4 : container.size()
        height: container.orientation == Qt.Vertical ? container.size() : container.height - 4
    }

我们使用size 函数根据屏幕方向计算拇指的宽度和高度。

我们使用states 在用户移动滚动区域时显示滚动条:

    states: State {
        name: "visible"
        when: container.orientation == Qt.Vertical ?
                  container.scrollArea.movingVertically :
                  container.scrollArea.movingHorizontally
        PropertyChanges { container { opacity: 1.0 } }
    }

当状态从"visible" 变为默认状态时,我们使用transitionsNumberAnimation 应用到opacity 属性:

    transitions: Transition {
        from: "visible"; to: ""
        NumberAnimation { properties: "opacity"; duration: 600 }
    }
}

创建页脚

Main.qml 中,我们使用Component 类型和Rectangle 类型为新闻列表视图创建页脚:

pragma ComponentBehavior: Bound

import QtQuick
import QtQuick.Window
import QtQml.XmlListModel

Rectangle {
    id: window

    width: 800
    height: 480

    property string currentFeed: rssFeeds.get(0).feed
    property bool loading: feedModel.status === XmlListModel.Loading
    property bool isPortrait: Screen.primaryOrientation === Qt.PortraitOrientation

    onLoadingChanged: {
        if (feedModel.status == XmlListModel.Ready)
            list.positionViewAtBeginning()
    }

    RssFeeds { id: rssFeeds }

    XmlListModel {
        id: feedModel

        source: "https://" + window.currentFeed
        query: "/rss/channel/item"

        XmlListModelRole { name: "title"; elementName: "title"; attributeName: ""}

我们将页脚的width 与组件的宽度绑定,将height 与关闭按钮的高度绑定,以便在不显示新闻条目时对齐。

创建按钮

Main.qml 中,我们使用Image 类型创建一个简单的按压按钮,用户可以点击该按钮关闭应用程序:

        XmlListModelRole { name: "content"; elementName: "content"; attributeName: "url" }
        XmlListModelRole { name: "link"; elementName: "link"; attributeName: "" }
        XmlListModelRole { name: "pubDate"; elementName: "pubDate"; attributeName: "" }
    }

    ListView {
        id: categories
        property int itemWidth: 190

        width: window.isPortrait ? parent.width : itemWidth
        height: window.isPortrait ? itemWidth : parent.height
        orientation: window.isPortrait ? ListView.Horizontal : ListView.Vertical
        anchors.top: parent.top
        model: rssFeeds
        delegate: CategoryDelegate {
            itemSize: categories.itemWidth
            isLoading: window.loading
            onClicked: function () {
                if (window.currentFeed == feed)
                    feedModel.reload()
                else
                    window.currentFeed = feed

            }
        }
        spacing: 3
    }

    ScrollBar {
        id: listScrollBar

        orientation: window.isPortrait ? Qt.Horizontal : Qt.Vertical
        height: window.isPortrait ? 8 : categories.height;
        width: window.isPortrait ? categories.width : 8
        scrollArea: categories;
        anchors.right: categories.right
    }

    ListView {
        id: list

        anchors.left: window.isPortrait ? window.left : categories.right
        anchors.right: closeButton.left
        anchors.top: window.isPortrait ? categories.bottom : window.top
        anchors.bottom: window.bottom
        anchors.leftMargin: 30
        anchors.rightMargin: 4
        clip: window.isPortrait
        model: feedModel
        footer: footerText
        delegate: NewsDelegate {}
    }

    ScrollBar {
        scrollArea: list
        width: 8
        anchors.right: window.right
        anchors.top: window.isPortrait ? categories.bottom : window.top
        anchors.bottom: window.bottom
    }

    Component {
        id: footerText

        Rectangle {
            width: parent.width
            height: closeButton.height
            color: "lightgray"

            Text {
                text: "RSS Feed from Yahoo News"
                anchors.centerIn: parent
                font.pixelSize: 14
            }
        }
    }

    Image {
        id: closeButton
        source: "content/images/btn_close.png"
        scale: 0.8
        anchors.top: parent.top
        anchors.right: parent.right
        anchors.margins: 4
        opacity: (window.isPortrait && categories.moving) ? 0.2 : 1.0
        Behavior on opacity {
            NumberAnimation { duration: 300; easing.type: Easing.OutSine }
        }

        MouseArea {
            anchors.fill: parent
            onClicked: {
                Qt.quit()
            }
        }
    }

我们使用anchors 将关闭按钮定位在新闻列表视图的右上角,边距为 4 像素。由于关闭按钮在纵向方向上与类别列表重叠,因此我们对opacity 属性进行了动画处理,使按钮在用户滚动类别列表时几乎完全透明。

我们在MouseArea 中使用onClicked 信号处理器,在用户选择关闭按钮时调用quit() 函数。

示例项目 @ code.qt.io

另请参阅 QML 应用程序

© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.