Qt Quick 演示 - RSS 新闻

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

RSS 新闻 展示了以下 Qt Quick 功能

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

运行示例

要从 Qt Creator 运行示例,打开 欢迎 模式并从 示例 中选择示例。有关更多信息,请访问 构建和运行示例

使用自定义类型

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

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

要使用自定义类型,我们在主 QML 文件 rssnews.qml 中添加一个 import 语句,导入名为 content 的文件夹,其中包含这些类型

import "./content"

创建主窗口

rssnews.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 数据和根据屏幕方向调整屏幕布局。

创建类别列表

rssnews.qml 中,我们使用在 RssFeeds.qml 中指定的 RssFeeds 自定义类型来创建一个内容类别列表

    RssFeeds { id: rssFeeds }

RssFeeds.qml 中,我们使用 ListModel 类型和一个 ListElement 类型来创建一个类别列表,列表元素代表内容类别

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 类型类似,只是它们包含一系列 role 定义而不是属性。角色既定义了数据如何被访问,也包含了数据本身。

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

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

    ListView {
        id: categories
        property int itemWidth: 190

        width: isPortrait ? parent.width : itemWidth
        height: isPortrait ? itemWidth : parent.height
        orientation: isPortrait ? ListView.Horizontal : ListView.Vertical
        anchors.top: parent.top
        model: rssFeeds
        delegate: CategoryDelegate { itemSize: categories.itemWidth }
        spacing: 3
    }

要将类别列表水平布置在纵向窗口的顶部和横向模式下的左侧,我们使用 orientation 属性。根据方向,我们将列表的宽度或高度绑定到固定值(itemWidth)。

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

我们使用 model 属性从 rssFeeds 模型中加载 XML 数据,并将 CategoryDelegate 作为代理来创建列表中的每个项目。

创建列表元素

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

Rectangle {
    id: delegate

    property bool selected: ListView.isCurrentItem

我们将 selected 属性设置为 ListView.isCurrentItem 动态属性,以指定如果 delegate 是当前项,则 selectedtrue

我们使用 Image 类型的 source 属性显示图像,在代理中居中,由 rssFeeds 列表模型中 image 角色指定的列表元素指定

    Image {
        anchors.centerIn: parent
        source: 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: name
        color: selected ? "#ffffff" : "#ebebdd"
        scale: 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 = index
            if (window.currentFeed == feed)
                feedModel.reload()
            else
                window.currentFeed = feed
        }
    }

anchors.fill 属性设置为 delegate 以使用户能够在列表元素内点击任何地方。

我们使用 onClicked 信号处理程序加载类别列表的 XML 数据。如果已点击的类别是当前项目,将调用 reload() 函数重新加载数据。

下载 XML 数据

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

    XmlListModel {
        id: feedModel

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

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

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

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

        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: "" }
    }

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

    ListView {
        id: list

        anchors.left: isPortrait ? window.left : categories.right
        anchors.right: closeButton.left
        anchors.top: isPortrait ? categories.bottom : window.top
        anchors.bottom: window.bottom
        anchors.leftMargin: 30
        anchors.rightMargin: 4
        clip: 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
    width: delegate.ListView.view.width
    spacing: 8

在列内部,我们使用Row和其他列来定位图片和标题文字

    Row {
        width: parent.width
        spacing: 8

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

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

        Text {
            id: titleText

            text: 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: timeSinceEvent(pubDate) + " (<a href=\"" + link + "\">Link</a>)"
        onLinkActivated: function(link) {
            Qt.openUrlExternally(link)
        }
    }

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

向用户提供反馈

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

    BusyIndicator {
        scale: 0.8
        visible: delegate.ListView.isCurrentItem && window.loading
        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
    }
}

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

创建滚动条

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

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

    ScrollBar {
        id: listScrollBar

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

与分类列表视图类似,我们根据isPortrait属性调整滚动条的宽度和高度。

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

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

    ScrollBar {
        scrollArea: list
        width: 8
        anchors.right: window.right
        anchors.top: 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()函数计算得到

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

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

我们使用states来在用户移动滚动区域时使滚动条可见

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

我们使用transitions在状态从"visible"变为默认状态时应用到opacity属性的NumberAnimation

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

创建页脚

rssnews.qml中,我们使用带有Rectangle类型的Component类型来为新闻列表视图创建页脚

    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
            }
        }
    }

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

创建按钮

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

    Image {
        id: closeButton
        source: "content/images/btn_close.png"
        scale: 0.8
        anchors.top: parent.top
        anchors.right: parent.right
        anchors.margins: 4
        opacity: (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应用程序

© 2024 The Qt Company Ltd. 此处包含的文档贡献的版权归各自的拥有者。本提供的文档是根据由自由软件基金会发布的GNU自由文档许可证版本1.3的条款进行许可的。Qt及其相关标志是The Qt Company Ltd.在芬兰及/或全世界范围的商标。所有其他商标均为其各自所有者的财产。